diff --git a/packages/cosmic-proto/src/address-hooks.js b/packages/cosmic-proto/src/address-hooks.js index 24d28ad69a8..fcf55c9e8d2 100644 --- a/packages/cosmic-proto/src/address-hooks.js +++ b/packages/cosmic-proto/src/address-hooks.js @@ -192,6 +192,7 @@ harden(encodeAddressHook); * @param {string} addressHook * @param {number} [charLimit] * @returns {{ baseAddress: string; query: HookQuery }} + * @throws {Error} if no hook string or hook string does not start with `?` */ export const decodeAddressHook = (addressHook, charLimit) => { const { baseAddress, hookData } = splitHookedAddress(addressHook, charLimit); diff --git a/packages/fast-usdc/package.json b/packages/fast-usdc/package.json index 9f8e87b89ad..30e20a49253 100644 --- a/packages/fast-usdc/package.json +++ b/packages/fast-usdc/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@agoric/client-utils": "^0.1.0", + "@agoric/cosmic-proto": "^0.4.0", "@agoric/ertp": "^0.16.2", "@agoric/internal": "^0.3.2", "@agoric/notifier": "^0.6.2", diff --git a/packages/fast-usdc/src/cli/transfer.js b/packages/fast-usdc/src/cli/transfer.js index 9120f93be0a..41719ac1c37 100644 --- a/packages/fast-usdc/src/cli/transfer.js +++ b/packages/fast-usdc/src/cli/transfer.js @@ -6,6 +6,7 @@ import { makeVStorage, pickEndpoint, } from '@agoric/client-utils'; +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { queryFastUSDCLocalChainAccount } from '../util/agoric.js'; import { depositForBurn, makeProvider } from '../util/cctp.js'; import { @@ -22,7 +23,7 @@ import { const transfer = async ( /** @type {File} */ configFile, /** @type {string} */ amount, - /** @type {string} */ destination, + /** @type {string} */ EUD, out = console, fetch = globalThis.fetch, /** @type {VStorage | undefined} */ vstorage, @@ -39,13 +40,13 @@ const transfer = async ( { chainName: 'agoric', rpcAddrs: [pickEndpoint(netConfig)] }, ); const agoricAddr = await queryFastUSDCLocalChainAccount(vstorage, out); - const appendedAddr = `${agoricAddr}?EUD=${destination}`; - out.log(`forwarding destination ${appendedAddr}`); + const encodedAddr = encodeAddressHook(agoricAddr, { EUD }); + out.log(`forwarding destination ${encodedAddr}`); const { exists, address } = await queryForwardingAccount( config.nobleApi, config.nobleToAgoricChannel, - appendedAddr, + encodedAddr, out, fetch, ); @@ -58,13 +59,13 @@ const transfer = async ( signer, signerAddress, config.nobleToAgoricChannel, - appendedAddr, + encodedAddr, out, ); out.log(res); } catch (e) { out.error( - `Error registering noble forwarding account for ${appendedAddr} on channel ${config.nobleToAgoricChannel}`, + `Error registering noble forwarding account for ${encodedAddr} on channel ${config.nobleToAgoricChannel}`, ); throw e; } diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index 5fc01abfa38..c863d2d8cd3 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -1,3 +1,4 @@ +import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { AmountMath } from '@agoric/ertp'; import { assertAllDefined, makeTracer } from '@agoric/internal'; import { AnyNatAmountShape, ChainAddressShape } from '@agoric/orchestration'; @@ -5,13 +6,12 @@ import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; import { q } from '@endo/errors'; import { E } from '@endo/far'; -import { M } from '@endo/patterns'; +import { M, mustMatch } from '@endo/patterns'; import { CctpTxEvidenceShape, - EudParamShape, + AddressHookShape, EvmHashShape, } from '../type-guards.js'; -import { addressTools } from '../utils/address.js'; import { makeFeeTools } from '../utils/fees.js'; /** @@ -22,7 +22,7 @@ import { makeFeeTools } from '../utils/fees.js'; * @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js'; * @import {VowTools} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; - * @import {CctpTxEvidence, EvmHash, FeeConfig, LogFn, NobleAddress} from '../types.js'; + * @import {CctpTxEvidence, AddressHook, EvmHash, FeeConfig, LogFn, NobleAddress} from '../types.js'; * @import {StatusManager} from './status-manager.js'; * @import {LiquidityPoolKit} from './liquidity-pool.js'; */ @@ -148,11 +148,10 @@ export const prepareAdvancerKit = ( const { borrowerFacet, poolAccount } = this.state; const { recipientAddress } = evidence.aux; - // throws if EUD is not found - const { EUD } = addressTools.getQueryParams( - recipientAddress, - EudParamShape, - ); + const decoded = decodeAddressHook(recipientAddress); + mustMatch(decoded, AddressHookShape); + const { EUD } = /** @type {AddressHook['query']} */ (decoded.query); + log(`decoded EUD: ${EUD}`); // throws if the bech32 prefix is not found const destination = chainHub.makeChainAddress(EUD); diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index 5d092dd87cb..b1fc7159bca 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -5,8 +5,8 @@ import { atob } from '@endo/base64'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; +import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { PendingTxStatus } from '../constants.js'; -import { addressTools } from '../utils/address.js'; import { makeFeeTools } from '../utils/fees.js'; import { EvmHashShape } from '../type-guards.js'; @@ -151,14 +151,19 @@ export const prepareSettler = ( return; } - if (!addressTools.hasQueryParams(tx.receiver)) { - console.log('not query params', tx.receiver); - return; - } - - const { EUD } = addressTools.getQueryParams(tx.receiver); - if (!EUD) { - console.log('no EUD parameter', tx.receiver); + let EUD; + try { + ({ EUD } = decodeAddressHook(tx.receiver).query); + if (!EUD) { + log('no EUD parameter', tx.receiver); + return; + } + if (typeof EUD !== 'string') { + log('EUD is not a string', EUD); + return; + } + } catch (e) { + log('no query params', tx.receiver); return; } diff --git a/packages/fast-usdc/src/type-guards.js b/packages/fast-usdc/src/type-guards.js index a645f90bae4..4d5bc3e2092 100644 --- a/packages/fast-usdc/src/type-guards.js +++ b/packages/fast-usdc/src/type-guards.js @@ -6,7 +6,7 @@ import { PendingTxStatus } from './constants.js'; * @import {TypedPattern} from '@agoric/internal'; * @import {FastUsdcTerms} from './fast-usdc.contract.js'; * @import {USDCProposalShapes} from './pool-share-math.js'; - * @import {CctpTxEvidence, FeeConfig, PendingTx, PoolMetrics, ChainPolicy, FeedPolicy} from './types.js'; + * @import {CctpTxEvidence, FeeConfig, PendingTx, PoolMetrics, ChainPolicy, FeedPolicy, AddressHook} from './types.js'; */ /** @@ -67,10 +67,12 @@ export const PendingTxShape = { }; harden(PendingTxShape); -export const EudParamShape = { - EUD: M.string(), +/** @type {TypedPattern} */ +export const AddressHookShape = { + baseAddress: M.string(), + query: { EUD: M.string() }, }; -harden(EudParamShape); +harden(AddressHookShape); const NatAmountShape = { brand: BrandShape, value: M.nat() }; /** @type {TypedPattern} */ diff --git a/packages/fast-usdc/src/types.ts b/packages/fast-usdc/src/types.ts index ee9a90efffd..e9d976f5758 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -82,5 +82,14 @@ export type FastUSDCConfig = { assetInfo: [Denom, DenomDetail & { brandKey?: string }][]; } & CopyRecord; +/** decoded address hook parameters */ +export type AddressHook = { + baseAddress: string; + query: { + /** end user destination address */ + EUD: string; + }; +}; + export type * from './constants.js'; export type { LiquidityPoolKit } from './exos/liquidity-pool.js'; diff --git a/packages/fast-usdc/src/utils/address.js b/packages/fast-usdc/src/utils/address.js deleted file mode 100644 index a84e8f0c5c9..00000000000 --- a/packages/fast-usdc/src/utils/address.js +++ /dev/null @@ -1,71 +0,0 @@ -import { makeError, q } from '@endo/errors'; -import { M, mustMatch } from '@endo/patterns'; - -/** - * @import {Pattern} from '@endo/patterns'; - */ - -/** - * Default pattern matcher for `getQueryParams`. - * Does not assert keys exist, but ensures existing keys are strings. - */ -const QueryParamsShape = M.splitRecord( - {}, - {}, - M.recordOf(M.string(), M.string()), -); - -/** - * Very minimal 'URL query string'-like parser that handles: - * - Query string delimiter (?) - * - Key-value separator (=) - * - Query parameter separator (&) - * - * Does not handle: - * - Subpaths (`agoric1bech32addr/opt/account?k=v`) - * - URI encoding/decoding (`%20` -> ` `) - * - note: `decodeURIComponent` seems to be available in XS - * - Multiple question marks (foo?bar=1?baz=2) - * - Empty parameters (foo=) - * - Array parameters (`foo?k=v1&k=v2` -> k: [v1, v2]) - * - Parameters without values (foo&bar=2) - */ -export const addressTools = { - /** - * @param {string} address - * @returns {boolean} - */ - hasQueryParams: address => { - try { - const params = addressTools.getQueryParams(address); - return Object.keys(params).length > 0; - } catch { - return false; - } - }, - /** - * @param {string} address - * @param {Pattern} [shape] - * @returns {Record} - * @throws {Error} if the address cannot be parsed or params do not match `shape` - */ - getQueryParams: (address, shape = QueryParamsShape) => { - const parts = address.split('?'); - if (parts.length !== 2) { - throw makeError(`Unable to parse query params: ${q(address)}`); - } - /** @type {Record} */ - const result = {}; - const paramPairs = parts[1].split('&'); - for (const pair of paramPairs) { - const [key, value] = pair.split('='); - if (!key || !value) { - throw makeError(`Invalid parameter format in pair: ${q(pair)}`); - } - result[key] = value; - } - harden(result); - mustMatch(result, shape); - return result; - }, -}; diff --git a/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.md b/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.md index d88e5f3f969..156d050eba9 100644 --- a/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.md +++ b/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.md @@ -15,7 +15,7 @@ Generated by [AVA](https://avajs.dev). typeUrl: '/noble.forwarding.v1.MsgRegisterAccount', value: { channel: 'channel-test-7', - recipient: 'agoric123456?EUD=dydx1234', + recipient: 'agoric10rchp4vc53apxn32q42c3zryml8xq3xshyzuhjk6405wtxy7tl3d7e0f8az423pav3ukg7p3xgengqpq4066gy', signer: 'noble09876', }, }, diff --git a/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.snap b/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.snap index 4bce5856ec4..816eae48527 100644 Binary files a/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.snap and b/packages/fast-usdc/test/cli/snapshots/transfer.test.ts.snap differ diff --git a/packages/fast-usdc/test/cli/transfer.test.ts b/packages/fast-usdc/test/cli/transfer.test.ts index 729f40f63cc..28cd3d41569 100644 --- a/packages/fast-usdc/test/cli/transfer.test.ts +++ b/packages/fast-usdc/test/cli/transfer.test.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import transfer from '../../src/cli/transfer.js'; import { mockOut, @@ -63,14 +64,18 @@ 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 agoricSettlementAccount = + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek'; const settlementAccountVstoragePath = 'published.fastUsdc.settlementAccount'; const vstorageMock = makeVstorageMock({ [settlementAccountVstoragePath]: agoricSettlementAccount, }); const amount = '150'; - const destination = 'dydx1234'; - const nobleFwdAccountQuery = `${nobleApi}/noble/forwarding/v1/address/${nobleToAgoricChannel}/${agoricSettlementAccount}${encodeURIComponent('?EUD=')}${destination}/`; + const EUD = 'dydx1234'; + const nobleFwdAccountQuery = `${nobleApi}/noble/forwarding/v1/address/${nobleToAgoricChannel}/${encodeAddressHook( + agoricSettlementAccount, + { EUD }, + )}/`; const fetchMock = makeFetchMock({ [nobleFwdAccountQuery]: { address: 'noble14lwerrcfzkzrv626w49pkzgna4dtga8c5x479h', @@ -84,7 +89,7 @@ test('Transfer registers the noble forwarding account if it does not exist', asy await transfer.transfer( file, amount, - destination, + EUD, // @ts-expect-error mocking console out, fetchMock.fetch, @@ -114,14 +119,18 @@ 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 agoricSettlementAccount = + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek'; const settlementAccountVstoragePath = 'published.fastUsdc.settlementAccount'; const vstorageMock = makeVstorageMock({ [settlementAccountVstoragePath]: agoricSettlementAccount, }); const amount = '150'; - const destination = 'dydx1234'; - const nobleFwdAccountQuery = `${nobleApi}/noble/forwarding/v1/address/${nobleToAgoricChannel}/${agoricSettlementAccount}${encodeURIComponent('?EUD=')}${destination}/`; + const EUD = 'dydx1234'; + const nobleFwdAccountQuery = `${nobleApi}/noble/forwarding/v1/address/${nobleToAgoricChannel}/${encodeAddressHook( + agoricSettlementAccount, + { EUD }, + )}/`; const fetchMock = makeFetchMock({ [nobleFwdAccountQuery]: { address: 'noble14lwerrcfzkzrv626w49pkzgna4dtga8c5x479h', @@ -135,7 +144,7 @@ test('Transfer signs and broadcasts the depositForBurn message on Ethereum', asy await transfer.transfer( file, amount, - destination, + EUD, // @ts-expect-error mocking console out, fetchMock.fetch, diff --git a/packages/fast-usdc/test/exos/advancer.test.ts b/packages/fast-usdc/test/exos/advancer.test.ts index 908880d3fb4..3b8cd52c9e8 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -7,12 +7,15 @@ import { Far } from '@endo/pass-style'; import type { NatAmount } from '@agoric/ertp'; import { type ZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js'; import { q } from '@endo/errors'; +import { + decodeAddressHook, + encodeAddressHook, +} from '@agoric/cosmic-proto/address-hooks.js'; import { PendingTxStatus } from '../../src/constants.js'; import { prepareAdvancer } from '../../src/exos/advancer.js'; import type { SettlerKit } from '../../src/exos/settler.js'; import { prepareStatusManager } from '../../src/exos/status-manager.js'; import { makeFeeTools } from '../../src/utils/fees.js'; -import { addressTools } from '../../src/utils/address.js'; import { commonSetup } from '../supports.js'; import { MockCctpTxEvidences, intermediateRecipient } from '../fixtures.js'; import { @@ -180,10 +183,12 @@ test('updates status to ADVANCING in happy path', async t => { PendingTxStatus.Advancing, 'ADVANCED status in happy path', ); - - t.deepEqual(inspectLogs(0), [ - 'Advance transfer fulfilled', - '{"advanceAmount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + t.deepEqual(inspectLogs(), [ + ['decoded EUD: osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'], + [ + 'Advance transfer fulfilled', + '{"advanceAmount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + ], ]); // We expect to see an `Advanced` update, but that is now Settler's job. @@ -195,8 +200,7 @@ test('updates status to ADVANCING in happy path', async t => { forwardingAddress: mockEvidence.tx.forwardingAddress, fullAmount: usdc.make(mockEvidence.tx.amount), destination: { - value: addressTools.getQueryParams(mockEvidence.aux.recipientAddress) - .EUD, + value: decodeAddressHook(mockEvidence.aux.recipientAddress).query.EUD, }, }, true, // indicates transfer succeeded @@ -232,9 +236,12 @@ test('updates status to OBSERVED on insufficient pool funds', async t => { 'OBSERVED status on insufficient pool funds', ); - t.deepEqual(inspectLogs(0), [ - 'Advancer error:', - '"[Error: Cannot borrow. Requested {\\"brand\\":\\"[Alleged: USDC brand]\\",\\"value\\":\\"[294999999n]\\"} must be less than pool balance {\\"brand\\":\\"[Alleged: USDC brand]\\",\\"value\\":\\"[1n]\\"}.]"', + t.deepEqual(inspectLogs(), [ + ['decoded EUD: dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'], + [ + 'Advancer error:', + '"[Error: Cannot borrow. Requested {\\"brand\\":\\"[Alleged: USDC brand]\\",\\"value\\":\\"[294999999n]\\"} must be less than pool balance {\\"brand\\":\\"[Alleged: USDC brand]\\",\\"value\\":\\"[1n]\\"}.]"', + ], ]); }); @@ -256,9 +263,12 @@ test('updates status to OBSERVED if makeChainAddress fails', async t => { 'OBSERVED status on makeChainAddress failure', ); - t.deepEqual(inspectLogs(0), [ - 'Advancer error:', - '"[Error: Chain info not found for bech32Prefix \\"random\\"]"', + t.deepEqual(inspectLogs(), [ + ['decoded EUD: random1addr'], + [ + 'Advancer error:', + '"[Error: Chain info not found for bech32Prefix \\"random\\"]"', + ], ]); }); @@ -289,9 +299,9 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t mockPoolAccount.transferVResolver.reject(new Error('simulated error')); await eventLoopIteration(); - t.deepEqual(inspectLogs(0), [ - 'Advance transfer rejected', - '"[Error: simulated error]"', + t.deepEqual(inspectLogs(), [ + ['decoded EUD: dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'], + ['Advance transfer rejected', '"[Error: simulated error]"'], ]); // We expect to see an `AdvancedFailed` update, but that is now Settler's job. @@ -306,8 +316,7 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t usdc.make(mockEvidence.tx.amount), ), destination: { - value: addressTools.getQueryParams(mockEvidence.aux.recipientAddress) - .EUD, + value: decodeAddressHook(mockEvidence.aux.recipientAddress).query.EUD, }, }, false, // this indicates transfer failed @@ -334,9 +343,30 @@ test('updates status to OBSERVED if pre-condition checks fail', async t => { 'tx is recorded as OBSERVED', ); - t.deepEqual(inspectLogs(0), [ - 'Advancer error:', - '"[Error: Unable to parse query params: \\"agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek\\"]"', + t.deepEqual(inspectLogs(), [ + [ + 'Advancer error:', + '"[Error: query: {} - Must have missing properties [\\"EUD\\"]]"', + ], + ]); + + await advancer.handleTransactionEvent({ + ...MockCctpTxEvidences.AGORIC_NO_PARAMS( + encodeAddressHook( + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + { EUD: 'osmo1234', extra: 'value' }, + ), + ), + txHash: + '0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', + }); + + const [, ...remainingLogs] = inspectLogs(); + t.deepEqual(remainingLogs, [ + [ + 'Advancer error:', + '"[Error: query: {\\"EUD\\":\\"osmo1234\\",\\"extra\\":\\"value\\"} - Must not have unexpected properties: [\\"extra\\"]]"', + ], ]); }); @@ -356,18 +386,23 @@ test('will not advance same txHash:chainId evidence twice', async t => { resolveLocalTransferV(); mockPoolAccount.transferVResolver.resolve(); await eventLoopIteration(); - - t.deepEqual(inspectLogs(0), [ - 'Advance transfer fulfilled', - '{"advanceAmount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + t.deepEqual(inspectLogs(), [ + ['decoded EUD: osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men'], + [ + 'Advance transfer fulfilled', + '{"advanceAmount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + ], ]); // Second attempt void advancer.handleTransactionEvent(mockEvidence); await eventLoopIteration(); - t.deepEqual(inspectLogs(1), [ - 'txHash already seen:', - '0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702', + const [, , ...remainingLogs] = inspectLogs(); + t.deepEqual(remainingLogs, [ + [ + 'txHash already seen:', + '0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702', + ], ]); }); diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index 872bffc2c0a..f7869f3def1 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -27,11 +27,14 @@ import { E } from '@endo/far'; import { matches, objectMap } from '@endo/patterns'; import { makePromiseKit } from '@endo/promise-kit'; import path from 'path'; +import { + decodeAddressHook, + encodeAddressHook, +} from '@agoric/cosmic-proto/address-hooks.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'; import type { CctpTxEvidence, FeeConfig, PoolMetrics } from '../src/types.js'; -import { addressTools } from '../src/utils/address.js'; import { makeFeeTools } from '../src/utils/fees.js'; import { MockCctpTxEvidences } from './fixtures.js'; import { commonSetup, uusdcOnAgoric } from './supports.js'; @@ -382,7 +385,7 @@ const makeCustomer = ( return enough; }, sendFast: async (t: ExecutionContext, amount: bigint, EUD: string) => { - const recipientAddress = `${settleAddr}?EUD=${EUD}`; + const recipientAddress = encodeAddressHook(settleAddr, { EUD }); // KLUDGE: UI would ask noble for a forwardingAddress // "cctp" here has some noble stuff mixed in. const tx = cctp.makeTx(amount, recipientAddress); @@ -411,9 +414,7 @@ const makeCustomer = ( t.deepEqual(bank, []); // no vbank GIVE / GRAB } - const { EUD } = addressTools.getQueryParams( - evidence.aux.recipientAddress, - ); + const { EUD } = decodeAddressHook(evidence.aux.recipientAddress).query; const myMsg = local.find(lm => { if (lm.type !== 'VLOCALCHAIN_EXECUTE_TX') return false; @@ -442,7 +443,7 @@ const makeCustomer = ( 'C4', ); t.log(who, 'sees', ibcTransferMsg.token, 'sent to', EUD); - if (!EUD.startsWith('noble')) { + if (!(EUD as string).startsWith('noble')) { t.like( JSON.parse(ibcTransferMsg.memo), { diff --git a/packages/fast-usdc/test/fixtures.ts b/packages/fast-usdc/test/fixtures.ts index cbd83ccb657..7ffcedafb73 100644 --- a/packages/fast-usdc/test/fixtures.ts +++ b/packages/fast-usdc/test/fixtures.ts @@ -1,7 +1,8 @@ -import type { VTransferIBCEvent } from '@agoric/vats'; +import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js'; import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; import type { ChainAddress } from '@agoric/orchestration'; +import type { VTransferIBCEvent } from '@agoric/vats'; import type { CctpTxEvidence } from '../src/types.js'; const mockScenarios = [ @@ -31,7 +32,10 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + encodeAddressHook( + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + { EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }, + ), }, chainId: 1, }), @@ -49,7 +53,10 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + encodeAddressHook( + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + { EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men' }, + ), }, chainId: 1, }), @@ -85,7 +92,10 @@ export const MockCctpTxEvidences: Record< forwardingChannel: 'channel-21', recipientAddress: receiverAddress || - 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=random1addr', + encodeAddressHook( + 'agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek', + { EUD: 'random1addr' }, + ), }, chainId: 1, }), diff --git a/packages/fast-usdc/test/utils/address.test.ts b/packages/fast-usdc/test/utils/address.test.ts deleted file mode 100644 index d1c6ea23a3f..00000000000 --- a/packages/fast-usdc/test/utils/address.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { M } from '@endo/patterns'; - -import { addressTools } from '../../src/utils/address.js'; -import { EudParamShape } from '../../src/type-guards.js'; - -const FIXTURES = { - AGORIC_WITH_DYDX: - 'agoric1bech32addr?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - AGORIC_WITH_OSMO: - 'agoric1bech32addr?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - AGORIC_WITH_MULTIPLE: - 'agoric1bech32addr?EUD=osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men&CID=dydx-mainnet-1', - AGORIC_NO_PARAMS: 'agoric1bech32addr', - INVALID_MULTIPLE_QUESTION: 'agoric1bech32addr?param1=value1?param2=value2', - INVALID_PARAM_FORMAT: 'agoric1bech32addr?invalidparam', -} as const; - -// hasQueryParams tests -test('hasQueryParams: returns true when address has parameters', t => { - t.true(addressTools.hasQueryParams(FIXTURES.AGORIC_WITH_DYDX)); - t.true(addressTools.hasQueryParams(FIXTURES.AGORIC_WITH_OSMO)); - t.true(addressTools.hasQueryParams(FIXTURES.AGORIC_WITH_MULTIPLE)); -}); - -test('hasQueryParams: returns false when address has no parameters', t => { - t.false(addressTools.hasQueryParams(FIXTURES.AGORIC_NO_PARAMS)); -}); - -test('hasQueryParams: returns false for invalid parameter formats', t => { - t.false(addressTools.hasQueryParams(FIXTURES.INVALID_MULTIPLE_QUESTION)); - t.false(addressTools.hasQueryParams(FIXTURES.INVALID_PARAM_FORMAT)); -}); - -// getQueryParams tests - positive cases -test('getQueryParams: correctly parses address with single EUD parameter', t => { - const result = addressTools.getQueryParams( - FIXTURES.AGORIC_WITH_DYDX, - EudParamShape, - ); - t.deepEqual(result, { - EUD: 'dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - }); -}); - -test('getQueryParams: correctly parses address with multiple parameters', t => { - const pattern = harden({ EUD: M.string(), CID: M.string() }); - const result = addressTools.getQueryParams( - FIXTURES.AGORIC_WITH_MULTIPLE, - pattern, - ); - t.deepEqual(result, { - EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - CID: 'dydx-mainnet-1', - }); -}); - -test('getQueryParams: returns all parameters when no shape is provided', t => { - const result = addressTools.getQueryParams(FIXTURES.AGORIC_WITH_MULTIPLE); - t.deepEqual(result, { - EUD: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - CID: 'dydx-mainnet-1', - }); -}); - -test('getQueryParams: correctly handles address with no parameters', t => { - t.throws(() => addressTools.getQueryParams(FIXTURES.AGORIC_NO_PARAMS), { - message: 'Unable to parse query params: "agoric1bech32addr"', - }); -}); - -// getQueryParams tests - negative cases -test('getQueryParams: throws error for multiple question marks', t => { - t.throws( - () => addressTools.getQueryParams(FIXTURES.INVALID_MULTIPLE_QUESTION), - { - message: - 'Unable to parse query params: "agoric1bech32addr?param1=value1?param2=value2"', - }, - ); -}); - -test('getQueryParams: throws error for invalid parameter format', t => { - t.throws(() => addressTools.getQueryParams(FIXTURES.INVALID_PARAM_FORMAT), { - message: 'Invalid parameter format in pair: "invalidparam"', - }); -});