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 * 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,
};

View File

@ -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);
});