UNPKG

@solana/transactions

Version:

Helpers for creating and serializing transactions

306 lines (301 loc) • 13.8 kB
'use strict'; var addresses = require('@solana/addresses'); var codecsCore = require('@solana/codecs-core'); var codecsDataStructures = require('@solana/codecs-data-structures'); var codecsNumbers = require('@solana/codecs-numbers'); var errors = require('@solana/errors'); var transactionMessages = require('@solana/transaction-messages'); var rpcTypes = require('@solana/rpc-types'); var codecsStrings = require('@solana/codecs-strings'); var keys = require('@solana/keys'); // src/codecs/transaction-codec.ts function getSignaturesToEncode(signaturesMap) { const signatures = Object.values(signaturesMap); if (signatures.length === 0) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES); } return signatures.map((signature) => { if (!signature) { return new Uint8Array(64).fill(0); } return signature; }); } function getSignaturesEncoder() { return codecsCore.transformEncoder( codecsDataStructures.getArrayEncoder(codecsCore.fixEncoderSize(codecsDataStructures.getBytesEncoder(), 64), { size: codecsNumbers.getShortU16Encoder() }), getSignaturesToEncode ); } // src/codecs/transaction-codec.ts function getTransactionEncoder() { return codecsDataStructures.getStructEncoder([ ["signatures", getSignaturesEncoder()], ["messageBytes", codecsDataStructures.getBytesEncoder()] ]); } function getTransactionDecoder() { return codecsCore.transformDecoder( codecsDataStructures.getStructDecoder([ ["signatures", codecsDataStructures.getArrayDecoder(codecsCore.fixDecoderSize(codecsDataStructures.getBytesDecoder(), 64), { size: codecsNumbers.getShortU16Decoder() })], ["messageBytes", codecsDataStructures.getBytesDecoder()] ]), decodePartiallyDecodedTransaction ); } function getTransactionCodec() { return codecsCore.combineCodec(getTransactionEncoder(), getTransactionDecoder()); } function decodePartiallyDecodedTransaction(transaction) { const { messageBytes, signatures } = transaction; const signerAddressesDecoder = codecsDataStructures.getTupleDecoder([ // read transaction version transactionMessages.getTransactionVersionDecoder(), // read first byte of header, `numSignerAccounts` // padRight to skip the next 2 bytes, `numReadOnlySignedAccounts` and `numReadOnlyUnsignedAccounts` which we don't need codecsCore.padRightDecoder(codecsNumbers.getU8Decoder(), 2), // read static addresses codecsDataStructures.getArrayDecoder(addresses.getAddressDecoder(), { size: codecsNumbers.getShortU16Decoder() }) ]); const [_txVersion, numRequiredSignatures, staticAddresses] = signerAddressesDecoder.decode(messageBytes); const signerAddresses = staticAddresses.slice(0, numRequiredSignatures); if (signerAddresses.length !== signatures.length) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, { numRequiredSignatures, signaturesLength: signatures.length, signerAddresses }); } const signaturesMap = {}; signerAddresses.forEach((address, index) => { const signatureForAddress = signatures[index]; if (signatureForAddress.every((b) => b === 0)) { signaturesMap[address] = null; } else { signaturesMap[address] = signatureForAddress; } }); return { messageBytes, signatures: Object.freeze(signaturesMap) }; } var SYSTEM_PROGRAM_ADDRESS = "11111111111111111111111111111111"; function compiledInstructionIsAdvanceNonceInstruction(instruction, staticAddresses) { return staticAddresses[instruction.programAddressIndex] === SYSTEM_PROGRAM_ADDRESS && // Test for `AdvanceNonceAccount` instruction data instruction.data != null && isAdvanceNonceAccountInstructionData(instruction.data) && // Test for exactly 3 accounts instruction.accountIndices?.length === 3; } function isAdvanceNonceAccountInstructionData(data) { return data.byteLength === 4 && data[0] === 4 && data[1] === 0 && data[2] === 0 && data[3] === 0; } async function getTransactionLifetimeConstraintFromCompiledTransactionMessage(compiledTransactionMessage) { const firstInstruction = compiledTransactionMessage.instructions[0]; const { staticAccounts } = compiledTransactionMessage; if (firstInstruction && compiledInstructionIsAdvanceNonceInstruction(firstInstruction, staticAccounts)) { const nonceAccountAddress = staticAccounts[firstInstruction.accountIndices[0]]; if (!nonceAccountAddress) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE, { nonce: compiledTransactionMessage.lifetimeToken }); } return { nonce: compiledTransactionMessage.lifetimeToken, nonceAccountAddress }; } else { return { blockhash: compiledTransactionMessage.lifetimeToken, // This is not known from the compiled message, so we set it to the maximum possible value lastValidBlockHeight: 0xffffffffffffffffn }; } } function isTransactionWithBlockhashLifetime(transaction) { return "lifetimeConstraint" in transaction && "blockhash" in transaction.lifetimeConstraint && typeof transaction.lifetimeConstraint.blockhash === "string" && typeof transaction.lifetimeConstraint.lastValidBlockHeight === "bigint" && rpcTypes.isBlockhash(transaction.lifetimeConstraint.blockhash); } function assertIsTransactionWithBlockhashLifetime(transaction) { if (!isTransactionWithBlockhashLifetime(transaction)) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME); } } function isTransactionWithDurableNonceLifetime(transaction) { return "lifetimeConstraint" in transaction && "nonce" in transaction.lifetimeConstraint && typeof transaction.lifetimeConstraint.nonce === "string" && typeof transaction.lifetimeConstraint.nonceAccountAddress === "string" && addresses.isAddress(transaction.lifetimeConstraint.nonceAccountAddress); } function assertIsTransactionWithDurableNonceLifetime(transaction) { if (!isTransactionWithDurableNonceLifetime(transaction)) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME); } } function compileTransaction(transactionMessage) { const compiledMessage = transactionMessages.compileTransactionMessage(transactionMessage); const messageBytes = transactionMessages.getCompiledTransactionMessageEncoder().encode(compiledMessage); const transactionSigners = compiledMessage.staticAccounts.slice(0, compiledMessage.header.numSignerAccounts); const signatures = {}; for (const signerAddress of transactionSigners) { signatures[signerAddress] = null; } let lifetimeConstraint; if (transactionMessages.isTransactionMessageWithBlockhashLifetime(transactionMessage)) { lifetimeConstraint = { blockhash: transactionMessage.lifetimeConstraint.blockhash, lastValidBlockHeight: transactionMessage.lifetimeConstraint.lastValidBlockHeight }; } else if (transactionMessages.isTransactionMessageWithDurableNonceLifetime(transactionMessage)) { lifetimeConstraint = { nonce: transactionMessage.lifetimeConstraint.nonce, nonceAccountAddress: transactionMessage.instructions[0].accounts[0].address }; } return Object.freeze({ ...lifetimeConstraint ? { lifetimeConstraint } : void 0, messageBytes, signatures: Object.freeze(signatures) }); } var base58Decoder; function getSignatureFromTransaction(transaction) { if (!base58Decoder) base58Decoder = codecsStrings.getBase58Decoder(); const signatureBytes = Object.values(transaction.signatures)[0]; if (!signatureBytes) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING); } const transactionSignature = base58Decoder.decode(signatureBytes); return transactionSignature; } async function partiallySignTransaction(keyPairs, transaction) { let newSignatures; let unexpectedSigners; await Promise.all( keyPairs.map(async (keyPair) => { const address = await addresses.getAddressFromPublicKey(keyPair.publicKey); const existingSignature = transaction.signatures[address]; if (existingSignature === void 0) { unexpectedSigners ||= /* @__PURE__ */ new Set(); unexpectedSigners.add(address); return; } if (unexpectedSigners) { return; } const newSignature = await keys.signBytes(keyPair.privateKey, transaction.messageBytes); if (existingSignature !== null && codecsCore.bytesEqual(newSignature, existingSignature)) { return; } newSignatures ||= {}; newSignatures[address] = newSignature; }) ); if (unexpectedSigners && unexpectedSigners.size > 0) { const expectedSigners = Object.keys(transaction.signatures); throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION, { expectedAddresses: expectedSigners, unexpectedAddresses: [...unexpectedSigners] }); } if (!newSignatures) { return transaction; } return Object.freeze({ ...transaction, signatures: Object.freeze({ ...transaction.signatures, ...newSignatures }) }); } async function signTransaction(keyPairs, transaction) { const out = await partiallySignTransaction(keyPairs, transaction); assertIsFullySignedTransaction(out); Object.freeze(out); return out; } function isFullySignedTransaction(transaction) { return Object.entries(transaction.signatures).every(([_, signatureBytes]) => !!signatureBytes); } function assertIsFullySignedTransaction(transaction) { const missingSigs = []; Object.entries(transaction.signatures).forEach(([address, signatureBytes]) => { if (!signatureBytes) { missingSigs.push(address); } }); if (missingSigs.length > 0) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, { addresses: missingSigs }); } } function getBase64EncodedWireTransaction(transaction) { const wireTransactionBytes = getTransactionEncoder().encode(transaction); return codecsStrings.getBase64Decoder().decode(wireTransactionBytes); } var TRANSACTION_PACKET_SIZE = 1280; var TRANSACTION_PACKET_HEADER = 40 + 8; var TRANSACTION_SIZE_LIMIT = TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; function getTransactionSize(transaction) { return getTransactionEncoder().getSizeFromValue(transaction); } function isTransactionWithinSizeLimit(transaction) { return getTransactionSize(transaction) <= TRANSACTION_SIZE_LIMIT; } function assertIsTransactionWithinSizeLimit(transaction) { const transactionSize = getTransactionSize(transaction); if (transactionSize > TRANSACTION_SIZE_LIMIT) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { transactionSize, transactionSizeLimit: TRANSACTION_SIZE_LIMIT }); } } // src/sendable-transaction.ts function isSendableTransaction(transaction) { return isFullySignedTransaction(transaction) && isTransactionWithinSizeLimit(transaction); } function assertIsSendableTransaction(transaction) { assertIsFullySignedTransaction(transaction); assertIsTransactionWithinSizeLimit(transaction); } function getTransactionMessageSize(transactionMessage) { return getTransactionSize(compileTransaction(transactionMessage)); } function isTransactionMessageWithinSizeLimit(transactionMessage) { return getTransactionMessageSize(transactionMessage) <= TRANSACTION_SIZE_LIMIT; } function assertIsTransactionMessageWithinSizeLimit(transactionMessage) { const transactionSize = getTransactionMessageSize(transactionMessage); if (transactionSize > TRANSACTION_SIZE_LIMIT) { throw new errors.SolanaError(errors.SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { transactionSize, transactionSizeLimit: TRANSACTION_SIZE_LIMIT }); } } exports.TRANSACTION_PACKET_HEADER = TRANSACTION_PACKET_HEADER; exports.TRANSACTION_PACKET_SIZE = TRANSACTION_PACKET_SIZE; exports.TRANSACTION_SIZE_LIMIT = TRANSACTION_SIZE_LIMIT; exports.assertIsFullySignedTransaction = assertIsFullySignedTransaction; exports.assertIsSendableTransaction = assertIsSendableTransaction; exports.assertIsTransactionMessageWithinSizeLimit = assertIsTransactionMessageWithinSizeLimit; exports.assertIsTransactionWithBlockhashLifetime = assertIsTransactionWithBlockhashLifetime; exports.assertIsTransactionWithDurableNonceLifetime = assertIsTransactionWithDurableNonceLifetime; exports.assertIsTransactionWithinSizeLimit = assertIsTransactionWithinSizeLimit; exports.compileTransaction = compileTransaction; exports.getBase64EncodedWireTransaction = getBase64EncodedWireTransaction; exports.getSignatureFromTransaction = getSignatureFromTransaction; exports.getTransactionCodec = getTransactionCodec; exports.getTransactionDecoder = getTransactionDecoder; exports.getTransactionEncoder = getTransactionEncoder; exports.getTransactionLifetimeConstraintFromCompiledTransactionMessage = getTransactionLifetimeConstraintFromCompiledTransactionMessage; exports.getTransactionMessageSize = getTransactionMessageSize; exports.getTransactionSize = getTransactionSize; exports.isFullySignedTransaction = isFullySignedTransaction; exports.isSendableTransaction = isSendableTransaction; exports.isTransactionMessageWithinSizeLimit = isTransactionMessageWithinSizeLimit; exports.isTransactionWithBlockhashLifetime = isTransactionWithBlockhashLifetime; exports.isTransactionWithDurableNonceLifetime = isTransactionWithDurableNonceLifetime; exports.isTransactionWithinSizeLimit = isTransactionWithinSizeLimit; exports.partiallySignTransaction = partiallySignTransaction; exports.signTransaction = signTransaction; //# sourceMappingURL=index.node.cjs.map //# sourceMappingURL=index.node.cjs.map