diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 1aa422a105..f7f610aaeb 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -6,8 +6,8 @@ import nacl from 'tweetnacl'; import bs58 from 'bs58'; import * as Layout from './layout'; +import {PublicKey} from './publickey'; import type {Account} from './account'; -import type {PublicKey} from './publickey'; /** * @typedef {string} TransactionSignature @@ -100,16 +100,22 @@ export class Transaction { */ fee: number = 0; + /** + * Construct an empty Transaction + */ constructor(opts?: TransactionCtorFields) { opts && Object.assign(this, opts); } - add(instruction: TransactionInstructionCtorFields): Transaction { - if (this.instructions.length !== 0) { - throw new Error('Multiple instructions not supported yet'); + /** + * Add 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)); } - - this.instructions.push(new TransactionInstruction(instruction)); return this; } @@ -122,43 +128,67 @@ export class Transaction { throw new Error('Transaction lastId required'); } - if (this.instructions.length !== 1) { - throw new Error('No instruction provided'); + if (this.instructions.length < 1) { + throw new Error('No instructions provided'); } - const {keys, programId, userdata} = this.instructions[0]; - const programIds = [programId]; - const instructions = [ - { - programId: 0, - accountsLength: keys.length, - accounts: [...keys.keys()], - userdataLength: userdata.length, + const keys = []; + const programIds = []; + this.instructions.forEach(instruction => { + const programId = instruction.programId.toString(); + if (!programIds.includes(programId)) { + programIds.push(programId); + } + + instruction.keys + .map(key => key.toString()) + .forEach(key => { + if (!keys.includes(key)) { + keys.push(key); + } + }); + }); + + const instructions = this.instructions.map(instruction => { + const {userdata, programId} = instruction; + return { + programIdIndex: programIds.indexOf(programId.toString()), + keyIndices: instruction.keys.map(key => keys.indexOf(key.toString())), userdata, - }, - ]; + }; + }); + + instructions.forEach(instruction => { + assert(instruction.programIdIndex >= 0); + instruction.keyIndices.forEach(keyIndex => assert(keyIndex >= 0)); + }); const instructionLayout = BufferLayout.struct([ - BufferLayout.u8('programId'), + BufferLayout.u8('programIdIndex'), - BufferLayout.u32('accountsLength'), - BufferLayout.u32('accountsLengthPadding'), + BufferLayout.u32('keyIndicesLength'), + BufferLayout.u32('keyIndicesLengthPadding'), BufferLayout.seq( - BufferLayout.u8('account'), + BufferLayout.u8('keyIndex'), BufferLayout.offset(BufferLayout.u32(), -8), - 'accounts' + 'keyIndices' + ), + BufferLayout.u32('userdataLength'), + BufferLayout.u32('userdataLengthPadding'), + BufferLayout.seq( + BufferLayout.u8('userdatum'), + BufferLayout.offset(BufferLayout.u32(), -8), + 'userdata' ), - BufferLayout.ns64('userdataLength'), - BufferLayout.blob(userdata.length, 'userdata'), ]); const signDataLayout = BufferLayout.struct([ - BufferLayout.u32('accountKeysLength'), - BufferLayout.u32('accountKeysLengthPadding'), + BufferLayout.u32('keysLength'), + BufferLayout.u32('keysLengthPadding'), BufferLayout.seq( - Layout.publicKey('accountKey'), + Layout.publicKey('key'), BufferLayout.offset(BufferLayout.u32(), -8), - 'accountKeys' + 'keys' ), Layout.publicKey('lastId'), BufferLayout.ns64('fee'), @@ -181,10 +211,10 @@ export class Transaction { ]); const transaction = { - accountKeys: keys.map((key) => key.toBuffer()), + keys: keys.map((key) => (new PublicKey(key)).toBuffer()), lastId: Buffer.from(bs58.decode(lastId)), - fee: 0, - programIds: programIds.map((programId) => programId.toBuffer()), + fee: this.fee, + programIds: programIds.map(programId => (new PublicKey(programId)).toBuffer()), instructions, }; diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 4f81af1a36..5f95c3143b 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -362,3 +362,51 @@ test('transaction', async () => { ]); expect(await connection.getBalance(accountTo.publicKey)).toBe(31); }); + + +test('multi-instruction transaction', async () => { + if (mockRpcEnabled) { + console.log('non-live test skipped'); + return; + } + + const accountFrom = new Account(); + const accountTo = new Account(); + const connection = new Connection(url); + + await connection.requestAirdrop(accountFrom.publicKey, 12); + expect(await connection.getBalance(accountFrom.publicKey)).toBe(12); + + await connection.requestAirdrop(accountTo.publicKey, 21); + expect(await connection.getBalance(accountTo.publicKey)).toBe(21); + + // 1. Move(accountFrom, accountTo) + // 2. Move(accountTo, accountFrom) + const transaction = SystemProgram.move( + accountFrom.publicKey, + accountTo.publicKey, + 10 + ) + .add(SystemProgram.move( + accountTo.publicKey, + accountFrom.publicKey, + 10 + )); + + const signature = await connection.sendTransaction(accountFrom, transaction); + let i = 0; + for (;;) { + if (await connection.confirmTransaction(signature)) { + break; + } + + expect(mockRpcEnabled).toBe(false); + expect(++i).toBeLessThan(10); + await sleep(500); + } + await expect(connection.getSignatureStatus(signature)).resolves.toBe('Confirmed'); + + expect(await connection.getBalance(accountFrom.publicKey)).toBe(12); + expect(await connection.getBalance(accountTo.publicKey)).toBe(21); +}); +