Créer une transaction

CreateDvp enregistre les conditions d'une transaction 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 financer les jambes).

Les extraits utilisent directement le client généré, avec des adresses fictives, afin que les comptes et les signataires concerné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 des instructions et laissent la signature et l'envoi à l'appelant.

Dériver les adresses

L'enregistrement de la transaction 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 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 transaction. La création étant sans permission, un nonce prévisible permet à un tiers d'occuper l'adresse qu'une transaction devait utiliser. Passez le nonce et les montants en bigint en TypeScript ; le client généré rejette un number.

Remplacez chaque placeholder ..._HERE par une véritable adresse avant l'exécution. address() valide le base58 et lève une erreur sur un placeholder, et pubkey! ne compile pas.

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 d'au plus 64 octets, une destination de règlement pour chaque partie et une heure de règlement au plus tôt (voir Remarques). La destination est l'endroit où atterrit le produit de cette partie au règlement, par exemple le portefeuille de dépôt d'un conservateur ; elle correspond par défaut à l'adresse de la partie et est figée une fois que l'enregistrement existe. Les remboursements et récupérations vont toujours vers le token account de la partie pour le mint de cette jambe, jamais vers une destination, quel que soit celui qui a alimenté le séquestre.

Le payeur finance le rent de l'enregistrement, du tombstone du nonce et des deux séquestres. À la fermeture, ce rent revient à celui qui signe l'instruction de fermeture, et non au payeur. Le tombstone n'est jamais fermé : chaque transaction 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 être 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 peut faire un bond 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 celle-ci 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

Is this page helpful?

Table des matières

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