From 339b557097cc668f0d200ac93a6404e03596b764 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Wed, 23 Aug 2023 20:46:34 -0700 Subject: [PATCH 1/2] feat: support non-vbank purses --- packages/rpc/src/chainStorageWatcher.ts | 23 +++ packages/rpc/test/chainStorageWatcher.test.ts | 35 +++- .../src/wallet-connection/watchWallet.js | 172 +++++++++++++----- 3 files changed, 181 insertions(+), 49 deletions(-) diff --git a/packages/rpc/src/chainStorageWatcher.ts b/packages/rpc/src/chainStorageWatcher.ts index 6762f10..64a78eb 100644 --- a/packages/rpc/src/chainStorageWatcher.ts +++ b/packages/rpc/src/chainStorageWatcher.ts @@ -201,10 +201,33 @@ export const makeAgoricChainStorageWatcher = ( return () => stopWatching(pathKey, subscriber as Subscriber); }; + const queryOnce = (path: [AgoricChainStoragePathKind, string]) => + new Promise(res => { + const stop = watchLatest(path, val => { + stop(); + res(val); + }); + }); + + // Assumes argument is an unserialized presence. + const presenceToSlot = (o: unknown) => marshaller.toCapData(o).slots[0]; + + const queryBoardAux = (boardObjects: unknown[]) => { + const boardIds = boardObjects.map(presenceToSlot); + return boardIds.map(id => + queryOnce([ + AgoricChainStoragePathKind.Data, + `published.boardAux.${id}`, + ]), + ); + }; + return { watchLatest, chainId, rpcAddr, marshaller, + queryOnce, + queryBoardAux, }; }; diff --git a/packages/rpc/test/chainStorageWatcher.test.ts b/packages/rpc/test/chainStorageWatcher.test.ts index 961af24..8b48a48 100644 --- a/packages/rpc/test/chainStorageWatcher.test.ts +++ b/packages/rpc/test/chainStorageWatcher.test.ts @@ -43,7 +43,7 @@ describe('makeAgoricChainStorageWatcher', () => { it('can handle multiple paths at once', async () => { const expected1 = 'test result'; const expected2 = ['child1', 'child2']; - const path = 'published.vitest.fakePath'; + const path = 'vitest.fakePath'; fetch.mockResolvedValue( createFetchResponse([ @@ -91,7 +91,7 @@ describe('makeAgoricChainStorageWatcher', () => { it('can handle unserialized values', async () => { const expected = 126560000000; - const path = 'published.vitest.unserializedValue'; + const path = 'vitest.unserializedValue'; fetch.mockResolvedValue( createUnserializedFetchResponse([ @@ -110,9 +110,32 @@ describe('makeAgoricChainStorageWatcher', () => { expect(await value).toEqual(expected); }); + it('can do single queries', async () => { + const expected = 126560000000; + const path = 'vitest.unserializedValue'; + + fetch.mockResolvedValue( + createUnserializedFetchResponse([ + { value: expected, kind: AgoricChainStoragePathKind.Data, id: 0 }, + ]), + ); + + const value = watcher.queryOnce([ + AgoricChainStoragePathKind.Data, + path, + ]); + + vi.advanceTimersToNextTimer(); + expect(await value).toEqual(expected); + expect(fetch).toHaveBeenCalledOnce(); + + vi.advanceTimersToNextTimer(); + expect(fetch).toHaveBeenCalledOnce(); + }); + it('notifies for changed data values', async () => { const expected1 = 'test result'; - const path = 'published.vitest.fakePath'; + const path = 'vitest.fakePath'; fetch.mockResolvedValue( createFetchResponse([ @@ -159,7 +182,7 @@ describe('makeAgoricChainStorageWatcher', () => { it('notifies for changed children values', async () => { const expected1 = ['child1', 'child2']; - const path = 'published.vitest.fakePath'; + const path = 'vitest.fakePath'; fetch.mockResolvedValue( createFetchResponse([ @@ -204,7 +227,7 @@ describe('makeAgoricChainStorageWatcher', () => { it('handles errors', async () => { const expected = 'test error log'; - const path = 'published.vitest.fakePath'; + const path = 'vitest.fakePath'; fetch.mockResolvedValue( createFetchResponse([ @@ -232,7 +255,7 @@ describe('makeAgoricChainStorageWatcher', () => { it('can unsubscribe from paths', async () => { const expected1 = ['child1', 'child2']; - const path = 'published.vitest.fakePath'; + const path = 'vitest.fakePath'; fetch.mockResolvedValue( createFetchResponse([ diff --git a/packages/web-components/src/wallet-connection/watchWallet.js b/packages/web-components/src/wallet-connection/watchWallet.js index 5ae2a17..d092797 100644 --- a/packages/web-components/src/wallet-connection/watchWallet.js +++ b/packages/web-components/src/wallet-connection/watchWallet.js @@ -7,6 +7,8 @@ import { queryBankBalances } from '../queryBankBalances.js'; /** @typedef {import('@agoric/smart-wallet/src/types.js').Petname} Petname */ +/** @typedef {import('@keplr-wallet/types').Coin} Coin */ + /** * @typedef {{ * brand?: unknown, @@ -17,6 +19,17 @@ import { queryBankBalances } from '../queryBankBalances.js'; * }} PurseInfo */ +/** + * @typedef {[ + * string, + * { + * brand: unknown, + * issuerName: string, + * displayInfo: unknown + * } + * ][]} VBankAssets + */ + const POLL_INTERVAL_MS = 6000; /** @@ -82,51 +95,124 @@ export const watchWallet = async (chainStorageWatcher, address) => { const watchChainBalances = () => { const brandToPurse = new Map(); - let vbankAssets; - let bank; - - const possiblyUpdateBankPurses = () => { - if (!vbankAssets || !bank) return; - - const bankMap = new Map(bank.map(({ denom, amount }) => [denom, amount])); - - vbankAssets.forEach(([denom, info]) => { - const amount = bankMap.get(denom) ?? 0n; - const purseInfo = { - brand: info.brand, - currentAmount: AmountMath.make(info.brand, BigInt(amount)), - brandPetname: info.issuerName, - pursePetname: info.issuerName, - displayInfo: info.displayInfo, - }; - brandToPurse.set(info.brand, purseInfo); - }); - - updatePurses(brandToPurse); - }; - - const watchBank = async () => { - const balances = await queryBankBalances( - address, - chainStorageWatcher.rpcAddr, - ); - bank = balances; - possiblyUpdateBankPurses(); - setTimeout(watchBank, POLL_INTERVAL_MS); - }; - const watchVbankAssets = async () => { - chainStorageWatcher.watchLatest( - ['data', 'published.agoricNames.vbankAsset'], - value => { - vbankAssets = value; - possiblyUpdateBankPurses(); - }, - ); - }; + { + /** @type {VBankAssets} */ + let vbankAssets; + /** @type {Coin[]} */ + let bank; + + const possiblyUpdateBankPurses = () => { + if (!vbankAssets || !bank) return; + + const bankMap = new Map( + bank.map(({ denom, amount }) => [denom, amount]), + ); + + vbankAssets.forEach(([denom, info]) => { + const amount = bankMap.get(denom) ?? 0n; + const purseInfo = { + brand: info.brand, + currentAmount: AmountMath.make(info.brand, BigInt(amount)), + brandPetname: info.issuerName, + pursePetname: info.issuerName, + displayInfo: info.displayInfo, + }; + brandToPurse.set(info.brand, purseInfo); + }); + + updatePurses(brandToPurse); + }; + + const watchBank = async () => { + const balances = await queryBankBalances( + address, + chainStorageWatcher.rpcAddr, + ); + bank = balances; + possiblyUpdateBankPurses(); + setTimeout(watchBank, POLL_INTERVAL_MS); + }; + + const watchVbankAssets = () => { + chainStorageWatcher.watchLatest( + ['data', 'published.agoricNames.vbankAsset'], + value => { + vbankAssets = value; + possiblyUpdateBankPurses(); + }, + ); + }; + + void watchVbankAssets(); + void watchBank(); + } - void watchVbankAssets(); - void watchBank(); + { + /** @type { [string, unknown][] } */ + let agoricBrands; + /** @type { {balance: unknown, brand: unknown}[] } */ + let nonBankPurses; + /** @type { Map } */ + let brandToBoardAux; + + const possiblyUpdateNonBankPurses = () => { + if (!agoricBrands || !nonBankPurses || !brandToBoardAux) return; + + nonBankPurses.forEach(({ balance, brand }) => { + const petname = agoricBrands + ?.find(([_petname, b]) => b === brand) + ?.at(0); + const { displayInfo } = brandToBoardAux.get(brand) ?? {}; + const purseInfo = { + brand, + currentAmount: balance, + brandPetname: petname, + pursePetname: petname, + displayInfo, + }; + brandToPurse.set(brand, purseInfo); + }); + + updatePurses(brandToPurse); + }; + + const watchBrands = () => { + chainStorageWatcher.watchLatest( + ['data', 'published.agoricNames.brand'], + value => { + agoricBrands = value; + possiblyUpdateNonBankPurses(); + }, + ); + }; + + const watchPurses = () => + chainStorageWatcher.watchLatest( + ['data', `published.wallet.${address}.current`], + async value => { + const { purses } = value; + if (nonBankPurses === purses) return; + + await null; + if (purses.length !== nonBankPurses?.length) { + const brands = purses.map(p => p.brand); + const boardAux = await Promise.all( + chainStorageWatcher.queryBoardAux(brands), + ); + brandToBoardAux = new Map( + brands.map((brand, index) => [brand, boardAux[index]]), + ); + } + + nonBankPurses = purses; + possiblyUpdateNonBankPurses(); + }, + ); + + void watchBrands(); + void watchPurses(); + } }; const watchWalletUpdates = async () => { From 61717a41677b00efe266c56a33e8b862629312a4 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Thu, 24 Aug 2023 23:05:45 -0700 Subject: [PATCH 2/2] fix: backwards compatible without boardAux --- packages/rpc/src/chainStorageWatcher.ts | 14 +++++++++----- .../src/wallet-connection/watchWallet.js | 16 ++++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/rpc/src/chainStorageWatcher.ts b/packages/rpc/src/chainStorageWatcher.ts index 64a78eb..c219bad 100644 --- a/packages/rpc/src/chainStorageWatcher.ts +++ b/packages/rpc/src/chainStorageWatcher.ts @@ -202,11 +202,15 @@ export const makeAgoricChainStorageWatcher = ( }; const queryOnce = (path: [AgoricChainStoragePathKind, string]) => - new Promise(res => { - const stop = watchLatest(path, val => { - stop(); - res(val); - }); + new Promise((res, rej) => { + const stop = watchLatest( + path, + val => { + stop(); + res(val); + }, + e => rej(e), + ); }); // Assumes argument is an unserialized presence. diff --git a/packages/web-components/src/wallet-connection/watchWallet.js b/packages/web-components/src/wallet-connection/watchWallet.js index d092797..e69602f 100644 --- a/packages/web-components/src/wallet-connection/watchWallet.js +++ b/packages/web-components/src/wallet-connection/watchWallet.js @@ -197,12 +197,16 @@ export const watchWallet = async (chainStorageWatcher, address) => { await null; if (purses.length !== nonBankPurses?.length) { const brands = purses.map(p => p.brand); - const boardAux = await Promise.all( - chainStorageWatcher.queryBoardAux(brands), - ); - brandToBoardAux = new Map( - brands.map((brand, index) => [brand, boardAux[index]]), - ); + try { + const boardAux = await Promise.all( + chainStorageWatcher.queryBoardAux(brands), + ); + brandToBoardAux = new Map( + brands.map((brand, index) => [brand, boardAux[index]]), + ); + } catch (e) { + console.error('Error getting boardAux for brands', brands, e); + } } nonBankPurses = purses;