NOUVEAU : Helius acquiert Light Protocol
comment envoyer des transactions Solana à l’aide de nonces durables
Blog/Développement

Transactions Solana : nonces durables (2023)

Responsable des relations avec les développeursHunter Davis sur LinkedIn
6 min de lecture

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 :

  1. Ce qu’est un nonce durable.
  2. L’utilité d’un nonce durable.
  3. 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

  1. Clonez notre dépôt d’exemple contenant les utilitaires existants :
Code
git clone
  1. Accédez au dossier du projet et installez npm :
Code
cd durable-noncenpm install
  1. Accédez au dossier wallets situé dans le dossier nonce. Il contiendra nos clés locales de test :
Code
cd wallets
  1. Une fois Solana CLI installé, créez un portefeuille pour la paire de clés qui effectuera le paiement :
Code
solana-keygen new -o ./wallet.json
  1. Définissez maintenant ce portefeuille comme votre portefeuille dans la CLI afin de recevoir des SOL par airdrop :
Code
solana config set --keypair ./wallet.json
  1. Vous pouvez maintenant envoyer des SOL par airdrop à cette adresse en exécutant la commande suivante :
Code
solana aidrop 2
  1. Nous devons également créer et approvisionner un autre portefeuille pour l’autorité du nonce :
Code
solana-keygen new -o ./nonceAuth.json
  1. 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.

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

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

  1. 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.
Code
async function nonce() {
  const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  1. Nous allons maintenant construire une transaction pour créer le compte de nonce. Pour cela, nous utiliserons l’instruction SystemProgram.createAccount afin d’allouer de l’espace au compte de nonce.
Code
const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );
  1. 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.
Code
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" });
  1. Après l’envoi de la transaction, nous confirmerons son statut pour vérifier que le compte de nonce a bien été créé.
Code
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);
  1. 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.
Code
// Initialize the nonce value within the account
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );
  1. Comme à l’étape précédente, nous signerons la transaction et l’enverrons au réseau afin d’initialiser le compte de nonce.
Code
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" });
  1. Enfin, nous confirmerons le statut de la transaction d’initialisation pour vérifier que le compte de nonce est correctement initialisé.
Code
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 :

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

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

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

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

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

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

Code
ts-node main

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

Code
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent:  64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U

Vous avez maintenant utilisé un nonce durable dans une transaction réussie !

Code complet

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

‍

‍

‍

Abonnez-vous à Helius

Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication