From 688304b16412a6915fdd43dab19d8d32a7c62a71 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 07:46:20 -0400 Subject: [PATCH 01/23] enable chaining prove, typedocs --- src/examples/chaining-tx-methods.ts | 13 ++++-- src/lib/mina/transaction.ts | 64 ++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/examples/chaining-tx-methods.ts b/src/examples/chaining-tx-methods.ts index abdb604d3d..4692649b55 100644 --- a/src/examples/chaining-tx-methods.ts +++ b/src/examples/chaining-tx-methods.ts @@ -40,6 +40,8 @@ let initialBalance = 10_000_000_000; let zkapp = new SimpleZkapp(zkappAddress); await SimpleZkapp.analyzeMethods(); +console.log('deploy'); + await Mina.transaction(sender, async () => { let senderUpdate = AccountUpdate.fundNewAccount(sender); senderUpdate.send({ to: zkappAddress, amount: initialBalance }); @@ -52,10 +54,13 @@ await Mina.transaction(sender, async () => { console.log('initial state: ' + zkapp.x.get()); console.log('increment'); -const incrementTx = Mina.transaction(sender, async () => { + +await Mina.transaction(sender, async () => { await zkapp.increment(); -}).sign([senderKey]); -await incrementTx.then((v) => v.prove()); -await incrementTx.send().wait(); +}) + .sign([senderKey]) + .prove() + .send() + .wait(); console.log('final state: ' + zkapp.x.get()); diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 6dc1d564cd..d6c92b025d 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -92,7 +92,7 @@ type Transaction = { * 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 promise that resolves to a {@link PendingTransaction} instance representing the submitted transaction if the submission is successful. + * @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 @@ -104,7 +104,7 @@ type Transaction = { * } * ``` */ - send(): Promise; + 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. @@ -280,13 +280,32 @@ type RejectedTransaction = Pick< errors: string[]; }; +/** + * A `Promise` with some additional methods for making chained method calls + * into the pending value upon its resolution. + */ type TransactionPromise = Promise & { + /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ sign(...args: Parameters): TransactionPromise; + /** Equivalent to calling the resolved `Transaction`'s `send` method. */ send(): PendingTransactionPromise; + /** + * 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. + */ + proofPromise?: Promise<(Proof | undefined)[]>; }; function toTransactionPromise( - getPromise: () => Promise + getPromise: () => Promise, + proofPromise?: Promise<(Proof | undefined)[]> ): TransactionPromise { const pending = getPromise().then(); return Object.assign(pending, { @@ -296,10 +315,23 @@ function toTransactionPromise( send() { return toPendingTransactionPromise(() => pending.then((v) => v.send())); }, - }) as TransactionPromise; + prove() { + const proofPromise_ = proofPromise ?? pending.then((v) => v.prove()); + return toTransactionPromise(async () => { + await proofPromise_; + return await pending; + }, proofPromise_); + }, + proofPromise, + }); } +/** + * 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']; }; @@ -311,7 +343,7 @@ function toPendingTransactionPromise( wait(...args: Parameters) { return pending.then((v) => v.wait(...args)); }, - }) as PendingTransactionPromise; + }); } async function createTransaction( @@ -439,16 +471,18 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { toGraphqlQuery() { return sendZkappQuery(self.toJSON()); }, - async send() { - const pendingTransaction = await sendTransaction(self); - if (pendingTransaction.errors.length > 0) { - throw Error( - `Transaction failed with errors:\n- ${pendingTransaction.errors.join( - '\n- ' - )}` - ); - } - return pendingTransaction; + 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); From 4b359d2ca2074b73ae82fe7edb394c7f8bfae400 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 07:50:35 -0400 Subject: [PATCH 02/23] get rid of needless console log in example --- src/examples/chaining-tx-methods.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/examples/chaining-tx-methods.ts b/src/examples/chaining-tx-methods.ts index 4692649b55..948434e528 100644 --- a/src/examples/chaining-tx-methods.ts +++ b/src/examples/chaining-tx-methods.ts @@ -40,8 +40,6 @@ let initialBalance = 10_000_000_000; let zkapp = new SimpleZkapp(zkappAddress); await SimpleZkapp.analyzeMethods(); -console.log('deploy'); - await Mina.transaction(sender, async () => { let senderUpdate = AccountUpdate.fundNewAccount(sender); senderUpdate.send({ to: zkappAddress, amount: initialBalance }); From abdf47d52387e0d7cf5b10e59aaa9e7f4427a211 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 08:20:40 -0400 Subject: [PATCH 03/23] improve TransactionPromise typing --- src/examples/chaining-tx-methods.ts | 2 +- src/lib/mina/transaction.ts | 45 +++++++++++++++++------------ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/examples/chaining-tx-methods.ts b/src/examples/chaining-tx-methods.ts index 948434e528..87bfe0dec6 100644 --- a/src/examples/chaining-tx-methods.ts +++ b/src/examples/chaining-tx-methods.ts @@ -53,7 +53,7 @@ console.log('initial state: ' + zkapp.x.get()); console.log('increment'); -await Mina.transaction(sender, async () => { +const x = Mina.transaction(sender, async () => { await zkapp.increment(); }) .sign([senderKey]) diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index d6c92b025d..fa82a460a2 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -284,24 +284,31 @@ type RejectedTransaction = Pick< * A `Promise` with some additional methods for making chained method calls * into the pending value upon its resolution. */ -type TransactionPromise = Promise & { - /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ - sign(...args: Parameters): TransactionPromise; - /** Equivalent to calling the resolved `Transaction`'s `send` method. */ - send(): PendingTransactionPromise; - /** - * 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. - */ - proofPromise?: Promise<(Proof | undefined)[]>; -}; +type TransactionPromise = + Promise & { + /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ + sign(...args: Parameters): TransactionPromise; + /** 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. + */ + proofPromise: Promise< + (Proof | undefined)[] + >; + }); function toTransactionPromise( getPromise: () => Promise, @@ -323,7 +330,7 @@ function toTransactionPromise( }, proofPromise_); }, proofPromise, - }); + }) as never as TransactionPromise; } /** From ed4d448c5f7b0dc9eb686481f12c78be49cf027f Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 08:21:18 -0400 Subject: [PATCH 04/23] revert example change --- src/examples/chaining-tx-methods.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/examples/chaining-tx-methods.ts b/src/examples/chaining-tx-methods.ts index 87bfe0dec6..948434e528 100644 --- a/src/examples/chaining-tx-methods.ts +++ b/src/examples/chaining-tx-methods.ts @@ -53,7 +53,7 @@ console.log('initial state: ' + zkapp.x.get()); console.log('increment'); -const x = Mina.transaction(sender, async () => { +await Mina.transaction(sender, async () => { await zkapp.increment(); }) .sign([senderKey]) From e8a23dad99ff443db1da089e1a1635383ab61498 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 10:11:02 -0400 Subject: [PATCH 05/23] typing mania --- .../zkapps/simple-zkapp-with-proof.ts | 47 +-- src/lib/mina/local-blockchain.ts | 316 +++++++++--------- src/lib/mina/mina-instance.ts | 7 +- src/lib/mina/mina.ts | 198 +++++------ src/lib/mina/transaction.ts | 141 ++++---- src/mina-signer/tests/zkapp.unit-test.ts | 4 +- 6 files changed, 373 insertions(+), 340 deletions(-) diff --git a/src/examples/zkapps/simple-zkapp-with-proof.ts b/src/examples/zkapps/simple-zkapp-with-proof.ts index aa098a19ce..f8778f3381 100644 --- a/src/examples/zkapps/simple-zkapp-with-proof.ts +++ b/src/examples/zkapps/simple-zkapp-with-proof.ts @@ -63,11 +63,9 @@ let { verificationKey: trivialVerificationKey } = await TrivialZkapp.compile(); // submitting transactions? or is this an irrelevant use case? // would also improve the return type -- `Proof` instead of `(Proof | undefined)[]` console.log('prove (trivial zkapp)'); -let [trivialProof] = await ( - await Mina.transaction(feePayer, async () => { - await new TrivialZkapp(zkappAddress2).proveSomething(Field(1)); - }) -).prove(); +let [trivialProof] = await Mina.transaction(feePayer, async () => { + await new TrivialZkapp(zkappAddress2).proveSomething(Field(1)); +}).prove().proof; trivialProof = await testJsonRoundtripAndVerify( TrivialProof, @@ -81,19 +79,22 @@ let { verificationKey } = await NotSoSimpleZkapp.compile(); let zkapp = new NotSoSimpleZkapp(zkappAddress); console.log('deploy'); -let tx = await Mina.transaction(feePayer, async () => { +await Mina.transaction(feePayer, async () => { AccountUpdate.fundNewAccount(feePayer); await zkapp.deploy(); -}); -await tx.prove(); -await tx.sign([feePayerKey, zkappKey]).send(); +}) + .prove() + .sign([feePayerKey, zkappKey]) + .send(); console.log('initialize'); -tx = await Mina.transaction(feePayer, async () => { +let txA = Mina.transaction(feePayer, async () => { await zkapp.initialize(trivialProof!); -}); -let [proof] = await tx.prove(); -await tx.sign([feePayerKey]).send(); +}) + .prove() + .sign([feePayerKey]); +let [proof] = await txA.proof; +await txA.send(); proof = await testJsonRoundtripAndVerify( NotSoSimpleZkapp.Proof(), @@ -104,11 +105,13 @@ proof = await testJsonRoundtripAndVerify( console.log('initial state: ' + zkapp.x.get()); console.log('update'); -tx = await Mina.transaction(feePayer, async () => { +let txB = Mina.transaction(feePayer, async () => { await zkapp.update(Field(3), proof!, trivialProof!); -}); -[proof] = await tx.prove(); -await tx.sign([feePayerKey]).send(); +}) + .prove() + .sign([feePayerKey]); +[proof] = await txB.proof; +await txB.send(); proof = await testJsonRoundtripAndVerify( NotSoSimpleZkapp.Proof(), @@ -119,11 +122,13 @@ proof = await testJsonRoundtripAndVerify( console.log('state 2: ' + zkapp.x.get()); console.log('update'); -tx = await Mina.transaction(feePayer, async () => { +let txC = Mina.transaction(feePayer, async () => { await zkapp.update(Field(3), proof!, trivialProof!); -}); -[proof] = await tx.prove(); -await tx.sign([feePayerKey]).send(); +}) + .prove() + .sign([feePayerKey]); +[proof] = await txC.proof; +await txC.send(); proof = await testJsonRoundtripAndVerify( NotSoSimpleZkapp.Proof(), diff --git a/src/lib/mina/local-blockchain.ts b/src/lib/mina/local-blockchain.ts index 0d600332b5..6a5d00bcb8 100644 --- a/src/lib/mina/local-blockchain.ts +++ b/src/lib/mina/local-blockchain.ts @@ -25,6 +25,8 @@ import { IncludedTransaction, RejectedTransaction, PendingTransactionStatus, + PendingTransactionPromise, + toPendingTransactionPromise, } from './transaction.js'; import { type FeePayerSpec, @@ -117,147 +119,150 @@ function LocalBlockchain({ getNetworkState() { return networkState; }, - async sendTransaction(txn: Transaction): Promise { - let zkappCommandJson = ZkappCommand.toJSON(txn.transaction); - let commitments = transactionCommitments( - TypesBigint.ZkappCommand.fromJSON(zkappCommandJson), - minaNetworkId - ); + sendTransaction(txn: Transaction): PendingTransactionPromise { + return toPendingTransactionPromise(async () => { + let zkappCommandJson = ZkappCommand.toJSON(txn.transaction); + let commitments = transactionCommitments( + TypesBigint.ZkappCommand.fromJSON(zkappCommandJson), + minaNetworkId + ); - if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction); + if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction); - // create an ad-hoc ledger to record changes to accounts within the transaction - let simpleLedger = SimpleLedger.create(); + // create an ad-hoc ledger to record changes to accounts within the transaction + let simpleLedger = SimpleLedger.create(); - for (const update of txn.transaction.accountUpdates) { - let authIsProof = !!update.authorization.proof; - let kindIsProof = update.body.authorizationKind.isProved.toBoolean(); - // checks and edge case where a proof is expected, but the developer forgot to invoke await tx.prove() - // this resulted in an assertion OCaml error, which didn't contain any useful information - if (kindIsProof && !authIsProof) { - throw Error( - `The actual authorization does not match the expected authorization kind. Did you forget to invoke \`await tx.prove();\`?` - ); - } + for (const update of txn.transaction.accountUpdates) { + let authIsProof = !!update.authorization.proof; + let kindIsProof = update.body.authorizationKind.isProved.toBoolean(); + // checks and edge case where a proof is expected, but the developer forgot to invoke await tx.prove() + // this resulted in an assertion OCaml error, which didn't contain any useful information + if (kindIsProof && !authIsProof) { + throw Error( + `The actual authorization does not match the expected authorization kind. Did you forget to invoke \`await tx.prove();\`?` + ); + } - let account = simpleLedger.load(update.body); + let account = simpleLedger.load(update.body); - // the first time we encounter an account, use it from the persistent ledger - if (account === undefined) { - let accountJson = ledger.getAccount( - Ml.fromPublicKey(update.body.publicKey), - Ml.constFromField(update.body.tokenId) - ); - if (accountJson !== undefined) { - let storedAccount = Account.fromJSON(accountJson); - simpleLedger.store(storedAccount); - account = storedAccount; + // the first time we encounter an account, use it from the persistent ledger + if (account === undefined) { + let accountJson = ledger.getAccount( + Ml.fromPublicKey(update.body.publicKey), + Ml.constFromField(update.body.tokenId) + ); + if (accountJson !== undefined) { + let storedAccount = Account.fromJSON(accountJson); + simpleLedger.store(storedAccount); + account = storedAccount; + } } - } - // TODO: verify account update even if the account doesn't exist yet, using a default initial account - if (account !== undefined) { - let publicInput = update.toPublicInput(txn.transaction); - await verifyAccountUpdate( - account, - update, - publicInput, - commitments, - this.proofsEnabled, - this.getNetworkId() - ); - simpleLedger.apply(update); + // TODO: verify account update even if the account doesn't exist yet, using a default initial account + if (account !== undefined) { + let publicInput = update.toPublicInput(txn.transaction); + await verifyAccountUpdate( + account, + update, + publicInput, + commitments, + this.proofsEnabled, + this.getNetworkId() + ); + simpleLedger.apply(update); + } } - } - let status: PendingTransactionStatus = 'pending'; - const errors: string[] = []; - try { - ledger.applyJsonTransaction( - JSON.stringify(zkappCommandJson), - defaultNetworkConstants.accountCreationFee.toString(), - JSON.stringify(networkState) - ); - } catch (err: any) { - status = 'rejected'; + let status: PendingTransactionStatus = 'pending'; + const errors: string[] = []; try { - const errorMessages = JSON.parse(err.message); - const formattedError = invalidTransactionError( - txn.transaction, - errorMessages, - { - accountCreationFee: - defaultNetworkConstants.accountCreationFee.toString(), - } + ledger.applyJsonTransaction( + JSON.stringify(zkappCommandJson), + defaultNetworkConstants.accountCreationFee.toString(), + JSON.stringify(networkState) ); - errors.push(formattedError); - } catch (parseError: any) { - const fallbackErrorMessage = - err.message || parseError.message || 'Unknown error occurred'; - errors.push(fallbackErrorMessage); + } catch (err: any) { + status = 'rejected'; + try { + const errorMessages = JSON.parse(err.message); + const formattedError = invalidTransactionError( + txn.transaction, + errorMessages, + { + accountCreationFee: + defaultNetworkConstants.accountCreationFee.toString(), + } + ); + errors.push(formattedError); + } catch (parseError: any) { + const fallbackErrorMessage = + err.message || parseError.message || 'Unknown error occurred'; + errors.push(fallbackErrorMessage); + } } - } - // fetches all events from the transaction and stores them - // events are identified and associated with a publicKey and tokenId - txn.transaction.accountUpdates.forEach((p, i) => { - let pJson = zkappCommandJson.accountUpdates[i]; - let addr = pJson.body.publicKey; - let tokenId = pJson.body.tokenId; - events[addr] ??= {}; - if (p.body.events.data.length > 0) { - events[addr][tokenId] ??= []; - let updatedEvents = p.body.events.data.map((data) => { - return { - data, - transactionInfo: { - transactionHash: '', - transactionStatus: '', - transactionMemo: '', - }, - }; - }); - events[addr][tokenId].push({ - events: updatedEvents, - blockHeight: networkState.blockchainLength, - globalSlot: networkState.globalSlotSinceGenesis, - // The following fields are fetched from the Mina network. For now, we mock these values out - // since networkState does not contain these fields. - blockHash: '', - parentBlockHash: '', - chainStatus: '', - }); - } + // fetches all events from the transaction and stores them + // events are identified and associated with a publicKey and tokenId + txn.transaction.accountUpdates.forEach((p, i) => { + let pJson = zkappCommandJson.accountUpdates[i]; + let addr = pJson.body.publicKey; + let tokenId = pJson.body.tokenId; + events[addr] ??= {}; + if (p.body.events.data.length > 0) { + events[addr][tokenId] ??= []; + let updatedEvents = p.body.events.data.map((data) => { + return { + data, + transactionInfo: { + transactionHash: '', + transactionStatus: '', + transactionMemo: '', + }, + }; + }); + events[addr][tokenId].push({ + events: updatedEvents, + blockHeight: networkState.blockchainLength, + globalSlot: networkState.globalSlotSinceGenesis, + // The following fields are fetched from the Mina network. For now, we mock these values out + // since networkState does not contain these fields. + blockHash: '', + parentBlockHash: '', + chainStatus: '', + }); + } - // actions/sequencing events + // actions/sequencing events - // most recent action state - let storedActions = actions[addr]?.[tokenId]; - let latestActionState_ = - storedActions?.[storedActions.length - 1]?.hash; - // if there exists no hash, this means we initialize our latest hash with the empty state - let latestActionState = - latestActionState_ !== undefined - ? Field(latestActionState_) - : Actions.emptyActionState(); + // most recent action state + let storedActions = actions[addr]?.[tokenId]; + let latestActionState_ = + storedActions?.[storedActions.length - 1]?.hash; + // if there exists no hash, this means we initialize our latest hash with the empty state + let latestActionState = + latestActionState_ !== undefined + ? Field(latestActionState_) + : Actions.emptyActionState(); - actions[addr] ??= {}; - if (p.body.actions.data.length > 0) { - let newActionState = Actions.updateSequenceState( - latestActionState, - p.body.actions.hash - ); - actions[addr][tokenId] ??= []; - actions[addr][tokenId].push({ - actions: pJson.body.actions, - hash: newActionState.toString(), - }); - } - }); + actions[addr] ??= {}; + if (p.body.actions.data.length > 0) { + let newActionState = Actions.updateSequenceState( + latestActionState, + p.body.actions.hash + ); + actions[addr][tokenId] ??= []; + actions[addr][tokenId].push({ + actions: pJson.body.actions, + hash: newActionState.toString(), + }); + } + }); - const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); - const pendingTransaction: Omit = - { + const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); + const pendingTransaction: Omit< + PendingTransaction, + 'wait' | 'safeWait' + > = { status, errors, transaction: txn.transaction, @@ -266,39 +271,40 @@ function LocalBlockchain({ toPretty: txn.toPretty, }; - const wait = async (_options?: { - maxAttempts?: number; - interval?: number; - }): Promise => { - const pendingTransaction = await safeWait(_options); - if (pendingTransaction.status === 'rejected') { - throw Error( - `Transaction failed with errors:\n${pendingTransaction.errors.join( - '\n' - )}` - ); - } - return pendingTransaction; - }; + const wait = async (_options?: { + maxAttempts?: number; + interval?: number; + }): Promise => { + 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 => { - if (status === 'rejected') { - return createRejectedTransaction( - pendingTransaction, - pendingTransaction.errors - ); - } - return createIncludedTransaction(pendingTransaction); - }; + const safeWait = async (_options?: { + maxAttempts?: number; + interval?: number; + }): Promise => { + if (status === 'rejected') { + return createRejectedTransaction( + pendingTransaction, + pendingTransaction.errors + ); + } + return createIncludedTransaction(pendingTransaction); + }; - return { - ...pendingTransaction, - wait, - safeWait, - }; + return { + ...pendingTransaction, + wait, + safeWait, + }; + }); }, transaction(sender: FeePayerSpec, f: () => Promise) { return toTransactionPromise(async () => { diff --git a/src/lib/mina/mina-instance.ts b/src/lib/mina/mina-instance.ts index 70b82a90a4..7b06877b70 100644 --- a/src/lib/mina/mina-instance.ts +++ b/src/lib/mina/mina-instance.ts @@ -71,13 +71,16 @@ type NetworkConstants = { }; type Mina = { - transaction(sender: FeePayerSpec, f: () => Promise): TransactionPromise; + transaction( + sender: FeePayerSpec, + f: () => Promise + ): TransactionPromise; currentSlot(): UInt32; hasAccount(publicKey: PublicKey, tokenId?: Field): boolean; getAccount(publicKey: PublicKey, tokenId?: Field): Account; getNetworkState(): NetworkValue; getNetworkConstants(): NetworkConstants; - sendTransaction(transaction: Transaction): Promise; + sendTransaction(transaction: Transaction): Promise; fetchEvents: ( publicKey: PublicKey, tokenId?: Field, diff --git a/src/lib/mina/mina.ts b/src/lib/mina/mina.ts index 285bf7e181..1dd36eb7c1 100644 --- a/src/lib/mina/mina.ts +++ b/src/lib/mina/mina.ts @@ -43,6 +43,7 @@ import { transaction, createRejectedTransaction, createIncludedTransaction, + toPendingTransactionPromise, } from './transaction.js'; import { reportGetAccountError, @@ -56,7 +57,7 @@ export { LocalBlockchain, Network, currentTransaction, - Transaction, + type Transaction, type PendingTransaction, type IncludedTransaction, type RejectedTransaction, @@ -84,6 +85,7 @@ export { // for internal testing only filterGroups, type NetworkConstants, + TransactionUtil, }; // patch active instance so that we can still create basic transactions without giving Mina network details @@ -94,8 +96,8 @@ setActiveInstance({ }, }); -const Transaction = { - fromJSON(json: Types.Json.ZkappCommand): Transaction { +const TransactionUtil = { + fromJSON(json: Types.Json.ZkappCommand): Transaction { let transaction = ZkappCommand.fromJSON(json); return newTransaction(transaction, activeInstance.proofsEnabled); }, @@ -255,22 +257,25 @@ function Network( `getNetworkState: Could not fetch network state from graphql endpoint ${minaGraphqlEndpoint} outside of a transaction.` ); }, - async sendTransaction(txn: Transaction): Promise { - verifyTransactionLimits(txn.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))); - } + 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'; - const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); - const pendingTransaction: Omit = - { + const status: PendingTransactionStatus = + errors.length === 0 ? 'pending' : 'rejected'; + const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); + const pendingTransaction: Omit< + PendingTransaction, + 'wait' | 'safeWait' + > = { status, data: response?.data, errors, @@ -280,92 +285,93 @@ function Network( toPretty: txn.toPretty, }; - const pollTransactionStatus = async ( - transactionHash: string, - maxAttempts: number, - interval: number, - attempts: number = 0 - ): Promise => { - let res: Awaited>; - 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]); + const pollTransactionStatus = async ( + transactionHash: string, + maxAttempts: number, + interval: number, + attempts: number = 0 + ): Promise => { + let res: Awaited>; + 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, + ]); } - } 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}`, - ]); - } + 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 - ); - }; + 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 => { - return pollTransactionStatus(hash, maxAttempts, interval); - }; + // 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 => { + return pollTransactionStatus(hash, maxAttempts, interval); + }; - const wait = async (options?: { - maxAttempts?: number; - interval?: number; - }): Promise => { - const pendingTransaction = await safeWait(options); - if (pendingTransaction.status === 'rejected') { - throw Error( - `Transaction failed with errors:\n${pendingTransaction.errors.join( - '\n' - )}` - ); - } - return pendingTransaction; - }; + const wait = async (options?: { + maxAttempts?: number; + interval?: number; + }): Promise => { + 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 => { - if (status === 'rejected') { - return createRejectedTransaction( - pendingTransaction, - pendingTransaction.errors - ); - } - return await poll(options?.maxAttempts, options?.interval); - }; + const safeWait = async (options?: { + maxAttempts?: number; + interval?: number; + }): Promise => { + if (status === 'rejected') { + return createRejectedTransaction( + pendingTransaction, + pendingTransaction.errors + ); + } + return await poll(options?.maxAttempts, options?.interval); + }; - return { - ...pendingTransaction, - wait, - safeWait, - }; + return { + ...pendingTransaction, + wait, + safeWait, + }; + }); }, transaction(sender: FeePayerSpec, f: () => Promise) { return toTransactionPromise(async () => { diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index fa82a460a2..7a517a06d9 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -46,7 +46,7 @@ export { * This type encompasses methods for serializing the transaction, signing it, generating proofs, * and submitting it to the network. */ -type Transaction = { +type Transaction = { /** * Transaction structure used to describe a state transition on the Mina blockchain. */ @@ -77,17 +77,7 @@ type Transaction = { * console.log('Transaction signed successfully.'); * ``` */ - sign(privateKeys: PrivateKey[]): Transaction; - /** - * 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<(Proof | undefined)[]>; + sign(privateKeys: PrivateKey[]): Transaction; /** * 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. @@ -119,7 +109,23 @@ type Transaction = { * ``` */ safeSend(): Promise; -}; +} & (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`. */ + proof: (Proof | undefined)[]; + }); type PendingTransactionStatus = 'pending' | 'rejected'; /** @@ -128,7 +134,7 @@ type PendingTransactionStatus = 'pending' | 'rejected'; * adding methods to monitor the transaction's progress towards being finalized (either included in a block or rejected). */ type PendingTransaction = Pick< - Transaction, + Transaction, 'transaction' | 'toJSON' | 'toPretty' > & { /** @@ -284,53 +290,51 @@ type RejectedTransaction = Pick< * A `Promise` with some additional methods for making chained method calls * into the pending value upon its resolution. */ -type TransactionPromise = - Promise & { - /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ - sign(...args: Parameters): TransactionPromise; - /** 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. - */ - proofPromise: Promise< - (Proof | undefined)[] - >; - }); +type TransactionPromise = Promise< + Transaction +> & { + /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ + sign( + ...args: Parameters['sign']> + ): TransactionPromise; + /** 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. + */ + proof: Promise['proof']>; + }); -function toTransactionPromise( - getPromise: () => Promise, - proofPromise?: Promise<(Proof | undefined)[]> -): TransactionPromise { +function toTransactionPromise( + getPromise: () => Promise> +): TransactionPromise { const pending = getPromise().then(); return Object.assign(pending, { - sign(...args: Parameters) { + sign(...args: Parameters['sign']>) { return toTransactionPromise(() => pending.then((v) => v.sign(...args))); }, send() { return toPendingTransactionPromise(() => pending.then((v) => v.send())); }, prove() { - const proofPromise_ = proofPromise ?? pending.then((v) => v.prove()); - return toTransactionPromise(async () => { - await proofPromise_; - return await pending; - }, proofPromise_); + return toTransactionPromise(() => + pending.then((v) => (v as never as Transaction).prove()) + ); }, - proofPromise, - }) as never as TransactionPromise; + proofs: pending.then((v) => (v as never as Transaction).proof), + }) as never as TransactionPromise; } /** @@ -362,7 +366,7 @@ async function createTransaction( isFinalRunOutsideCircuit = true, proofsEnabled = true, } = {} -): Promise { +): Promise> { if (currentTransaction.has()) { throw new Error('Cannot start new transaction within another transaction'); } @@ -455,18 +459,23 @@ async function createTransaction( } function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { - let self: Transaction = { + let self: Transaction = { transaction, sign(privateKeys: PrivateKey[]) { self.transaction = addMissingSignatures(self.transaction, privateKeys); return self; }, - async prove() { - let { zkappCommand, proofs } = await addMissingProofs(self.transaction, { - proofsEnabled, + prove() { + return toTransactionPromise(async () => { + let { zkappCommand, proofs } = await addMissingProofs( + self.transaction, + { + proofsEnabled, + } + ); + self.transaction = zkappCommand; + return Object.assign(self as never as Transaction, { proofs }); }); - self.transaction = zkappCommand; - return proofs; }, toJSON() { let json = ZkappCommand.toJSON(self.transaction); @@ -480,7 +489,9 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { }, send() { return toPendingTransactionPromise(async () => { - const pendingTransaction = await sendTransaction(self); + const pendingTransaction = await sendTransaction( + self as never as Transaction + ); if (pendingTransaction.errors.length > 0) { throw Error( `Transaction failed with errors:\n- ${pendingTransaction.errors.join( @@ -492,7 +503,9 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { }); }, async safeSend() { - const pendingTransaction = await sendTransaction(self); + const pendingTransaction = await sendTransaction( + self as never as Transaction + ); if (pendingTransaction.errors.length > 0) { return createRejectedTransaction( pendingTransaction, @@ -521,12 +534,12 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { function transaction( sender: FeePayerSpec, f: () => Promise -): TransactionPromise; -function transaction(f: () => Promise): TransactionPromise; +): TransactionPromise; +function transaction(f: () => Promise): TransactionPromise; function transaction( senderOrF: FeePayerSpec | (() => Promise), fOrUndefined?: () => Promise -): TransactionPromise { +): TransactionPromise { let sender: FeePayerSpec; let f: () => Promise; if (fOrUndefined !== undefined) { @@ -539,7 +552,7 @@ function transaction( return activeInstance.transaction(sender, f); } -async function sendTransaction(txn: Transaction) { +async function sendTransaction(txn: Transaction) { return await activeInstance.sendTransaction(txn); } diff --git a/src/mina-signer/tests/zkapp.unit-test.ts b/src/mina-signer/tests/zkapp.unit-test.ts index 1b3ffb66c3..e7d2bac1b5 100644 --- a/src/mina-signer/tests/zkapp.unit-test.ts +++ b/src/mina-signer/tests/zkapp.unit-test.ts @@ -3,7 +3,7 @@ import * as TransactionJson from '../../bindings/mina-transaction/gen/transactio import Client from '../mina-signer.js'; import { accountUpdateExample } from '../src/test-vectors/accountUpdate.js'; import { expect } from 'expect'; -import { Transaction } from '../../lib/mina/mina.js'; +import { TransactionUtil } from '../../lib/mina/mina.js'; import { PrivateKey } from '../../lib/provable/crypto/signature.js'; import { Signature } from '../src/signature.js'; import { mocks } from '../../bindings/crypto/constants.js'; @@ -100,7 +100,7 @@ let transactionJson = { memo: zkappCommand.data.zkappCommand.memo, }; -let tx = Transaction.fromJSON(transactionJson); +let tx = TransactionUtil.fromJSON(transactionJson); tx.transaction.feePayer.lazyAuthorization = { kind: 'lazy-signature' }; tx.transaction.accountUpdates[1].lazyAuthorization = { kind: 'lazy-signature' }; tx.sign([PrivateKey.fromBase58(privateKey)]); From d718c2878799cf7540a4248e111988a9601a8c38 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 10:15:47 -0400 Subject: [PATCH 06/23] remove default type param and clean up example --- src/examples/zkapps/simple-zkapp-with-proof.ts | 18 +++++++++--------- src/lib/mina/transaction.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/examples/zkapps/simple-zkapp-with-proof.ts b/src/examples/zkapps/simple-zkapp-with-proof.ts index f8778f3381..40d50a909d 100644 --- a/src/examples/zkapps/simple-zkapp-with-proof.ts +++ b/src/examples/zkapps/simple-zkapp-with-proof.ts @@ -88,13 +88,13 @@ await Mina.transaction(feePayer, async () => { .send(); console.log('initialize'); -let txA = Mina.transaction(feePayer, async () => { +let tx = Mina.transaction(feePayer, async () => { await zkapp.initialize(trivialProof!); }) .prove() .sign([feePayerKey]); -let [proof] = await txA.proof; -await txA.send(); +let [proof] = await tx.proof; +await tx.send(); proof = await testJsonRoundtripAndVerify( NotSoSimpleZkapp.Proof(), @@ -105,13 +105,13 @@ proof = await testJsonRoundtripAndVerify( console.log('initial state: ' + zkapp.x.get()); console.log('update'); -let txB = Mina.transaction(feePayer, async () => { +tx = Mina.transaction(feePayer, async () => { await zkapp.update(Field(3), proof!, trivialProof!); }) .prove() .sign([feePayerKey]); -[proof] = await txB.proof; -await txB.send(); +[proof] = await tx.proof; +await tx.send(); proof = await testJsonRoundtripAndVerify( NotSoSimpleZkapp.Proof(), @@ -122,13 +122,13 @@ proof = await testJsonRoundtripAndVerify( console.log('state 2: ' + zkapp.x.get()); console.log('update'); -let txC = Mina.transaction(feePayer, async () => { +tx = Mina.transaction(feePayer, async () => { await zkapp.update(Field(3), proof!, trivialProof!); }) .prove() .sign([feePayerKey]); -[proof] = await txC.proof; -await txC.send(); +[proof] = await tx.proof; +await tx.send(); proof = await testJsonRoundtripAndVerify( NotSoSimpleZkapp.Proof(), diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 7a517a06d9..709f015476 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -46,7 +46,7 @@ export { * This type encompasses methods for serializing the transaction, signing it, generating proofs, * and submitting it to the network. */ -type Transaction = { +type Transaction = { /** * Transaction structure used to describe a state transition on the Mina blockchain. */ From a10a4c488d2e25908914a8877a0e25aa7c1accf5 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 10:57:56 -0400 Subject: [PATCH 07/23] add signed type to Transaction and TransactionPromise --- src/examples/chaining-tx-methods.ts | 9 ++ src/lib/mina/local-blockchain.ts | 4 +- src/lib/mina/mina-instance.ts | 6 +- src/lib/mina/mina.ts | 2 +- src/lib/mina/transaction.ts | 151 ++++++++++++++++------------ 5 files changed, 101 insertions(+), 71 deletions(-) diff --git a/src/examples/chaining-tx-methods.ts b/src/examples/chaining-tx-methods.ts index 948434e528..2cafc57056 100644 --- a/src/examples/chaining-tx-methods.ts +++ b/src/examples/chaining-tx-methods.ts @@ -7,6 +7,7 @@ import { SmartContract, Mina, AccountUpdate, + TransactionPromise, } from 'o1js'; class SimpleZkapp extends SmartContract { @@ -62,3 +63,11 @@ await Mina.transaction(sender, async () => { .wait(); console.log('final state: ' + zkapp.x.get()); + +const a = Mina.transaction(sender, async () => { + await zkapp.increment(); +}); +a satisfies TransactionPromise; +const b = a.prove() satisfies TransactionPromise; +const c = b.sign([senderKey]) satisfies TransactionPromise; +await c.send().wait(); diff --git a/src/lib/mina/local-blockchain.ts b/src/lib/mina/local-blockchain.ts index 6a5d00bcb8..fbb5edc27a 100644 --- a/src/lib/mina/local-blockchain.ts +++ b/src/lib/mina/local-blockchain.ts @@ -119,7 +119,9 @@ function LocalBlockchain({ getNetworkState() { return networkState; }, - sendTransaction(txn: Transaction): PendingTransactionPromise { + sendTransaction( + txn: Transaction + ): PendingTransactionPromise { return toPendingTransactionPromise(async () => { let zkappCommandJson = ZkappCommand.toJSON(txn.transaction); let commitments = transactionCommitments( diff --git a/src/lib/mina/mina-instance.ts b/src/lib/mina/mina-instance.ts index 7b06877b70..94d437c911 100644 --- a/src/lib/mina/mina-instance.ts +++ b/src/lib/mina/mina-instance.ts @@ -74,13 +74,15 @@ type Mina = { transaction( sender: FeePayerSpec, f: () => Promise - ): TransactionPromise; + ): TransactionPromise; currentSlot(): UInt32; hasAccount(publicKey: PublicKey, tokenId?: Field): boolean; getAccount(publicKey: PublicKey, tokenId?: Field): Account; getNetworkState(): NetworkValue; getNetworkConstants(): NetworkConstants; - sendTransaction(transaction: Transaction): Promise; + sendTransaction( + transaction: Transaction + ): Promise; fetchEvents: ( publicKey: PublicKey, tokenId?: Field, diff --git a/src/lib/mina/mina.ts b/src/lib/mina/mina.ts index 1dd36eb7c1..ecc53a1878 100644 --- a/src/lib/mina/mina.ts +++ b/src/lib/mina/mina.ts @@ -97,7 +97,7 @@ setActiveInstance({ }); const TransactionUtil = { - fromJSON(json: Types.Json.ZkappCommand): Transaction { + fromJSON(json: Types.Json.ZkappCommand): Transaction { let transaction = ZkappCommand.fromJSON(json); return newTransaction(transaction, activeInstance.proofsEnabled); }, diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 709f015476..49be132796 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -41,12 +41,7 @@ export { createIncludedTransaction, }; -/** - * 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 = { +type TransactionCommon = { /** * Transaction structure used to describe a state transition on the Mina blockchain. */ @@ -66,18 +61,6 @@ type Transaction = { * @returns The GraphQL query string for the {@link Transaction}. */ toGraphqlQuery(): string; - /** - * 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; /** * 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. @@ -94,6 +77,17 @@ type Transaction = { * } * ``` */ +}; + +/** + * 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< + Proven extends boolean, + Signed extends boolean +> = 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. @@ -110,22 +104,38 @@ type Transaction = { */ safeSend(): Promise; } & (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`. */ - proof: (Proof | undefined)[]; - }); + ? { + /** + * 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`. */ + proof: (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'; /** @@ -134,7 +144,7 @@ type PendingTransactionStatus = 'pending' | 'rejected'; * adding methods to monitor the transaction's progress towards being finalized (either included in a block or rejected). */ type PendingTransaction = Pick< - Transaction, + TransactionCommon, 'transaction' | 'toJSON' | 'toPretty' > & { /** @@ -290,13 +300,10 @@ type RejectedTransaction = Pick< * 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 `sign` method. */ - sign( - ...args: Parameters['sign']> - ): TransactionPromise; +type TransactionPromise< + Proven extends boolean, + Signed extends boolean +> = Promise> & { /** Equivalent to calling the resolved `Transaction`'s `send` method. */ send(): PendingTransactionPromise; } & (Proven extends false @@ -306,7 +313,7 @@ type TransactionPromise = Promise< * new `TransactionPromise` with the field `proofPromise` containing * a promise which resolves to the proof array. */ - prove(): TransactionPromise; + prove(): TransactionPromise; } : { /** @@ -314,27 +321,39 @@ type TransactionPromise = Promise< * contains a `prove` call, then this field contains a promise resolving to the * proof array which was output from the underlying `prove` call. */ - proof: Promise['proof']>; - }); + proof: Promise['proof']>; + }) & + (Signed extends false + ? { + /** Equivalent to calling the resolved `Transaction`'s `sign` method. */ + sign( + ...args: Parameters['sign']> + ): TransactionPromise; + } + : {}); -function toTransactionPromise( - getPromise: () => Promise> -): TransactionPromise { +function toTransactionPromise( + getPromise: () => Promise> +): TransactionPromise { const pending = getPromise().then(); return Object.assign(pending, { - sign(...args: Parameters['sign']>) { - return toTransactionPromise(() => pending.then((v) => v.sign(...args))); + 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()) + pending.then((v) => (v as never as Transaction).prove()) ); }, - proofs: pending.then((v) => (v as never as Transaction).proof), - }) as never as TransactionPromise; + proofs: pending.then( + (v) => (v as never as Transaction).proof + ), + }) as never as TransactionPromise; } /** @@ -366,7 +385,7 @@ async function createTransaction( isFinalRunOutsideCircuit = true, proofsEnabled = true, } = {} -): Promise> { +): Promise> { if (currentTransaction.has()) { throw new Error('Cannot start new transaction within another transaction'); } @@ -459,14 +478,14 @@ async function createTransaction( } function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { - let self: Transaction = { + let self: Transaction = { transaction, sign(privateKeys: PrivateKey[]) { self.transaction = addMissingSignatures(self.transaction, privateKeys); return self; }, prove() { - return toTransactionPromise(async () => { + return toTransactionPromise(async () => { let { zkappCommand, proofs } = await addMissingProofs( self.transaction, { @@ -474,7 +493,9 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { } ); self.transaction = zkappCommand; - return Object.assign(self as never as Transaction, { proofs }); + return Object.assign(self as never as Transaction, { + proofs, + }); }); }, toJSON() { @@ -489,9 +510,7 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { }, send() { return toPendingTransactionPromise(async () => { - const pendingTransaction = await sendTransaction( - self as never as Transaction - ); + const pendingTransaction = await sendTransaction(self); if (pendingTransaction.errors.length > 0) { throw Error( `Transaction failed with errors:\n- ${pendingTransaction.errors.join( @@ -503,9 +522,7 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { }); }, async safeSend() { - const pendingTransaction = await sendTransaction( - self as never as Transaction - ); + const pendingTransaction = await sendTransaction(self); if (pendingTransaction.errors.length > 0) { return createRejectedTransaction( pendingTransaction, @@ -534,12 +551,12 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { function transaction( sender: FeePayerSpec, f: () => Promise -): TransactionPromise; -function transaction(f: () => Promise): TransactionPromise; +): TransactionPromise; +function transaction(f: () => Promise): TransactionPromise; function transaction( senderOrF: FeePayerSpec | (() => Promise), fOrUndefined?: () => Promise -): TransactionPromise { +): TransactionPromise { let sender: FeePayerSpec; let f: () => Promise; if (fOrUndefined !== undefined) { @@ -552,7 +569,7 @@ function transaction( return activeInstance.transaction(sender, f); } -async function sendTransaction(txn: Transaction) { +async function sendTransaction(txn: Transaction) { return await activeInstance.sendTransaction(txn); } From f3655216dd1ad6b38e7179e146fd6dcf82b548f4 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 11:02:34 -0400 Subject: [PATCH 08/23] fix signature of sendTransaction in mina-instance --- src/lib/mina/mina-instance.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib/mina/mina-instance.ts b/src/lib/mina/mina-instance.ts index 94d437c911..77bc328ba9 100644 --- a/src/lib/mina/mina-instance.ts +++ b/src/lib/mina/mina-instance.ts @@ -6,11 +6,14 @@ import { UInt64, UInt32 } from '../provable/int.js'; import { PublicKey } from '../provable/crypto/signature.js'; import type { EventActionFilterOptions } from '././../mina/graphql.js'; import type { NetworkId } from '../../mina-signer/src/types.js'; -import type { Transaction, PendingTransaction } from './mina.js'; import type { Account } from './account.js'; import type { NetworkValue } from './precondition.js'; import type * as Fetch from './fetch.js'; -import type { TransactionPromise } from './transaction.js'; +import type { + TransactionPromise, + PendingTransactionPromise, + Transaction, +} from './transaction.js'; export { Mina, @@ -82,7 +85,7 @@ type Mina = { getNetworkConstants(): NetworkConstants; sendTransaction( transaction: Transaction - ): Promise; + ): PendingTransactionPromise; fetchEvents: ( publicKey: PublicKey, tokenId?: Field, From 38e08747c6ef8203a742ebf024d8bb79c79ba2ef Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 11:06:14 -0400 Subject: [PATCH 09/23] add todo comment --- src/lib/mina/transaction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 49be132796..6326e16a0e 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -569,6 +569,7 @@ function transaction( return activeInstance.transaction(sender, f); } +// TODO: should we instead constrain to `Transaction`? async function sendTransaction(txn: Transaction) { return await activeInstance.sendTransaction(txn); } From f990ce6a9d778a69ce36a0a0f7b4f791f4436167 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 16:17:28 -0400 Subject: [PATCH 10/23] make proof plural --- src/examples/zkapps/simple-zkapp-with-proof.ts | 8 ++++---- src/lib/mina/transaction.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/examples/zkapps/simple-zkapp-with-proof.ts b/src/examples/zkapps/simple-zkapp-with-proof.ts index 40d50a909d..1c40a39d4a 100644 --- a/src/examples/zkapps/simple-zkapp-with-proof.ts +++ b/src/examples/zkapps/simple-zkapp-with-proof.ts @@ -65,7 +65,7 @@ let { verificationKey: trivialVerificationKey } = await TrivialZkapp.compile(); console.log('prove (trivial zkapp)'); let [trivialProof] = await Mina.transaction(feePayer, async () => { await new TrivialZkapp(zkappAddress2).proveSomething(Field(1)); -}).prove().proof; +}).prove().proofs; trivialProof = await testJsonRoundtripAndVerify( TrivialProof, @@ -93,7 +93,7 @@ let tx = Mina.transaction(feePayer, async () => { }) .prove() .sign([feePayerKey]); -let [proof] = await tx.proof; +let [proof] = await tx.proofs; await tx.send(); proof = await testJsonRoundtripAndVerify( @@ -110,7 +110,7 @@ tx = Mina.transaction(feePayer, async () => { }) .prove() .sign([feePayerKey]); -[proof] = await tx.proof; +[proof] = await tx.proofs; await tx.send(); proof = await testJsonRoundtripAndVerify( @@ -127,7 +127,7 @@ tx = Mina.transaction(feePayer, async () => { }) .prove() .sign([feePayerKey]); -[proof] = await tx.proof; +[proof] = await tx.proofs; await tx.send(); proof = await testJsonRoundtripAndVerify( diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 6326e16a0e..a77bc3ea58 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -118,7 +118,7 @@ type Transaction< } : { /** The proofs generated as the result of calling `prove`. */ - proof: (Proof | undefined)[]; + proofs: (Proof | undefined)[]; }) & (Signed extends false ? { @@ -321,7 +321,7 @@ type TransactionPromise< * contains a `prove` call, then this field contains a promise resolving to the * proof array which was output from the underlying `prove` call. */ - proof: Promise['proof']>; + proofs: Promise['proofs']>; }) & (Signed extends false ? { @@ -351,7 +351,7 @@ function toTransactionPromise( ); }, proofs: pending.then( - (v) => (v as never as Transaction).proof + (v) => (v as never as Transaction).proofs ), }) as never as TransactionPromise; } From 1ad572660a93430eda73f3c6be5a88cf21955086 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 17:07:54 -0400 Subject: [PATCH 11/23] add to changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83dc3214a..1e20e6a5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm _Security_ in case of vulnerabilities. --> +## [0.19.0](TODO) + +### Breaking changes + +- A `Transaction`'s `prove` method no longer returns the proofs promise directly, but rather returns a `Transaction` promise, the resolved value of which contains a `proofs` prop. https://github.com/o1-labs/o1js/pull/1567 +- The `Transaction` type now has two type params `Proven extends boolean` and `Signed extends boolean`, which are used to conditionally show/hide relevant state. https://github.com/o1-labs/o1js/pull/1567 + +### Added + +- The API of `Mina.transaction` has been reworked such that one can call methods directly on the returned promise. Method-chaining such as the following is now supported. https://github.com/o1-labs/o1js/pull/1567 + ## [0.18.0](https://github.com/o1-labs/o1js/compare/74948acac...1b6fd8b8e) - 2024-04-09 ### Breaking changes From 9a4354c1a3c5ea3629dd95860e6f8fb0eb2dc1c5 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 17:10:35 -0400 Subject: [PATCH 12/23] clean up changelog sentence --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e20e6a5de..879b54ee3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added -- The API of `Mina.transaction` has been reworked such that one can call methods directly on the returned promise. Method-chaining such as the following is now supported. https://github.com/o1-labs/o1js/pull/1567 +- The API of `Mina.transaction` has been reworked such that one can call methods directly on the returned promise. This enables a fluent / method-chaining API. https://github.com/o1-labs/o1js/pull/1567 ## [0.18.0](https://github.com/o1-labs/o1js/compare/74948acac...1b6fd8b8e) - 2024-04-09 From f3a02766b71a5b7c160e53b33b6e0f568044aaa2 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Fri, 12 Apr 2024 17:11:53 -0400 Subject: [PATCH 13/23] more changelog cleanup --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 879b54ee3c..cf3dd71f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added -- The API of `Mina.transaction` has been reworked such that one can call methods directly on the returned promise. This enables a fluent / method-chaining API. https://github.com/o1-labs/o1js/pull/1567 +- `Mina.transaction` has been reworked such that one can call methods directly on the returned promise (now a `TransactionPromise`). This enables a fluent / method-chaining API. https://github.com/o1-labs/o1js/pull/1567 +- `TransactionPendingPromise` enables calling `wait` directly on the promise returned by calling `send` on a `Transaction`. ## [0.18.0](https://github.com/o1-labs/o1js/compare/74948acac...1b6fd8b8e) - 2024-04-09 From 02c6f475241c66703a167cea6736be967d02629e Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Sat, 13 Apr 2024 09:48:04 -0400 Subject: [PATCH 14/23] Transaction shadowing in the same file --- CHANGELOG.md | 2 +- src/index.ts | 2 +- src/lib/mina/mina.ts | 15 +++------------ src/lib/mina/transaction.ts | 12 +++++++++++- src/mina-signer/tests/zkapp.unit-test.ts | 4 ++-- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3dd71f1d..62829573b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - `Mina.transaction` has been reworked such that one can call methods directly on the returned promise (now a `TransactionPromise`). This enables a fluent / method-chaining API. https://github.com/o1-labs/o1js/pull/1567 -- `TransactionPendingPromise` enables calling `wait` directly on the promise returned by calling `send` on a `Transaction`. +- `TransactionPendingPromise` enables calling `wait` directly on the promise returned by calling `send` on a `Transaction`. https://github.com/o1-labs/o1js/pull/1567 ## [0.18.0](https://github.com/o1-labs/o1js/compare/74948acac...1b6fd8b8e) - 2024-04-09 diff --git a/src/index.ts b/src/index.ts index bbbc46de9b..e986598784 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,7 +51,7 @@ export { MerkleList, MerkleListIterator } from './lib/provable/merkle-list.js'; export * as Mina from './lib/mina/mina.js'; export { - type Transaction, + Transaction, type TransactionPromise, type PendingTransaction, type IncludedTransaction, diff --git a/src/lib/mina/mina.ts b/src/lib/mina/mina.ts index ecc53a1878..d84c5d9ccb 100644 --- a/src/lib/mina/mina.ts +++ b/src/lib/mina/mina.ts @@ -2,7 +2,7 @@ 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 { ZkappCommand, TokenId, Authorization } from './account-update.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'; @@ -31,7 +31,7 @@ import { } from './mina-instance.js'; import { type EventActionFilterOptions } from './graphql.js'; import { - type Transaction, + Transaction, type PendingTransaction, type IncludedTransaction, type RejectedTransaction, @@ -39,7 +39,6 @@ import { type PendingTransactionPromise, createTransaction, toTransactionPromise, - newTransaction, transaction, createRejectedTransaction, createIncludedTransaction, @@ -57,7 +56,7 @@ export { LocalBlockchain, Network, currentTransaction, - type Transaction, + Transaction, type PendingTransaction, type IncludedTransaction, type RejectedTransaction, @@ -85,7 +84,6 @@ export { // for internal testing only filterGroups, type NetworkConstants, - TransactionUtil, }; // patch active instance so that we can still create basic transactions without giving Mina network details @@ -96,13 +94,6 @@ setActiveInstance({ }, }); -const TransactionUtil = { - fromJSON(json: Types.Json.ZkappCommand): Transaction { - let transaction = ZkappCommand.fromJSON(json); - return newTransaction(transaction, activeInstance.proofsEnabled); - }, -}; - /** * Represents the Mina blockchain running on a real network */ diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index a77bc3ea58..acbc51ef8f 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -21,9 +21,10 @@ import * as Fetch from './fetch.js'; import { type SendZkAppResponse, sendZkappQuery } from './graphql.js'; import { type FetchMode } from './transaction-context.js'; import { assertPromise } from '../util/assert.js'; +import { Types } from 'src/bindings/mina-transaction/types.js'; export { - type Transaction, + Transaction, type TransactionPromise, type PendingTransaction, type IncludedTransaction, @@ -79,6 +80,15 @@ type TransactionCommon = { */ }; +namespace Transaction { + export function fromJSON( + json: Types.Json.ZkappCommand + ): Transaction { + let transaction = ZkappCommand.fromJSON(json); + return newTransaction(transaction, activeInstance.proofsEnabled); + } +} + /** * Defines the structure and operations associated with a transaction. * This type encompasses methods for serializing the transaction, signing it, generating proofs, diff --git a/src/mina-signer/tests/zkapp.unit-test.ts b/src/mina-signer/tests/zkapp.unit-test.ts index e7d2bac1b5..1b3ffb66c3 100644 --- a/src/mina-signer/tests/zkapp.unit-test.ts +++ b/src/mina-signer/tests/zkapp.unit-test.ts @@ -3,7 +3,7 @@ import * as TransactionJson from '../../bindings/mina-transaction/gen/transactio import Client from '../mina-signer.js'; import { accountUpdateExample } from '../src/test-vectors/accountUpdate.js'; import { expect } from 'expect'; -import { TransactionUtil } from '../../lib/mina/mina.js'; +import { Transaction } from '../../lib/mina/mina.js'; import { PrivateKey } from '../../lib/provable/crypto/signature.js'; import { Signature } from '../src/signature.js'; import { mocks } from '../../bindings/crypto/constants.js'; @@ -100,7 +100,7 @@ let transactionJson = { memo: zkappCommand.data.zkappCommand.memo, }; -let tx = TransactionUtil.fromJSON(transactionJson); +let tx = Transaction.fromJSON(transactionJson); tx.transaction.feePayer.lazyAuthorization = { kind: 'lazy-signature' }; tx.transaction.accountUpdates[1].lazyAuthorization = { kind: 'lazy-signature' }; tx.sign([PrivateKey.fromBase58(privateKey)]); From 9b4bfe871f00afd1be81ed5e552ba1377b7ce1a9 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Sat, 13 Apr 2024 15:03:22 -0400 Subject: [PATCH 15/23] remove changelog heading --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62829573b6..5c28e8c814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm _Security_ in case of vulnerabilities. --> -## [0.19.0](TODO) - ### Breaking changes - A `Transaction`'s `prove` method no longer returns the proofs promise directly, but rather returns a `Transaction` promise, the resolved value of which contains a `proofs` prop. https://github.com/o1-labs/o1js/pull/1567 From dc59a79a72b7003154d82ea45d1617a741556ca1 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Tue, 16 Apr 2024 16:59:04 -0400 Subject: [PATCH 16/23] fix integration test error --- src/bindings | 2 +- src/examples/zkapps/hello-world/run.ts | 4 ++-- src/lib/mina/transaction.ts | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/bindings b/src/bindings index 30cefd9fa7..dcbee6171f 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 30cefd9fa75a9c0cb9091edfc96410dea6bbacf1 +Subproject commit dcbee6171feb354109f09677e593d9bd965770ad diff --git a/src/examples/zkapps/hello-world/run.ts b/src/examples/zkapps/hello-world/run.ts index 50372b53d3..0aa23f73db 100644 --- a/src/examples/zkapps/hello-world/run.ts +++ b/src/examples/zkapps/hello-world/run.ts @@ -10,10 +10,10 @@ let txn, txn2, txn3, txn4; let Local = Mina.LocalBlockchain({ proofsEnabled: false }); Mina.setActiveInstance(Local); -// test accounts that pays all the fees, and puts additional funds into the zkapp +// test accounts that pays all the fees, and puts additional funds into the contract const [feePayer1, feePayer2, feePayer3, feePayer4] = Local.testAccounts; -// zkapp account +// contract account const contractAccount = Mina.TestPublicKey.random(); const contract = new HelloWorld(contractAccount); diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index acbc51ef8f..f06a9da095 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -360,9 +360,11 @@ function toTransactionPromise( pending.then((v) => (v as never as Transaction).prove()) ); }, - proofs: pending.then( - (v) => (v as never as Transaction).proofs - ), + proofs: pending + .then((v) => (v as never as Transaction).proofs) + .catch( + () => [] as never as (Proof | undefined)[] + ), }) as never as TransactionPromise; } From ea5e13bc3d3108d3f48611a888e2abda81b21f60 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Wed, 17 Apr 2024 07:54:08 -0400 Subject: [PATCH 17/23] make proofs into a method --- src/bindings | 2 +- src/examples/zkapps/simple-zkapp-with-proof.ts | 4 +++- src/lib/mina/local-blockchain.ts | 11 +++++++---- src/lib/mina/transaction.ts | 12 ++++++------ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/bindings b/src/bindings index dcbee6171f..8a489b839b 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit dcbee6171feb354109f09677e593d9bd965770ad +Subproject commit 8a489b839ba4c7e35443a0b2213607e5c0540a7d diff --git a/src/examples/zkapps/simple-zkapp-with-proof.ts b/src/examples/zkapps/simple-zkapp-with-proof.ts index 2cfb824a6e..a6110e194a 100644 --- a/src/examples/zkapps/simple-zkapp-with-proof.ts +++ b/src/examples/zkapps/simple-zkapp-with-proof.ts @@ -57,7 +57,9 @@ let { verificationKey: trivialVerificationKey } = await TrivialZkapp.compile(); console.log('prove (trivial zkapp)'); let [trivialProof] = await Mina.transaction(feePayer, async () => { await new TrivialZkapp(notSoSimpleContractAccount).proveSomething(Field(1)); -}).prove().proofs; +}) + .prove() + .proofs(); trivialProof = await testJsonRoundtripAndVerify( TrivialProof, diff --git a/src/lib/mina/local-blockchain.ts b/src/lib/mina/local-blockchain.ts index ca018a4db0..7eeac2b462 100644 --- a/src/lib/mina/local-blockchain.ts +++ b/src/lib/mina/local-blockchain.ts @@ -264,11 +264,14 @@ async function LocalBlockchain({ latestActionState_ !== undefined ? Field(latestActionState_) : Actions.emptyActionState(); + }); - let test = await Test(); - const hash = test.transactionHash.hashZkAppCommand(txn.toJSON()); - const pendingTransaction: Omit = - { + let test = await Test(); + const hash = test.transactionHash.hashZkAppCommand(txn.toJSON()); + const pendingTransaction: Omit< + PendingTransaction, + 'wait' | 'safeWait' + > = { status, errors, transaction: txn.transaction, diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index f06a9da095..db6e0ecedd 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -331,7 +331,7 @@ type 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']>; + proofs(): Promise['proofs']>; }) & (Signed extends false ? { @@ -360,11 +360,11 @@ function toTransactionPromise( pending.then((v) => (v as never as Transaction).prove()) ); }, - proofs: pending - .then((v) => (v as never as Transaction).proofs) - .catch( - () => [] as never as (Proof | undefined)[] - ), + proofs() { + return pending.then( + (v) => (v as never as Transaction).proofs + ); + }, }) as never as TransactionPromise; } From 442c3402594275d5709807fb8c5ed4fcbdc34158 Mon Sep 17 00:00:00 2001 From: Harry Solovay Date: Wed, 17 Apr 2024 11:06:43 -0400 Subject: [PATCH 18/23] clean up reducer-composite eg --- .../zkapps/reducer/reducer-composite.ts | 182 ++++++++++-------- 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/src/examples/zkapps/reducer/reducer-composite.ts b/src/examples/zkapps/reducer/reducer-composite.ts index 5acb1092d3..b6597e34d3 100644 --- a/src/examples/zkapps/reducer/reducer-composite.ts +++ b/src/examples/zkapps/reducer/reducer-composite.ts @@ -15,63 +15,85 @@ import { import assert from 'node:assert/strict'; import { getProfiler } from '../../utils/profiler.js'; -class MaybeIncrement extends Struct({ +class Action extends Struct({ isIncrement: Bool, - otherData: Field, -}) {} -const INCREMENT = { isIncrement: Bool(true), otherData: Field(0) }; + data: Field, +}) { + static increment(count: number) { + return new this({ + isIncrement: Bool(true), + data: Field(count), + }); + } + + static other(data: Field) { + return new this({ + isIncrement: Bool(false), + data, + }); + } +} + +class CounterState extends Struct({ + count: Field, + rollup: Field, // helper field to store the point in the action history that our on-chain state is at +}) { + static initial = new this({ + count: Field(0), + rollup: Reducer.initialActionState, + }); +} class Counter extends SmartContract { // the "reducer" field describes a type of action that we can dispatch, and reduce later - reducer = Reducer({ actionType: MaybeIncrement }); + reducer = Reducer({ actionType: Action }); // on-chain version of our state. it will typically lag behind the // version that's implicitly represented by the list of actions - @state(Field) counter = State(); - // helper field to store the point in the action history that our on-chain state is at - @state(Field) actionState = State(); + @state(CounterState) state = State(); - @method async incrementCounter() { - this.reducer.dispatch(INCREMENT); + @method async increment() { + this.reducer.dispatch(Action.increment(1)); } - @method async dispatchData(data: Field) { - this.reducer.dispatch({ isIncrement: Bool(false), otherData: data }); + + @method async other(data: Field) { + this.reducer.dispatch(Action.other(data)); } - @method async rollupIncrements() { + @method async rollup() { // get previous counter & actions hash, assert that they're the same as on-chain values - let counter = this.counter.get(); - this.counter.requireEquals(counter); - let actionState = this.actionState.get(); - this.actionState.requireEquals(actionState); + const state = this.state.getAndRequireEquals(); // compute the new counter and hash from pending actions - let pendingActions = this.reducer.getActions({ - fromActionState: actionState, + const pending = this.reducer.getActions({ + fromActionState: state.rollup, }); - let { state: newCounter, actionState: newActionState } = - this.reducer.reduce( - pendingActions, - // state type - Field, - // function that says how to apply an action - (state: Field, action: MaybeIncrement) => { - return Provable.if(action.isIncrement, state.add(1), state); - }, - { state: counter, actionState } - ); + const reduced = this.reducer.reduce( + // state type + pending, + // the accumulator type + CounterState, + // function that says how to apply an action + (state, action) => { + const count = Provable.if( + action.isIncrement, + state.count.add(1), + state.count + ); + return new CounterState({ ...state, count }); + }, + { state, actionState: state.rollup } + ); // update on-chain state - this.counter.set(newCounter); - this.actionState.set(newActionState); + this.state.set(reduced.state); } } const ReducerProfiler = getProfiler('Reducer zkApp'); ReducerProfiler.start('Reducer zkApp test flow'); const doProofs = true; -const initialCounter = Field(0); let Local = await Mina.LocalBlockchain({ proofsEnabled: doProofs }); Mina.setActiveInstance(Local); @@ -89,77 +111,73 @@ if (doProofs) { } console.log('deploy'); -let tx = await Mina.transaction(feePayer, async () => { +await Mina.transaction(feePayer, async () => { AccountUpdate.fundNewAccount(feePayer); await contract.deploy(); - contract.counter.set(initialCounter); - contract.actionState.set(Reducer.initialActionState); -}); -await tx.sign([feePayer.key, contractAccount.key]).send(); + contract.state.set(CounterState.initial); +}) + .sign([feePayer.key, contractAccount.key]) + .prove() + .send(); console.log('applying actions..'); console.log('action 1'); - -tx = await Mina.transaction(feePayer, async () => { - await contract.incrementCounter(); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); +await increment(); console.log('action 2'); -tx = await Mina.transaction(feePayer, async () => { - await contract.incrementCounter(); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); +await increment(); console.log('action 3'); -tx = await Mina.transaction(feePayer, async () => { - await contract.incrementCounter(); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); - -console.log('rolling up pending actions..'); +await increment(); -console.log('state before: ' + contract.counter.get()); +console.log('count before: ' + contract.state.get().count); -tx = await Mina.transaction(feePayer, async () => { - await contract.rollupIncrements(); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); +console.log('rolling up pending actions..'); +await rollup(); -console.log('state after rollup: ' + contract.counter.get()); -assert.deepEqual(contract.counter.get().toString(), '3'); +console.log('state after rollup:', contract.state.get().count); +assert.deepEqual(contract.state.get().count.toString(), '3'); console.log('applying more actions'); console.log('action 4 (no increment)'); -tx = await Mina.transaction(feePayer, async () => { - await contract.dispatchData(Field.random()); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); +await Mina.transaction(feePayer, async () => { + await contract.other(Field.random()); +}) + .prove() + .sign([feePayer.key]) + .send(); console.log('action 5'); -tx = await Mina.transaction(feePayer, async () => { - await contract.incrementCounter(); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); +await increment(); console.log('rolling up pending actions..'); -console.log('state before: ' + contract.counter.get()); +console.log('state before: ' + contract.state.get().count); -tx = await Mina.transaction(feePayer, async () => { - await contract.rollupIncrements(); -}); -await tx.prove(); -await tx.sign([feePayer.key]).send(); +await rollup(); -console.log('state after rollup: ' + contract.counter.get()); -assert.equal(contract.counter.get().toString(), '4'); +console.log('state after rollup: ' + contract.state.get().count); +assert.equal(contract.state.get().count.toString(), '4'); ReducerProfiler.stop().store(); + +// + +function increment() { + return Mina.transaction(feePayer, async () => { + await contract.increment(); + }) + .prove() + .sign([feePayer.key]) + .send(); +} + +function rollup() { + return Mina.transaction(feePayer, async () => { + await contract.rollup(); + }) + .prove() + .sign([feePayer.key]) + .send(); +} From c60ac20082efc117dabd3a080110f35c0236eeef Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 17 Apr 2024 10:51:39 -0700 Subject: [PATCH 19/23] feat(local-blockchain): fix support for actions --- src/lib/mina/local-blockchain.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/mina/local-blockchain.ts b/src/lib/mina/local-blockchain.ts index 7eeac2b462..7c7eacbc30 100644 --- a/src/lib/mina/local-blockchain.ts +++ b/src/lib/mina/local-blockchain.ts @@ -264,6 +264,19 @@ async function LocalBlockchain({ latestActionState_ !== undefined ? Field(latestActionState_) : Actions.emptyActionState(); + + actions[addr] ??= {}; + if (p.body.actions.data.length > 0) { + let newActionState = Actions.updateSequenceState( + latestActionState, + p.body.actions.hash + ); + actions[addr][tokenId] ??= []; + actions[addr][tokenId].push({ + actions: pJson.body.actions, + hash: newActionState.toString(), + }); + } }); let test = await Test(); From 02f97773957d3ed918937adc9c42660ef3d12cd6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 17 Apr 2024 10:52:12 -0700 Subject: [PATCH 20/23] feat(examples): fixup examples causing issues in integration tests --- src/examples/simple-zkapp.ts | 1 - .../zkapps/reducer/reducer-composite.ts | 182 ++++++++---------- 2 files changed, 82 insertions(+), 101 deletions(-) diff --git a/src/examples/simple-zkapp.ts b/src/examples/simple-zkapp.ts index a70a21f727..337b3a4762 100644 --- a/src/examples/simple-zkapp.ts +++ b/src/examples/simple-zkapp.ts @@ -133,7 +133,6 @@ tx = await Mina.transaction(sender, async () => { }); await tx.prove(); await tx.sign([sender.key]).send(); -sender; console.log('final state: ' + zkapp.x.get()); console.log(`final balance: ${zkapp.account.balance.get().div(1e9)} MINA`); diff --git a/src/examples/zkapps/reducer/reducer-composite.ts b/src/examples/zkapps/reducer/reducer-composite.ts index b6597e34d3..5acb1092d3 100644 --- a/src/examples/zkapps/reducer/reducer-composite.ts +++ b/src/examples/zkapps/reducer/reducer-composite.ts @@ -15,85 +15,63 @@ import { import assert from 'node:assert/strict'; import { getProfiler } from '../../utils/profiler.js'; -class Action extends Struct({ +class MaybeIncrement extends Struct({ isIncrement: Bool, - data: Field, -}) { - static increment(count: number) { - return new this({ - isIncrement: Bool(true), - data: Field(count), - }); - } - - static other(data: Field) { - return new this({ - isIncrement: Bool(false), - data, - }); - } -} - -class CounterState extends Struct({ - count: Field, - rollup: Field, // helper field to store the point in the action history that our on-chain state is at -}) { - static initial = new this({ - count: Field(0), - rollup: Reducer.initialActionState, - }); -} + otherData: Field, +}) {} +const INCREMENT = { isIncrement: Bool(true), otherData: Field(0) }; class Counter extends SmartContract { // the "reducer" field describes a type of action that we can dispatch, and reduce later - reducer = Reducer({ actionType: Action }); + reducer = Reducer({ actionType: MaybeIncrement }); // on-chain version of our state. it will typically lag behind the // version that's implicitly represented by the list of actions - @state(CounterState) state = State(); + @state(Field) counter = State(); + // helper field to store the point in the action history that our on-chain state is at + @state(Field) actionState = State(); - @method async increment() { - this.reducer.dispatch(Action.increment(1)); + @method async incrementCounter() { + this.reducer.dispatch(INCREMENT); } - - @method async other(data: Field) { - this.reducer.dispatch(Action.other(data)); + @method async dispatchData(data: Field) { + this.reducer.dispatch({ isIncrement: Bool(false), otherData: data }); } - @method async rollup() { + @method async rollupIncrements() { // get previous counter & actions hash, assert that they're the same as on-chain values - const state = this.state.getAndRequireEquals(); + let counter = this.counter.get(); + this.counter.requireEquals(counter); + let actionState = this.actionState.get(); + this.actionState.requireEquals(actionState); // compute the new counter and hash from pending actions - const pending = this.reducer.getActions({ - fromActionState: state.rollup, + let pendingActions = this.reducer.getActions({ + fromActionState: actionState, }); - const reduced = this.reducer.reduce( - // state type - pending, - // the accumulator type - CounterState, - // function that says how to apply an action - (state, action) => { - const count = Provable.if( - action.isIncrement, - state.count.add(1), - state.count - ); - return new CounterState({ ...state, count }); - }, - { state, actionState: state.rollup } - ); + let { state: newCounter, actionState: newActionState } = + this.reducer.reduce( + pendingActions, + // state type + Field, + // function that says how to apply an action + (state: Field, action: MaybeIncrement) => { + return Provable.if(action.isIncrement, state.add(1), state); + }, + { state: counter, actionState } + ); // update on-chain state - this.state.set(reduced.state); + this.counter.set(newCounter); + this.actionState.set(newActionState); } } const ReducerProfiler = getProfiler('Reducer zkApp'); ReducerProfiler.start('Reducer zkApp test flow'); const doProofs = true; +const initialCounter = Field(0); let Local = await Mina.LocalBlockchain({ proofsEnabled: doProofs }); Mina.setActiveInstance(Local); @@ -111,73 +89,77 @@ if (doProofs) { } console.log('deploy'); -await Mina.transaction(feePayer, async () => { +let tx = await Mina.transaction(feePayer, async () => { AccountUpdate.fundNewAccount(feePayer); await contract.deploy(); - contract.state.set(CounterState.initial); -}) - .sign([feePayer.key, contractAccount.key]) - .prove() - .send(); + contract.counter.set(initialCounter); + contract.actionState.set(Reducer.initialActionState); +}); +await tx.sign([feePayer.key, contractAccount.key]).send(); console.log('applying actions..'); console.log('action 1'); -await increment(); + +tx = await Mina.transaction(feePayer, async () => { + await contract.incrementCounter(); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); console.log('action 2'); -await increment(); +tx = await Mina.transaction(feePayer, async () => { + await contract.incrementCounter(); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); console.log('action 3'); -await increment(); - -console.log('count before: ' + contract.state.get().count); +tx = await Mina.transaction(feePayer, async () => { + await contract.incrementCounter(); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); console.log('rolling up pending actions..'); -await rollup(); -console.log('state after rollup:', contract.state.get().count); -assert.deepEqual(contract.state.get().count.toString(), '3'); +console.log('state before: ' + contract.counter.get()); + +tx = await Mina.transaction(feePayer, async () => { + await contract.rollupIncrements(); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); + +console.log('state after rollup: ' + contract.counter.get()); +assert.deepEqual(contract.counter.get().toString(), '3'); console.log('applying more actions'); console.log('action 4 (no increment)'); -await Mina.transaction(feePayer, async () => { - await contract.other(Field.random()); -}) - .prove() - .sign([feePayer.key]) - .send(); +tx = await Mina.transaction(feePayer, async () => { + await contract.dispatchData(Field.random()); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); console.log('action 5'); -await increment(); +tx = await Mina.transaction(feePayer, async () => { + await contract.incrementCounter(); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); console.log('rolling up pending actions..'); -console.log('state before: ' + contract.state.get().count); +console.log('state before: ' + contract.counter.get()); -await rollup(); +tx = await Mina.transaction(feePayer, async () => { + await contract.rollupIncrements(); +}); +await tx.prove(); +await tx.sign([feePayer.key]).send(); -console.log('state after rollup: ' + contract.state.get().count); -assert.equal(contract.state.get().count.toString(), '4'); +console.log('state after rollup: ' + contract.counter.get()); +assert.equal(contract.counter.get().toString(), '4'); ReducerProfiler.stop().store(); - -// - -function increment() { - return Mina.transaction(feePayer, async () => { - await contract.increment(); - }) - .prove() - .sign([feePayer.key]) - .send(); -} - -function rollup() { - return Mina.transaction(feePayer, async () => { - await contract.rollup(); - }) - .prove() - .sign([feePayer.key]) - .send(); -} From c60ecf0590d222a7db32090e1c9724d92b115cfc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 17 Apr 2024 12:30:52 -0700 Subject: [PATCH 21/23] chore(mina): update mina submodule to latest commit for up-to-date features and fixes --- src/mina | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mina b/src/mina index 03e10fc739..a883196f55 160000 --- a/src/mina +++ b/src/mina @@ -1 +1 @@ -Subproject commit 03e10fc739d74f375cedd7388a84767bee4c7eb0 +Subproject commit a883196f55b1a6c7ce41f9f3cc68c5a5e7ae2422 From 155a12b4e035badf7e3b94628dec14827fe8111b Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 17 Apr 2024 12:37:13 -0700 Subject: [PATCH 22/23] fix(fake-proof.ts): change the way contractProof is extracted from tx.prove() to ensure correct data extraction --- src/tests/fake-proof.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/fake-proof.ts b/src/tests/fake-proof.ts index f369d6440f..e1c3ebc232 100644 --- a/src/tests/fake-proof.ts +++ b/src/tests/fake-proof.ts @@ -87,7 +87,7 @@ assert( // contract accepts proof let tx = await Mina.transaction(() => zkApp.verifyReal(realProof)); -let [contractProof] = await tx.prove(); +let [contractProof] = (await tx.prove()).proofs; assert( await verify(contractProof!, contractVk.data), 'recursive contract accepts real proof' From 8e766336d87d623fd89f4301a3d92462b44b950d Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 17 Apr 2024 13:30:40 -0700 Subject: [PATCH 23/23] fix(on-chain-state-mgmt-zkapp-ui.js): change proof extraction from transaction.prove() to correctly get proof data This change is necessary because the previous implementation was not correctly extracting the proof data from the transaction.prove() promise. --- tests/artifacts/javascript/on-chain-state-mgmt-zkapp-ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/artifacts/javascript/on-chain-state-mgmt-zkapp-ui.js b/tests/artifacts/javascript/on-chain-state-mgmt-zkapp-ui.js index b13583877f..bfeed49252 100644 --- a/tests/artifacts/javascript/on-chain-state-mgmt-zkapp-ui.js +++ b/tests/artifacts/javascript/on-chain-state-mgmt-zkapp-ui.js @@ -78,7 +78,7 @@ updateButton.addEventListener('click', async (event) => { ); }); - const [proof] = await transaction.prove(); + const [proof] = (await transaction.prove()).proofs; if (verificationKey) { let isVerified = await verify(proof, verificationKey.data);