diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index ba947633d9..1cf5e2ebfd 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -439,15 +439,44 @@ const VersionResult = pick({ 'feature-set': optional(number()), }); +export type SimulatedTransactionAccountInfo = { + /** `true` if this account's data contains a loaded program */ + executable: boolean; + /** Identifier of the program that owns the account */ + owner: string; + /** Number of lamports assigned to the account */ + lamports: number; + /** Optional data assigned to the account */ + data: string[]; + /** Optional rent epoch info for account */ + rentEpoch?: number; +}; + export type SimulatedTransactionResponse = { err: TransactionError | string | null; logs: Array | null; + accounts?: SimulatedTransactionAccountInfo[] | null; + unitsConsumed?: number; }; const SimulatedTransactionResponseStruct = jsonRpcResultAndContext( pick({ err: nullable(union([pick({}), string()])), logs: nullable(array(string())), + accounts: optional( + nullable( + array( + pick({ + executable: boolean(), + owner: string(), + lamports: number(), + data: array(string()), + rentEpoch: optional(number()), + }), + ), + ), + ), + unitsConsumed: optional(number()), }), ); @@ -1679,6 +1708,8 @@ export type AccountInfo = { lamports: number; /** Optional data assigned to the account */ data: T; + /** Optional rent epoch infor for account */ + rentEpoch?: number; }; /** @@ -3430,9 +3461,17 @@ export class Connection { * Simulate a transaction */ async simulateTransaction( - transaction: Transaction, + transactionOrMessage: Transaction | Message, signers?: Array, + includeAccounts?: boolean | Array, ): Promise> { + let transaction; + if (transactionOrMessage instanceof Transaction) { + transaction = transactionOrMessage; + } else { + transaction = Transaction.populate(transactionOrMessage); + } + if (transaction.nonceInfo && signers) { transaction.sign(...signers); } else { @@ -3466,7 +3505,8 @@ export class Connection { } } - const signData = transaction.serializeMessage(); + const message = transaction._compile(); + const signData = message.serialize(); const wireTransaction = transaction._serialize(signData); const encodedTransaction = wireTransaction.toString('base64'); const config: any = { @@ -3474,6 +3514,19 @@ export class Connection { commitment: this.commitment, }; + if (includeAccounts) { + const addresses = ( + Array.isArray(includeAccounts) + ? includeAccounts + : message.nonProgramIds() + ).map(key => key.toBase58()); + + config['accounts'] = { + encoding: 'base64', + addresses, + }; + } + if (signers) { config.sigVerify = true; } diff --git a/web3.js/src/message.ts b/web3.js/src/message.ts index f9c981d2fb..48f49a127c 100644 --- a/web3.js/src/message.ts +++ b/web3.js/src/message.ts @@ -65,11 +65,26 @@ export class Message { recentBlockhash: Blockhash; instructions: CompiledInstruction[]; + private indexToProgramIds: Map = new Map< + number, + PublicKey + >(); + constructor(args: MessageArgs) { this.header = args.header; this.accountKeys = args.accountKeys.map(account => new PublicKey(account)); this.recentBlockhash = args.recentBlockhash; this.instructions = args.instructions; + this.instructions.forEach(ix => + this.indexToProgramIds.set( + ix.programIdIndex, + this.accountKeys[ix.programIdIndex], + ), + ); + } + + isAccountSigner(index: number): boolean { + return index < this.header.numRequiredSignatures; } isAccountWritable(index: number): boolean { @@ -83,6 +98,18 @@ export class Message { ); } + isProgramId(index: number): boolean { + return this.indexToProgramIds.has(index); + } + + programIds(): PublicKey[] { + return [...this.indexToProgramIds.values()]; + } + + nonProgramIds(): PublicKey[] { + return this.accountKeys.filter((_, index) => !this.isProgramId(index)); + } + serialize(): Buffer { const numKeys = this.accountKeys.length; diff --git a/web3.js/src/transaction.ts b/web3.js/src/transaction.ts index 48c1dca024..5466464f5a 100644 --- a/web3.js/src/transaction.ts +++ b/web3.js/src/transaction.ts @@ -666,7 +666,10 @@ export class Transaction { /** * Populate Transaction object from message and signatures */ - static populate(message: Message, signatures: Array): Transaction { + static populate( + message: Message, + signatures: Array = [], + ): Transaction { const transaction = new Transaction(); transaction.recentBlockhash = message.recentBlockhash; if (message.header.numRequiredSignatures > 0) { @@ -688,9 +691,10 @@ export class Transaction { const pubkey = message.accountKeys[account]; return { pubkey, - isSigner: transaction.signatures.some( - keyObj => keyObj.publicKey.toString() === pubkey.toString(), - ), + isSigner: + transaction.signatures.some( + keyObj => keyObj.publicKey.toString() === pubkey.toString(), + ) || message.isAccountSigner(account), isWritable: message.isAccountWritable(account), }; }); diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index 91c2a248cd..10135be180 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -17,6 +17,7 @@ import { StakeProgram, sendAndConfirmTransaction, Keypair, + Message, } from '../src'; import invariant from '../src/util/assert'; import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing'; @@ -2818,6 +2819,65 @@ describe('Connection', () => { }); if (process.env.TEST_LIVE) { + it('simulate transaction with message', async () => { + connection._commitment = 'confirmed'; + + const account1 = Keypair.generate(); + const account2 = Keypair.generate(); + + await helpers.airdrop({ + connection, + address: account1.publicKey, + amount: LAMPORTS_PER_SOL, + }); + + await helpers.airdrop({ + connection, + address: account2.publicKey, + amount: LAMPORTS_PER_SOL, + }); + + const recentBlockhash = await ( + await helpers.recentBlockhash({connection}) + ).blockhash; + const message = new Message({ + accountKeys: [ + account1.publicKey.toString(), + account2.publicKey.toString(), + 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo', + ], + header: { + numReadonlySignedAccounts: 1, + numReadonlyUnsignedAccounts: 2, + numRequiredSignatures: 1, + }, + instructions: [ + { + accounts: [0, 1], + data: bs58.encode(Buffer.alloc(5).fill(9)), + programIdIndex: 2, + }, + ], + recentBlockhash, + }); + + const results1 = await connection.simulateTransaction( + message, + [account1], + true, + ); + + expect(results1.value.accounts).lengthOf(2); + + const results2 = await connection.simulateTransaction( + message, + [account1], + [account1.publicKey], + ); + + expect(results2.value.accounts).lengthOf(1); + }).timeout(10000); + it('transaction', async () => { connection._commitment = 'confirmed';