fix: support multiple Transaction signatures

This commit is contained in:
Michael Vines 2018-11-18 08:48:14 -08:00
parent 5311ed7f68
commit fa7e2722d1
13 changed files with 101 additions and 64 deletions

View File

@ -76,7 +76,7 @@ showBalance()
0, 0,
solanaWeb3.BudgetProgram.programId, solanaWeb3.BudgetProgram.programId,
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -89,7 +89,7 @@ showBalance()
solanaWeb3.BudgetProgram.space, solanaWeb3.BudgetProgram.space,
solanaWeb3.BudgetProgram.programId, solanaWeb3.BudgetProgram.programId,
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -105,7 +105,7 @@ showBalance()
new Date('2050'), new Date('2050'),
), ),
); );
return connection.sendTransaction(contractFunds, transaction); return connection.sendTransaction(transaction, contractFunds);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -117,7 +117,7 @@ showBalance()
account2.publicKey, account2.publicKey,
new Date('2050'), new Date('2050'),
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)

View File

@ -74,7 +74,7 @@ showBalance()
approver1.publicKey, approver1.publicKey,
1, 1,
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(() => { .then(() => {
@ -84,7 +84,7 @@ showBalance()
approver2.publicKey, approver2.publicKey,
1, 1,
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -97,7 +97,7 @@ showBalance()
0, 0,
solanaWeb3.BudgetProgram.programId, solanaWeb3.BudgetProgram.programId,
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -110,7 +110,7 @@ showBalance()
solanaWeb3.BudgetProgram.space, solanaWeb3.BudgetProgram.space,
solanaWeb3.BudgetProgram.programId, solanaWeb3.BudgetProgram.programId,
); );
return connection.sendTransaction(account1, transaction); return connection.sendTransaction(transaction, account1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -124,7 +124,7 @@ showBalance()
solanaWeb3.BudgetProgram.signatureCondition(approver1.publicKey), solanaWeb3.BudgetProgram.signatureCondition(approver1.publicKey),
solanaWeb3.BudgetProgram.signatureCondition(approver2.publicKey), solanaWeb3.BudgetProgram.signatureCondition(approver2.publicKey),
); );
return connection.sendTransaction(contractFunds, transaction); return connection.sendTransaction(transaction, contractFunds);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -135,7 +135,7 @@ showBalance()
contractState.publicKey, contractState.publicKey,
account2.publicKey, account2.publicKey,
); );
return connection.sendTransaction(approver1, transaction); return connection.sendTransaction(transaction, approver1);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)
@ -146,7 +146,7 @@ showBalance()
contractState.publicKey, contractState.publicKey,
account2.publicKey, account2.publicKey,
); );
return connection.sendTransaction(approver2, transaction); return connection.sendTransaction(transaction, approver2);
}) })
.then(confirmTransaction) .then(confirmTransaction)
.then(showBalance) .then(showBalance)

View File

@ -66,8 +66,8 @@ declare module '@solana/web3.js' {
amount: number, amount: number,
): Promise<TransactionSignature>; ): Promise<TransactionSignature>;
sendTransaction( sendTransaction(
from: Account,
transaction: Transaction, transaction: Transaction,
...signers: Array<Account>
): Promise<TransactionSignature>; ): Promise<TransactionSignature>;
onAccountChange( onAccountChange(
publickey: PublicKey, publickey: PublicKey,
@ -242,8 +242,7 @@ declare module '@solana/web3.js' {
// === src/util/send-and-confirm-transaction.js === // === src/util/send-and-confirm-transaction.js ===
declare export function sendAndConfirmTransaction( declare export function sendAndConfirmTransaction(
connection: Connection, connection: Connection,
from: Account,
transaction: Transaction, transaction: Transaction,
runtimeErrorOk?: boolean, ...signers: Array<Account>
): Promise<TransactionSignature>; ): Promise<TransactionSignature>;
} }

View File

@ -41,7 +41,7 @@ export class BpfLoader {
elf.length, elf.length,
BpfLoader.programId, BpfLoader.programId,
); );
await sendAndConfirmTransaction(connection, owner, transaction); await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, BpfLoader.programId); const loader = new Loader(connection, BpfLoader.programId);
await loader.load(programAccount, elf); await loader.load(programAccount, elf);

