solana/web3.js/src/transaction.js

498 lines
14 KiB
JavaScript
Raw Normal View History

// @flow
import invariant from 'assert';
import * as BufferLayout from 'buffer-layout';
import nacl from 'tweetnacl';
import bs58 from 'bs58';
import * as Layout from './layout';
import {PublicKey} from './publickey';
import {Account} from './account';
import * as shortvec from './util/shortvec-encoding';
2019-03-04 08:06:33 -08:00
import type {Blockhash} from './blockhash';
/**
* @typedef {string} TransactionSignature
*/
export type TransactionSignature = string;
/**
* Maximum over-the-wire size of a Transaction
*/
export const PACKET_DATA_SIZE = 512;
/**
* List of TransactionInstruction object fields that may be initialized at construction
*
* @typedef {Object} TransactionInstructionCtorFields
* @property {?Array<PublicKey>} keys
* @property {?PublicKey} programId
* @property {?Buffer} userdata
*/
type TransactionInstructionCtorFields = {|
2018-11-04 11:41:21 -08:00
keys?: Array<PublicKey>,
programId?: PublicKey,
userdata?: Buffer,
|};
/**
* Transaction Instruction class
*/
export class TransactionInstruction {
/**
* Public keys to include in this transaction
*/
keys: Array<PublicKey> = [];
/**
* Program Id to execute
*/
programId: PublicKey;
/**
* Program input
*/
userdata: Buffer = Buffer.alloc(0);
constructor(opts?: TransactionInstructionCtorFields) {
opts && Object.assign(this, opts);
}
}
/**
* @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
* @property {?number} fee
2019-03-04 08:06:33 -08:00
* @property (?recentBlockhash} A recent block hash
* @property (?signatures} One or more signatures
*
2018-09-18 12:46:59 -07:00
*/
type TransactionCtorFields = {|
2018-11-04 11:41:21 -08:00
fee?: number,
2019-03-04 08:06:33 -08:00
recentBlockhash?: Blockhash,
signatures?: Array<SignaturePubkeyPair>,
2018-09-18 12:46:59 -07:00
|};
/**
* Transaction class
*/
export class Transaction {
/**
* Signatures for the transaction. Typically created by invoking the
* `sign()` method
*/
signatures: Array<SignaturePubkeyPair> = [];
/**
* The first (primary) Transaction signature
*/
get signature(): Buffer | null {
if (this.signatures.length > 0) {
return this.signatures[0].signature;
}
return null;
}
2018-09-18 12:46:59 -07:00
/**
* The instructions to atomically execute
2018-09-18 12:46:59 -07:00
*/
instructions: Array<TransactionInstruction> = [];
2018-09-18 12:46:59 -07:00
/**
* A recent transaction id. Must be populated by the caller
*/
2019-03-04 08:06:33 -08:00
recentBlockhash: ?Blockhash;
/**
* Fee for this transaction
*/
fee: number = 1;
/**
* Construct an empty Transaction
*/
2018-09-18 12:46:59 -07:00
constructor(opts?: TransactionCtorFields) {
opts && Object.assign(this, opts);
}
/**
* Add one or more instructions to this Transaction
*/
add(
...items: Array<Transaction | TransactionInstructionCtorFields>
): Transaction {
if (items.length === 0) {
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;
}
/**
* @private
*/
_getSignData(): Buffer {
2019-03-04 08:06:33 -08:00
const {recentBlockhash} = this;
if (!recentBlockhash) {
throw new Error('Transaction recentBlockhash required');
}
if (this.instructions.length < 1) {
throw new Error('No instructions provided');
}
const keys = this.signatures.map(({publicKey}) => publicKey.toString());
const programIds = [];
this.instructions.forEach(instruction => {
const programId = instruction.programId.toString();
if (!programIds.includes(programId)) {
programIds.push(programId);
}
2018-12-19 19:28:28 -08:00
instruction.keys
.map(key => key.toString())
.forEach(key => {
if (!keys.includes(key)) {
keys.push(key);
}
});
});
let keyCount = [];
shortvec.encodeLength(keyCount, keys.length);
let programIdCount = [];
shortvec.encodeLength(programIdCount, programIds.length);
const instructions = this.instructions.map(instruction => {
const {userdata, programId} = instruction;
let keyIndicesCount = [];
shortvec.encodeLength(keyIndicesCount, instruction.keys.length);
let userdataCount = [];
shortvec.encodeLength(userdataCount, instruction.userdata.length);
return {
programIdIndex: programIds.indexOf(programId.toString()),
keyIndicesCount: Buffer.from(keyIndicesCount),
keyIndices: Buffer.from(
instruction.keys.map(key => keys.indexOf(key.toString())),
),
userdataLength: Buffer.from(userdataCount),
userdata,
};
});
instructions.forEach(instruction => {
invariant(instruction.programIdIndex >= 0);
instruction.keyIndices.forEach(keyIndex => invariant(keyIndex >= 0));
});
let instructionCount = [];
shortvec.encodeLength(instructionCount, instructions.length);
let instructionBuffer = Buffer.alloc(PACKET_DATA_SIZE);
Buffer.from(instructionCount).copy(instructionBuffer);
let instructionBufferLength = instructionCount.length;
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',
),
BufferLayout.blob(instruction.userdataLength.length, 'userdataLength'),
BufferLayout.seq(
BufferLayout.u8('userdatum'),
instruction.userdata.length,
'userdata',
),
]);
const length = instructionLayout.encode(
instruction,
instructionBuffer,
instructionBufferLength,
);
instructionBufferLength += length;
});
instructionBuffer = instructionBuffer.slice(0, instructionBufferLength);
const signDataLayout = BufferLayout.struct([
BufferLayout.blob(keyCount.length, 'keyCount'),
BufferLayout.seq(Layout.publicKey('key'), keys.length, 'keys'),
2019-03-04 08:06:33 -08:00
Layout.publicKey('recentBlockhash'),
BufferLayout.ns64('fee'),
BufferLayout.blob(programIdCount.length, 'programIdCount'),
BufferLayout.seq(
Layout.publicKey('programId'),
programIds.length,
2018-11-04 11:41:21 -08:00
'programIds',
),
]);
const transaction = {
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)),
fee: this.fee,
programIdCount: Buffer.from(programIdCount),
2018-11-04 11:41:21 -08:00
programIds: programIds.map(programId =>
new PublicKey(programId).toBuffer(),
),
};
let signData = Buffer.alloc(2048);
const length = signDataLayout.encode(transaction, signData);
instructionBuffer.copy(signData, length);
signData = signData.slice(0, length + instructionBuffer.length);
return signData;
}
/**
* 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.
*
2019-03-04 08:06:33 -08:00
* The Transaction must be assigned a valid `recentBlockhash` before invoking this method
*/
sign(...signers: Array<Account>) {
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');
}
const signatures: Array<SignaturePubkeyPair> = partialSigners.map(
accountOrPublicKey => {
const publicKey =
accountOrPublicKey instanceof Account
? accountOrPublicKey.publicKey
: accountOrPublicKey;
return {
signature: null,
publicKey,
};
},
);
this.signatures = signatures;
const signData = this._getSignData();
partialSigners.forEach((accountOrPublicKey, index) => {
if (accountOrPublicKey instanceof PublicKey) {
return;
}
const signature = nacl.sign.detached(
signData,
accountOrPublicKey.secretKey,
);
invariant(signature.length === 64);
signatures[index].signature = Buffer.from(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 = Buffer.from(signature);
}
/**
* Serialize the Transaction in the wire format.
*
* The Transaction must have a valid `signature` before invoking this method
*/
serialize(): Buffer {
const {signatures} = this;
if (!signatures) {
throw new Error('Transaction has not been signed');
}
const signData = this._getSignData();
const signatureCount = [];
shortvec.encodeLength(signatureCount, signatures.length);
const transactionLength =
signatureCount.length + signatures.length * 64 + signData.length;
const wireTransaction = Buffer.alloc(transactionLength);
invariant(signatures.length < 256);
Buffer.from(signatureCount).copy(wireTransaction, 0);
signatures.forEach(({signature}, index) => {
invariant(signature !== null, `null signature`);
invariant(signature.length === 64, `signature has invalid length`);
Buffer.from(signature).copy(
wireTransaction,
signatureCount.length + index * 64,
);
});
signData.copy(
wireTransaction,
signatureCount.length + signatures.length * 64,
);
invariant(
wireTransaction.length <= PACKET_DATA_SIZE,
`Transaction too large: ${wireTransaction.length} > ${PACKET_DATA_SIZE}`,
);
return wireTransaction;
}
/**
* Deprecated method
* @private
*/
get keys(): Array<PublicKey> {
invariant(this.instructions.length === 1);
return this.instructions[0].keys;
}
/**
* Deprecated method
* @private
*/
get programId(): PublicKey {
invariant(this.instructions.length === 1);
return this.instructions[0].programId;
}
/**
* Deprecated method
* @private
*/
get userdata(): Buffer {
invariant(this.instructions.length === 1);
return this.instructions[0].userdata;
}
/**
* Parse a wire transaction into a Transaction object.
*/
static from(buffer: Buffer): Transaction {
const PUBKEY_LENGTH = 32;
const SIGNATURE_LENGTH = 64;
let transaction = new Transaction();
// 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);
signatures.push(signature);
}
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);
accounts.push(account);
}
2019-03-04 08:06:33 -08:00
const recentBlockhash = byteArray.slice(0, PUBKEY_LENGTH);
byteArray = byteArray.slice(PUBKEY_LENGTH);
let fee = 0;
for (let i = 0; i < 8; i++) {
fee += byteArray.shift() >> (8 * i);
}
const programIdCount = shortvec.decodeLength(byteArray);
let programs = [];
for (let i = 0; i < programIdCount; i++) {
const program = byteArray.slice(0, PUBKEY_LENGTH);
byteArray = byteArray.slice(PUBKEY_LENGTH);
programs.push(program);
}
const instructionCount = shortvec.decodeLength(byteArray);
let instructions = [];
for (let i = 0; i < instructionCount; i++) {
let instruction = {};
instruction.programIndex = byteArray.shift();
const accountIndexCount = shortvec.decodeLength(byteArray);
instruction.accountIndex = byteArray.slice(0, accountIndexCount);
byteArray = byteArray.slice(accountIndexCount);
const userdataLength = shortvec.decodeLength(byteArray);
instruction.userdata = byteArray.slice(0, userdataLength);
byteArray = byteArray.slice(userdataLength);
instructions.push(instruction);
}
// Populate Transaction object
2019-03-04 08:06:33 -08:00
transaction.recentBlockhash = new PublicKey(recentBlockhash).toBase58();
transaction.fee = fee;
for (let i = 0; i < signatureCount; i++) {
const sigPubkeyPair = {
signature: Buffer.from(signatures[i]),
publicKey: new PublicKey(accounts[i]),
};
transaction.signatures.push(sigPubkeyPair);
}
for (let i = 0; i < instructionCount; i++) {
let instructionData = {
keys: [],
programId: new PublicKey(programs[instructions[i].programIndex]),
userdata: Buffer.from(instructions[i].userdata),
};
for (let j = 0; j < instructions[i].accountIndex.length; j++) {
instructionData.keys.push(
new PublicKey(accounts[instructions[i].accountIndex[j]]),
);
}
let instruction = new TransactionInstruction(instructionData);
transaction.instructions.push(instruction);
}
return transaction;
}
2018-09-14 08:27:40 -07:00
}