diff --git a/web3.js/examples/budget-timestamp.js b/web3.js/examples/budget-timestamp.js index fefb02fbf9..5933c956a0 100644 --- a/web3.js/examples/budget-timestamp.js +++ b/web3.js/examples/budget-timestamp.js @@ -76,7 +76,7 @@ showBalance() 0, solanaWeb3.BudgetProgram.programId, ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(showBalance) @@ -89,7 +89,7 @@ showBalance() solanaWeb3.BudgetProgram.space, solanaWeb3.BudgetProgram.programId, ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(showBalance) @@ -105,7 +105,7 @@ showBalance() new Date('2050'), ), ); - return connection.sendTransaction(contractFunds, transaction); + return connection.sendTransaction(transaction, contractFunds); }) .then(confirmTransaction) .then(showBalance) @@ -117,7 +117,7 @@ showBalance() account2.publicKey, new Date('2050'), ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(showBalance) diff --git a/web3.js/examples/budget-two-approvers.js b/web3.js/examples/budget-two-approvers.js index 47a8b7e836..8d37c4aa20 100644 --- a/web3.js/examples/budget-two-approvers.js +++ b/web3.js/examples/budget-two-approvers.js @@ -74,7 +74,7 @@ showBalance() approver1.publicKey, 1, ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(() => { @@ -84,7 +84,7 @@ showBalance() approver2.publicKey, 1, ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(showBalance) @@ -97,7 +97,7 @@ showBalance() 0, solanaWeb3.BudgetProgram.programId, ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(showBalance) @@ -110,7 +110,7 @@ showBalance() solanaWeb3.BudgetProgram.space, solanaWeb3.BudgetProgram.programId, ); - return connection.sendTransaction(account1, transaction); + return connection.sendTransaction(transaction, account1); }) .then(confirmTransaction) .then(showBalance) @@ -124,7 +124,7 @@ showBalance() solanaWeb3.BudgetProgram.signatureCondition(approver1.publicKey), solanaWeb3.BudgetProgram.signatureCondition(approver2.publicKey), ); - return connection.sendTransaction(contractFunds, transaction); + return connection.sendTransaction(transaction, contractFunds); }) .then(confirmTransaction) .then(showBalance) @@ -135,7 +135,7 @@ showBalance() contractState.publicKey, account2.publicKey, ); - return connection.sendTransaction(approver1, transaction); + return connection.sendTransaction(transaction, approver1); }) .then(confirmTransaction) .then(showBalance) @@ -146,7 +146,7 @@ showBalance() contractState.publicKey, account2.publicKey, ); - return connection.sendTransaction(approver2, transaction); + return connection.sendTransaction(transaction, approver2); }) .then(confirmTransaction) .then(showBalance) diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 340466ea1a..52ce6e7298 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -66,8 +66,8 @@ declare module '@solana/web3.js' { amount: number, ): Promise; sendTransaction( - from: Account, transaction: Transaction, + ...signers: Array ): Promise; onAccountChange( publickey: PublicKey, @@ -242,8 +242,7 @@ declare module '@solana/web3.js' { // === src/util/send-and-confirm-transaction.js === declare export function sendAndConfirmTransaction( connection: Connection, - from: Account, transaction: Transaction, - runtimeErrorOk?: boolean, + ...signers: Array ): Promise; } diff --git a/web3.js/src/bpf-loader.js b/web3.js/src/bpf-loader.js index 77113fdcea..db2a69d90a 100644 --- a/web3.js/src/bpf-loader.js +++ b/web3.js/src/bpf-loader.js @@ -41,7 +41,7 @@ export class BpfLoader { elf.length, BpfLoader.programId, ); - await sendAndConfirmTransaction(connection, owner, transaction); + await sendAndConfirmTransaction(connection, transaction, owner); const loader = new Loader(connection, BpfLoader.programId); await loader.load(programAccount, elf); diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 490ee7296e..99aa1d2408 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -368,8 +368,8 @@ export class Connection { * Sign and send a transaction */ async sendTransaction( - from: Account, transaction: Transaction, + ...signers: Array ): Promise { for (;;) { // Attempt to use the previous last id for up to 1 second @@ -379,7 +379,7 @@ export class Connection { this._lastIdInfo.seconds === seconds ) { transaction.lastId = this._lastIdInfo.lastId; - transaction.sign(from); + transaction.sign(...signers); if (!transaction.signature) { throw new Error('!signature'); // should never happen } diff --git a/web3.js/src/loader.js b/web3.js/src/loader.js index 89211c2f16..06019cde17 100644 --- a/web3.js/src/loader.js +++ b/web3.js/src/loader.js @@ -72,7 +72,7 @@ export class Loader { userdata, }); transactions.push( - sendAndConfirmTransaction(this.connection, program, transaction), + sendAndConfirmTransaction(this.connection, transaction, program), ); // Run up to 8 Loads in parallel to prevent too many parallel transactions from @@ -116,6 +116,6 @@ export class Loader { userdata, }); transaction.add(SystemProgram.spawn(program.publicKey)); - await sendAndConfirmTransaction(this.connection, program, transaction); + await sendAndConfirmTransaction(this.connection, transaction, program); } } diff --git a/web3.js/src/native-loader.js b/web3.js/src/native-loader.js index 5839f751cd..e44d042307 100644 --- a/web3.js/src/native-loader.js +++ b/web3.js/src/native-loader.js @@ -44,7 +44,7 @@ export class NativeLoader { bytes.length + 1, NativeLoader.programId, ); - await sendAndConfirmTransaction(connection, owner, transaction); + await sendAndConfirmTransaction(connection, transaction, owner); const loader = new Loader(connection, NativeLoader.programId); await loader.load(programAccount, bytes); diff --git a/web3.js/src/token-program.js b/web3.js/src/token-program.js index 129684129a..3a47a2ecb6 100644 --- a/web3.js/src/token-program.js +++ b/web3.js/src/token-program.js @@ -233,14 +233,14 @@ export class Token { 1 + userdata.length, programId, ); - await sendAndConfirmTransaction(connection, owner, transaction); + await sendAndConfirmTransaction(connection, transaction, owner); transaction = new Transaction().add({ keys: [tokenAccount.publicKey, initialAccountPublicKey], programId, userdata, }); - await sendAndConfirmTransaction(connection, tokenAccount, transaction); + await sendAndConfirmTransaction(connection, transaction, tokenAccount); return [token, initialAccountPublicKey]; } @@ -282,7 +282,7 @@ export class Token { 1 + TokenAccountInfoLayout.span, this.programId, ); - await sendAndConfirmTransaction(this.connection, owner, transaction); + await sendAndConfirmTransaction(this.connection, transaction, owner); // Initialize the token account const keys = [tokenAccount.publicKey, owner.publicKey, this.token]; @@ -295,7 +295,7 @@ export class Token { userdata, }); - await sendAndConfirmTransaction(this.connection, tokenAccount, transaction); + await sendAndConfirmTransaction(this.connection, transaction, tokenAccount); return tokenAccount.publicKey; } @@ -377,7 +377,6 @@ export class Token { ): Promise { return await sendAndConfirmTransaction( this.connection, - owner, new Transaction().add( await this.transferInstruction( owner.publicKey, @@ -386,6 +385,7 @@ export class Token { amount, ), ), + owner, ); } @@ -405,10 +405,10 @@ export class Token { ): Promise { await sendAndConfirmTransaction( this.connection, - owner, new Transaction().add( this.approveInstruction(owner.publicKey, account, delegate, amount), ), + owner, ); } @@ -441,10 +441,10 @@ export class Token { ): Promise { await sendAndConfirmTransaction( this.connection, - owner, new Transaction().add( this.setOwnerInstruction(owner.publicKey, account, newOwner), ), + owner, ); } diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 1dfb179127..39f4ea55aa 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -1,6 +1,6 @@ // @flow -import assert from 'assert'; +import invariant from 'assert'; import * as BufferLayout from 'buffer-layout'; import nacl from 'tweetnacl'; import bs58 from 'bs58'; @@ -61,25 +61,39 @@ export class TransactionInstruction { * List of Transaction object fields that may be initialized at construction * * @typedef {Object} TransactionCtorFields - * @property {?Buffer} signature - * @property {?Array} keys - * @property {?PublicKey} programId * @property {?number} fee - * @property {?Buffer} userdata */ type TransactionCtorFields = {| fee?: number, |}; +/** + * @private + */ +type SignaturePubkeyPair = {| + signature: Buffer | null, + publicKey: PublicKey, +|}; + /** * Transaction class */ export class Transaction { /** - * Current signature of the transaction. Typically created by invoking the - * `sign()` method + * Signatures for the transaction. Typically created by invoking the + * `sign()` method one or more times. */ - signature: ?Buffer; + signatures: Array = []; + + /** + * The first (primary) Transaction signature + */ + get signature(): Buffer | null { + if (this.signatures.length > 0) { + return this.signatures[0].signature; + } + return null; + } /** * The instructions to atomically execute @@ -128,7 +142,7 @@ export class Transaction { throw new Error('No instructions provided'); } - const keys = []; + const keys = this.signatures.map(({publicKey}) => publicKey.toString()); const programIds = []; this.instructions.forEach(instruction => { const programId = instruction.programId.toString(); @@ -153,8 +167,8 @@ export class Transaction { }); instructions.forEach(instruction => { - assert(instruction.programIdIndex >= 0); - instruction.keyIndices.forEach(keyIndex => assert(keyIndex >= 0)); + invariant(instruction.programIdIndex >= 0); + instruction.keyIndices.forEach(keyIndex => invariant(keyIndex >= 0)); }); const instructionLayout = BufferLayout.struct([ @@ -222,14 +236,34 @@ export class Transaction { } /** - * Sign the Transaction with the specified account + * Sign the Transaction with the specified accounts. Multiple signatures may + * be applied to a Transaction. The first signature is considered "primary" + * and is used when testing for Transaction confirmation. + * + * Transaction fields should not be modified after the first call to `sign`, + * as doing so may invalidate the signature and cause the Transaction to be + * rejected. * * The Transaction must be assigned a valid `lastId` before invoking this method */ - sign(from: Account) { + sign(...signers: Array) { + if (signers.length === 0) { + throw new Error('No signers'); + } + const signatures: Array = signers.map(account => { + return { + signature: null, + publicKey: account.publicKey, + }; + }); + this.signatures = signatures; const signData = this._getSignData(); - this.signature = nacl.sign.detached(signData, from.secretKey); - assert(this.signature.length === 64); + + signers.forEach((account, index) => { + const signature = nacl.sign.detached(signData, account.secretKey); + invariant(signature.length === 64); + signatures[index].signature = signature; + }); } /** @@ -238,19 +272,27 @@ export class Transaction { * The Transaction must have a valid `signature` before invoking this method */ serialize(): Buffer { - const {signature} = this; - if (!signature) { + const {signatures} = this; + if (!signatures) { throw new Error('Transaction has not been signed'); } const signData = this._getSignData(); const wireTransaction = Buffer.alloc( - 8 + signature.length + signData.length, + 8 + signatures.length * 64 + signData.length, + ); + invariant(signatures.length < 256); + wireTransaction.writeUInt8(signatures.length, 0); + signatures.forEach(({signature}, index) => { + invariant(signature !== null, `null signature`); + invariant(signature.length === 64, `signature has invalid length`); + Buffer.from(signature).copy(wireTransaction, 8 + index * 64); + }); + signData.copy(wireTransaction, 8 + signatures.length * 64); + invariant( + wireTransaction.length < 512, + `${wireTransaction.length}, ${signatures.length}`, ); - - wireTransaction.writeUInt8(1, 0); // TODO: Support multiple transaction signatures - Buffer.from(signature).copy(wireTransaction, 8); - signData.copy(wireTransaction, 8 + signature.length); return wireTransaction; } @@ -259,7 +301,7 @@ export class Transaction { * @private */ get keys(): Array { - assert(this.instructions.length === 1); + invariant(this.instructions.length === 1); return this.instructions[0].keys; } @@ -268,7 +310,7 @@ export class Transaction { * @private */ get programId(): PublicKey { - assert(this.instructions.length === 1); + invariant(this.instructions.length === 1); return this.instructions[0].programId; } @@ -277,7 +319,7 @@ export class Transaction { * @private */ get userdata(): Buffer { - assert(this.instructions.length === 1); + invariant(this.instructions.length === 1); return this.instructions[0].userdata; } } diff --git a/web3.js/src/util/send-and-confirm-transaction.js b/web3.js/src/util/send-and-confirm-transaction.js index 8d99a767b4..b2894068fc 100644 --- a/web3.js/src/util/send-and-confirm-transaction.js +++ b/web3.js/src/util/send-and-confirm-transaction.js @@ -11,15 +11,14 @@ import type {TransactionSignature} from '../transaction'; */ export async function sendAndConfirmTransaction( connection: Connection, - from: Account, transaction: Transaction, - runtimeErrorOk: boolean = false, + ...signers: Array ): Promise { let sendRetries = 10; let signature; for (;;) { const start = Date.now(); - signature = await connection.sendTransaction(from, transaction); + signature = await connection.sendTransaction(transaction, ...signers); // Wait up to a couple seconds for a confirmation let status = 'SignatureNotFound'; @@ -41,10 +40,7 @@ export async function sendAndConfirmTransaction( } } - if ( - status === 'Confirmed' || - (status === 'ProgramRuntimeError' && runtimeErrorOk) - ) { + if (status === 'Confirmed') { break; } diff --git a/web3.js/test/bpf-loader.test.js b/web3.js/test/bpf-loader.test.js index 196eb6df4c..b815dea031 100644 --- a/web3.js/test/bpf-loader.test.js +++ b/web3.js/test/bpf-loader.test.js @@ -31,5 +31,5 @@ test('load BPF program', async () => { keys: [from.publicKey], programId, }); - await sendAndConfirmTransaction(connection, from, transaction); + await sendAndConfirmTransaction(connection, transaction, from); }); diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 60b564f85d..69c55699de 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -353,7 +353,7 @@ test('transaction', async () => { accountTo.publicKey, 10, ); - const signature = await connection.sendTransaction(accountFrom, transaction); + const signature = await connection.sendTransaction(transaction, accountFrom); mockRpc.push([ url, @@ -448,7 +448,7 @@ test('multi-instruction transaction', async () => { 10, ).add(SystemProgram.move(accountTo.publicKey, accountFrom.publicKey, 10)); - const signature = await connection.sendTransaction(accountFrom, transaction); + const signature = await connection.sendTransaction(transaction, accountFrom, accountTo); let i = 0; for (;;) { if (await connection.confirmTransaction(signature)) { @@ -492,7 +492,7 @@ test('account change notification', async () => { 3, BpfLoader.programId, ); - await sendAndConfirmTransaction(connection, owner, transaction); + await sendAndConfirmTransaction(connection, transaction, owner); const loader = new Loader(connection, BpfLoader.programId); await loader.load(programAccount, [1, 2, 3]); diff --git a/web3.js/test/native-loader.test.js b/web3.js/test/native-loader.test.js index a429cfbd87..0eeb23c086 100644 --- a/web3.js/test/native-loader.test.js +++ b/web3.js/test/native-loader.test.js @@ -29,5 +29,5 @@ test('load native program', async () => { programId, }); - await sendAndConfirmTransaction(connection, from, transaction); + await sendAndConfirmTransaction(connection, transaction, from); });