fix: support serialization of partially signed transactions
This commit is contained in:
parent
4bb6c2fffb
commit
a59d305e09
|
@ -617,6 +617,11 @@ declare module '@solana/web3.js' {
|
|||
signatures?: Array<SignaturePubkeyPair>;
|
||||
};
|
||||
|
||||
export type SerializeConfig = {
|
||||
requireAllSignatures?: boolean;
|
||||
verifySignatures?: boolean;
|
||||
};
|
||||
|
||||
export class Transaction {
|
||||
signatures: Array<SignaturePubkeyPair>;
|
||||
signature?: Buffer;
|
||||
|
@ -640,7 +645,7 @@ declare module '@solana/web3.js' {
|
|||
addSignature(pubkey: PublicKey, signature: Buffer): void;
|
||||
setSigners(...signer: Array<PublicKey>): void;
|
||||
verifySignatures(): boolean;
|
||||
serialize(): Buffer;
|
||||
serialize(config?: SerializeConfig): Buffer;
|
||||
}
|
||||
|
||||
// === src/stake-program.js ===
|
||||
|
|
|
@ -621,6 +621,11 @@ declare module '@solana/web3.js' {
|
|||
signatures?: Array<SignaturePubkeyPair>,
|
||||
|};
|
||||
|
||||
declare export type SerializeConfig = {
|
||||
requireAllSignatures?: boolean,
|
||||
verifySignatures?: boolean,
|
||||
};
|
||||
|
||||
declare export class Transaction {
|
||||
signatures: Array<SignaturePubkeyPair>;
|
||||
signature: ?Buffer;
|
||||
|
@ -644,7 +649,7 @@ declare module '@solana/web3.js' {
|
|||
addSignature(pubkey: PublicKey, signature: Buffer): void;
|
||||
setSigners(...signers: Array<PublicKey>): void;
|
||||
verifySignatures(): boolean;
|
||||
serialize(): Buffer;
|
||||
serialize(config?: SerializeConfig): Buffer;
|
||||
}
|
||||
|
||||
// === src/stake-program.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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue