CreateDvp registra los términos de una operación y crea las dos cuentas de escrow que
las partes financiarán. No mueve ningún token. Cualquiera puede enviarlo, por lo que el registro puede
provenir de una contraparte, del operador de la autoridad de liquidación o de un tercero,
y cada uno de los demás participantes verifica los términos almacenados antes de actuar en función de ellos (consulta
financiar los tramos).
Los fragmentos de código usan directamente el cliente generado, con direcciones de ejemplo, para que se vean las cuentas y los firmantes implicados. Apuntan a devnet, donde el programa está desplegado en la misma dirección que en mainnet-beta.
Instalación
A fecha de 2 de octubre de 2026, no se ha publicado ningún paquete para los clientes de DvP. Ambos se generan a partir del IDL del programa en el repositorio y se usan desde el código fuente.
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
Estas páginas están escritas para el commit df9919e, con @solana/kit 8 en
TypeScript y los crates solana-* 2.x en Rust.
Los fragmentos de cada página de paso se mantienen entre páginas: cada página usa el cliente, los firmantes
y las direcciones definidos en las páginas anteriores. payerSigner, userASigner,
userBSigner y authoritySigner son objetos TransactionSigner para el pagador de rent,
las dos partes y la autoridad de liquidación, por ejemplo obtenidos con
createKeyPairSignerFromBytes. Los fragmentos de Rust construyen instrucciones y dejan
la firma y el envío a cargo de quien las llama.
Derivar las direcciones
El registro de la operación es un program-derived address calculado a partir de la autoridad de liquidación, las dos partes, las dos mints y un nonce. Cada escrow es la associated token account del registro para una de las mints. Ninguna de las dos derivaciones necesita acceso a la red.
Usa un nonce aleatorio de 64 bits para cada operación. Como la creación no requiere permisos, un
nonce predecible permite que un tercero ocupe la dirección que iba a
usar una operación. Pasa el nonce y los importes como bigint en TypeScript; el cliente
generado rechaza un number.
Sustituye cada marcador ..._HERE por una dirección real antes de ejecutar.
address() valida base58 y lanza un error con un marcador, y pubkey! no
compila.
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,});
Registrar los términos
CreateDvp recibe los dos importes en unidades base, una fecha de expiración, el nonce y cuatro
campos opcionales: una referencia opaca de hasta 64 bytes, un destino de liquidación
para cada parte y una hora mínima de liquidación (consulta Notas). El destino es
donde llegan los fondos de esa parte en la liquidación, por ejemplo la wallet de depósito de un custodio;
por defecto es la propia dirección de la parte y queda fijado una vez que existe el
registro. Los reembolsos y las recuperaciones siempre van a la token account propia de la parte
para la mint de ese tramo, nunca a un destino, sea quien sea quien haya financiado el escrow.
El pagador financia el rent del registro, de la marca de nonce usado (tombstone) y de ambos escrows.
Al cerrar, ese rent va a quien firme la instrucción de cierre, no de vuelta
al pagador. La tombstone nunca se cierra, por lo que cada operación deja un pequeño saldo
permanente. Al presupuestar los SOL del pagador, calcula los importes de rent con el
método RPC getMinimumBalanceForRentExemption en lugar de codificarlos a mano.
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);
El fragmento de TypeScript necesita tener disponibles getProgramDerivedAddress y getAddressEncoder
de @solana/kit, y DVP_SWAP_PROGRAM_PROGRAM_ADDRESS del cliente. El builder de Rust deja el system program y el associated token program
con sus valores predeterminados.
Notas
- Expiración. Solo Settle comprueba la expiración. Reclaim, Cancel y Reject funcionan después de ella, y un depósito que llegue tras la expiración puede ser recuperado por la parte de ese tramo. La expiración puede ser como máximo un año posterior a la creación.
- La hora es la hora de la red. El programa lee el reloj onchain, que puede diferir de la hora real, a veces en decenas de segundos, y puede saltar hacia delante. Deja un margen entre la expiración y la hora de liquidación prevista, evita financiar o liquidar cerca de cualquiera de los dos límites y no confíes en la expiración para imponer un plazo al segundo. El README del programa sugiere presupuestar aproximadamente un minuto de desfase.
- Liquidación mínima. Opcional. Si se define, Settle se rechaza antes de esa hora, y no debe ser posterior a la expiración.
- Comprobaciones al crear. Create falla con
PartyNotSignerCapablesi una parte no es una cuenta propiedad del sistema (esto excluye un multisig de SPL Token),SettlementDestinationIsSwapDvp,SelfDvp,SameMint,ZeroAmount,SettlementAuthorityIsPartyoExpiryNotInFuture. La lista completa está en la tabla de errores.
Siguientes pasos
- Financiar los tramos: cada parte transfiere su tramo al escrow de la operación
Is this page helpful?