feat: add support for partial Transaction signing

This commit is contained in:
Michael Vines
2018-11-28 10:06:17 -08:00
parent ac4bfaad32
commit 0026b44531
3 changed files with 120 additions and 22 deletions

View File

@ -69,9 +69,7 @@ declare module '@solana/web3.js' {
transaction: Transaction, transaction: Transaction,
...signers: Array<Account> ...signers: Array<Account>
): Promise<TransactionSignature>; ): Promise<TransactionSignature>;
sendRawTransaction( sendRawTransaction(wireTransaction: Buffer): Promise<TransactionSignature>;
wireTransaction: Buffer,
): Promise<TransactionSignature>;
onAccountChange( onAccountChange(
publickey: PublicKey, publickey: PublicKey,
callback: AccountChangeCallback, callback: AccountChangeCallback,
@ -114,6 +112,8 @@ declare module '@solana/web3.js' {
declare type TransactionCtorFields = {| declare type TransactionCtorFields = {|
fee?: number, fee?: number,
lastId?: TransactionId,
signatures?: Array<SignaturePubkeyPair>,
|}; |};
declare export class Transaction { declare export class Transaction {
@ -123,9 +123,11 @@ declare module '@solana/web3.js' {
constructor(opts?: TransactionCtorFields): Transaction; constructor(opts?: TransactionCtorFields): Transaction;
add( add(
item: TransactionInstruction | TransactionInstructionCtorFields, ...items: Array<TransactionInstruction | TransactionInstructionCtorFields>
): Transaction; ): Transaction;
sign(from: Account): void; sign(...signers: Array<Account>): void;
signPartial(...partialSigners: Array<PublicKey | Account>): void;
addSigner(signer: Account): void;
serialize(): Buffer; serialize(): Buffer;
} }

View File

@ -7,7 +7,7 @@ import bs58 from 'bs58';
import * as Layout from './layout'; import * as Layout from './layout';
import {PublicKey} from './publickey'; import {PublicKey} from './publickey';
import type {Account} from './account'; import {Account} from './account';
/** /**
* @typedef {string} TransactionSignature * @typedef {string} TransactionSignature
@ -67,9 +67,14 @@ export class TransactionInstruction {
* *
* @typedef {Object} TransactionCtorFields * @typedef {Object} TransactionCtorFields
* @property {?number} fee * @property {?number} fee
* @property (?lastId} A recent transaction id
* @property (?signatures} One or more signatures
*
*/ */
type TransactionCtorFields = {| type TransactionCtorFields = {|
fee?: number, fee?: number,
lastId?: TransactionId,
signatures?: Array<SignaturePubkeyPair>,
|}; |};
/** /**
@ -86,7 +91,7 @@ type SignaturePubkeyPair = {|
export class Transaction { export class Transaction {
/** /**
* Signatures for the transaction. Typically created by invoking the * Signatures for the transaction. Typically created by invoking the
* `sign()` method one or more times. * `sign()` method
*/ */
signatures: Array<SignaturePubkeyPair> = []; signatures: Array<SignaturePubkeyPair> = [];
@ -123,14 +128,22 @@ export class Transaction {
} }
/** /**
* Add instructions to this Transaction * Add one or more instructions to this Transaction
*/ */
add(item: Transaction | TransactionInstructionCtorFields): Transaction { add(
if (item instanceof Transaction) { ...items: Array<Transaction | TransactionInstructionCtorFields>
this.instructions = this.instructions.concat(item.instructions); ): Transaction {
} else { if (items.length === 0) {
this.instructions.push(new TransactionInstruction(item)); throw new Error('No instructions');
} }
items.forEach(item => {
if (item instanceof Transaction) {
this.instructions = this.instructions.concat(item.instructions);
} else {
this.instructions.push(new TransactionInstruction(item));
}
});
return this; return this;
} }
@ -252,25 +265,68 @@ export class Transaction {
* 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(...signers: Array<Account>) { sign(...signers: Array<Account>) {
if (signers.length === 0) { this.signPartial(...signers);
}
/**
* Partially sign a Transaction with the specified accounts. The `Account`
* inputs will be used to sign the Transaction immediately, while any
* `PublicKey` inputs will be referenced in the signed Transaction but need to
* be filled in later by calling `addSigner()` with the matching `Account`.
*
* All the caveats from the `sign` method apply to `signPartial`
*/
signPartial(...partialSigners: Array<PublicKey | Account>) {
if (partialSigners.length === 0) {
throw new Error('No signers'); throw new Error('No signers');
} }
const signatures: Array<SignaturePubkeyPair> = signers.map(account => { const signatures: Array<SignaturePubkeyPair> = partialSigners.map(
return { accountOrPublicKey => {
signature: null, const publicKey =
publicKey: account.publicKey, accountOrPublicKey instanceof Account
}; ? accountOrPublicKey.publicKey
}); : accountOrPublicKey;
return {
signature: null,
publicKey,
};
},
);
this.signatures = signatures; this.signatures = signatures;
const signData = this._getSignData(); const signData = this._getSignData();
signers.forEach((account, index) => { partialSigners.forEach((accountOrPublicKey, index) => {
const signature = nacl.sign.detached(signData, account.secretKey); if (accountOrPublicKey instanceof PublicKey) {
return;
}
const signature = nacl.sign.detached(
signData,
accountOrPublicKey.secretKey,
);
invariant(signature.length === 64); invariant(signature.length === 64);
signatures[index].signature = signature; signatures[index].signature = signature;
}); });
} }
/**
* Fill in a signature for a partially signed Transaction. The `signer` must
* be the corresponding `Account` for a `PublicKey` that was previously provided to
* `signPartial`
*/
addSigner(signer: Account) {
const index = this.signatures.findIndex(sigpair =>
signer.publicKey.equals(sigpair.publicKey),
);
if (index < 0) {
throw new Error(`Unknown signer: ${signer.publicKey.toString()}`);
}
const signData = this._getSignData();
const signature = nacl.sign.detached(signData, signer.secretKey);
invariant(signature.length === 64);
this.signatures[index].signature = signature;
}
/** /**
* Serialize the Transaction in the wire format. * Serialize the Transaction in the wire format.
* *

View File

@ -0,0 +1,40 @@
// @flow
import {Account} from '../src/account';
import {Transaction} from '../src/transaction';
import {SystemProgram} from '../src/system-program';
test('signPartial', () => {
const account1 = new Account();
const account2 = new Account();
const lastId = account1.publicKey.toBase58(); // Fake lastId
const move = SystemProgram.move(account1.publicKey, account2.publicKey, 123);
const transaction = new Transaction({lastId}).add(move);
transaction.sign(account1, account2);
const partialTransaction = new Transaction({lastId}).add(move);
partialTransaction.signPartial(account1, account2.publicKey);
expect(partialTransaction.signatures[1].signature).toBeNull();
partialTransaction.addSigner(account2);
expect(partialTransaction).toEqual(transaction);
});
test('transfer signatures', () => {
const account1 = new Account();
const account2 = new Account();
const lastId = account1.publicKey.toBase58(); // Fake lastId
const move1 = SystemProgram.move(account1.publicKey, account2.publicKey, 123);
const move2 = SystemProgram.move(account2.publicKey, account1.publicKey, 123);
const orgTransaction = new Transaction({lastId}).add(move1, move2);
orgTransaction.sign(account1, account2);
const newTransaction = new Transaction({
lastId: orgTransaction.lastId,
fee: orgTransaction.fee,
signatures: orgTransaction.signatures,
}).add(move1, move2);
expect(newTransaction).toEqual(orgTransaction);
});