import { Test } from '../../../bindings.js'; import { Types } from '../../../bindings/mina-transaction/v1/types.js'; import { Proof } from '../../proof-system/proof.js'; import { Empty } from '../../proof-system/zkprogram.js'; import { PrivateKey, PublicKey } from '../../provable/crypto/signature.js'; import { UInt32, UInt64 } from '../../provable/int.js'; import { Provable } from '../../provable/provable.js'; import { Field } from '../../provable/wrapped.js'; import { assertPromise } from '../../util/assert.js'; import { AccountUpdate, AccountUpdateLayout, FeePayerUnsigned, TokenId, ZkappCommand, ZkappPublicInput, addMissingProofs, addMissingSignatures, } from './account-update.js'; import { Account } from './account.js'; import * as Fetch from './fetch.js'; import { sendZkappQuery, type DepthOptions, type SendZkAppResponse, type TransactionDepthInfo, } from './graphql.js'; import { activeInstance, type FeePayerSpec } from './mina-instance.js'; import { assertPreconditionInvariants } from './precondition.js'; import { currentTransaction, type FetchMode } from './transaction-context.js'; import { getTotalTimeRequired } from './transaction-validation.js'; export { Transaction, createIncludedTransaction, createRejectedTransaction, createTransaction, getAccount, newTransaction, sendTransaction, toPendingTransactionPromise, toTransactionPromise, transaction, type IncludedTransaction, type PendingTransaction, type PendingTransactionPromise, type PendingTransactionStatus, type RejectedTransaction, type TransactionPromise, type WaitForFinalityOptions, }; type TransactionCommon = { /** * Transaction structure used to describe a state transition on the Mina blockchain. */ transaction: ZkappCommand; /** * Serializes the transaction to a JSON string. * @returns A string representation of the {@link Transaction}. */ toJSON(): string; /** * Produces a pretty-printed JSON representation of the {@link Transaction}. * @returns A formatted string representing the transaction in JSON. */ toPretty(): any; /** * Constructs the GraphQL query string used for submitting the transaction to a Mina daemon. * @returns The GraphQL query string for the {@link Transaction}. */ toGraphqlQuery(): string; /** * Submits the {@link Transaction} to the network. This method asynchronously sends the transaction * for processing. If successful, it returns a {@link PendingTransaction} instance, which can be used to monitor the transaction's progress. * If the transaction submission fails, this method throws an error that should be caught and handled appropriately. * @returns A {@link PendingTransactionPromise}, which resolves to a {@link PendingTransaction} instance representing the submitted transaction if the submission is successful. * @throws An error if the transaction cannot be sent or processed by the network, containing details about the failure. * @example * ```ts * try { * const pendingTransaction = await transaction.send(); * console.log('Transaction sent successfully to the Mina daemon.'); * } catch (error) { * console.error('Failed to send transaction to the Mina daemon:', error); * } * ``` */ }; namespace Transaction { /** * Deserializes a transaction from a JSON object or JSON string representation. * This method accepts both parsed JSON objects and JSON strings, making it flexible for different use cases. * * @param json A JSON object representation of a transaction (Types.Json.ZkappCommand) or a JSON string * @returns A new Transaction instance reconstructed from the JSON input * * @example * ```ts * const originalTx = await Mina.transaction(sender, () => { * zkapp.someMethod(); * }); * const serialized = originalTx.toJSON(); * const deserializedTx = Transaction.fromJSON(serialized); * ``` */ export function fromJSON(json: Types.Json.ZkappCommand | string): Transaction { let transaction = ZkappCommand.fromJSON(json); return newTransaction(transaction, activeInstance.proofsEnabled); } /** * Computes the hash of a transaction represented as a JSON object or JSON string. * This hash serves as a unique identifier for the transaction and is essential for tracking * and verifying transactions on the Mina blockchain. */ export async function hash(json: Types.Json.ZkappCommand | string): Promise { let mlTest = await Test(); const hash = mlTest.transactionHash.hashZkAppCommand( JSON.stringify(ZkappCommand.toJSON(ZkappCommand.fromJSON(json))) ); return hash; } } /** * Defines the structure and operations associated with a transaction. * This type encompasses methods for serializing the transaction, signing it, generating proofs, * and submitting it to the network. */ type Transaction = TransactionCommon & { send(): PendingTransactionPromise; /** * Sends the {@link Transaction} to the network. Unlike the standard {@link Transaction.send}, this function does not throw an error if internal errors are detected. Instead, it returns a {@link PendingTransaction} if the transaction is successfully sent for processing or a {@link RejectedTransaction} if it encounters errors during processing or is outright rejected by the Mina daemon. * @returns {Promise} A promise that resolves to a {@link PendingTransaction} if the transaction is accepted for processing, or a {@link RejectedTransaction} if the transaction fails or is rejected. * @example * ```ts * const result = await transaction.safeSend(); * if (result.status === 'pending') { * console.log('Transaction sent successfully to the Mina daemon.'); * } else if (result.status === 'rejected') { * console.error('Transaction failed with errors:', result.errors); * } * ``` */ safeSend(): Promise; /** * Modifies a transaction to set the fee to the new fee provided. Because this change invalidates proofs and signatures both are removed. The nonce is not increased so sending both transitions will not risk both being accepted. * @returns {TransactionPromise} The same transaction with the new fee and the proofs and signatures removed. * @example * ```ts * tx.send(); * // Waits for some time and decide to resend with a higher fee * * tx.setFee(newFee); * await tx.sign([feePayerKey])); * await tx.send(); * ``` */ setFee(newFee: UInt64): TransactionPromise; /** * setFeePerSnarkCost behaves identically to {@link Transaction.setFee} but the fee is given per estimated cost of snarking the transition as given by {@link getTotalTimeRequired}. This is useful because it should reflect what snark workers would charge in times of network contention. */ setFeePerSnarkCost(newFeePerSnarkCost: number): TransactionPromise; } & (Proven extends false ? { /** * Initiates the proof generation process for the {@link Transaction}. This asynchronous operation is * crucial for zero-knowledge-based transactions, where proofs are required to validate state transitions. * This can take some time. * @example * ```ts * await transaction.prove(); * ``` */ prove(): Promise>; } : { /** The proofs generated as the result of calling `prove`. */ proofs: (Proof | undefined)[]; }) & (Signed extends false ? { /** * Signs all {@link AccountUpdate}s included in the {@link Transaction} that require a signature. * {@link AccountUpdate}s that require a signature can be specified with `{AccountUpdate|SmartContract}.requireSignature()`. * @param privateKeys The list of keys that should be used to sign the {@link Transaction} * @returns The {@link Transaction} instance with all required signatures applied. * @example * ```ts * const signedTx = transaction.sign([userPrivateKey]); * console.log('Transaction signed successfully.'); * ``` */ sign(privateKeys: PrivateKey[]): Transaction; } : {}); type PendingTransactionStatus = 'pending' | 'rejected'; /** * Represents a transaction that has been submitted to the blockchain but has not yet reached a final state. * The {@link PendingTransaction} type extends certain functionalities from the base {@link Transaction} type, * adding methods to monitor the transaction's progress towards being finalized (either included in a block or rejected). */ type PendingTransaction = Pick & { /** * @property {PendingTransactionStatus} status The status of the transaction after being sent to the Mina daemon. * This property indicates the transaction's initial processing status but does not guarantee its eventual inclusion in a block. * A status of `pending` suggests the transaction was accepted by the Mina daemon for processing, * whereas a status of `rejected` indicates that the transaction was not accepted. * Use the {@link PendingTransaction.wait()} or {@link PendingTransaction.safeWait()} methods to track the transaction's progress towards finalization and to determine whether it's included in a block. * @example * ```ts * if (pendingTransaction.status === 'pending') { * console.log('Transaction accepted for processing by the Mina daemon.'); * try { * await pendingTransaction.wait(); * console.log('Transaction successfully included in a block.'); * } catch (error) { * console.error('Transaction was rejected or failed to be included in a block:', error); * } * } else { * console.error('Transaction was not accepted for processing by the Mina daemon.'); * } * ``` */ status: PendingTransactionStatus; /** * Waits for the transaction to be included in a block. This method polls the Mina daemon to check the transaction's status, and throws an error if the transaction is rejected. * @param {Object} [options] Configuration options for polling behavior. * @param {number} [options.maxAttempts] The maximum number of attempts to check the transaction status. * @param {number} [options.interval] The interval, in milliseconds, between status checks. * @returns {Promise} A promise that resolves to the transaction's final state or throws an error. * @throws {Error} If the transaction is rejected or fails to finalize within the given attempts. * @example * ```ts * try { * const transaction = await pendingTransaction.wait({ maxAttempts: 10, interval: 2000 }); * console.log('Transaction included in a block.'); * } catch (error) { * console.error('Transaction rejected or failed to finalize:', error); * } * ``` */ wait(options?: { maxAttempts?: number; interval?: number }): Promise; /** * Waits for the transaction to be included in a block. This method polls the Mina daemon to check the transaction's status * @param {Object} [options] Configuration options for polling behavior. * @param {number} [options.maxAttempts] The maximum number of polling attempts. * @param {number} [options.interval] The time interval, in milliseconds, between each polling attempt. * @returns {Promise} A promise that resolves to the transaction's final state. * @example * ```ts * const transaction = await pendingTransaction.wait({ maxAttempts: 5, interval: 1000 }); * console.log(transaction.status); // 'included' or 'rejected' * ``` */ safeWait(options?: { maxAttempts?: number; interval?: number; }): Promise; /** * Returns the transaction hash as a string identifier. * @property {string} The hash of the transaction. * @example * ```ts * const txHash = pendingTransaction.hash; * console.log(`Transaction hash: ${txHash}`); * ``` */ hash: string; /** * Optional. Contains response data from a ZkApp transaction submission. * * @property {SendZkAppResponse} [data] The response data from the transaction submission. */ data?: SendZkAppResponse; /** * An array of error messages related to the transaction processing. * * @property {string[]} errors Descriptive error messages if the transaction encountered issues during processing. * @example * ```ts * if (!pendingTransaction.status === 'rejected') { * console.error(`Transaction errors: ${pendingTransaction.errors.join(', ')}`); * } * ``` */ errors: string[]; /** * setFee is the same as {@link Transaction.setFee(newFee)} but for a {@link PendingTransaction}. */ setFee(newFee: UInt64): TransactionPromise; /** * setFeePerSnarkCost is the same as {@link Transaction.setFeePerSnarkCost(newFeePerSnarkCost)} but for a {@link PendingTransaction}. */ setFeePerSnarkCost(newFeePerSnarkCost: number): TransactionPromise; }; /** * Represents a transaction that has been successfully included in a block. */ type IncludedTransaction = Pick< PendingTransaction, 'transaction' | 'toJSON' | 'toPretty' | 'hash' | 'data' > & { /** * @property {string} status The final status of the transaction, indicating successful inclusion in a block. * @example * ```ts * try { * const includedTx: IncludedTransaction = await pendingTransaction.wait(); * // If wait() resolves, it means the transaction was successfully included. * console.log(`Transaction ${includedTx.hash} included in a block.`); * } catch (error) { * // If wait() throws, the transaction was not included in a block. * console.error('Transaction failed to be included in a block:', error); * } * ``` */ status: 'included'; /** * The block height at which this transaction was included. * May be undefined if not tracked (e.g., when using LocalBlockchain without block height simulation). */ inclusionBlockHeight?: number; /** * Fetches the current depth (confirmation count) of this transaction. * Depth represents the number of blocks built on top of the block containing this transaction. * * @param options - Optional configuration for the depth query * @param options.blockLength - Number of blocks to search (default: 20) * @param options.finalityThreshold - Blocks required for finality (default: 15, which provides 99.9% confidence) * @returns TransactionDepthInfo with depth, block heights, and finality status * @throws Error if the transaction cannot be found in recent blocks or a network error occurs * * @example * ```ts * const included = await pendingTransaction.wait(); * const depthInfo = await included.getDepth(); * console.log(`Depth: ${depthInfo.depth}, Finalized: ${depthInfo.isFinalized}`); * ``` * * @see https://docs.minaprotocol.com/mina-protocol/lifecycle-of-a-payment */ getDepth(options?: DepthOptions): Promise; /** * Safe variant of {@link IncludedTransaction.getDepth} that returns null instead of throwing. * * @param options - Optional configuration for the depth query * @returns TransactionDepthInfo if found, null otherwise * * @example * ```ts * const depthInfo = await included.safeGetDepth(); * if (depthInfo?.isFinalized) { * console.log('Transaction has reached finality!'); * } * ``` */ safeGetDepth(options?: DepthOptions): Promise; /** * Polls the network until the transaction reaches the specified finality threshold. * Returns the final {@link TransactionDepthInfo} once finality is reached. * * Use the `onProgress` callback to receive updates on each poll — useful for * updating UIs with confirmation progress. * * @param options - Configuration for the finality wait * @param options.finalityThreshold - Blocks required for finality (default: 15) * @param options.interval - Polling interval in ms (default: 60000) * @param options.maxAttempts - Max polling attempts before giving up (default: 30) * @param options.onProgress - Callback invoked on each poll with the current depth info * @returns TransactionDepthInfo once finality is reached * @throws Error if max attempts exceeded or transaction not found * * @example * ```ts * // Simple usage — fire and forget * included.waitForFinality().then(info => { * console.log(`Finalized at depth: ${info.depth}`); * }); * * // With progress callback for UI updates * included.waitForFinality({ * finalityThreshold: 10, * onProgress: (info) => { * console.log(`${info.depth}/${info.finalityThreshold} confirmations`); * }, * }); * ``` * * @see https://docs.minaprotocol.com/mina-protocol/lifecycle-of-a-payment */ waitForFinality(options?: WaitForFinalityOptions): Promise; }; /** * Options for {@link IncludedTransaction.waitForFinality}. */ type WaitForFinalityOptions = { /** * Number of blocks required for finality (default: 15). * @see https://docs.minaprotocol.com/mina-protocol/lifecycle-of-a-payment */ finalityThreshold?: number; /** Polling interval in milliseconds (default: 60000) */ interval?: number; /** Maximum number of polling attempts before throwing (default: 30) */ maxAttempts?: number; /** Callback invoked on each poll with the current depth info */ onProgress?: (info: TransactionDepthInfo) => void; }; /** * Represents a transaction that has been rejected and not included in a blockchain block. */ type RejectedTransaction = Pick< PendingTransaction, 'transaction' | 'toJSON' | 'toPretty' | 'hash' | 'data' > & { /** * @property {string} status The final status of the transaction, specifically indicating that it has been rejected. * @example * ```ts * try { * const txResult = await pendingTransaction.wait(); * // This line will not execute if the transaction is rejected, as `.wait()` will throw an error instead. * console.log(`Transaction ${txResult.hash} was successfully included in a block.`); * } catch (error) { * console.error(`Transaction ${error.transaction.hash} was rejected.`); * error.errors.forEach((error, i) => { * console.error(`Error ${i + 1}: ${error}`); * }); * } * ``` */ status: 'rejected'; /** * @property {string[]} errors An array of error messages detailing the reasons for the transaction's rejection. */ errors: string[]; }; /** * A `Promise` with some additional methods for making chained method calls * into the pending value upon its resolution. */ type TransactionPromise = Promise< Transaction > & { /** Equivalent to calling the resolved `Transaction`'s `send` method. */ send(): PendingTransactionPromise; } & (Proven extends false ? { /** * Calls `prove` upon resolution of the `Transaction`. Returns a * new `TransactionPromise` with the field `proofPromise` containing * a promise which resolves to the proof array. */ prove(): TransactionPromise; } : { /** * If the chain of method calls that produced the current `TransactionPromise` * contains a `prove` call, then this field contains a promise resolving to the * proof array which was output from the underlying `prove` call. */ proofs(): Promise['proofs']>; }) & (Signed extends false ? { /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ sign( ...args: Parameters['sign']> ): TransactionPromise; } : {}); function toTransactionPromise( getPromise: () => Promise> ): TransactionPromise { const pending = getPromise().then(); return Object.assign(pending, { sign(...args: Parameters['sign']>) { return toTransactionPromise(() => pending.then((v) => (v as Transaction).sign(...args)) ); }, send() { return toPendingTransactionPromise(() => pending.then((v) => v.send())); }, prove() { return toTransactionPromise(() => pending.then((v) => (v as never as Transaction).prove()) ); }, proofs() { return pending.then((v) => (v as never as Transaction).proofs); }, }) as never as TransactionPromise; } /** * A `Promise` with an additional `wait` method, which calls * into the inner `TransactionStatus`'s `wait` method upon its resolution. */ type PendingTransactionPromise = Promise & { /** Equivalent to calling the resolved `PendingTransaction`'s `wait` method. */ wait: PendingTransaction['wait']; }; function toPendingTransactionPromise( getPromise: () => Promise ): PendingTransactionPromise { const pending = getPromise().then(); return Object.assign(pending, { wait(...args: Parameters) { return pending.then((v) => v.wait(...args)); }, }); } async function createTransaction( feePayer: FeePayerSpec, f: () => Promise, numberOfRuns: 0 | 1 | undefined, { fetchMode = 'cached' as FetchMode, isFinalRunOutsideCircuit = true, proofsEnabled = true } = {} ): Promise> { if (currentTransaction.has()) { throw new Error('Cannot start new transaction within another transaction'); } let feePayerSpec: { sender?: PublicKey; fee?: number | string | UInt64; memo?: string; nonce?: number; }; if (feePayer === undefined) { feePayerSpec = {}; } else if (feePayer instanceof PublicKey) { feePayerSpec = { sender: feePayer }; } else { feePayerSpec = feePayer; } let { sender, fee, memo = '', nonce } = feePayerSpec; let transactionId = currentTransaction.enter({ sender, layout: new AccountUpdateLayout(), fetchMode, isFinalRunOutsideCircuit, numberOfRuns, }); // run circuit try { if (fetchMode === 'test') { await Provable.runUnchecked(async () => { await assertPromise(f()); Provable.asProver(() => { let tx = currentTransaction.get(); tx.layout.toConstantInPlace(); }); }); } else { await assertPromise(f()); } } catch (err) { currentTransaction.leave(transactionId); throw err; } let accountUpdates = currentTransaction.get().layout.toFlatList({ mutate: true }); try { // check that on-chain values weren't used without setting a precondition for (let accountUpdate of accountUpdates) { assertPreconditionInvariants(accountUpdate); } } catch (err) { currentTransaction.leave(transactionId); throw err; } let feePayerAccountUpdate: FeePayerUnsigned; if (sender !== undefined) { // if senderKey is provided, fetch account to get nonce and mark to be signed let nonce_; let senderAccount = getAccount(sender, TokenId.default); if (nonce === undefined) { nonce_ = senderAccount.nonce; } else { nonce_ = UInt32.from(nonce); senderAccount.nonce = nonce_; Fetch.addCachedAccount(senderAccount); } feePayerAccountUpdate = AccountUpdate.defaultFeePayer(sender, nonce_); if (fee !== undefined) { feePayerAccountUpdate.body.fee = fee instanceof UInt64 ? fee : UInt64.from(String(fee)); } } else { // otherwise use a dummy fee payer that has to be filled in later feePayerAccountUpdate = AccountUpdate.dummyFeePayer(); } let transaction: ZkappCommand = { accountUpdates, feePayer: feePayerAccountUpdate, memo, }; currentTransaction.leave(transactionId); return newTransaction(transaction, proofsEnabled); } function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { let self: Transaction = { transaction, sign(privateKeys: PrivateKey[]) { self.transaction = addMissingSignatures(self.transaction, privateKeys); return self; }, prove() { return toTransactionPromise(async () => { let { zkappCommand, proofs } = await addMissingProofs(self.transaction, { proofsEnabled, }); self.transaction = zkappCommand; return Object.assign(self as never as Transaction, { proofs, }); }); }, toJSON() { let json = ZkappCommand.toJSON(self.transaction); return JSON.stringify(json); }, toPretty() { return ZkappCommand.toPretty(self.transaction); }, toGraphqlQuery() { return sendZkappQuery(self.toJSON()); }, send() { return toPendingTransactionPromise(async () => { const pendingTransaction = await sendTransaction(self); if (pendingTransaction.errors.length > 0) { throw Error( `Transaction failed with errors:\n- ${pendingTransaction.errors.join('\n- ')}` ); } return pendingTransaction; }); }, async safeSend() { const pendingTransaction = await sendTransaction(self); if (pendingTransaction.errors.length > 0) { return createRejectedTransaction(pendingTransaction, pendingTransaction.errors); } return pendingTransaction; }, setFeePerSnarkCost(newFeePerSnarkCost: number) { let { totalTimeRequired } = getTotalTimeRequired(transaction.accountUpdates); return this.setFee(new UInt64(Math.round(totalTimeRequired * newFeePerSnarkCost))); }, setFee(newFee: UInt64) { return toTransactionPromise(async () => { self = self as Transaction; self.transaction.accountUpdates.forEach((au) => { if (au.body.useFullCommitment.toBoolean()) { au.authorization.signature = undefined; au.lazyAuthorization = { kind: 'lazy-signature' }; } }); self.transaction.feePayer.body.fee = newFee; self.transaction.feePayer.lazyAuthorization = { kind: 'lazy-signature' }; return self; }); }, }; return self; } /** * Construct a smart contract transaction. Within the callback passed to this function, * you can call into the methods of smart contracts. * * ``` * let tx = await Mina.transaction(sender, async () => { * await myZkapp.update(); * await someOtherZkapp.someOtherMethod(); * }); * ``` * * @return A transaction that can subsequently be submitted to the chain. */ function transaction( sender: FeePayerSpec, f: () => Promise ): TransactionPromise; function transaction(f: () => Promise): TransactionPromise; function transaction( senderOrF: FeePayerSpec | (() => Promise), fOrUndefined?: () => Promise ): TransactionPromise { let sender: FeePayerSpec; let f: () => Promise; if (fOrUndefined !== undefined) { sender = senderOrF as FeePayerSpec; f = fOrUndefined; } else { sender = undefined; f = senderOrF as () => Promise; } return activeInstance.transaction(sender, f); } // TODO: should we instead constrain to `Transaction`? async function sendTransaction(txn: Transaction) { return await activeInstance.sendTransaction(txn); } /** * @return The account data associated to the given public key. */ function getAccount(publicKey: PublicKey, tokenId?: Field): Account { return activeInstance.getAccount(publicKey, tokenId); } function createRejectedTransaction( { transaction, data, toJSON, toPretty, hash }: Omit, errors: string[] ): RejectedTransaction { return { status: 'rejected', errors, transaction, toJSON, toPretty, hash, data, }; } function createIncludedTransaction( { transaction, data, toJSON, toPretty, hash }: Omit, inclusionBlockHeight?: number ): IncludedTransaction { const safeGetDepth = async (options?: DepthOptions): Promise => { return Fetch.fetchTransactionDepth(hash, options); }; const getDepth = async (options?: DepthOptions): Promise => { const result = await safeGetDepth(options); if (result === null) { throw Error( `Transaction ${hash} not found in recent blocks. It may have been pruned from the transition frontier or not yet included.` ); } return result; }; const waitForFinality = async ( options?: WaitForFinalityOptions ): Promise => { const finalityThreshold = options?.finalityThreshold ?? 15; const interval = options?.interval ?? 120000; const maxAttempts = options?.maxAttempts ?? 45; for (let attempt = 0; attempt < maxAttempts; attempt++) { const info = await safeGetDepth({ finalityThreshold }); if (info) { options?.onProgress?.(info); if (info.isFinalized) { return info; } } if (attempt < maxAttempts - 1) { await new Promise((resolve) => setTimeout(resolve, interval)); } } throw Error( `Transaction ${hash} did not reach finality after ${maxAttempts} attempts (threshold: ${finalityThreshold} blocks).` ); }; return { status: 'included', transaction, toJSON, toPretty, hash, data, inclusionBlockHeight, getDepth, safeGetDepth, waitForFinality, }; }