diff --git a/packages/cosmic-proto/src/address-hooks.js b/packages/cosmic-proto/src/address-hooks.js index aaa119f43a1..c1dcb2e415a 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 4fc29f6c253..f63c2b7474c 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -1,16 +1,16 @@ +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'; import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; 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'; /** @@ -21,7 +21,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'; */ @@ -147,11 +147,9 @@ 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 b8952d8086a..931961c01b7 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 ebc50505267..5281521ef12 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 eac08e8a5b5..30dd64acbbc 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -85,5 +85,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 d724e4a1cf8..b91e52485a9 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -6,13 +6,16 @@ import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; import { Far } from '@endo/pass-style'; import type { NatAmount } from '@agoric/ertp'; import { type ZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js'; -import { Fail, q } from '@endo/errors'; +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 { @@ -215,8 +218,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 @@ -344,8 +346,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 @@ -373,10 +374,29 @@ test('updates status to OBSERVED if pre-condition checks fail', async t => { ); 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( - 'Unable to parse query params: "agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek"', + 'query: {"EUD":"osmo1234","extra":"value"} - Must not have unexpected properties: ["extra"]', ), ], ]); 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"', - }); -});