diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5ed5183c..c66610b5e9 100644 --- a/CHANGELOG.md +++ b/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` ([2427](https://github.com/coral-xyz/anchor/pull/2427)). - cli: Add `--arch sbf` option to compile programs using `cargo build-sbf` ([#2398](https://github.com/coral-xyz/anchor/pull/2398)). ### Fixes diff --git a/tests/misc/tests/misc/misc.ts b/tests/misc/tests/misc/misc.ts index 18f143929a..9a9c38e3a0 100644 --- a/tests/misc/tests/misc/misc.ts +++ b/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,151 @@ 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() + ); + type SendParams = Parameters; + const testThis: SendParams = [ + 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 = 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 send VersionedTransaction with extra signatures", async () => { + // Test sending with signatures + const initSpace = 100; + const rentExemptAmount = + await provider.connection.getMinimumBalanceForRentExemption(initSpace); + + const newAccount = Keypair.generate(); + let createAccountIx = SystemProgram.createAccount({ + fromPubkey: provider.publicKey, + lamports: rentExemptAmount, + newAccountPubkey: newAccount.publicKey, + programId: program.programId, + space: initSpace, + }); + let createAccountTx = new VersionedTransaction( + new TransactionMessage({ + instructions: [createAccountIx], + payerKey: provider.publicKey, + recentBlockhash: (await provider.connection.getLatestBlockhash()) + .blockhash, + }).compileToV0Message() + ); + await provider.simulate(createAccountTx, [], "processed"); + await provider.sendAndConfirm(createAccountTx, [newAccount], { + skipPreflight: false, + commitment: "confirmed", + }); + let newAccountInfo = await provider.connection.getAccountInfo( + newAccount.publicKey + ); + assert.strictEqual( + newAccountInfo.owner.toBase58(), + program.programId.toBase58() + ); + }); + it("Can embed programs into genesis from the Anchor.toml", async () => { const pid = new anchor.web3.PublicKey( "FtMNMKp9DZHKWUyVAsj3Q5QV8ow4P3fUPP7ZrWEQJzKr" diff --git a/ts/packages/anchor/src/nodewallet.ts b/ts/packages/anchor/src/nodewallet.ts index 6d7924aff3..8580a55d2c 100644 --- a/ts/packages/anchor/src/nodewallet.ts +++ b/ts/packages/anchor/src/nodewallet.ts @@ -1,6 +1,12 @@ 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"; +import { isVersionedTransaction } from "./utils/common.js"; /** * Node only wallet. @@ -30,14 +36,27 @@ export default class NodeWallet implements Wallet { return new NodeWallet(payer); } - async signTransaction(tx: Transaction): Promise { - tx.partialSign(this.payer); + async signTransaction( + tx: T + ): Promise { + if (isVersionedTransaction(tx)) { + tx.sign([this.payer]); + } else { + tx.partialSign(this.payer); + } + return tx; } - async signAllTransactions(txs: Transaction[]): Promise { + async signAllTransactions( + txs: T[] + ): Promise { return txs.map((t) => { - t.partialSign(this.payer); + if (isVersionedTransaction(t)) { + t.sign([this.payer]); + } else { + t.partialSign(this.payer); + } return t; }); } diff --git a/ts/packages/anchor/src/provider.ts b/ts/packages/anchor/src/provider.ts index e0fd503d7b..02affbe0e1 100644 --- a/ts/packages/anchor/src/provider.ts +++ b/ts/packages/anchor/src/provider.ts @@ -9,9 +9,11 @@ import { Commitment, SendTransactionError, SendOptions, + VersionedTransaction, + RpcResponseAndContext, } from "@solana/web3.js"; import { bs58 } from "./utils/bytes/index.js"; -import { isBrowser } from "./utils/common.js"; +import { isBrowser, isVersionedTransaction } from "./utils/common.js"; import { simulateTransaction, SuccessfulTxSimulationResponse, @@ -22,21 +24,24 @@ export default interface Provider { readonly publicKey?: PublicKey; send?( - tx: Transaction, + tx: Transaction | VersionedTransaction, signers?: Signer[], opts?: SendOptions ): Promise; sendAndConfirm?( - tx: Transaction, + tx: Transaction | VersionedTransaction, signers?: Signer[], opts?: ConfirmOptions ): Promise; - sendAll?( - txWithSigners: { tx: Transaction; signers?: Signer[] }[], + sendAll?( + txWithSigners: { + tx: T; + signers?: Signer[]; + }[], opts?: ConfirmOptions ): Promise>; 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 { @@ -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 (isVersionedTransaction(tx)) { + 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( + isVersionedTransaction(tx) + ? 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( + txWithSigners: { + tx: T; + signers?: Signer[]; + }[], opts?: ConfirmOptions ): Promise> { 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 (isVersionedTransaction(r.tx)) { + 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( + isVersionedTransaction(tx) + ? 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 { - 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; + if (isVersionedTransaction(tx)) { + 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); @@ -301,10 +343,15 @@ export type SendTxRequest = { /** * Wallet interface for objects that can be used to sign provider transactions. + * VersionedTransactions sign everything at once */ export interface Wallet { - signTransaction(tx: Transaction): Promise; - signAllTransactions(txs: Transaction[]): Promise; + signTransaction( + tx: T + ): Promise; + signAllTransactions( + txs: T[] + ): Promise; publicKey: PublicKey; } @@ -312,7 +359,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 { const sendOptions = options && { diff --git a/ts/packages/anchor/src/utils/common.ts b/ts/packages/anchor/src/utils/common.ts index c1793d5a18..3f86944d0d 100644 --- a/ts/packages/anchor/src/utils/common.ts +++ b/ts/packages/anchor/src/utils/common.ts @@ -1,3 +1,5 @@ +import { Transaction, VersionedTransaction } from "@solana/web3.js"; + /** * Returns true if being run inside a web browser, * false if in a Node process or electron app. @@ -18,3 +20,15 @@ export function chunks(array: T[], size: number): T[][] { (_, index) => array.slice(index * size, (index + 1) * size) ); } + +/** + * Check if a transaction object is a VersionedTransaction or not + * + * @param tx + * @returns bool + */ +export const isVersionedTransaction = ( + tx: Transaction | VersionedTransaction +): tx is VersionedTransaction => { + return "version" in tx; +};