From ebfc261fb7e7595c29190f41b705708038a8a347 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Mon, 4 Dec 2023 23:51:17 -0800 Subject: [PATCH] feat!: dont batch queries --- packages/rpc/package.json | 3 + packages/rpc/src/batchQuery.ts | 78 ------ packages/rpc/src/chainStorageWatcher.ts | 185 ++++++------- packages/rpc/src/vstorageQuery.ts | 72 +++++ packages/rpc/test/chainStorageWatcher.test.ts | 257 +++++++++++++----- .../src/wallet-connection/watchWallet.js | 21 +- yarn.lock | 34 +++ 7 files changed, 386 insertions(+), 264 deletions(-) delete mode 100644 packages/rpc/src/batchQuery.ts create mode 100644 packages/rpc/src/vstorageQuery.ts diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 3e7aa5f..c090787 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -17,6 +17,8 @@ }, "dependencies": { "@endo/marshal": "^0.8.9", + "axios": "^1.6.2", + "axios-retry": "^4.0.0", "vite": "^4.3.2", "vite-tsconfig-paths": "^4.2.0" }, @@ -24,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/parser": "^5.35.1", "@vitest/coverage-c8": "^0.25.3", + "axios-mock-adapter": "^1.22.0", "eslint": "^8.22.0", "eslint-plugin-import": "^2.26.0", "happy-dom": "^9.20.3", diff --git a/packages/rpc/src/batchQuery.ts b/packages/rpc/src/batchQuery.ts deleted file mode 100644 index 5c19ec5..0000000 --- a/packages/rpc/src/batchQuery.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable import/extensions */ -import type { FromCapData } from '@endo/marshal'; -import { AgoricChainStoragePathKind } from './types'; - -export const pathToKey = (path: [AgoricChainStoragePathKind, string]) => - path.join('.'); - -export const keyToPath = (key: string) => { - const parts = key.split('.'); - return [parts[0], parts.slice(1).join('.')] as [ - AgoricChainStoragePathKind, - string, - ]; -}; - -export const batchVstorageQuery = async ( - node: string, - unmarshal: FromCapData, - paths: [AgoricChainStoragePathKind, string][], -) => { - const urls = paths.map( - path => new URL(`${node}/agoric/vstorage/${path[0]}/${path[1]}`).href, - ); - const requests = urls.map(url => fetch(url)); - - return Promise.all(requests) - .then(responseDatas => Promise.all(responseDatas.map(res => res.json()))) - .then(responses => - responses.map((res, index) => { - if (paths[index][0] === AgoricChainStoragePathKind.Children) { - return [ - pathToKey(paths[index]), - { value: res.children, blockHeight: undefined }, - ]; - } - - if (!res.value) { - return [ - pathToKey(paths[index]), - { - error: `Cannot parse value of response for path [${ - paths[index] - }]: ${JSON.stringify(res)}`, - }, - ]; - } - - const parseIfJSON = (d: string | unknown) => { - if (typeof d !== 'string') return d; - try { - return JSON.parse(d); - } catch { - return d; - } - }; - - const data = parseIfJSON(res.value); - - const latestValue = - typeof data.values !== 'undefined' - ? parseIfJSON(data.values[data.values.length - 1]) - : parseIfJSON(data.value); - - const unserialized = - typeof latestValue.slots !== 'undefined' - ? unmarshal(latestValue) - : latestValue; - - return [ - pathToKey(paths[index]), - { - blockHeight: data.blockHeight, - value: unserialized, - }, - ]; - }), - ); -}; diff --git a/packages/rpc/src/chainStorageWatcher.ts b/packages/rpc/src/chainStorageWatcher.ts index 2dffbe7..60e616c 100644 --- a/packages/rpc/src/chainStorageWatcher.ts +++ b/packages/rpc/src/chainStorageWatcher.ts @@ -2,7 +2,7 @@ /* eslint-disable import/extensions */ import { makeClientMarshaller } from './marshal'; import { AgoricChainStoragePathKind } from './types'; -import { batchVstorageQuery, keyToPath, pathToKey } from './batchQuery'; +import { vstorageQuery, keyToPath, pathToKey } from './vstorageQuery'; import type { UpdateHandler } from './types'; type Subscriber = { @@ -35,13 +35,20 @@ export type ChainStorageWatcher = ReturnType< typeof makeAgoricChainStorageWatcher >; +/** + This is used to avoid notifying subscribers for already-seen + values. For `data` queries, it is the stringified blockheight of where the + value appeared. For `children` queries, it is the stringified array of + children. + */ +type LatestValueIdentifier = string; + /** * Periodically queries the most recent data from chain storage. * @param apiAddr API server URL * @param chainId the chain id to use * @param onError * @param marshaller CapData marshal to use - * @param newPathQueryDelayMs * @param refreshLowerBoundMs * @param refreshUpperBoundMs * @returns @@ -51,136 +58,101 @@ export const makeAgoricChainStorageWatcher = ( chainId: string, onError?: (e: Error) => void, marshaller = makeClientMarshaller(), - newPathQueryDelayMs = defaults.newPathQueryDelayMs, refreshLowerBoundMs = defaults.refreshLowerBoundMs, refreshUpperBoundMs = defaults.refreshUpperBoundMs, ) => { - // Map of paths to [identifier, value] pairs of most recent response values. - // - // The 'identifier' is used to avoid notifying subscribers for already-seen - // values. For 'data' queries, 'identifier' is the blockheight of the - // response. For 'children' queries, 'identifier' is the stringified array - // of children. + /** + * Map from vstorage paths to `[identifier, value]` pairs containing their + * most recent response values and their {@link LatestValueIdentifier}. + */ const latestValueCache = new Map< string, - [identifier: string, value: unknown] + [identifier: LatestValueIdentifier, value: unknown] >(); const watchedPathsToSubscribers = new Map>>(); - let isNewPathWatched = false; - let isQueryInProgress = false; - let nextQueryTimeout: number | null = null; - - const queueNextQuery = () => { - if (isQueryInProgress || !watchedPathsToSubscribers.size) { - return; - } + const watchedPathsToRefreshTimeouts = new Map(); - if (isNewPathWatched) { - // If there is any new path to watch, schedule another query very soon. - if (nextQueryTimeout) { - window.clearTimeout(nextQueryTimeout); - } - nextQueryTimeout = window.setTimeout(queryUpdates, newPathQueryDelayMs); - } else { - // Otherwise, refresh after a normal interval. - nextQueryTimeout = window.setTimeout( - queryUpdates, + const refreshDataForPath = async ( + path: [AgoricChainStoragePathKind, string], + ) => { + const queueNextRefresh = () => { + window.clearTimeout(watchedPathsToRefreshTimeouts.get(pathKey)); + const timeout = window.setTimeout( + () => refreshDataForPath(path), randomRefreshPeriod(refreshLowerBoundMs, refreshUpperBoundMs), ); - } - }; + watchedPathsToRefreshTimeouts.set(pathKey, timeout); + }; - const queryUpdates = async () => { - isQueryInProgress = true; - nextQueryTimeout = null; - isNewPathWatched = false; - - const paths = [...watchedPathsToSubscribers.keys()].map(keyToPath); - - if (!paths.length) { - isQueryInProgress = false; + const pathKey = pathToKey(path); + let response; + try { + response = await vstorageQuery(apiAddr, marshaller.fromCapData, path); + } catch (e: unknown) { + console.error(`Error querying vstorage for path ${path}:`, e); + if (onError && e instanceof Error) { + onError(e); + } + // Try again later until client tells us to stop. + queueNextRefresh(); return; } - try { - const responses = await batchVstorageQuery( - apiAddr, - marshaller.fromCapData, - paths, - ); - const data = Object.fromEntries(responses); - watchedPathsToSubscribers.forEach((subscribers, path) => { - // Path was watched after query fired, wait until next round. - if (!data[path]) return; - - if (data[path].error) { - subscribers.forEach(s => { - if (s.onError) { - s.onError(harden(data[path].error)); - } - }); - return; - } - - const { blockHeight, value } = data[path]; - const lastValue = latestValueCache.get(path); - - if ( - lastValue && - (blockHeight === lastValue[0] || - (blockHeight === undefined && - JSON.stringify(value) === lastValue[0])) - ) { - // The value isn't new, don't emit. - return; - } - - latestValueCache.set(path, [ - blockHeight ?? JSON.stringify(value), - value, - ]); - - subscribers.forEach(s => { - s.onUpdate(harden(value)); - }); - }); - } catch (e) { - onError && onError(e as Error); - } finally { - isQueryInProgress = false; - queueNextQuery(); + const { value, blockHeight } = response; + const [latestValueIdentifier, latestValue] = + latestValueCache.get(pathKey) || []; + + if ( + latestValue && + (blockHeight === latestValueIdentifier || + // Blockheight is undefined so fallback to using the stringified value + // as the identifier, as is the case for `children` queries. + (blockHeight === undefined && JSON.stringify(value) === latestValue)) + ) { + // The value isn't new, don't emit. + queueNextRefresh(); + return; } + + latestValueCache.set(pathKey, [ + // Fallback to using stringified value as identifier if no blockHeight, + // as is the case for `children` queries. + blockHeight ?? JSON.stringify(value), + value, + ]); + + const subscribersForPath = watchedPathsToSubscribers.get(pathKey); + subscribersForPath?.forEach(s => { + s.onUpdate(harden(value)); + }); + queueNextRefresh(); }; const stopWatching = (pathKey: string, subscriber: Subscriber) => { const subscribersForPath = watchedPathsToSubscribers.get(pathKey); if (!subscribersForPath?.size) { - throw new Error(`cannot unsubscribe from unwatched path ${pathKey}`); + throw new Error( + `already stopped watching path ${pathKey}, nothing to do`, + ); } if (subscribersForPath.size === 1) { watchedPathsToSubscribers.delete(pathKey); latestValueCache.delete(pathKey); + window.clearTimeout(watchedPathsToRefreshTimeouts.get(pathKey)); + watchedPathsToRefreshTimeouts.delete(pathKey); } else { subscribersForPath.delete(subscriber); } }; - const queueNewPathForQuery = () => { - if (!isNewPathWatched) { - isNewPathWatched = true; - queueNextQuery(); - } - }; - const watchLatest = ( path: [AgoricChainStoragePathKind, string], onUpdate: (latestValue: T) => void, - onPathError?: (log: string) => void, ) => { const pathKey = pathToKey(path); - const subscriber = makePathSubscriber(onUpdate, onPathError); + const subscriber = makePathSubscriber(onUpdate); const latestValue = latestValueCache.get(pathKey); if (latestValue) { @@ -195,23 +167,20 @@ export const makeAgoricChainStorageWatcher = ( pathKey, new Set([subscriber as Subscriber]), ); - queueNewPathForQuery(); + refreshDataForPath(path); } return () => stopWatching(pathKey, subscriber as Subscriber); }; - const queryOnce = (path: [AgoricChainStoragePathKind, string]) => - new Promise((res, rej) => { - const stop = watchLatest( - path, - val => { - stop(); - res(val); - }, - e => rej(e), - ); - }); + const queryOnce = async (path: [AgoricChainStoragePathKind, string]) => { + const { value } = await vstorageQuery( + apiAddr, + marshaller.fromCapData, + path, + ); + return value; + }; // Assumes argument is an unserialized presence. const presenceToSlot = (o: unknown) => marshaller.toCapData(o).slots[0]; diff --git a/packages/rpc/src/vstorageQuery.ts b/packages/rpc/src/vstorageQuery.ts new file mode 100644 index 0000000..3f60ee3 --- /dev/null +++ b/packages/rpc/src/vstorageQuery.ts @@ -0,0 +1,72 @@ +/* eslint-disable import/extensions */ +import axios from 'axios'; +// eslint-disable-next-line import/no-extraneous-dependencies +import axiosRetry from 'axios-retry'; +import type { FromCapData } from '@endo/marshal'; +import { AgoricChainStoragePathKind } from './types'; + +export const pathToKey = (path: [AgoricChainStoragePathKind, string]) => + path.join('.'); + +export const keyToPath = (key: string) => { + const parts = key.split('.'); + return [parts[0], parts.slice(1).join('.')] as [ + AgoricChainStoragePathKind, + string, + ]; +}; + +const parseIfJSON = (d: string | unknown) => { + if (typeof d !== 'string') return d; + try { + return JSON.parse(d); + } catch { + return d; + } +}; + +// Exported for testing. +export const axiosClient = axios.create(); + +axiosRetry(axiosClient, { + retries: 2, + retryDelay: axiosRetry.exponentialDelay, +}); + +export const vstorageQuery = async ( + node: string, + unmarshal: FromCapData, + [pathKind, path]: [AgoricChainStoragePathKind, string], +): Promise<{ blockHeight?: string; value: T }> => { + const url = new URL(`${node}/agoric/vstorage/${pathKind}/${path}`).href; + const request = axiosClient.get(url); + + return request.then(({ data: res }) => { + if (pathKind === AgoricChainStoragePathKind.Children) { + return { value: res.children, blockHeight: undefined }; + } + + if (!res.value) { + return { + value: res.value, + }; + } + + const data = parseIfJSON(res.value); + + const latestValue = + typeof data.values === 'undefined' + ? parseIfJSON(data.value) + : parseIfJSON(data.values[data.values.length - 1]); + + const unserialized = + typeof latestValue.slots === 'undefined' + ? latestValue + : unmarshal(latestValue); + + return { + blockHeight: data.blockHeight, + value: unserialized, + }; + }); +}; diff --git a/packages/rpc/test/chainStorageWatcher.test.ts b/packages/rpc/test/chainStorageWatcher.test.ts index 05b1648..db60f49 100644 --- a/packages/rpc/test/chainStorageWatcher.test.ts +++ b/packages/rpc/test/chainStorageWatcher.test.ts @@ -1,11 +1,11 @@ /* eslint-disable no-use-before-define */ /* eslint-disable import/extensions */ import { expect, it, describe, beforeEach, vi, afterEach } from 'vitest'; +import MockAdapter from 'axios-mock-adapter'; import { makeAgoricChainStorageWatcher } from '../src/chainStorageWatcher'; +import { axiosClient } from '../src/vstorageQuery'; import { AgoricChainStoragePathKind } from '../src/types'; -const fetch = vi.fn(); -global.fetch = fetch; global.harden = val => val; const fakeApiAddr = 'https://fake.api'; @@ -14,24 +14,23 @@ const marshal = (val: unknown) => val; const unmarshal = (val: unknown) => val; let watcher: ReturnType; +let mockAxios: MockAdapter; +let onError: ReturnType; vi.mock('../src/marshal', () => ({ makeMarshal: () => {} })); describe('makeAgoricChainStorageWatcher', () => { beforeEach(() => { - watcher = makeAgoricChainStorageWatcher( - fakeApiAddr, - fakeChainId, - undefined, - { - fromCapData: unmarshal, - // @ts-expect-error mock doesnt require capdata type - toCapData: marshal, - unserialize: unmarshal, - // @ts-expect-error mock doesnt require capdata type - serialize: marshal, - }, - ); + onError = vi.fn(); + mockAxios = new MockAdapter(axiosClient); + watcher = makeAgoricChainStorageWatcher(fakeApiAddr, fakeChainId, onError, { + fromCapData: unmarshal, + // @ts-expect-error mock doesnt require capdata type + toCapData: marshal, + unserialize: unmarshal, + // @ts-expect-error mock doesnt require capdata type + serialize: marshal, + }); vi.useFakeTimers(); }); @@ -47,15 +46,16 @@ describe('makeAgoricChainStorageWatcher', () => { const requestUrl1 = `${fakeApiAddr}/agoric/vstorage/data/${path}`; const requestUrl2 = `${fakeApiAddr}/agoric/vstorage/children/${path}`; - fetch.mockImplementation(requestUrl => { - if (requestUrl === requestUrl1) { - return createFetchResponse(AgoricChainStoragePathKind.Data, expected1); - } - return createFetchResponse( - AgoricChainStoragePathKind.Children, - expected2, + mockAxios + .onGet(requestUrl1) + .replyOnce(() => + createFetchResponse(AgoricChainStoragePathKind.Data, expected1), + ); + mockAxios + .onGet(requestUrl2) + .replyOnce(() => + createFetchResponse(AgoricChainStoragePathKind.Children, expected2), ); - }); const value1 = new Promise(res => { watcher.watchLatest( @@ -74,43 +74,39 @@ describe('makeAgoricChainStorageWatcher', () => { expect(await value1).toEqual(expected1); expect(await value2).toEqual(expected2); - - expect(fetch).toHaveBeenCalledTimes(2); - expect(fetch).toHaveBeenCalledWith(requestUrl1); - expect(fetch).toHaveBeenCalledWith(requestUrl2); + expect(mockAxios.history.get.length).toBe(2); }); it('can do single queries', async () => { const expected = 126560000000; const path = 'test.fakePath'; - fetch.mockImplementation(_ => { - return createFetchResponse( - AgoricChainStoragePathKind.Data, - JSON.stringify(expected), - undefined, - false, + mockAxios + .onGet() + .replyOnce(() => + createFetchResponse( + AgoricChainStoragePathKind.Data, + JSON.stringify(expected), + undefined, + false, + ), ); - }); - const value = watcher.queryOnce([ - AgoricChainStoragePathKind.Data, - path, - ]); + const value = watcher.queryOnce([AgoricChainStoragePathKind.Data, path]); vi.advanceTimersToNextTimer(); expect(await value).toEqual(expected); - expect(fetch).toHaveBeenCalledOnce(); + expect(mockAxios.history.get.length).toBe(1); vi.advanceTimersToNextTimer(); - expect(fetch).toHaveBeenCalledOnce(); + expect(mockAxios.history.get.length).toBe(1); }); it('notifies for changed data values', async () => { const expected1 = 'test result'; const path = 'test.fakePath'; - fetch.mockImplementation(_ => { + mockAxios.onGet().replyOnce(_ => { return createFetchResponse( AgoricChainStoragePathKind.Data, expected1, @@ -131,10 +127,10 @@ describe('makeAgoricChainStorageWatcher', () => { ); vi.advanceTimersToNextTimer(); expect(await values[0].value).toEqual(expected1); - expect(fetch).toHaveBeenCalledOnce(); + expect(mockAxios.history.get.length).toBe(1); const expected2 = `${expected1}foo`; - fetch.mockImplementation(_ => { + mockAxios.onGet().replyOnce(_ => { return createFetchResponse( AgoricChainStoragePathKind.Data, expected2, @@ -144,14 +140,14 @@ describe('makeAgoricChainStorageWatcher', () => { vi.advanceTimersToNextTimer(); expect(await values[1].value).toEqual(expected2); - expect(fetch).toHaveBeenCalledTimes(2); + expect(mockAxios.history.get.length).toBe(2); }); it('notifies for changed children values', async () => { const expected1 = ['child1', 'child2']; const path = 'test.fakePath'; - fetch.mockImplementation(_ => { + mockAxios.onGet().replyOnce(() => { return createFetchResponse( AgoricChainStoragePathKind.Children, expected1, @@ -171,10 +167,10 @@ describe('makeAgoricChainStorageWatcher', () => { ); vi.advanceTimersToNextTimer(); expect(await values[0].value).toEqual(expected1); - expect(fetch).toHaveBeenCalledOnce(); + expect(mockAxios.history.get.length).toBe(1); const expected2 = [...expected1, 'child3']; - fetch.mockImplementation(_ => { + mockAxios.onGet().replyOnce(() => { return createFetchResponse( AgoricChainStoragePathKind.Children, expected2, @@ -183,14 +179,14 @@ describe('makeAgoricChainStorageWatcher', () => { vi.advanceTimersToNextTimer(); expect(await values[1].value).toEqual(expected2); - expect(fetch).toHaveBeenCalledTimes(2); + expect(mockAxios.history.get.length).toBe(2); }); it('can unsubscribe from paths', async () => { const expected1 = ['child1', 'child2']; const path = 'test.fakePath'; - fetch.mockImplementation(_ => { + mockAxios.onGet().replyOnce(_ => { return createFetchResponse( AgoricChainStoragePathKind.Children, expected1, @@ -210,12 +206,141 @@ describe('makeAgoricChainStorageWatcher', () => { ); vi.advanceTimersToNextTimer(); expect(await values[0].value).toEqual(expected1); - expect(fetch).toHaveBeenCalledOnce(); + expect(mockAxios.history.get.length).toBe(1); unsub(); vi.advanceTimersToNextTimer(); - expect(fetch).toHaveBeenCalledOnce(); + expect(mockAxios.history.get.length).toBe(1); + }); + + it('queryOnce retries up to two times on server errors', async () => { + const expected = 126560000000; + const path = 'test.fakePath'; + + const failedResponse = createFetchResponse( + AgoricChainStoragePathKind.Data, + '{}', + undefined, + false, + 500, + ); + mockAxios + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => + createFetchResponse( + AgoricChainStoragePathKind.Data, + JSON.stringify(expected), + undefined, + false, + 200, + ), + ); + + const value = watcher.queryOnce([AgoricChainStoragePathKind.Data, path]); + + vi.advanceTimersByTimeAsync(1000); + expect(await value).toEqual(expected); + expect(mockAxios.history.get.length).toBe(3); + }); + + it('queryOnce throws on three consecutive server errors', async () => { + const expected = 126560000000; + const path = 'test.fakePath'; + + const failedResponse = createFetchResponse( + AgoricChainStoragePathKind.Data, + '{}', + undefined, + false, + 500, + ); + mockAxios + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => failedResponse); + + const value = watcher.queryOnce([AgoricChainStoragePathKind.Data, path]); + vi.advanceTimersByTimeAsync(1000); + + await expect(async () => value).rejects.toThrow( + 'Request failed with status code 500', + ); + expect(mockAxios.history.get.length).toBe(3); + }); + + it('watchLatest retries up to two times on server errors', async () => { + const expected1 = 'test result'; + const path = 'test.fakePath'; + + const failedResponse = createFetchResponse( + AgoricChainStoragePathKind.Data, + '{}', + undefined, + true, + 500, + ); + mockAxios + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => + createFetchResponse(AgoricChainStoragePathKind.Data, expected1), + ); + + const result = new Promise(res => { + watcher.watchLatest( + [AgoricChainStoragePathKind.Data, path], + value => res(value), + ); + }); + vi.advanceTimersByTimeAsync(1000); + expect(await result).toEqual(expected1); + expect(mockAxios.history.get.length).toBe(3); + }); + + it('watchLatest calls onError after three consecutive server errors', async () => { + const expected1 = 'test result'; + const path = 'test.fakePath'; + + const failedResponse = createFetchResponse( + AgoricChainStoragePathKind.Data, + '{}', + undefined, + true, + 500, + ); + mockAxios + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => failedResponse) + .onGet() + .replyOnce(() => failedResponse); + + const result = new Promise((res, rej) => { + watcher.watchLatest( + [AgoricChainStoragePathKind.Data, path], + value => res(value), + ); + setTimeout(() => rej(new Error('timeout')), 1000); + }); + vi.advanceTimersByTimeAsync(1000); + + await expect(async () => result).rejects.toThrow('timeout'); + expect(onError).toHaveBeenCalledWith( + new Error('Request failed with status code 500'), + ); + expect(mockAxios.history.get.length).toBe(3); }); }); @@ -224,22 +349,22 @@ const createFetchResponse = ( value: unknown, blockHeight?: number, json = true, -) => ({ - json: () => - new Promise(res => - res( - kind === AgoricChainStoragePathKind.Children - ? { children: value } - : { - value: JSON.stringify({ - values: json ? [JSON.stringify(marshal(value))] : undefined, - value: !json ? value : undefined, - blockHeight: String(blockHeight ?? 0), - }), + code = 200, +) => + new Promise(res => { + res([ + code, + kind === AgoricChainStoragePathKind.Children + ? { children: value } + : { + value: { + values: json ? [JSON.stringify(marshal(value))] : undefined, + value: !json ? value : undefined, + blockHeight: String(blockHeight ?? 0), }, - ), - ), -}); + }, + ]); + }); const future = () => { let resolve: (value: T) => void; diff --git a/packages/web-components/src/wallet-connection/watchWallet.js b/packages/web-components/src/wallet-connection/watchWallet.js index cadcffa..1e8a2b1 100644 --- a/packages/web-components/src/wallet-connection/watchWallet.js +++ b/packages/web-components/src/wallet-connection/watchWallet.js @@ -70,9 +70,18 @@ export const watchWallet = (chainStorageWatcher, address, rpc) => { ); let lastPaths; + let isWalletMissing = false; chainStorageWatcher.watchLatest( ['data', `published.wallet.${address}.current`], value => { + if (!value && !isWalletMissing) { + smartWalletStatusNotifierKit.updater.updateState( + harden({ provisioned: false }), + ); + isWalletMissing = true; + return; + } + smartWalletStatusNotifierKit.updater.updateState( harden({ provisioned: true }), ); @@ -83,18 +92,6 @@ export const watchWallet = (chainStorageWatcher, address, rpc) => { harden(currentPaths), ); }, - err => { - if ( - !lastPaths && - err === 'could not get vstorage path: unknown request' - ) { - smartWalletStatusNotifierKit.updater.updateState( - harden({ provisioned: false }), - ); - } else { - throw Error(err); - } - }, ); const watchChainBalances = () => { diff --git a/yarn.lock b/yarn.lock index 47a6433..c9ff898 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4533,6 +4533,21 @@ axe-core@^4.3.3, axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0" integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== +axios-mock-adapter@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz#0f3e6be0fc9b55baab06f2d49c0b71157e7c053d" + integrity sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw== + dependencies: + fast-deep-equal "^3.1.3" + is-buffer "^2.0.5" + +axios-retry@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-4.0.0.tgz#d5cb8ea1db18e05ce6f08aa5fe8b2663bba48e60" + integrity sha512-F6P4HVGITD/v4z9Lw2mIA24IabTajvpDZmKa6zq/gGwn57wN5j1P3uWrAV0+diqnW6kTM2fTqmWNfgYWGmMuiA== + dependencies: + is-retry-allowed "^2.2.0" + axios@0.21.4, axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -4557,6 +4572,15 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" + integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -7727,6 +7751,11 @@ is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-builtin-module@^3.1.0, is-builtin-module@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" @@ -7893,6 +7922,11 @@ is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-retry-allowed@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" + integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== + is-set@^2.0.1, is-set@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"