Créer un échange

CreateDvp enregistre les conditions d'un échange et crée les deux comptes de séquestre que les parties alimenteront. Il ne déplace aucun token. N'importe qui peut l'envoyer, l'enregistrement peut donc provenir d'une contrepartie, de l'opérateur de l'autorité de règlement ou d'un tiers, et chaque autre participant vérifie les conditions stockées avant d'agir en conséquence (voir alimenter les jambes).

Les extraits utilisent directement le client généré, avec des adresses fictives, afin que les comptes et les signataires impliqués soient visibles. Ils ciblent devnet, où le programme est déployé à la même adresse que sur mainnet-beta.

Installation

Aucun package n'est publié pour les clients DvP au 2 octobre 2026. Les deux sont générés à partir de l'IDL du programme dans le dépôt et utilisés depuis les sources.

git clone https://github.com/solana-foundation/dvp.git
cd dvp && git checkout df9919ed02c25a93620e7f6820107c9050ce3b92
pnpm install && pnpm generate-clients && pnpm build
# The client is clients/typescript/src; copy or link it into your project as ./dvp
pnpm add @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token-2022 @solana-program/memo

Ces pages sont rédigées pour le commit df9919e, avec @solana/kit 8 en TypeScript et les crates solana-* 2.x en Rust.

Les extraits de chaque page d'étape se complètent : chaque page utilise le client, les signataires et les adresses définis dans les pages précédentes. payerSigner, userASigner, userBSigner et authoritySigner sont des TransactionSigner pour le payeur du rent, les deux parties et l'autorité de règlement, par exemple issus de createKeyPairSignerFromBytes. Les extraits Rust construisent les instructions et laissent la signature et l'envoi à l'appelant.

Dériver les adresses

L'enregistrement de l'échange est une Program Derived Address calculée à partir de l'autorité de règlement, des deux parties, des deux mints et d'un nonce. Chaque compte de séquestre est l'associated token account de l'enregistrement pour l'un des mints. Aucune des deux dérivations ne nécessite d'accès réseau.

Utilisez un nonce aléatoire de 64 bits pour chaque échange. La création étant sans permission, un nonce prévisible permet à un tiers d'occuper l'adresse qu'un échange devait utiliser. Passez le nonce et les montants en bigint en TypeScript ; le client généré rejette un number.

Remplacez chaque espace réservé ..._HERE par une véritable adresse avant l'exécution. address() valide le base58 et lève une exception sur un espace réservé, et pubkey! échoue à la compilation.

import { address, createClient } from "@solana/kit";
import { solanaRpc } from "@solana/kit-plugin-rpc";
import { signer } from "@solana/kit-plugin-signer";
import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022";
import { findSwapDvpEscrowAta, findSwapDvpPda } from "./dvp";
const client = createClient()
.use(signer(payerSigner))
.use(
solanaRpc({
rpcUrl: "https://api.devnet.solana.com",
rpcSubscriptionsUrl: "wss://api.devnet.solana.com",
}),
);
const settlementAuthority = address("SETTLEMENT_AUTHORITY_ADDRESS_HERE");
const userA = address("ASSET_LEG_PARTY_ADDRESS_HERE");
const userB = address("CASH_LEG_PARTY_ADDRESS_HERE");
const mintA = address("ASSET_MINT_ADDRESS_HERE");
const mintB = address("CASH_MINT_ADDRESS_HERE");
const tokenProgramA = TOKEN_2022_PROGRAM_ADDRESS;
const tokenProgramB = TOKEN_2022_PROGRAM_ADDRESS;
// A fresh random nonce per trade, as a bigint.
const nonce = new DataView(
crypto.getRandomValues(new Uint8Array(8)).buffer,
).getBigUint64(0);
const [swapDvp] = await findSwapDvpPda({
settlementAuthority,
userA,
userB,
mintA,
mintB,
nonce,
});
const [escrowA] = await findSwapDvpEscrowAta({
swapDvp,
mint: mintA,
tokenProgram: tokenProgramA,
});
const [escrowB] = await findSwapDvpEscrowAta({
swapDvp,
mint: mintB,
tokenProgram: tokenProgramB,
});

