Simple versioned transaction support (#2427)

This commit is contained in:
Henry-E 2023-03-07 15:08:29 +00:00 committed by GitHub
parent 1dc16d6642
commit 30083bd83c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 286 additions and 58 deletions

View File

@ -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 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)). - 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)). - 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)). - cli: Add `--arch sbf` option to compile programs using `cargo build-sbf` ([#2398](https://github.com/coral-xyz/anchor/pull/2398)).
### Fixes ### Fixes

View File

@ -6,6 +6,8 @@ import {
SystemProgram, SystemProgram,
Message, Message,
VersionedTransaction, VersionedTransaction,
AddressLookupTableProgram,
TransactionMessage,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { import {
TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID,
@ -61,6 +63,151 @@ const miscTest = (
assert.strictEqual(dataAccount.data, 99); 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<typeof provider.sendAndConfirm>;
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 () => { it("Can embed programs into genesis from the Anchor.toml", async () => {
const pid = new anchor.web3.PublicKey( const pid = new anchor.web3.PublicKey(
"FtMNMKp9DZHKWUyVAsj3Q5QV8ow4P3fUPP7ZrWEQJzKr" "FtMNMKp9DZHKWUyVAsj3Q5QV8ow4P3fUPP7ZrWEQJzKr"

View File

@ -1,6 +1,12 @@
import { Buffer } from "buffer"; 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 { Wallet } from "./provider";
import { isVersionedTransaction } from "./utils/common.js";
/** /**
* Node only wallet. * Node only wallet.
@ -30,14 +36,27 @@ export default class NodeWallet implements Wallet {
return new NodeWallet(payer); return new NodeWallet(payer);
} }
async signTransaction(tx: Transaction): Promise<Transaction> { async signTransaction<T extends Transaction | VersionedTransaction>(
tx: T
): Promise<T> {
if (isVersionedTransaction(tx)) {
tx.sign([this.payer]);
} else {
tx.partialSign(this.payer); tx.partialSign(this.payer);
}
return tx; return tx;
} }
async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> { async signAllTransactions<T extends Transaction | VersionedTransaction>(
txs: T[]
): Promise<T[]> {
return txs.map((t) => { return txs.map((t) => {
if (isVersionedTransaction(t)) {
t.sign([this.payer]);
} else {
t.partialSign(this.payer); t.partialSign(this.payer);
}
return t; return t;
}); });
} }

View File

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

View File

@ -1,3 +1,5 @@
import { Transaction, VersionedTransaction } from "@solana/web3.js";
/** /**
* Returns true if being run inside a web browser, * Returns true if being run inside a web browser,
* false if in a Node process or electron app. * false if in a Node process or electron app.
@ -18,3 +20,15 @@ export function chunks<T>(array: T[], size: number): T[][] {
(_, index) => array.slice(index * size, (index + 1) * size) (_, 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;
};