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()),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue