web3.js: add accounts support to simulateTransaction (#19590)

* feat: add accounts support to simulateTransaction

* feat: introduce test for simulateTransaction on Message objects

* feat: populate transaction from message defaults to no signatures

* fix: remove unused constant

* fix: small formatting error

* fix: eslint and prettier were fighting over ternary indentation

* fix: make simulated transaction result accounts nullable
This commit is contained in:
Josh 2021-09-16 14:10:28 -07:00 committed by GitHub
parent 1a91621c29
commit 49d3d79459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 6 deletions

View File

@ -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<string> | 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<T> = {
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<Signer>,
includeAccounts?: boolean | Array<PublicKey>,
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
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;
}

View File

@ -65,11 +65,26 @@ export class Message {
recentBlockhash: Blockhash;
instructions: CompiledInstruction[];
private indexToProgramIds: Map<number, PublicKey> = 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;

View File

@ -666,7 +666,10 @@ export class Transaction {
/**
* Populate Transaction object from message and signatures
*/
static populate(message: Message, signatures: Array<string>): Transaction {
static populate(
message: Message,
signatures: Array<string> = [],
): 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),
};
});

View File

@ -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';