View File

@ -368,8 +368,8 @@ export class Connection {
* Sign and send a transaction * Sign and send a transaction
*/ */
async sendTransaction( async sendTransaction(
from: Account,
transaction: Transaction, transaction: Transaction,
...signers: Array<Account>
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
for (;;) { for (;;) {
// Attempt to use the previous last id for up to 1 second // Attempt to use the previous last id for up to 1 second
@ -379,7 +379,7 @@ export class Connection {
this._lastIdInfo.seconds === seconds this._lastIdInfo.seconds === seconds
) { ) {
transaction.lastId = this._lastIdInfo.lastId; transaction.lastId = this._lastIdInfo.lastId;
transaction.sign(from); transaction.sign(...signers);
if (!transaction.signature) { if (!transaction.signature) {
throw new Error('!signature'); // should never happen throw new Error('!signature'); // should never happen
} }

View File

@ -72,7 +72,7 @@ export class Loader {
userdata, userdata,
}); });
transactions.push( 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 // Run up to 8 Loads in parallel to prevent too many parallel transactions from
@ -116,6 +116,6 @@ export class Loader {
userdata, userdata,
}); });
transaction.add(SystemProgram.spawn(program.publicKey)); transaction.add(SystemProgram.spawn(program.publicKey));
await sendAndConfirmTransaction(this.connection, program, transaction); await sendAndConfirmTransaction(this.connection, transaction, program);
} }
} }

View File

@ -44,7 +44,7 @@ export class NativeLoader {
bytes.length + 1, bytes.length + 1,
NativeLoader.programId, NativeLoader.programId,
); );
await sendAndConfirmTransaction(connection, owner, transaction); await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, NativeLoader.programId); const loader = new Loader(connection, NativeLoader.programId);
await loader.load(programAccount, bytes); await loader.load(programAccount, bytes);

View File

@ -233,14 +233,14 @@ export class Token {
1 + userdata.length, 1 + userdata.length,
programId, programId,
); );
await sendAndConfirmTransaction(connection, owner, transaction); await sendAndConfirmTransaction(connection, transaction, owner);
transaction = new Transaction().add({ transaction = new Transaction().add({
keys: [tokenAccount.publicKey, initialAccountPublicKey], keys: [tokenAccount.publicKey, initialAccountPublicKey],
programId, programId,
userdata, userdata,
}); });
await sendAndConfirmTransaction(connection, tokenAccount, transaction); await sendAndConfirmTransaction(connection, transaction, tokenAccount);
return [token, initialAccountPublicKey]; return [token, initialAccountPublicKey];
} }
@ -282,7 +282,7 @@ export class Token {
1 + TokenAccountInfoLayout.span, 1 + TokenAccountInfoLayout.span,
this.programId, this.programId,
); );
await sendAndConfirmTransaction(this.connection, owner, transaction); await sendAndConfirmTransaction(this.connection, transaction, owner);
// Initialize the token account // Initialize the token account
const keys = [tokenAccount.publicKey, owner.publicKey, this.token]; const keys = [tokenAccount.publicKey, owner.publicKey, this.token];
@ -295,7 +295,7 @@ export class Token {
userdata, userdata,
}); });
await sendAndConfirmTransaction(this.connection, tokenAccount, transaction); await sendAndConfirmTransaction(this.connection, transaction, tokenAccount);
return tokenAccount.publicKey; return tokenAccount.publicKey;
} }
@ -377,7 +377,6 @@ export class Token {
): Promise<?TransactionSignature> { ): Promise<?TransactionSignature> {
return await sendAndConfirmTransaction( return await sendAndConfirmTransaction(
this.connection, this.connection,
owner,
new Transaction().add( new Transaction().add(
await this.transferInstruction( await this.transferInstruction(
owner.publicKey, owner.publicKey,
@ -386,6 +385,7 @@ export class Token {
amount, amount,
), ),
), ),
owner,
); );
} }
@ -405,10 +405,10 @@ export class Token {
): Promise<void> { ): Promise<void> {
await sendAndConfirmTransaction( await sendAndConfirmTransaction(
this.connection, this.connection,
owner,
new Transaction().add( new Transaction().add(
this.approveInstruction(owner.publicKey, account, delegate, amount), this.approveInstruction(owner.publicKey, account, delegate, amount),
), ),
owner,
); );
} }
@ -441,10 +441,10 @@ export class Token {
): Promise<void> { ): Promise<void> {
await sendAndConfirmTransaction( await sendAndConfirmTransaction(
this.connection, this.connection,
owner,
new Transaction().add( new Transaction().add(
this.setOwnerInstruction(owner.publicKey, account, newOwner), this.setOwnerInstruction(owner.publicKey, account, newOwner),
), ),
owner,
); );
} }

