
Транзакции Solana: долговечные nonce (2023)
Введение
При отправке транзакции в Solana разработчику необходимо получить последний хеш блока через Solana RPC. Этот важный шаг помогает предотвратить атаки повторного воспроизведения. После подписания и отправки транзакции с определенным хешем блока никто не сможет использовать тот же хеш для ее повторного воспроизведения или отправки.
Представьте, что вам нужно отправить транзакцию, требующую подписи из офлайн-хранилища или аппаратного кошелька, например Ledger. Однако хеши блоков быстро истекают, из-за чего транзакция может стать недействительной. Эту проблему решают долговечные nonce, позволяющие безопасно проводить офлайн-транзакции.
Из этого руководства вы узнаете:
- Что такое долговечный nonce.
- Для чего нужен долговечный nonce.
- Как использовать долговечные nonce в транзакциях.
Транзакции: требования
Прежде чем начать, убедитесь, что у вас есть:
- Базовые знания JavaScript.
- Установленный NodeJS.
- Установленный Solana CLI.
- Установленный Git
Настройка окружения
- Клонируйте наш пример репозитория с готовыми утилитами:
git clone- Перейдите в папку проекта и установите npm:
cd durable-noncenpm install- Перейдите в папку wallets внутри папки nonce. В ней будут храниться локальные ключи для тестирования:
cd wallets- После установки Solana CLI создайте кошелек для оплачивающей пары ключей:
solana-keygen new -o ./wallet.json- Теперь укажите этот кошелек в CLI, чтобы получить Solana через airdrop:
solana config set --keypair ./wallet.json- Теперь вы можете получить Solana на этот адрес через airdrop, выполнив следующую команду:
solana aidrop 2- Также нужно создать и пополнить еще один кошелек для управляющего nonce:
solana-keygen new -o ./nonceAuth.json- Для созданного публичного ключа можно получить 1 SOL через airdrop на сайте-кране здесь.
Окружение настроено, и теперь можно перейти к следующим шагам.
Что такое долговечный nonce?
Аккаунт долговечного nonce в Solana можно представить как банковскую ячейку. При создании такого аккаунта Solana присваивает ему уникальный постоянный код — «долговечный nonce». В отличие от обычных nonce, которые меняются с каждой транзакцией, он остается неизменным и служит постоянной точкой отсчета.
Это особенно полезно для «офлайн-транзакций». При создании транзакции вы указываете этот nonce из своего аккаунта. Solana сверяет его с сохраненным значением и при совпадении разрешает транзакцию. Таким образом, аккаунт долговечного nonce служит механизмом хранения и проверки. Он подтверждает подлинность транзакций с учетом высокой скорости сети Solana и сценариев работы офлайн.
Долговечные nonce можно использовать в различных сценариях, например:
- Запланированные транзакции: можно настроить выполнение транзакций в определенное время в будущем. Долговечные nonce обеспечивают безопасное выполнение таких транзакций.
- Мультиподписные кошельки: в кошельках с мультиподписью долговечные nonce обеспечивают дополнительный уровень безопасности и координации между несколькими подписантами.
- Программы, требующие взаимодействия в будущем: некоторым программам в Solana необходимо взаимодействовать с другими программами или сервисами через определенные интервалы. Долговечные nonce помогают сохранять целостность этих взаимодействий.
- Взаимодействие с другими блокчейнами: когда Solana взаимодействует с другими блокчейнами, долговечные nonce помогают обеспечить действительность межсетевых транзакций.
Теперь можно приступить к созданию примера.
Транзакции Solana: этапы реализации
Шаг 1. Настройте зависимости и константы
На этом шаге вы импортируете необходимые модули и утилиты, а также определите константы и пары ключей для примера. Эти зависимости и константы будут использоваться на всех этапах обработки транзакции.
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
NonceAccount,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { encodeAndWriteTransaction, loadWallet, readAndDecodeTransaction } from "./utils";
const nonceAuthKeypair = loadWallet('./wallets/nonceAuth.json');
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet('./wallets/wallet.json');
const connection = new Connection("https://devnet.helius-rpc.com/?api-key=");
const waitTime = 120000;
const TranferAmount = LAMPORTS_PER_SOL * 0.01;Шаг 2. Создайте функцию sendTransaction
Функция sendTransaction управляет отправкой транзакции с использованием долговечного nonce. Она отвечает за создание и подтверждение nonce, а также выполнение транзакции.
async function sendTransaction() {
console.log("Starting Nonce Transaction")
try {
const nonceCreationTxSig = await nonce();
const confirmationStatus = await connection.confirmTransaction(nonceCreationTxSig);
if (!confirmationStatus.value.err) {
console.log("Nonce account creation confirmed.");
const nonce = await getNonce();
await createTx(nonce);
await signOffline(waitTime);
await executeTx();
} else {
console.error("Nonce account creation transaction failed:", confirmationStatus.value.err);
}
} catch (error) {
console.error(error);
}
}Шаг 3. Создайте функцию nonce
Функция nonce создает и инициализирует аккаунт долговечного nonce. Для этого она рассчитывает арендную плату за аккаунт, получает последний хеш блока и формирует транзакции для создания и инициализации аккаунта nonce.
- Перед созданием аккаунта nonce необходимо рассчитать арендную плату за хранение его данных и получить последний хеш блока.
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();- Теперь сформируем транзакцию для создания аккаунта nonce. Для выделения пространства под аккаунт nonce используется инструкция
SystemProgram.createAccount.
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);- Подпишем транзакцию управляющими парами ключей и отправим ее в сеть Solana. Эта транзакция создает аккаунт долговечного nonce.
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
const signature = await connection.sendRawTransaction(createNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });- После отправки транзакции подтвердим ее статус, чтобы убедиться, что аккаунт nonce успешно создан.
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error("Nonce account creation transaction failed: " + confirmationStatus.value.err);
}
console.log("Nonce account created:", signature);- Чтобы полноценно использовать аккаунт nonce, необходимо инициализировать его значение. Создадим новую транзакцию для выполнения инструкции
SystemProgram.nonceInitialize.
// Initialize the nonce value within the account
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);- Как и на предыдущем шаге, подпишем транзакцию и отправим ее в сеть для инициализации аккаунта nonce.
const { blockhash: initBlockhash, lastValidBlockHeight: initLastValidBlockHeight } = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair);
const initSignature = await connection.sendRawTransaction(initializeNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });- Наконец, подтвердим статус транзакции инициализации, чтобы убедиться, что аккаунт nonce правильно инициализирован.
const initConfirmationStatus = await connection.confirmTransaction(initSignature);
if (initConfirmationStatus.value.err) {
throw new Error("Nonce initialization transaction failed: " + initConfirmationStatus.value.err);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}Полная функция должна выглядеть примерно так:
async function nonce() {
// For creating the nonce account
const rent = await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
// Create the nonce account
const signature = await connection.sendRawTransaction(
createNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error(
"Nonce account creation transaction failed: " +
confirmationStatus.value.err
);
}
console.log("Nonce account created:", signature);
// Now, initialize the nonce
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);
const {
blockhash: initBlockhash,
lastValidBlockHeight: initLastValidBlockHeight,
} = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair
const initSignature = await connection.sendRawTransaction(
initializeNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const initConfirmationStatus = await connection.confirmTransaction(
initSignature
);
if (initConfirmationStatus.value.err) {
throw new Error(
"Nonce initialization transaction failed: " +
initConfirmationStatus.value.err
);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}Шаг 4. Создайте функцию getNonce
Определите функцию getNonce, которая получает значение nonce из созданного аккаунта nonce.
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}Шаг 5. Создайте функцию createTx
Определите функцию createTx. Она создает пример транзакции с инструкцией продвижения nonce и инструкцией перевода. Функция использует ранее полученный nonce для подтверждения подлинности транзакции.
async function createTx(nonce) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
return serialisedTx;
}Шаг 6. Создайте функцию signOffline
Определите функцию signOffline, которая подписывает транзакцию офлайн. Она имитирует задержку офлайн-режима, а затем подписывает транзакцию парами ключей отправителя и управляющего nonce.
async function createTx(nonce) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
return serialisedTx;
}Шаг 7. Создайте функцию executeTx
Функция executeTx отправляет подписанную транзакцию в сеть Solana для выполнения. Это заключительный этап процесса, на котором транзакция передается в сеть.
async function executeTx() {
const signedTx = await readAndDecodeTransaction("./signedTxn.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log("Tx sent: ", sig);
}Шаг 8. Создайте функцию fetchNonceInfo
Функция fetchNonceInfo получает информацию о nonce из созданного аккаунта nonce. При необходимости она выполняет до трех повторных попыток. Это помогает убедиться, что используемый в транзакции nonce актуален и действителен.
async function fetchNonceInfo(retries = 3) {
while (retries > 0) {
const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
if (accountInfo) {
const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
return nonceAccount;
}
retries--;
if (retries > 0) {
console.log(`Retry fetching nonce in 3 seconds. ${retries} retries left.`);
await new Promise(res => setTimeout(res, 3000)); // wait for 3 seconds
}
}
throw new Error("No account info found");
}Шаг 9. Вызовите функцию sendTransaction
Наконец, вызовите функцию sendTransaction, чтобы запустить процесс транзакции. Она объединяет все описанные ранее шаги для создания, подписания и выполнения транзакции с долговечным nonce.
ts-node mainПосле запуска sendTransaction будет создана подпись успешно выполненной транзакции. Эта подпись необходима для отслеживания и проверки транзакции в сети Solana.
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent: 64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5UТеперь вы успешно использовали долговечный nonce в транзакции!
Полный код
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
NonceAccount,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import {
encodeAndWriteTransaction,
loadWallet,
readAndDecodeTransaction,
} from "./utils";
const TranferAmount = LAMPORTS_PER_SOL * 0.01;
const nonceAuthKeypair = loadWallet("./wallets/nonceAuth.json");
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet("./wallets/wallet.json");
const connection = new Connection(
"https://devnet.helius-rpc.com/?api-key="
);
const waitTime = 120000;
async function sendTransaction() {
try {
// Create nonce and get its creation transaction signature
const nonceCreationTxSig = await nonce();
// Ensure nonce account creation is confirmed before moving forward
const confirmationStatus = await connection.confirmTransaction(
nonceCreationTxSig
);
if (!confirmationStatus.value.err) {
console.log("Nonce account creation confirmed.");
const nonce = await getNonce();
await createTx(nonce);
await signOffline(waitTime);
await executeTx();
} else {
console.error(
"Nonce account creation transaction failed:",
confirmationStatus.value.err
);
}
} catch (error) {
console.error(error);
}
}
async function nonce() {
// For creating the nonce account
const rent = await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
);
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
createNonceTx.recentBlockhash = blockhash;
createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
createNonceTx.sign(nonceAuthKeypair, nonceKeypair);
try {
// Create the nonce account
const signature = await connection.sendRawTransaction(
createNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const confirmationStatus = await connection.confirmTransaction(signature);
if (confirmationStatus.value.err) {
throw new Error(
"Nonce account creation transaction failed: " +
confirmationStatus.value.err
);
}
console.log("Nonce account created:", signature);
// Now, initialize the nonce
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);
const {
blockhash: initBlockhash,
lastValidBlockHeight: initLastValidBlockHeight,
} = await connection.getLatestBlockhash();
initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
initializeNonceTx.recentBlockhash = initBlockhash;
initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair
const initSignature = await connection.sendRawTransaction(
initializeNonceTx.serialize(),
{ skipPreflight: false, preflightCommitment: "single" }
);
const initConfirmationStatus = await connection.confirmTransaction(
initSignature
);
if (initConfirmationStatus.value.err) {
throw new Error(
"Nonce initialization transaction failed: " +
initConfirmationStatus.value.err
);
}
console.log("Nonce initialized:", initSignature);
return initSignature;
} catch (error) {
console.error("Failed in createNonce function: ", error);
throw error;
}
}
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}
async function createTx(nonce: string) {
const destination = Keypair.generate();
const advanceNonceIx = SystemProgram.nonceAdvance({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
});
const transferIx = SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: destination.publicKey,
lamports: TranferAmount,
});
const sampleTx = new Transaction();
sampleTx.add(advanceNonceIx, transferIx);
sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
sampleTx.feePayer = senderKeypair.publicKey;
const serialisedTx = encodeAndWriteTransaction(
sampleTx,
"./unsigned.json",
false
);
return serialisedTx;
}
async function signOffline(waitTime = 120000): Promise {
await new Promise((resolve) => setTimeout(resolve, waitTime));
const unsignedTx = readAndDecodeTransaction("./unsigned.json");
unsignedTx.sign(senderKeypair, nonceAuthKeypair); // Sign with both keys
const serialisedTx = encodeAndWriteTransaction(unsignedTx, "./signed.json");
return serialisedTx;
}
async function executeTx() {
const signedTx = readAndDecodeTransaction("./signed.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log(" Tx sent: ", sig);
}
async function fetchNonceInfo(retries = 3): Promise {
while (retries > 0) {
const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
if (accountInfo) {
const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
return nonceAccount;
}
retries--;
if (retries > 0) {
console.log(
`Retry fetching nonce in 3 seconds. ${retries} retries left.`
);
await new Promise((res) => setTimeout(res, 3000)); // wait for 3 seconds
}
}
throw new Error("No account info found");
}
sendTransaction();Транзакции Solana: использование Helius RPC
Helius может выступать мощным посредником при взаимодействии с Solana RPC, упрощая получение данных о хешах блоков, необходимых для долговечных nonce. С Helius вы можете надежнее управлять жизненным циклом транзакций Solana, особенно в офлайн-сценариях. Он предоставляет удобный доступ к хешам блоков и помогает разработчикам сделать приложения устойчивее к истечению срока действия транзакций.
Таким образом, долговечные nonce в транзакциях Solana позволяют безопасно и надежно обрабатывать офлайн-транзакции и подтверждать их подлинность. Следуя шагам из этого руководства, разработчики могут реализовать долговечные nonce в своих приложениях Solana, повысив их безопасность и гибкость.
Похожие статьи
Подпишитесь на Helius
Следите за новостями разработки на Solana и получайте уведомления о наших публикациях


