diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 6c3dc5ab37..94f17cd9fb 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -617,6 +617,11 @@ declare module '@solana/web3.js' { signatures?: Array; }; + export type SerializeConfig = { + requireAllSignatures?: boolean; + verifySignatures?: boolean; + }; + export class Transaction { signatures: Array; signature?: Buffer; @@ -640,7 +645,7 @@ declare module '@solana/web3.js' { addSignature(pubkey: PublicKey, signature: Buffer): void; setSigners(...signer: Array): void; verifySignatures(): boolean; - serialize(): Buffer; + serialize(config?: SerializeConfig): Buffer; } // === src/stake-program.js === diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 2b8f76c36b..1e46861026 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -621,6 +621,11 @@ declare module '@solana/web3.js' { signatures?: Array, |}; + declare export type SerializeConfig = { + requireAllSignatures?: boolean, + verifySignatures?: boolean, + }; + declare export class Transaction { signatures: Array; signature: ?Buffer; @@ -644,7 +649,7 @@ declare module '@solana/web3.js' { addSignature(pubkey: PublicKey, signature: Buffer): void; setSigners(...signers: Array): void; verifySignatures(): boolean; - serialize(): Buffer; + serialize(config?: SerializeConfig): Buffer; } // === src/stake-program.js === diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 8ef22778dd..3ea3d289ef 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -62,6 +62,18 @@ export type TransactionInstructionCtorFields = {| 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 */ @@ -462,37 +474,49 @@ export class Transaction { * Verify signatures of a complete, signed Transaction */ verifySignatures(): boolean { - return this._verifySignatures(this.serializeMessage()); + return this._verifySignatures(this.serializeMessage(), true); } /** * @private */ - _verifySignatures(signData: Buffer): boolean { - let verified = true; + _verifySignatures(signData: Buffer, requireAllSignatures: boolean): boolean { for (const {signature, publicKey} of this.signatures) { - if ( - !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) - ) { - verified = false; + if (signature === null) { + if (requireAllSignatures) { + return false; + } + } else { + if ( + !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) + ) { + return false; + } } } - return verified; + return true; } /** * 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; - 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'); } const signData = this.serializeMessage(); - if (!this._verifySignatures(signData)) { + if ( + verifySignatures && + !this._verifySignatures(signData, requireAllSignatures) + ) { throw new Error('Transaction has not been signed correctly'); } diff --git a/web3.js/test/transaction.test.js b/web3.js/test/transaction.test.js index 48f6bd3a89..11edc4934a 100644 --- a/web3.js/test/transaction.test.js +++ b/web3.js/test/transaction.test.js @@ -136,9 +136,41 @@ test('partialSign', () => { partialTransaction.setSigners(account1.publicKey, account2.publicKey); expect(partialTransaction.signatures[0].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); + + 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', () => { @@ -392,6 +424,9 @@ test('serialize unsigned transaction', () => { expect(() => { expectedTransaction.serialize(); }).toThrow(Error); + expect(() => { + expectedTransaction.serialize({verifySignatures: false}); + }).toThrow(Error); expect(() => { expectedTransaction.serializeMessage(); }).toThrow('Transaction feePayer required'); @@ -407,6 +442,18 @@ test('serialize unsigned transaction', () => { // Serializing the message is allowed when signature array has null signatures 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 expectedTransaction.partialSign(sender); expect(expectedTransaction.signatures.length).toBe(1);