diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 723d7b1eae..f9afffac51 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -69,9 +69,7 @@ declare module '@solana/web3.js' { transaction: Transaction, ...signers: Array ): Promise; - sendRawTransaction( - wireTransaction: Buffer, - ): Promise; + sendRawTransaction(wireTransaction: Buffer): Promise; onAccountChange( publickey: PublicKey, callback: AccountChangeCallback, @@ -114,6 +112,8 @@ declare module '@solana/web3.js' { declare type TransactionCtorFields = {| fee?: number, + lastId?: TransactionId, + signatures?: Array, |}; declare export class Transaction { @@ -123,9 +123,11 @@ declare module '@solana/web3.js' { constructor(opts?: TransactionCtorFields): Transaction; add( - item: TransactionInstruction | TransactionInstructionCtorFields, + ...items: Array ): Transaction; - sign(from: Account): void; + sign(...signers: Array): void; + signPartial(...partialSigners: Array): void; + addSigner(signer: Account): void; serialize(): Buffer; } diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index f5828e8d73..e8f3eb6d20 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -7,7 +7,7 @@ import bs58 from 'bs58'; import * as Layout from './layout'; import {PublicKey} from './publickey'; -import type {Account} from './account'; +import {Account} from './account'; /** * @typedef {string} TransactionSignature @@ -67,9 +67,14 @@ export class TransactionInstruction { * * @typedef {Object} TransactionCtorFields * @property {?number} fee + * @property (?lastId} A recent transaction id + * @property (?signatures} One or more signatures + * */ type TransactionCtorFields = {| fee?: number, + lastId?: TransactionId, + signatures?: Array, |}; /** @@ -86,7 +91,7 @@ type SignaturePubkeyPair = {| export class Transaction { /** * Signatures for the transaction. Typically created by invoking the - * `sign()` method one or more times. + * `sign()` method */ signatures: Array = []; @@ -123,14 +128,22 @@ export class Transaction { } /** - * Add instructions to this Transaction + * Add one or more instructions to this Transaction */ - add(item: Transaction | TransactionInstructionCtorFields): Transaction { - if (item instanceof Transaction) { - this.instructions = this.instructions.concat(item.instructions); - } else { - this.instructions.push(new TransactionInstruction(item)); + add( + ...items: Array + ): Transaction { + if (items.length === 0) { + throw new Error('No instructions'); } + + items.forEach(item => { + if (item instanceof Transaction) { + this.instructions = this.instructions.concat(item.instructions); + } else { + this.instructions.push(new TransactionInstruction(item)); + } + }); return this; } @@ -252,25 +265,68 @@ export class Transaction { * The Transaction must be assigned a valid `lastId` before invoking this method */ sign(...signers: Array) { - if (signers.length === 0) { + this.signPartial(...signers); + } + + /** + * Partially sign a Transaction with the specified accounts. The `Account` + * inputs will be used to sign the Transaction immediately, while any + * `PublicKey` inputs will be referenced in the signed Transaction but need to + * be filled in later by calling `addSigner()` with the matching `Account`. + * + * All the caveats from the `sign` method apply to `signPartial` + */ + signPartial(...partialSigners: Array) { + if (partialSigners.length === 0) { throw new Error('No signers'); } - const signatures: Array = signers.map(account => { - return { - signature: null, - publicKey: account.publicKey, - }; - }); + const signatures: Array = partialSigners.map( + accountOrPublicKey => { + const publicKey = + accountOrPublicKey instanceof Account + ? accountOrPublicKey.publicKey + : accountOrPublicKey; + return { + signature: null, + publicKey, + }; + }, + ); this.signatures = signatures; const signData = this._getSignData(); - signers.forEach((account, index) => { - const signature = nacl.sign.detached(signData, account.secretKey); + partialSigners.forEach((accountOrPublicKey, index) => { + if (accountOrPublicKey instanceof PublicKey) { + return; + } + const signature = nacl.sign.detached( + signData, + accountOrPublicKey.secretKey, + ); invariant(signature.length === 64); signatures[index].signature = signature; }); } + /** + * Fill in a signature for a partially signed Transaction. The `signer` must + * be the corresponding `Account` for a `PublicKey` that was previously provided to + * `signPartial` + */ + addSigner(signer: Account) { + const index = this.signatures.findIndex(sigpair => + signer.publicKey.equals(sigpair.publicKey), + ); + if (index < 0) { + throw new Error(`Unknown signer: ${signer.publicKey.toString()}`); + } + + const signData = this._getSignData(); + const signature = nacl.sign.detached(signData, signer.secretKey); + invariant(signature.length === 64); + this.signatures[index].signature = signature; + } + /** * Serialize the Transaction in the wire format. * diff --git a/web3.js/test/transaction.test.js b/web3.js/test/transaction.test.js new file mode 100644 index 0000000000..142780ba4e --- /dev/null +++ b/web3.js/test/transaction.test.js @@ -0,0 +1,40 @@ +// @flow +import {Account} from '../src/account'; +import {Transaction} from '../src/transaction'; +import {SystemProgram} from '../src/system-program'; + +test('signPartial', () => { + const account1 = new Account(); + const account2 = new Account(); + const lastId = account1.publicKey.toBase58(); // Fake lastId + const move = SystemProgram.move(account1.publicKey, account2.publicKey, 123); + + const transaction = new Transaction({lastId}).add(move); + transaction.sign(account1, account2); + + const partialTransaction = new Transaction({lastId}).add(move); + partialTransaction.signPartial(account1, account2.publicKey); + expect(partialTransaction.signatures[1].signature).toBeNull(); + partialTransaction.addSigner(account2); + + expect(partialTransaction).toEqual(transaction); +}); + +test('transfer signatures', () => { + const account1 = new Account(); + const account2 = new Account(); + const lastId = account1.publicKey.toBase58(); // Fake lastId + const move1 = SystemProgram.move(account1.publicKey, account2.publicKey, 123); + const move2 = SystemProgram.move(account2.publicKey, account1.publicKey, 123); + + const orgTransaction = new Transaction({lastId}).add(move1, move2); + orgTransaction.sign(account1, account2); + + const newTransaction = new Transaction({ + lastId: orgTransaction.lastId, + fee: orgTransaction.fee, + signatures: orgTransaction.signatures, + }).add(move1, move2); + + expect(newTransaction).toEqual(orgTransaction); +});