diff --git a/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts b/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts index 8d1787a1ab1..940339eec69 100644 --- a/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts +++ b/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @jessie.js/safe-await-separator */ /** * @file Bootstrap test integration vaults with smart-wallet. The tests in this * file are NOT independent; a single `test.before()` handler creates shared diff --git a/packages/boot/test/bootstrapTests/vtransfer.test.ts b/packages/boot/test/bootstrapTests/vtransfer.test.ts index fd0bec558ac..7f865e596fe 100644 --- a/packages/boot/test/bootstrapTests/vtransfer.test.ts +++ b/packages/boot/test/bootstrapTests/vtransfer.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @jessie.js/safe-await-separator -- confused by casting 'as' */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import type { TestFn } from 'ava'; diff --git a/packages/boot/test/upgrading/upgrade-vats.test.ts b/packages/boot/test/upgrading/upgrade-vats.test.ts index d5a3d22c14f..9711c7dd121 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.ts +++ b/packages/boot/test/upgrading/upgrade-vats.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @jessie.js/safe-await-separator -- test */ import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; import { BridgeId, deepCopyJsonable } from '@agoric/internal'; diff --git a/packages/fast-usdc/src/exos/status-manager.js b/packages/fast-usdc/src/exos/status-manager.js index ad4c33c44d1..ca17487e01b 100644 --- a/packages/fast-usdc/src/exos/status-manager.js +++ b/packages/fast-usdc/src/exos/status-manager.js @@ -13,21 +13,29 @@ import { PendingTxStatus, TxStatus } from '../constants.js'; /** * @import {MapStore, SetStore} from '@agoric/store'; * @import {Zone} from '@agoric/zone'; - * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx, EvmHash, LogFn} from '../types.js'; + * @import {CctpTxEvidence, NobleAddress, PendingTx, EvmHash, LogFn} from '../types.js'; + */ + +/** + * @typedef {`pendingTx:${bigint}:${NobleAddress}`} PendingTxKey + * The string template is for developer visibility but not meant to ever be parsed. + * + * @typedef {`seenTx:${string}:${EvmHash}`} SeenTxKey + * The string template is for developer visibility but not meant to ever be parsed. */ /** * Create the key for the pendingTxs MapStore. * - * The key is a composite of `txHash` and `chainId` and not meant to be - * parsable. + * The key is a composite but not meant to be parsable. * * @param {NobleAddress} addr * @param {bigint} amount * @returns {PendingTxKey} */ const makePendingTxKey = (addr, amount) => - `pendingTx:${JSON.stringify([addr, String(amount)])}`; + // amount can't contain colon + `pendingTx:${amount}:${addr}`; /** * Get the key for the pendingTxs MapStore. @@ -43,15 +51,15 @@ const pendingTxKeyOf = evidence => { /** * Get the key for the seenTxs SetStore. * - * The key is a composite of `NobleAddress` and transaction `amount` and not - * meant to be parsable. + * The key is a composite but not meant to be parsable. * * @param {CctpTxEvidence} evidence * @returns {SeenTxKey} */ const seenTxKeyOf = evidence => { const { txHash, chainId } = evidence; - return `seenTx:${JSON.stringify([txHash, chainId])}`; + // chainId can't contain colon + return `seenTx:${chainId}:${txHash}`; }; /** @@ -68,12 +76,12 @@ const seenTxKeyOf = evidence => { * XXX consider separate facets for `Advancing` and `Settling` capabilities. * * @param {Zone} zone - * @param {() => Promise} makeStatusNode + * @param {ERef} transactionsNode * @param {StatusManagerPowers} caps */ export const prepareStatusManager = ( zone, - makeStatusNode, + transactionsNode, { log = makeTracer('Advancer', true), } = /** @type {StatusManagerPowers} */ ({}), @@ -93,9 +101,8 @@ export const prepareStatusManager = ( * @param {CctpTxEvidence['txHash']} hash * @param {TxStatus} status */ - const recordStatus = (hash, status) => { - const statusNodeP = makeStatusNode(); - const txnNodeP = E(statusNodeP).makeChildNode(hash); + const publishStatus = (hash, status) => { + const txnNodeP = E(transactionsNode).makeChildNode(hash); // Don't await, just writing to vstorage. void E(txnNodeP).setValue(status); }; @@ -109,7 +116,7 @@ export const prepareStatusManager = ( * @param {CctpTxEvidence} evidence * @param {PendingTxStatus} status */ - const recordPendingTx = (evidence, status) => { + const initPendingTx = (evidence, status) => { const seenKey = seenTxKeyOf(evidence); if (seenTxs.has(seenKey)) { throw makeError(`Transaction already seen: ${q(seenKey)}`); @@ -121,9 +128,31 @@ export const prepareStatusManager = ( pendingTxKeyOf(evidence), harden({ ...evidence, status }), ); - recordStatus(evidence.txHash, status); + publishStatus(evidence.txHash, status); }; + /** + * Update the pending transaction status. + * + * @param {{sender: NobleAddress, amount: bigint}} keyParts + * @param {PendingTxStatus} status + */ + function setPendingTxStatus({ sender, amount }, status) { + const key = makePendingTxKey(sender, amount); + pendingTxs.has(key) || Fail`no advancing tx with ${{ sender, amount }}`; + const pending = pendingTxs.get(key); + const ix = pending.findIndex(tx => tx.status === PendingTxStatus.Advancing); + ix >= 0 || Fail`no advancing tx with ${{ sender, amount }}`; + const [prefix, tx, suffix] = [ + pending.slice(0, ix), + pending[ix], + pending.slice(ix + 1), + ]; + const txpost = { ...tx, status }; + pendingTxs.set(key, harden([...prefix, txpost, ...suffix])); + publishStatus(tx.txHash, status); + } + return zone.exo( 'Fast USDC Status Manager', M.interface('StatusManagerI', { @@ -156,10 +185,13 @@ export const prepareStatusManager = ( { /** * Add a new transaction with ADVANCING status + * + * NB: this acts like observe() but skips recording the OBSERVED state + * * @param {CctpTxEvidence} evidence */ advance(evidence) { - recordPendingTx(evidence, PendingTxStatus.Advancing); + initPendingTx(evidence, PendingTxStatus.Advancing); }, /** @@ -171,24 +203,10 @@ export const prepareStatusManager = ( * @throws {Error} if nothing to advance */ advanceOutcome(sender, amount, success) { - const key = makePendingTxKey(sender, amount); - pendingTxs.has(key) || Fail`no advancing tx with ${{ sender, amount }}`; - const pending = pendingTxs.get(key); - const ix = pending.findIndex( - tx => tx.status === PendingTxStatus.Advancing, + setPendingTxStatus( + { sender, amount }, + success ? PendingTxStatus.Advanced : PendingTxStatus.AdvanceFailed, ); - ix >= 0 || Fail`no advancing tx with ${{ sender, amount }}`; - const [prefix, tx, suffix] = [ - pending.slice(0, ix), - pending[ix], - pending.slice(ix + 1), - ]; - const status = success - ? PendingTxStatus.Advanced - : PendingTxStatus.AdvanceFailed; - const txpost = { ...tx, status }; - pendingTxs.set(key, harden([...prefix, txpost, ...suffix])); - recordStatus(tx.txHash, status); }, /** @@ -196,7 +214,7 @@ export const prepareStatusManager = ( * @param {CctpTxEvidence} evidence */ observe(evidence) { - recordPendingTx(evidence, PendingTxStatus.Observed); + initPendingTx(evidence, PendingTxStatus.Observed); }, /** @@ -247,7 +265,7 @@ export const prepareStatusManager = ( * @param {EvmHash} txHash */ disbursed(txHash) { - recordStatus(txHash, TxStatus.Disbursed); + publishStatus(txHash, TxStatus.Disbursed); }, /** @@ -259,7 +277,7 @@ export const prepareStatusManager = ( */ forwarded(txHash, address, amount) { if (txHash) { - recordStatus(txHash, TxStatus.Forwarded); + publishStatus(txHash, TxStatus.Forwarded); } else { // TODO store (early) `Minted` transactions to check against incoming evidence log( diff --git a/packages/fast-usdc/src/fast-usdc.contract.js b/packages/fast-usdc/src/fast-usdc.contract.js index 91dbdebdc2c..734bd423a3c 100644 --- a/packages/fast-usdc/src/fast-usdc.contract.js +++ b/packages/fast-usdc/src/fast-usdc.contract.js @@ -95,8 +95,8 @@ export const contract = async (zcf, privateArgs, zone, tools) => { marshaller, ); - const makeStatusNode = () => E(storageNode).makeChildNode(STATUS_NODE); - const statusManager = prepareStatusManager(zone, makeStatusNode); + const statusNode = E(storageNode).makeChildNode(STATUS_NODE); + const statusManager = prepareStatusManager(zone, statusNode); const { USDC } = terms.brands; const { withdrawToSeat } = tools.zoeTools; diff --git a/packages/fast-usdc/src/types.ts b/packages/fast-usdc/src/types.ts index cedb38b9b98..ee9a90efffd 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -38,12 +38,6 @@ export interface PendingTx extends CctpTxEvidence { status: PendingTxStatus; } -/** internal key for `StatusManager` exo */ -export type PendingTxKey = `pendingTx:${string}`; - -/** internal key for `StatusManager` exo */ -export type SeenTxKey = `seenTx:${string}`; - export type FeeConfig = { flat: Amount<'nat'>; variableRate: Ratio; diff --git a/packages/fast-usdc/src/util/agoric.js b/packages/fast-usdc/src/util/agoric.js index 1439f60ffda..4f7e4d65254 100644 --- a/packages/fast-usdc/src/util/agoric.js +++ b/packages/fast-usdc/src/util/agoric.js @@ -5,7 +5,7 @@ export const queryFastUSDCLocalChainAccount = async ( out = console, ) => { const agoricAddr = await vstorage.readLatest( - 'published.fastUSDC.settlementAccount', + 'published.fastUsdc.settlementAccount', ); out.log(`Got Fast USDC Local Chain Account ${agoricAddr}`); return agoricAddr; diff --git a/packages/fast-usdc/test/cli/transfer.test.ts b/packages/fast-usdc/test/cli/transfer.test.ts index d3c319f1f6b..729f40f63cc 100644 --- a/packages/fast-usdc/test/cli/transfer.test.ts +++ b/packages/fast-usdc/test/cli/transfer.test.ts @@ -64,7 +64,7 @@ test('Transfer registers the noble forwarding account if it does not exist', asy const out = mockOut(); const file = mockFile(path, JSON.stringify(config)); const agoricSettlementAccount = 'agoric123456'; - const settlementAccountVstoragePath = 'published.fastUSDC.settlementAccount'; + const settlementAccountVstoragePath = 'published.fastUsdc.settlementAccount'; const vstorageMock = makeVstorageMock({ [settlementAccountVstoragePath]: agoricSettlementAccount, }); @@ -115,7 +115,7 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy const out = mockOut(); const file = mockFile(path, JSON.stringify(config)); const agoricSettlementAccount = 'agoric123456'; - const settlementAccountVstoragePath = 'published.fastUSDC.settlementAccount'; + const settlementAccountVstoragePath = 'published.fastUsdc.settlementAccount'; const vstorageMock = makeVstorageMock({ [settlementAccountVstoragePath]: agoricSettlementAccount, }); diff --git a/packages/fast-usdc/test/exos/advancer.test.ts b/packages/fast-usdc/test/exos/advancer.test.ts index 818554dacca..908880d3fb4 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -44,7 +44,7 @@ const createTestExtensions = (t, common: CommonSetup) => { const statusManager = prepareStatusManager( rootZone.subZone('status-manager'), - async () => storageNode.makeChildNode('status'), + storageNode.makeChildNode('status'), ); const mockAccounts = prepareMockOrchAccounts(rootZone.subZone('accounts'), { @@ -162,6 +162,7 @@ test('updates status to ADVANCING in happy path', async t => { mocks: { mockPoolAccount, resolveLocalTransferV }, }, brands: { usdc }, + bootstrap: { storage }, } = t.context; const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -174,14 +175,9 @@ test('updates status to ADVANCING in happy path', async t => { // wait for handleTransactionEvent to do work await eventLoopIteration(); - const entries = statusManager.lookupPending( - mockEvidence.tx.forwardingAddress, - mockEvidence.tx.amount, - ); - t.deepEqual( - entries, - [{ ...mockEvidence, status: PendingTxStatus.Advancing }], + storage.data.get(`mockChainStorageRoot.status.${mockEvidence.txHash}`), + PendingTxStatus.Advancing, 'ADVANCED status in happy path', ); @@ -210,6 +206,7 @@ test('updates status to ADVANCING in happy path', async t => { test('updates status to OBSERVED on insufficient pool funds', async t => { const { + bootstrap: { storage }, extensions: { services: { makeAdvancer, statusManager }, helpers: { inspectLogs }, @@ -229,14 +226,9 @@ test('updates status to OBSERVED on insufficient pool funds', async t => { void advancer.handleTransactionEvent(mockEvidence); await eventLoopIteration(); - const entries = statusManager.lookupPending( - mockEvidence.tx.forwardingAddress, - mockEvidence.tx.amount, - ); - t.deepEqual( - entries, - [{ ...mockEvidence, status: PendingTxStatus.Observed }], + storage.data.get(`mockChainStorageRoot.status.${mockEvidence.txHash}`), + PendingTxStatus.Observed, 'OBSERVED status on insufficient pool funds', ); @@ -248,6 +240,7 @@ test('updates status to OBSERVED on insufficient pool funds', async t => { test('updates status to OBSERVED if makeChainAddress fails', async t => { const { + bootstrap: { storage }, extensions: { services: { advancer, statusManager }, helpers: { inspectLogs }, @@ -257,14 +250,9 @@ test('updates status to OBSERVED if makeChainAddress fails', async t => { const mockEvidence = MockCctpTxEvidences.AGORIC_UNKNOWN_EUD(); await advancer.handleTransactionEvent(mockEvidence); - const entries = statusManager.lookupPending( - mockEvidence.tx.forwardingAddress, - mockEvidence.tx.amount, - ); - t.deepEqual( - entries, - [{ ...mockEvidence, status: PendingTxStatus.Observed }], + storage.data.get(`mockChainStorageRoot.status.${mockEvidence.txHash}`), + PendingTxStatus.Observed, 'OBSERVED status on makeChainAddress failure', ); @@ -276,6 +264,7 @@ test('updates status to OBSERVED if makeChainAddress fails', async t => { test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t => { const { + bootstrap: { storage }, extensions: { services: { advancer, feeTools, statusManager }, helpers: { inspectLogs, inspectNotifyCalls }, @@ -291,14 +280,9 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t resolveLocalTransferV(); await eventLoopIteration(); - const entries = statusManager.lookupPending( - mockEvidence.tx.forwardingAddress, - mockEvidence.tx.amount, - ); - t.deepEqual( - entries, - [{ ...mockEvidence, status: PendingTxStatus.Advancing }], + storage.data.get(`mockChainStorageRoot.status.${mockEvidence.txHash}`), + PendingTxStatus.Advancing, 'tx is Advancing', ); @@ -333,6 +317,7 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t test('updates status to OBSERVED if pre-condition checks fail', async t => { const { + bootstrap: { storage }, extensions: { services: { advancer, statusManager }, helpers: { inspectLogs }, @@ -343,14 +328,9 @@ test('updates status to OBSERVED if pre-condition checks fail', async t => { await advancer.handleTransactionEvent(mockEvidence); - const entries = statusManager.lookupPending( - mockEvidence.tx.forwardingAddress, - mockEvidence.tx.amount, - ); - t.deepEqual( - entries, - [{ ...mockEvidence, status: PendingTxStatus.Observed }], + storage.data.get(`mockChainStorageRoot.status.${mockEvidence.txHash}`), + PendingTxStatus.Observed, 'tx is recorded as OBSERVED', ); diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index f900c7cd191..2c8075e9e2f 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -47,7 +47,7 @@ const makeTestContext = async t => { const { log, inspectLogs } = makeTestLogger(t.log); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - async () => common.commonPrivateArgs.storageNode.makeChildNode('status'), + common.commonPrivateArgs.storageNode.makeChildNode('status'), { log }, ); const { zcf, callLog } = mockZcf(zone.subZone('Mock ZCF')); diff --git a/packages/fast-usdc/test/exos/status-manager.test.ts b/packages/fast-usdc/test/exos/status-manager.test.ts index d37015f0d57..71757ea731a 100644 --- a/packages/fast-usdc/test/exos/status-manager.test.ts +++ b/packages/fast-usdc/test/exos/status-manager.test.ts @@ -10,7 +10,7 @@ import type { CctpTxEvidence } from '../../src/types.js'; type Common = Awaited>; type TestContext = { - makeStatusNode: () => Promise; + statusNode: ERef; storage: Common['bootstrap']['storage']; }; @@ -19,8 +19,7 @@ const test = anyTest as TestFn; test.beforeEach(async t => { const common = await commonSetup(t); t.context = { - makeStatusNode: async () => - common.commonPrivateArgs.storageNode.makeChildNode('status'), + statusNode: common.commonPrivateArgs.storageNode.makeChildNode('status'), storage: common.bootstrap.storage, }; }); @@ -29,7 +28,7 @@ test('advance creates new entry with ADVANCED status', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -47,7 +46,7 @@ test('ADVANCED transactions are published to vstorage', async t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -65,7 +64,7 @@ test('observe creates new entry with OBSERVED status', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); statusManager.observe(evidence); @@ -82,7 +81,7 @@ test('OBSERVED transactions are published to vstorage', async t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -100,19 +99,19 @@ test('cannot process same tx twice', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); statusManager.advance(evidence); t.throws(() => statusManager.advance(evidence), { message: - 'Transaction already seen: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"', + 'Transaction already seen: "seenTx:1:0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"', }); t.throws(() => statusManager.observe(evidence), { message: - 'Transaction already seen: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"', + 'Transaction already seen: "seenTx:1:0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702"', }); // new txHash should not throw @@ -125,7 +124,7 @@ test('isSeen checks if a tx has been processed', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -143,7 +142,7 @@ test('dequeueStatus removes entries from PendingTxs', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); @@ -195,7 +194,7 @@ test('cannot advanceOutcome without ADVANCING entry', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); const advanceOutcomeFn = () => @@ -224,7 +223,7 @@ test('advanceOutcome transitions to ADVANCED and ADVANCE_FAILED', async t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); const e2 = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); @@ -260,7 +259,7 @@ test('dequeueStatus returns undefined when nothing is settleable', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const e1 = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -274,7 +273,7 @@ test('dequeueStatus returns first (earliest) matched entry', async t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -371,7 +370,7 @@ test('lookupPending returns empty array when presented a key it has not seen', t const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); t.deepEqual(statusManager.lookupPending('noble123', 1n), []); }); @@ -380,7 +379,7 @@ test('StatusManagerKey logic handles addresses with hyphens', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager( zone.subZone('status-manager'), - t.context.makeStatusNode, + t.context.statusNode, ); const evidence: CctpTxEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); evidence.tx.forwardingAddress = 'noble1-foo';