import bs58 from 'bs58'; import {Buffer} from 'buffer'; import {PACKET_DATA_SIZE, SIGNATURE_LENGTH_IN_BYTES} from './constants'; import {Connection} from '../connection'; import {Message} from '../message'; import {PublicKey} from '../publickey'; import * as shortvec from '../utils/shortvec-encoding'; import {toBuffer} from '../utils/to-buffer'; import invariant from '../utils/assert'; import type {Signer} from '../keypair'; import type {Blockhash} from '../blockhash'; import type {CompiledInstruction} from '../message'; import {sign, verify} from '../utils/ed25519'; /** * Transaction signature as base-58 encoded string */ export type TransactionSignature = string; export const enum TransactionStatus { BLOCKHEIGHT_EXCEEDED, PROCESSED, TIMED_OUT, NONCE_INVALID, } /** * Default (empty) signature */ const DEFAULT_SIGNATURE = Buffer.alloc(SIGNATURE_LENGTH_IN_BYTES).fill(0); /** * Account metadata used to define instructions */ export type AccountMeta = { /** An account's public key */ pubkey: PublicKey; /** True if an instruction requires a transaction signature matching `pubkey` */ isSigner: boolean; /** True if the `pubkey` can be loaded as a read-write account. */ isWritable: boolean; }; /** * List of TransactionInstruction object fields that may be initialized at construction */ export type TransactionInstructionCtorFields = { keys: Array; programId: PublicKey; data?: Buffer; }; /** * Configuration object for Transaction.serialize() */ export type SerializeConfig = { /** Require all transaction signatures be present (default: true) */ requireAllSignatures?: boolean; /** Verify provided signatures (default: true) */ verifySignatures?: boolean; }; /** * @internal */ export interface TransactionInstructionJSON { keys: { pubkey: string; isSigner: boolean; isWritable: boolean; }[]; programId: string; data: number[]; } /** * Transaction Instruction class */ export class TransactionInstruction { /** * Public keys to include in this transaction * Boolean represents whether this pubkey needs to sign the transaction */ keys: Array; /** * Program Id to execute */ programId: PublicKey; /** * Program input */ data: Buffer = Buffer.alloc(0); constructor(opts: TransactionInstructionCtorFields) { this.programId = opts.programId; this.keys = opts.keys; if (opts.data) { this.data = opts.data; } } /** * @internal */ toJSON(): TransactionInstructionJSON { return { keys: this.keys.map(({pubkey, isSigner, isWritable}) => ({ pubkey: pubkey.toJSON(), isSigner, isWritable, })), programId: this.programId.toJSON(), data: [...this.data], }; } } /** * Pair of signature and corresponding public key */ export type SignaturePubkeyPair = { signature: Buffer | null; publicKey: PublicKey; }; /** * List of Transaction object fields that may be initialized at construction */ export type TransactionCtorFields_DEPRECATED = { /** Optional nonce information used for offline nonce'd transactions */ nonceInfo?: NonceInformation | null; /** The transaction fee payer */ feePayer?: PublicKey | null; /** One or more signatures */ signatures?: Array; /** A recent blockhash */ recentBlockhash?: Blockhash; }; // For backward compatibility; an unfortunate consequence of being // forced to over-export types by the documentation generator. // See https://github.com/solana-labs/solana/pull/25820 export type TransactionCtorFields = TransactionCtorFields_DEPRECATED; /** * Blockhash-based transactions have a lifetime that are defined by * the blockhash they include. Any transaction whose blockhash is * too old will be rejected. */ export type TransactionBlockhashCtor = { /** The transaction fee payer */ feePayer?: PublicKey | null; /** One or more signatures */ signatures?: Array; /** A recent blockhash */ blockhash: Blockhash; /** the last block chain can advance to before tx is declared expired */ lastValidBlockHeight: number; }; /** * Use these options to construct a durable nonce transaction. */ export type TransactionNonceCtor = { /** The transaction fee payer */ feePayer?: PublicKey | null; minContextSlot: number; nonceInfo: NonceInformation; /** One or more signatures */ signatures?: Array; }; /** * Nonce information to be used to build an offline Transaction. */ export type NonceInformation = { /** The current blockhash stored in the nonce */ nonce: Blockhash; /** AdvanceNonceAccount Instruction */ nonceInstruction: TransactionInstruction; }; /** * @internal */ export interface TransactionJSON { recentBlockhash: string | null; feePayer: string | null; nonceInfo: { nonce: string; nonceInstruction: TransactionInstructionJSON; } | null; instructions: TransactionInstructionJSON[]; signers: string[]; } /** * Transaction class */ export class Transaction { /** * Signatures for the transaction. Typically created by invoking the * `sign()` method */ signatures: Array = []; /** * The first (payer) Transaction signature */ get signature(): Buffer | null { if (this.signatures.length > 0) { return this.signatures[0].signature; } return null; } /** * The transaction fee payer */ feePayer?: PublicKey; /** * The instructions to atomically execute */ instructions: Array = []; /** * A recent transaction id. Must be populated by the caller */ recentBlockhash?: Blockhash; /** * the last block chain can advance to before tx is declared expired * */ lastValidBlockHeight?: number; /** * Optional Nonce information. If populated, transaction will use a durable * Nonce hash instead of a recentBlockhash. Must be populated by the caller */ nonceInfo?: NonceInformation; /** * If this is a nonce transaction this represents the minimum slot from which * to evaluate if the nonce has advanced when attempting to confirm the * transaction. This protects against a case where the transaction confirmation * logic loads the nonce account from an old slot and assumes the mismatch in * nonce value implies that the nonce has been advanced. */ minNonceContextSlot?: number; /** * @internal */ _message?: Message; /** * @internal */ _json?: TransactionJSON; // Construct a transaction with a blockhash and lastValidBlockHeight constructor(opts?: TransactionBlockhashCtor); // Construct a transaction using a durable nonce constructor(opts?: TransactionNonceCtor); /** * @deprecated `TransactionCtorFields` has been deprecated and will be removed in a future version. * Please supply a `TransactionBlockhashCtor` instead. */ constructor(opts?: TransactionCtorFields_DEPRECATED); /** * Construct an empty Transaction */ constructor( opts?: | TransactionBlockhashCtor | TransactionNonceCtor | TransactionCtorFields_DEPRECATED, ) { if (!opts) { return; } if (opts.feePayer) { this.feePayer = opts.feePayer; } if (opts.signatures) { this.signatures = opts.signatures; } if (Object.prototype.hasOwnProperty.call(opts, 'nonceInfo')) { const {minContextSlot, nonceInfo} = opts as TransactionNonceCtor; this.minNonceContextSlot = minContextSlot; this.nonceInfo = nonceInfo; } else if ( Object.prototype.hasOwnProperty.call(opts, 'lastValidBlockHeight') ) { const {blockhash, lastValidBlockHeight} = opts as TransactionBlockhashCtor; this.recentBlockhash = blockhash; this.lastValidBlockHeight = lastValidBlockHeight; } else { const {recentBlockhash, nonceInfo} = opts as TransactionCtorFields_DEPRECATED; if (nonceInfo) { this.nonceInfo = nonceInfo; } this.recentBlockhash = recentBlockhash; } } /** * @internal */ toJSON(): TransactionJSON { return { recentBlockhash: this.recentBlockhash || null, feePayer: this.feePayer ? this.feePayer.toJSON() : null, nonceInfo: this.nonceInfo ? { nonce: this.nonceInfo.nonce, nonceInstruction: this.nonceInfo.nonceInstruction.toJSON(), } : null, instructions: this.instructions.map(instruction => instruction.toJSON()), signers: this.signatures.map(({publicKey}) => { return publicKey.toJSON(); }), }; } /** * Add one or more instructions to this Transaction */ add( ...items: Array< Transaction | TransactionInstruction | TransactionInstructionCtorFields > ): Transaction { if (items.length === 0) { throw new Error('No instructions'); } items.forEach((item: any) => { if ('instructions' in item) { this.instructions = this.instructions.concat(item.instructions); } else if ('data' in item && 'programId' in item && 'keys' in item) { this.instructions.push(item); } else { this.instructions.push(new TransactionInstruction(item)); } }); return this; } /** * Compile transaction data */ compileMessage(): Message { if ( this._message && JSON.stringify(this.toJSON()) === JSON.stringify(this._json) ) { return this._message; } let recentBlockhash; let instructions: TransactionInstruction[]; if (this.nonceInfo) { recentBlockhash = this.nonceInfo.nonce; if (this.instructions[0] != this.nonceInfo.nonceInstruction) { instructions = [this.nonceInfo.nonceInstruction, ...this.instructions]; } else { instructions = this.instructions; } } else { recentBlockhash = this.recentBlockhash; instructions = this.instructions; } if (!recentBlockhash) { throw new Error('Transaction recentBlockhash required'); } if (instructions.length < 1) { console.warn('No instructions provided'); } let feePayer: PublicKey; if (this.feePayer) { feePayer = this.feePayer; } else if (this.signatures.length > 0 && this.signatures[0].publicKey) { // Use implicit fee payer feePayer = this.signatures[0].publicKey; } else { throw new Error('Transaction fee payer required'); } for (let i = 0; i < instructions.length; i++) { if (instructions[i].programId === undefined) { throw new Error( `Transaction instruction index ${i} has undefined program id`, ); } } const programIds: string[] = []; const accountMetas: AccountMeta[] = []; instructions.forEach(instruction => { instruction.keys.forEach(accountMeta => { accountMetas.push({...accountMeta}); }); const programId = instruction.programId.toString(); if (!programIds.includes(programId)) { programIds.push(programId); } }); // Append programID account metas programIds.forEach(programId => { accountMetas.push({ pubkey: new PublicKey(programId), isSigner: false, isWritable: false, }); }); // Cull duplicate account metas const uniqueMetas: AccountMeta[] = []; accountMetas.forEach(accountMeta => { const pubkeyString = accountMeta.pubkey.toString(); const uniqueIndex = uniqueMetas.findIndex(x => { return x.pubkey.toString() === pubkeyString; }); if (uniqueIndex > -1) { uniqueMetas[uniqueIndex].isWritable = uniqueMetas[uniqueIndex].isWritable || accountMeta.isWritable; uniqueMetas[uniqueIndex].isSigner = uniqueMetas[uniqueIndex].isSigner || accountMeta.isSigner; } else { uniqueMetas.push(accountMeta); } }); // Sort. Prioritizing first by signer, then by writable uniqueMetas.sort(function (x, y) { if (x.isSigner !== y.isSigner) { // Signers always come before non-signers return x.isSigner ? -1 : 1; } if (x.isWritable !== y.isWritable) { // Writable accounts always come before read-only accounts return x.isWritable ? -1 : 1; } // Otherwise, sort by pubkey, stringwise. return x.pubkey.toBase58().localeCompare(y.pubkey.toBase58()); }); // Move fee payer to the front const feePayerIndex = uniqueMetas.findIndex(x => { return x.pubkey.equals(feePayer); }); if (feePayerIndex > -1) { const [payerMeta] = uniqueMetas.splice(feePayerIndex, 1); payerMeta.isSigner = true; payerMeta.isWritable = true; uniqueMetas.unshift(payerMeta); } else { uniqueMetas.unshift({ pubkey: feePayer, isSigner: true, isWritable: true, }); } // Disallow unknown signers for (const signature of this.signatures) { const uniqueIndex = uniqueMetas.findIndex(x => { return x.pubkey.equals(signature.publicKey); }); if (uniqueIndex > -1) { if (!uniqueMetas[uniqueIndex].isSigner) { uniqueMetas[uniqueIndex].isSigner = true; console.warn( 'Transaction references a signature that is unnecessary, ' + 'only the fee payer and instruction signer accounts should sign a transaction. ' + 'This behavior is deprecated and will throw an error in the next major version release.', ); } } else { throw new Error(`unknown signer: ${signature.publicKey.toString()}`); } } let numRequiredSignatures = 0; let numReadonlySignedAccounts = 0; let numReadonlyUnsignedAccounts = 0; // Split out signing from non-signing keys and count header values const signedKeys: string[] = []; const unsignedKeys: string[] = []; uniqueMetas.forEach(({pubkey, isSigner, isWritable}) => { if (isSigner) { signedKeys.push(pubkey.toString()); numRequiredSignatures += 1; if (!isWritable) { numReadonlySignedAccounts += 1; } } else { unsignedKeys.push(pubkey.toString()); if (!isWritable) { numReadonlyUnsignedAccounts += 1; } } }); const accountKeys = signedKeys.concat(unsignedKeys); const compiledInstructions: CompiledInstruction[] = instructions.map( instruction => { const {data, programId} = instruction; return { programIdIndex: accountKeys.indexOf(programId.toString()), accounts: instruction.keys.map(meta => accountKeys.indexOf(meta.pubkey.toString()), ), data: bs58.encode(data), }; }, ); compiledInstructions.forEach(instruction => { invariant(instruction.programIdIndex >= 0); instruction.accounts.forEach(keyIndex => invariant(keyIndex >= 0)); }); return new Message({ header: { numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts, }, accountKeys, recentBlockhash, instructions: compiledInstructions, }); } /** * @internal */ _compile(): Message { const message = this.compileMessage(); const signedKeys = message.accountKeys.slice( 0, message.header.numRequiredSignatures, ); if (this.signatures.length === signedKeys.length) { const valid = this.signatures.every((pair, index) => { return signedKeys[index].equals(pair.publicKey); }); if (valid) return message; } this.signatures = signedKeys.map(publicKey => ({ signature: null, publicKey, })); return message; } /** * Get a buffer of the Transaction data that need to be covered by signatures */ serializeMessage(): Buffer { return this._compile().serialize(); } /** * Get the estimated fee associated with a transaction */ async getEstimatedFee(connection: Connection): Promise { return (await connection.getFeeForMessage(this.compileMessage())).value; } /** * Specify the public keys which will be used to sign the Transaction. * The first signer will be used as the transaction fee payer account. * * Signatures can be added with either `partialSign` or `addSignature` * * @deprecated Deprecated since v0.84.0. Only the fee payer needs to be * specified and it can be set in the Transaction constructor or with the * `feePayer` property. */ setSigners(...signers: Array) { if (signers.length === 0) { throw new Error('No signers'); } const seen = new Set(); this.signatures = signers .filter(publicKey => { const key = publicKey.toString(); if (seen.has(key)) { return false; } else { seen.add(key); return true; } }) .map(publicKey => ({signature: null, publicKey})); } /** * Sign the Transaction with the specified signers. Multiple signatures may * be applied to a Transaction. The first signature is considered "primary" * and is used identify and confirm transactions. * * If the Transaction `feePayer` is not set, the first signer will be used * as the transaction fee payer account. * * 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 `recentBlockhash` before invoking this method */ sign(...signers: Array) { if (signers.length === 0) { throw new Error('No signers'); } // Dedupe signers const seen = new Set(); const uniqueSigners = []; for (const signer of signers) { const key = signer.publicKey.toString(); if (seen.has(key)) { continue; } else { seen.add(key); uniqueSigners.push(signer); } } this.signatures = uniqueSigners.map(signer => ({ signature: null, publicKey: signer.publicKey, })); const message = this._compile(); this._partialSign(message, ...uniqueSigners); } /** * Partially sign a transaction with the specified accounts. All accounts must * correspond to either the fee payer or a signer account in the transaction * instructions. * * All the caveats from the `sign` method apply to `partialSign` */ partialSign(...signers: Array) { if (signers.length === 0) { throw new Error('No signers'); } // Dedupe signers const seen = new Set(); const uniqueSigners = []; for (const signer of signers) { const key = signer.publicKey.toString(); if (seen.has(key)) { continue; } else { seen.add(key); uniqueSigners.push(signer); } } const message = this._compile(); this._partialSign(message, ...uniqueSigners); } /** * @internal */ _partialSign(message: Message, ...signers: Array) { const signData = message.serialize(); signers.forEach(signer => { const signature = sign(signData, signer.secretKey); this._addSignature(signer.publicKey, toBuffer(signature)); }); } /** * Add an externally created signature to a transaction. The public key * must correspond to either the fee payer or a signer account in the transaction * instructions. */ addSignature(pubkey: PublicKey, signature: Buffer) { this._compile(); // Ensure signatures array is populated this._addSignature(pubkey, signature); } /** * @internal */ _addSignature(pubkey: PublicKey, signature: Buffer) { invariant(signature.length === 64); const index = this.signatures.findIndex(sigpair => pubkey.equals(sigpair.publicKey), ); if (index < 0) { throw new Error(`unknown signer: ${pubkey.toString()}`); } this.signatures[index].signature = Buffer.from(signature); } /** * Verify signatures of a Transaction * Optional parameter specifies if we're expecting a fully signed Transaction or a partially signed one. * If no boolean is provided, we expect a fully signed Transaction by default. */ verifySignatures(requireAllSignatures?: boolean): boolean { return this._verifySignatures( this.serializeMessage(), requireAllSignatures === undefined ? true : requireAllSignatures, ); } /** * @internal */ _verifySignatures( signData: Uint8Array, requireAllSignatures: boolean, ): boolean { for (const {signature, publicKey} of this.signatures) { if (signature === null) { if (requireAllSignatures) { return false; } } else { if (!verify(signature, signData, publicKey.toBytes())) { return false; } } } return true; } /** * Serialize the Transaction in the wire format. */ serialize(config?: SerializeConfig): Buffer { const {requireAllSignatures, verifySignatures} = Object.assign( {requireAllSignatures: true, verifySignatures: true}, config, ); const signData = this.serializeMessage(); if ( verifySignatures && !this._verifySignatures(signData, requireAllSignatures) ) { throw new Error('Signature verification failed'); } return this._serialize(signData); } /** * @internal */ _serialize(signData: Buffer): Buffer { const {signatures} = this; const signatureCount: number[] = []; 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) => { if (signature !== null) { 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 * @internal */ get keys(): Array { invariant(this.instructions.length === 1); return this.instructions[0].keys.map(keyObj => keyObj.pubkey); } /** * Deprecated method * @internal */ get programId(): PublicKey { invariant(this.instructions.length === 1); return this.instructions[0].programId; } /** * Deprecated method * @internal */ get data(): Buffer { invariant(this.instructions.length === 1); return this.instructions[0].data; } /** * Parse a wire transaction into a Transaction object. */ static from(buffer: Buffer | Uint8Array | Array): 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_IN_BYTES); byteArray = byteArray.slice(SIGNATURE_LENGTH_IN_BYTES); signatures.push(bs58.encode(Buffer.from(signature))); } return Transaction.populate(Message.from(byteArray), signatures); } /** * Populate Transaction object from message and signatures */ static populate( message: Message, signatures: Array = [], ): Transaction { const transaction = new Transaction(); transaction.recentBlockhash = message.recentBlockhash; if (message.header.numRequiredSignatures > 0) { transaction.feePayer = message.accountKeys[0]; } signatures.forEach((signature, index) => { const sigPubkeyPair = { signature: signature == bs58.encode(DEFAULT_SIGNATURE) ? null : bs58.decode(signature), publicKey: message.accountKeys[index], }; transaction.signatures.push(sigPubkeyPair); }); message.instructions.forEach(instruction => { const keys = instruction.accounts.map(account => { const pubkey = message.accountKeys[account]; return { pubkey, isSigner: transaction.signatures.some( keyObj => keyObj.publicKey.toString() === pubkey.toString(), ) || message.isAccountSigner(account), isWritable: message.isAccountWritable(account), }; }); transaction.instructions.push( new TransactionInstruction({ keys, programId: message.accountKeys[instruction.programIdIndex], data: bs58.decode(instruction.data), }), ); }); transaction._message = message; transaction._json = transaction.toJSON(); return transaction; } }