NOVO: Helius adquire a Light Protocol
como enviar transações da Solana usando nonces duráveis
Blog/Desenvolvimento

Transações da Solana: nonces duráveis (2023)

Líder de Relações com DesenvolvedoresHunter Davis no LinkedIn
6 min de leitura

Introdução

Ao enviar uma transação na Solana como desenvolvedor, você precisa buscar o blockhash mais recente por meio do RPC da Solana. Essa etapa essencial ajuda a evitar ataques de repetição, garantindo que, após uma transação ser assinada e enviada usando um blockhash específico, ninguém possa usar o mesmo hash para repetir ou reenviar essa transação.

Imagine que você precise enviar uma transação que exige uma assinatura de um armazenamento frio offline ou de uma carteira de hardware como a Ledger. No entanto, os blockhashes expiram rapidamente, o que pode invalidar sua transação. É aí que entram os nonces duráveis, que permitem realizar transações offline com segurança.

Ao final deste guia, você entenderá:

  1. O que é um nonce durável.
  2. A finalidade de um nonce durável.
  3. Como usar nonces duráveis em transações.

Transações: pré-requisitos

Antes de começar, verifique se você tem:

  • Conhecimentos básicos de JavaScript.
  • NodeJS instalado.
  • Solana CLI instalada.
  • Git instalado

Configuração do ambiente

  1. Clone nosso repositório de exemplo com utilitários existentes:
Código
git clone
  1. Acesse a pasta do projeto e instale o npm:
Código
cd durable-noncenpm install
  1. Acesse a pasta de carteiras dentro da pasta de nonce. Ela armazenará nossas chaves locais para testes:
Código
cd wallets
  1. Com a Solana CLI instalada, crie uma carteira para o par de chaves pagador:
Código
solana-keygen new -o ./wallet.json
  1. Agora, defina essa carteira como sua carteira na CLI para receber Solana via airdrop:
Código
solana config set --keypair ./wallet.json
  1. Agora você pode fazer um airdrop de Solana para esse endereço executando o seguinte comando:
Código
solana aidrop 2
  1. Também precisamos criar e financiar outra carteira para a autoridade do nonce:
Código
solana-keygen new -o ./nonceAuth.json
  1. Para a chave pública gerada, podemos usar um site de faucet para enviar 1 SOL por airdrop, disponível aqui.

Agora que nosso ambiente está configurado, podemos avançar para as próximas etapas.

O que é um nonce durável?

Uma conta de nonce durável na Solana pode ser vista como um cofre. Quando você cria essa conta, a Solana atribui a ela um código exclusivo e estável chamado "nonce durável". Diferentemente dos nonces comuns, que mudam a cada transação, esse permanece estável e serve como uma referência consistente.

Isso é especialmente útil para transações "offline". Ao criar uma transação, você referencia esse nonce da sua conta. A Solana o valida comparando-o com o valor armazenado e, se houver correspondência, a transação é aprovada. Portanto, uma conta de nonce durável funciona tanto como mecanismo de armazenamento quanto de validação, garantindo a autenticidade das transações e, ao mesmo tempo, atendendo ao ritmo acelerado e aos cenários offline da rede Solana.

Os nonces duráveis podem ser usados em vários casos, como:

  • Transações agendadas: você pode configurar transações para ocorrerem em um momento específico no futuro. Os nonces duráveis garantem que essas transações agendadas sejam executadas com segurança.
  • Carteiras multisig: no contexto de carteiras com múltiplas assinaturas, os nonces duráveis oferecem uma camada adicional de segurança e coordenação entre vários signatários.
  • Programas que precisam de interação futura: alguns programas na Solana exigem interações com outros programas ou serviços em intervalos específicos. Os nonces duráveis ajudam a manter a integridade dessas interações.
  • Interação com outras blockchains: quando a Solana interage com outras blockchains, os nonces duráveis ajudam a garantir a validade das transações entre diferentes blockchains.

Agora podemos começar a desenvolver nosso exemplo.

Transações da Solana: etapas de desenvolvimento

Etapa 1: configure as dependências e constantes

Nesta etapa, você importará os módulos e utilitários necessários e definirá as constantes e os pares de chaves do exemplo. Essas dependências e constantes serão usadas durante todo o processo da transação.

Código
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;

Etapa 2: crie a função sendTransaction

A função sendTransaction coordena o processo de envio de uma transação usando um nonce durável. Essa função lida com a criação e a confirmação do nonce e com a execução da transação.

