Offline na Transaksyon

Mag-sign Transaksyon

Upang lumikha ng isang offline na transaksyon, kailangan mong lagdaan ang transaksyon at pagkatapos kahit sino ay maaaring i-broadcast ito sa network.

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  Transaction,
  SystemProgram,
  LAMPORTS_PER_SOL,
  Message,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";
import * as bs58 from "bs58";

// to complete a offline transaction, I will seperate them into four steps
// 1. Create Transaction
// 2. Sign Transaction
// 3. Recover Transaction
// 4. Send Transaction

(async () => {
  // create connection
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  // create a example tx, alice transfer to bob and feePayer is `feePayer`
  // alice and feePayer are signer in this tx
  const feePayer = Keypair.generate();
  await connection.confirmTransaction(
    await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL)
  );
  const alice = Keypair.generate();
  await connection.confirmTransaction(
    await connection.requestAirdrop(alice.publicKey, LAMPORTS_PER_SOL)
  );
  const bob = Keypair.generate();

  // 1. Create Transaction
  let tx = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: alice.publicKey,
      toPubkey: bob.publicKey,
      lamports: 0.1 * LAMPORTS_PER_SOL,
    })
  );
  tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
  tx.feePayer = feePayer.publicKey;
  let realDataNeedToSign = tx.serializeMessage(); // the real data singer need to sign.

  // 2. Sign Transaction
  // use any lib you like, the main idea is to use ed25519 to sign it.
  // the return signature should be 64 bytes.
  let feePayerSignature = nacl.sign.detached(
    realDataNeedToSign,
    feePayer.secretKey
  );
  let aliceSignature = nacl.sign.detached(realDataNeedToSign, alice.secretKey);

  // 3. Recover Tranasction

  // you can verify signatures before you recovering the transaction
  let verifyFeePayerSignatureResult = nacl.sign.detached.verify(
    realDataNeedToSign,
    feePayerSignature,
    feePayer.publicKey.toBytes() // you should use the raw pubkey (32 bytes) to verify
  );
  console.log(`verify feePayer signature: ${verifyFeePayerSignatureResult}`);

  let verifyAliceSignatureResult = nacl.sign.detached.verify(
    realDataNeedToSign,
    aliceSignature,
    alice.publicKey.toBytes()
  );
  console.log(`verify alice signature: ${verifyAliceSignatureResult}`);

  // there are two ways you can recover the tx
  // 3.a Recover Tranasction (use populate then addSignauture)
  {
    let recoverTx = Transaction.populate(Message.from(realDataNeedToSign));
    recoverTx.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
    recoverTx.addSignature(alice.publicKey, Buffer.from(aliceSignature));

    // 4. Send transaction
    console.log(
      `txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
    );
  }

  // or

  // 3.b. Recover Tranasction (use populate with signature)
  {
    let recoverTx = Transaction.populate(Message.from(realDataNeedToSign), [
      bs58.encode(feePayerSignature),
      bs58.encode(aliceSignature),
    ]);

    // 4. Send transaction
    console.log(
      `txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
    );
  }

  // if this process takes too long, your recent blockhash will expire (after 150 blocks).
  // you can use `durable nonce` to get rid of it.
})();

Bahagyang Sign Transaksyon

Kapag ang isang transaksyon ay nangangailangan ng maraming lagda, maaari mo itong bahagyang lagdaan. Ang iba pang mga pumirma ay maaaring pumirma at mai-broadcast ito sa network.

Ilang halimbawa kung kailan ito kapaki-pakinabang:

  • Magpadala ng token ng SPL bilang kapalit ng pagbabayad
  • Pumirma ng isang transaksyon upang ma-verify mo ang pagiging tunay nito
  • Tumawag sa mga pasadyang programa sa isang transaksyon na nangangailangan ng iyong lagda

Sa halimbawang ito, pinadalhan ni Bob si Alice ng isang token ng SPL bilang kapalit sa kanyang pagbabayad:

Press </> button to view full source
import {
  createTransferCheckedInstruction,
  getAssociatedTokenAddress,
  getMint,
  getOrCreateAssociatedTokenAccount,
} from "@solana/spl-token";
import {
  clusterApiUrl,
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import base58 from "bs58";

/* The transaction:
 * - sends 0.01 SOL from Alice to Bob
 * - sends 1 token from Bob to Alice
 * - is partially signed by Bob, so Alice can approve + send it
 */

(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const alicePublicKey = new PublicKey(
    "5YNmS1R9nNSCDzb5a7mMJ1dwK9uHeAAF4CmPEwKgVWr8"
  );
  const bobKeypair = Keypair.fromSecretKey(
    base58.decode(
      "4NMwxzmYj2uvHuq8xoqhY8RXg63KSVJM1DXkpbmkUY7YQWuoyQgFnnzn6yo3CMnqZasnNPNuAT2TLwQsCaKkUddp"
    )
  );
  const tokenAddress = new PublicKey(
    "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"
  );
  const bobTokenAddress = await getAssociatedTokenAddress(
    tokenAddress,
    bobKeypair.publicKey
  );

  // Alice may not have a token account, so Bob creates one if not
  const aliceTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    bobKeypair, // Bob pays the fee to create it
    tokenAddress, // which token the account is for
    alicePublicKey // who the token account is for
  );

  // Get the details about the token mint
  const tokenMint = await getMint(connection, tokenAddress);

  // Get a recent blockhash to include in the transaction
  const { blockhash } = await connection.getLatestBlockhash("finalized");

  const transaction = new Transaction({
    recentBlockhash: blockhash,
    // Alice pays the transaction fee
    feePayer: alicePublicKey,
  });

  // Transfer 0.01 SOL from Alice -> Bob
  transaction.add(
    SystemProgram.transfer({
      fromPubkey: alicePublicKey,
      toPubkey: bobKeypair.publicKey,
      lamports: 0.01 * LAMPORTS_PER_SOL,
    })
  );

  // Transfer 1 token from Bob -> Alice
  transaction.add(
    createTransferCheckedInstruction(
      bobTokenAddress, // source
      tokenAddress, // mint
      aliceTokenAccount.address, // destination
      bobKeypair.publicKey, // owner of source account
      1 * 10 ** tokenMint.decimals, // amount to transfer
      tokenMint.decimals // decimals of token
    )
  );

  // Partial sign as Bob
  transaction.partialSign(bobKeypair);

  // Serialize the transaction and convert to base64 to return it
  const serializedTransaction = transaction.serialize({
    // We will need Alice to deserialize and sign the transaction
    requireAllSignatures: false,
  });
  const transactionBase64 = serializedTransaction.toString("base64");
  return transactionBase64;

  // The caller of this can convert it back to a transaction object:
  const recoveredTransaction = Transaction.from(
    Buffer.from(transactionBase64, "base64")
  );
})();

Matibay Nonce

Ang RecentBlockhash ay isang mahalagang halaga para sa isang transaksyon. Tatanggihan ang iyong transaksyon kung gumamit ka ng nag-expire kamakailang blockhash (pagkatapos ng 150 block). Maaari mong gamitin ang durable nonce para makakuha ng hindi nag-expire kamakailang blockhash. Upang ma-trigger ang mekanismong ito, dapat ang iyong transaksyon

  1. gumamit ng nonce na naka-store sa nonce account bilang kamakailang blockhash
  2. ilagay ang nonce advance na operasyon sa unang pagtuturo

Lumikha ng Nonce Account

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  Transaction,
  NONCE_ACCOUNT_LENGTH,
  SystemProgram,
  LAMPORTS_PER_SOL,
} from "@solana/web3.js";

(async () => {
  // Setup our connection and wallet
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const feePayer = Keypair.generate();

  // Fund our wallet with 1 SOL
  const airdropSignature = await connection.requestAirdrop(
    feePayer.publicKey,
    LAMPORTS_PER_SOL
  );
  await connection.confirmTransaction(airdropSignature);

  const nonceAccountAuth = Keypair.generate();

  let nonceAccount = Keypair.generate();
  console.log(`nonce account: ${nonceAccount.publicKey.toBase58()}`);

  let tx = new Transaction().add(
    // create nonce account
    SystemProgram.createAccount({
      fromPubkey: feePayer.publicKey,
      newAccountPubkey: nonceAccount.publicKey,
      lamports: await connection.getMinimumBalanceForRentExemption(
        NONCE_ACCOUNT_LENGTH
      ),
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    }),
    // init nonce account
    SystemProgram.nonceInitialize({
      noncePubkey: nonceAccount.publicKey, // nonce account pubkey
      authorizedPubkey: nonceAccountAuth.publicKey, // nonce account authority (for advance and close)
    })
  );

  console.log(
    `txhash: ${await connection.sendTransaction(tx, [feePayer, nonceAccount])}`
  );
})();

Kumuha ng Nonce Account

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  PublicKey,
  Keypair,
  NonceAccount,
} from "@solana/web3.js";

(async () => {
  // connection
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const nonceAccountPubkey = new PublicKey(
    "7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4"
  );

  let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);
  let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
  console.log(`nonce: ${nonceAccount.nonce}`);
  console.log(`authority: ${nonceAccount.authorizedPubkey.toBase58()}`);
  console.log(`fee calculator: ${JSON.stringify(nonceAccount.feeCalculator)}`);
})();

Gumamit ng Nonce Account

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  PublicKey,
  Keypair,
  Transaction,
  SystemProgram,
  NonceAccount,
  LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import * as bs58 from "bs58";

(async () => {
  // Setup our connection and wallet
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const feePayer = Keypair.generate();

  // Fund our wallet with 1 SOL
  const airdropSignature = await connection.requestAirdrop(
    feePayer.publicKey,
    LAMPORTS_PER_SOL
  );
  await connection.confirmTransaction(airdropSignature);

  // G2FAbFQPFa5qKXCetoFZQEvF9BVvCKbvUZvodpVidnoY
  const nonceAccountAuth = Keypair.fromSecretKey(
    bs58.decode(
      "4NMwxzmYj2uvHuq8xoqhY8RXg63KSVJM1DXkpbmkUY7YQWuoyQgFnnzn6yo3CMnqZasnNPNuAT2TLwQsCaKkUddp"
    )
  );

  const nonceAccountPubkey = new PublicKey(
    "7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4"
  );
  let nonceAccountInfo = await connection.getAccountInfo(nonceAccountPubkey);
  let nonceAccount = NonceAccount.fromAccountData(nonceAccountInfo.data);

  let tx = new Transaction().add(
    // nonce advance must be the first instruction
    SystemProgram.nonceAdvance({
      noncePubkey: nonceAccountPubkey,
      authorizedPubkey: nonceAccountAuth.publicKey,
    }),
    // after that, you do what you really want to do, here we append a transfer instruction as an example.
    SystemProgram.transfer({
      fromPubkey: feePayer.publicKey,
      toPubkey: nonceAccountAuth.publicKey,
      lamports: 1,
    })
  );
  // assign `nonce` as recentBlockhash
  tx.recentBlockhash = nonceAccount.nonce;
  tx.feePayer = feePayer.publicKey;
  tx.sign(
    feePayer,
    nonceAccountAuth
  ); /* fee payer + nonce account authority + ... */

  console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);
})();
Last Updated:
Contributors: mh