Kaynağa Gözat

Simple Versioned Transaction Support (#2407)

Henry-E 2 yıl önce
ebeveyn
işleme
064dde4b1a

+ 1 - 0
CHANGELOG.md

@@ -23,6 +23,7 @@ The minor version will be incremented upon a breaking change and the patch versi
 - client: Add support for multithreading to the rust client: use flag `--multithreaded` ([#2321](https://github.com/coral-xyz/anchor/pull/2321)).
 - client: Add `async_rpc` a method which returns a nonblocking solana rpc client ([2322](https://github.com/coral-xyz/anchor/pull/2322)).
 - avm, cli: Use the `rustls-tls` feature of `reqwest` so that users don't need OpenSSL installed ([#2385](https://github.com/coral-xyz/anchor/pull/2385)).
+- ts: Add `VersionedTransaction` support. Methods in the `Provider` class and `Wallet` interface now use the argument `tx: Transaction | VersionedTransaction` ([2407](https://github.com/coral-xyz/anchor/pull/2407)).
 - cli: Add `--arch sbf` option to compile programs using `cargo build-sbf` ([#2398](https://github.com/coral-xyz/anchor/pull/2398)).
 
 ### Fixes

+ 100 - 0
tests/misc/tests/misc/misc.ts

@@ -6,6 +6,8 @@ import {
   SystemProgram,
   Message,
   VersionedTransaction,
+  AddressLookupTableProgram,
+  TransactionMessage,
 } from "@solana/web3.js";
 import {
   TOKEN_PROGRAM_ID,
@@ -61,6 +63,104 @@ const miscTest = (
       assert.strictEqual(dataAccount.data, 99);
     });
 
+    it("Can send VersionedTransaction", async () => {
+      // Create the lookup table
+      const recentSlot = await provider.connection.getSlot();
+      const [loookupTableInstruction, lookupTableAddress] =
+        AddressLookupTableProgram.createLookupTable({
+          authority: provider.publicKey,
+          payer: provider.publicKey,
+          recentSlot,
+        });
+      const extendInstruction = AddressLookupTableProgram.extendLookupTable({
+        payer: provider.publicKey,
+        authority: provider.publicKey,
+        lookupTable: lookupTableAddress,
+        addresses: [provider.publicKey, SystemProgram.programId],
+      });
+      let createLookupTableTx = new VersionedTransaction(
+        new TransactionMessage({
+          instructions: [loookupTableInstruction, extendInstruction],
+          payerKey: program.provider.publicKey,
+          recentBlockhash: (await provider.connection.getLatestBlockhash())
+            .blockhash,
+        }).compileToV0Message()
+      );
+      await provider.sendAndConfirm(createLookupTableTx, [], {
+        skipPreflight: true,
+      });
+
+      // Use the lookup table in a transaction
+      const transferAmount = 1_000_000;
+      const lookupTableAccount = await provider.connection
+        .getAddressLookupTable(lookupTableAddress)
+        .then((res) => res.value);
+      const target = anchor.web3.Keypair.generate();
+      let transferInstruction = SystemProgram.transfer({
+        fromPubkey: provider.publicKey,
+        lamports: transferAmount,
+        toPubkey: target.publicKey,
+      });
+      let transferUsingLookupTx = new VersionedTransaction(
+        new TransactionMessage({
+          instructions: [transferInstruction],
+          payerKey: program.provider.publicKey,
+          recentBlockhash: (await provider.connection.getLatestBlockhash())
+            .blockhash,
+        }).compileToV0Message([lookupTableAccount])
+      );
+      await provider.simulate(transferUsingLookupTx, [], "processed");
+      await provider.sendAndConfirm(transferUsingLookupTx, [], {
+        skipPreflight: true,
+        commitment: "confirmed",
+      });
+      let newBalance = await provider.connection.getBalance(
+        target.publicKey,
+        "confirmed"
+      );
+      assert.strictEqual(newBalance, transferAmount);
+
+      // Test sendAll with versioned transaction
+      let oneTransferUsingLookupTx = new VersionedTransaction(
+        new TransactionMessage({
+          instructions: [
+            SystemProgram.transfer({
+              fromPubkey: provider.publicKey,
+              // Needed to make the transactions distinct
+              lamports: transferAmount + 1,
+              toPubkey: target.publicKey,
+            }),
+          ],
+          payerKey: program.provider.publicKey,
+          recentBlockhash: (await provider.connection.getLatestBlockhash())
+            .blockhash,
+        }).compileToV0Message([lookupTableAccount])
+      );
+      let twoTransferUsingLookupTx = new VersionedTransaction(
+        new TransactionMessage({
+          instructions: [
+            SystemProgram.transfer({
+              fromPubkey: provider.publicKey,
+              lamports: transferAmount,
+              toPubkey: target.publicKey,
+            }),
+          ],
+          payerKey: program.provider.publicKey,
+          recentBlockhash: (await provider.connection.getLatestBlockhash())
+            .blockhash,
+        }).compileToV0Message([lookupTableAccount])
+      );
+      await provider.sendAll(
+        [{ tx: oneTransferUsingLookupTx }, { tx: twoTransferUsingLookupTx }],
+        { skipPreflight: true, commitment: "confirmed" }
+      );
+      newBalance = await provider.connection.getBalance(
+        target.publicKey,
+        "confirmed"
+      );
+      assert.strictEqual(newBalance, transferAmount * 3 + 1);
+    });
+
     it("Can embed programs into genesis from the Anchor.toml", async () => {
       const pid = new anchor.web3.PublicKey(
         "FtMNMKp9DZHKWUyVAsj3Q5QV8ow4P3fUPP7ZrWEQJzKr"

+ 22 - 5
ts/packages/anchor/src/nodewallet.ts

@@ -1,5 +1,10 @@
 import { Buffer } from "buffer";
-import { Keypair, PublicKey, Transaction } from "@solana/web3.js";
+import {
+  Keypair,
+  PublicKey,
+  Transaction,
+  VersionedTransaction,
+} from "@solana/web3.js";
 import { Wallet } from "./provider";
 
 /**
@@ -30,14 +35,26 @@ export default class NodeWallet implements Wallet {
     return new NodeWallet(payer);
   }
 
-  async signTransaction(tx: Transaction): Promise<Transaction> {
-    tx.partialSign(this.payer);
+  async signTransaction<T extends Transaction | VersionedTransaction>(
+    tx: T
+  ): Promise<T> {
+    if (tx instanceof VersionedTransaction) {
+      tx.sign([this.payer]);
+    } else {
+      tx.partialSign(this.payer);
+    }
     return tx;
   }
 
-  async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
+  async signAllTransactions<T extends Transaction | VersionedTransaction>(
+    txs: T[]
+  ): Promise<T[]> {
     return txs.map((t) => {
-      t.partialSign(this.payer);
+      if (t instanceof VersionedTransaction) {
+        t.sign([this.payer]);
+      } else {
+        t.partialSign(this.payer);
+      }
       return t;
     });
   }

+ 99 - 53
ts/packages/anchor/src/provider.ts

@@ -9,6 +9,8 @@ import {
   Commitment,
   SendTransactionError,
   SendOptions,
+  VersionedTransaction,
+  RpcResponseAndContext,
 } from "@solana/web3.js";
 import { bs58 } from "./utils/bytes/index.js";
 import { isBrowser } from "./utils/common.js";
@@ -22,21 +24,24 @@ export default interface Provider {
   readonly publicKey?: PublicKey;
 
   send?(
-    tx: Transaction,
+    tx: Transaction | VersionedTransaction,
     signers?: Signer[],
     opts?: SendOptions
   ): Promise<TransactionSignature>;
   sendAndConfirm?(
-    tx: Transaction,
+    tx: Transaction | VersionedTransaction,
     signers?: Signer[],
     opts?: ConfirmOptions
   ): Promise<TransactionSignature>;
-  sendAll?(
-    txWithSigners: { tx: Transaction; signers?: Signer[] }[],
+  sendAll?<T extends Transaction | VersionedTransaction>(
+    txWithSigners: {
+      tx: T;
+      signers?: Signer[];
+    }[],
     opts?: ConfirmOptions
   ): Promise<Array<TransactionSignature>>;
   simulate?(
-    tx: Transaction,
+    tx: Transaction | VersionedTransaction,
     signers?: Signer[],
     commitment?: Commitment,
     includeAccounts?: boolean | PublicKey[]
@@ -124,7 +129,7 @@ export class AnchorProvider implements Provider {
    * @param opts    Transaction confirmation options.
    */
   async sendAndConfirm(
-    tx: Transaction,
+    tx: Transaction | VersionedTransaction,
     signers?: Signer[],
     opts?: ConfirmOptions
   ): Promise<TransactionSignature> {
@@ -132,17 +137,23 @@ export class AnchorProvider implements Provider {
       opts = this.opts;
     }
 
-    tx.feePayer = tx.feePayer || this.wallet.publicKey;
-
-    tx.recentBlockhash = (
-      await this.connection.getLatestBlockhash(opts.preflightCommitment)
-    ).blockhash;
-
+    if (tx instanceof VersionedTransaction) {
+      if (signers) {
+        tx.sign(signers);
+      }
+    } else {
+      tx.feePayer = tx.feePayer ?? this.wallet.publicKey;
+      tx.recentBlockhash = (
+        await this.connection.getLatestBlockhash(opts.preflightCommitment)
+      ).blockhash;
+
+      if (signers) {
+        for (const signer of signers) {
+          tx.partialSign(signer);
+        }
+      }
+    }
     tx = await this.wallet.signTransaction(tx);
-    (signers ?? []).forEach((kp) => {
-      tx.partialSign(kp);
-    });
-
     const rawTx = tx.serialize();
 
     try {
@@ -155,10 +166,14 @@ export class AnchorProvider implements Provider {
         // (the json RPC does not support any shorter than "confirmed" for 'getTransaction')
         // because that will see the tx sent with `sendAndConfirmRawTransaction` no matter which
         // commitment `sendAndConfirmRawTransaction` used
-        const failedTx = await this.connection.getTransaction(
-          bs58.encode(tx.signature!),
-          { commitment: "confirmed" }
+        const txSig = bs58.encode(
+          tx instanceof VersionedTransaction
+            ? tx.signatures?.[0] || new Uint8Array()
+            : tx.signature ?? new Uint8Array()
         );
+        const failedTx = await this.connection.getTransaction(txSig, {
+          commitment: "confirmed",
+        });
         if (!failedTx) {
           throw err;
         } else {
@@ -173,34 +188,44 @@ export class AnchorProvider implements Provider {
 
   /**
    * Similar to `send`, but for an array of transactions and signers.
+   * All transactions need to be of the same type, it doesn't support a mix of `VersionedTransaction`s and `Transaction`s.
    *
    * @param txWithSigners Array of transactions and signers.
    * @param opts          Transaction confirmation options.
    */
-  async sendAll(
-    txWithSigners: { tx: Transaction; signers?: Signer[] }[],
+  async sendAll<T extends Transaction | VersionedTransaction>(
+    txWithSigners: {
+      tx: T;
+      signers?: Signer[];
+    }[],
     opts?: ConfirmOptions
   ): Promise<Array<TransactionSignature>> {
     if (opts === undefined) {
       opts = this.opts;
     }
-    const blockhash = await this.connection.getLatestBlockhash(
-      opts.preflightCommitment
-    );
+    const recentBlockhash = (
+      await this.connection.getLatestBlockhash(opts.preflightCommitment)
+    ).blockhash;
 
     let txs = txWithSigners.map((r) => {
-      let tx = r.tx;
-      let signers = r.signers ?? [];
-
-      tx.feePayer = tx.feePayer || this.wallet.publicKey;
-
-      tx.recentBlockhash = blockhash.blockhash;
+      if (r.tx instanceof VersionedTransaction) {
+        let tx: VersionedTransaction = r.tx;
+        if (r.signers) {
+          tx.sign(r.signers);
+        }
+        return tx;
+      } else {
+        let tx: Transaction = r.tx;
+        let signers = r.signers ?? [];
 
-      signers.forEach((kp) => {
-        tx.partialSign(kp);
-      });
+        tx.feePayer = tx.feePayer ?? this.wallet.publicKey;
+        tx.recentBlockhash = recentBlockhash;
 
-      return tx;
+        signers.forEach((kp) => {
+          tx.partialSign(kp);
+        });
+        return tx;
+      }
     });
 
     const signedTxs = await this.wallet.signAllTransactions(txs);
@@ -223,10 +248,14 @@ export class AnchorProvider implements Provider {
           // (the json RPC does not support any shorter than "confirmed" for 'getTransaction')
           // because that will see the tx sent with `sendAndConfirmRawTransaction` no matter which
           // commitment `sendAndConfirmRawTransaction` used
-          const failedTx = await this.connection.getTransaction(
-            bs58.encode(tx.signature!),
-            { commitment: "confirmed" }
+          const txSig = bs58.encode(
+            tx instanceof VersionedTransaction
+              ? tx.signatures?.[0] || new Uint8Array()
+              : tx.signature ?? new Uint8Array()
           );
+          const failedTx = await this.connection.getTransaction(txSig, {
+            commitment: "confirmed",
+          });
           if (!failedTx) {
             throw err;
           } else {
@@ -253,29 +282,42 @@ export class AnchorProvider implements Provider {
    * @param opts    Transaction confirmation options.
    */
   async simulate(
-    tx: Transaction,
+    tx: Transaction | VersionedTransaction,
     signers?: Signer[],
     commitment?: Commitment,
     includeAccounts?: boolean | PublicKey[]
   ): Promise<SuccessfulTxSimulationResponse> {
-    tx.feePayer = tx.feePayer || this.wallet.publicKey;
-
-    tx.recentBlockhash = (
+    let recentBlockhash = (
       await this.connection.getLatestBlockhash(
         commitment ?? this.connection.commitment
       )
     ).blockhash;
 
-    if (signers) {
-      tx = await this.wallet.signTransaction(tx);
+    let result: RpcResponseAndContext<SimulatedTransactionResponse>;
+    if (tx instanceof VersionedTransaction) {
+      if (signers) {
+        tx.sign(signers);
+        tx = await this.wallet.signTransaction(tx);
+      }
+
+      // Doesn't support includeAccounts which has been changed to something
+      // else in later versions of this function.
+      result = await this.connection.simulateTransaction(tx, { commitment });
+    } else {
+      tx.feePayer = tx.feePayer || this.wallet.publicKey;
+      tx.recentBlockhash = recentBlockhash;
+
+      if (signers) {
+        tx = await this.wallet.signTransaction(tx);
+      }
+      result = await simulateTransaction(
+        this.connection,
+        tx,
+        signers,
+        commitment,
+        includeAccounts
+      );
     }
-    const result = await simulateTransaction(
-      this.connection,
-      tx,
-      signers,
-      commitment,
-      includeAccounts
-    );
 
     if (result.value.err) {
       throw new SimulateError(result.value);
@@ -303,8 +345,12 @@ export type SendTxRequest = {
  * Wallet interface for objects that can be used to sign provider transactions.
  */
 export interface Wallet {
-  signTransaction(tx: Transaction): Promise<Transaction>;
-  signAllTransactions(txs: Transaction[]): Promise<Transaction[]>;
+  signTransaction<T extends Transaction | VersionedTransaction>(
+    tx: T
+  ): Promise<T>;
+  signAllTransactions<T extends Transaction | VersionedTransaction>(
+    txs: T[]
+  ): Promise<T[]>;
   publicKey: PublicKey;
 }
 
@@ -312,7 +358,7 @@ export interface Wallet {
 // a better error if 'confirmTransaction` returns an error status
 async function sendAndConfirmRawTransaction(
   connection: Connection,
-  rawTransaction: Buffer,
+  rawTransaction: Buffer | Uint8Array,
   options?: ConfirmOptions
 ): Promise<TransactionSignature> {
   const sendOptions = options && {