UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

328 lines (327 loc) 16.7 kB
import { Test } from '../../../bindings.js'; import { UInt64 } from '../../provable/int.js'; import { TokenId, Authorization } from './account-update.js'; import * as Fetch from './fetch.js'; import { humanizeErrors, invalidTransactionError } from './errors.js'; import { Types } from '../../../bindings/mina-transaction/v1/types.js'; import { currentTransaction } from './transaction-context.js'; import { activeInstance, setActiveInstance, defaultNetworkConstants, currentSlot, getAccount, hasAccount, getBalance, getNetworkId, getNetworkConstants, getNetworkState, fetchEvents, fetchActions, getActions, getProofsEnabled, } from './mina-instance.js'; import { Transaction, createTransaction, toTransactionPromise, transaction, createRejectedTransaction, createIncludedTransaction, toPendingTransactionPromise, } from './transaction.js'; import { reportGetAccountError, verifyTransactionLimits, defaultNetworkState, filterGroups, } from './transaction-validation.js'; import { LocalBlockchain, TestPublicKey } from './local-blockchain.js'; export { LocalBlockchain, Network, currentTransaction, Transaction, TestPublicKey, activeInstance, setActiveInstance, transaction, sender, currentSlot, getAccount, hasAccount, getBalance, getNetworkId, getNetworkConstants, getNetworkState, fetchEvents, fetchActions, getActions, faucet, waitForFunding, getProofsEnabled, // for internal testing only filterGroups, }; // patch active instance so that we can still create basic transactions without giving Mina network details setActiveInstance({ ...activeInstance, transaction(sender, f) { return toTransactionPromise(() => createTransaction(sender, f, 0)); }, }); function Network(options) { let minaNetworkId = 'devnet'; let minaGraphqlEndpoint; let archiveEndpoint; let lightnetAccountManagerEndpoint; let enforceTransactionLimits = true; if (options && typeof options === 'string') { minaGraphqlEndpoint = options; Fetch.setGraphqlEndpoint(minaGraphqlEndpoint); } else if (options && typeof options === 'object') { if (options.networkId) { minaNetworkId = options.networkId; } if (!options.mina) throw new Error("Network: malformed input. Please provide an object with 'mina' endpoint."); if (Array.isArray(options.mina) && options.mina.length !== 0) { minaGraphqlEndpoint = options.mina[0]; Fetch.setGraphqlEndpoint(minaGraphqlEndpoint, options.minaDefaultHeaders); Fetch.setMinaGraphqlFallbackEndpoints(options.mina.slice(1)); } else if (typeof options.mina === 'string') { minaGraphqlEndpoint = options.mina; Fetch.setGraphqlEndpoint(minaGraphqlEndpoint, options.minaDefaultHeaders); } if (options.archive !== undefined) { if (Array.isArray(options.archive) && options.archive.length !== 0) { archiveEndpoint = options.archive[0]; Fetch.setArchiveGraphqlEndpoint(archiveEndpoint, options.archiveDefaultHeaders); Fetch.setArchiveGraphqlFallbackEndpoints(options.archive.slice(1)); } else if (typeof options.archive === 'string') { archiveEndpoint = options.archive; Fetch.setArchiveGraphqlEndpoint(archiveEndpoint, options.archiveDefaultHeaders); } } if (options.lightnetAccountManager !== undefined && typeof options.lightnetAccountManager === 'string') { lightnetAccountManagerEndpoint = options.lightnetAccountManager; Fetch.setLightnetAccountManagerEndpoint(lightnetAccountManagerEndpoint); } if (options.bypassTransactionLimits !== undefined && typeof options.bypassTransactionLimits === 'boolean') { enforceTransactionLimits = !options.bypassTransactionLimits; } } else { throw new Error("Network: malformed input. Please provide a string or an object with 'mina' and 'archive' endpoints."); } return { getNetworkId: () => minaNetworkId, getNetworkConstants() { if (currentTransaction()?.fetchMode === 'test') { Fetch.markNetworkToBeFetched(minaGraphqlEndpoint); const genesisConstants = Fetch.getCachedGenesisConstants(minaGraphqlEndpoint); return genesisConstants !== undefined ? genesisToNetworkConstants(genesisConstants) : defaultNetworkConstants; } if (!currentTransaction.has() || currentTransaction.get().fetchMode === 'cached') { const genesisConstants = Fetch.getCachedGenesisConstants(minaGraphqlEndpoint); if (genesisConstants !== undefined) return genesisToNetworkConstants(genesisConstants); } return defaultNetworkConstants; }, currentSlot() { throw Error('currentSlot() is not implemented yet for remote blockchains.'); }, hasAccount(publicKey, tokenId = TokenId.default) { if (!currentTransaction.has() || currentTransaction.get().fetchMode === 'cached') { return !!Fetch.getCachedAccount(publicKey, tokenId, minaGraphqlEndpoint); } return false; }, getAccount(publicKey, tokenId = TokenId.default) { if (currentTransaction()?.fetchMode === 'test') { Fetch.markAccountToBeFetched(publicKey, tokenId, minaGraphqlEndpoint); let account = Fetch.getCachedAccount(publicKey, tokenId, minaGraphqlEndpoint); return account ?? dummyAccount(publicKey); } if (!currentTransaction.has() || currentTransaction.get().fetchMode === 'cached') { let account = Fetch.getCachedAccount(publicKey, tokenId, minaGraphqlEndpoint); if (account !== undefined) return account; } throw Error(`${reportGetAccountError(publicKey.toBase58(), TokenId.toBase58(tokenId))}\nGraphql endpoint: ${minaGraphqlEndpoint}`); }, getNetworkState() { if (currentTransaction()?.fetchMode === 'test') { Fetch.markNetworkToBeFetched(minaGraphqlEndpoint); let network = Fetch.getCachedNetwork(minaGraphqlEndpoint); return network ?? defaultNetworkState(); } if (!currentTransaction.has() || currentTransaction.get().fetchMode === 'cached') { let network = Fetch.getCachedNetwork(minaGraphqlEndpoint); if (network !== undefined) return network; } throw Error(`getNetworkState: Could not fetch network state from graphql endpoint ${minaGraphqlEndpoint} outside of a transaction.`); }, sendTransaction(txn) { return toPendingTransactionPromise(async () => { if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction); let [response, error] = await Fetch.sendZkapp(txn.toJSON()); let errors = []; if (response === undefined && error !== undefined) { errors = [JSON.stringify(error)]; } else if (response && response.errors && response.errors.length > 0) { response?.errors.forEach((e) => errors.push(JSON.stringify(e))); } const updatedErrors = humanizeErrors(errors); const status = errors.length === 0 ? 'pending' : 'rejected'; let mlTest = await Test(); const hash = mlTest.transactionHash.hashZkAppCommand(txn.toJSON()); const pendingTransaction = { status, data: response?.data, errors: updatedErrors, transaction: txn.transaction, setFee: txn.setFee, setFeePerSnarkCost: txn.setFeePerSnarkCost, hash, toJSON: txn.toJSON, toPretty: txn.toPretty, }; const pollTransactionStatus = async (transactionHash, maxAttempts, interval, attempts = 0) => { let res; try { res = await Fetch.checkZkappTransaction(transactionHash); if (res.success) { return createIncludedTransaction(pendingTransaction); } else if (res.failureReason) { const error = invalidTransactionError(txn.transaction, res.failureReason, { accountCreationFee: defaultNetworkConstants.accountCreationFee.toString(), }); return createRejectedTransaction(pendingTransaction, [error]); } } catch (error) { return createRejectedTransaction(pendingTransaction, [error.message]); } if (maxAttempts && attempts >= maxAttempts) { return createRejectedTransaction(pendingTransaction, [ `Exceeded max attempts.\nTransactionId: ${transactionHash}\nAttempts: ${attempts}\nLast received status: ${res}`, ]); } await new Promise((resolve) => setTimeout(resolve, interval)); return pollTransactionStatus(transactionHash, maxAttempts, interval, attempts + 1); }; // default is 45 attempts * 20s each = 15min // the block time on berkeley is currently longer than the average 3-4min, so its better to target a higher block time // fetching an update every 20s is more than enough with a current block time of 3min const poll = async (maxAttempts = 45, interval = 20000) => { return pollTransactionStatus(hash, maxAttempts, interval); }; const wait = async (options) => { const pendingTransaction = await safeWait(options); if (pendingTransaction.status === 'rejected') { throw Error(`Transaction failed with errors:\n${pendingTransaction.errors.join('\n')}`); } return pendingTransaction; }; const safeWait = async (options) => { if (status === 'rejected') { return createRejectedTransaction(pendingTransaction, pendingTransaction.errors); } return await poll(options?.maxAttempts, options?.interval); }; return { ...pendingTransaction, wait, safeWait, }; }); }, transaction(sender, f) { return toTransactionPromise(async () => { // TODO we run the transaction twice to be able to fetch data in between let tx = await createTransaction(sender, f, 0, { fetchMode: 'test', isFinalRunOutsideCircuit: false, }); await Fetch.fetchMissingData(minaGraphqlEndpoint, archiveEndpoint); let hasProofs = tx.transaction.accountUpdates.some(Authorization.hasLazyProof); return await createTransaction(sender, f, 1, { fetchMode: 'cached', isFinalRunOutsideCircuit: !hasProofs, }); }); }, async fetchEvents(publicKey, tokenId = TokenId.default, filterOptions = {}, headers) { const pubKey = publicKey.toBase58(); const token = TokenId.toBase58(tokenId); const from = filterOptions.from ? Number(filterOptions.from.toString()) : undefined; const to = filterOptions.to ? Number(filterOptions.to.toString()) : undefined; return Fetch.fetchEvents({ publicKey: pubKey, tokenId: token, from, to }, archiveEndpoint, headers); }, async fetchActions(publicKey, actionStates, tokenId = TokenId.default, from, to, headers) { const pubKey = publicKey.toBase58(); const token = TokenId.toBase58(tokenId); const { fromActionState, endActionState } = actionStates ?? {}; const fromActionStateBase58 = fromActionState ? fromActionState.toString() : undefined; const endActionStateBase58 = endActionState ? endActionState.toString() : undefined; return Fetch.fetchActions({ publicKey: pubKey, actionStates: { fromActionState: fromActionStateBase58, endActionState: endActionStateBase58, }, from, to, tokenId: token, }, archiveEndpoint, headers); }, getActions(publicKey, actionStates, tokenId = TokenId.default) { if (currentTransaction()?.fetchMode === 'test') { Fetch.markActionsToBeFetched(publicKey, tokenId, archiveEndpoint, actionStates); let actions = Fetch.getCachedActions(publicKey, tokenId); return actions ?? []; } if (!currentTransaction.has() || currentTransaction.get().fetchMode === 'cached') { let actions = Fetch.getCachedActions(publicKey, tokenId); if (actions !== undefined) return actions; } throw Error(`getActions: Could not find actions for the public key ${publicKey.toBase58()}`); }, proofsEnabled: true, }; } /** * Returns the public key of the current transaction's sender account. * * Throws an error if not inside a transaction, or the sender wasn't passed in. */ function sender() { let tx = currentTransaction(); if (tx === undefined) throw Error(`The sender is not available outside a transaction. Make sure you only use it within \`Mina.transaction\` blocks or smart contract methods.`); let sender = currentTransaction()?.sender; if (sender === undefined) throw Error(`The sender is not available, because the transaction block was created without the optional \`sender\` argument. Here's an example for how to pass in the sender and make it available: Mina.transaction(sender, // <-- pass in sender's public key here () => { // methods can use this.sender }); `); return sender; } function dummyAccount(pubkey) { let dummy = Types.Account.empty(); if (pubkey) dummy.publicKey = pubkey; return dummy; } async function waitForFunding(address, headers) { let attempts = 0; let maxAttempts = 30; let interval = 30000; const executePoll = async (resolve, reject) => { let { account } = await Fetch.fetchAccount({ publicKey: address }, undefined, { headers }); attempts++; if (account) { return resolve(); } else if (maxAttempts && attempts === maxAttempts) { return reject(new Error(`Exceeded max attempts`)); } else { setTimeout(executePoll, interval, resolve, reject); } }; return new Promise(executePoll); } /** * Requests the [testnet faucet](https://faucet.minaprotocol.com/api/v1/faucet) to fund a public key. */ async function faucet(pub, network = 'devnet', headers) { let address = pub.toBase58(); let response = await fetch('https://faucet.minaprotocol.com/api/v1/faucet', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ network, address: address, }), }); response = await response.json(); if (response.status.toString() !== 'success') { throw new Error(`Error funding account ${address}, got response status: ${response.status}, text: ${response.statusText}`); } await waitForFunding(address, headers); } function genesisToNetworkConstants(genesisConstants) { return { genesisTimestamp: UInt64.from(Date.parse(genesisConstants.genesisTimestamp)), slotTime: UInt64.from(genesisConstants.slotDuration), accountCreationFee: UInt64.from(genesisConstants.accountCreationFee), }; } //# sourceMappingURL=mina.js.map