
Transactions Solana : nonces durables (2023)
Introduction
Lorsque vous soumettez une transaction sur Solana en tant que développeur, vous devez récupérer le dernier hash de bloc via le RPC Solana. Cette étape essentielle permet de limiter les attaques par rejeu : une fois qu’une transaction est signée et soumise avec un hash de bloc donné, personne ne peut utiliser ce même hash pour rejouer ou soumettre à nouveau cette transaction.
Imaginez que vous deviez soumettre une transaction nécessitant la signature d’un stockage à froid hors ligne ou d’un portefeuille matériel comme Ledger. Cependant, les hash de blocs expirent rapidement, ce qui peut rendre votre transaction non valide. C’est là qu’interviennent les nonces durables, qui permettent d’effectuer des transactions hors ligne sécurisées.
À la fin de ce guide, vous comprendrez :
- Ce qu’est un nonce durable.
- L’utilité d’un nonce durable.
- Comment utiliser des nonces durables dans des transactions.
Transactions : prérequis
Avant de commencer, assurez-vous de disposer des éléments suivants :
- Connaissances de base en JavaScript.
- NodeJS installé.
- Solana CLI installé.
- Git installé
Configuration de l’environnement
- Clonez notre dépôt d’exemple contenant les utilitaires existants :
git clone- Accédez au dossier du projet et installez npm :
cd durable-noncenpm install- Accédez au dossier wallets situé dans le dossier nonce. Il contiendra nos clés locales de test :
cd wallets- Une fois Solana CLI installé, créez un portefeuille pour la paire de clés qui effectuera le paiement :
solana-keygen new -o ./wallet.json- Définissez maintenant ce portefeuille comme votre portefeuille dans la CLI afin de recevoir des SOL par airdrop :
solana config set --keypair ./wallet.json- Vous pouvez maintenant envoyer des SOL par airdrop à cette adresse en exécutant la commande suivante :
solana aidrop 2- Nous devons également créer et approvisionner un autre portefeuille pour l’autorité du nonce :
solana-keygen new -o ./nonceAuth.json- Pour la clé publique générée, nous pouvons utiliser un faucet afin de lui envoyer 1 SOL par airdrop, disponible ici.
Maintenant que notre environnement est configuré, nous pouvons passer aux étapes suivantes.
Qu’est-ce qu’un nonce durable ?
Un compte de nonce durable sur Solana peut être comparé à un coffre-fort. Lorsque vous créez ce compte, Solana lui attribue un code unique et stable appelé « nonce durable ». Contrairement aux nonces classiques qui changent à chaque transaction, celui-ci reste stable et sert de référence constante.
Cela s’avère particulièrement utile pour les transactions « hors ligne ». Lorsque vous créez une transaction, vous référencez ce nonce depuis votre compte. Solana le compare à la valeur enregistrée et, s’ils correspondent, valide la transaction. Un compte de nonce durable constitue donc à la fois un mécanisme de stockage et de validation. Il garantit l’authenticité des transactions tout en s’adaptant au rythme rapide du réseau Solana et aux scénarios hors ligne.
Les nonces durables peuvent être utilisés dans différents cas, notamment :
- Transactions planifiées : vous pouvez configurer des transactions pour qu’elles soient exécutées à une date ultérieure précise. Les nonces durables garantissent l’exécution sécurisée de ces transactions planifiées.
- Portefeuilles multisignatures : dans le cadre de portefeuilles multisignatures, les nonces durables apportent une couche supplémentaire de sécurité et de coordination entre plusieurs signataires.
- Programmes nécessitant une interaction future : certains programmes sur Solana doivent interagir avec d’autres programmes ou services à intervalles précis. Les nonces durables contribuent à préserver l’intégrité de ces interactions.
- Interactions avec d’autres blockchains : lorsque Solana interagit avec d’autres blockchains, les nonces durables contribuent à garantir la validité des transactions inter-chaînes.
Nous pouvons maintenant commencer à créer notre exemple.
Transactions Solana : étapes de création
Étape 1 : configurer les dépendances et les constantes
Au cours de cette étape, vous importerez les modules et utilitaires nécessaires, puis définirez les constantes et les paires de clés de l’exemple. Ces dépendances et constantes seront utilisées tout au long du processus de transaction.
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;Étape 2 : créer la fonction sendTransaction
La fonction sendTransaction orchestre le processus d’envoi d’une transaction à l’aide d’un nonce durable. Elle gère la création et la confirmation du nonce, ainsi que l’exécution de la transaction.
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);
}
}Étape 3 : créer la fonction nonce
La fonction nonce crée et initialise le compte de nonce durable. Cela implique de calculer le loyer requis pour le compte, de récupérer le dernier hash de bloc et de construire les transactions permettant de créer et d’initialiser le compte de nonce.
- Avant de créer le compte de nonce, nous devons calculer le loyer requis pour le stockage des données du compte et récupérer le dernier hash de bloc.
async function nonce() {
const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();- Nous allons maintenant construire une transaction pour créer le compte de nonce. Pour cela, nous utiliserons l’instruction
SystemProgram.createAccountafin d’allouer de l’espace au compte de nonce.
const createNonceTx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: nonceAuthKeypair.publicKey,
newAccountPubkey: nonceKeypair.publicKey,
lamports: rent,
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
})
);- Nous signerons la transaction avec les paires de clés de l’autorité, puis l’enverrons au réseau Solana. Cette transaction crée le compte de nonce durable.
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" });- Après l’envoi de la transaction, nous confirmerons son statut pour vérifier que le compte de nonce a bien été créé.
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);- Pour utiliser pleinement le compte de nonce, nous devons initialiser sa valeur. Nous créerons une nouvelle transaction afin d’exécuter l’instruction
SystemProgram.nonceInitialize.
// Initialize the nonce value within the account
const initializeNonceTx = new Transaction().add(
SystemProgram.nonceInitialize({
noncePubkey: nonceKeypair.publicKey,
authorizedPubkey: nonceAuthKeypair.publicKey,
})
);- Comme à l’étape précédente, nous signerons la transaction et l’enverrons au réseau afin d’initialiser le compte de 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" });- Enfin, nous confirmerons le statut de la transaction d’initialisation pour vérifier que le compte de nonce est correctement initialisé.
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;
}
}La fonction complète devrait ressembler à ceci :
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;
}
}Étape 4 : créer la fonction getNonce
Définissez la fonction getNonce, chargée de récupérer la valeur du nonce depuis le compte de nonce créé.
async function getNonce() {
const nonceAccount = await fetchNonceInfo();
return nonceAccount.nonce;
}Étape 5 : créer la fonction createTx
Définissez la fonction createTx, qui crée un exemple de transaction contenant à la fois l’instruction d’avancement du nonce et une instruction de transfert. Elle utilise le nonce précédemment récupéré pour garantir l’authenticité de la transaction.
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;
}Étape 6 : créer la fonction signOffline
Définissez la fonction signOffline, chargée de signer la transaction hors ligne. Elle simule un délai hors ligne avant de signer la transaction avec les paires de clés de l’expéditeur et de l’autorité du 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;
}Étape 7 : créer la fonction executeTx
La fonction executeTx envoie la transaction signée au réseau Solana afin qu’elle soit exécutée. Il s’agit de la dernière étape du processus, au cours de laquelle la transaction est diffusée sur le réseau.
async function executeTx() {
const signedTx = await readAndDecodeTransaction("./signedTxn.json");
const sig = await connection.sendRawTransaction(signedTx.serialize());
console.log("Tx sent: ", sig);
}Étape 8 : créer la fonction fetchNonceInfo
La fonction fetchNonceInfo récupère les informations du nonce depuis le compte de nonce créé, avec jusqu’à trois nouvelles tentatives si nécessaire. Cela permet de vérifier que le nonce utilisé dans la transaction est à jour et valide.
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");
}Étape 9 : appeler la fonction sendTransaction
Enfin, appelez la fonction sendTransaction pour lancer le processus de transaction. Cette fonction rassemble toutes les étapes précédemment définies afin de créer, signer et exécuter une transaction à l’aide d’un nonce durable.
ts-node mainL’exécution de sendTransaction générera une signature de transaction si celle-ci réussit. Cette signature est une information essentielle pour suivre et vérifier la transaction sur le réseau Solana.
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent: 64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5UVous avez maintenant utilisé un nonce durable dans une transaction réussie !
Code complet
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();Transactions Solana : utiliser les RPC Helius
Helius peut servir d’intermédiaire puissant pour interagir avec les RPC de Solana, en simplifiant la récupération des informations de hash de bloc nécessaires aux nonces durables. Grâce à Helius, vous pouvez gérer le cycle de vie de vos transactions Solana de manière plus fiable, en particulier dans les scénarios hors ligne. Helius fournit un accès simplifié aux hash de blocs, ce qui aide les développeurs à rendre leurs applications plus résistantes à l’expiration des transactions.
En résumé, les nonces durables des transactions Solana offrent un moyen sûr et fiable de gérer les transactions hors ligne et de garantir leur authenticité. En suivant les étapes décrites dans ce guide, les développeurs peuvent intégrer des nonces durables à leurs applications Solana afin d’en améliorer la sécurité et la flexibilité.
Articles associés
Abonnez-vous à Helius
Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication


