From 2ab028674747e20b64b18825c250781b4533347b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 17 Dec 2024 18:29:49 -0500 Subject: [PATCH] chore: `advancer` validates `settlementAddress` --- .../boot/test/fast-usdc/fast-usdc.test.ts | 9 ++- packages/fast-usdc/src/exos/advancer.js | 22 +++++- packages/fast-usdc/src/fast-usdc.contract.js | 1 + packages/fast-usdc/test/cli/transfer.test.ts | 7 +- packages/fast-usdc/test/exos/advancer.test.ts | 75 +++++++++++++++++-- .../fast-usdc/test/fast-usdc.contract.test.ts | 68 ++++++++++++----- packages/fast-usdc/test/fixtures.ts | 31 ++++---- packages/fast-usdc/test/supports.ts | 7 +- 8 files changed, 170 insertions(+), 50 deletions(-) diff --git a/packages/boot/test/fast-usdc/fast-usdc.test.ts b/packages/boot/test/fast-usdc/fast-usdc.test.ts index 3c17e8b6f55..04db95f9c78 100644 --- a/packages/boot/test/fast-usdc/fast-usdc.test.ts +++ b/packages/boot/test/fast-usdc/fast-usdc.test.ts @@ -13,6 +13,7 @@ import { } from '@agoric/internal/src/storage-test-utils.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { BridgeId } from '@agoric/internal'; +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { makeWalletFactoryContext, type WalletFactoryTestContext, @@ -233,7 +234,13 @@ test.serial('makes usdc advance', async t => { }); await eventLoopIteration(); - const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + const accountsData = storage.data.get('published.fastUsdc'); + const { settlementAccount } = JSON.parse(JSON.parse(accountsData!).values[0]); + const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO( + encodeAddressHook(settlementAccount, { + EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }), + ); harness?.useRunPolicy(true); await Promise.all( diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index f63c2b7474c..65afb222af7 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -6,6 +6,7 @@ import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; import { E } from '@endo/far'; import { M, mustMatch } from '@endo/patterns'; +import { Fail, q } from '@endo/errors'; import { CctpTxEvidenceShape, AddressHookShape, @@ -56,6 +57,7 @@ const AdvancerKitI = harden({ advancer: M.interface('AdvancerI', { handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(), setIntermediateRecipient: M.call(ChainAddressShape).returns(), + setSettlementAddress: M.call(ChainAddressShape).returns(), }), depositHandler: M.interface('DepositHandlerI', { onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns(VowShape), @@ -117,13 +119,15 @@ export const prepareAdvancerKit = ( * borrowerFacet: LiquidityPoolKit['borrower']; * poolAccount: HostInterface>; * intermediateRecipient?: ChainAddress; + * settlementAddress?: ChainAddress; * }} config */ config => harden({ ...config, - // make sure the state record has this property, perhaps with an undefined value + // make sure the state record has these properties, perhaps with an undefined value intermediateRecipient: config.intermediateRecipient, + settlementAddress: config.settlementAddress, }), { advancer: { @@ -145,10 +149,17 @@ export const prepareAdvancerKit = ( return; } - const { borrowerFacet, poolAccount } = this.state; + const { borrowerFacet, poolAccount, settlementAddress } = + this.state; const { recipientAddress } = evidence.aux; const decoded = decodeAddressHook(recipientAddress); mustMatch(decoded, AddressHookShape); + if (!settlementAddress?.value) { + throw Fail`⚠️ No 'settlementAddress'. must call 'publishAddresses' first.`; + } + if (decoded.baseAddress !== settlementAddress.value) { + throw Fail`⚠️ baseAddress of address hook ${q(decoded.baseAddress)} does not match the expected address ${q(settlementAddress.value)}`; + } const { EUD } = /** @type {AddressHook['query']} */ (decoded.query); log(`decoded EUD: ${EUD}`); // throws if the bech32 prefix is not found @@ -172,10 +183,10 @@ export const prepareAdvancerKit = ( harden({ USDC: advanceAmount }), ); void watch(depositV, this.facets.depositHandler, { - fullAmount, advanceAmount, destination, forwardingAddress: evidence.tx.forwardingAddress, + fullAmount, tmpSeat, txHash: evidence.txHash, }); @@ -188,6 +199,10 @@ export const prepareAdvancerKit = ( setIntermediateRecipient(intermediateRecipient) { this.state.intermediateRecipient = intermediateRecipient; }, + /** @param {ChainAddress} settlementAddress */ + setSettlementAddress(settlementAddress) { + this.state.settlementAddress = settlementAddress; + }, }, depositHandler: { /** @@ -271,6 +286,7 @@ export const prepareAdvancerKit = ( borrowerFacet: M.remotable(), poolAccount: M.remotable(), intermediateRecipient: M.opt(ChainAddressShape), + settlementAddress: M.opt(ChainAddressShape), }), }, ); diff --git a/packages/fast-usdc/src/fast-usdc.contract.js b/packages/fast-usdc/src/fast-usdc.contract.js index b9629bb05c3..fae082f9dd7 100644 --- a/packages/fast-usdc/src/fast-usdc.contract.js +++ b/packages/fast-usdc/src/fast-usdc.contract.js @@ -186,6 +186,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { E(settlementAccount).getAddress(), ]), ); + advancer.setSettlementAddress(settlementAccountAddress); const addresses = harden({ poolAccount: poolAccountAddress.value, settlementAccount: settlementAccountAddress.value, diff --git a/packages/fast-usdc/test/cli/transfer.test.ts b/packages/fast-usdc/test/cli/transfer.test.ts index 28cd3d41569..dcaec6476a6 100644 --- a/packages/fast-usdc/test/cli/transfer.test.ts +++ b/packages/fast-usdc/test/cli/transfer.test.ts @@ -8,6 +8,7 @@ import { makeFetchMock, makeMockSigner, } from '../../testing/mocks.js'; +import { settlementAddress } from '../fixtures.js'; test('Errors if config missing', async t => { const path = 'config/dir/.fast-usdc/config.json'; @@ -64,8 +65,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 = - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek'; + const agoricSettlementAccount = settlementAddress.value; const settlementAccountVstoragePath = 'published.fastUsdc.settlementAccount'; const vstorageMock = makeVstorageMock({ [settlementAccountVstoragePath]: agoricSettlementAccount, @@ -119,8 +119,7 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy }; const out = mockOut(); const file = mockFile(path, JSON.stringify(config)); - const agoricSettlementAccount = - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek'; + const agoricSettlementAccount = settlementAddress.value; 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 4bb122220df..f070031f938 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -19,7 +19,11 @@ import type { SettlerKit } from '../../src/exos/settler.js'; import { prepareStatusManager } from '../../src/exos/status-manager.js'; import type { LiquidityPoolKit } from '../../src/types.js'; import { makeFeeTools } from '../../src/utils/fees.js'; -import { MockCctpTxEvidences, intermediateRecipient } from '../fixtures.js'; +import { + MockCctpTxEvidences, + settlementAddress, + intermediateRecipient, +} from '../fixtures.js'; import { makeTestFeeConfig, makeTestLogger, @@ -127,6 +131,7 @@ const createTestExtensions = (t, common: CommonSetup) => { notifyFacet: mockNotifyF, poolAccount: mockAccounts.mockPoolAccount.account, intermediateRecipient, + settlementAddress, }); return { @@ -141,6 +146,7 @@ const createTestExtensions = (t, common: CommonSetup) => { }, mocks: { ...mockAccounts, + mockBorrowerF, mockNotifyF, resolveLocalTransferV, rejectLocalTransfeferV, @@ -260,6 +266,7 @@ test('updates status to OBSERVED on insufficient pool funds', async t => { notifyFacet: mockNotifyF, poolAccount: mockPoolAccount.account, intermediateRecipient, + settlementAddress, }); const evidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); @@ -393,10 +400,10 @@ test('updates status to OBSERVED if pre-condition checks fail', async t => { await advancer.handleTransactionEvent({ ...MockCctpTxEvidences.AGORIC_NO_PARAMS( - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'osmo1234', extra: 'value' }, - ), + encodeAddressHook(settlementAddress.value, { + EUD: 'osmo1234', + extra: 'value', + }), ), txHash: '0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', @@ -550,6 +557,7 @@ test('alerts if `returnToPool` fallback fails', async t => { notifyFacet: mockNotifyF, poolAccount: mockPoolAccount.account, intermediateRecipient, + settlementAddress, }); const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); @@ -588,3 +596,60 @@ test('alerts if `returnToPool` fallback fails', async t => { 'Advancing tx is recorded as AdvanceFailed', ); }); + +test('rejects advances to unknown settlementAccount', async t => { + const { + extensions: { + services: { advancer }, + helpers: { inspectLogs }, + }, + } = t.context; + + const invalidSettlementAcct = + 'agoric1ax7hmw49tmqrdld7emc5xw3wf43a49rtkacr9d5nfpqa0y7k6n0sl8v94h'; + t.not(settlementAddress.value, invalidSettlementAcct); + const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO( + encodeAddressHook(invalidSettlementAcct, { + EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }), + ); + + void advancer.handleTransactionEvent(mockEvidence); + await eventLoopIteration(); + t.deepEqual(inspectLogs(), [ + [ + 'Advancer error:', + Error( + '⚠️ baseAddress of address hook "agoric1ax7hmw49tmqrdld7emc5xw3wf43a49rtkacr9d5nfpqa0y7k6n0sl8v94h" does not match the expected address "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek"', + ), + ], + ]); +}); + +test('does not advance without settlementAddress set', async t => { + const { + extensions: { + services: { makeAdvancer }, + helpers: { inspectLogs }, + mocks: { mockPoolAccount, mockNotifyF, mockBorrowerF }, + }, + } = t.context; + + // make a new advancer without setting `settlementAddress` + const advancer = makeAdvancer({ + borrowerFacet: mockBorrowerF, + notifyFacet: mockNotifyF, + poolAccount: mockPoolAccount.account, + intermediateRecipient, + }); + const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + + void advancer.handleTransactionEvent(mockEvidence); + await eventLoopIteration(); + t.deepEqual(inspectLogs(), [ + [ + 'Advancer error:', + Error("⚠️ No 'settlementAddress'. must call 'publishAddresses' first."), + ], + ]); +}); diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index f7869f3def1..faac72d1399 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -31,6 +31,8 @@ import { decodeAddressHook, encodeAddressHook, } from '@agoric/cosmic-proto/address-hooks.js'; +import { makeTestAddress } from '@agoric/cosmic-proto/tools/make-test-address.js'; +import type { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; import type { OperatorKit } from '../src/exos/operator-kit.js'; import type { FastUsdcSF } from '../src/fast-usdc.contract.js'; import { PoolMetricsShape } from '../src/type-guards.js'; @@ -135,12 +137,15 @@ const makeTestContext = async (t: ExecutionContext) => { }; const mint = async (e: CctpTxEvidence) => { - const settlerAddr = 'agoric1fakeLCAAddress1'; // TODO: get from contract - const rxd = await receiveUSDCAt(settlerAddr, e.tx.amount); + const accountsData = common.bootstrap.storage.data.get('fun'); + const { settlementAccount } = JSON.parse( + JSON.parse(accountsData!).values[0], + ); + const rxd = await receiveUSDCAt(settlementAccount, e.tx.amount); await VE(transferBridge).fromBridge( buildVTransferEvent({ receiver: e.aux.recipientAddress, - target: settlerAddr, + target: settlementAccount, sourceChannel: agToNoble.transferChannel.counterPartyChannelId, denom: 'uusdc', amount: e.tx.amount, @@ -189,8 +194,8 @@ test('getStaticInfo', async t => { t.deepEqual(await E(publicFacet).getStaticInfo(), { addresses: { - poolAccount: 'agoric1fakeLCAAddress', - settlementAccount: 'agoric1fakeLCAAddress1', + poolAccount: makeTestAddress(), + settlementAccount: makeTestAddress(1), }, }); }); @@ -336,7 +341,6 @@ const makeLP = async ( }; const makeEVM = (template = MockCctpTxEvidences.AGORIC_PLUS_OSMO()) => { - const [settleAddr] = template.aux.recipientAddress.split('?'); let nonce = 0; const makeTx = (amount: bigint, recipientAddress: string): CctpTxEvidence => { @@ -363,15 +367,12 @@ const makeCustomer = ( cctp: ReturnType['cctp'], txPublisher: Publisher, feeConfig: FeeConfig, // TODO: get from vstorage (or at least: a subscriber) + storage: ReturnType, ) => { const USDC = feeConfig.flat.brand; const feeTools = makeFeeTools(feeConfig); const sent = [] as CctpTxEvidence[]; - // TODO: get settlerAddr from vstorage - const [settleAddr] = - MockCctpTxEvidences.AGORIC_PLUS_OSMO().aux.recipientAddress.split('?'); - const me = harden({ checkPoolAvailable: async ( t: ExecutionContext, @@ -385,7 +386,11 @@ const makeCustomer = ( return enough; }, sendFast: async (t: ExecutionContext, amount: bigint, EUD: string) => { - const recipientAddress = encodeAddressHook(settleAddr, { EUD }); + const accountsData = storage.data.get('fun'); + const { settlementAccount } = JSON.parse( + JSON.parse(accountsData!).values[0], + ); + const recipientAddress = encodeAddressHook(settlementAccount, { EUD }); // KLUDGE: UI would ask noble for a forwardingAddress // "cctp" here has some noble stuff mixed in. const tx = cctp.makeTx(amount, recipientAddress); @@ -524,6 +529,7 @@ test.serial('C25 - LPs can deposit USDC', async t => { test.serial('STORY01: advancing happy path for 100 USDC', async t => { const { common: { + bootstrap: { storage }, brands: { usdc }, commonPrivateArgs: { feeConfig }, utils: { inspectBankBridge, transmitTransferAck }, @@ -533,7 +539,7 @@ test.serial('STORY01: advancing happy path for 100 USDC', async t => { bridges: { snapshot, since }, mint, } = t.context; - const cust1 = makeCustomer('Carl', cctp, txPub.publisher, feeConfig); + const cust1 = makeCustomer('Carl', cctp, txPub.publisher, feeConfig, storage); const bridgePos = snapshot(); const sent1 = await cust1.sendFast(t, 108_000_000n, 'osmo1234advanceHappy'); @@ -549,7 +555,7 @@ test.serial('STORY01: advancing happy path for 100 USDC', async t => { t.deepEqual(inspectBankBridge().at(-1), { amount: String(expectedAdvance.value), denom: uusdcOnAgoric, - recipient: 'agoric1fakeLCAAddress', + recipient: makeTestAddress(), type: 'VBANK_GIVE', }); @@ -654,6 +660,7 @@ test.serial('With 250 available, 3 race to get ~100', async t => { bridges: { snapshot, since }, evm: { cctp, txPub }, common: { + bootstrap: { storage }, commonPrivateArgs: { feeConfig }, utils: { transmitTransferAck }, }, @@ -662,9 +669,9 @@ test.serial('With 250 available, 3 race to get ~100', async t => { } = t.context; const cust = { - racer1: makeCustomer('Racer1', cctp, txPub.publisher, feeConfig), - racer2: makeCustomer('Racer2', cctp, txPub.publisher, feeConfig), - racer3: makeCustomer('Racer3', cctp, txPub.publisher, feeConfig), + racer1: makeCustomer('Racer1', cctp, txPub.publisher, feeConfig, storage), + racer2: makeCustomer('Racer2', cctp, txPub.publisher, feeConfig, storage), + racer3: makeCustomer('Racer3', cctp, txPub.publisher, feeConfig, storage), }; await cust.racer3.checkPoolAvailable(t, 125_000_000n, metricsSub); @@ -709,12 +716,19 @@ test.serial('STORY09: insufficient liquidity: no FastUSDC option', async t => { // MarketMaker’s account const { common: { + bootstrap: { storage }, commonPrivateArgs: { feeConfig }, }, evm: { cctp, txPub }, startKit: { metricsSub }, } = t.context; - const early = makeCustomer('Unice', cctp, txPub.publisher, feeConfig); + const early = makeCustomer( + 'Unice', + cctp, + txPub.publisher, + feeConfig, + storage, + ); const available = await early.checkPoolAvailable(t, 5_000_000n, metricsSub); t.false(available); }); @@ -722,6 +736,7 @@ test.serial('STORY09: insufficient liquidity: no FastUSDC option', async t => { test.serial('C20 - Contract MUST function with an empty pool', async t => { const { common: { + bootstrap: { storage }, commonPrivateArgs: { feeConfig }, utils: { transmitTransferAck }, }, @@ -730,7 +745,13 @@ test.serial('C20 - Contract MUST function with an empty pool', async t => { bridges: { snapshot, since }, mint, } = t.context; - const custEmpty = makeCustomer('Earl', cctp, txPub.publisher, feeConfig); + const custEmpty = makeCustomer( + 'Earl', + cctp, + txPub.publisher, + feeConfig, + storage, + ); const bridgePos = snapshot(); const sent = await custEmpty.sendFast(t, 150_000_000n, 'osmo123'); const bridgeTraffic = since(bridgePos); @@ -753,6 +774,7 @@ test.serial('Settlement for unknown transaction (operator down)', async t => { bridges: { snapshot, since }, evm: { cctp, txPub }, common: { + bootstrap: { storage }, commonPrivateArgs: { feeConfig }, utils: { transmitTransferAck }, }, @@ -760,7 +782,13 @@ test.serial('Settlement for unknown transaction (operator down)', async t => { } = t.context; const operators = await sync.ocw.promise; - const opDown = makeCustomer('Otto', cctp, txPub.publisher, feeConfig); + const opDown = makeCustomer( + 'Otto', + cctp, + txPub.publisher, + feeConfig, + storage, + ); // what removeOperator will do await E(E.get(E(operators[1]).getKit()).admin).disable(); @@ -779,7 +807,7 @@ test.serial('Settlement for unknown transaction (operator down)', async t => { }, { amount: '20000000', - recipient: 'agoric1fakeLCAAddress1', + recipient: 'agoric1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc09z0g', type: 'VBANK_GIVE', }, ], diff --git a/packages/fast-usdc/test/fixtures.ts b/packages/fast-usdc/test/fixtures.ts index 35ec0c231e2..bc5ff0874c7 100644 --- a/packages/fast-usdc/test/fixtures.ts +++ b/packages/fast-usdc/test/fixtures.ts @@ -37,10 +37,9 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }, - ), + encodeAddressHook(settlementAddress.value, { + EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }), }, chainId: 1, }), @@ -59,10 +58,9 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }, - ), + encodeAddressHook(settlementAddress.value, { + EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }), }, chainId: 1, }), @@ -79,9 +77,7 @@ export const MockCctpTxEvidences: Record< }, aux: { forwardingChannel: 'channel-21', - recipientAddress: - receiverAddress || - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + recipientAddress: receiverAddress || settlementAddress.value, }, chainId: 1, }), @@ -100,10 +96,9 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - encodeAddressHook( - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', - { EUD: 'random1addr' }, - ), + encodeAddressHook(settlementAddress.value, { + EUD: 'random1addr', + }), }, chainId: 1, }), @@ -166,3 +161,9 @@ export const intermediateRecipient: ChainAddress = harden({ value: 'noble1test', encoding: 'bech32', }); + +export const settlementAddress: ChainAddress = harden({ + chainId: 'agoric-3', + encoding: 'bech32' as const, + value: 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', +}); diff --git a/packages/fast-usdc/test/supports.ts b/packages/fast-usdc/test/supports.ts index 984352b3c07..d5fa073828a 100644 --- a/packages/fast-usdc/test/supports.ts +++ b/packages/fast-usdc/test/supports.ts @@ -40,6 +40,7 @@ import { makeHeapZone, type Zone } from '@agoric/zone'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { E } from '@endo/far'; import type { ExecutionContext } from 'ava'; +import { makeTestAddress } from '@agoric/cosmic-proto/tools/make-test-address.js'; import { makeTestFeeConfig } from './mocks.js'; export { @@ -139,8 +140,10 @@ export const commonSetup = async (t: ExecutionContext) => { await E(transferBridge).initHandler(bridgeTargetKit.bridgeHandler); const localBridgeMessages = [] as any[]; - const localchainBridge = makeFakeLocalchainBridge(rootZone, obj => - localBridgeMessages.push(obj), + const localchainBridge = makeFakeLocalchainBridge( + rootZone, + obj => localBridgeMessages.push(obj), + makeTestAddress, ); const localchain = prepareLocalChainTools( rootZone.subZone('localchain'),