Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: backport serialization fixes #10

Merged
merged 3 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3518,6 +3518,8 @@ export class Connection {
transaction = transactionOrMessage;
} else {
transaction = Transaction.populate(transactionOrMessage);
// HACK: this function relies on mutating the populated transaction
transaction._message = transaction._json = undefined;
}

if (transaction.nonceInfo && signers) {
Expand Down
4 changes: 4 additions & 0 deletions src/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export class PublicKey extends Struct {
return bs58.encode(this.toBytes());
}

toJSON(): string {
return this.toBase58();
}

/**
* Return the byte array representation of the public key
*/
Expand Down
82 changes: 82 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ export type SerializeConfig = {
verifySignatures?: boolean;
};

/**
* @internal
*/
export interface TransactionInstructionJSON {
keys: {
pubkey: string;
isSigner: boolean;
isWritable: boolean;
}[];
programId: string;
data: number[];
}

/**
* Transaction Instruction class
*/
Expand Down Expand Up @@ -92,6 +105,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],
};
}
}

/**
Expand Down Expand Up @@ -127,6 +155,20 @@ export type NonceInformation = {
nonceInstruction: TransactionInstruction;
};

/**
* @internal
*/
export interface TransactionJSON {
recentBlockhash: string | null;
feePayer: string | null;
nonceInfo: {
nonce: string;
nonceInstruction: TransactionInstructionJSON;
} | null;
instructions: TransactionInstructionJSON[];
signers: string[];
}

/**
* Transaction class
*/
Expand Down Expand Up @@ -168,13 +210,43 @@ export class Transaction {
*/
nonceInfo?: NonceInformation;

/**
* @internal
*/
_message?: Message;

/**
* @internal
*/
_json?: TransactionJSON;

/**
* Construct an empty Transaction
*/
constructor(opts?: TransactionCtorFields) {
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()),
signers: this.signatures.map(({publicKey}) => {
return publicKey.toJSON();
}),
};
}

/**
* Add one or more instructions to this Transaction
*/
Expand Down Expand Up @@ -203,6 +275,13 @@ export class Transaction {
* Compile transaction data
*/
compileMessage(): Message {
if (
this._message &&
JSON.stringify(this.toJSON()) === JSON.stringify(this._json)
) {
return this._message;
}

const {nonceInfo} = this;
if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) {
this.recentBlockhash = nonceInfo.nonce;
Expand Down Expand Up @@ -708,6 +787,9 @@ export class Transaction {
);
});

transaction._message = message;
transaction._json = transaction.toJSON();

return transaction;
}
}
140 changes: 135 additions & 5 deletions test/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {expect} from 'chai';

import {Keypair} from '../src/keypair';
import {PublicKey} from '../src/publickey';
import {Transaction} from '../src/transaction';
import {Transaction, TransactionInstruction} from '../src/transaction';
import {StakeProgram} from '../src/stake-program';
import {SystemProgram} from '../src/system-program';
import {Message} from '../src/message';
Expand Down Expand Up @@ -341,14 +341,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', () => {
Expand Down Expand Up @@ -387,6 +387,54 @@ describe('Transaction', () => {
expect(transaction.recentBlockhash).to.eq(recentBlockhash);
});

it('populate then compile transaction', () => {
const recentBlockhash = new PublicKey(1).toString();
const message = new Message({
accountKeys: [
new PublicKey(1).toString(),
new PublicKey(2).toString(),
new PublicKey(3).toString(),
new PublicKey(4).toString(),
new PublicKey(5).toString(),
],
header: {
numReadonlySignedAccounts: 0,
numReadonlyUnsignedAccounts: 3,
numRequiredSignatures: 2,
},
instructions: [
{
accounts: [1, 2, 3],
data: bs58.encode(Buffer.alloc(5).fill(9)),
programIdIndex: 2,
},
],
recentBlockhash,
});

const signatures = [
bs58.encode(Buffer.alloc(64).fill(1)),
bs58.encode(Buffer.alloc(64).fill(2)),
];

const transaction = Transaction.populate(message, signatures);
const compiledMessage = transaction.compileMessage();
expect(compiledMessage).to.eql(message);

// show that without caching the message, the populated message
// might not be the same when re-compiled
transaction._message = undefined;
const compiledMessage2 = transaction.compileMessage();
expect(compiledMessage2).not.to.eql(message);

// show that even if message is cached, transaction may still
// be modified
transaction._message = message;
transaction.recentBlockhash = new PublicKey(100).toString();
const compiledMessage3 = transaction.compileMessage();
expect(compiledMessage3).not.to.eql(message);
});

it('serialize unsigned transaction', () => {
const sender = Keypair.fromSeed(Uint8Array.from(Array(32).fill(8))); // Arbitrary known account
const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash
Expand Down Expand Up @@ -505,4 +553,86 @@ describe('Transaction', () => {
tx.addSignature(from.publicKey, toBuffer(signature));
expect(tx.verifySignatures()).to.be.true;
});

it('can serialize, deserialize, and reserialize with a partial signer', () => {
const signer = Keypair.generate();
const acc0Writable = Keypair.generate();
const acc1Writable = Keypair.generate();
const acc2Writable = Keypair.generate();
const t0 = new Transaction({
recentBlockhash: 'HZaTsZuhN1aaz9WuuimCFMyH7wJ5xiyMUHFCnZSMyguH',
feePayer: signer.publicKey,
});
t0.add(
new TransactionInstruction({
keys: [
{
pubkey: signer.publicKey,
isWritable: true,
isSigner: true,
},
{
pubkey: acc0Writable.publicKey,
isWritable: true,
isSigner: false,
},
],
programId: Keypair.generate().publicKey,
}),
);
t0.add(
new TransactionInstruction({
keys: [
{
pubkey: acc1Writable.publicKey,
isWritable: false,
isSigner: false,
},
],
programId: Keypair.generate().publicKey,
}),
);
t0.add(
new TransactionInstruction({
keys: [
{
pubkey: acc2Writable.publicKey,
isWritable: true,
isSigner: false,
},
],
programId: Keypair.generate().publicKey,
}),
);
t0.add(
new TransactionInstruction({
keys: [
{
pubkey: signer.publicKey,
isWritable: true,
isSigner: true,
},
{
pubkey: acc0Writable.publicKey,
isWritable: false,
isSigner: false,
},
{
pubkey: acc2Writable.publicKey,
isWritable: false,
isSigner: false,
},
{
pubkey: acc1Writable.publicKey,
isWritable: true,
isSigner: false,
},
],
programId: Keypair.generate().publicKey,
}),
);
const t1 = Transaction.from(t0.serialize({requireAllSignatures: false}));
t1.partialSign(signer);
t1.serialize();
});
});