Crear una operación

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.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

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

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 PartyNotSignerCapable si una parte no es una cuenta propiedad del sistema (esto excluye un multisig de SPL Token), SettlementDestinationIsSwapDvp, SelfDvp, SameMint, ZeroAmount, SettlementAuthorityIsParty o ExpiryNotInFuture. La lista completa está en la tabla de errores.

Siguientes pasos

Is this page helpful?

Tabla de Contenidos

Editar Página