Código
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);
  }
}

Etapa 3: crie a função nonce

A função nonce é responsável por criar e inicializar a conta de nonce durável. Isso envolve calcular o aluguel necessário para a conta, buscar o blockhash mais recente e construir transações para criar e inicializar a conta de nonce.

  1. Antes de criar a conta de nonce, precisamos calcular o aluguel necessário para armazenar os dados da conta e buscar o blockhash mais recente.
Código
async function nonce() {
  const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  1. Agora, construiremos uma transação para criar a conta de nonce. Isso envolve usar a instrução SystemProgram.createAccount para alocar espaço para a conta de nonce.
Código
const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );
  1. Assinaremos a transação com os pares de chaves da autoridade e a enviaremos para a rede Solana. Essa transação cria a conta de nonce durável.
Código
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. Após enviar a transação, confirmaremos seu status para garantir que a conta de nonce tenha sido criada com sucesso.
Código
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. Para usar plenamente a conta de nonce, precisamos inicializar seu valor. Criaremos uma nova transação para executar a instrução SystemProgram.nonceInitialize.
Código
// Initialize the nonce value within the account
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );
  1. Assim como na etapa anterior, assinaremos a transação e a enviaremos para a rede a fim de inicializar a conta de nonce.
Código
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. Por fim, confirmaremos o status da transação de inicialização para garantir que a conta de nonce tenha sido inicializada corretamente.
Código
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;
  }
}

A função completa deve ser semelhante a esta:

Código
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;
  }
}

Etapa 4: crie a função getNonce

Defina a função getNonce, responsável por buscar o valor do nonce na conta de nonce criada.

Código
async function getNonce() {
  const nonceAccount = await fetchNonceInfo();
  return nonceAccount.nonce;
}

Etapa 5: crie a função createTx

Defina a função createTx, que cria uma transação de exemplo contendo a instrução de avanço do nonce e uma instrução de transferência. Ela usa o nonce buscado anteriormente para garantir a autenticidade da transação.

Código
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;
}

Etapa 6: crie a função signOffline

Defina a função signOffline, responsável por assinar a transação offline. Ela simula um atraso offline antes de assinar a transação com os pares de chaves do remetente e da autoridade do nonce.

Código
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;
}

Etapa 7: crie a função executeTx

A função executeTx é responsável por enviar a transação assinada para execução na rede Solana. Essa é a etapa final do processo, quando a transação é transmitida para a rede.

Código
async function executeTx() {
  const signedTx = await readAndDecodeTransaction("./signedTxn.json");
  const sig = await connection.sendRawTransaction(signedTx.serialize());
  console.log("Tx sent: ", sig);
}

Etapa 8: crie a função fetchNonceInfo

A função fetchNonceInfo busca informações do nonce na conta de nonce criada, repetindo a tentativa até três vezes, se necessário. Isso ajuda a garantir que o nonce usado na transação esteja atualizado e seja válido.

Código
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");
}

Etapa 9: chame a função sendTransaction

Por fim, chame a função sendTransaction para iniciar o processo da transação. Essa função reúne todas as etapas definidas anteriormente para criar, assinar e executar uma transação usando um nonce durável.

Código
ts-node main

A execução de sendTransaction gerará uma assinatura para uma transação bem-sucedida. Essa assinatura é uma informação essencial para rastrear e verificar a transação na rede Solana.

Código
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent:  64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U

Agora você usou um nonce durável em uma transação bem-sucedida!

Código completo

Código
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();

Transações da Solana: como usar os RPCs da Helius

A Helius pode atuar como uma intermediária poderosa para interagir com os RPCs da Solana, simplificando o processo de busca das informações de blockhash necessárias para nonces duráveis. Com a Helius, você pode gerenciar o ciclo de vida das suas transações da Solana com mais confiabilidade, especialmente em cenários offline. Ela oferece acesso simplificado aos blockhashes, ajudando os desenvolvedores a tornar suas aplicações mais resistentes à expiração de transações.

Em resumo, os nonces duráveis nas transações da Solana oferecem uma maneira segura e confiável de lidar com transações offline e garantir sua autenticidade. Seguindo as etapas apresentadas neste guia, os desenvolvedores podem implementar nonces duráveis em suas aplicações da Solana, aumentando a segurança e a flexibilidade.

‍

‍

‍

Assine a Helius

Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos