UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

906 lines 40.9 kB
import { PrivateKey, PublicKey } from '../../provable/crypto/signature.js'; import { UInt32, UInt64 } from '../../provable/int.js'; import { Field } from '../../provable/wrapped.js'; import { AccountTiming } from '../v2/account.js'; import { Actions, TokenId } from './account-update.js'; import { fillPartialAccount, parseFetchedAccount } from './account.js'; import { EpochSeed, LedgerHash, StateHash } from './base58-encodings.js'; import { accountQuery, currentSlotQuery, genesisConstantsQuery, getActionsQuery, getEventsQuery, lastBlockQuery, lastBlockQueryFailureCheck, sendZkappQuery, transactionStatusQuery, } from './graphql.js'; export { Lightnet, addCachedAccount, checkZkappTransaction, fetchAccount, fetchActions, fetchCurrentSlot, fetchEvents, fetchGenesisConstants, fetchLastBlock, fetchMissingData, fetchTimedAccountInfo, fetchTransactionDepth, fetchTransactionStatus, getCachedAccount, getCachedActions, getCachedGenesisConstants, getCachedNetwork, makeGraphqlRequest, markAccountToBeFetched, markActionsToBeFetched, markNetworkToBeFetched, networkConfig, parseFetchedAccount, sendZkapp, setArchiveDefaultHeaders, setArchiveGraphqlEndpoint, setArchiveGraphqlFallbackEndpoints, setGraphqlEndpoint, setGraphqlEndpoints, setLightnetAccountManagerEndpoint, setMinaDefaultHeaders, setMinaGraphqlFallbackEndpoints, }; let networkConfig = { minaEndpoint: '', minaFallbackEndpoints: [], archiveEndpoint: '', archiveFallbackEndpoints: [], lightnetAccountManagerEndpoint: '', minaDefaultHeaders: {}, archiveDefaultHeaders: {}, }; function checkForValidUrl(url) { try { new URL(url); return true; } catch { return false; } } /** * Sets up the default headers to be used for all Mina node GraphQL requests, example usage: * ```typescript * setMinaDefaultHeaders({ Authorization: 'Bearer example-token' }); * ``` * * It can be overridden by passing headers to the individual fetch functions, example usage: * ```typescript * setMinaDefaultHeaders({ Authorization: 'Bearer default-token' }); * await fetchAccount({publicKey}, minaEndpoint, { headers: { Authorization: 'Bearer override-token' } }); * ``` * @param headers Arbitrary sized headers to be used for all Mina node GraphQL requests. */ function setMinaDefaultHeaders(headers) { networkConfig.minaDefaultHeaders = headers; } /** * Sets up the default headers to be used for all Archive node GraphQL requests, example usage: * ```typescript * setArchiveDefaultHeaders({ Authorization: 'Bearer example-token' }); * ``` * * It can be overridden by passing headers to the individual fetch functions, example usage: * ```typescript * setArchiveDefaultHeaders({ Authorization: 'Bearer default-token' }); * await fetchEvents({publicKey}, archiveEndpoint, { headers: { Authorization: 'Bearer override-token' } }); * ``` * @param headers Arbitrary sized headers to be used for all Mina Archive node GraphQL requests. */ function setArchiveDefaultHeaders(headers) { networkConfig.archiveDefaultHeaders = headers; } function setGraphqlEndpoints([graphqlEndpoint, ...fallbackEndpoints]) { setGraphqlEndpoint(graphqlEndpoint); setMinaGraphqlFallbackEndpoints(fallbackEndpoints); } function setGraphqlEndpoint(graphqlEndpoint, minaDefaultHeaders) { if (!checkForValidUrl(graphqlEndpoint)) { throw new Error(`Invalid GraphQL endpoint: ${graphqlEndpoint}. Please specify a valid URL.`); } networkConfig.minaEndpoint = graphqlEndpoint; if (minaDefaultHeaders) setMinaDefaultHeaders(minaDefaultHeaders); } function setMinaGraphqlFallbackEndpoints(graphqlEndpoints) { if (graphqlEndpoints.some((endpoint) => !checkForValidUrl(endpoint))) { throw new Error(`Invalid GraphQL endpoint: ${graphqlEndpoints}. Please specify a valid URL.`); } networkConfig.minaFallbackEndpoints = graphqlEndpoints; } /** * Sets up a GraphQL endpoint to be used for fetching information from an Archive Node. * */ function setArchiveGraphqlEndpoint(graphqlEndpoint, archiveDefaultHeaders) { if (!checkForValidUrl(graphqlEndpoint)) { throw new Error(`Invalid GraphQL endpoint: ${graphqlEndpoint}. Please specify a valid URL.`); } networkConfig.archiveEndpoint = graphqlEndpoint; if (archiveDefaultHeaders) setArchiveDefaultHeaders(archiveDefaultHeaders); } function setArchiveGraphqlFallbackEndpoints(graphqlEndpoints) { if (graphqlEndpoints.some((endpoint) => !checkForValidUrl(endpoint))) { throw new Error(`Invalid GraphQL endpoint: ${graphqlEndpoints}. Please specify a valid URL.`); } networkConfig.archiveFallbackEndpoints = graphqlEndpoints; } /** * Sets up the lightnet account manager endpoint to be used for accounts acquisition and releasing. * * @param endpoint Account manager endpoint. */ function setLightnetAccountManagerEndpoint(endpoint) { if (!checkForValidUrl(endpoint)) { throw new Error(`Invalid account manager endpoint: ${endpoint}. Please specify a valid URL.`); } networkConfig.lightnetAccountManagerEndpoint = endpoint; } /** * Gets account information on the specified publicKey by performing a GraphQL query * to the specified endpoint. This will call the 'GetAccountInfo' query which fetches * zkapp related account information. * * If an error is returned by the specified endpoint, an error is thrown. Otherwise, * the data is returned. * * @param accountInfo The public key and token id of the account to fetch * @param accountInfo.publicKey The specified publicKey to get account information on * @param accountInfo.tokenId The specified tokenId to get account information on * @param graphqlEndpoint The graphql endpoint to fetch from * @param config An object that exposes an additional timeout and header options * @returns zkapp information on the specified account or an error is thrown */ async function fetchAccount(accountInfo, graphqlEndpoint = networkConfig.minaEndpoint, { timeout = defaultTimeout, headers } = {}) { let publicKeyBase58 = accountInfo.publicKey instanceof PublicKey ? accountInfo.publicKey.toBase58() : accountInfo.publicKey; let tokenIdBase58 = typeof accountInfo.tokenId === 'string' || !accountInfo.tokenId ? accountInfo.tokenId : TokenId.toBase58(accountInfo.tokenId); return await fetchAccountInternal({ publicKey: publicKeyBase58, tokenId: tokenIdBase58 }, graphqlEndpoint, { timeout, headers: { ...networkConfig.minaDefaultHeaders, ...headers }, }); } // internal version of fetchAccount which does the same, but returns the original JSON version // of the account, to save some back-and-forth conversions when caching accounts async function fetchAccountInternal(accountInfo, graphqlEndpoint = networkConfig.minaEndpoint, config) { const { publicKey, tokenId } = accountInfo; let [response, error] = await makeGraphqlRequest(accountQuery(publicKey, tokenId ?? TokenId.toBase58(TokenId.default)), graphqlEndpoint, networkConfig.minaFallbackEndpoints, config); if (error !== undefined) return { account: undefined, error }; let fetchedAccount = response?.data?.account; if (!fetchedAccount) { return { account: undefined, error: { statusCode: 404, statusText: `fetchAccount: Account with public key ${publicKey} does not exist.`, }, }; } let account = parseFetchedAccount(fetchedAccount); // account successfully fetched - add to cache before returning addCachedAccountInternal(account, graphqlEndpoint); return { account, error: undefined, }; } /** * Fetches detailed balance information for a time-locked account. * * This function retrieves account data and calculates the liquid and locked * balances based on the current global slot and the account's vesting schedule. * * @param accountInfo - The account identifier containing publicKey and optional tokenId * @param graphqlEndpoint - The GraphQL endpoint to fetch from (defaults to configured endpoint) * @param config - Optional fetch configuration with timeout and headers * @returns An object containing balance details and timing information, or an error */ async function fetchTimedAccountInfo(accountInfo, graphqlEndpoint = networkConfig.minaEndpoint, { timeout = defaultTimeout, headers } = {}) { const accountResult = await fetchAccount(accountInfo, graphqlEndpoint, { timeout, headers, }); if (accountResult.error) { return { error: accountResult.error }; } const account = accountResult.account; if (!account.timing.isTimed.toBoolean()) { return { error: { statusCode: 400, statusText: 'Account is not time-locked. This function should only be called for accounts with vesting schedules.', }, }; } const lastBlock = await fetchLastBlock(graphqlEndpoint, headers); const globalSlot = lastBlock.globalSlotSinceGenesis; const accountTiming = new AccountTiming(account.timing); const totalBalance = account.balance; // lockedBalance is the minimum balance that must remain (from min_balance_at_slot in OCaml) const lockedBalance = accountTiming.minimumBalanceAtSlot(globalSlot); // liquidBalance is what's available to spend (total - locked) const liquidBalance = totalBalance.sub(lockedBalance); return { account, totalBalance, lockedBalance, liquidBalance, blockHeight: lastBlock.blockchainLength, globalSlot, error: undefined, }; } // Specify 5min as the default timeout const defaultTimeout = 5 * 60 * 1000; let accountCache = {}; let networkCache = {}; let actionsCache = {}; let accountsToFetch = {}; let networksToFetch = {}; let actionsToFetch = {}; let genesisConstantsCache = {}; const emptyKey = PublicKey.empty().toBase58(); function markAccountToBeFetched(publicKey, tokenId, graphqlEndpoint) { let publicKeyBase58 = publicKey.toBase58(); if (publicKeyBase58 === emptyKey) return; let tokenBase58 = TokenId.toBase58(tokenId); accountsToFetch[`${publicKeyBase58};${tokenBase58};${graphqlEndpoint}`] = { publicKey: publicKeyBase58, tokenId: tokenBase58, graphqlEndpoint, }; } function markNetworkToBeFetched(graphqlEndpoint) { networksToFetch[graphqlEndpoint] = { graphqlEndpoint }; } function markActionsToBeFetched(publicKey, tokenId, graphqlEndpoint, actionStates = {}) { let publicKeyBase58 = publicKey.toBase58(); let tokenBase58 = TokenId.toBase58(tokenId); let { fromActionState, endActionState } = actionStates; let fromActionStateBase58 = fromActionState ? fromActionState.toString() : undefined; let endActionStateBase58 = endActionState ? endActionState.toString() : undefined; actionsToFetch[`${publicKeyBase58};${tokenBase58};${graphqlEndpoint}`] = { publicKey: publicKeyBase58, tokenId: tokenBase58, actionStates: { fromActionState: fromActionStateBase58, endActionState: endActionStateBase58, }, graphqlEndpoint, }; } async function fetchMissingData(graphqlEndpoint, archiveEndpoint) { let promises = Object.entries(accountsToFetch).map(async ([key, { publicKey, tokenId }]) => { let response = await fetchAccountInternal({ publicKey, tokenId }, graphqlEndpoint); if (response.error === undefined) delete accountsToFetch[key]; }); let actionPromises = Object.entries(actionsToFetch).map(async ([key, { publicKey, actionStates, tokenId }]) => { let response = await fetchActions({ publicKey, actionStates, tokenId }, archiveEndpoint); if (!('error' in response) || response.error === undefined) delete actionsToFetch[key]; }); promises.push(...actionPromises); let network = Object.entries(networksToFetch).find(([, network]) => { return network.graphqlEndpoint === graphqlEndpoint; }); if (network !== undefined) { promises.push((async () => { const [lastBlockOk, constantsOk] = await Promise.all([ fetchLastBlock(graphqlEndpoint) .then(() => true) .catch(() => false), fetchGenesisConstants(graphqlEndpoint) .then(() => true) .catch(() => false), ]); if (lastBlockOk && constantsOk) { delete networksToFetch[network[0]]; } })()); } await Promise.all(promises); } function getCachedAccount(publicKey, tokenId, graphqlEndpoint = networkConfig.minaEndpoint) { return accountCache[accountCacheKey(publicKey, tokenId, graphqlEndpoint)]?.account; } function getCachedNetwork(graphqlEndpoint = networkConfig.minaEndpoint) { return networkCache[graphqlEndpoint]?.network; } function getCachedActions(publicKey, tokenId, graphqlEndpoint = networkConfig.archiveEndpoint) { return actionsCache[accountCacheKey(publicKey, tokenId, graphqlEndpoint)]?.actions; } function getCachedGenesisConstants(graphqlEndpoint = networkConfig.minaEndpoint) { return genesisConstantsCache[graphqlEndpoint]; } /** * Adds an account to the local cache, indexed by a GraphQL endpoint. */ function addCachedAccount(partialAccount, graphqlEndpoint = networkConfig.minaEndpoint) { let account = fillPartialAccount(partialAccount); addCachedAccountInternal(account, graphqlEndpoint); } function addCachedAccountInternal(account, graphqlEndpoint) { accountCache[accountCacheKey(account.publicKey, account.tokenId, graphqlEndpoint)] = { account, graphqlEndpoint, timestamp: Date.now(), }; } function addCachedActions({ publicKey, tokenId }, actions, graphqlEndpoint) { actionsCache[`${publicKey};${tokenId};${graphqlEndpoint}`] = { actions, graphqlEndpoint, timestamp: Date.now(), }; } function accountCacheKey(publicKey, tokenId, graphqlEndpoint) { return `${publicKey.toBase58()};${TokenId.toBase58(tokenId)};${graphqlEndpoint}`; } /** * Fetches the last block on the Mina network. * * This returns comprehensive network state information including the global slot * since genesis (`globalSlotSinceGenesis`), blockchain length, ledger hashes, * currency supply, and epoch data. * * For a lightweight query that only fetches slot information, use `fetchCurrentSlot()`, * which can return either the global slot since genesis or the slot within the current epoch. */ async function fetchLastBlock(graphqlEndpoint = networkConfig.minaEndpoint, headers) { let [resp, error] = await makeGraphqlRequest(lastBlockQuery, graphqlEndpoint, networkConfig.minaFallbackEndpoints, { headers: { ...networkConfig.minaDefaultHeaders, ...headers } }); if (error) throw Error(error.statusText); let lastBlock = resp?.data?.bestChain?.[0]; if (lastBlock === undefined) { throw Error('Failed to fetch latest network state.'); } let network = parseFetchedBlock(lastBlock); networkCache[graphqlEndpoint] = { network, graphqlEndpoint, timestamp: Date.now(), }; return network; } /** * Fetches the current slot number of the Mina network. * * By default, returns the global slot since genesis (the cumulative count of all slots * since the network launched). This matches `fetchLastBlock().globalSlotSinceGenesis`. * * Alternatively, you can fetch the slot within the current epoch by passing `slotType: 'epoch'`. * The epoch slot resets to 0 at each epoch boundary (approximately every 14 days) and * ranges from 0 to ~7,139. * * @param graphqlEndpoint GraphQL endpoint to fetch from * @param slotType Type of slot to fetch: 'global' (default) for slot since genesis, or 'epoch' for slot within current epoch * @param headers Optional headers to pass to the fetch request * @returns The slot number (either global or epoch-relative based on slotType) * * @example * ```ts * // Fetch global slot (default) * const globalSlot = await fetchCurrentSlot('https://api.minascan.io/node/devnet/v1/graphql'); * * // Fetch epoch-relative slot * const epochSlot = await fetchCurrentSlot( * 'https://api.minascan.io/node/devnet/v1/graphql', * 'epoch' * ); * ``` */ async function fetchCurrentSlot(graphqlEndpoint = networkConfig.minaEndpoint, slotType = 'global', headers) { let [resp, error] = await makeGraphqlRequest(currentSlotQuery, graphqlEndpoint, networkConfig.minaFallbackEndpoints, { headers: { ...networkConfig.minaDefaultHeaders, ...headers } }); if (error) throw Error(`Error making GraphQL request: ${error.statusText}`); let bestChain = resp?.data?.bestChain; if (!bestChain || bestChain.length === 0) { throw Error('Failed to fetch the current slot. The response data is undefined.'); } const consensusState = bestChain[0].protocolState.consensusState; return slotType === 'epoch' ? consensusState.slot : consensusState.slotSinceGenesis; } async function fetchLatestBlockZkappStatus(blockLength, graphqlEndpoint = networkConfig.minaEndpoint) { let [resp, error] = await makeGraphqlRequest(lastBlockQueryFailureCheck(blockLength), graphqlEndpoint, networkConfig.minaFallbackEndpoints, { headers: networkConfig.minaDefaultHeaders }); if (error) throw Error(`Error making GraphQL request: ${error.statusText}`); let bestChain = resp?.data; if (bestChain === undefined) { throw Error('Failed to fetch the latest zkApp transaction status. The response data is undefined.'); } return bestChain; } async function checkZkappTransaction(transactionHash, blockLength = 20) { let bestChainBlocks = await fetchLatestBlockZkappStatus(blockLength); for (let block of bestChainBlocks.bestChain) { for (let zkappCommand of block.transactions.zkappCommands) { if (zkappCommand.hash === transactionHash) { const blockHeight = parseInt(block.protocolState.consensusState.blockHeight, 10); if (zkappCommand.failureReason !== null) { let failureReason = zkappCommand.failureReason.reverse().map((failure) => { return [failure.failures.map((failureItem) => failureItem)]; }); return { success: false, failureReason, blockHeight, }; } else { return { success: true, failureReason: null, blockHeight, }; } } } } return { success: false, failureReason: null, blockHeight: undefined, }; } /** * Default finality threshold of 15 blocks provides 99.9% confidence. * @see https://docs.minaprotocol.com/mina-protocol/lifecycle-of-a-payment */ const DEFAULT_FINALITY_THRESHOLD = 15; /** * Fetches the depth (confirmation count) of a transaction in the blockchain. * Depth represents how many blocks have been built on top of the block containing the transaction. * * @param transactionHash - The hash of the transaction to check * @param options - Optional configuration for the depth query * @param options.blockLength - Number of blocks to search for the transaction (default: 20) * @param options.finalityThreshold - Number of blocks required for finality (default: 15, which provides 99.9% confidence) * @returns TransactionDepthInfo if the transaction is found and successful, null otherwise * * @example * ```ts * // Check depth of a transaction * const depthInfo = await fetchTransactionDepth('5JuKp...'); * if (depthInfo) { * console.log(`Depth: ${depthInfo.depth}, Finalized: ${depthInfo.isFinalized}`); * } * * // Use custom finality threshold * const depthInfo = await fetchTransactionDepth('5JuKp...', { finalityThreshold: 10 }); * ``` * * @see https://docs.minaprotocol.com/mina-protocol/lifecycle-of-a-payment */ async function fetchTransactionDepth(transactionHash, options) { const blockLength = options?.blockLength ?? 20; const finalityThreshold = options?.finalityThreshold ?? DEFAULT_FINALITY_THRESHOLD; let bestChainBlocks = await fetchLatestBlockZkappStatus(blockLength); if (bestChainBlocks.bestChain.length === 0) { return null; } // The first block in bestChain is the current tip const currentBlockHeight = parseInt(bestChainBlocks.bestChain[0].protocolState.consensusState.blockHeight, 10); // Search through blocks for the transaction for (let block of bestChainBlocks.bestChain) { for (let zkappCommand of block.transactions.zkappCommands) { if (zkappCommand.hash === transactionHash) { // Transaction found - if it has failures, it's not validly included if (zkappCommand.failureReason !== null) { return null; } const inclusionBlockHeight = parseInt(block.protocolState.consensusState.blockHeight, 10); // Depth should never be negative (safeguard against edge cases) const depth = Math.max(0, currentBlockHeight - inclusionBlockHeight); return { depth, inclusionBlockHeight, currentBlockHeight, isFinalized: depth >= finalityThreshold, finalityThreshold, }; } } } return null; } function parseFetchedBlock({ protocolState: { blockchainState: { snarkedLedgerHash }, consensusState: { blockHeight, minWindowDensity, totalCurrency, slotSinceGenesis, nextEpochData, stakingEpochData, }, }, }) { return { snarkedLedgerHash: LedgerHash.fromBase58(snarkedLedgerHash), // TODO: use date or utcDate? blockchainLength: UInt32.from(blockHeight), minWindowDensity: UInt32.from(minWindowDensity), totalCurrency: UInt64.from(totalCurrency), globalSlotSinceGenesis: UInt32.from(slotSinceGenesis), nextEpochData: parseEpochData(nextEpochData), stakingEpochData: parseEpochData(stakingEpochData), }; } function parseEpochData({ ledger: { hash, totalCurrency }, seed, startCheckpoint, lockCheckpoint, epochLength, }) { return { ledger: { hash: LedgerHash.fromBase58(hash), totalCurrency: UInt64.from(totalCurrency), }, seed: EpochSeed.fromBase58(seed), startCheckpoint: StateHash.fromBase58(startCheckpoint), lockCheckpoint: StateHash.fromBase58(lockCheckpoint), epochLength: UInt32.from(epochLength), }; } /** * Fetches the status of a transaction. */ async function fetchTransactionStatus(txId, graphqlEndpoint = networkConfig.minaEndpoint, headers) { let [resp, error] = await makeGraphqlRequest(transactionStatusQuery(txId), graphqlEndpoint, networkConfig.minaFallbackEndpoints, { headers: { ...networkConfig.minaDefaultHeaders, ...headers } }); if (error) throw Error(error.statusText); let txStatus = resp?.data?.transactionStatus; if (txStatus === undefined || txStatus === null) { throw Error(`Failed to fetch transaction status. TransactionId: ${txId}`); } return txStatus; } /** * Sends a zkApp command (transaction) to the specified GraphQL endpoint. */ function sendZkapp(json, graphqlEndpoint = networkConfig.minaEndpoint, { timeout = defaultTimeout, headers } = {}) { return makeGraphqlRequest(sendZkappQuery(json), graphqlEndpoint, networkConfig.minaFallbackEndpoints, { timeout, headers: { ...networkConfig.minaDefaultHeaders, ...headers }, }); } /** * Asynchronously fetches event data for an account from the Mina Archive Node GraphQL API. * @param accountInfo - The account information object. * @param accountInfo.publicKey - The account public key. * @param [accountInfo.tokenId] - The optional token ID for the account. * @param [graphqlEndpoint=networkConfig.archiveEndpoint] - The GraphQL endpoint to query. Defaults to the Archive Node GraphQL API. * @param [filterOptions={}] - The optional filter options object. * @param headers - Optional headers to pass to the fetch request * @returns A promise that resolves to an array of objects containing event data, block information and transaction information for the account. * @throws If the GraphQL request fails or the response is invalid. * @example * ```ts * const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' }; * const events = await fetchEvents(accountInfo); * console.log(events); * ``` */ async function fetchEvents(queryInputs, graphqlEndpoint = networkConfig.archiveEndpoint, headers) { if (!graphqlEndpoint) throw Error('fetchEvents: Specified GraphQL endpoint is undefined. When using events, you must set the archive node endpoint in Mina.Network(). Please ensure your Mina.Network() configuration includes an archive node endpoint.'); let [response, error] = await makeGraphqlRequest(getEventsQuery(queryInputs), graphqlEndpoint, networkConfig.archiveFallbackEndpoints, { headers: { ...networkConfig.archiveDefaultHeaders, ...headers } }); if (error) throw Error(error.statusText); let fetchedEvents = response?.data.events; if (fetchedEvents === undefined) { throw Error(`Failed to fetch events data. Account: ${queryInputs.publicKey} Token: ${queryInputs.tokenId}`); } return fetchedEvents.map((event) => { let events = event.eventData.map(({ data, transactionInfo }) => { return { data, transactionInfo, }; }); return { events, blockHeight: UInt32.from(event.blockInfo.height), blockHash: event.blockInfo.stateHash, parentBlockHash: event.blockInfo.parentHash, globalSlot: UInt32.from(event.blockInfo.globalSlotSinceGenesis), chainStatus: event.blockInfo.chainStatus, }; }); } /** * Fetches account actions for a specified public key and token ID by performing a GraphQL query. * * @param accountInfo - An {@link ActionsQueryInputs} containing the public key, and optional query parameters for the actions query * @param graphqlEndpoint - The GraphQL endpoint to fetch from. Defaults to the configured Mina endpoint. * @param headers - Optional headers to pass to the fetch request * * @returns A promise that resolves to an object containing the final actions hash for the account, and a list of actions * @throws Will throw an error if the GraphQL endpoint is invalid or if the fetch request fails. * * @example * ```ts * const accountInfo = { publicKey: 'B62qiwmXrWn7Cok5VhhB3KvCwyZ7NHHstFGbiU5n7m8s2RqqNW1p1wF' }; * const actionsList = await fetchAccount(accountInfo); * console.log(actionsList); * ``` */ async function fetchActions(queryInputs, graphqlEndpoint = networkConfig.archiveEndpoint, headers) { if (!graphqlEndpoint) throw Error('fetchActions: Specified GraphQL endpoint is undefined. When using actions, you must set the archive node endpoint in Mina.Network(). Please ensure your Mina.Network() configuration includes an archive node endpoint.'); queryInputs.tokenId = queryInputs.tokenId ?? TokenId.toBase58(TokenId.default); let [response, _error] = await makeGraphqlRequest(getActionsQuery(queryInputs), graphqlEndpoint, networkConfig.archiveFallbackEndpoints, { headers: { ...networkConfig.archiveDefaultHeaders, ...headers } }); let fetchedActions = response?.data.actions; if (fetchedActions === undefined) { return { error: { statusCode: 404, statusText: `fetchActions: Account with public key ${queryInputs.publicKey} with tokenId ${queryInputs.tokenId} does not exist.`, }, }; } const actionsList = createActionsList({ publicKey: queryInputs.publicKey, actionStates: { fromActionState: queryInputs.actionStates?.fromActionState, endActionState: queryInputs.actionStates?.endActionState, }, }, fetchedActions); addCachedActions({ publicKey: queryInputs.publicKey, tokenId: queryInputs.tokenId }, actionsList, graphqlEndpoint); return actionsList; } /** * Given a graphQL response from #getActionsQuery, process the actions into a canonical actions list */ export function createActionsList(accountInfo, fetchedActions) { const { publicKey, actionStates } = accountInfo; let actionsList = []; // correct for archive node sending one block too many if (fetchedActions.length !== 0 && fetchedActions[0].actionState.actionStateOne === actionStates.fromActionState) { fetchedActions = fetchedActions.slice(1); } fetchedActions.forEach((actionBlock) => { let { actionData } = actionBlock; let latestActionState = Field(actionBlock.actionState.actionStateTwo); let actionState = actionBlock.actionState.actionStateOne; if (actionData.length === 0) throw Error(`No action data was found for the account ${publicKey} with the latest action state ${actionState}`); // DEPRECATED: In case the archive node is running an out-of-date version, best guess is to sort by the account update id // As of 2025-01-07, minascan is running a version of the node which supports `sequenceNumber` and `zkappAccountUpdateIds` fields // We could consider removing this fallback since no other nodes are widely used if (!actionData[0].transactionInfo) { actionData = actionData.sort((a1, a2) => { return Number(a1.accountUpdateId) - Number(a2.accountUpdateId); }); } else { // sort actions within one block by transaction sequence number and account update sequence actionData = actionData.sort((a1, a2) => { const a1TxSequence = a1.transactionInfo.sequenceNumber; const a2TxSequence = a2.transactionInfo.sequenceNumber; if (a1TxSequence === a2TxSequence) { const a1AuSequence = a1.transactionInfo.zkappAccountUpdateIds.indexOf(Number(a1.accountUpdateId)); const a2AuSequence = a2.transactionInfo.zkappAccountUpdateIds.indexOf(Number(a2.accountUpdateId)); return a1AuSequence - a2AuSequence; } else { return a1TxSequence - a2TxSequence; } }); } // split actions by account update let actionsByAccountUpdate = []; let currentAccountUpdateId = 'none'; let currentActions; actionData.forEach(({ accountUpdateId, data }) => { if (accountUpdateId === currentAccountUpdateId) { currentActions.push(data); } else { currentAccountUpdateId = accountUpdateId; currentActions = [data]; actionsByAccountUpdate.push(currentActions); } }); // re-hash actions for (let actions of actionsByAccountUpdate) { latestActionState = updateActionState(actions, latestActionState); actionsList.push({ actions, hash: latestActionState.toString() }); } const finalActionState = latestActionState.toString(); const expectedActionState = actionState; if (finalActionState !== expectedActionState) { throw new Error(`Failed to derive correct actions hash for ${publicKey}. Derived hash: ${finalActionState}, expected hash: ${expectedActionState}). All action hashes derived: ${JSON.stringify(actionsList, null, 2)} Please try a different Archive Node API endpoint. `); } }); return actionsList; } /** * Fetches genesis constants. */ async function fetchGenesisConstants(graphqlEndpoint = networkConfig.minaEndpoint, headers) { let [resp, error] = await makeGraphqlRequest(genesisConstantsQuery, graphqlEndpoint, networkConfig.minaFallbackEndpoints, { headers: { ...networkConfig.minaDefaultHeaders, ...headers } }); if (error) throw Error(error.statusText); const genesisConstants = resp?.data?.genesisConstants; const consensusConfiguration = resp?.data?.daemonStatus?.consensusConfiguration; if (genesisConstants === undefined || consensusConfiguration === undefined) { throw Error('Failed to fetch genesis constants.'); } const data = { genesisTimestamp: genesisConstants.genesisTimestamp, coinbase: Number(genesisConstants.coinbase), accountCreationFee: Number(genesisConstants.accountCreationFee), epochDuration: Number(consensusConfiguration.epochDuration), k: Number(consensusConfiguration.k), slotDuration: Number(consensusConfiguration.slotDuration), slotsPerEpoch: Number(consensusConfiguration.slotsPerEpoch), }; genesisConstantsCache[graphqlEndpoint] = data; return data; } var Lightnet; (function (Lightnet) { /** * Gets random key pair (public and private keys) from account manager * that operates with accounts configured in target network Genesis Ledger. * * If an error is returned by the specified endpoint, an error is thrown. Otherwise, * the data is returned. * * @param options.isRegularAccount Whether to acquire key pair of regular or zkApp account (one with already configured verification key) * @param options.lightnetAccountManagerEndpoint Account manager endpoint to fetch from * @returns Key pair */ async function acquireKeyPair(options = {}) { const { isRegularAccount = true, lightnetAccountManagerEndpoint = networkConfig.lightnetAccountManagerEndpoint, } = options; const response = await fetch(`${lightnetAccountManagerEndpoint}/acquire-account?isRegularAccount=${isRegularAccount}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (response.ok) { const data = await response.json(); if (data) { return { publicKey: PublicKey.fromBase58(data.pk), privateKey: PrivateKey.fromBase58(data.sk), }; } } throw new Error('Failed to acquire the key pair'); } Lightnet.acquireKeyPair = acquireKeyPair; /** * Releases previously acquired key pair by public key. * * @param options.publicKey Public key of previously acquired key pair to release * @param options.lightnetAccountManagerEndpoint Account manager endpoint to fetch from * @returns Response message from the account manager as string or null if the request failed */ async function releaseKeyPair(options) { const { publicKey, lightnetAccountManagerEndpoint = networkConfig.lightnetAccountManagerEndpoint, } = options; const response = await fetch(`${lightnetAccountManagerEndpoint}/release-account`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ pk: publicKey, }), }); if (response.ok) { const data = await response.json(); if (data) { return data.message; } } return null; } Lightnet.releaseKeyPair = releaseKeyPair; /** * Gets previously acquired key pairs list. * * @param options.lightnetAccountManagerEndpoint Account manager endpoint to fetch from * @returns Key pairs list or null if the request failed */ async function listAcquiredKeyPairs(options) { const { lightnetAccountManagerEndpoint = networkConfig.lightnetAccountManagerEndpoint } = options; const response = await fetch(`${lightnetAccountManagerEndpoint}/list-acquired-accounts`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (response.ok) { const data = await response.json(); if (data) { return data.map((account) => ({ publicKey: PublicKey.fromBase58(account.pk), privateKey: PrivateKey.fromBase58(account.sk), })); } } return null; } Lightnet.listAcquiredKeyPairs = listAcquiredKeyPairs; })(Lightnet || (Lightnet = {})); function updateActionState(actions, actionState) { let actionHash = Actions.fromJSON(actions).hash; return Actions.updateSequenceState(actionState, actionHash); } // TODO it seems we're not actually catching most errors here async function makeGraphqlRequest(query, graphqlEndpoint = networkConfig.minaEndpoint, fallbackEndpoints, { timeout = defaultTimeout, headers } = {}) { if (graphqlEndpoint === 'none') throw Error("Should have made a graphql request, but don't know to which endpoint. Try calling `setGraphqlEndpoint` first."); let timeouts = []; const clearTimeouts = () => { timeouts.forEach((t) => clearTimeout(t)); timeouts = []; }; const makeRequest = async (url) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); timeouts.push(timer); let body = JSON.stringify({ operationName: null, query, variables: {} }); try { let response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, }, body, signal: controller.signal, }); return checkResponseStatus(response); } finally { clearTimeouts(); } }; // try to fetch from endpoints in pairs let timeoutErrors = []; let urls = [graphqlEndpoint, ...fallbackEndpoints]; for (let i = 0; i < urls.length; i += 2) { let url1 = urls[i]; let url2 = urls[i + 1]; if (url2 === undefined) { try { return await makeRequest(url1); } catch (error) { return [undefined, inferError(error)]; } } try { return await Promise.race([makeRequest(url1), makeRequest(url2)]); } catch (unknownError) { let error = inferError(unknownError); if (error.statusCode === 408) { // If the request timed out, try the next 2 endpoints timeoutErrors.push({ url1, url2, error }); } else { // If the request failed for some other reason (e.g. o1js error), return the error return [undefined, error]; } } } const statusText = timeoutErrors .map(({ url1, url2, error }) => `Request to ${url1} and ${url2} timed out. Error: ${error}`) .join('\n'); return [undefined, { statusCode: 408, statusText }]; } async function checkResponseStatus(response) { if (response.ok) { let jsonResponse = await response.json(); if (jsonResponse.errors && jsonResponse.errors.length > 0) { return [ undefined, { statusCode: response.status, statusText: jsonResponse.errors.map((error) => error.message).join('\n'), }, ]; } else if (jsonResponse.data === undefined) { return [ undefined, { statusCode: response.status, statusText: `GraphQL response data is undefined`, }, ]; } return [jsonResponse, undefined]; } else { return [ undefined, { statusCode: response.status, statusText: response.statusText, }, ]; } } function inferError(error) { let errorMessage = JSON.stringify(error); if (error instanceof AbortSignal) { return { statusCode: 408, statusText: `Request Timeout: ${errorMessage}` }; } else { return { statusCode: 500, statusText: `Unknown Error: ${errorMessage}`, }; } } //# sourceMappingURL=fetch.js.map