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) <wash678@gmail.com>
Co-authored-by: Jack May <jack@solana.com>
Co-authored-by: Justin Starry <justin@solana.com>
This commit is contained in:
Jordan Sexton 2022-04-16 14:28:57 -05:00 committed by GitHub
parent 6c5a3ca4a0
commit 21dacefbee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 12 deletions

View File

@ -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'],

View File

@ -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) {

View File

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

View File

@ -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', () => {