UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

492 lines (466 loc) 17.6 kB
import { Test } from '../../../snarky.js'; import { Field } from '../../provable/wrapped.js'; import { UInt64 } from '../../provable/int.js'; import { PublicKey } from '../../provable/crypto/signature.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 { Account } from './account.js'; import { NetworkId } from '../../../mina-signer/src/types.js'; import { currentTransaction } from './transaction-context.js'; import { type FeePayerSpec, type ActionStates, type NetworkConstants, activeInstance, setActiveInstance, Mina, defaultNetworkConstants, currentSlot, getAccount, hasAccount, getBalance, getNetworkId, getNetworkConstants, getNetworkState, fetchEvents, fetchActions, getActions, getProofsEnabled, } from './mina-instance.js'; import { type EventActionFilterOptions } from './graphql.js'; import { Transaction, type PendingTransaction, type IncludedTransaction, type RejectedTransaction, type PendingTransactionStatus, type PendingTransactionPromise, 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, type PendingTransaction, type IncludedTransaction, type RejectedTransaction, type PendingTransactionStatus, type PendingTransactionPromise, TestPublicKey, activeInstance, setActiveInstance, transaction, sender, currentSlot, getAccount, hasAccount, getBalance, getNetworkId, getNetworkConstants, getNetworkState, fetchEvents, fetchActions, getActions, FeePayerSpec, ActionStates, faucet, waitForFunding, getProofsEnabled, // for internal testing only filterGroups, type NetworkConstants, }; // patch active instance so that we can still create basic transactions without giving Mina network details setActiveInstance({ ...activeInstance, transaction(sender: FeePayerSpec, f: () => Promise<void>) { return toTransactionPromise(() => createTransaction(sender, f, 0)); }, }); /** * Represents the Mina blockchain running on a real network */ function Network(graphqlEndpoint: string): Mina; function Network(options: { networkId?: NetworkId; mina: string | string[]; archive?: string | string[]; lightnetAccountManager?: string; bypassTransactionLimits?: boolean; minaDefaultHeaders?: HeadersInit; archiveDefaultHeaders?: HeadersInit; }): Mina; function Network( options: | { networkId?: NetworkId; mina: string | string[]; archive?: string | string[]; lightnetAccountManager?: string; bypassTransactionLimits?: boolean; minaDefaultHeaders?: HeadersInit; archiveDefaultHeaders?: HeadersInit; } | string ): Mina { let minaNetworkId: NetworkId = 'devnet'; let minaGraphqlEndpoint: string; let archiveEndpoint: string; let lightnetAccountManagerEndpoint: string; let enforceTransactionLimits: boolean = 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: PublicKey, tokenId: Field = TokenId.default) { if (!currentTransaction.has() || currentTransaction.get().fetchMode === 'cached') { return !!Fetch.getCachedAccount(publicKey, tokenId, minaGraphqlEndpoint); } return false; }, getAccount(publicKey: PublicKey, tokenId: Field = 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: string[] = []; if (response === undefined && error !== undefined) { errors = [JSON.stringify(error)]; } else if (response && response.errors && response.errors.length > 0) { response?.errors.forEach((e: any) => errors.push(JSON.stringify(e))); } const updatedErrors = humanizeErrors(errors); const status: PendingTransactionStatus = errors.length === 0 ? 'pending' : 'rejected'; let mlTest = await Test(); const hash = mlTest.transactionHash.hashZkAppCommand(txn.toJSON()); const pendingTransaction: Omit<PendingTransaction, 'wait' | 'safeWait'> = { 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: string, maxAttempts: number, interval: number, attempts: number = 0 ): Promise<IncludedTransaction | RejectedTransaction> => { let res: Awaited<ReturnType<typeof Fetch.checkZkappTransaction>>; 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 as 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: number = 45, interval: number = 20000 ): Promise<IncludedTransaction | RejectedTransaction> => { return pollTransactionStatus(hash, maxAttempts, interval); }; const wait = async (options?: { maxAttempts?: number; interval?: number; }): Promise<IncludedTransaction> => { 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?: { maxAttempts?: number; interval?: number; }): Promise<IncludedTransaction | RejectedTransaction> => { if (status === 'rejected') { return createRejectedTransaction(pendingTransaction, pendingTransaction.errors); } return await poll(options?.maxAttempts, options?.interval); }; return { ...pendingTransaction, wait, safeWait, }; }); }, transaction(sender: FeePayerSpec, f: () => Promise<void>) { return toTransactionPromise(async () => { // TODO we run the transcation 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: PublicKey, tokenId: Field = TokenId.default, filterOptions: EventActionFilterOptions = {}, headers?: HeadersInit ) { 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: PublicKey, actionStates?: ActionStates, tokenId: Field = TokenId.default, from?: number, to?: number, headers?: HeadersInit ) { 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: PublicKey, actionStates?: ActionStates, tokenId: Field = 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?: PublicKey): Account { let dummy = Types.Account.empty(); if (pubkey) dummy.publicKey = pubkey; return dummy; } async function waitForFunding(address: string, headers?: HeadersInit): Promise<void> { let attempts = 0; let maxAttempts = 30; let interval = 30000; const executePoll = async (resolve: () => void, reject: (err: Error) => void | Error) => { 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: PublicKey, network: string = 'devnet', headers?: HeadersInit) { 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: Fetch.GenesisConstants): NetworkConstants { return { genesisTimestamp: UInt64.from(Date.parse(genesisConstants.genesisTimestamp)), slotTime: UInt64.from(genesisConstants.slotDuration), accountCreationFee: UInt64.from(genesisConstants.accountCreationFee), }; }