From 1ab201114cf90854c1dbc31318ef13a60b304628 Mon Sep 17 00:00:00 2001 From: hattyhattington17 <181872047+hattyhattington17@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:39:57 -0700 Subject: [PATCH 1/4] expose method to rebuild internal state from fetched actions on an OffchainStateInstance --- src/lib/mina/actions/offchain-state.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 6b091df29..3841c3f16 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -104,6 +104,11 @@ type OffchainStateInstance< * Commitments to the offchain state, to use in your onchain state. */ commitments(): State; + + /** + * Rebuilds the internal state map by fetching actions up to the current actionState in offchainStateCommitments + * */ + fetchInternalState(): void; }; type OffchainState = { @@ -470,6 +475,17 @@ function OffchainState< return result.proof; }, + async fetchInternalState() { + let actionState = await onchainActionState(); + let { merkleMap, valueMap } = await fetchMerkleMap( + height, + internal.contract, + actionState + ); + internal.merkleMap = merkleMap; + internal.valueMap = valueMap; + }, + async settle( proof: Proof ) { From 80d1f3c1fc12faaeede607e78757780efaf42bf8 Mon Sep 17 00:00:00 2001 From: hattyhattington17 <181872047+hattyhattington17@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:21:02 -0700 Subject: [PATCH 2/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd4c5c94..a2d3ccf02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Method for optional types to assert none https://github.com/o1-labs/o1js/pull/1922 - Increased maximum supported amount of methods in a `SmartContract` or `ZkProgram` to 30. https://github.com/o1-labs/o1js/pull/1918 - Expose low-level conversion methods `Proof.{_proofToBase64,_proofFromBase64}` https://github.com/o1-labs/o1js/pull/1928 +- `fetchInternalState` method on `OffchainStateInstance` which fetches settled actions from the archive node and rebuilds the internal state trees. https://github.com/o1-labs/o1js/pull/1953 ### Fixed From 438536d703ed545f336dc9d7edb05353ea31a70f Mon Sep 17 00:00:00 2001 From: hattyhattington17 <181872047+hattyhattington17@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:37:21 -0700 Subject: [PATCH 3/4] fix type on fetchInternalState --- src/lib/mina/actions/offchain-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/mina/actions/offchain-state.ts b/src/lib/mina/actions/offchain-state.ts index 3841c3f16..5ccaefe15 100644 --- a/src/lib/mina/actions/offchain-state.ts +++ b/src/lib/mina/actions/offchain-state.ts @@ -108,7 +108,7 @@ type OffchainStateInstance< /** * Rebuilds the internal state map by fetching actions up to the current actionState in offchainStateCommitments * */ - fetchInternalState(): void; + fetchInternalState(): Promise; }; type OffchainState = { From f4fbc4c307460d1ca302bb8c28d2a342d2e363fb Mon Sep 17 00:00:00 2001 From: hattyhattington17 <181872047+hattyhattington17@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:25:48 -0700 Subject: [PATCH 4/4] save progress, demo lightnet fetch error --- package.json | 4 + run-ci-live-tests.sh | 9 ++ .../LilyPadContract.ts | 41 ++++++ .../actionStateErrorDemo.ts | 135 ++++++++++++++++++ .../fetch-state-process.ts | 54 +++++++ .../multi-thread-lightnet.ts | 98 +++++++++++++ .../settling-process.ts | 112 +++++++++++++++ src/lib/mina/fetch.ts | 3 + src/lib/mina/state.ts | 9 +- 9 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 src/lib/mina/actions/offchain-contract-tests/LilyPadContract.ts create mode 100644 src/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.ts create mode 100644 src/lib/mina/actions/offchain-contract-tests/fetch-state-process.ts create mode 100644 src/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.ts create mode 100644 src/lib/mina/actions/offchain-contract-tests/settling-process.ts diff --git a/package.json b/package.json index 617bd77c7..8e8addd43 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,10 @@ "node": ">=18.14.0" }, "scripts": { + "runner": "npm run build && node /Users/hattington/code/o1labs/o1js/dist/node/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.js", + "demoSettle": "npm run build && node dist/node/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.js", + "demoFetch": "npm run build && node dist/node/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.js", + "dev": "npx tsc -p tsconfig.test.json && node src/build/copy-to-dist.js", "build": "node src/build/copy-artifacts.js && rimraf ./dist/node && npm run dev && node src/build/build-node.js", "build:bindings": "./src/bindings/scripts/build-o1js-node.sh", diff --git a/run-ci-live-tests.sh b/run-ci-live-tests.sh index d49f1dbc6..669af6f49 100755 --- a/run-ci-live-tests.sh +++ b/run-ci-live-tests.sh @@ -23,6 +23,8 @@ DEX_PROC=$! FETCH_PROC=$! ./run src/tests/transaction-flow.ts --bundle | add_prefix "TRANSACTION_FLOW" & TRANSACTION_FLOW_PROC=$! +./run src/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.ts --bundle | add_prefix "OFFCHAIN_STATE_PROC" & +OFFCHAIN_STATE_PROC=$! # Wait for each process and capture their exit statuses FAILURE=0 @@ -61,6 +63,13 @@ if [ $? -ne 0 ]; then echo "" FAILURE=1 fi +wait $OFFCHAIN_STATE_PROC +if [ $? -ne 0 ]; then + echo "" + echo "OFFCHAIN_STATE test failed." + echo "" + FAILURE=1 +fi # Exit with failure if any process failed if [ $FAILURE -ne 0 ]; then diff --git a/src/lib/mina/actions/offchain-contract-tests/LilyPadContract.ts b/src/lib/mina/actions/offchain-contract-tests/LilyPadContract.ts new file mode 100644 index 000000000..0492f1064 --- /dev/null +++ b/src/lib/mina/actions/offchain-contract-tests/LilyPadContract.ts @@ -0,0 +1,41 @@ +// offchain state object, this is where the data is actually exposed +import { + AccountUpdate, + Experimental, + fetchAccount, + Lightnet, + method, + Mina, + PrivateKey, + PublicKey, + SmartContract, + state, +} from 'o1js'; +import OffchainState = Experimental.OffchainState; + +export const LilypadState = OffchainState( + { currentOccupant: OffchainState.Field(PublicKey) }, + { logTotalCapacity: 4, maxActionsPerUpdate: 2, maxActionsPerProof: 2 } +); +export class LilyPadStateProof extends LilypadState.Proof {} + +export class OffchainStorageLilyPad extends SmartContract { + @state(Experimental.OffchainState.Commitments) + offchainStateCommitments = LilypadState.emptyCommitments(); + offchainState = LilypadState.init(this); + + @method + async visit() { + const senderPublicKey = this.sender.getAndRequireSignature(); + const currentOccupantOption = await this.offchainState.fields.currentOccupant.get(); + this.offchainState.fields.currentOccupant.update({ + from: currentOccupantOption, + to: senderPublicKey, + }); + } + + @method + async settle(proof: LilyPadStateProof) { + await this.offchainState.settle(proof); + } +} diff --git a/src/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.ts b/src/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.ts new file mode 100644 index 000000000..88fdcba15 --- /dev/null +++ b/src/lib/mina/actions/offchain-contract-tests/actionStateErrorDemo.ts @@ -0,0 +1,135 @@ +// offchain state object, this is where the data is actually exposed +import { + AccountUpdate, + fetchAccount, + Lightnet, + Mina, + PrivateKey, + PublicKey, +} from 'o1js'; + import {LilypadState, OffchainStorageLilyPad} from "./LilyPadContract.js"; + +/** +* Set this address before running the fetch state process + * */ +let deployedZkAppAddress = ''; + +// configure lightnet and retrieve keys +Mina.setActiveInstance( + Mina.Network({ + mina: 'http://127.0.0.1:8080/graphql', + archive: 'http://127.0.0.1:8282', + lightnetAccountManager: 'http://127.0.0.1:8181', + }) +); +const senderPrivateKey = (await Lightnet.acquireKeyPair()).privateKey; + +// compile zkprograms +await LilypadState.compile(); +await OffchainStorageLilyPad.compile(); + +let zkApp: OffchainStorageLilyPad; +if (!deployedZkAppAddress) { + // deploy OffchainStorageLilyPad if it is not already deployed + const zkAppPrivateKey: PrivateKey = PrivateKey.random(); + zkApp = new OffchainStorageLilyPad(zkAppPrivateKey.toPublicKey()); + const deployTx = await Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + AccountUpdate.fundNewAccount(senderPrivateKey.toPublicKey()); + await zkApp.deploy(); + } + ); + await deployTx.prove(); + const deployTxPromise = await deployTx + .sign([senderPrivateKey, zkAppPrivateKey]) + .send(); + await deployTxPromise.wait(); + console.log( + `Deployed OffchainStorageLilyPad to address ${zkAppPrivateKey + .toPublicKey() + .toBase58()}` + ); +} else { + // init OffchainStorageLilyPad at deployedZkAppAddress + zkApp = new OffchainStorageLilyPad( + PublicKey.fromBase58(deployedZkAppAddress) + ); + await fetchAccount({ publicKey: zkApp.address }); + console.log( + `Interacting with deployed OffchainStorageLilyPad at address ${deployedZkAppAddress}` + ); +} + +zkApp.offchainState.setContractInstance(zkApp); +console.log( + 'fetchAccount', + ( + await fetchAccount({ publicKey: zkApp.address }) + )?.account?.publicKey.toBase58() +); +console.log('OffchainStorageLilyPad starting state:'); +await logAppState(); +// stop settle process here, copy address from logs into deployedZkAppAddress, trigger fetch process and allow to run up to this point +// after fetch process hits this point, execute settle process to run state updates and settlement +// after state updates and settlement, execute fetch process +await zkApp.offchainState.fetchInternalState(); +await logAppState(); + +// call visit on the contract which will dispatch state updates but will not directly update the OffchainStateInstance +await (await visit(senderPrivateKey)).wait(); +console.log('Executed visits, app state should be unchanged: '); + +// Create a settlement proof +console.log('\nSettling visits on chain'); +const settlementProof = await zkApp.offchainState.createSettlementProof(); +// await logAppState(); // todo: logging the state here gives a root mismatch error because the internal state map is updated by createSettlementProof before the on chain value is changed +const settleTx = await Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + await zkApp.settle(settlementProof); + } +); +await settleTx.prove(); +const settleTxPromise = await settleTx.sign([senderPrivateKey]).send(); +await settleTxPromise.wait(); + +console.log( + 'Executed OffchainStorageLilyPad.settle(), on chain state has been updated with the effects of the dispatched visits: ' +); + +// must call fetchAccount after executing settle transaction in order to retrieve the most up to date on chain commitments +await logAppState(); + + +/************************************************************************** + * Helpers + ***************************************************************************/ +async function visit(sender: PrivateKey) { + const tx = await Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + await zkApp.visit(); + } + ); + await tx.prove(); + const txPromise = await tx.sign([sender]).send(); + + console.log( + `${sender.toPublicKey().toBase58()} called OffchainStorageLilyPad.visit()` + ); + return txPromise; +} + +async function logAppState() { + await fetchAccount({ publicKey: zkApp.address }); + const onchainStateCommitment = zkApp.offchainStateCommitments.get(); + + console.log( + `${process.pid} onchainStateCommitment.root=${onchainStateCommitment.root.toString()} ` + ); + + const currentOccupant = + await zkApp.offchainState.fields.currentOccupant.get(); + console.log(`currentOccupant: ${currentOccupant.value.toBase58()}`); +} diff --git a/src/lib/mina/actions/offchain-contract-tests/fetch-state-process.ts b/src/lib/mina/actions/offchain-contract-tests/fetch-state-process.ts new file mode 100644 index 000000000..76c0da44a --- /dev/null +++ b/src/lib/mina/actions/offchain-contract-tests/fetch-state-process.ts @@ -0,0 +1,54 @@ + import { Message } from './multi-thread-lightnet.js'; +import { fetchAccount, Mina, PublicKey } from 'o1js'; + import {LilypadState, OffchainStorageLilyPad} from "./LilyPadContract.js"; + +process.send?.(`Starting fetch state process: ${process.pid}`); +/** + * Configure lightnet and retrieve signing keys + * */ +const network = Mina.Network({ + mina: 'http://localhost:8080/graphql', + archive: 'http://127.0.0.1:8282', + lightnetAccountManager: 'http://localhost:8181', +}); +Mina.setActiveInstance(network); +let zkApp: OffchainStorageLilyPad; + +await LilypadState.compile(); +await OffchainStorageLilyPad.compile(); +// notify main process that this process is ready to begin work +process.send?.({ type: 'READY' }); + +process.on('message', async (msg: Message) => { + console.log( + `Fetch state process received message from root: ${JSON.stringify(msg)}` + ); + + /** + * Compile offchain state zkprogram and contract, deploy contract + * */ + if (msg.type === 'DEPLOYED') { + // account = PublicKey.fromBase58(msg.account); + zkApp = new OffchainStorageLilyPad(PublicKey.fromBase58(msg.address)); + zkApp.offchainState.setContractInstance(zkApp); + await logState(); + process.send?.({ type: 'INSTANTIATED' }); + } else if (msg.type === 'FETCH_STATE') { + // todo: expect root mismatch + await logState(); + + // todo: this is erroring on the other test + await zkApp.offchainState.fetchInternalState(); + await logState(); + } +}); + +async function logState() { + await fetchAccount({ publicKey: zkApp.address }); + const root = zkApp.offchainStateCommitments.get().root.toString(); + const currentOccupant = ( + await zkApp.offchainState.fields.currentOccupant.get() + ).value.toString(); + const message = `offchainState: (currentOccupant => ${currentOccupant}),(root => ${root})`; + process.send?.(message); +} diff --git a/src/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.ts b/src/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.ts new file mode 100644 index 000000000..657c9bb9a --- /dev/null +++ b/src/lib/mina/actions/offchain-contract-tests/multi-thread-lightnet.ts @@ -0,0 +1,98 @@ +import {fork} from 'child_process'; + +/** + * Multi process offchain state synchronization test + * + * Launch two processes - a settling process and a fetch state process + * Settling process deploys the contract + * Offchain state process instantiates the contract at the deployed address + * Settling process updates the state and calls settle + * Offchain state process reads the state and gets a root mismatch error + * Offchain state process calls fetchInternalState, reads the state, and sees the updated state + * */ + +export type Message = + | { type: 'READY' } + | { type: 'DEPLOY' } + | { type: 'DEPLOYED'; address: string; account: string } + | { type: 'INSTANTIATED' } + | { type: 'UPDATE_STATE' } + | { type: 'STATE_UPDATED' } + | { type: 'FETCH_STATE' }; + +let settlingReady = false; +let fetchStateReady = false; + +// Launch the processes +const settlingProcess = fork( + 'dist/node/lib/mina/actions/offchain-contract-tests/settling-process.js' +); +const fetchStateProcess = fork( + 'dist/node/lib/mina/actions/offchain-contract-tests/fetch-state-process.js' +); + +// Listen for messages from the child processes +settlingProcess.on('message', (msg: Message) => { + console.log( + `Settling process dispatched message to root: ${JSON.stringify(msg)}` + ); + + if (msg.type === 'READY') { + settlingReady = true; + // both processes are ready, dispatch DEPLOY to settling process to deploy contract + if (settlingReady && fetchStateReady) { + console.log('Both processes are ready. Starting the test...'); + settlingProcess.send({ type: 'DEPLOY' }); + } + } else if (msg.type === 'DEPLOYED') { + // settling process finished deploying contract, tell fetchState process to instantiate the contract and offchain state + fetchStateProcess.send({ + type: 'DEPLOYED', + address: msg.address, + account: msg.account, + }); + } else if (msg.type === 'STATE_UPDATED') { + // settle state process has updated and settled state, tell fetch state process to try to retrieve the new state which lets us test synchronization + fetchStateProcess.send({ type: 'FETCH_STATE' }); + } +}); +fetchStateProcess.on('message', (msg: Message) => { + console.log( + `Fetch state process dispatched message to root: ${JSON.stringify(msg)}` + ); + + if (msg.type === 'READY') { + fetchStateReady = true; + // both processes are ready, dispatch DEPLOY to settling process to deploy contract + if (settlingReady && fetchStateReady) { + console.log('Both processes are ready. Starting the test...'); + settlingProcess.send({ type: 'DEPLOY' }); + } + } else if (msg.type === 'INSTANTIATED') { + // fetch state process instantiated contract, tell settle state process to update the state and settle it + settlingProcess.send({ type: 'UPDATE_STATE' }); + } +}); + +function cleanup() { + settlingProcess.kill(); + fetchStateProcess.kill(); + console.log('Child processes terminated.'); +} + +function handleProcessEvents(processName: string, processInstance: any) { + processInstance.on('error', (err: Error) => { + console.error(`${processName} threw an error: ${err.message}`); + }); + + processInstance.on('exit', (code: number) => { + if (code !== 0) { + console.error(`${processName} exited with code ${code}`); + } else { + console.log(`${processName} exited successfully.`); + } + }); +} + +handleProcessEvents('Settling process', settlingProcess); +handleProcessEvents('Fetch state process', fetchStateProcess); diff --git a/src/lib/mina/actions/offchain-contract-tests/settling-process.ts b/src/lib/mina/actions/offchain-contract-tests/settling-process.ts new file mode 100644 index 000000000..c98da38cc --- /dev/null +++ b/src/lib/mina/actions/offchain-contract-tests/settling-process.ts @@ -0,0 +1,112 @@ + import { Message } from './multi-thread-lightnet.js'; +import { fetchAccount, Lightnet, Mina, UInt64 } from 'o1js'; + import {LilypadState, OffchainStorageLilyPad} from "./LilyPadContract.js"; + + + +process.send?.(`Starting settling process: ${process.pid}`); +/** + * Configure lightnet and retrieve signing keys + * */ +const network = Mina.Network({ + mina: 'http://localhost:8080/graphql', + archive: 'http://127.0.0.1:8282', + lightnetAccountManager: 'http://localhost:8181', +}); +Mina.setActiveInstance(network); +const senderPrivateKey = (await Lightnet.acquireKeyPair()).privateKey; +const zkAppPrivateKey = (await Lightnet.acquireKeyPair()).privateKey; + +const zkApp = new OffchainStorageLilyPad(zkAppPrivateKey.toPublicKey()); +zkApp.offchainState.setContractInstance(zkApp); + +await LilypadState.compile(); +await OffchainStorageLilyPad.compile(); + +// notify main process that this process is ready to begin work +process.send?.({ type: 'READY' }); + +process.on('message', async (msg: Message) => { + console.log( + `Settling process received message from root: ${JSON.stringify(msg)}` + ); + /** + * Compile offchain state zkprogram and contract, deploy contract + * */ + if (msg.type === 'DEPLOY') { + const deployTx = await Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + await zkApp.deploy(); + } + ); + await deployTx.prove(); + const deployTxPromise = await deployTx + .sign([senderPrivateKey, zkAppPrivateKey]) + .send(); + await deployTxPromise.wait(); + await logState(); + + + // let's add a state update since it seems like the map being empty is causing a new map to be loaded on all gets + const accountCreationTx = Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + await zkApp.visit( ); + } + ); + await accountCreationTx.sign([senderPrivateKey]); + await accountCreationTx.prove(); + await accountCreationTx.send().wait(); + process.send?.("Ran initial account update"); + + process.send?.({ + type: 'DEPLOYED', + address: zkApp.address.toBase58(), + account: senderPrivateKey.toPublicKey().toBase58(), + }); + } else if (msg.type === 'UPDATE_STATE') { + /** + * Modify zkapp offchain state by creating an account + * Settle state + * */ + const accountCreationTx = Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + await zkApp.visit(); + } + ); + await accountCreationTx.sign([senderPrivateKey]); + await accountCreationTx.prove(); + await accountCreationTx.send().wait(); + process.send?.("Updated state without settling"); + // todo: verify state = 0 + await logState(); + + process.send?.("Settling on chain state"); + const settlementProof = await zkApp.offchainState.createSettlementProof(); + // todo: logging the state here gives a root mismatch error because the internal state map is updated by createSettlementProof before the on chain value is changed + const settleTx = await Mina.transaction( + { fee: 1e9, sender: senderPrivateKey.toPublicKey() }, + async () => { + // update the offchain state and on chain commitment + await zkApp.settle(settlementProof); + } + ); + await settleTx.prove(); + const settleTxPromise = await settleTx.sign([senderPrivateKey]).send(); + await settleTxPromise.wait(); + // todo: verify state = 10000 + await logState(); + + process.send?.({ type: 'STATE_UPDATED' }); + } +}); + +async function logState() { + await fetchAccount({ publicKey: zkApp.address }); + const root = zkApp.offchainStateCommitments.get().root.toString(); + const currentOccupant = (await zkApp.offchainState.fields.currentOccupant.get()).value.toString(); + const message = `offchainState: (currentOccupant => ${currentOccupant}), (root => ${root})`; + process.send?.(message); +} diff --git a/src/lib/mina/fetch.ts b/src/lib/mina/fetch.ts index f2269001f..548cecdff 100644 --- a/src/lib/mina/fetch.ts +++ b/src/lib/mina/fetch.ts @@ -1069,6 +1069,7 @@ async function makeGraphqlRequest( try { return await makeRequest(url1); } catch (error) { + console.log("Caught error in makeRequest: ", JSON.stringify(error)); return [undefined, inferError(error)] as [undefined, FetchError]; } } @@ -1076,6 +1077,8 @@ async function makeGraphqlRequest( return await Promise.race([makeRequest(url1), makeRequest(url2)]); } catch (unknownError) { let error = inferError(unknownError); + console.log("Caught error in makeRequest 2: ", JSON.stringify(error)); + if (error.statusCode === 408) { // If the request timed out, try the next 2 endpoints timeoutErrors.push({ url1, url2, error }); diff --git a/src/lib/mina/state.ts b/src/lib/mina/state.ts index cfa23b605..87ac4bd6b 100644 --- a/src/lib/mina/state.ts +++ b/src/lib/mina/state.ts @@ -364,13 +364,20 @@ function createState(defaultValue?: T): InternalStateType { let address: PublicKey = this._contract.instance.address; let tokenId: Field = this._contract.instance.tokenId; let account: Account | undefined; + let error: { + statusCode: number; + statusText: string; + } | undefined; if (networkConfig.minaEndpoint === '') { account = Mina.getAccount(address, tokenId); } else { - ({ account } = await fetchAccount({ + ({ account, error } = await fetchAccount({ publicKey: address, tokenId: TokenId.toBase58(tokenId), })); + if (error !== undefined) { + console.log(`Error fetching account in state.fetch: ${JSON.stringify(error)}`); + } } if (account === undefined) return undefined;