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.gitcd dvp && git checkout df9919ed02c25a93620e7f6820107c9050ce3b92pnpm install && pnpm generate-clients && pnpm build# The client is clients/typescript/src; copy or link it into your project as ./dvppnpm 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 assetamountB: 998_500_000n, // 998.50 units of a 6-decimal cash tokenexpiryTimestamp: nowSeconds + 60n * 60n * 24n, // one daynonce,refString: "ORDER-2026-10-02-0001",userASettlementDestination: null, // defaults to userAuserBSettlementDestination: null, // defaults to userBearliestSettlementTimestamp: 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
PartyNotSignerCapablesi une partie n'est pas un compte appartenant au System Program (ce qui exclut un multisig SPL Token),SettlementDestinationIsSwapDvp,SelfDvp,SameMint,ZeroAmount,SettlementAuthorityIsPartyouExpiryNotInFuture. 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?