o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
543 lines (518 loc) • 17.2 kB
text/typescript
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 { invalidTransactionError } from './errors.js';
import { Types } from '../../bindings/mina-transaction/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;
}): Mina;
function Network(
options:
| {
networkId?: NetworkId;
mina: string | string[];
archive?: string | string[];
lightnetAccountManager?: string;
}
| string
): Mina {
let minaNetworkId: NetworkId = 'testnet';
let minaGraphqlEndpoint: string;
let archiveEndpoint: string;
let lightnetAccountManagerEndpoint: string;
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);
Fetch.setMinaGraphqlFallbackEndpoints(options.mina.slice(1));
} else if (typeof options.mina === 'string') {
minaGraphqlEndpoint = options.mina;
Fetch.setGraphqlEndpoint(minaGraphqlEndpoint);
}
if (options.archive !== undefined) {
if (Array.isArray(options.archive) && options.archive.length !== 0) {
archiveEndpoint = options.archive[0];
Fetch.setArchiveGraphqlEndpoint(archiveEndpoint);
Fetch.setArchiveGraphqlFallbackEndpoints(options.archive.slice(1));
} else if (typeof options.archive === 'string') {
archiveEndpoint = options.archive;
Fetch.setArchiveGraphqlEndpoint(archiveEndpoint);
}
}
if (
options.lightnetAccountManager !== undefined &&
typeof options.lightnetAccountManager === 'string'
) {
lightnetAccountManagerEndpoint = options.lightnetAccountManager;
Fetch.setLightnetAccountManagerEndpoint(lightnetAccountManagerEndpoint);
}
} 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 () => {
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 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,
transaction: txn.transaction,
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 = {}
) {
let pubKey = publicKey.toBase58();
let token = TokenId.toBase58(tokenId);
return Fetch.fetchEvents(
{ publicKey: pubKey, tokenId: token },
archiveEndpoint,
filterOptions
);
},
async fetchActions(
publicKey: PublicKey,
actionStates?: ActionStates,
tokenId: Field = TokenId.default
) {
let pubKey = publicKey.toBase58();
let token = TokenId.toBase58(tokenId);
let { fromActionState, endActionState } = actionStates ?? {};
let fromActionStateBase58 = fromActionState
? fromActionState.toString()
: undefined;
let endActionStateBase58 = endActionState
? endActionState.toString()
: undefined;
return Fetch.fetchActions(
{
publicKey: pubKey,
actionStates: {
fromActionState: fromActionStateBase58,
endActionState: endActionStateBase58,
},
tokenId: token,
},
archiveEndpoint
);
},
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}`
);
},
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): 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 });
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 = 'berkeley-qanet') {
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);
}
function genesisToNetworkConstants(
genesisConstants: Fetch.GenesisConstants
): NetworkConstants {
return {
genesisTimestamp: UInt64.from(
Date.parse(genesisConstants.genesisTimestamp)
),
slotTime: UInt64.from(genesisConstants.slotDuration),
accountCreationFee: UInt64.from(genesisConstants.accountCreationFee),
};
}