Создание сделки

CreateDvp записывает условия сделки и создаёт два эскроу-аккаунта, которые стороны будут пополнять. Токены он не перемещает. Отправить его может любой, поэтому запись может поступить от контрагента, оператора расчётного органа или третьего лица, а каждый другой участник проверяет сохранённые условия, прежде чем действовать на их основе (см. внесение активов в эскроу).

В примерах сгенерированный клиент используется напрямую, с адресами-заполнителями, чтобы были видны задействованные аккаунты и подписанты. Они рассчитаны на devnet, где программа развёрнута по тому же адресу, что и в mainnet-beta.

Установка

По состоянию на 2 октября 2026 года для клиентов DvP не опубликовано ни одного пакета. Оба клиента генерируются из IDL программы в репозитории и используются из исходного кода.

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

Эти страницы написаны для коммита df9919e с @solana/kit 8 в TypeScript и крейтами solana-* 2.x в Rust.

Фрагменты кода на каждой странице шагов продолжают друг друга: каждая страница использует клиент, подписантов и адреса, определённые на предыдущих страницах. payerSigner, userASigner, userBSigner и authoritySigner — это TransactionSigner для плательщика rent, двух сторон и расчётного органа соответственно, например полученные из createKeyPairSignerFromBytes. Фрагменты на Rust собирают инструкции, а подписание и отправку оставляют вызывающей стороне.

Вычисление адресов

Запись о сделке — это program-derived address, вычисляемый из адреса расчётного органа, двух сторон, двух минтов и нонса. Каждый эскроу — это associated token account записи для одного из минтов. Оба вычисления не требуют доступа к сети.

Используйте для каждой сделки случайный 64-битный нонс. Поскольку создание не требует разрешений, предсказуемый нонс позволяет третьей стороне занять адрес, который собиралась использовать сделка. В TypeScript передавайте нонс и суммы как bigint; сгенерированный клиент отклоняет number.

Перед запуском замените каждый заполнитель ..._HERE реальным адресом. address() проверяет base58 и выбрасывает ошибку при встрече заполнителя, а pubkey! не скомпилируется.

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

Запись условий

CreateDvp принимает две суммы в базовых единицах, срок действия, нонс и четыре необязательных поля: непрозрачную ссылку размером до 64 байт, адрес назначения расчёта для каждой стороны и самое раннее время расчёта (см. «Примечания»). Адрес назначения — это место, куда при расчёте поступают средства соответствующей стороны, например депозитный кошелёк кастодиана; по умолчанию это собственный адрес стороны, и после создания записи он не меняется. Возвраты и изъятия средств всегда направляются на собственный token account стороны для минта соответствующей части сделки, а не на адрес назначения, независимо от того, кто пополнил эскроу.

Плательщик вносит rent за запись, tombstone-запись нонса и оба эскроу. При закрытии этот rent получает тот, кто подписывает закрывающую инструкцию, а не плательщик. Tombstone никогда не закрывается, поэтому после каждой сделки остаётся небольшой постоянный баланс. При планировании SOL плательщика определяйте суммы rent методом RPC getMinimumBalanceForRentExemption, а не задавайте их жёстко в коде.

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

Для фрагмента на TypeScript в области видимости должны быть getProgramDerivedAddress и getAddressEncoder из @solana/kit, а также DVP_SWAP_PROGRAM_PROGRAM_ADDRESS из клиента. Билдер на Rust оставляет system program и associated token program со значениями по умолчанию.

Примечания

  • Срок действия. Срок действия проверяет только Settle. Reclaim, Cancel и Reject работают и после его истечения, а депозит, поступивший после истечения срока, может вернуть сторона соответствующей части сделки. Срок действия не может превышать один год с момента создания.
  • Время — это время сети. Программа читает ончейн-часы, которые могут расходиться с реальным временем, иногда на десятки секунд, и могут скачком уходить вперёд. Оставляйте запас между сроком действия и планируемым временем расчёта, избегайте пополнения и расчёта вблизи любой из границ и не полагайтесь на срок действия как на дедлайн с точностью до секунды. README программы советует закладывать около минуты расхождения.
  • Самое раннее время расчёта. Необязательный параметр. Если он задан, Settle до этого момента отклоняется, и он не должен быть позже срока действия.
  • Проверки при создании. Create завершается ошибкой PartyNotSignerCapable, если сторона не является аккаунтом, принадлежащим system program (это исключает мультиподпись SPL Token), а также ошибками SettlementDestinationIsSwapDvp, SelfDvp, SameMint, ZeroAmount, SettlementAuthorityIsParty или ExpiryNotInFuture. Полный список приведён в таблице ошибок.

Дальнейшие шаги

Is this page helpful?

Содержание

Редактировать страницу