diff --git a/packages/builders/test/snapshots/orchestration-imports.test.js.md b/packages/builders/test/snapshots/orchestration-imports.test.js.md index 4030cf35908..4e09d751ad8 100644 --- a/packages/builders/test/snapshots/orchestration-imports.test.js.md +++ b/packages/builders/test/snapshots/orchestration-imports.test.js.md @@ -167,6 +167,9 @@ Generated by [AVA](https://avajs.dev). bech32Prefix: Object @match:string { payload: [], }, + cctpDestinationDomain: Object @match:kind { + payload: 'number', + }, connections: Object @match:recordOf { payload: [ Object @match:any { diff --git a/packages/builders/test/snapshots/orchestration-imports.test.js.snap b/packages/builders/test/snapshots/orchestration-imports.test.js.snap index 44d4ef6c72d..6f84b2bb41b 100644 Binary files a/packages/builders/test/snapshots/orchestration-imports.test.js.snap and b/packages/builders/test/snapshots/orchestration-imports.test.js.snap differ diff --git a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md index 06fdfc8ca41..517b58ef478 100644 --- a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md +++ b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md @@ -38,6 +38,11 @@ Generated by [AVA](https://avajs.dev). brandDenom: { 'Alleged: USDC brand': 'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9', }, + chainIdToChainName: { + 'agoric-3': 'agoric', + 'noble-1': 'noble', + 'osmosis-1': 'osmosis', + }, chainInfos: { agoric: { bech32Prefix: 'agoric', @@ -1593,6 +1598,11 @@ Generated by [AVA](https://avajs.dev). brandDenom: { 'Alleged: USDC brand': 'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9', }, + chainIdToChainName: { + 'agoric-3': 'agoric', + 'noble-1': 'noble', + 'osmosis-1': 'osmosis', + }, chainInfos: { agoric: { bech32Prefix: 'agoric', diff --git a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap index eb51e85e3f4..cdf8ac5bd53 100644 Binary files a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap and b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap differ diff --git a/packages/orchestration/package.json b/packages/orchestration/package.json index 6a87081605d..b76c33375b7 100644 --- a/packages/orchestration/package.json +++ b/packages/orchestration/package.json @@ -46,6 +46,7 @@ "@agoric/vow": "^0.1.0", "@agoric/zoe": "^0.26.2", "@agoric/zone": "^0.2.2", + "@cosmjs/encoding": "^0.32.4", "@endo/base64": "^1.0.9", "@endo/errors": "^1.2.9", "@endo/far": "^1.1.10", diff --git a/packages/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index 801d6f22bce..d03d9d31968 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -104,6 +104,7 @@ export interface CosmosAssetInfo extends Record { export type CosmosChainInfo = Readonly<{ /** can be used to lookup chainInfo (chainId) from an address value */ bech32Prefix?: string; + cctpDestinationDomain?: number; chainId: string; connections?: Record; // chainId or wellKnownName @@ -309,6 +310,16 @@ export interface LiquidStakingMethods { liquidStake: (amount: AmountArg) => Promise; } +export interface NobleMethods { + /** burn USDC on Noble and mint on a destination chain via CCTP */ + depositForBurn: ( + mintRecipient: CosmosChainAddress, + amount: AmountArg, + ) => Promise; + // consider including `registerForwardingAccount` (`MsgRegisterAccount`), so a contract can create its own forwarding address + // Requires `noble/forwarding` protos: https://github.com/noble-assets/forwarding/blob/main/proto/noble/forwarding/v1/tx.proto +} + // TODO support StakingAccountQueries /** Methods supported only on Agoric chain accounts */ export interface LocalAccountMethods extends StakingAccountActions { diff --git a/packages/orchestration/src/ethereum-api.ts b/packages/orchestration/src/ethereum-api.ts index d294033756e..4fa88e98180 100644 --- a/packages/orchestration/src/ethereum-api.ts +++ b/packages/orchestration/src/ethereum-api.ts @@ -2,6 +2,8 @@ * Info for an Ethereum-based chain. */ export type EthChainInfo = Readonly<{ + // XXX consider ~BaseChainInfo type, with `cctpDestinationDomain` + `chainId` + cctpDestinationDomain?: number; chainId: string; allegedName: string; }>; diff --git a/packages/orchestration/src/examples/send-anywhere.contract.js b/packages/orchestration/src/examples/send-anywhere.contract.js index 344e9b25563..f72de33526a 100644 --- a/packages/orchestration/src/examples/send-anywhere.contract.js +++ b/packages/orchestration/src/examples/send-anywhere.contract.js @@ -5,9 +5,10 @@ import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js'; import { AnyNatAmountShape } from '../typeGuards.js'; import { withOrchestration } from '../utils/start-helper.js'; import { registerChainsAndAssets } from '../utils/chain-hub-helper.js'; -import * as flows from './send-anywhere.flows.js'; import * as sharedFlows from './shared.flows.js'; +import * as cctpFlows from './send-cctp.flows.js'; + /** * @import {Remote, Vow} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; @@ -40,7 +41,7 @@ export const contract = async ( zcf, privateArgs, zone, - { chainHub, orchestrateAll, vowTools, zoeTools }, + { chainHub, orchestrate, vowTools, zoeTools }, ) => { const creatorFacet = prepareChainHubAdmin(zone, chainHub); @@ -49,7 +50,10 @@ export const contract = async ( /** @type {(msg: string) => Vow} */ const log = msg => vowTools.watch(E(logNode).setValue(msg)); - const { makeLocalAccount } = orchestrateAll(sharedFlows, {}); + // XXX why can't we do both at once? + const makeLocalAccount = orchestrate('f1', {}, sharedFlows.makeLocalAccount); + const makeNobleAccount = orchestrate('f2', {}, cctpFlows.makeNobleAccount); + /** * Setup a shared local account for use in async-flow functions. Typically, * exo initState functions need to resolve synchronously, but `makeOnce` @@ -62,13 +66,19 @@ export const contract = async ( const sharedLocalAccountP = zone.makeOnce('localAccount', () => makeLocalAccount(), ); - + const nobleAccountP = zone.makeOnce('nobleAccount', () => makeNobleAccount()); // orchestrate uses the names on orchestrationFns to do a "prepare" of the associated behavior - const orchFns = orchestrateAll(flows, { - log, - sharedLocalAccountP, - zoeTools, - }); + const sendByCCTP = orchestrate( + 'sendByCCTP', + { + log, + sharedLocalAccountP, + nobleAccountP, + zoeTools, + }, + // @ts-expect-error deprecated, but alternative is TBD. + cctpFlows.sendByCCTP, + ); const publicFacet = zone.exo( 'Send PF', @@ -78,7 +88,7 @@ export const contract = async ( { makeSendInvitation() { return zcf.makeInvitation( - orchFns.sendIt, + sendByCCTP, 'send', undefined, M.splitRecord({ give: SingleNatAmountRecord }), diff --git a/packages/orchestration/src/examples/send-cctp.flows.js b/packages/orchestration/src/examples/send-cctp.flows.js new file mode 100644 index 00000000000..19817355d89 --- /dev/null +++ b/packages/orchestration/src/examples/send-cctp.flows.js @@ -0,0 +1,121 @@ +import { NonNullish } from '@agoric/internal'; +import { Fail, makeError, q } from '@endo/errors'; +import { M, mustMatch } from '@endo/patterns'; + +/** + * @import {GuestInterface, GuestOf} from '@agoric/async-flow'; + * @import {Vow} from '@agoric/vow'; + * @import {LocalOrchestrationAccountKit} from '../exos/local-orchestration-account.js'; + * @import {ZoeTools} from '../utils/zoe-tools.js'; + * @import {Orchestrator, OrchestrationFlow, LocalAccountMethods, OrchestrationAccountCommon, OrchestrationAccount, IBCConnectionInfo, AccountIdArg, ForwardInfo, Chain} from '../types.js'; + * @import {IBCChannelID} from '@agoric/vats'; + */ + +const { entries } = Object; + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + */ +export const makeNobleAccount = async orch => { + const nobleChain = await orch.getChain('noble'); + return nobleChain.makeAccount(); +}; +harden(makeNobleAccount); + +// TODO use case should be handled by `sendIt` based on the destination +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {object} ctx + * @param {Promise>} ctx.sharedLocalAccountP + * @param {Promise< + * GuestInterface< + * import('../exos/cosmos-orchestration-account.js').CosmosOrchestrationAccountKit['holder'] + * > + * >} ctx.nobleAccountP + * @param {GuestInterface} ctx.zoeTools + * @param {GuestOf<(msg: string) => Vow>} ctx.log + * @param {ZCFSeat} seat + * @param {{ chainName: string; destAddr: string }} offerArgs + */ +export const sendByCCTP = async ( + orch, + { + sharedLocalAccountP, + nobleAccountP, + log, + zoeTools: { localTransfer, withdrawToSeat }, + }, + seat, + offerArgs, +) => { + mustMatch(offerArgs, harden({ chainName: M.scalar(), destAddr: M.string() })); + const { chainName, destAddr } = offerArgs; + // NOTE the proposal shape ensures that the `give` is a single asset + const { give } = seat.getProposal(); + const [[_kw, amt]] = entries(give); + void log(`sending {${amt.value}} from ${chainName} to ${destAddr}`); + const agoric = await orch.getChain('agoric'); + const assets = await agoric.getVBankAssetInfo(); + void log(`got info for denoms: ${assets.map(a => a.denom).join(', ')}`); + const { denom } = NonNullish( + assets.find(a => a.brand === amt.brand), + `${amt.brand} not registered in vbank`, + ); + + /** @type {Chain} */ + const chain = await orch.getChain(chainName); + const info = await chain.getChainInfo(); + const { chainId } = info; + assert(typeof chainId === 'string', 'bad chainId'); + void log(`got info for chain: ${chainName} ${chainId}`); + + /** + * @type {OrchestrationAccount<{ chainId: 'agoric' }>} + */ + // @ts-expect-error XXX methods returning vows https://github.com/Agoric/agoric-sdk/issues/9822 + const sharedLocalAccount = await sharedLocalAccountP; + await localTransfer(seat, sharedLocalAccount, give); + + if (typeof info.cctpDestinationDomain !== 'number') { + // within the inter-chain; no CCTP needed + void log(`completed transfer to localAccount`); + + try { + await sharedLocalAccount.transfer( + { + value: destAddr, + encoding: 'bech32', + chainId, + }, + { denom, value: amt.value }, + ); + void log(`completed transfer to ${destAddr}`); + } catch (e) { + await withdrawToSeat(sharedLocalAccount, seat, give); + const errorMsg = `IBC Transfer failed ${q(e)}`; + void log(`ERROR: ${errorMsg}`); + seat.fail(errorMsg); + throw makeError(errorMsg); + } + } else { + // XXX is there a cleaner way to determine that this is USCD? + assets.some(a => a.brand === amt.brand && a.issuerName === 'USDC') || + Fail`CCTP must use USDC`; + + const nobleAccount = await nobleAccountP; + const nobleAddr = await nobleAccount.getAddress(); + const denomAmt = { denom, value: amt.value }; + await sharedLocalAccount.transfer(nobleAddr, denomAmt); + void log('assets are now on noble'); + + const encoding = 'ethereum'; // XXX TODO. could be solana? + /** @type {AccountIdArg} */ + const mintRecipient = { chainId, encoding, value: destAddr }; + await nobleAccount.depositForBurn(mintRecipient, denomAmt); + } + seat.exit(); + void log(`transfer complete, seat exited`); +}; +harden(sendByCCTP); diff --git a/packages/orchestration/src/exos/README.md b/packages/orchestration/src/exos/README.md index 0599cbbbf1a..a9b9969d024 100644 --- a/packages/orchestration/src/exos/README.md +++ b/packages/orchestration/src/exos/README.md @@ -92,6 +92,7 @@ classDiagram asContinuingOffer() delegate() deposit() + depositForBurn() executeTx() getAddress() getBalance() @@ -114,6 +115,7 @@ classDiagram asContinuingOffer() deactivate() delegate() + depositForBurn() executeEncodedTx() getAddress() getBalance() diff --git a/packages/orchestration/src/exos/chain-hub-admin.js b/packages/orchestration/src/exos/chain-hub-admin.js index 2a365508ebe..f5f29765d57 100644 --- a/packages/orchestration/src/exos/chain-hub-admin.js +++ b/packages/orchestration/src/exos/chain-hub-admin.js @@ -34,11 +34,9 @@ export const prepareChainHubAdmin = (zone, chainHub) => { const makeCreatorFacet = zone.exo( 'ChainHub Admin', M.interface('ChainHub Admin', { - registerChain: M.callWhen( - M.string(), - CosmosChainInfoShape, - ConnectionInfoShape, - ).returns(M.undefined()), + registerChain: M.callWhen(M.string(), CosmosChainInfoShape) + .optional(ConnectionInfoShape) + .returns(M.undefined()), registerAsset: M.call(M.string(), DenomDetailShape).returns(M.promise()), }), { @@ -47,7 +45,7 @@ export const prepareChainHubAdmin = (zone, chainHub) => { * * @param {string} chainName - must not exist in chainHub * @param {CosmosChainInfo} chainInfo - * @param {IBCConnectionInfo} connectionInfo - from Agoric chain + * @param {IBCConnectionInfo} [connectionInfo] - from Agoric chain */ async registerChain(chainName, chainInfo, connectionInfo) { // when() because chainHub methods return vows. If this were inside @@ -56,6 +54,10 @@ export const prepareChainHubAdmin = (zone, chainHub) => { chainHub.getChainInfo('agoric'), ); chainHub.registerChain(chainName, chainInfo); + if (!connectionInfo) { + console.log('no connection info for', chainName, 'assuming CCTP'); + return; + } chainHub.registerConnection( agoricChainInfo.chainId, chainInfo.chainId, diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index d9d52607e4f..ce73018caa9 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -203,9 +203,11 @@ export const TransferRouteShape = M.splitRecord( ); const ChainHubI = M.interface('ChainHub', { + // TODO: support more than `CosmosChainInfoShape` registerChain: M.call(M.string(), CosmosChainInfoShape).returns(), updateChain: M.call(M.string(), CosmosChainInfoShape).returns(), getChainInfo: M.call(M.string()).returns(VowShape), + getChainInfoByChainId: M.call(M.string()).returns(CosmosChainInfoShape), registerConnection: M.call( M.string(), M.string(), @@ -234,10 +236,10 @@ const ChainHubI = M.interface('ChainHub', { /** * Make a new ChainHub in the zone. * - * The resulting object is an Exo singleton. It has no precious state. It's only + * The resulting object is an Exo singleton. It has no precious state. Its only * state is a cache of queries to agoricNames and whatever info was provided in * registration calls. When you need a newer version you can simply make a hub - * hub and repeat the registrations. + * and repeat the registrations. * * @param {Zone} zone * @param {Remote} agoricNames @@ -247,6 +249,7 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { /** @type {MapStore} */ const chainInfos = zone.mapStore('chainInfos', { keyShape: M.string(), + // TODO: support more than `CosmosChainInfoShape` valueShape: CosmosChainInfoShape, }); /** @type {MapStore} */ @@ -270,6 +273,11 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { keyShape: M.string(), valueShape: M.string(), }); + /** @type {MapStore} */ + const chainIdToChainName = zone.mapStore('chainIdToChainName', { + keyShape: M.string(), + valueShape: M.string(), + }); /** * @param {Denom} denom - from perspective of the src/holding chain @@ -305,6 +313,7 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { // TODO consider makeAtomicProvider for vows if (!chainInfos.has(chainName)) { chainInfos.init(chainName, chainInfo); + chainIdToChainName.init(chainInfo.chainId, chainName); if (chainInfo.bech32Prefix) { bech32PrefixToChainName.init(chainInfo.bech32Prefix, chainName); } @@ -355,9 +364,7 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { * @template {string} C2 * @param {C1} primaryName * @param {C2} counterName - * @returns {Promise< - * [ActualChainInfo, ActualChainInfo, IBCConnectionInfo] - * >} + * @returns {Promise<[ChainInfo, ChainInfo, IBCConnectionInfo]>} */ // eslint-disable-next-line no-restricted-syntax -- TODO more exact rules for vow best practices async (primaryName, counterName) => { @@ -370,7 +377,7 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { const connectionInfo = await vowTools.asPromise( chainHub.getConnectionInfo(primary, counter), ); - return /** @type {[ActualChainInfo, ActualChainInfo, IBCConnectionInfo]} */ ([ + return /** @type {[ChainInfo, ChainInfo, IBCConnectionInfo]} */ ([ primary, counter, connectionInfo, @@ -393,7 +400,10 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { */ registerChain(name, chainInfo) { chainInfos.init(name, chainInfo); - if (chainInfo.bech32Prefix) { + if (!chainIdToChainName.has(chainInfo.chainId)) { + chainIdToChainName.init(chainInfo.chainId, name); + } + if ('bech32Prefix' in chainInfo && chainInfo.bech32Prefix) { bech32PrefixToChainName.init(chainInfo.bech32Prefix, name); } }, @@ -413,25 +423,35 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { bech32PrefixToChainName.delete(oldInfo.bech32Prefix); } chainInfos.set(chainName, chainInfo); - if (chainInfo.bech32Prefix) { + if ('bech32Prefix' in chainInfo && chainInfo.bech32Prefix) { bech32PrefixToChainName.init(chainInfo.bech32Prefix, chainName); } }, /** - * @template {string} K - * @param {K} chainName - * @returns {Vow>} + * @param {string} chainName + * @returns {Vow} */ getChainInfo(chainName) { // Either from registerChain or memoized remote lookup() if (chainInfos.has(chainName)) { - return /** @type {Vow>} */ ( + return /** @type {Vow} */ ( vowTools.asVow(() => chainInfos.get(chainName)) ); } return lookupChainInfo(chainName); }, + /** @param {string} chainId */ + getChainInfoByChainId(chainId) { + // Either from registerChain or memoized remote lookup() + chainIdToChainName.has(chainId) || + Fail`Chain Info not found for ${q(chainId)}`; + const chainName = chainIdToChainName.get(chainId); + chainInfos.has(chainName) || Fail`Chain Info not found for ${q(chainId)}`; + return /** @type {ActualChainInfo} */ ( + chainInfos.get(chainName) + ); + }, /** * @param {string} primaryChainId * @param {string} counterpartyChainId @@ -494,12 +514,9 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { * @template {string} C2 * @param {C1} primaryName the primary chain name * @param {C2} counterName the counterparty chain name - * @returns {Vow< - * [ActualChainInfo, ActualChainInfo, IBCConnectionInfo] - * >} + * @returns {Vow<[ChainInfo, ChainInfo, IBCConnectionInfo]>} */ getChainsAndConnection(primaryName, counterName) { - // @ts-expect-error XXX generic parameter propagation return lookupChainsAndConnection(primaryName, counterName); }, diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 72811216245..e5a211a30a8 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -1,5 +1,6 @@ /** @file Use-object for the owner of a staking account */ import { toRequestQueryJson } from '@agoric/cosmic-proto'; +import { MsgDepositForBurn } from '@agoric/cosmic-proto/circle/cctp/v1/tx.js'; import { QueryAllBalancesRequest, QueryAllBalancesResponse, @@ -64,10 +65,11 @@ import { } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; import { makeTimestampHelper } from '../utils/time.js'; +import { leftPadEthAddressTo32Bytes } from '../utils/address.js'; /** * @import {HostOf} from '@agoric/async-flow'; - * @import {AmountArg, IcaAccount, CosmosChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, StakingAccountQueries, OrchestrationAccountCommon, CosmosRewardsResponse, IBCConnectionInfo, IBCMsgTransferOptions, ChainHub, CosmosDelegationResponse} from '../types.js'; + * @import {AmountArg, IcaAccount, CosmosChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, StakingAccountQueries, NobleMethods, OrchestrationAccountCommon, CosmosRewardsResponse, IBCConnectionInfo, IBCMsgTransferOptions, ChainHub, CosmosDelegationResponse} from '../types.js'; * @import {ContractMeta, Invitation, OfferHandler, ZCF, ZCFSeat} from '@agoric/zoe'; * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'; * @import {Coin} from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; @@ -139,8 +141,17 @@ const stakingAccountQueriesMethods = { getRewards: M.call().returns(VowShape), }; +/** @see {NobleMethods} */ +const nobleMethods = { + depositForBurn: M.call(CosmosChainAddressShape, AmountArgShape).returns( + VowShape, + ), +}; + /** @see {OrchestrationAccountCommon} */ +// TODO: consider renaming to CosmosOrchAccountHolder export const IcaAccountHolderI = M.interface('IcaAccountHolder', { + ...nobleMethods, ...orchestrationAccountMethods, ...stakingAccountActionsMethods, ...stakingAccountQueriesMethods, @@ -208,7 +219,7 @@ export const prepareCosmosOrchestrationAccountKit = ( amountToCoin: M.call(AmountArgShape).returns(M.record()), }), returnVoidWatcher: M.interface('returnVoidWatcher', { - onFulfilled: M.call(M.or(M.string(), M.record())) + onFulfilled: M.call(M.any()) .optional(M.arrayOf(M.undefined())) .returns(M.undefined()), }), @@ -1128,6 +1139,43 @@ export const prepareCosmosOrchestrationAccountKit = ( watch(E(this.facets.helper.owned()).executeEncodedTx(msgs, opts)), ); }, + /** @type {HostOf} */ + depositForBurn(destination, amount) { + return asVow(() => { + trace('depositForBurn', { destination, amount }); + const { helper } = this.facets; + const { chainAddress } = this.state; + + const { cctpDestinationDomain } = chainHub.getChainInfoByChainId( + destination.chainId, + ); + + if (typeof cctpDestinationDomain !== 'number') { + throw Fail`${q(destination.chainId)} does not have "cctpDestinationDomain" set in ChainInfo`; + } + + // see https://github.com/circlefin/noble-cctp/blob/master/examples/depositForBurn.ts#L52-L70 + const mintRecipient = leftPadEthAddressTo32Bytes(destination.value); + + // TODO: do we need to support MsgDepositForBurnWithCaller? It's the + // same payload, plus `destinationCaller: Uint8Array`. + // (Functionally, only destinationCaller can call MsgReceive on the + // destination chain to mint) + const msg = MsgDepositForBurn.toProtoMsg({ + amount: helper.amountToCoin(amount)?.amount, + from: chainAddress.value, + destinationDomain: cctpDestinationDomain, + mintRecipient, + // safe to hardcode this constant? maybe best as part of opts bag with a default value? + burnToken: 'uusdc', + }); + + return watch( + E(helper.owned()).executeEncodedTx([Any.toJSON(msg)]), + this.facets.returnVoidWatcher, + ); + }); + }, }, }, ); diff --git a/packages/orchestration/src/orchestration-api.ts b/packages/orchestration/src/orchestration-api.ts index 8e3954b736a..053cf51c684 100644 --- a/packages/orchestration/src/orchestration-api.ts +++ b/packages/orchestration/src/orchestration-api.ts @@ -18,6 +18,7 @@ import type { KnownChains, LocalAccountMethods, ICQQueryFunction, + NobleMethods, } from './types.js'; import type { ResolvedContinuingOfferResult } from './utils/zoe-tools.js'; @@ -104,7 +105,9 @@ export type OrchestrationAccount = (CI extends CosmosChainInfo ? CI['chainId'] extends `agoric${string}` ? LocalAccountMethods - : CosmosChainAccountMethods + : CI['chainId'] extends `noble${string}` + ? CosmosChainAccountMethods & NobleMethods + : CosmosChainAccountMethods : object); /** diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index f6a245bfc08..52589b53a4b 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -66,6 +66,7 @@ export const startStakeAtom = async ({ chainId: cosmoshub.chainId, hostConnectionId: connectionInfo.counterparty.connection_id, controllerConnectionId: connectionInfo.id, + // @ts-expect-error cosmoshub is /** @type {CosmosChainInfo} */ icqEnabled: cosmoshub.icqEnabled, }, privateArgs: await deeplyFulfilledObject( diff --git a/packages/orchestration/src/proposals/start-stakeOsmo.js b/packages/orchestration/src/proposals/start-stakeOsmo.js index 77bd2d562c8..19f089c2681 100644 --- a/packages/orchestration/src/proposals/start-stakeOsmo.js +++ b/packages/orchestration/src/proposals/start-stakeOsmo.js @@ -71,6 +71,7 @@ export const startStakeOsmo = async ({ chainId: osmosis.chainId, hostConnectionId: connectionInfo.counterparty.connection_id, controllerConnectionId: connectionInfo.id, + // @ts-expect-error osmosis is /** @type {CosmosChainInfo} */ icqEnabled: osmosis.icqEnabled, }, privateArgs: await deeplyFulfilledObject( diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 3085af7a913..d17c80adae2 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -97,6 +97,7 @@ export const CosmosChainInfoShape = M.splitRecord( }, { bech32Prefix: M.string(), + cctpDestinationDomain: M.number(), connections: M.record(), stakingTokens: M.arrayOf({ denom: M.string() }), // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 diff --git a/packages/orchestration/src/utils/address.js b/packages/orchestration/src/utils/address.js index 908b8010d22..4b35b28664d 100644 --- a/packages/orchestration/src/utils/address.js +++ b/packages/orchestration/src/utils/address.js @@ -1,4 +1,5 @@ import { Fail, q } from '@endo/errors'; +import { fromHex } from '@cosmjs/encoding'; /** * @import {IBCConnectionID} from '@agoric/vats'; @@ -141,3 +142,13 @@ export const parseAccountId = partialId => { throw Fail`Invalid accountId: ${q(partialId)}`; }; harden(parseAccountId); + +// Left pad the mint recipient address with 0's to 32 bytes. +// standard ETH addresses are 20 bytes, but for ABI data structures and other +// reasons, 32 bytes are used. +export const leftPadEthAddressTo32Bytes = rawAddress => { + const cleanedAddress = rawAddress.replace(/^0x/, ''); + const zeroesNeeded = 64 - cleanedAddress.length; + const paddedAddress = '0'.repeat(zeroesNeeded) + cleanedAddress; + return fromHex(paddedAddress); +}; diff --git a/packages/orchestration/test/examples/send-cctp.test.ts b/packages/orchestration/test/examples/send-cctp.test.ts new file mode 100644 index 00000000000..81c9a490a31 --- /dev/null +++ b/packages/orchestration/test/examples/send-cctp.test.ts @@ -0,0 +1,243 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { E } from '@endo/far'; +import { makeIssuerKit } from '@agoric/ertp'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import path from 'path'; +import type { + CosmosChainInfo, + IBCConnectionInfo, +} from '../../src/cosmos-api.js'; +import { commonSetup } from '../supports.js'; +import { denomHash } from '../../src/utils/denomHash.js'; +import type { DenomDetail } from '../../src/types.js'; +import fetchedChainInfo from '../../src/fetched-chain-info.js'; + +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const contractName = 'sendAnywhere'; +const contractFile = `${dirname}/../../src/examples/send-anywhere.contract.js`; +type StartFn = + typeof import('../../src/examples/send-anywhere.contract.js').start; + +const txChannelDefaults = { + counterPartyPortId: 'transfer', + version: 'ics20-1', + portId: 'transfer', + ordering: 1, // ORDER_UNORDERED + state: 3, // STATE_OPEN +}; + +const AgoricDevnetConfig = { + chains: { + axelar: { + chainId: 'axelar-testnet-lisbon-3', + }, + osmosis: { + chainId: 'osmo-test-5', + bech32Prefix: 'osmosis', + connections: { + 'axelar-testnet-lisbon-3': { + id: 'connection-1233333', // XXX??? + client_id: '07-tendermint-1258', + state: 3, // STATE_OPEN, + counterparty: { + client_id: 'XXXX???', + connection_id: 'connection-87665', // XXX??? + }, + transferChannel: { + channelId: 'channel-566434324', // XXX??? + counterPartyChannelId: 'channel-112233', // ??? + ...txChannelDefaults, + }, + } as IBCConnectionInfo, + }, + } as CosmosChainInfo, + }, + connections: { + 'axelar-testnet-lisbon-3': { + // arbitrary ids... + id: 'connection-8600', + client_id: '07-tendermint-12700', + state: 3, // STATE_OPEN + counterparty: { + client_id: '07-tendermint-432600', + connection_id: 'connection-379300', + }, + transferChannel: { + counterPartyChannelId: 'channel-1006200', + channelId: 'channel-6500', + ...txChannelDefaults, + }, + } as IBCConnectionInfo, + + 'osmo-test-5': { + id: 'connection-86', + client_id: '07-tendermint-127', + state: 3, // STATE_OPEN + counterparty: { + client_id: '07-tendermint-4326', + connection_id: 'connection-3793', + }, + transferChannel: { + counterPartyChannelId: 'channel-10062', + channelId: 'channel-65', + ...txChannelDefaults, + }, + } as IBCConnectionInfo, + }, +}; + +const registerUSDC = async ({ bankManager, agoricNamesAdmin }) => { + const usdcKit = withAmountUtils(makeIssuerKit('USDC')); + const { issuer, mint, brand } = usdcKit; + + // TODO: use USDC denom + // const { axelar } = AgoricDevnetConfig.chains; + const { noble } = fetchedChainInfo; + const { channelId: agoricToNoble } = + fetchedChainInfo.agoric.connections[noble.chainId].transferChannel; + const denom = `ibc/${denomHash({ channelId: agoricToNoble, denom: 'uusdc' })}`; + + const issuerName = 'USDC'; + const proposedName = 'USDC noble'; + await E(bankManager).addAsset(denom, issuerName, proposedName, { + issuer, + mint, + brand, + }); + await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( + denom, + /** @type {AssetInfo} */ harden({ + brand, + issuer, + issuerName, + denom, + proposedName, + displayInfo: { IOU: true }, + }), + ); + + return harden({ ...usdcKit, denom }); +}; + +test('send to base via noble CCTP', async t => { + t.log('bootstrap, orchestration core-eval'); + const { + bootstrap, + commonPrivateArgs, + brands: { ist }, + utils: { inspectLocalBridge, transmitTransferAck, populateChainHub }, + mocks: { ibcBridge }, + } = await commonSetup(t); + const vt = bootstrap.vowTools; + + populateChainHub(); + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + + t.log('contract coreEval', contractName); + + const installation: Installation = + await bundleAndInstall(contractFile); + + const { bankManager, agoricNamesAdmin } = bootstrap; + const usdcKit = await registerUSDC({ + bankManager, + agoricNamesAdmin, + }); + + const storageNode = await E(bootstrap.storage.rootNode).makeChildNode( + contractName, + ); + + // Mix osmosis from AgoricDevnetConfig with commonPrivateArgs + const { + agoric, + osmosis: _x, + ...withoutOsmosis + } = commonPrivateArgs.chainInfo; + const { osmosis } = AgoricDevnetConfig.chains; + const connections = { + ...agoric.connections, + ...AgoricDevnetConfig.connections, + // XXX to get from Agoric to axelar, use osmosis + 'axelar-testnet-lisbon-3': AgoricDevnetConfig.connections[osmosis.chainId], + }; + const chainInfo = { + ...withoutOsmosis, + osmosis, + agoric: { ...agoric, connections }, + }; + const assetInfo: Array<[string, DenomDetail]> = [ + ...commonPrivateArgs.assetInfo, + ]; + const sendKit = await E(zoe).startInstance( + installation, + { Stable: ist.issuer, USDC: usdcKit.issuer }, + {}, + { + ...commonPrivateArgs, + assetInfo, + chainInfo, + storageNode, + }, + ); + + t.log('register the base chain'); + await E(sendKit.creatorFacet).registerChain( + 'base', + { + chainId: 'E8453', + // allegedName: 'base', + // https://developers.circle.com/stablecoins/supported-domains + cctpDestinationDomain: 6, + }, + // TODO: make this optional, since we don't really use it + fetchedChainInfo.agoric.connections['noble-1'], + ); + + t.log('client uses contract to send to EVM chain via CCTP'); + { + const anAmt = usdcKit.units(4.25); + const Send = await E(usdcKit.mint).mintPayment(anAmt); + + const publicFacet = await E(zoe).getPublicFacet(sendKit.instance); + const inv = E(publicFacet).makeSendInvitation(); + const userSeat = await E(zoe).offer( + inv, + { give: { Send: anAmt } }, + { Send }, + { + destAddr: '0x20E68F6c276AC6E297aC46c84Ab260928276691D', + chainName: 'base', + }, + ); + await transmitTransferAck(); + + await vt.when(E(userSeat).getOfferResult()); + const history = inspectLocalBridge(); + const { messages, address: fakeLocalChainAddr } = history.at(-1); + t.is(messages.length, 1); + const [txfr] = messages; + t.log('local bridge', txfr); + t.like(txfr, { + '@type': '/ibc.applications.transfer.v1.MsgTransfer', + receiver: 'cosmos1test', // TODO port setBech32Prefix from fastUSDC + sender: fakeLocalChainAddr, + sourceChannel: 'channel-62', + token: { + amount: '4250000', + // see test above + denom: + 'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9', + }, + }); + t.is( + usdcKit.denom, + 'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9', + ); + } + + t.log(ibcBridge.inspectDibcBridge()); +}); diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md index da8fa18a1e4..07d3d1fec71 100644 --- a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md @@ -24,6 +24,7 @@ Generated by [AVA](https://avajs.dev). StateUnwrapper_kindHandle: 'Alleged: kind', asyncFuncEagerWakers: [ Object @Alleged: asyncFlow flow {}, + Object @Alleged: asyncFlow flow {}, ], asyncFuncFailures: {}, flowForOutcomeVow: { @@ -80,6 +81,48 @@ Generated by [AVA](https://avajs.dev). 'Alleged: BLD brand': 'ubld', 'Alleged: IST brand': 'uist', }, + chainIdToChainName: { + 'agoric-3': 'agoric', + 'archway-1': 'archway', + 'beezee-1': 'beezee', + 'carbon-1': 'carbon', + 'cataclysm-1': 'nibiru', + celestia: 'celestia', + 'core-1': 'persistence', + 'coreum-mainnet-1': 'coreum', + 'cosmoshub-4': 'cosmoshub', + 'crescent-1': 'crescent', + 'dydx-mainnet-1': 'dydx', + 'dymension_1100-1': 'dymension', + 'empowerchain-1': 'empowerchain', + 'evmos_9001-2': 'evmos', + 'haqq_11235-1': 'haqq', + 'injective-1': 'injective', + 'juno-1': 'juno', + 'kaiyo-1': 'kujira', + 'kava_2222-10': 'kava', + 'lava-mainnet-1': 'lava', + 'migaloo-1': 'migaloo', + 'neutron-1': 'neutron', + 'noble-1': 'noble', + 'omniflixhub-1': 'omniflixhub', + 'osmosis-1': 'osmosis', + 'pacific-1': 'sei', + 'phoenix-1': 'terra2', + 'pio-mainnet-1': 'provenance', + 'pirin-1': 'nolus', + 'planq_7070-2': 'planq', + 'pryzm-1': 'pryzm', + 'quicksilver-2': 'quicksilver', + 'secret-4': 'secretnetwork', + 'shido_9008-1': 'shido', + 'sifchain-1': 'sifchain', + 'stargaze-1': 'stargaze', + 'stride-1': 'stride', + 'titan_18888-1': 'titan', + 'umee-1': 'umee', + 'vota-ash': 'doravota', + }, chainInfos: { agoric: { bech32Prefix: 'agoric', @@ -4689,11 +4732,15 @@ Generated by [AVA](https://avajs.dev). 'Send PF_kindHandle': 'Alleged: kind', 'Send PF_singleton': 'Alleged: Send PF', localAccount: 'Vow', + nobleAccount: 'Vow', orchestration: { - makeLocalAccount: { + f1: { asyncFlow_kindHandle: 'Alleged: kind', }, - sendIt: { + f2: { + asyncFlow_kindHandle: 'Alleged: kind', + }, + sendByCCTP: { asyncFlow_kindHandle: 'Alleged: kind', endowments: { 0: { @@ -4735,6 +4782,8 @@ Generated by [AVA](https://avajs.dev). VowRejectionTracker_kindHandle: 'Alleged: kind', VowRejectionTracker_singleton: 'Alleged: VowRejectionTracker', WatchUtils_kindHandle: 'Alleged: kind', - retryableFlowForOutcomeVow: {}, + retryableFlowForOutcomeVow: { + 'Alleged: VowInternalsKit vowV0': 'Alleged: lookupChainsAndConnection flow', + }, }, } diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap index 0e546206182..26658f1fc55 100644 Binary files a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap and b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap differ diff --git a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md index 988e7ca6faf..9ffac685211 100644 --- a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.md @@ -37,6 +37,10 @@ Generated by [AVA](https://avajs.dev). brandDenom: { 'Alleged: BLD brand': 'ubld', }, + chainIdToChainName: { + 'agoric-3': 'agoric', + 'cosmoshub-4': 'cosmoshub', + }, chainInfos: { agoric: { bech32Prefix: 'agoric', diff --git a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.snap b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.snap index cf5069eaed4..0120661df94 100644 Binary files a/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.snap and b/packages/orchestration/test/examples/snapshots/staking-combinations.test.ts.snap differ diff --git a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md index 4183011ae3d..25e292ad3b8 100644 --- a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.md @@ -36,6 +36,11 @@ Generated by [AVA](https://avajs.dev). stride: 'stride', }, brandDenom: {}, + chainIdToChainName: { + 'agoric-3': 'agoric', + 'osmosis-1': 'osmosis', + 'stride-1': 'stride', + }, chainInfos: { agoric: { bech32Prefix: 'agoric', diff --git a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap index a4f86b7a9dd..5e45686cd95 100644 Binary files a/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap and b/packages/orchestration/test/examples/snapshots/unbond.contract.test.ts.snap differ diff --git a/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts b/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts index f590bfa7af0..9cef6687893 100644 --- a/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts +++ b/packages/orchestration/test/exos/cosmos-orchestration-account.test.ts @@ -47,6 +47,7 @@ import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; import { decodeBase64 } from '@endo/base64'; import type { EReturn } from '@endo/far'; import type { TestFn } from 'ava'; +import { MsgDepositForBurn } from '@agoric/cosmic-proto/circle/cctp/v1/tx.js'; import type { CosmosValidatorAddress } from '../../src/cosmos-api.js'; import fetchedChainInfo from '../../src/fetched-chain-info.js'; import type { @@ -940,3 +941,54 @@ test('executeEncodedTx', async t => { 'delegateMsgSuccess', ); }); + +test(`depositForBurn via Noble to Base`, async t => { + t.context.utils.populateChainHub(); + const { chainHub } = t.context.facadeServices; + chainHub.registerChain('base', { + chainId: 'E8453', + cctpDestinationDomain: 0, + }); + const makeTestCOAKit = prepareMakeTestCOAKit(t, t.context, { noble: true }); + const nobleAccount = await makeTestCOAKit(); + const amount = { + denom: 'uusdc', + value: 10n, + }; + + const actual = await E(nobleAccount).depositForBurn( + { + value: '0xe0d43135EBd2593907F8f56c25ADC1Bf94FCf993', + chainId: 'E8453', + // allegedName: 'base', + encoding: 'ethereum', + }, + amount, + ); + + t.log('check the bridge'); + t.deepEqual(actual, undefined); + + const getAndDecodeLatestPacket = async () => { + await eventLoopIteration(); + const { bridgeDowncalls } = await t.context.utils.inspectDibcBridge(); + const latest = bridgeDowncalls[ + bridgeDowncalls.length - 1 + ] as IBCMethod<'sendPacket'>; + const { messages } = parseOutgoingTxPacket(latest.packet.data); + return MsgDepositForBurn.decode(messages[0].value); + }; + + const packet = await getAndDecodeLatestPacket(); + t.log({ packet }); + t.like( + packet, + { + amount: '10', + burnToken: 'uusdc', + destinationDomain: 0, + from: 'cosmos1test', + }, + 'it worked', + ); +}); diff --git a/packages/orchestration/test/exos/make-test-coa-kit.ts b/packages/orchestration/test/exos/make-test-coa-kit.ts index 4217c3c7df7..f91c0fca74d 100644 --- a/packages/orchestration/test/exos/make-test-coa-kit.ts +++ b/packages/orchestration/test/exos/make-test-coa-kit.ts @@ -21,8 +21,8 @@ export const prepareMakeTestCOAKit = ( bootstrap, commonPrivateArgs: { marshaller }, facadeServices, - utils, }: EReturn, + { noble } = { noble: false }, { zcf = Far('MockZCF', {}) } = {}, ) => { t.log('exo setup - prepareCosmosOrchestrationAccount'); @@ -47,7 +47,7 @@ export const prepareMakeTestCOAKit = ( return async ({ storageNode = bootstrap.storage.rootNode.makeChildNode('accounts'), - chainId = 'cosmoshub-4', + chainId = noble ? 'noble-1' : 'cosmoshub-4', hostConnectionId = 'connection-0' as const, controllerConnectionId = 'connection-1' as const, icqEnabled = false, diff --git a/packages/orchestration/test/exos/portfolio-holder-kit.test.ts b/packages/orchestration/test/exos/portfolio-holder-kit.test.ts index b456399b57c..c9dc2ebc74d 100644 --- a/packages/orchestration/test/exos/portfolio-holder-kit.test.ts +++ b/packages/orchestration/test/exos/portfolio-holder-kit.test.ts @@ -24,7 +24,7 @@ test('portfolio holder kit behaviors', async t => { }, }); - const makeTestCOAKit = prepareMakeTestCOAKit(t, common, { + const makeTestCOAKit = prepareMakeTestCOAKit(t, common, undefined, { zcf: mockZcf, }); const makeTestLOAKit = prepareMakeTestLOAKit(t, common, { zcf: mockZcf }); diff --git a/packages/orchestration/test/ibc-mocks.ts b/packages/orchestration/test/ibc-mocks.ts index 6992227ee34..4a2fc33fd34 100644 --- a/packages/orchestration/test/ibc-mocks.ts +++ b/packages/orchestration/test/ibc-mocks.ts @@ -20,6 +20,10 @@ import { MsgSend, MsgSendResponse, } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/tx.js'; +import { + MsgDepositForBurn, + MsgDepositForBurnResponse, +} from '@agoric/cosmic-proto/circle/cctp/v1/tx.js'; import { buildMsgResponseString, buildQueryResponseString, @@ -28,6 +32,7 @@ import { buildQueryPacketString, createMockAckMap, } from '../tools/ibc-mocks.js'; +import { leftPadEthAddressTo32Bytes } from '../src/utils/address.js'; /** * TODO: provide mappings to cosmos error codes (and module specific error codes) @@ -122,6 +127,26 @@ export const protoMsgMocks = { msg: buildTxPacketString([MsgSend.toProtoMsg(bankSendMulti)]), ack: buildMsgResponseString(MsgSendResponse, {}), }, + depositForBurn: { + // msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ21ZS0lTOWphWEpqYkdVdVkyTjBjQzUyTVM1TmMyZEVaWEJ2YzJsMFJtOXlRblZ5YmhKQkNndGpiM050YjNNeGRHVnpkQklITkRJMU1EQXdNQmdHSWlBQUFBQUFBQUFBQUFBQUFBQWc1bzlzSjJyRzRwZXNSc2hLc21DU2duWnBIU29GZFhWelpHTT0iLCJtZW1vIjoiIn0=', + msg: buildTxPacketString([ + MsgDepositForBurn.toProtoMsg({ + amount: '4250000', + burnToken: 'uusdc', + from: 'cosmos1test', + destinationDomain: 6, + mintRecipient: leftPadEthAddressTo32Bytes( + '0x20E68F6c276AC6E297aC46c84Ab260928276691D', + ), + }), + ]), + ack: buildMsgResponseString(MsgDepositForBurnResponse, {}), + }, + depositForBurnForBase: { + msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ2w4S0lTOWphWEpqYkdVdVkyTjBjQzUyTVM1TmMyZEVaWEJ2YzJsMFJtOXlRblZ5YmhJNkNndGpiM050YjNNeGRHVnpkQklDTVRBaUlBQUFBQUFBQUFBQUFBQUFBT0RVTVRYcjBsazVCL2oxYkNXdHdiK1UvUG1US2dWMWRYTmtZdz09IiwibWVtbyI6IiJ9', + // 'depositForBurn via Noble to Base' in cosmos-orchestration-account.test.ts + ack: buildMsgResponseString(MsgDepositForBurnResponse, {}), + }, }; export const defaultMockAckMap: Record = diff --git a/packages/orchestration/test/network-fakes.ts b/packages/orchestration/test/network-fakes.ts index b137a61930a..6dba6c9dd84 100644 --- a/packages/orchestration/test/network-fakes.ts +++ b/packages/orchestration/test/network-fakes.ts @@ -31,7 +31,7 @@ import { decodeProtobufBase64 } from '../tools/protobuf-decoder.js'; const trace = makeTracer('NetworkFakes'); /** - * Mimic IBC Channel version negotation + * Mimic IBC Channel version negotiation * * As part of the IBC Channel initialization, the version field is negotiated * with the host. `version` is a String or JSON string as determined by the IBC diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index 2d8a85cb056..67c118635f9 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -151,7 +151,14 @@ export const commonSetup = async (t: ExecutionContext) => { ibcSequenceNonce += 1n; // let the promise for the transfer start await eventLoopIteration(); - const lastMsgTransfer = localBridgeMessages.at(-1).messages[0]; + if (localBridgeMessages.length < 1) + throw Error('no messages on the local bridge'); + + const b1 = localBridgeMessages.at(-1); + if (!b1.messages || b1.messages.length < 1) + throw Error('no messages in the last tx'); + + const lastMsgTransfer = b1.messages[0]; await E(transferBridge).fromBridge( buildVTransferEvent({ receiver: lastMsgTransfer.receiver, diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index a5c96b352a8..b586722de60 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -1,5 +1,5 @@ /** - * @file pure types types, no runtime, ignored by Ava + * @file pure types, no runtime, ignored by Ava */ import type { HostInterface, HostOf } from '@agoric/async-flow'; @@ -319,4 +319,30 @@ expectNotType(chainAddr); expectType< (validator: CosmosValidatorAddress, amount: AmountArg) => Promise >(account.delegate); + + expectType<(destination, amount: AmountArg) => Promise>( + // @ts-expect-error `depositForBurn` only available for noble + account.depositForBurn, + ); +} + +// Test NobleAccountMethods +{ + type ChainFacade = Chain< + CosmosChainInfo & { + chainId: 'noble-1'; + } + >; + const remoteChain: ChainFacade = null as any; + const account = await remoteChain.makeAccount(); + + expectType<(destination, amount: AmountArg) => Promise>( + account.depositForBurn, + ); + + // Verify delegate is not available (no stakingTokens parameter) + expectType< + (validator: CosmosValidatorAddress, amount: AmountArg) => Promise + // @ts-expect-error StakingMethods not available on noble + >(account.delegate); }