Enregistrer les conditions

CreateDvp prend les deux montants en unités de base, une expiration, le nonce et quatre champs facultatifs : une référence opaque de 64 octets maximum, une destination de règlement pour chaque côté et une heure de règlement au plus tôt (voir Remarques). La destination est l'endroit où les produits de cette partie arrivent lors du règlement, par exemple le portefeuille de dépôt d'un dépositaire ; elle correspond par défaut à l'adresse de la partie et est figée une fois que l'enregistrement existe. Les remboursements et les récupérations vont toujours vers le token account propre à la partie pour le mint de cette jambe, jamais vers une destination, quel que soit le financeur du compte de séquestre.

Le payeur finance le rent de l'enregistrement, de la pierre tombale du nonce et des deux comptes de séquestre. À la fermeture, ce rent revient à la personne qui signe l'instruction de fermeture, et non au payeur. La pierre tombale n'est jamais fermée ; chaque échange laisse donc un petit solde permanent. Pour budgétiser les SOL du payeur, calculez les montants de rent avec la méthode RPC getMinimumBalanceForRentExemption plutôt que de les coder en dur.

import { getCreateDvpInstruction } from "./dvp";
const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
const createInstruction = getCreateDvpInstruction({
payer: payerSigner,
swapDvp,
nonceTombstone: (
await getProgramDerivedAddress({
programAddress: DVP_SWAP_PROGRAM_PROGRAM_ADDRESS,
seeds: ["nonce", getAddressEncoder().encode(swapDvp)],
})
)[0],
settlementAuthority,
userA,
userB,
mintA,
mintB,
dvpAtaA: escrowA,
dvpAtaB: escrowB,
tokenProgramA,
tokenProgramB,
amountA: 1_000_000_000n, // 1,000 units of a 6-decimal asset
amountB: 998_500_000n, // 998.50 units of a 6-decimal cash token
expiryTimestamp: nowSeconds + 60n * 60n * 24n, // one day
nonce,
refString: "ORDER-2026-10-02-0001",
userASettlementDestination: null, // defaults to userA
userBSettlementDestination: null, // defaults to userB
earliestSettlementTimestamp: null,
});
const { context } = await client.sendTransaction([createInstruction]);
console.log("Trade record:", swapDvp, "signature:", context.signature);

L'extrait TypeScript nécessite que getProgramDerivedAddress et getAddressEncoder de @solana/kit, ainsi que DVP_SWAP_PROGRAM_PROGRAM_ADDRESS du client, soient dans la portée. Le builder Rust laisse le System Program et l'Associated Token Program à leurs valeurs par défaut.

Remarques

  • Expiration. Seul Settle vérifie l'expiration. Reclaim, Cancel et Reject fonctionnent après celle-ci, et un dépôt arrivé après l'expiration peut être récupéré par la partie de la jambe concernée. L'expiration peut se situer au plus un an après la création.
  • Le temps est celui du réseau. Le programme lit l'horloge onchain, qui peut différer de l'heure réelle, parfois de plusieurs dizaines de secondes, et qui peut faire un saut en avant. Laissez une marge entre l'expiration et l'heure de règlement prévue, évitez d'alimenter ou de régler à proximité de l'une ou l'autre limite, et ne comptez pas sur l'expiration pour imposer une échéance à la seconde près. Le README du programme suggère de prévoir environ une minute de dérive.
  • Règlement au plus tôt. Facultatif. S'il est défini, Settle est rejeté avant cette heure, et elle ne doit pas être postérieure à l'expiration.
  • Vérifications à la création. Create échoue avec PartyNotSignerCapable si une partie n'est pas un compte appartenant au System Program (ce qui exclut un multisig SPL Token), SettlementDestinationIsSwapDvp, SelfDvp, SameMint, ZeroAmount, SettlementAuthorityIsParty ou ExpiryNotInFuture. La liste complète figure dans le tableau des erreurs.

Étapes suivantes

  • Alimenter les jambes : chaque partie transfère sa jambe dans le compte de séquestre de l'échange

Is this page helpful?

Table des matières

Modifier la page
© 2026 Fondation Solana. Tous droits réservés.