View File

@ -1,6 +1,6 @@
// @flow // @flow
import assert from 'assert'; import invariant from 'assert';
import * as BufferLayout from 'buffer-layout'; import * as BufferLayout from 'buffer-layout';
import nacl from 'tweetnacl'; import nacl from 'tweetnacl';
import bs58 from 'bs58'; import bs58 from 'bs58';
@ -61,25 +61,39 @@ export class TransactionInstruction {
* List of Transaction object fields that may be initialized at construction * List of Transaction object fields that may be initialized at construction
* *
* @typedef {Object} TransactionCtorFields * @typedef {Object} TransactionCtorFields
* @property {?Buffer} signature
* @property {?Array<PublicKey>} keys
* @property {?PublicKey} programId
* @property {?number} fee * @property {?number} fee
* @property {?Buffer} userdata
*/ */
type TransactionCtorFields = {| type TransactionCtorFields = {|
fee?: number, fee?: number,
|}; |};
/**
* @private
*/
type SignaturePubkeyPair = {|
signature: Buffer | null,
publicKey: PublicKey,
|};
/** /**
* Transaction class * Transaction class
*/ */
export class Transaction { export class Transaction {
/** /**
* Current signature of the transaction. Typically created by invoking the * Signatures for the transaction. Typically created by invoking the
* `sign()` method * `sign()` method one or more times.
*/ */
signature: ?Buffer; signatures: Array<SignaturePubkeyPair> = [];
/**
* 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 * The instructions to atomically execute
@ -128,7 +142,7 @@ export class Transaction {
throw new Error('No instructions provided'); throw new Error('No instructions provided');
} }
const keys = []; const keys = this.signatures.map(({publicKey}) => publicKey.toString());
const programIds = []; const programIds = [];
this.instructions.forEach(instruction => { this.instructions.forEach(instruction => {
const programId = instruction.programId.toString(); const programId = instruction.programId.toString();
@ -153,8 +167,8 @@ export class Transaction {
}); });
instructions.forEach(instruction => { instructions.forEach(instruction => {
assert(instruction.programIdIndex >= 0); invariant(instruction.programIdIndex >= 0);
instruction.keyIndices.forEach(keyIndex => assert(keyIndex >= 0)); instruction.keyIndices.forEach(keyIndex => invariant(keyIndex >= 0));
}); });
const instructionLayout = BufferLayout.struct([ 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 * The Transaction must be assigned a valid `lastId` before invoking this method
*/ */
sign(from: Account) { sign(...signers: Array<Account>) {
if (signers.length === 0) {
throw new Error('No signers');
}
const signatures: Array<SignaturePubkeyPair> = signers.map(account => {
return {
signature: null,
publicKey: account.publicKey,
};
});
this.signatures = signatures;
const signData = this._getSignData(); 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 * The Transaction must have a valid `signature` before invoking this method
*/ */
serialize(): Buffer { serialize(): Buffer {
const {signature} = this; const {signatures} = this;
if (!signature) { if (!signatures) {
throw new Error('Transaction has not been signed'); throw new Error('Transaction has not been signed');
} }
const signData = this._getSignData(); const signData = this._getSignData();
const wireTransaction = Buffer.alloc( 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; return wireTransaction;
} }
@ -259,7 +301,7 @@ export class Transaction {
* @private * @private
*/ */
get keys(): Array<PublicKey> { get keys(): Array<PublicKey> {
assert(this.instructions.length === 1); invariant(this.instructions.length === 1);
return this.instructions[0].keys; return this.instructions[0].keys;
} }
@ -268,7 +310,7 @@ export class Transaction {
* @private * @private
*/ */
get programId(): PublicKey { get programId(): PublicKey {
assert(this.instructions.length === 1); invariant(this.instructions.length === 1);
return this.instructions[0].programId; return this.instructions[0].programId;
} }
@ -277,7 +319,7 @@ export class Transaction {
* @private * @private
*/ */
get userdata(): Buffer { get userdata(): Buffer {
assert(this.instructions.length === 1); invariant(this.instructions.length === 1);
return this.instructions[0].userdata; return this.instructions[0].userdata;
} }
} }

View File

@ -11,15 +11,14 @@ import type {TransactionSignature} from '../transaction';
*/ */
export async function sendAndConfirmTransaction( export async function sendAndConfirmTransaction(
connection: Connection, connection: Connection,
from: Account,
transaction: Transaction, transaction: Transaction,
runtimeErrorOk: boolean = false, ...signers: Array<Account>
): Promise<?TransactionSignature> { ): Promise<?TransactionSignature> {
let sendRetries = 10; let sendRetries = 10;
let signature; let signature;
for (;;) { for (;;) {
const start = Date.now(); 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 // Wait up to a couple seconds for a confirmation
let status = 'SignatureNotFound'; let status = 'SignatureNotFound';
@ -41,10 +40,7 @@ export async function sendAndConfirmTransaction(
} }
} }
if ( if (status === 'Confirmed') {
status === 'Confirmed' ||
(status === 'ProgramRuntimeError' && runtimeErrorOk)
) {
break; break;
} }

View File

@ -31,5 +31,5 @@ test('load BPF program', async () => {
keys: [from.publicKey], keys: [from.publicKey],
programId, programId,
}); });
await sendAndConfirmTransaction(connection, from, transaction); await sendAndConfirmTransaction(connection, transaction, from);
}); });

View File

@ -353,7 +353,7 @@ test('transaction', async () => {
accountTo.publicKey, accountTo.publicKey,
10, 10,
); );
const signature = await connection.sendTransaction(accountFrom, transaction); const signature = await connection.sendTransaction(transaction, accountFrom);
mockRpc.push([ mockRpc.push([
url, url,
@ -448,7 +448,7 @@ test('multi-instruction transaction', async () => {
10, 10,
).add(SystemProgram.move(accountTo.publicKey, accountFrom.publicKey, 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; let i = 0;
for (;;) { for (;;) {
if (await connection.confirmTransaction(signature)) { if (await connection.confirmTransaction(signature)) {
@ -492,7 +492,7 @@ test('account change notification', async () => {
3, 3,
BpfLoader.programId, BpfLoader.programId,
); );
await sendAndConfirmTransaction(connection, owner, transaction); await sendAndConfirmTransaction(connection, transaction, owner);
const loader = new Loader(connection, BpfLoader.programId); const loader = new Loader(connection, BpfLoader.programId);
await loader.load(programAccount, [1, 2, 3]); await loader.load(programAccount, [1, 2, 3]);

View File

@ -29,5 +29,5 @@ test('load native program', async () => {
programId, programId,
}); });
await sendAndConfirmTransaction(connection, from, transaction); await sendAndConfirmTransaction(connection, transaction, from);
}); });