feat: add support for partial Transaction signing
This commit is contained in:
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
...items: Array<Transaction | TransactionInstructionCtorFields>
|
||||||
|
): Transaction {
|
||||||
|
if (items.length === 0) {
|
||||||
|
throw new Error('No instructions');
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
if (item instanceof Transaction) {
|
if (item instanceof Transaction) {
|
||||||
this.instructions = this.instructions.concat(item.instructions);
|
this.instructions = this.instructions.concat(item.instructions);
|
||||||
} else {
|
} else {
|
||||||
this.instructions.push(new TransactionInstruction(item));
|
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(
|
||||||
|
accountOrPublicKey => {
|
||||||
|
const publicKey =
|
||||||
|
accountOrPublicKey instanceof Account
|
||||||
|
? accountOrPublicKey.publicKey
|
||||||
|
: accountOrPublicKey;
|
||||||
return {
|
return {
|
||||||
signature: null,
|
signature: null,
|
||||||
publicKey: account.publicKey,
|
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.
|
||||||
*
|
*
|
||||||
|
40
web3.js/test/transaction.test.js
Normal file
40
web3.js/test/transaction.test.js
Normal 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);
|
||||||
|
});
|
Reference in New Issue
Block a user