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:
parent
1a91621c29
commit
49d3d79459
|
@ -439,15 +439,44 @@ const VersionResult = pick({
|
||||||
'feature-set': optional(number()),
|
'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 = {
|
export type SimulatedTransactionResponse = {
|
||||||
err: TransactionError | string | null;
|
err: TransactionError | string | null;
|
||||||
logs: Array<string> | null;
|
logs: Array<string> | null;
|
||||||
|
accounts?: SimulatedTransactionAccountInfo[] | null;
|
||||||
|
unitsConsumed?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SimulatedTransactionResponseStruct = jsonRpcResultAndContext(
|
const SimulatedTransactionResponseStruct = jsonRpcResultAndContext(
|
||||||
pick({
|
pick({
|
||||||
err: nullable(union([pick({}), string()])),
|
err: nullable(union([pick({}), string()])),
|
||||||
logs: nullable(array(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;
|
lamports: number;
|
||||||
/** Optional data assigned to the account */
|
/** Optional data assigned to the account */
|
||||||
data: T;
|
data: T;
|
||||||
|
/** Optional rent epoch infor for account */
|
||||||
|
rentEpoch?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3430,9 +3461,17 @@ export class Connection {
|
||||||
* Simulate a transaction
|
* Simulate a transaction
|
||||||
*/
|
*/
|
||||||
async simulateTransaction(
|
async simulateTransaction(
|
||||||
transaction: Transaction,
|
transactionOrMessage: Transaction | Message,
|
||||||
signers?: Array<Signer>,
|
signers?: Array<Signer>,
|
||||||
|
includeAccounts?: boolean | Array<PublicKey>,
|
||||||
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
|
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
|
||||||
|
let transaction;
|
||||||
|
if (transactionOrMessage instanceof Transaction) {
|
||||||
|
transaction = transactionOrMessage;
|
||||||
|
} else {
|
||||||
|
transaction = Transaction.populate(transactionOrMessage);
|
||||||
|
}
|
||||||
|
|
||||||
if (transaction.nonceInfo && signers) {
|
if (transaction.nonceInfo && signers) {
|
||||||
transaction.sign(...signers);
|
transaction.sign(...signers);
|
||||||
} else {
|
} 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 wireTransaction = transaction._serialize(signData);
|
||||||
const encodedTransaction = wireTransaction.toString('base64');
|
const encodedTransaction = wireTransaction.toString('base64');
|
||||||
const config: any = {
|
const config: any = {
|
||||||
|
@ -3474,6 +3514,19 @@ export class Connection {
|
||||||
commitment: this.commitment,
|
commitment: this.commitment,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (includeAccounts) {
|
||||||
|
const addresses = (
|
||||||
|
Array.isArray(includeAccounts)
|
||||||
|
? includeAccounts
|
||||||
|
: message.nonProgramIds()
|
||||||
|
).map(key => key.toBase58());
|
||||||
|
|
||||||
|
config['accounts'] = {
|
||||||
|
encoding: 'base64',
|
||||||
|
addresses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (signers) {
|
if (signers) {
|
||||||
config.sigVerify = true;
|
config.sigVerify = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,11 +65,26 @@ export class Message {
|
||||||
recentBlockhash: Blockhash;
|
recentBlockhash: Blockhash;
|
||||||
instructions: CompiledInstruction[];
|
instructions: CompiledInstruction[];
|
||||||
|
|
||||||
|
private indexToProgramIds: Map<number, PublicKey> = new Map<
|
||||||
|
number,
|
||||||
|
PublicKey
|
||||||
|
>();
|
||||||
|
|
||||||
constructor(args: MessageArgs) {
|
constructor(args: MessageArgs) {
|
||||||
this.header = args.header;
|
this.header = args.header;
|
||||||
this.accountKeys = args.accountKeys.map(account => new PublicKey(account));
|
this.accountKeys = args.accountKeys.map(account => new PublicKey(account));
|
||||||
this.recentBlockhash = args.recentBlockhash;
|
this.recentBlockhash = args.recentBlockhash;
|
||||||
this.instructions = args.instructions;
|
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 {
|
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 {
|
serialize(): Buffer {
|
||||||
const numKeys = this.accountKeys.length;
|
const numKeys = this.accountKeys.length;
|
||||||
|
|
||||||
|
|
|
@ -666,7 +666,10 @@ export class Transaction {
|
||||||
/**
|
/**
|
||||||
* Populate Transaction object from message and signatures
|
* 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();
|
const transaction = new Transaction();
|
||||||
transaction.recentBlockhash = message.recentBlockhash;
|
transaction.recentBlockhash = message.recentBlockhash;
|
||||||
if (message.header.numRequiredSignatures > 0) {
|
if (message.header.numRequiredSignatures > 0) {
|
||||||
|
@ -688,9 +691,10 @@ export class Transaction {
|
||||||
const pubkey = message.accountKeys[account];
|
const pubkey = message.accountKeys[account];
|
||||||
return {
|
return {
|
||||||
pubkey,
|
pubkey,
|
||||||
isSigner: transaction.signatures.some(
|
isSigner:
|
||||||
|
transaction.signatures.some(
|
||||||
keyObj => keyObj.publicKey.toString() === pubkey.toString(),
|
keyObj => keyObj.publicKey.toString() === pubkey.toString(),
|
||||||
),
|
) || message.isAccountSigner(account),
|
||||||
isWritable: message.isAccountWritable(account),
|
isWritable: message.isAccountWritable(account),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
StakeProgram,
|
StakeProgram,
|
||||||
sendAndConfirmTransaction,
|
sendAndConfirmTransaction,
|
||||||
Keypair,
|
Keypair,
|
||||||
|
Message,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
import invariant from '../src/util/assert';
|
import invariant from '../src/util/assert';
|
||||||
import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing';
|
import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing';
|
||||||
|
@ -2818,6 +2819,65 @@ describe('Connection', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.env.TEST_LIVE) {
|
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 () => {
|
it('transaction', async () => {
|
||||||
connection._commitment = 'confirmed';
|
connection._commitment = 'confirmed';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue