From 21dacefbee0c6c92b3dcc1be338e5db1b5b45c08 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Sat, 16 Apr 2022 14:28:57 -0500 Subject: [PATCH] fix: transactions populated from RPC requests retain original account key order (#23720) * fix: transaction populate * chore: web3: fix tx serialization test * chore: web3: run prettier * fix: web3: transaction populate * fix: web3: handle nonce info * add hash calc config.use_write_cache (#24005) * restore existing overlapping overflow (#24010) * Stringify populated transaction fields * fix: web3: compare stringified JSON * chore: web3: remove eslint indent rule that conflicts with prettier * fix: web3: explicitly call toJSON * fix: web3: add test for compileMessage * fix: web3: make JSON internal * fix: web3: connection simulation from message relies on mutating transaction Co-authored-by: Jeff Washington (jwash) Co-authored-by: Jack May Co-authored-by: Justin Starry --- web3.js/.eslintrc.js | 8 --- web3.js/src/connection.ts | 2 + web3.js/src/transaction.ts | 85 ++++++++++++++++++++++++++++++++ web3.js/test/transaction.test.ts | 13 +++-- 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/web3.js/.eslintrc.js b/web3.js/.eslintrc.js index ac7aa65f2..b8439079e 100644 --- a/web3.js/.eslintrc.js +++ b/web3.js/.eslintrc.js @@ -31,14 +31,6 @@ module.exports = { 'newlines-between': 'always', }, ], - indent: [ - 'error', - 2, - { - MemberExpression: 1, - SwitchCase: 1, - }, - ], 'linebreak-style': ['error', 'unix'], 'no-console': [0], 'no-trailing-spaces': ['error'], diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 204d259d5..31036f023 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -3895,6 +3895,8 @@ export class Connection { transaction.instructions = transactionOrMessage.instructions; } else { transaction = Transaction.populate(transactionOrMessage); + // HACK: this function relies on mutating the populated transaction + transaction._message = transaction._json = undefined; } if (transaction.nonceInfo && signers) { diff --git a/web3.js/src/transaction.ts b/web3.js/src/transaction.ts index b3e4ec9ca..32dbbe613 100644 --- a/web3.js/src/transaction.ts +++ b/web3.js/src/transaction.ts @@ -66,6 +66,19 @@ export type SerializeConfig = { verifySignatures?: boolean; }; +/** + * @internal + */ +export interface TransactionInstructionJSON { + keys: { + pubkey: string; + isSigner: boolean; + isWritable: boolean; + }[]; + programId: string; + data: number[]; +} + /** * Transaction Instruction class */ @@ -93,6 +106,21 @@ export class TransactionInstruction { this.data = opts.data; } } + + /** + * @internal + */ + toJSON(): TransactionInstructionJSON { + return { + keys: this.keys.map(({pubkey, isSigner, isWritable}) => ({ + pubkey: pubkey.toJSON(), + isSigner, + isWritable, + })), + programId: this.programId.toJSON(), + data: [...this.data], + }; + } } /** @@ -128,6 +156,20 @@ export type NonceInformation = { nonceInstruction: TransactionInstruction; }; +/** + * @internal + */ +export interface TransactionJSON { + recentBlockhash: string | null; + feePayer: string | null; + nonceInfo: { + nonce: string; + nonceInstruction: TransactionInstructionJSON; + } | null; + instructions: TransactionInstructionJSON[]; + signatures: {publicKey: string; signature: number[] | null}[]; +} + /** * Transaction class */ @@ -169,6 +211,16 @@ export class Transaction { */ nonceInfo?: NonceInformation; + /** + * @internal + */ + _message?: Message; + + /** + * @internal + */ + _json?: TransactionJSON; + /** * Construct an empty Transaction */ @@ -176,6 +228,27 @@ export class Transaction { opts && Object.assign(this, opts); } + /** + * @internal + */ + toJSON(): TransactionJSON { + return { + recentBlockhash: this.recentBlockhash || null, + feePayer: this.feePayer ? this.feePayer.toJSON() : null, + nonceInfo: this.nonceInfo + ? { + nonce: this.nonceInfo.nonce, + nonceInstruction: this.nonceInfo.nonceInstruction.toJSON(), + } + : null, + instructions: this.instructions.map(instruction => instruction.toJSON()), + signatures: this.signatures.map(({publicKey, signature}) => ({ + publicKey: publicKey.toJSON(), + signature: signature ? [...signature] : null, + })), + }; + } + /** * Add one or more instructions to this Transaction */ @@ -204,6 +277,15 @@ export class Transaction { * Compile transaction data */ compileMessage(): Message { + if (this._message) { + if (JSON.stringify(this.toJSON()) !== JSON.stringify(this._json)) { + throw new Error( + 'Transaction mutated after being populated from Message', + ); + } + return this._message; + } + const {nonceInfo} = this; if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) { this.recentBlockhash = nonceInfo.nonce; @@ -719,6 +801,9 @@ export class Transaction { ); }); + transaction._message = message; + transaction._json = transaction.toJSON(); + return transaction; } } diff --git a/web3.js/test/transaction.test.ts b/web3.js/test/transaction.test.ts index de9450c40..144932371 100644 --- a/web3.js/test/transaction.test.ts +++ b/web3.js/test/transaction.test.ts @@ -365,14 +365,14 @@ describe('Transaction', () => { }).add(transfer); expectedTransaction.sign(sender); - const wireTransaction = Buffer.from( + const serializedTransaction = Buffer.from( 'AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=', 'base64', ); - const tx = Transaction.from(wireTransaction); + const deserializedTransaction = Transaction.from(serializedTransaction); - expect(tx).to.eql(expectedTransaction); - expect(wireTransaction).to.eql(expectedTransaction.serialize()); + expect(expectedTransaction.serialize()).to.eql(serializedTransaction); + expect(deserializedTransaction.serialize()).to.eql(serializedTransaction); }); it('populate transaction', () => { @@ -409,6 +409,11 @@ describe('Transaction', () => { expect(transaction.instructions).to.have.length(1); expect(transaction.signatures).to.have.length(2); expect(transaction.recentBlockhash).to.eq(recentBlockhash); + + transaction.feePayer = new PublicKey(6); + expect(() => transaction.compileMessage()).to.throw( + 'Transaction mutated after being populated from Message', + ); }); it('serialize unsigned transaction', () => {