From 7cfa3f02101ded2b9f674e788257298c0b6a4d86 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 09:13:50 -0800 Subject: [PATCH 01/12] refactor(transaction): replace sendOrThrowIfError() with send() to simplify error handling --- src/examples/zkapps/dex/run.ts | 24 ++++----- src/examples/zkapps/dex/upgradability.ts | 18 +++---- src/examples/zkapps/hello-world/run.ts | 16 +++--- src/lib/account-update.unit-test.ts | 2 +- src/lib/caller.unit-test.ts | 2 +- src/lib/mina/mina-instance.ts | 6 ++- src/lib/mina/transaction.ts | 63 +++++++++++++++++------- src/lib/precondition.test.ts | 32 +++++------- src/lib/token.test.ts | 12 ++--- src/tests/transaction-flow.ts | 2 +- 10 files changed, 96 insertions(+), 81 deletions(-) diff --git a/src/examples/zkapps/dex/run.ts b/src/examples/zkapps/dex/run.ts index e053a6463e..0244ae541e 100644 --- a/src/examples/zkapps/dex/run.ts +++ b/src/examples/zkapps/dex/run.ts @@ -237,9 +237,7 @@ async function main({ withVesting }: { withVesting: boolean }) { (USER_DX * oldBalances.total.lqXY) / oldBalances.dex.X ); } else { - await expect(tx.sendOrThrowIfError()).rejects.toThrow( - /Update_not_permitted_timing/ - ); + await expect(tx.send()).rejects.toThrow(/Update_not_permitted_timing/); } /** @@ -254,14 +252,14 @@ async function main({ withVesting }: { withVesting: boolean }) { }); await tx.prove(); tx.sign([keys.user2]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow(/Overflow/); + await expect(tx.send()).rejects.toThrow(/Overflow/); console.log('supplying with insufficient tokens (should fail)'); tx = await Mina.transaction(addresses.user, () => { dex.supplyLiquidityBase(UInt64.from(1e9), UInt64.from(1e9)); }); await tx.prove(); tx.sign([keys.user]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow(/Overflow/); + await expect(tx.send()).rejects.toThrow(/Overflow/); /** * - Resulting operation will overflow the SC’s receiving token by type or by any other applicable limits; @@ -280,7 +278,7 @@ async function main({ withVesting }: { withVesting: boolean }) { ); }); await tx.prove(); - await tx.sign([feePayerKey, keys.tokenY]).sendOrThrowIfError(); + await tx.sign([feePayerKey, keys.tokenY]).send(); console.log('supply overflowing liquidity'); await expect(async () => { tx = await Mina.transaction(addresses.tokenX, () => { @@ -291,7 +289,7 @@ async function main({ withVesting }: { withVesting: boolean }) { }); await tx.prove(); tx.sign([keys.tokenX]); - await tx.sendOrThrowIfError(); + await tx.send(); }).rejects.toThrow(); /** @@ -318,7 +316,7 @@ async function main({ withVesting }: { withVesting: boolean }) { dex.supplyLiquidity(UInt64.from(10)); }); await tx.prove(); - await expect(tx.sign([keys.tokenX]).sendOrThrowIfError()).rejects.toThrow( + await expect(tx.sign([keys.tokenX]).send()).rejects.toThrow( /Update_not_permitted_balance/ ); @@ -345,9 +343,7 @@ async function main({ withVesting }: { withVesting: boolean }) { }); await tx.prove(); tx.sign([keys.user]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow( - /Source_minimum_balance_violation/ - ); + await expect(tx.send()).rejects.toThrow(/Source_minimum_balance_violation/); // another slot => now it should work Local.incrementGlobalSlot(1); @@ -456,7 +452,7 @@ async function main({ withVesting }: { withVesting: boolean }) { }); await tx.prove(); tx.sign([keys.user, keys.user2]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow( + await expect(tx.send()).rejects.toThrow( /Account_balance_precondition_unsatisfied/ ); @@ -491,9 +487,7 @@ async function main({ withVesting }: { withVesting: boolean }) { dex.redeemLiquidity(UInt64.from(1n)); }); await tx.prove(); - await expect(tx.sign([keys.user2]).sendOrThrowIfError()).rejects.toThrow( - /Overflow/ - ); + await expect(tx.sign([keys.user2]).send()).rejects.toThrow(/Overflow/); [oldBalances, balances] = [balances, getTokenBalances()]; /** diff --git a/src/examples/zkapps/dex/upgradability.ts b/src/examples/zkapps/dex/upgradability.ts index 70ba253967..366ca2c467 100644 --- a/src/examples/zkapps/dex/upgradability.ts +++ b/src/examples/zkapps/dex/upgradability.ts @@ -123,9 +123,9 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) { }); await tx.prove(); - await expect( - tx.sign([feePayerKey, keys.dex]).sendOrThrowIfError() - ).rejects.toThrow(/Cannot update field 'delegate'/); + await expect(tx.sign([feePayerKey, keys.dex]).send()).rejects.toThrow( + /Cannot update field 'delegate'/ + ); console.log('changing delegate permission back to normal'); @@ -185,9 +185,9 @@ async function atomicActionsTest({ withVesting }: { withVesting: boolean }) { fieldUpdate.requireSignature(); }); await tx.prove(); - await expect( - tx.sign([feePayerKey, keys.dex]).sendOrThrowIfError() - ).rejects.toThrow(/Cannot update field 'delegate'/); + await expect(tx.sign([feePayerKey, keys.dex]).send()).rejects.toThrow( + /Cannot update field 'delegate'/ + ); /** * # Atomic Actions 3 @@ -461,9 +461,9 @@ async function upgradeabilityTests({ withVesting }: { withVesting: boolean }) { modifiedDex.deploy(); // cannot deploy new VK because its forbidden }); await tx.prove(); - await expect( - tx.sign([feePayerKey, keys.dex]).sendOrThrowIfError() - ).rejects.toThrow(/Cannot update field 'verificationKey'/); + await expect(tx.sign([feePayerKey, keys.dex]).send()).rejects.toThrow( + /Cannot update field 'verificationKey'/ + ); console.log('trying to invoke modified swap method'); // method should still be valid since the upgrade was forbidden diff --git a/src/examples/zkapps/hello-world/run.ts b/src/examples/zkapps/hello-world/run.ts index da37518357..272b4ea8f0 100644 --- a/src/examples/zkapps/hello-world/run.ts +++ b/src/examples/zkapps/hello-world/run.ts @@ -27,7 +27,7 @@ txn = await Mina.transaction(feePayer1.publicKey, () => { AccountUpdate.fundNewAccount(feePayer1.publicKey); zkAppInstance.deploy(); }); -await txn.sign([feePayer1.privateKey, zkAppPrivateKey]).sendOrThrowIfError(); +await txn.sign([feePayer1.privateKey, zkAppPrivateKey]).send(); const initialState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); @@ -45,7 +45,7 @@ txn = await Mina.transaction(feePayer1.publicKey, () => { zkAppInstance.update(Field(4), adminPrivateKey); }); await txn.prove(); -await txn.sign([feePayer1.privateKey]).sendOrThrowIfError(); +await txn.sign([feePayer1.privateKey]).send(); currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); @@ -70,7 +70,7 @@ try { zkAppInstance.update(Field(16), wrongAdminPrivateKey); }); await txn.prove(); - await txn.sign([feePayer1.privateKey]).sendOrThrowIfError(); + await txn.sign([feePayer1.privateKey]).send(); } catch (err: any) { handleError(err, 'Account_delegate_precondition_unsatisfied'); } @@ -91,7 +91,7 @@ try { zkAppInstance.update(Field(30), adminPrivateKey); }); await txn.prove(); - await txn.sign([feePayer1.privateKey]).sendOrThrowIfError(); + await txn.sign([feePayer1.privateKey]).send(); } catch (err: any) { handleError(err, 'assertEquals'); } @@ -118,7 +118,7 @@ try { } ); await txn.prove(); - await txn.sign([feePayer1.privateKey]).sendOrThrowIfError(); + await txn.sign([feePayer1.privateKey]).send(); } catch (err: any) { handleError(err, 'assertEquals'); } @@ -134,7 +134,7 @@ txn2 = await Mina.transaction({ sender: feePayer2.publicKey, fee: '2' }, () => { zkAppInstance.update(Field(16), adminPrivateKey); }); await txn2.prove(); -await txn2.sign([feePayer2.privateKey]).sendOrThrowIfError(); +await txn2.sign([feePayer2.privateKey]).send(); currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); @@ -151,7 +151,7 @@ txn3 = await Mina.transaction({ sender: feePayer3.publicKey, fee: '1' }, () => { zkAppInstance.update(Field(256), adminPrivateKey); }); await txn3.prove(); -await txn3.sign([feePayer3.privateKey]).sendOrThrowIfError(); +await txn3.sign([feePayer3.privateKey]).send(); currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); @@ -174,7 +174,7 @@ try { } ); await txn4.prove(); - await txn4.sign([feePayer4.privateKey]).sendOrThrowIfError(); + await txn4.sign([feePayer4.privateKey]).send(); } catch (err: any) { handleError(err, 'assertEquals'); } diff --git a/src/lib/account-update.unit-test.ts b/src/lib/account-update.unit-test.ts index 2b85457d05..845cd25a48 100644 --- a/src/lib/account-update.unit-test.ts +++ b/src/lib/account-update.unit-test.ts @@ -120,7 +120,7 @@ function createAccountUpdate() { AccountUpdate.fundNewAccount(feePayer); }); tx.sign(); - await expect(tx.sendOrThrowIfError()).rejects.toThrow( + await expect(tx.send()).rejects.toThrow( 'Check signature: Invalid signature on fee payer for key' ); } diff --git a/src/lib/caller.unit-test.ts b/src/lib/caller.unit-test.ts index 01eef352d3..2fed8f00d1 100644 --- a/src/lib/caller.unit-test.ts +++ b/src/lib/caller.unit-test.ts @@ -27,6 +27,6 @@ let tx = await Mina.transaction(privateKey, () => { }); // according to this test, the child doesn't get token permissions -await expect(tx.sendOrThrowIfError()).rejects.toThrow( +await expect(tx.send()).rejects.toThrow( 'can not use or pass on token permissions' ); diff --git a/src/lib/mina/mina-instance.ts b/src/lib/mina/mina-instance.ts index 324dfefaeb..1caabddbd2 100644 --- a/src/lib/mina/mina-instance.ts +++ b/src/lib/mina/mina-instance.ts @@ -6,7 +6,11 @@ import { UInt64, UInt32 } from '../int.js'; import { PublicKey, PrivateKey } from '../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 { + Transaction, + PendingTransaction, + RejectedTransaction, +} from '../mina.js'; import type { Account } from './account.js'; import type { NetworkValue } from '../precondition.js'; import type * as Fetch from '../fetch.js'; diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 9d74aa9bb6..66c61d3318 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -104,14 +104,14 @@ type Transaction = { * @example * ```ts * try { - * const pendingTransaction = await transaction.sendOrThrowIfError(); + * const pendingTransaction = await transaction.send(); * console.log('Transaction sent successfully to the Mina daemon.'); * } catch (error) { * console.error('Transaction failed with errors:', error); * } * ``` */ - sendOrThrowIfError(): Promise; + sendSafe(): Promise; }; /** @@ -411,9 +411,6 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { return sendZkappQuery(self.toJSON()); }, async send() { - return await sendTransaction(self); - }, - async sendOrThrowIfError() { const pendingTransaction = await sendTransaction(self); if (pendingTransaction.errors.length > 0) { throw Error( @@ -424,6 +421,16 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { } return pendingTransaction; }, + async sendSafe() { + const pendingTransaction = await sendTransaction(self); + if (pendingTransaction.errors.length > 0) { + return createRejectedTransaction( + pendingTransaction, + pendingTransaction.errors + ); + } + return pendingTransaction; + }, }; return self; } @@ -490,7 +497,7 @@ function getAccount(publicKey: PublicKey, tokenId?: Field): Account { return activeInstance.getAccount(publicKey, tokenId); } -function createIncludedOrRejectedTransaction( +function createRejectedTransaction( { transaction, data, @@ -499,18 +506,28 @@ function createIncludedOrRejectedTransaction( hash, }: Omit, errors: string[] -): IncludedTransaction | RejectedTransaction { - if (errors.length > 0) { - return { - status: 'rejected', - errors, - transaction, - toJSON, - toPretty, - hash, - data, - }; - } +): RejectedTransaction { + return { + status: 'rejected', + errors, + transaction, + toJSON, + toPretty, + hash, + data, + }; +} + +function createIncludedTransaction({ + transaction, + data, + toJSON, + toPretty, + hash, +}: Omit< + PendingTransaction, + 'wait' | 'waitOrThrowIfError' +>): IncludedTransaction { return { status: 'included', transaction, @@ -520,3 +537,13 @@ function createIncludedOrRejectedTransaction( data, }; } + +function createIncludedOrRejectedTransaction( + transaction: Omit, + errors: string[] +): IncludedTransaction | RejectedTransaction { + if (errors.length > 0) { + return createRejectedTransaction(transaction, errors); + } + return createIncludedTransaction(transaction); +} diff --git a/src/lib/precondition.test.ts b/src/lib/precondition.test.ts index 1228a2ec1f..07d0243d68 100644 --- a/src/lib/precondition.test.ts +++ b/src/lib/precondition.test.ts @@ -238,7 +238,7 @@ describe('preconditions', () => { precondition().assertEquals(p.add(1) as any); AccountUpdate.attachToTransaction(zkapp.self); }); - await tx.sign([feePayerKey]).sendOrThrowIfError(); + await tx.sign([feePayerKey]).send(); }).rejects.toThrow(/unsatisfied/); } }); @@ -251,7 +251,7 @@ describe('preconditions', () => { precondition().requireEquals(p.add(1) as any); AccountUpdate.attachToTransaction(zkapp.self); }); - await tx.sign([feePayerKey]).sendOrThrowIfError(); + await tx.sign([feePayerKey]).send(); }).rejects.toThrow(/unsatisfied/); } }); @@ -263,7 +263,7 @@ describe('preconditions', () => { precondition().assertEquals(p.not()); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow( /unsatisfied/ ); } @@ -276,7 +276,7 @@ describe('preconditions', () => { precondition().requireEquals(p.not()); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow( /unsatisfied/ ); } @@ -288,9 +288,7 @@ describe('preconditions', () => { zkapp.account.delegate.assertEquals(publicKey); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( - /unsatisfied/ - ); + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow(/unsatisfied/); }); it('unsatisfied requireEquals should be rejected (public key)', async () => { @@ -299,9 +297,7 @@ describe('preconditions', () => { zkapp.account.delegate.requireEquals(publicKey); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( - /unsatisfied/ - ); + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow(/unsatisfied/); }); it('unsatisfied assertBetween should be rejected', async () => { @@ -311,7 +307,7 @@ describe('preconditions', () => { precondition().assertBetween(p.add(20), p.add(30)); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow( /unsatisfied/ ); } @@ -324,7 +320,7 @@ describe('preconditions', () => { precondition().requireBetween(p.add(20), p.add(30)); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow( /unsatisfied/ ); } @@ -335,9 +331,7 @@ describe('preconditions', () => { zkapp.currentSlot.assertBetween(UInt32.from(20), UInt32.from(30)); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( - /unsatisfied/ - ); + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow(/unsatisfied/); }); it('unsatisfied currentSlot.requireBetween should be rejected', async () => { @@ -345,9 +339,7 @@ describe('preconditions', () => { zkapp.currentSlot.requireBetween(UInt32.from(20), UInt32.from(30)); AccountUpdate.attachToTransaction(zkapp.self); }); - await expect(tx.sign([feePayerKey]).sendOrThrowIfError()).rejects.toThrow( - /unsatisfied/ - ); + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow(/unsatisfied/); }); // TODO: is this a gotcha that should be addressed? @@ -359,9 +351,7 @@ describe('preconditions', () => { zkapp.requireSignature(); AccountUpdate.attachToTransaction(zkapp.self); }); - expect(() => - tx.sign([zkappKey, feePayerKey]).sendOrThrowIfError() - ).toThrow(); + expect(() => tx.sign([zkappKey, feePayerKey]).send()).toThrow(); }); }); diff --git a/src/lib/token.test.ts b/src/lib/token.test.ts index aa5140b3ef..df138aefc1 100644 --- a/src/lib/token.test.ts +++ b/src/lib/token.test.ts @@ -328,7 +328,7 @@ describe('Token', () => { tokenZkapp.requireSignature(); }) ).sign([zkAppBKey, feePayerKey, tokenZkappKey]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow(); + await expect(tx.send()).rejects.toThrow(); }); }); @@ -396,7 +396,7 @@ describe('Token', () => { }) ).sign([zkAppBKey, feePayerKey, tokenZkappKey]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow(); + await expect(tx.send()).rejects.toThrow(); }); test('should error if sender sends more tokens than they have', async () => { @@ -420,7 +420,7 @@ describe('Token', () => { tokenZkapp.requireSignature(); }) ).sign([zkAppBKey, feePayerKey, tokenZkappKey]); - await expect(tx.sendOrThrowIfError()).rejects.toThrow(); + await expect(tx.send()).rejects.toThrow(); }); }); }); @@ -581,9 +581,9 @@ describe('Token', () => { }); AccountUpdate.attachToTransaction(tokenZkapp.self); }); - await expect( - tx.sign([feePayerKey]).sendOrThrowIfError() - ).rejects.toThrow(/Update_not_permitted_access/); + await expect(tx.sign([feePayerKey]).send()).rejects.toThrow( + /Update_not_permitted_access/ + ); }); }); }); diff --git a/src/tests/transaction-flow.ts b/src/tests/transaction-flow.ts index 438c6f8fcb..4aafeebd1d 100644 --- a/src/tests/transaction-flow.ts +++ b/src/tests/transaction-flow.ts @@ -108,7 +108,7 @@ async function sendAndVerifyTransaction( ) { await transaction.prove(); if (throwOnFail) { - const pendingTransaction = await transaction.sendOrThrowIfError(); + const pendingTransaction = await transaction.send(); return await pendingTransaction.waitOrThrowIfError(); } else { const pendingTransaction = await transaction.send(); From 9ae5d8ae7508ab26a98dddcaff3ec01d531e412f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 09:31:27 -0800 Subject: [PATCH 02/12] refactor(transaction): replace waitOrThrowIfError() with wait() and safeWait() to simplify error handling --- src/lib/mina.ts | 85 +++++++++++++++----------------- src/lib/mina/local-blockchain.ts | 54 ++++++++++---------- src/lib/mina/transaction.ts | 24 +++------ 3 files changed, 76 insertions(+), 87 deletions(-) diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 309fc42e10..81e022a265 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -40,7 +40,8 @@ import { createTransaction, newTransaction, transaction, - createIncludedOrRejectedTransaction, + createRejectedTransaction, + createIncludedTransaction, } from './mina/transaction.js'; import { reportGetAccountError, @@ -272,18 +273,16 @@ function Network( const isSuccess = errors.length === 0; const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); - const pendingTransaction: Omit< - PendingTransaction, - 'wait' | 'waitOrThrowIfError' - > = { - isSuccess, - data: response?.data, - errors, - transaction: txn.transaction, - hash, - toJSON: txn.toJSON, - toPretty: txn.toPretty, - }; + const pendingTransaction: Omit = + { + isSuccess, + data: response?.data, + errors, + transaction: txn.transaction, + hash, + toJSON: txn.toJSON, + toPretty: txn.toPretty, + }; const pollTransactionStatus = async ( transactionHash: string, @@ -295,7 +294,7 @@ function Network( try { res = await Fetch.checkZkappTransaction(transactionHash); if (res.success) { - return createIncludedOrRejectedTransaction(pendingTransaction, []); + return createIncludedTransaction(pendingTransaction); } else if (res.failureReason) { const error = invalidTransactionError( txn.transaction, @@ -305,18 +304,16 @@ function Network( defaultNetworkConstants.accountCreationFee.toString(), } ); - return createIncludedOrRejectedTransaction(pendingTransaction, [ - error, - ]); + return createRejectedTransaction(pendingTransaction, [error]); } } catch (error) { - return createIncludedOrRejectedTransaction(pendingTransaction, [ + return createRejectedTransaction(pendingTransaction, [ (error as Error).message, ]); } if (maxAttempts && attempts >= maxAttempts) { - return createIncludedOrRejectedTransaction(pendingTransaction, [ + return createRejectedTransaction(pendingTransaction, [ `Exceeded max attempts.\nTransactionId: ${transactionHash}\nAttempts: ${attempts}\nLast received status: ${res}`, ]); } @@ -330,34 +327,21 @@ function Network( ); }; - const wait = async (options?: { - maxAttempts?: number; - interval?: number; - }): Promise => { - if (!isSuccess) { - return createIncludedOrRejectedTransaction( - pendingTransaction, - pendingTransaction.errors - ); - } - - // 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 maxAttempts = options?.maxAttempts ?? 45; - const interval = options?.interval ?? 20000; - return pollTransactionStatus( - pendingTransaction.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 waitOrThrowIfError = async (options?: { + const wait = async (options?: { maxAttempts?: number; interval?: number; - }): Promise => { - const pendingTransaction = await wait(options); + }): Promise => { + const pendingTransaction = await safeWait(options); if (pendingTransaction.status === 'rejected') { throw Error( `Transaction failed with errors:\n${pendingTransaction.errors.join( @@ -368,10 +352,23 @@ function Network( return pendingTransaction; }; + const safeWait = async (options?: { + maxAttempts?: number; + interval?: number; + }): Promise => { + if (!isSuccess) { + return createRejectedTransaction( + pendingTransaction, + pendingTransaction.errors + ); + } + return await poll(options?.maxAttempts, options?.interval); + }; + return { ...pendingTransaction, wait, - waitOrThrowIfError, + safeWait, }; }, async transaction(sender: DeprecatedFeePayerSpec, f: () => void) { diff --git a/src/lib/mina/local-blockchain.ts b/src/lib/mina/local-blockchain.ts index 852abf34ae..2d5bd11cb1 100644 --- a/src/lib/mina/local-blockchain.ts +++ b/src/lib/mina/local-blockchain.ts @@ -18,8 +18,11 @@ import { invalidTransactionError } from './errors.js'; import { Transaction, PendingTransaction, - createIncludedOrRejectedTransaction, createTransaction, + createIncludedTransaction, + createRejectedTransaction, + IncludedTransaction, + RejectedTransaction, } from './transaction.js'; import { type DeprecatedFeePayerSpec, @@ -257,33 +260,21 @@ function LocalBlockchain({ }); const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); - const pendingTransaction: Omit< - PendingTransaction, - 'wait' | 'waitOrThrowIfError' - > = { - isSuccess, - errors, - transaction: txn.transaction, - hash, - toJSON: txn.toJSON, - toPretty: txn.toPretty, - }; + const pendingTransaction: Omit = + { + isSuccess, + errors, + transaction: txn.transaction, + hash, + toJSON: txn.toJSON, + toPretty: txn.toPretty, + }; const wait = async (_options?: { maxAttempts?: number; interval?: number; - }) => { - return createIncludedOrRejectedTransaction( - pendingTransaction, - pendingTransaction.errors - ); - }; - - const waitOrThrowIfError = async (_options?: { - maxAttempts?: number; - interval?: number; - }) => { - const pendingTransaction = await wait(_options); + }): Promise => { + const pendingTransaction = await safeWait(_options); if (pendingTransaction.status === 'rejected') { throw Error( `Transaction failed with errors:\n${pendingTransaction.errors.join( @@ -294,10 +285,23 @@ function LocalBlockchain({ return pendingTransaction; }; + const safeWait = async (_options?: { + maxAttempts?: number; + interval?: number; + }): Promise => { + if (!isSuccess) { + return createRejectedTransaction( + pendingTransaction, + pendingTransaction.errors + ); + } + return createIncludedTransaction(pendingTransaction); + }; + return { ...pendingTransaction, wait, - waitOrThrowIfError, + safeWait, }; }, async transaction(sender: DeprecatedFeePayerSpec, f: () => void) { diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 66c61d3318..d3159c8f01 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -36,7 +36,8 @@ export { newTransaction, getAccount, transaction, - createIncludedOrRejectedTransaction, + createRejectedTransaction, + createIncludedTransaction, }; /** @@ -160,7 +161,7 @@ type PendingTransaction = Pick< wait(options?: { maxAttempts?: number; interval?: number; - }): Promise; + }): Promise; /** * Similar to `wait`, but throws an error if the transaction is rejected or if it fails to finalize within the given attempts. @@ -178,7 +179,7 @@ type PendingTransaction = Pick< * } * ``` */ - waitOrThrowIfError(options?: { + safeWait(options?: { maxAttempts?: number; interval?: number; }): Promise; @@ -504,7 +505,7 @@ function createRejectedTransaction( toJSON, toPretty, hash, - }: Omit, + }: Omit, errors: string[] ): RejectedTransaction { return { @@ -524,10 +525,7 @@ function createIncludedTransaction({ toJSON, toPretty, hash, -}: Omit< - PendingTransaction, - 'wait' | 'waitOrThrowIfError' ->): IncludedTransaction { +}: Omit): IncludedTransaction { return { status: 'included', transaction, @@ -537,13 +535,3 @@ function createIncludedTransaction({ data, }; } - -function createIncludedOrRejectedTransaction( - transaction: Omit, - errors: string[] -): IncludedTransaction | RejectedTransaction { - if (errors.length > 0) { - return createRejectedTransaction(transaction, errors); - } - return createIncludedTransaction(transaction); -} From 9f666876026690a43b88afa6ff85bc12b75fc418 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 09:36:46 -0800 Subject: [PATCH 03/12] refactor(transaction-flow.ts): replace waitOrThrowIfError() with wait() --- src/tests/transaction-flow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/transaction-flow.ts b/src/tests/transaction-flow.ts index 4aafeebd1d..401989c602 100644 --- a/src/tests/transaction-flow.ts +++ b/src/tests/transaction-flow.ts @@ -109,10 +109,10 @@ async function sendAndVerifyTransaction( await transaction.prove(); if (throwOnFail) { const pendingTransaction = await transaction.send(); - return await pendingTransaction.waitOrThrowIfError(); + return await pendingTransaction.wait(); } else { const pendingTransaction = await transaction.send(); - return await pendingTransaction.wait(); + return await pendingTransaction.safeWait(); } } From 24b29583b99f87ab662f835f5af9739d9b7c042f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 09:51:26 -0800 Subject: [PATCH 04/12] refactor(transaction.ts): rename sendSafe method to safeSend for better readability and consistency with other method names --- src/lib/mina/transaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index d3159c8f01..d441040aa5 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -112,7 +112,7 @@ type Transaction = { * } * ``` */ - sendSafe(): Promise; + safeSend(): Promise; }; /** @@ -422,7 +422,7 @@ function newTransaction(transaction: ZkappCommand, proofsEnabled?: boolean) { } return pendingTransaction; }, - async sendSafe() { + async safeSend() { const pendingTransaction = await sendTransaction(self); if (pendingTransaction.errors.length > 0) { return createRejectedTransaction( From 9082494413cc5dd67eb0c9093b80f89b374aa4bf Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 09:52:11 -0800 Subject: [PATCH 05/12] refactor(mina.ts, local-blockchain.ts, transaction.ts): replace isSuccess boolean with status enum for better clarity --- src/examples/zkapps/dex/run-live.ts | 2 +- src/lib/mina.ts | 15 +++++++++------ src/lib/mina/local-blockchain.ts | 9 +++++---- src/lib/mina/transaction.ts | 4 +++- src/tests/transaction-flow.ts | 6 ++++-- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/examples/zkapps/dex/run-live.ts b/src/examples/zkapps/dex/run-live.ts index 554b77b85b..80f915182c 100644 --- a/src/examples/zkapps/dex/run-live.ts +++ b/src/examples/zkapps/dex/run-live.ts @@ -286,7 +286,7 @@ async function ensureFundedAccount(privateKeyBase58: string) { } function logPendingTransaction(pendingTx: Mina.PendingTransaction) { - if (!pendingTx.isSuccess) throw Error('transaction failed'); + if (pendingTx.status === 'rejected') throw Error('transaction failed'); console.log( 'tx sent: ' + (useCustomLocalNetwork diff --git a/src/lib/mina.ts b/src/lib/mina.ts index 81e022a265..48b2d6698c 100644 --- a/src/lib/mina.ts +++ b/src/lib/mina.ts @@ -37,6 +37,7 @@ import { type PendingTransaction, type IncludedTransaction, type RejectedTransaction, + type PendingTransactionStatus, createTransaction, newTransaction, transaction, @@ -57,9 +58,10 @@ export { Network, currentTransaction, Transaction, - PendingTransaction, - IncludedTransaction, - RejectedTransaction, + type PendingTransaction, + type IncludedTransaction, + type RejectedTransaction, + type PendingTransactionStatus, activeInstance, setActiveInstance, transaction, @@ -271,11 +273,12 @@ function Network( response?.errors.forEach((e: any) => errors.push(JSON.stringify(e))); } - const isSuccess = errors.length === 0; + const status: PendingTransactionStatus = + errors.length === 0 ? 'pending' : 'rejected'; const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); const pendingTransaction: Omit = { - isSuccess, + status, data: response?.data, errors, transaction: txn.transaction, @@ -356,7 +359,7 @@ function Network( maxAttempts?: number; interval?: number; }): Promise => { - if (!isSuccess) { + if (status === 'rejected') { return createRejectedTransaction( pendingTransaction, pendingTransaction.errors diff --git a/src/lib/mina/local-blockchain.ts b/src/lib/mina/local-blockchain.ts index 2d5bd11cb1..afcac1b07f 100644 --- a/src/lib/mina/local-blockchain.ts +++ b/src/lib/mina/local-blockchain.ts @@ -23,6 +23,7 @@ import { createRejectedTransaction, IncludedTransaction, RejectedTransaction, + PendingTransactionStatus, } from './transaction.js'; import { type DeprecatedFeePayerSpec, @@ -174,7 +175,7 @@ function LocalBlockchain({ } } - let isSuccess = true; + let status: PendingTransactionStatus = 'pending'; const errors: string[] = []; try { ledger.applyJsonTransaction( @@ -183,7 +184,7 @@ function LocalBlockchain({ JSON.stringify(networkState) ); } catch (err: any) { - isSuccess = false; + status = 'rejected'; try { const errorMessages = JSON.parse(err.message); const formattedError = invalidTransactionError( @@ -262,7 +263,7 @@ function LocalBlockchain({ const hash = Test.transactionHash.hashZkAppCommand(txn.toJSON()); const pendingTransaction: Omit = { - isSuccess, + status, errors, transaction: txn.transaction, hash, @@ -289,7 +290,7 @@ function LocalBlockchain({ maxAttempts?: number; interval?: number; }): Promise => { - if (!isSuccess) { + if (status === 'rejected') { return createRejectedTransaction( pendingTransaction, pendingTransaction.errors diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index d441040aa5..3e82a9527f 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -31,6 +31,7 @@ export { type PendingTransaction, type IncludedTransaction, type RejectedTransaction, + type PendingTransactionStatus, createTransaction, sendTransaction, newTransaction, @@ -115,6 +116,7 @@ type Transaction = { safeSend(): Promise; }; +type PendingTransactionStatus = 'pending' | 'rejected'; /** * Represents a transaction that has been submitted to the blockchain but has not yet reached a final state. * The {@link PendingTransaction} type extends certain functionalities from the base {@link Transaction} type, @@ -144,7 +146,7 @@ type PendingTransaction = Pick< * } * ``` */ - isSuccess: boolean; + status: PendingTransactionStatus; /** * Waits for the transaction to be finalized and returns the result. diff --git a/src/tests/transaction-flow.ts b/src/tests/transaction-flow.ts index 401989c602..fd43e1dc73 100644 --- a/src/tests/transaction-flow.ts +++ b/src/tests/transaction-flow.ts @@ -111,8 +111,10 @@ async function sendAndVerifyTransaction( const pendingTransaction = await transaction.send(); return await pendingTransaction.wait(); } else { - const pendingTransaction = await transaction.send(); - return await pendingTransaction.safeWait(); + const pendingTransaction = await transaction.safeSend(); + if (pendingTransaction.status === 'pending') { + return await pendingTransaction.safeWait(); + } } } From 9ab75fb7a30494f615b3fe9aa851fa26d4da13dc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 09:53:33 -0800 Subject: [PATCH 06/12] feat(transaction-flow.ts): add error handling for failed transactions to improve robustness of the transaction flow test --- src/tests/transaction-flow.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/transaction-flow.ts b/src/tests/transaction-flow.ts index fd43e1dc73..90d59688a8 100644 --- a/src/tests/transaction-flow.ts +++ b/src/tests/transaction-flow.ts @@ -114,6 +114,8 @@ async function sendAndVerifyTransaction( const pendingTransaction = await transaction.safeSend(); if (pendingTransaction.status === 'pending') { return await pendingTransaction.safeWait(); + } else { + throw Error('Transaction failed'); } } } From e2300a4983273e904aae3083b4b8aea52cdff5ed Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 10:04:14 -0800 Subject: [PATCH 07/12] docs(transaction.ts): update comments to reflect changes in transaction status handling and error handling --- src/lib/mina/transaction.ts | 46 +++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index 3e82a9527f..e594ca9486 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -127,37 +127,43 @@ type PendingTransaction = Pick< 'transaction' | 'toJSON' | 'toPretty' > & { /** - * @property {boolean} isSuccess Indicates whether the transaction was successfully sent to the Mina daemon. - * It does not guarantee inclusion in a block. A value of `true` means the transaction was accepted by the Mina daemon for processing. - * However, the transaction may still be rejected later during the finalization process if it fails to be included in a block. - * Use `.wait()` or `.waitOrThrowIfError()` methods to determine the final state of the transaction. + * @property {PendingTransactionStatus} status The status of the transaction after being sent to the Mina daemon. + * This property indicates the transaction's initial processing status but does not guarantee its eventual inclusion in a block. + * A status of `pending` suggests the transaction was accepted by the Mina daemon for processing, + * whereas a status of `rejected` indicates that the transaction was not accepted. + * Use the {@link PendingTransaction.wait()} or {@link PendingTransaction.safeWait()} methods to track the transaction's progress towards finalization and to determine whether it's included in a block. * @example * ```ts - * if (pendingTransaction.isSuccess) { - * console.log('Transaction sent successfully to the Mina daemon.'); + * if (pendingTransaction.status === 'pending') { + * console.log('Transaction accepted for processing by the Mina daemon.'); * try { - * await pendingTransaction.waitOrThrowIfError(); - * console.log('Transaction was included in a block.'); + * await pendingTransaction.wait(); + * console.log('Transaction successfully included in a block.'); * } catch (error) { * console.error('Transaction was rejected or failed to be included in a block:', error); * } * } else { - * console.error('Failed to send transaction to the Mina daemon.'); + * console.error('Transaction was not accepted for processing by the Mina daemon.'); * } * ``` */ status: PendingTransactionStatus; /** - * Waits for the transaction to be finalized and returns the result. + * Waits for the transaction to be included in a block. This method polls the Mina daemon to check the transaction's status, and throws an error if the transaction is rejected. * @param {Object} [options] Configuration options for polling behavior. * @param {number} [options.maxAttempts] The maximum number of attempts to check the transaction status. * @param {number} [options.interval] The interval, in milliseconds, between status checks. - * @returns {Promise} A promise that resolves to the transaction's final state. + * @returns {Promise} A promise that resolves to the transaction's final state or throws an error. + * @throws {Error} If the transaction is rejected or fails to finalize within the given attempts. * @example * ```ts - * const transaction = await pendingTransaction.wait({ maxAttempts: 5, interval: 1000 }); - * console.log(transaction.status); // 'included' or 'rejected' + * try { + * const transaction = await pendingTransaction.waitOrThrowIfError({ maxAttempts: 10, interval: 2000 }); + * console.log('Transaction included in a block.'); + * } catch (error) { + * console.error('Transaction rejected or failed to finalize:', error); + * } * ``` */ wait(options?: { @@ -166,19 +172,15 @@ type PendingTransaction = Pick< }): Promise; /** - * Similar to `wait`, but throws an error if the transaction is rejected or if it fails to finalize within the given attempts. + * Waits for the transaction to be included in a block. This method polls the Mina daemon to check the transaction's status * @param {Object} [options] Configuration options for polling behavior. * @param {number} [options.maxAttempts] The maximum number of polling attempts. * @param {number} [options.interval] The time interval, in milliseconds, between each polling attempt. - * @returns {Promise} A promise that resolves to the transaction's final state or throws an error. + * @returns {Promise} A promise that resolves to the transaction's final state. * @example * ```ts - * try { - * const transaction = await pendingTransaction.waitOrThrowIfError({ maxAttempts: 10, interval: 2000 }); - * console.log('Transaction included in a block.'); - * } catch (error) { - * console.error('Transaction rejected or failed to finalize:', error); - * } + * const transaction = await pendingTransaction.wait({ maxAttempts: 5, interval: 1000 }); + * console.log(transaction.status); // 'included' or 'rejected' * ``` */ safeWait(options?: { @@ -210,7 +212,7 @@ type PendingTransaction = Pick< * @property {string[]} errors Descriptive error messages if the transaction encountered issues during processing. * @example * ```ts - * if (!pendingTransaction.isSuccess && pendingTransaction.errors.length > 0) { + * if (!pendingTransaction.status === 'rejected') { * console.error(`Transaction errors: ${pendingTransaction.errors.join(', ')}`); * } * ``` From 8d1554f37c1c6a172f91b3e1d63f32d5725874c6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 10:10:56 -0800 Subject: [PATCH 08/12] refactor(transaction.ts): improve error handling and documentation for transaction methods - Refactor send() method to throw an error when transaction submission fails, providing more explicit error handling. - Update send() method documentation to reflect changes and provide better usage examples. - Refactor safeSend() method to return a RejectedTransaction when internal errors are detected, providing more detailed feedback. - Update safeSend() method documentation to reflect changes and provide better usage examples. - Update wait() method usage examples in IncludedTransaction and RejectedTransaction to demonstrate error handling. --- src/lib/mina/transaction.ts | 52 +++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/lib/mina/transaction.ts b/src/lib/mina/transaction.ts index e594ca9486..5ce1dea2fd 100644 --- a/src/lib/mina/transaction.ts +++ b/src/lib/mina/transaction.ts @@ -90,26 +90,31 @@ type Transaction = { prove(): Promise<(Proof | undefined)[]>; /** * Submits the {@link Transaction} to the network. This method asynchronously sends the transaction - * for processing and returns a {@link PendingTransaction} instance, which can be used to monitor its progress. - * @returns A promise that resolves to a {@link PendingTransaction} instance representing the submitted 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. + * @throws An error if the transaction cannot be sent or processed by the network, containing details about the failure. * @example * ```ts - * const pendingTransaction = await transaction.send(); - * console.log('Transaction sent successfully to the Mina daemon.'); + * try { + * const pendingTransaction = await transaction.send(); + * console.log('Transaction sent successfully to the Mina daemon.'); + * } catch (error) { + * console.error('Failed to send transaction to the Mina daemon:', error); + * } * ``` */ send(): Promise; - /** - * Sends the {@link Transaction} to the network, unlike the standard send(), this function will throw an error if internal errors are detected. - * @throws {Error} If the transaction fails to be sent to the Mina daemon or if it encounters errors during processing. + * 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. * @example * ```ts - * try { - * const pendingTransaction = await transaction.send(); - * console.log('Transaction sent successfully to the Mina daemon.'); - * } catch (error) { - * console.error('Transaction failed with errors:', error); + * const result = await transaction.safeSend(); + * if (result.status === 'pending') { + * console.log('Transaction sent successfully to the Mina daemon.'); + * } else if (result.status === 'rejected') { + * console.error('Transaction failed with errors:', result.errors); * } * ``` */ @@ -231,9 +236,13 @@ type IncludedTransaction = Pick< * @property {string} status The final status of the transaction, indicating successful inclusion in a block. * @example * ```ts - * const includedTx: IncludedTransaction = await pendingTransaction.wait(); - * if (includedTx.status === 'included') { - * console.log(`Transaction ${includedTx.hash()} included in a block.`); + * try { + * const includedTx: IncludedTransaction = await pendingTransaction.wait(); + * // If wait() resolves, it means the transaction was successfully included. + * console.log(`Transaction ${includedTx.hash} included in a block.`); + * } catch (error) { + * // If wait() throws, the transaction was not included in a block. + * console.error('Transaction failed to be included in a block:', error); * } * ``` */ @@ -251,11 +260,14 @@ type RejectedTransaction = Pick< * @property {string} status The final status of the transaction, specifically indicating that it has been rejected. * @example * ```ts - * const rejectedTx: RejectedTransaction = await pendingTransaction.wait(); - * if (rejectedTx.status === 'rejected') { - * console.error(`Transaction ${rejectedTx.hash()} was rejected.`); - * rejectedTx.errors.forEach((error, i) => { - * console.error(`Error ${i + 1}: ${error}`); + * try { + * const txResult = await pendingTransaction.wait(); + * // This line will not execute if the transaction is rejected, as `.wait()` will throw an error instead. + * console.log(`Transaction ${txResult.hash} was successfully included in a block.`); + * } catch (error) { + * console.error(`Transaction ${error.transaction.hash} was rejected.`); + * error.errors.forEach((error, i) => { + * console.error(`Error ${i + 1}: ${error}`); * }); * } * ``` From 4ca39ab1d814b0c36b974891b98ce91b3dcc0fd6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 10:17:23 -0800 Subject: [PATCH 09/12] feat(CHANGELOG.md): introduce Transaction.safeSend() and PendingTransaction.safeWait() methods to handle transaction errors refactor(CHANGELOG.md): rename Transaction.isSuccess to Transaction.status for better transaction state representation --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9529faa796..a1a35be3e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Breaking changes +- Improved on parity fix of `Mina.Localblockchain` and`Mina.Network` to make `.send()` and `.wait()` throw an error by default if the transaction was not successful. https://github.com/o1-labs/o1js/pull/1480 + - Changed `Transaction.isSuccess` to `Transaction.status` to better represent the state of a transaction. + - `Transaction.safeSend()` and `PendingTransaction.safeWait()` are introduced to return a `IncludedTransaction` or `RejectedTransaction` object without throwing errors. - Fixed parity between `Mina.LocalBlockchain` and `Mina.Network` to have the same behaviors https://github.com/o1-labs/o1js/pull/1422 - Changed the `TransactionId` type to `Transaction`. Additionally added `PendingTransaction` and `RejectedTransaction` types to better represent the state of a transaction. - `transaction.send()` no longer throws an error if the transaction was not successful for `Mina.LocalBlockchain` and `Mina.Network`. Instead, it returns a `PendingTransaction` object that contains the error. Use `transaction.sendOrThrowIfError` to throw the error if the transaction was not successful. From 145a2386fd6e3bea6f214491aa02a204e965101e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 10:39:33 -0800 Subject: [PATCH 10/12] fix(run-live.ts): change condition to check transaction status instead of hash for better transaction tracking This change will allow us to track the transaction status more accurately and handle it accordingly. --- src/examples/zkapps/hello-world/run-live.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/zkapps/hello-world/run-live.ts b/src/examples/zkapps/hello-world/run-live.ts index 8fa166e632..dce5a63fc1 100644 --- a/src/examples/zkapps/hello-world/run-live.ts +++ b/src/examples/zkapps/hello-world/run-live.ts @@ -59,7 +59,7 @@ let transaction = await Mina.transaction( transaction.sign([senderKey, zkAppKey]); console.log('Sending the transaction.'); let pendingTx = await transaction.send(); -if (pendingTx.hash !== undefined) { +if (pendingTx.status === 'pending') { console.log(`Success! Deploy transaction sent. Your smart contract will be deployed as soon as the transaction is included in a block. @@ -77,7 +77,7 @@ transaction = await Mina.transaction({ sender, fee: transactionFee }, () => { await transaction.sign([senderKey]).prove(); console.log('Sending the transaction.'); pendingTx = await transaction.send(); -if (pendingTx.hash !== undefined) { +if (pendingTx.status === 'pending') { console.log(`Success! Update transaction sent. Your smart contract state will be updated as soon as the transaction is included in a block. From 8e920db34c2e12a85256bbbafd09568ba58df6e6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 11:02:08 -0800 Subject: [PATCH 11/12] fix(transaction-flow.ts): return pendingTransaction instead of throwing error to allow further processing of the transaction status --- src/tests/transaction-flow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/transaction-flow.ts b/src/tests/transaction-flow.ts index 90d59688a8..12a00b3bfa 100644 --- a/src/tests/transaction-flow.ts +++ b/src/tests/transaction-flow.ts @@ -115,7 +115,7 @@ async function sendAndVerifyTransaction( if (pendingTransaction.status === 'pending') { return await pendingTransaction.safeWait(); } else { - throw Error('Transaction failed'); + return pendingTransaction; } } } From 1ed69eb87ef881a60b28f1291e9df728172ecbe2 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 5 Mar 2024 12:47:12 -0800 Subject: [PATCH 12/12] docs(CHANGELOG.md): update breaking changes to collapse recent changes in transaction parity --- CHANGELOG.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a35be3e2..546e8141d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Breaking changes -- Improved on parity fix of `Mina.Localblockchain` and`Mina.Network` to make `.send()` and `.wait()` throw an error by default if the transaction was not successful. https://github.com/o1-labs/o1js/pull/1480 - - Changed `Transaction.isSuccess` to `Transaction.status` to better represent the state of a transaction. - - `Transaction.safeSend()` and `PendingTransaction.safeWait()` are introduced to return a `IncludedTransaction` or `RejectedTransaction` object without throwing errors. -- Fixed parity between `Mina.LocalBlockchain` and `Mina.Network` to have the same behaviors https://github.com/o1-labs/o1js/pull/1422 +- Fixed parity between `Mina.LocalBlockchain` and `Mina.Network` to have the same behaviors https://github.com/o1-labs/o1js/pull/1422 https://github.com/o1-labs/o1js/pull/1480 - Changed the `TransactionId` type to `Transaction`. Additionally added `PendingTransaction` and `RejectedTransaction` types to better represent the state of a transaction. - - `transaction.send()` no longer throws an error if the transaction was not successful for `Mina.LocalBlockchain` and `Mina.Network`. Instead, it returns a `PendingTransaction` object that contains the error. Use `transaction.sendOrThrowIfError` to throw the error if the transaction was not successful. - - `transaction.wait()` no longer throws an error if the transaction was not successful for `Mina.LocalBlockchain` and `Mina.Network`. Instead, it returns either a `IncludedTransaction` or `RejectedTransaction`. Use `transaction.waitOrThrowIfError` to throw the error if the transaction was not successful. + - `Transaction.safeSend()` and `PendingTransaction.safeWait()` are introduced to return a `IncludedTransaction` or `RejectedTransaction` object without throwing errors. + - `transaction.send()` throws an error if the transaction was not successful for both `Mina.LocalBlockchain` and `Mina.Network` and returns a `PendingTransaction` object if it was successful. Use `transaction.safeSend` to send a transaction that will not throw an error and either return a `PendingTransaction` or `RejectedTransaction`. + - `transaction.wait()` throws an error if the transaction was not successful for both `Mina.LocalBlockchain` and `Mina.Network` and returns a `IncludedTransaction` object if it was successful. Use `transaction.safeWait` to send a transaction that will not throw an error and either return a `IncludedTransaction` or `RejectedTransaction`. - `transaction.hash()` is no longer a function, it is now a property that returns the hash of the transaction. + - Changed `Transaction.isSuccess` to `Transaction.status` to better represent the state of a transaction. - Improved efficiency of computing `AccountUpdate.callData` by packing field elements into as few field elements as possible https://github.com/o1-labs/o1js/pull/1458 - This leads to a large reduction in the number of constraints used when inputs to a zkApp method are many field elements (e.g. a long list of `Bool`s) - Return events in the `LocalBlockchain` in reverse chronological order (latest events at the beginning) to match the behavior of the `Network` https://github.com/o1-labs/o1js/pull/1460