UNPKG

@solana/transactions

Version:

Helpers for creating and serializing transactions

279 lines (275 loc) • 13.1 kB
import { getAddressDecoder, isAddress, getAddressFromPublicKey } from '@solana/addresses'; import { transformDecoder, fixDecoderSize, combineCodec, padRightDecoder, bytesEqual, transformEncoder, fixEncoderSize } from '@solana/codecs-core'; import { getStructEncoder, getBytesEncoder, getStructDecoder, getArrayDecoder, getBytesDecoder, getTupleDecoder, getArrayEncoder } from '@solana/codecs-data-structures'; import { getShortU16Decoder, getU8Decoder, getShortU16Encoder } from '@solana/codecs-numbers'; import { SolanaError, SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, SOLANA_ERROR__TRANSACTION__NONCE_ACCOUNT_CANNOT_BE_IN_LOOKUP_TABLE, SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME, SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME, SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING, SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES } from '@solana/errors'; import { getTransactionVersionDecoder, compileTransactionMessage, getCompiledTransactionMessageEncoder, isTransactionMessageWithBlockhashLifetime, isTransactionMessageWithDurableNonceLifetime } from '@solana/transaction-messages'; import { isBlockhash } from '@solana/rpc-types'; import { getBase58Decoder, getBase64Decoder } from '@solana/codecs-strings'; import { signBytes } from '@solana/keys'; // src/codecs/transaction-codec.ts function getSignaturesToEncode(signaturesMap) { const signatures = Object.values(signaturesMap); if (signatures.length === 0) { throw new SolanaError(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 transformEncoder( getArrayEncoder(fixEncoderSize(getBytesEncoder(), 64), { size: getShortU16Encoder() }), getSignaturesToEncode ); } // src/codecs/transaction-codec.ts function getTransactionEncoder() { return getStructEncoder([ ["signatures", getSignaturesEncoder()], ["messageBytes", getBytesEncoder()] ]); } function getTransactionDecoder() { return transformDecoder( getStructDecoder([ ["signatures", getArrayDecoder(fixDecoderSize(getBytesDecoder(), 64), { size: getShortU16Decoder() })], ["messageBytes", getBytesDecoder()] ]), decodePartiallyDecodedTransaction ); } function getTransactionCodec() { return combineCodec(getTransactionEncoder(), getTransactionDecoder()); } function decodePartiallyDecodedTransaction(transaction) { const { messageBytes, signatures } = transaction; const signerAddressesDecoder = getTupleDecoder([ // read transaction version getTransactionVersionDecoder(), // read first byte of header, `numSignerAccounts` // padRight to skip the next 2 bytes, `numReadOnlySignedAccounts` and `numReadOnlyUnsignedAccounts` which we don't need padRightDecoder(getU8Decoder(), 2), // read static addresses getArrayDecoder(getAddressDecoder(), { size: getShortU16Decoder() }) ]); const [_txVersion, numRequiredSignatures, staticAddresses] = signerAddressesDecoder.decode(messageBytes); const signerAddresses = staticAddresses.slice(0, numRequiredSignatures); if (signerAddresses.length !== signatures.length) { throw new SolanaError(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 SolanaError(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" && isBlockhash(transaction.lifetimeConstraint.blockhash); } function assertIsTransactionWithBlockhashLifetime(transaction) { if (!isTransactionWithBlockhashLifetime(transaction)) { throw new SolanaError(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" && isAddress(transaction.lifetimeConstraint.nonceAccountAddress); } function assertIsTransactionWithDurableNonceLifetime(transaction) { if (!isTransactionWithDurableNonceLifetime(transaction)) { throw new SolanaError(SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME); } } function compileTransaction(transactionMessage) { const compiledMessage = compileTransactionMessage(transactionMessage); const messageBytes = 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 (isTransactionMessageWithBlockhashLifetime(transactionMessage)) { lifetimeConstraint = { blockhash: transactionMessage.lifetimeConstraint.blockhash, lastValidBlockHeight: transactionMessage.lifetimeConstraint.lastValidBlockHeight }; } else if (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 = getBase58Decoder(); const signatureBytes = Object.values(transaction.signatures)[0]; if (!signatureBytes) { throw new SolanaError(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 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 signBytes(keyPair.privateKey, transaction.messageBytes); if (existingSignature !== null && bytesEqual(newSignature, existingSignature)) { return; } newSignatures ||= {}; newSignatures[address] = newSignature; }) ); if (unexpectedSigners && unexpectedSigners.size > 0) { const expectedSigners = Object.keys(transaction.signatures); throw new SolanaError(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 SolanaError(SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, { addresses: missingSigs }); } } function getBase64EncodedWireTransaction(transaction) { const wireTransactionBytes = getTransactionEncoder().encode(transaction); return 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 SolanaError(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 SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { transactionSize, transactionSizeLimit: TRANSACTION_SIZE_LIMIT }); } } export { TRANSACTION_PACKET_HEADER, TRANSACTION_PACKET_SIZE, TRANSACTION_SIZE_LIMIT, assertIsFullySignedTransaction, assertIsSendableTransaction, assertIsTransactionMessageWithinSizeLimit, assertIsTransactionWithBlockhashLifetime, assertIsTransactionWithDurableNonceLifetime, assertIsTransactionWithinSizeLimit, compileTransaction, getBase64EncodedWireTransaction, getSignatureFromTransaction, getTransactionCodec, getTransactionDecoder, getTransactionEncoder, getTransactionLifetimeConstraintFromCompiledTransactionMessage, getTransactionMessageSize, getTransactionSize, isFullySignedTransaction, isSendableTransaction, isTransactionMessageWithinSizeLimit, isTransactionWithBlockhashLifetime, isTransactionWithDurableNonceLifetime, isTransactionWithinSizeLimit, partiallySignTransaction, signTransaction }; //# sourceMappingURL=index.node.mjs.map //# sourceMappingURL=index.node.mjs.map