Create a Trade

CreateDvp records the terms of a trade and creates the two escrow accounts the parties will fund. It moves no tokens. Anyone can send it, so the record may come from a counterparty, the settlement authority's operator, or a third party, and every other participant verifies the stored terms before acting on them (see fund the legs).

The snippets use the generated client directly, with placeholder addresses, so the accounts and signers involved are visible. They target devnet, where the program is deployed at the same address as mainnet-beta.

Install

No package is published for the DvP clients as of 2 October 2026. Both are generated from the program's IDL in the repository and used from source.

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

These pages are written against commit df9919e, with @solana/kit 8 in TypeScript and the solana-* 2.x crates in Rust.

The snippets on every step page carry over: each page uses the client, signers, and addresses defined on earlier pages. payerSigner, userASigner, userBSigner, and authoritySigner are TransactionSigners for the rent payer, the two parties, and the settlement authority, for example from createKeyPairSignerFromBytes. The Rust snippets build instructions and leave signing and sending to the caller.

Derive the addresses

The trade record is a program-derived address over the settlement authority, the two parties, the two mints, and a nonce. Each escrow is the record's associated token account for one of the mints. Both derivations need no network access.

Use a random 64-bit nonce for every trade. Because creation is permissionless, a predictable nonce lets a third party occupy the address a trade was going to use. Pass the nonce and the amounts as bigint in TypeScript; the generated client rejects a number.

Replace each ..._HERE placeholder with a real address before running. address() validates base58 and throws on a placeholder, and pubkey! fails to compile.

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,
});

Record the terms

CreateDvp takes the two amounts in base units, an expiry, the nonce, and four optional fields: an opaque reference of up to 64 bytes, a settlement destination for each side, and an earliest settlement time (see Notes). The destination is where that party's proceeds land at settlement, for example a custodian's deposit wallet; it defaults to the party's own address and is fixed once the record exists. Refunds and reclaims always go to the party's own token account for that leg's mint, never to a destination, whoever funded the escrow.

The payer funds the rent for the record, the nonce tombstone, and both escrows. On close, that rent goes to whoever signs the closing instruction, not back to the payer. The tombstone is never closed, so each trade leaves a small permanent balance. When budgeting the payer's SOL, derive the rent amounts with the getMinimumBalanceForRentExemption RPC method rather than hardcoding them.

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);

The TypeScript snippet needs getProgramDerivedAddress and getAddressEncoder from @solana/kit, and DVP_SWAP_PROGRAM_PROGRAM_ADDRESS from the client, in scope. The Rust builder leaves the system program and associated token program at their defaults.

Notes

  • Expiry. Only Settle checks the expiry. Reclaim, Cancel, and Reject work after it, and a deposit that lands after expiry is recoverable by the leg's party. The expiry can be at most one year after creation.
  • Time is network time. The program reads the onchain clock, which can differ from wall-clock time, at times by tens of seconds, and can jump forward. Leave a margin between the expiry and the intended settlement time, avoid funding or settling near either boundary, and do not rely on the expiry to enforce a deadline to the second. The program README suggests budgeting about a minute of drift.
  • Earliest settlement. Optional. If set, Settle is rejected before it, and it must not be later than the expiry.
  • Create-time checks. Create fails with PartyNotSignerCapable if a party is not a system-owned account (this rules out an SPL Token multisig), SettlementDestinationIsSwapDvp, SelfDvp, SameMint, ZeroAmount, SettlementAuthorityIsParty, or ExpiryNotInFuture. The full list is in the errors table.

Next steps

  • Fund the legs: each party transfers its leg into the trade's escrow

Is this page helpful?

Mục lục

Chỉnh sửa trang