2018-09-13 18:48:51 -07:00
|
|
|
// @flow
|
|
|
|
|
2018-11-18 08:48:14 -08:00
|
|
|
import invariant from 'assert';
|
2018-10-06 11:13:58 -07:00
|
|
|
import * as BufferLayout from 'buffer-layout';
|
2018-09-13 18:48:51 -07:00
|
|
|
import nacl from 'tweetnacl';
|
|
|
|
import bs58 from 'bs58';
|
|
|
|
|
2018-10-06 11:13:58 -07:00
|
|
|
import * as Layout from './layout';
|
2018-10-23 19:13:59 -07:00
|
|
|
import {PublicKey} from './publickey';
|
2018-11-28 10:06:17 -08:00
|
|
|
import {Account} from './account';
|
2019-01-31 01:16:07 -08:00
|
|
|
import * as shortvec from './util/shortvec-encoding';
|
2019-03-04 08:06:33 -08:00
|
|
|
import type {Blockhash} from './blockhash';
|
2018-09-13 18:48:51 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {string} TransactionSignature
|
|
|
|
*/
|
|
|
|
export type TransactionSignature = string;
|
|
|
|
|
2019-08-30 16:22:11 -07:00
|
|
|
/**
|
|
|
|
* Default (empty) signature
|
|
|
|
*
|
|
|
|
* Signatures are 64 bytes in length
|
|
|
|
*/
|
2020-01-13 15:17:32 -08:00
|
|
|
const DEFAULT_SIGNATURE = Buffer.alloc(64).fill(0);
|
2019-08-30 16:22:11 -07:00
|
|
|
|
2018-11-27 18:05:23 -08:00
|
|
|
/**
|
|
|
|
* Maximum over-the-wire size of a Transaction
|
2019-05-28 08:51:46 -07:00
|
|
|
*
|
|
|
|
* 1280 is IPv6 minimum MTU
|
|
|
|
* 40 bytes is the size of the IPv6 header
|
|
|
|
* 8 bytes is the size of the fragment header
|
2018-11-27 18:05:23 -08:00
|
|
|
*/
|
2019-05-28 08:51:46 -07:00
|
|
|
export const PACKET_DATA_SIZE = 1280 - 40 - 8;
|
2018-11-27 18:05:23 -08:00
|
|
|
|
2019-11-16 08:28:14 -08:00
|
|
|
const PUBKEY_LENGTH = 32;
|
|
|
|
const SIGNATURE_LENGTH = 64;
|
|
|
|
|
2018-10-23 15:17:43 -07:00
|
|
|
/**
|
|
|
|
* List of TransactionInstruction object fields that may be initialized at construction
|
|
|
|
*
|
|
|
|
* @typedef {Object} TransactionInstructionCtorFields
|
|
|
|
* @property {?Array<PublicKey>} keys
|
|
|
|
* @property {?PublicKey} programId
|
2019-03-14 13:27:47 -07:00
|
|
|
* @property {?Buffer} data
|
2018-10-23 15:17:43 -07:00
|
|
|
*/
|
2019-09-13 17:07:13 -07:00
|
|
|
export type TransactionInstructionCtorFields = {|
|
2019-11-06 09:42:01 -08:00
|
|
|
keys?: Array<{pubkey: PublicKey, isSigner: boolean, isWritable: boolean}>,
|
2018-11-04 11:41:21 -08:00
|
|
|
programId?: PublicKey,
|
2019-03-14 13:27:47 -07:00
|
|
|
data?: Buffer,
|
2018-10-23 15:17:43 -07:00
|
|
|
|};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transaction Instruction class
|
|
|
|
*/
|
|
|
|
export class TransactionInstruction {
|
|
|
|
/**
|
|
|
|
* Public keys to include in this transaction
|
2019-04-10 12:31:50 -07:00
|
|
|
* Boolean represents whether this pubkey needs to sign the transaction
|
2018-10-23 15:17:43 -07:00
|
|
|
*/
|
2019-05-23 17:24:38 -07:00
|
|
|
keys: Array<{
|
|
|
|
pubkey: PublicKey,
|
|
|
|
isSigner: boolean,
|
2019-11-06 09:42:01 -08:00
|
|
|
isWritable: boolean,
|
2019-05-23 17:24:38 -07:00
|
|
|
}> = [];
|
2018-10-23 15:17:43 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Program Id to execute
|
|
|
|
*/
|
|
|
|
programId: PublicKey;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Program input
|
|
|
|
*/
|
2019-03-14 13:27:47 -07:00
|
|
|
data: Buffer = Buffer.alloc(0);
|
2018-10-23 15:17:43 -07:00
|
|
|
|
|
|
|
constructor(opts?: TransactionInstructionCtorFields) {
|
|
|
|
opts && Object.assign(this, opts);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-28 10:46:09 -08:00
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
type SignaturePubkeyPair = {|
|
|
|
|
signature: Buffer | null,
|
|
|
|
publicKey: PublicKey,
|
|
|
|
|};
|
|
|
|
|
2018-09-18 12:46:59 -07:00
|
|
|
/**
|
|
|
|
* List of Transaction object fields that may be initialized at construction
|
2018-09-20 15:35:41 -07:00
|
|
|
*
|
|
|
|
* @typedef {Object} TransactionCtorFields
|
2019-03-04 08:06:33 -08:00
|
|
|
* @property (?recentBlockhash} A recent block hash
|
2018-11-28 10:06:17 -08:00
|
|
|
* @property (?signatures} One or more signatures
|
|
|
|
*
|
2018-09-18 12:46:59 -07:00
|
|
|
*/
|
|
|
|
type TransactionCtorFields = {|
|
2019-05-23 16:18:13 -07:00
|
|
|
recentBlockhash?: Blockhash | null,
|
2020-01-06 17:12:04 -08:00
|
|
|
nonceInfo?: NonceInformation | null,
|
2018-11-28 10:06:17 -08:00
|
|
|
signatures?: Array<SignaturePubkeyPair>,
|
2018-09-18 12:46:59 -07:00
|
|
|
|};
|
|
|
|
|
2020-01-06 17:12:04 -08:00
|
|
|
/**
|
|
|
|
* NonceInformation to be used to build a Transaction.
|
|
|
|
*
|
|
|
|
* @typedef {Object} NonceInformation
|
|
|
|
* @property {nonce} The current Nonce blockhash
|
2020-01-13 14:31:02 -08:00
|
|
|
* @property {nonceInstruction} The AdvanceNonceAccount Instruction
|
2020-01-06 17:12:04 -08:00
|
|
|
*/
|
|
|
|
type NonceInformation = {|
|
|
|
|
nonce: Blockhash,
|
|
|
|
nonceInstruction: TransactionInstruction,
|
|
|
|
|};
|
|
|
|
|
2018-09-13 18:48:51 -07:00
|
|
|
/**
|
2018-10-23 15:17:43 -07:00
|
|
|
* Transaction class
|
2018-09-13 18:48:51 -07:00
|
|
|
*/
|
|
|
|
export class Transaction {
|
|
|
|
/**
|
2018-11-18 08:48:14 -08:00
|
|
|
* Signatures for the transaction. Typically created by invoking the
|
2018-11-28 10:06:17 -08:00
|
|
|
* `sign()` method
|
2018-09-13 18:48:51 -07:00
|
|
|
*/
|
2018-11-18 08:48:14 -08:00
|
|
|
signatures: Array<SignaturePubkeyPair> = [];
|
|
|
|
|
|
|
|
/**
|
2019-05-08 09:33:04 -07:00
|
|
|
* The first (payer) Transaction signature
|
2018-11-18 08:48:14 -08:00
|
|
|
*/
|
|
|
|
get signature(): Buffer | null {
|
|
|
|
if (this.signatures.length > 0) {
|
|
|
|
return this.signatures[0].signature;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2018-09-13 18:48:51 -07:00
|
|
|
|
2018-09-18 12:46:59 -07:00
|
|
|
/**
|
2018-10-23 15:17:43 -07:00
|
|
|
* The instructions to atomically execute
|
2018-09-18 12:46:59 -07:00
|
|
|
*/
|
2018-10-23 15:17:43 -07:00
|
|
|
instructions: Array<TransactionInstruction> = [];
|
2018-09-18 12:46:59 -07:00
|
|
|
|
2018-09-13 18:48:51 -07:00
|
|
|
/**
|
|
|
|
* A recent transaction id. Must be populated by the caller
|
|
|
|
*/
|
2019-05-23 16:18:13 -07:00
|
|
|
recentBlockhash: Blockhash | null;
|
2018-09-13 18:48:51 -07:00
|
|
|
|
2020-01-06 17:12:04 -08:00
|
|
|
/**
|
|
|
|
* Optional Nonce information. If populated, transaction will use a durable
|
|
|
|
* Nonce hash instead of a recentBlockhash. Must be populated by the caller
|
|
|
|
*/
|
|
|
|
nonceInfo: NonceInformation | null;
|
|
|
|
|
2018-10-23 19:13:59 -07:00
|
|
|
/**
|
|
|
|
* Construct an empty Transaction
|
|
|
|
*/
|
2018-09-18 12:46:59 -07:00
|
|
|
constructor(opts?: TransactionCtorFields) {
|
|
|
|
opts && Object.assign(this, opts);
|
|
|
|
}
|
|
|
|
|
2018-10-23 19:13:59 -07:00
|
|
|
/**
|
2018-11-28 10:06:17 -08:00
|
|
|
* Add one or more instructions to this Transaction
|
2018-10-23 19:13:59 -07:00
|
|
|
*/
|
2018-11-28 10:06:17 -08:00
|
|
|
add(
|
2019-05-23 16:18:13 -07:00
|
|
|
...items: Array<
|
|
|
|
Transaction | TransactionInstruction | TransactionInstructionCtorFields,
|
|
|
|
>
|
2018-11-28 10:06:17 -08:00
|
|
|
): Transaction {
|
|
|
|
if (items.length === 0) {
|
|
|
|
throw new Error('No instructions');
|
2018-10-23 15:17:43 -07:00
|
|
|
}
|
2018-11-28 10:06:17 -08:00
|
|
|
|
|
|
|
items.forEach(item => {
|
|
|
|
if (item instanceof Transaction) {
|
|
|
|
this.instructions = this.instructions.concat(item.instructions);
|
2019-05-23 16:18:13 -07:00
|
|
|
} else if (item instanceof TransactionInstruction) {
|
|
|
|
this.instructions.push(item);
|
2018-11-28 10:06:17 -08:00
|
|
|
} else {
|
|
|
|
this.instructions.push(new TransactionInstruction(item));
|
|
|
|
}
|
|
|
|
});
|
2018-10-23 15:17:43 -07:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2018-09-13 18:48:51 -07:00
|
|
|
/**
|
2020-04-23 14:37:49 -07:00
|
|
|
* Get a buffer of the Transaction data that need to be covered by signatures
|
2018-09-13 18:48:51 -07:00
|
|
|
*/
|
2020-04-24 09:20:27 -07:00
|
|
|
serializeMessage(): Buffer {
|
2020-01-06 17:12:04 -08:00
|
|
|
const {nonceInfo} = this;
|
2020-01-07 16:57:56 -08:00
|
|
|
if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) {
|
2020-01-06 17:12:04 -08:00
|
|
|
this.recentBlockhash = nonceInfo.nonce;
|
|
|
|
this.instructions.unshift(nonceInfo.nonceInstruction);
|
|
|
|
}
|
2019-03-04 08:06:33 -08:00
|
|
|
const {recentBlockhash} = this;
|
|
|
|
if (!recentBlockhash) {
|
|
|
|
throw new Error('Transaction recentBlockhash required');
|
2018-09-13 18:48:51 -07:00
|
|
|
}
|
2018-10-23 15:17:43 -07:00
|
|
|
|
2018-10-23 19:13:59 -07:00
|
|
|
if (this.instructions.length < 1) {
|
|
|
|
throw new Error('No instructions provided');
|
2018-10-23 15:17:43 -07:00
|
|
|
}
|
|
|
|
|
2018-11-18 08:48:14 -08:00
|
|
|
const keys = this.signatures.map(({publicKey}) => publicKey.toString());
|
2019-11-06 09:42:01 -08:00
|
|
|
let numReadonlySignedAccounts = 0;
|
|
|
|
let numReadonlyUnsignedAccounts = 0;
|
2019-01-31 01:16:07 -08:00
|
|
|
|
2019-05-24 15:07:16 -07:00
|
|
|
const programIds = [];
|
|
|
|
|
2020-01-07 16:57:56 -08:00
|
|
|
const allKeys = [];
|
2018-10-23 19:13:59 -07:00
|
|
|
this.instructions.forEach(instruction => {
|
2019-04-10 12:31:50 -07:00
|
|
|
instruction.keys.forEach(keySignerPair => {
|
2020-01-07 16:57:56 -08:00
|
|
|
allKeys.push(keySignerPair);
|
2019-04-10 12:31:50 -07:00
|
|
|
});
|
2019-05-23 09:42:11 -07:00
|
|
|
|
|
|
|
const programId = instruction.programId.toString();
|
2019-05-24 15:07:16 -07:00
|
|
|
if (!programIds.includes(programId)) {
|
|
|
|
programIds.push(programId);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-03-23 21:03:26 -07:00
|
|
|
allKeys.sort(function (x, y) {
|
2020-01-07 16:57:56 -08:00
|
|
|
const checkSigner = x.isSigner === y.isSigner ? 0 : x.isSigner ? -1 : 1;
|
2020-01-08 12:59:58 -08:00
|
|
|
const checkWritable =
|
|
|
|
x.isWritable === y.isWritable ? 0 : x.isWritable ? -1 : 1;
|
2020-01-07 16:57:56 -08:00
|
|
|
return checkSigner || checkWritable;
|
|
|
|
});
|
|
|
|
|
|
|
|
allKeys.forEach(keySignerPair => {
|
|
|
|
const keyStr = keySignerPair.pubkey.toString();
|
|
|
|
if (!keys.includes(keyStr)) {
|
|
|
|
if (keySignerPair.isSigner) {
|
|
|
|
this.signatures.push({
|
|
|
|
signature: null,
|
|
|
|
publicKey: keySignerPair.pubkey,
|
|
|
|
});
|
|
|
|
if (!keySignerPair.isWritable) {
|
|
|
|
numReadonlySignedAccounts += 1;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!keySignerPair.isWritable) {
|
|
|
|
numReadonlyUnsignedAccounts += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
keys.push(keyStr);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-05-24 15:07:16 -07:00
|
|
|
programIds.forEach(programId => {
|
2019-05-23 09:42:11 -07:00
|
|
|
if (!keys.includes(programId)) {
|
|
|
|
keys.push(programId);
|
2019-11-06 09:42:01 -08:00
|
|
|
numReadonlyUnsignedAccounts += 1;
|
2019-05-23 09:42:11 -07:00
|
|
|
}
|
2018-10-23 19:13:59 -07:00
|
|
|
});
|
|
|
|
|
2019-01-31 01:16:07 -08:00
|
|
|
let keyCount = [];
|
|
|
|
shortvec.encodeLength(keyCount, keys.length);
|
|
|
|
|
2018-10-23 19:13:59 -07:00
|
|
|
const instructions = this.instructions.map(instruction => {
|
2019-03-14 13:27:47 -07:00
|
|
|
const {data, programId} = instruction;
|
2019-01-31 01:16:07 -08:00
|
|
|
let keyIndicesCount = [];
|
|
|
|
shortvec.encodeLength(keyIndicesCount, instruction.keys.length);
|
2019-03-14 13:27:47 -07:00
|
|
|
let dataCount = [];
|
|
|
|
shortvec.encodeLength(dataCount, instruction.data.length);
|
2018-10-23 19:13:59 -07:00
|
|
|
return {
|
2019-05-23 09:42:11 -07:00
|
|
|
programIdIndex: keys.indexOf(programId.toString()),
|
2019-01-31 01:16:07 -08:00
|
|
|
keyIndicesCount: Buffer.from(keyIndicesCount),
|
|
|
|
keyIndices: Buffer.from(
|
2019-04-10 12:31:50 -07:00
|
|
|
instruction.keys.map(keyObj =>
|
|
|
|
keys.indexOf(keyObj.pubkey.toString()),
|
|
|
|
),
|
2019-01-31 01:16:07 -08:00
|
|
|
),
|
2019-03-14 13:27:47 -07:00
|
|
|
dataLength: Buffer.from(dataCount),
|
|
|
|
data,
|
2018-10-23 19:13:59 -07:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
instructions.forEach(instruction => {
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(instruction.programIdIndex >= 0);
|
|
|
|
instruction.keyIndices.forEach(keyIndex => invariant(keyIndex >= 0));
|
2018-10-23 19:13:59 -07:00
|
|
|
});
|
2018-10-10 10:46:15 -07:00
|
|
|
|
2019-01-31 01:16:07 -08:00
|
|
|
let instructionCount = [];
|
|
|
|
shortvec.encodeLength(instructionCount, instructions.length);
|
|
|
|
let instructionBuffer = Buffer.alloc(PACKET_DATA_SIZE);
|
|
|
|
Buffer.from(instructionCount).copy(instructionBuffer);
|
|
|
|
let instructionBufferLength = instructionCount.length;
|
2018-10-10 10:46:15 -07:00
|
|
|
|
2019-01-31 01:16:07 -08:00
|
|
|
instructions.forEach(instruction => {
|
|
|
|
const instructionLayout = BufferLayout.struct([
|
|
|
|
BufferLayout.u8('programIdIndex'),
|
|
|
|
|
|
|
|
BufferLayout.blob(
|
|
|
|
instruction.keyIndicesCount.length,
|
|
|
|
'keyIndicesCount',
|
|
|
|
),
|
|
|
|
BufferLayout.seq(
|
|
|
|
BufferLayout.u8('keyIndex'),
|
|
|
|
instruction.keyIndices.length,
|
|
|
|
'keyIndices',
|
|
|
|
),
|
2019-03-14 13:27:47 -07:00
|
|
|
BufferLayout.blob(instruction.dataLength.length, 'dataLength'),
|
2019-01-31 01:16:07 -08:00
|
|
|
BufferLayout.seq(
|
|
|
|
BufferLayout.u8('userdatum'),
|
2019-03-14 13:27:47 -07:00
|
|
|
instruction.data.length,
|
|
|
|
'data',
|
2019-01-31 01:16:07 -08:00
|
|
|
),
|
|
|
|
]);
|
|
|
|
const length = instructionLayout.encode(
|
|
|
|
instruction,
|
|
|
|
instructionBuffer,
|
|
|
|
instructionBufferLength,
|
|
|
|
);
|
|
|
|
instructionBufferLength += length;
|
|
|
|
});
|
|
|
|
instructionBuffer = instructionBuffer.slice(0, instructionBufferLength);
|
2018-09-13 18:48:51 -07:00
|
|
|
|
2018-10-06 11:13:58 -07:00
|
|
|
const signDataLayout = BufferLayout.struct([
|
2019-04-10 12:31:50 -07:00
|
|
|
BufferLayout.blob(1, 'numRequiredSignatures'),
|
2019-11-06 09:42:01 -08:00
|
|
|
BufferLayout.blob(1, 'numReadonlySignedAccounts'),
|
|
|
|
BufferLayout.blob(1, 'numReadonlyUnsignedAccounts'),
|
2019-01-31 01:16:07 -08:00
|
|
|
BufferLayout.blob(keyCount.length, 'keyCount'),
|
|
|
|
BufferLayout.seq(Layout.publicKey('key'), keys.length, 'keys'),
|
2019-03-04 08:06:33 -08:00
|
|
|
Layout.publicKey('recentBlockhash'),
|
2018-10-06 11:13:58 -07:00
|
|
|
]);
|
|
|
|
|
2018-10-10 10:46:15 -07:00
|
|
|
const transaction = {
|
2019-05-08 09:33:04 -07:00
|
|
|
numRequiredSignatures: Buffer.from([this.signatures.length]),
|
2019-11-06 09:42:01 -08:00
|
|
|
numReadonlySignedAccounts: Buffer.from([numReadonlySignedAccounts]),
|
|
|
|
numReadonlyUnsignedAccounts: Buffer.from([numReadonlyUnsignedAccounts]),
|
2019-01-31 01:16:07 -08:00
|
|
|
keyCount: Buffer.from(keyCount),
|
2018-11-04 11:41:21 -08:00
|
|
|
keys: keys.map(key => new PublicKey(key).toBuffer()),
|
2019-03-04 08:06:33 -08:00
|
|
|
recentBlockhash: Buffer.from(bs58.decode(recentBlockhash)),
|
2018-10-10 10:46:15 -07:00
|
|
|
};
|
2018-09-13 18:48:51 -07:00
|
|
|
|
2018-10-10 10:46:15 -07:00
|
|
|
let signData = Buffer.alloc(2048);
|
|
|
|
const length = signDataLayout.encode(transaction, signData);
|
2019-01-31 01:16:07 -08:00
|
|
|
instructionBuffer.copy(signData, length);
|
|
|
|
signData = signData.slice(0, length + instructionBuffer.length);
|
2018-10-10 10:46:15 -07:00
|
|
|
|
2018-10-06 11:13:58 -07:00
|
|
|
return signData;
|
2018-09-13 18:48:51 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-11-18 08:48:14 -08:00
|
|
|
* 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.
|
2018-09-13 18:48:51 -07:00
|
|
|
*
|
2019-03-04 08:06:33 -08:00
|
|
|
* The Transaction must be assigned a valid `recentBlockhash` before invoking this method
|
2018-09-13 18:48:51 -07:00
|
|
|
*/
|
2018-11-18 08:48:14 -08:00
|
|
|
sign(...signers: Array<Account>) {
|
2018-11-28 10:06:17 -08:00
|
|
|
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) {
|
2018-11-18 08:48:14 -08:00
|
|
|
throw new Error('No signers');
|
|
|
|
}
|
2018-11-28 10:06:17 -08:00
|
|
|
const signatures: Array<SignaturePubkeyPair> = partialSigners.map(
|
|
|
|
accountOrPublicKey => {
|
|
|
|
const publicKey =
|
|
|
|
accountOrPublicKey instanceof Account
|
|
|
|
? accountOrPublicKey.publicKey
|
|
|
|
: accountOrPublicKey;
|
|
|
|
return {
|
|
|
|
signature: null,
|
|
|
|
publicKey,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
);
|
2018-11-18 08:48:14 -08:00
|
|
|
this.signatures = signatures;
|
2020-04-24 09:20:27 -07:00
|
|
|
const signData = this.serializeMessage();
|
2018-11-18 08:48:14 -08:00
|
|
|
|
2018-11-28 10:06:17 -08:00
|
|
|
partialSigners.forEach((accountOrPublicKey, index) => {
|
|
|
|
if (accountOrPublicKey instanceof PublicKey) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const signature = nacl.sign.detached(
|
|
|
|
signData,
|
|
|
|
accountOrPublicKey.secretKey,
|
|
|
|
);
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(signature.length === 64);
|
2019-01-31 01:16:07 -08:00
|
|
|
signatures[index].signature = Buffer.from(signature);
|
2018-11-18 08:48:14 -08:00
|
|
|
});
|
2018-09-13 18:48:51 -07:00
|
|
|
}
|
|
|
|
|
2018-11-28 10:06:17 -08:00
|
|
|
/**
|
|
|
|
* 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) {
|
2020-04-24 09:20:27 -07:00
|
|
|
const signData = this.serializeMessage();
|
2020-04-23 14:44:19 -07:00
|
|
|
const signature = nacl.sign.detached(signData, signer.secretKey);
|
|
|
|
this.addSignature(signer.publicKey, signature);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an externally created signature to a transaction
|
|
|
|
*/
|
|
|
|
addSignature(pubkey: PublicKey, signature: Buffer) {
|
|
|
|
invariant(signature.length === 64);
|
|
|
|
|
2018-11-28 10:06:17 -08:00
|
|
|
const index = this.signatures.findIndex(sigpair =>
|
2020-04-23 14:44:19 -07:00
|
|
|
pubkey.equals(sigpair.publicKey),
|
2018-11-28 10:06:17 -08:00
|
|
|
);
|
|
|
|
if (index < 0) {
|
2020-04-23 14:44:19 -07:00
|
|
|
throw new Error(`Unknown signer: ${pubkey.toString()}`);
|
2018-11-28 10:06:17 -08:00
|
|
|
}
|
|
|
|
|
2019-01-31 01:16:07 -08:00
|
|
|
this.signatures[index].signature = Buffer.from(signature);
|
2018-11-28 10:06:17 -08:00
|
|
|
}
|
|
|
|
|
2020-03-03 12:39:00 -08:00
|
|
|
/**
|
|
|
|
* Verify signatures of a complete, signed Transaction
|
|
|
|
*/
|
|
|
|
verifySignatures(): boolean {
|
|
|
|
let verified = true;
|
2020-04-24 09:20:27 -07:00
|
|
|
const signData = this.serializeMessage();
|
2020-03-03 12:39:00 -08:00
|
|
|
for (const {signature, publicKey} of this.signatures) {
|
|
|
|
if (
|
|
|
|
!nacl.sign.detached.verify(signData, signature, publicKey.toBuffer())
|
|
|
|
) {
|
|
|
|
verified = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return verified;
|
|
|
|
}
|
|
|
|
|
2018-09-13 18:48:51 -07:00
|
|
|
/**
|
|
|
|
* Serialize the Transaction in the wire format.
|
|
|
|
*
|
|
|
|
* The Transaction must have a valid `signature` before invoking this method
|
|
|
|
*/
|
|
|
|
serialize(): Buffer {
|
2018-11-18 08:48:14 -08:00
|
|
|
const {signatures} = this;
|
|
|
|
if (!signatures) {
|
2018-09-13 18:48:51 -07:00
|
|
|
throw new Error('Transaction has not been signed');
|
|
|
|
}
|
|
|
|
|
2020-04-24 09:20:27 -07:00
|
|
|
const signData = this.serializeMessage();
|
2019-01-31 01:16:07 -08:00
|
|
|
const signatureCount = [];
|
|
|
|
shortvec.encodeLength(signatureCount, signatures.length);
|
|
|
|
const transactionLength =
|
|
|
|
signatureCount.length + signatures.length * 64 + signData.length;
|
2019-02-02 10:00:20 -08:00
|
|
|
const wireTransaction = Buffer.alloc(transactionLength);
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(signatures.length < 256);
|
2019-02-02 10:00:20 -08:00
|
|
|
Buffer.from(signatureCount).copy(wireTransaction, 0);
|
2018-11-18 08:48:14 -08:00
|
|
|
signatures.forEach(({signature}, index) => {
|
2019-08-30 16:22:11 -07:00
|
|
|
if (signature !== null) {
|
|
|
|
invariant(signature.length === 64, `signature has invalid length`);
|
|
|
|
Buffer.from(signature).copy(
|
|
|
|
wireTransaction,
|
|
|
|
signatureCount.length + index * 64,
|
|
|
|
);
|
|
|
|
}
|
2018-11-18 08:48:14 -08:00
|
|
|
});
|
2019-01-31 01:16:07 -08:00
|
|
|
signData.copy(
|
|
|
|
wireTransaction,
|
2019-02-02 10:00:20 -08:00
|
|
|
signatureCount.length + signatures.length * 64,
|
2019-01-31 01:16:07 -08:00
|
|
|
);
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(
|
2018-11-27 18:05:23 -08:00
|
|
|
wireTransaction.length <= PACKET_DATA_SIZE,
|
|
|
|
`Transaction too large: ${wireTransaction.length} > ${PACKET_DATA_SIZE}`,
|
2018-11-16 19:28:57 -08:00
|
|
|
);
|
2018-09-13 18:48:51 -07:00
|
|
|
return wireTransaction;
|
|
|
|
}
|
2018-10-23 15:17:43 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Deprecated method
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
get keys(): Array<PublicKey> {
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(this.instructions.length === 1);
|
2019-04-10 12:31:50 -07:00
|
|
|
return this.instructions[0].keys.map(keyObj => keyObj.pubkey);
|
2018-10-23 15:17:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deprecated method
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
get programId(): PublicKey {
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(this.instructions.length === 1);
|
2018-10-23 15:17:43 -07:00
|
|
|
return this.instructions[0].programId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deprecated method
|
|
|
|
* @private
|
|
|
|
*/
|
2019-03-14 13:27:47 -07:00
|
|
|
get data(): Buffer {
|
2018-11-18 08:48:14 -08:00
|
|
|
invariant(this.instructions.length === 1);
|
2019-03-14 13:27:47 -07:00
|
|
|
return this.instructions[0].data;
|
2018-10-23 15:17:43 -07:00
|
|
|
}
|
2019-01-31 01:16:07 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a wire transaction into a Transaction object.
|
|
|
|
*/
|
2020-02-12 16:25:22 -08:00
|
|
|
static from(buffer: Buffer | Uint8Array | Array<number>): Transaction {
|
2019-01-31 01:16:07 -08:00
|
|
|
// Slice up wire data
|
|
|
|
let byteArray = [...buffer];
|
|
|
|
|
|
|
|
const signatureCount = shortvec.decodeLength(byteArray);
|
|
|
|
let signatures = [];
|
|
|
|
for (let i = 0; i < signatureCount; i++) {
|
|
|
|
const signature = byteArray.slice(0, SIGNATURE_LENGTH);
|
|
|
|
byteArray = byteArray.slice(SIGNATURE_LENGTH);
|
2020-01-13 15:17:32 -08:00
|
|
|
signatures.push(bs58.encode(Buffer.from(signature)));
|
2019-01-31 01:16:07 -08:00
|
|
|
}
|
|
|
|
|
2019-05-23 17:24:38 -07:00
|
|
|
const numRequiredSignatures = byteArray.shift();
|
|
|
|
// byteArray = byteArray.slice(1); // Skip numRequiredSignatures byte
|
2019-11-06 09:42:01 -08:00
|
|
|
const numReadonlySignedAccounts = byteArray.shift();
|
|
|
|
// byteArray = byteArray.slice(1); // Skip numReadonlySignedAccounts byte
|
|
|
|
const numReadonlyUnsignedAccounts = byteArray.shift();
|
|
|
|
// byteArray = byteArray.slice(1); // Skip numReadonlyUnsignedAccounts byte
|
2019-04-10 12:31:50 -07:00
|
|
|
|
2019-01-31 01:16:07 -08:00
|
|
|
const accountCount = shortvec.decodeLength(byteArray);
|
|
|
|
let accounts = [];
|
|
|
|
for (let i = 0; i < accountCount; i++) {
|
|
|
|
const account = byteArray.slice(0, PUBKEY_LENGTH);
|
|
|
|
byteArray = byteArray.slice(PUBKEY_LENGTH);
|
2020-01-13 15:17:32 -08:00
|
|
|
accounts.push(bs58.encode(Buffer.from(account)));
|
2019-01-31 01:16:07 -08:00
|
|
|
}
|
|
|
|
|
2019-03-04 08:06:33 -08:00
|
|
|
const recentBlockhash = byteArray.slice(0, PUBKEY_LENGTH);
|
2019-01-31 01:16:07 -08:00
|
|
|
byteArray = byteArray.slice(PUBKEY_LENGTH);
|
|
|
|
|
|
|
|
const instructionCount = shortvec.decodeLength(byteArray);
|
|
|
|
let instructions = [];
|
|
|
|
for (let i = 0; i < instructionCount; i++) {
|
|
|
|
let instruction = {};
|
2020-04-01 12:11:10 -07:00
|
|
|
instruction.programIdIndex = byteArray.shift();
|
2019-11-16 08:28:14 -08:00
|
|
|
const accountCount = shortvec.decodeLength(byteArray);
|
|
|
|
instruction.accounts = byteArray.slice(0, accountCount);
|
|
|
|
byteArray = byteArray.slice(accountCount);
|
2019-03-14 13:27:47 -07:00
|
|
|
const dataLength = shortvec.decodeLength(byteArray);
|
2020-01-13 15:17:32 -08:00
|
|
|
const data = byteArray.slice(0, dataLength);
|
|
|
|
instruction.data = bs58.encode(Buffer.from(data));
|
2019-03-14 13:27:47 -07:00
|
|
|
byteArray = byteArray.slice(dataLength);
|
2019-01-31 01:16:07 -08:00
|
|
|
instructions.push(instruction);
|
|
|
|
}
|
|
|
|
|
2019-11-16 08:28:14 -08:00
|
|
|
return Transaction._populate(
|
|
|
|
signatures,
|
|
|
|
accounts,
|
|
|
|
instructions,
|
|
|
|
recentBlockhash,
|
|
|
|
numRequiredSignatures,
|
|
|
|
numReadonlySignedAccounts,
|
|
|
|
numReadonlyUnsignedAccounts,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse an RPC result into a Transaction object.
|
|
|
|
*/
|
|
|
|
static fromRpcResult(rpcResult: any): Transaction {
|
2020-01-13 15:17:32 -08:00
|
|
|
const signatures = rpcResult.signatures;
|
|
|
|
const accounts = rpcResult.message.accountKeys;
|
|
|
|
const instructions = rpcResult.message.instructions;
|
2019-11-19 14:38:06 -08:00
|
|
|
const recentBlockhash = rpcResult.message.recentBlockhash;
|
2019-11-16 08:28:14 -08:00
|
|
|
const numRequiredSignatures =
|
2019-11-19 14:38:06 -08:00
|
|
|
rpcResult.message.header.numRequiredSignatures;
|
2019-11-16 08:28:14 -08:00
|
|
|
const numReadonlySignedAccounts =
|
2019-11-19 14:38:06 -08:00
|
|
|
rpcResult.message.header.numReadonlySignedAccounts;
|
2019-11-16 08:28:14 -08:00
|
|
|
const numReadonlyUnsignedAccounts =
|
2019-11-19 14:38:06 -08:00
|
|
|
rpcResult.message.header.numReadonlyUnsignedAccounts;
|
2019-11-16 08:28:14 -08:00
|
|
|
return Transaction._populate(
|
|
|
|
signatures,
|
|
|
|
accounts,
|
|
|
|
instructions,
|
|
|
|
recentBlockhash,
|
|
|
|
numRequiredSignatures,
|
|
|
|
numReadonlySignedAccounts,
|
|
|
|
numReadonlyUnsignedAccounts,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Populate Transaction object
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
static _populate(
|
2020-01-13 15:17:32 -08:00
|
|
|
signatures: Array<string>,
|
|
|
|
accounts: Array<string>,
|
2019-11-16 08:28:14 -08:00
|
|
|
instructions: Array<any>,
|
|
|
|
recentBlockhash: Array<number>,
|
|
|
|
numRequiredSignatures: number,
|
|
|
|
numReadonlySignedAccounts: number,
|
|
|
|
numReadonlyUnsignedAccounts: number,
|
|
|
|
): Transaction {
|
|
|
|
function isWritable(
|
|
|
|
i: number,
|
|
|
|
numRequiredSignatures: number,
|
|
|
|
numReadonlySignedAccounts: number,
|
|
|
|
numReadonlyUnsignedAccounts: number,
|
|
|
|
numKeys: number,
|
|
|
|
): boolean {
|
|
|
|
return (
|
|
|
|
i < numRequiredSignatures - numReadonlySignedAccounts ||
|
|
|
|
(i >= numRequiredSignatures &&
|
|
|
|
i < numKeys - numReadonlyUnsignedAccounts)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const transaction = new Transaction();
|
2019-03-04 08:06:33 -08:00
|
|
|
transaction.recentBlockhash = new PublicKey(recentBlockhash).toBase58();
|
2019-11-16 08:28:14 -08:00
|
|
|
for (let i = 0; i < signatures.length; i++) {
|
2019-01-31 01:16:07 -08:00
|
|
|
const sigPubkeyPair = {
|
2019-08-30 16:22:11 -07:00
|
|
|
signature:
|
2020-01-13 15:17:32 -08:00
|
|
|
signatures[i] == bs58.encode(DEFAULT_SIGNATURE)
|
2019-08-30 16:22:11 -07:00
|
|
|
? null
|
2020-01-13 15:17:32 -08:00
|
|
|
: bs58.decode(signatures[i]),
|
2019-01-31 01:16:07 -08:00
|
|
|
publicKey: new PublicKey(accounts[i]),
|
|
|
|
};
|
|
|
|
transaction.signatures.push(sigPubkeyPair);
|
|
|
|
}
|
2019-11-16 08:28:14 -08:00
|
|
|
for (let i = 0; i < instructions.length; i++) {
|
2019-01-31 01:16:07 -08:00
|
|
|
let instructionData = {
|
|
|
|
keys: [],
|
2020-04-01 12:11:10 -07:00
|
|
|
programId: new PublicKey(accounts[instructions[i].programIdIndex]),
|
2020-01-13 15:17:32 -08:00
|
|
|
data: bs58.decode(instructions[i].data),
|
2019-01-31 01:16:07 -08:00
|
|
|
};
|
2019-11-16 08:28:14 -08:00
|
|
|
for (let j = 0; j < instructions[i].accounts.length; j++) {
|
|
|
|
const pubkey = new PublicKey(accounts[instructions[i].accounts[j]]);
|
2019-05-23 17:24:38 -07:00
|
|
|
|
2019-04-10 12:31:50 -07:00
|
|
|
instructionData.keys.push({
|
|
|
|
pubkey,
|
|
|
|
isSigner: transaction.signatures.some(
|
|
|
|
keyObj => keyObj.publicKey.toString() === pubkey.toString(),
|
|
|
|
),
|
2019-11-11 11:08:00 -08:00
|
|
|
isWritable: isWritable(
|
2019-05-23 17:24:38 -07:00
|
|
|
j,
|
|
|
|
numRequiredSignatures,
|
2019-11-06 09:42:01 -08:00
|
|
|
numReadonlySignedAccounts,
|
|
|
|
numReadonlyUnsignedAccounts,
|
2019-05-23 17:24:38 -07:00
|
|
|
accounts.length,
|
|
|
|
),
|
2019-04-10 12:31:50 -07:00
|
|
|
});
|
2019-01-31 01:16:07 -08:00
|
|
|
}
|
|
|
|
let instruction = new TransactionInstruction(instructionData);
|
|
|
|
transaction.instructions.push(instruction);
|
|
|
|
}
|
|
|
|
return transaction;
|
|
|
|
}
|
2018-09-14 08:27:40 -07:00
|
|
|
}
|