fix: support serialization of partially signed transactions
This commit is contained in:
committed by
mergify[bot]
parent
4bb6c2fffb
commit
a59d305e09
7
web3.js/module.d.ts
vendored
7
web3.js/module.d.ts
vendored
@ -617,6 +617,11 @@ declare module '@solana/web3.js' {
|
|||||||
signatures?: Array<SignaturePubkeyPair>;
|
signatures?: Array<SignaturePubkeyPair>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SerializeConfig = {
|
||||||
|
requireAllSignatures?: boolean;
|
||||||
|
verifySignatures?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export class Transaction {
|
export class Transaction {
|
||||||
signatures: Array<SignaturePubkeyPair>;
|
signatures: Array<SignaturePubkeyPair>;
|
||||||
signature?: Buffer;
|
signature?: Buffer;
|
||||||
@ -640,7 +645,7 @@ declare module '@solana/web3.js' {
|
|||||||
addSignature(pubkey: PublicKey, signature: Buffer): void;
|
addSignature(pubkey: PublicKey, signature: Buffer): void;
|
||||||
setSigners(...signer: Array<PublicKey>): void;
|
setSigners(...signer: Array<PublicKey>): void;
|
||||||
verifySignatures(): boolean;
|
verifySignatures(): boolean;
|
||||||
serialize(): Buffer;
|
serialize(config?: SerializeConfig): Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === src/stake-program.js ===
|
// === src/stake-program.js ===
|
||||||
|
@ -621,6 +621,11 @@ declare module '@solana/web3.js' {
|
|||||||
signatures?: Array<SignaturePubkeyPair>,
|
signatures?: Array<SignaturePubkeyPair>,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
declare export type SerializeConfig = {
|
||||||
|
requireAllSignatures?: boolean,
|
||||||
|
verifySignatures?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
declare export class Transaction {
|
declare export class Transaction {
|
||||||
signatures: Array<SignaturePubkeyPair>;
|
signatures: Array<SignaturePubkeyPair>;
|
||||||
signature: ?Buffer;
|
signature: ?Buffer;
|
||||||
@ -644,7 +649,7 @@ declare module '@solana/web3.js' {
|
|||||||
addSignature(pubkey: PublicKey, signature: Buffer): void;
|
addSignature(pubkey: PublicKey, signature: Buffer): void;
|
||||||
setSigners(...signers: Array<PublicKey>): void;
|
setSigners(...signers: Array<PublicKey>): void;
|
||||||
verifySignatures(): boolean;
|
verifySignatures(): boolean;
|
||||||
serialize(): Buffer;
|
serialize(config?: SerializeConfig): Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === src/stake-program.js ===
|
// === src/stake-program.js ===
|
||||||
|
@ -62,6 +62,18 @@ export type TransactionInstructionCtorFields = {|
|
|||||||
data?: Buffer,
|
data?: Buffer,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration object for Transaction.serialize()
|
||||||
|
*
|
||||||
|
* @typedef {Object} SerializeConfig
|
||||||
|
* @property {boolean|undefined} requireAllSignatures Require all transaction signatures be present (default: true)
|
||||||
|
* @property {boolean|undefined} verifySignatures Verify provided signatures (default: true)
|
||||||
|
*/
|
||||||
|
export type SerializeConfig = {
|
||||||
|
requireAllSignatures?: boolean,
|
||||||
|
verifySignatures?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transaction Instruction class
|
* Transaction Instruction class
|
||||||
*/
|
*/
|
||||||
@ -462,37 +474,49 @@ export class Transaction {
|
|||||||
* Verify signatures of a complete, signed Transaction
|
* Verify signatures of a complete, signed Transaction
|
||||||
*/
|
*/
|
||||||
verifySignatures(): boolean {
|
verifySignatures(): boolean {
|
||||||
return this._verifySignatures(this.serializeMessage());
|
return this._verifySignatures(this.serializeMessage(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_verifySignatures(signData: Buffer): boolean {
|
_verifySignatures(signData: Buffer, requireAllSignatures: boolean): boolean {
|
||||||
let verified = true;
|
|
||||||
for (const {signature, publicKey} of this.signatures) {
|
for (const {signature, publicKey} of this.signatures) {
|
||||||
|
if (signature === null) {
|
||||||
|
if (requireAllSignatures) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (
|
if (
|
||||||
!nacl.sign.detached.verify(signData, signature, publicKey.toBuffer())
|
!nacl.sign.detached.verify(signData, signature, publicKey.toBuffer())
|
||||||
) {
|
) {
|
||||||
verified = false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return verified;
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize the Transaction in the wire format.
|
* Serialize the Transaction in the wire format.
|
||||||
*
|
|
||||||
* The Transaction must have a valid `signature` before invoking this method
|
|
||||||
*/
|
*/
|
||||||
serialize(): Buffer {
|
serialize(config?: SerializeConfig): Buffer {
|
||||||
const {signatures} = this;
|
const {signatures} = this;
|
||||||
if (!signatures || signatures.length === 0) {
|
|
||||||
|
const {requireAllSignatures, verifySignatures} = Object.assign(
|
||||||
|
{requireAllSignatures: true, verifySignatures: true},
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requireAllSignatures && signatures.length === 0) {
|
||||||
throw new Error('Transaction has not been signed');
|
throw new Error('Transaction has not been signed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const signData = this.serializeMessage();
|
const signData = this.serializeMessage();
|
||||||
if (!this._verifySignatures(signData)) {
|
if (
|
||||||
|
verifySignatures &&
|
||||||
|
!this._verifySignatures(signData, requireAllSignatures)
|
||||||
|
) {
|
||||||
throw new Error('Transaction has not been signed correctly');
|
throw new Error('Transaction has not been signed correctly');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,9 +136,41 @@ test('partialSign', () => {
|
|||||||
partialTransaction.setSigners(account1.publicKey, account2.publicKey);
|
partialTransaction.setSigners(account1.publicKey, account2.publicKey);
|
||||||
expect(partialTransaction.signatures[0].signature).toBeNull();
|
expect(partialTransaction.signatures[0].signature).toBeNull();
|
||||||
expect(partialTransaction.signatures[1].signature).toBeNull();
|
expect(partialTransaction.signatures[1].signature).toBeNull();
|
||||||
partialTransaction.partialSign(account1, account2);
|
|
||||||
|
partialTransaction.partialSign(account1);
|
||||||
|
expect(partialTransaction.signatures[0].signature).not.toBeNull();
|
||||||
|
expect(partialTransaction.signatures[1].signature).toBeNull();
|
||||||
|
|
||||||
|
expect(() => partialTransaction.serialize()).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
partialTransaction.serialize({requireAllSignatures: false}),
|
||||||
|
).not.toThrow();
|
||||||
|
|
||||||
|
partialTransaction.partialSign(account2);
|
||||||
|
|
||||||
|
expect(partialTransaction.signatures[0].signature).not.toBeNull();
|
||||||
|
expect(partialTransaction.signatures[1].signature).not.toBeNull();
|
||||||
|
|
||||||
|
expect(() => partialTransaction.serialize()).not.toThrow();
|
||||||
|
|
||||||
expect(partialTransaction).toEqual(transaction);
|
expect(partialTransaction).toEqual(transaction);
|
||||||
|
|
||||||
|
if (
|
||||||
|
partialTransaction.signatures[0].signature != null /* <-- pacify flow */
|
||||||
|
) {
|
||||||
|
partialTransaction.signatures[0].signature[0] = 0;
|
||||||
|
expect(() =>
|
||||||
|
partialTransaction.serialize({requireAllSignatures: false}),
|
||||||
|
).toThrow();
|
||||||
|
expect(() =>
|
||||||
|
partialTransaction.serialize({
|
||||||
|
verifySignatures: false,
|
||||||
|
requireAllSignatures: false,
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
} else {
|
||||||
|
throw new Error('unreachable');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('dedupe', () => {
|
describe('dedupe', () => {
|
||||||
@ -392,6 +424,9 @@ test('serialize unsigned transaction', () => {
|
|||||||
expect(() => {
|
expect(() => {
|
||||||
expectedTransaction.serialize();
|
expectedTransaction.serialize();
|
||||||
}).toThrow(Error);
|
}).toThrow(Error);
|
||||||
|
expect(() => {
|
||||||
|
expectedTransaction.serialize({verifySignatures: false});
|
||||||
|
}).toThrow(Error);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
expectedTransaction.serializeMessage();
|
expectedTransaction.serializeMessage();
|
||||||
}).toThrow('Transaction feePayer required');
|
}).toThrow('Transaction feePayer required');
|
||||||
@ -407,6 +442,18 @@ test('serialize unsigned transaction', () => {
|
|||||||
// Serializing the message is allowed when signature array has null signatures
|
// Serializing the message is allowed when signature array has null signatures
|
||||||
expectedTransaction.serializeMessage();
|
expectedTransaction.serializeMessage();
|
||||||
|
|
||||||
|
const expectedSerializationWithNoSignatures = Buffer.from(
|
||||||
|
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
||||||
|
'AAAAAAAAAAAAAAAAAAABAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9' +
|
||||||
|
'Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
||||||
|
'AAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwC' +
|
||||||
|
'AAAAMQAAAAAAAAA=',
|
||||||
|
'base64',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
expectedTransaction.serialize({requireAllSignatures: false}),
|
||||||
|
).toStrictEqual(expectedSerializationWithNoSignatures);
|
||||||
|
|
||||||
// Properly signed transaction succeeds
|
// Properly signed transaction succeeds
|
||||||
expectedTransaction.partialSign(sender);
|
expectedTransaction.partialSign(sender);
|
||||||
expect(expectedTransaction.signatures.length).toBe(1);
|
expect(expectedTransaction.signatures.length).toBe(1);
|
||||||
|
Reference in New Issue
Block a user