fix: multiple transaction instructions are now supported

This commit is contained in:
Michael Vines
2018-10-23 19:13:59 -07:00
parent e50b705de3
commit f168cdfd70
2 changed files with 110 additions and 32 deletions

View File

@ -6,8 +6,8 @@ import nacl from 'tweetnacl';
import bs58 from 'bs58'; import bs58 from 'bs58';
import * as Layout from './layout'; import * as Layout from './layout';
import {PublicKey} from './publickey';
import type {Account} from './account'; import type {Account} from './account';
import type {PublicKey} from './publickey';
/** /**
* @typedef {string} TransactionSignature * @typedef {string} TransactionSignature
@ -100,16 +100,22 @@ export class Transaction {
*/ */
fee: number = 0; fee: number = 0;
/**
* Construct an empty Transaction
*/
constructor(opts?: TransactionCtorFields) { constructor(opts?: TransactionCtorFields) {
opts && Object.assign(this, opts); opts && Object.assign(this, opts);
} }
add(instruction: TransactionInstructionCtorFields): Transaction { /**
if (this.instructions.length !== 0) { * Add instructions to this Transaction
throw new Error('Multiple instructions not supported yet'); */
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; return this;
} }
@ -122,43 +128,67 @@ export class Transaction {
throw new Error('Transaction lastId required'); throw new Error('Transaction lastId required');
} }
if (this.instructions.length !== 1) { if (this.instructions.length < 1) {
throw new Error('No instruction provided'); throw new Error('No instructions provided');
} }
const {keys, programId, userdata} = this.instructions[0]; const keys = [];
const programIds = [programId]; const programIds = [];
const instructions = [ this.instructions.forEach(instruction => {
{ const programId = instruction.programId.toString();
programId: 0, if (!programIds.includes(programId)) {
accountsLength: keys.length, programIds.push(programId);
accounts: [...keys.keys()], }
userdataLength: userdata.length,
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, userdata,
}, };
]; });
instructions.forEach(instruction => {
assert(instruction.programIdIndex >= 0);
instruction.keyIndices.forEach(keyIndex => assert(keyIndex >= 0));
});
const instructionLayout = BufferLayout.struct([ const instructionLayout = BufferLayout.struct([
BufferLayout.u8('programId'), BufferLayout.u8('programIdIndex'),
BufferLayout.u32('accountsLength'), BufferLayout.u32('keyIndicesLength'),
BufferLayout.u32('accountsLengthPadding'), BufferLayout.u32('keyIndicesLengthPadding'),
BufferLayout.seq( BufferLayout.seq(
BufferLayout.u8('account'), BufferLayout.u8('keyIndex'),
BufferLayout.offset(BufferLayout.u32(), -8), 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([ const signDataLayout = BufferLayout.struct([
BufferLayout.u32('accountKeysLength'), BufferLayout.u32('keysLength'),
BufferLayout.u32('accountKeysLengthPadding'), BufferLayout.u32('keysLengthPadding'),
BufferLayout.seq( BufferLayout.seq(
Layout.publicKey('accountKey'), Layout.publicKey('key'),
BufferLayout.offset(BufferLayout.u32(), -8), BufferLayout.offset(BufferLayout.u32(), -8),
'accountKeys' 'keys'
), ),
Layout.publicKey('lastId'), Layout.publicKey('lastId'),
BufferLayout.ns64('fee'), BufferLayout.ns64('fee'),
@ -181,10 +211,10 @@ export class Transaction {
]); ]);
const transaction = { const transaction = {
accountKeys: keys.map((key) => key.toBuffer()), keys: keys.map((key) => (new PublicKey(key)).toBuffer()),
lastId: Buffer.from(bs58.decode(lastId)), lastId: Buffer.from(bs58.decode(lastId)),
fee: 0, fee: this.fee,
programIds: programIds.map((programId) => programId.toBuffer()), programIds: programIds.map(programId => (new PublicKey(programId)).toBuffer()),
instructions, instructions,
}; };

View File

@ -362,3 +362,51 @@ test('transaction', async () => {
]); ]);
expect(await connection.getBalance(accountTo.publicKey)).toBe(31); 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);
});