diff --git a/packages/orchestration/src/chain-info.js b/packages/orchestration/src/chain-info.js index b5c90a1da6d..efded7f3550 100644 --- a/packages/orchestration/src/chain-info.js +++ b/packages/orchestration/src/chain-info.js @@ -1,10 +1,16 @@ import { E } from '@endo/far'; -import { mustMatch } from '@endo/patterns'; -import { normalizeConnectionInfo } from './exos/chain-hub.js'; +import { M, mustMatch } from '@endo/patterns'; +import { + ASSETS_KEY, + CHAIN_KEY, + CONNECTIONS_KEY, + normalizeConnectionInfo, +} from './exos/chain-hub.js'; import fetchedChainInfo from './fetched-chain-info.js'; // Refresh with scripts/refresh-chain-info.ts -import { CosmosChainInfoShape } from './typeGuards.js'; +import { CosmosAssetInfoShape, CosmosChainInfoShape } from './typeGuards.js'; -/** @import {CosmosChainInfo, EthChainInfo, IBCConnectionInfo} from './types.js'; */ +/** @import {CosmosAssetInfo, CosmosChainInfo, EthChainInfo, IBCConnectionInfo} from './types.js'; */ +/** @import {NameAdmin} from '@agoric/vats'; */ /** @typedef {CosmosChainInfo | EthChainInfo} ChainInfo */ @@ -66,7 +72,21 @@ const knownChains = /** @satisfies {Record} */ ( /** @typedef {typeof knownChains} KnownChains */ /** - * @param {ERef} agoricNamesAdmin + * TODO(#9572): include this in registerChain + * + * @param {ERef} agoricNamesAdmin + * @param {string} name + * @param {CosmosAssetInfo[]} assets + */ +export const registerChainAssets = async (agoricNamesAdmin, name, assets) => { + mustMatch(assets, M.arrayOf(CosmosAssetInfoShape)); + const { nameAdmin: assetAdmin } = + await E(agoricNamesAdmin).provideChild(ASSETS_KEY); + return E(assetAdmin).update(name, assets); +}; + +/** + * @param {ERef} agoricNamesAdmin * @param {string} name * @param {CosmosChainInfo} chainInfo * @param {(...messages: string[]) => void} [log] @@ -80,9 +100,9 @@ export const registerChain = async ( log = () => {}, handledConnections = new Set(), ) => { - const { nameAdmin } = await E(agoricNamesAdmin).provideChild('chain'); + const { nameAdmin } = await E(agoricNamesAdmin).provideChild(CHAIN_KEY); const { nameAdmin: connAdmin } = - await E(agoricNamesAdmin).provideChild('chainConnection'); + await E(agoricNamesAdmin).provideChild(CONNECTIONS_KEY); mustMatch(chainInfo, CosmosChainInfoShape); const { connections = {}, ...vertex } = chainInfo; diff --git a/packages/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index 6880bb5939f..06c4587e404 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -22,7 +22,7 @@ import type { LocalIbcAddress, RemoteIbcAddress, } from '@agoric/vats/tools/ibc-utils.js'; -import type { AmountArg, ChainAddress, DenomAmount } from './types.js'; +import type { AmountArg, ChainAddress, Denom, DenomAmount } from './types.js'; /** An address for a validator on some blockchain, e.g., cosmos, eth, etc. */ export type CosmosValidatorAddress = ChainAddress & { @@ -54,6 +54,29 @@ export type IBCConnectionInfo = { }; }; +/** + * https://github.com/cosmos/chain-registry/blob/master/assetlist.schema.json + */ +export type CosmosAssetInfo = { + base: Denom; + name: string; + display: string; + symbol: string; + denom_units: Array<{ denom: Denom; exponent: number }>; + traces?: Array<{ + type: 'ibc'; + counterparty: { + chain_name: string; + base_denom: Denom; + channel_id: IBCChannelID; + }; + chain: { + channel_id: IBCChannelID; + path: string; + }; + }>; +} & Record; + /** * Info for a Cosmos-based chain. */ diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index 7f235bac09c..24b280085b3 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -1,6 +1,7 @@ -import { Fail, makeError } from '@endo/errors'; +import { Fail, makeError, q } from '@endo/errors'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; +import { BrandShape } from '@agoric/ertp/src/typeGuards.js'; import { VowShape } from '@agoric/vow'; import { makeHeapZone } from '@agoric/zone'; @@ -9,10 +10,11 @@ import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; /** * @import {NameHub} from '@agoric/vats'; * @import {Vow, VowTools} from '@agoric/vow'; - * @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js'; + * @import {CosmosAssetInfo, CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js'; * @import {ChainInfo, KnownChains} from '../chain-info.js'; + * @import {Denom} from '../orchestration-api.js'; * @import {Remote} from '@agoric/internal'; - * @import {Zone} from '@agoric/zone'; + * @import {TypedPattern} from '@agoric/internal'; */ /** @@ -22,10 +24,26 @@ import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; * : ChainInfo} ActualChainInfo */ +/** + * @typedef {object} DenomDetail + * @property {string} baseName - name of issuing chain; e.g. cosmoshub + * @property {Denom} baseDenom - e.g. uatom + * @property {string} chainName - name of holding chain; e.g. agoric + * @property {Brand} [brand] - vbank brand, if registered + * @see {ChainHub} `registerAsset` method + */ +/** @type {TypedPattern} */ +export const DenomDetailShape = M.splitRecord( + { chainName: M.string(), baseName: M.string(), baseDenom: M.string() }, + { brand: BrandShape }, +); + /** agoricNames key for ChainInfo hub */ export const CHAIN_KEY = 'chain'; /** namehub for connection info */ export const CONNECTIONS_KEY = 'chainConnection'; +/** namehub for assets info */ +export const ASSETS_KEY = 'chainAssets'; /** * Character used in a connection tuple key to separate the two chain ids. Valid @@ -146,6 +164,8 @@ const ChainHubI = M.interface('ChainHub', { ).returns(), getConnectionInfo: M.call(ChainIdArgShape, ChainIdArgShape).returns(VowShape), getChainsAndConnection: M.call(M.string(), M.string()).returns(VowShape), + registerAsset: M.call(M.string(), DenomDetailShape).returns(), + lookupAsset: M.call(M.string()).returns(DenomDetailShape), }); /** @@ -172,6 +192,12 @@ export const makeChainHub = (agoricNames, vowTools) => { valueShape: IBCConnectionInfoShape, }); + /** @type {MapStore} */ + const denomDetails = zone.mapStore('denom', { + keyShape: M.string(), + valueShape: DenomDetailShape, + }); + const lookupChainInfo = vowTools.retriable( zone, 'lookupChainInfo', @@ -336,8 +362,53 @@ export const makeChainHub = (agoricNames, vowTools) => { // @ts-expect-error XXX generic parameter propagation return lookupChainsAndConnection(primaryName, counterName); }, + + /** + * Register an asset that may be held on a chain other than the issuing + * chain. + * + * @param {Denom} denom - on the holding chain, whose name is given in + * `detail.chainName` + * @param {DenomDetail} detail - chainName and baseName must be registered + */ + registerAsset(denom, detail) { + const { chainName, baseName } = detail; + chainInfos.has(chainName) || + Fail`must register chain ${q(chainName)} first`; + chainInfos.has(baseName) || + Fail`must register chain ${q(baseName)} first`; + denomDetails.init(denom, detail); + }, + /** + * Retrieve holding, issuing chain names etc. for a denom. + * + * @param {Denom} denom + */ + lookupAsset(denom) { + return denomDetails.get(denom); + }, }); return chainHub; }; /** @typedef {ReturnType} ChainHub */ + +/** + * @param {ChainHub} chainHub + * @param {string} name + * @param {CosmosAssetInfo[]} assets + */ +export const registerAssets = (chainHub, name, assets) => { + for (const { base, traces } of assets) { + const native = !traces; + native || traces.length === 1 || Fail`unexpected ${traces.length} traces`; + const [chainName, baseName, baseDenom] = native + ? [name, name, base] + : [ + name, + traces[0].counterparty.chain_name, + traces[0].counterparty.base_denom, + ]; + chainHub.registerAsset(base, { chainName, baseName, baseDenom }); + } +}; diff --git a/packages/orchestration/src/exos/orchestrator.js b/packages/orchestration/src/exos/orchestrator.js index 224656d878f..393cb5b024a 100644 --- a/packages/orchestration/src/exos/orchestrator.js +++ b/packages/orchestration/src/exos/orchestrator.js @@ -2,6 +2,7 @@ import { AmountShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; import { Shape as NetworkShape } from '@agoric/network'; +import { Fail, q } from '@endo/errors'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { @@ -15,7 +16,7 @@ import { /** * @import {Zone} from '@agoric/base-zone'; * @import {ChainHub} from './chain-hub.js'; - * @import {AsyncFlowTools, HostOf} from '@agoric/async-flow'; + * @import {AsyncFlowTools, HostInterface, HostOf} from '@agoric/async-flow'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {TimerService} from '@agoric/time'; * @import {LocalChain} from '@agoric/vats/src/localchain.js'; @@ -29,7 +30,6 @@ import { * @import {Chain, ChainInfo, IBCConnectionInfo, Orchestrator} from '../types.js'; */ -const { Fail } = assert; const { Vow$ } = NetworkShape; // TODO #9611 const trace = makeTracer('Orchestrator'); @@ -47,6 +47,7 @@ export const OrchestratorI = M.interface('Orchestrator', { * asyncFlowTools: AsyncFlowTools; * chainHub: ChainHub; * localchain: Remote; + * chainByName: MapStore>; * makeRecorderKit: MakeRecorderKit; * makeLocalChainFacade: MakeLocalChainFacade; * makeRemoteChainFacade: MakeRemoteChainFacade; @@ -62,9 +63,10 @@ export const prepareOrchestratorKit = ( { chainHub, localchain, + chainByName, makeLocalChainFacade, makeRemoteChainFacade, - vowTools: { watch }, + vowTools: { watch, asVow }, }, ) => zone.exoClassKit( @@ -72,15 +74,13 @@ export const prepareOrchestratorKit = ( { orchestrator: OrchestratorI, makeLocalChainFacadeWatcher: M.interface('makeLocalChainFacadeWatcher', { - onFulfilled: M.call(M.record()) - .optional(M.arrayOf(M.undefined())) - .returns(M.any()), // FIXME narrow + onFulfilled: M.call(M.record(), M.string()).returns(M.any()), // FIXME narrow }), makeRemoteChainFacadeWatcher: M.interface( 'makeRemoteChainFacadeWatcher', { - onFulfilled: M.call(M.any()) - .optional(M.arrayOf(M.undefined())) + onFulfilled: M.call(M.any(), M.string()) + .optional(M.arrayOf(M.undefined())) // XXX needed? .returns(M.any()), // FIXME narrow }, ), @@ -92,9 +92,14 @@ export const prepareOrchestratorKit = ( { /** Waits for `chainInfo` and returns a LocalChainFacade */ makeLocalChainFacadeWatcher: { - /** @param {ChainInfo} agoricChainInfo */ - onFulfilled(agoricChainInfo) { - return makeLocalChainFacade(agoricChainInfo); + /** + * @param {ChainInfo} agoricChainInfo + * @param {string} name + */ + onFulfilled(agoricChainInfo, name) { + const it = makeLocalChainFacade(agoricChainInfo); + chainByName.init(name, it); + return it; }, }, /** @@ -107,29 +112,50 @@ export const prepareOrchestratorKit = ( * RemoteChainFacade * * @param {[ChainInfo, ChainInfo, IBCConnectionInfo]} chainsAndConnection + * @param {string} name */ - onFulfilled([_agoricChainInfo, remoteChainInfo, connectionInfo]) { - return makeRemoteChainFacade(remoteChainInfo, connectionInfo); + onFulfilled([_agoricChainInfo, remoteChainInfo, connectionInfo], name) { + const it = makeRemoteChainFacade(remoteChainInfo, connectionInfo); + chainByName.init(name, it); + return it; }, }, orchestrator: { /** @type {HostOf} */ getChain(name) { + if (chainByName.has(name)) { + return asVow(() => chainByName.get(name)); + } if (name === 'agoric') { return watch( chainHub.getChainInfo('agoric'), this.facets.makeLocalChainFacadeWatcher, + name, ); } return watch( chainHub.getChainsAndConnection('agoric', name), this.facets.makeRemoteChainFacadeWatcher, + name, ); }, makeLocalAccount() { return watch(E(localchain).makeAccount()); }, - getBrandInfo: () => Fail`not yet implemented`, + /** @type {HostOf} */ + getBrandInfo(denom) { + const { chainName, baseName, baseDenom, brand } = + chainHub.lookupAsset(denom); + chainByName.has(chainName) || + Fail`use getChain(${q(chainName)}) before getBrandInfo(${q(denom)})`; + const chain = chainByName.get(chainName); + chainByName.has(baseName) || + Fail`use getChain(${q(baseName)}) before getBrandInfo(${q(denom)})`; + const base = chainByName.get(baseName); + // @ts-expect-error XXX HostOf<> not quite right? + return harden({ chain, base, brand, baseDenom }); + }, + /** @type {HostOf} */ asAmount: () => Fail`not yet implemented`, }, }, diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 800b2ced1ca..bc0760a9b16 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -4,7 +4,7 @@ import { M } from '@endo/patterns'; /** * @import {TypedPattern} from '@agoric/internal'; - * @import {ChainAddress, ChainInfo, CosmosChainInfo, DenomAmount} from './types.js'; + * @import {ChainAddress, CosmosAssetInfo, ChainInfo, CosmosChainInfo, DenomAmount, DenomDetail} from './types.js'; * @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js'; */ @@ -79,6 +79,17 @@ export const IBCConnectionInfoShape = M.splitRecord({ transferChannel: IBCChannelInfoShape, }); +/** @type {TypedPattern} */ +export const CosmosAssetInfoShape = M.splitRecord({ + base: M.string(), + name: M.string(), + display: M.string(), + symbol: M.string(), + denom_units: M.arrayOf( + M.splitRecord({ denom: M.string(), exponent: M.number() }), + ), +}); + /** @type {TypedPattern} */ export const CosmosChainInfoShape = M.splitRecord( { diff --git a/packages/orchestration/src/types.ts b/packages/orchestration/src/types.ts index 482820c7a66..9297dee237e 100644 --- a/packages/orchestration/src/types.ts +++ b/packages/orchestration/src/types.ts @@ -7,4 +7,5 @@ export type * from './exos/chain-account-kit.js'; export type * from './exos/icq-connection-kit.js'; export type * from './orchestration-api.js'; export type * from './exos/cosmos-interchain-service.js'; +export type * from './exos/chain-hub.js'; export type * from './vat-orchestration.js'; diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 9f9b9ac417f..f065e3097ae 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -114,10 +114,13 @@ export const provideOrchestration = ( vowTools, }); + const chainByName = zones.orchestration.mapStore('chainName'); + const makeOrchestratorKit = prepareOrchestratorKit(zones.orchestration, { asyncFlowTools, chainHub, localchain: remotePowers.localchain, + chainByName, makeRecorderKit, makeLocalChainFacade, makeRemoteChainFacade, diff --git a/packages/orchestration/test/assets.fixture.ts b/packages/orchestration/test/assets.fixture.ts new file mode 100644 index 00000000000..942be150c15 --- /dev/null +++ b/packages/orchestration/test/assets.fixture.ts @@ -0,0 +1,145 @@ +import type { CosmosAssetInfo } from '../src/cosmos-api.js'; + +// https://github.com/cosmos/chain-registry/blob/master/cosmoshub/assetlist.json +export const assets = { + cosmoshub: [ + { + description: + 'ATOM is the native cryptocurrency of the Cosmos network, designed to facilitate interoperability between multiple blockchains through its innovative hub-and-spoke model.', + extended_description: + "ATOM, the native cryptocurrency of the Cosmos network, is essential for achieving the project's goal of creating an 'Internet of Blockchains.' Launched in 2019, Cosmos aims to solve the scalability, usability, and interoperability issues prevalent in existing blockchain ecosystems. The Cosmos Hub, the central blockchain of the network, uses ATOM for transaction fees, staking, and governance. By staking ATOM, users can earn rewards and participate in governance, influencing decisions on network upgrades and changes.\n\nCosmos leverages the Tendermint consensus algorithm to achieve high transaction throughput and fast finality. Its Inter-Blockchain Communication (IBC) protocol enables seamless data and value transfer between different blockchains, fostering a highly interconnected and collaborative ecosystem. The flexibility and scalability offered by Cosmos have attracted numerous projects, enhancing its utility and adoption. ATOM's role in securing the network and facilitating governance underscores its importance in the broader blockchain landscape.", + denom_units: [ + { + denom: 'uatom', + exponent: 0, + }, + { + denom: 'atom', + exponent: 6, + }, + ], + base: 'uatom', + name: 'Cosmos Hub Atom', + display: 'atom', + symbol: 'ATOM', + logo_URIs: { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.svg', + }, + coingecko_id: 'cosmos', + images: [ + { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/cosmoshub/images/atom.svg', + theme: { + primary_color_hex: '#272d45', + }, + }, + ], + socials: { + website: 'https://cosmos.network', + twitter: 'https://twitter.com/cosmoshub', + }, + }, + { + description: 'Tether USDt on the Cosmos Hub', + denom_units: [ + { + denom: + 'ibc/F04D72CF9B5D9C849BB278B691CDFA2241813327430EC9CDC83F8F4CA4CDC2B0', + exponent: 0, + }, + { + denom: 'usdt', + exponent: 6, + }, + ], + type_asset: 'ics20', + base: 'ibc/F04D72CF9B5D9C849BB278B691CDFA2241813327430EC9CDC83F8F4CA4CDC2B0', + name: 'Tether USDt', + display: 'usdt', + symbol: 'USDt', + traces: [ + { + type: 'ibc', + counterparty: { + chain_name: 'kava', + base_denom: 'erc20/tether/usdt', + channel_id: 'channel-0', + }, + chain: { + channel_id: 'channel-277', + path: 'transfer/channel-277/erc20/tether/usdt', + }, + }, + ], + images: [ + { + image_sync: { + chain_name: 'kava', + base_denom: 'erc20/tether/usdt', + }, + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/usdt.svg', + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/usdt.png', + theme: { + circle: true, + primary_color_hex: '#009393', + background_color_hex: '#009393', + }, + }, + ], + logo_URIs: { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/usdt.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/usdt.svg', + }, + }, + { + description: 'FX on Cosmos Hub', + denom_units: [ + { + denom: + 'ibc/4925E6ABA571A44D2BE0286D2D29AF42A294D0FF2BB16490149A1B26EAD33729', + exponent: 0, + aliases: ['FX'], + }, + ], + type_asset: 'ics20', + base: 'ibc/4925E6ABA571A44D2BE0286D2D29AF42A294D0FF2BB16490149A1B26EAD33729', + name: 'Function X', + display: 'FX', + symbol: 'FX', + traces: [ + { + type: 'ibc', + counterparty: { + chain_name: 'fxcore', + base_denom: 'FX', + channel_id: 'channel-10', + }, + chain: { + channel_id: 'channel-585', + path: 'transfer/channel-585/FX', + }, + }, + ], + images: [ + { + image_sync: { + chain_name: 'fxcore', + base_denom: 'FX', + }, + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/fxcore/images/fx.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/fxcore/images/fx.svg', + theme: { + primary_color_hex: '#1c1c1c', + }, + }, + ], + logo_URIs: { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/fxcore/images/fx.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/fxcore/images/fx.svg', + }, + }, + ] as CosmosAssetInfo[], +}; +harden(assets); diff --git a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md index 95beb6676c2..ddff92fb013 100644 --- a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.md @@ -36,6 +36,7 @@ Generated by [AVA](https://avajs.dev). LocalChainFacade_kindHandle: 'Alleged: kind', Orchestrator_kindHandle: 'Alleged: kind', RemoteChainFacade_kindHandle: 'Alleged: kind', + chainName: {}, sendIt: { asyncFlow_kindHandle: 'Alleged: kind', endowments: { diff --git a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap index 785dda987fc..85e446a9dcb 100644 Binary files a/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap and b/packages/orchestration/test/examples/snapshots/sendAnywhere.test.ts.snap differ diff --git a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md index 9c684aaa5e0..f714aeab3a4 100644 --- a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.md @@ -37,6 +37,10 @@ Generated by [AVA](https://avajs.dev). LocalChainFacade_kindHandle: 'Alleged: kind', Orchestrator_kindHandle: 'Alleged: kind', RemoteChainFacade_kindHandle: 'Alleged: kind', + chainName: { + omniflixhub: 'Alleged: RemoteChainFacade public', + stride: 'Alleged: RemoteChainFacade public', + }, }, vows: { PromiseWatcher_kindHandle: 'Alleged: kind', diff --git a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap index 90dff347a73..55ffa320693 100644 Binary files a/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap and b/packages/orchestration/test/examples/snapshots/unbondExample.test.ts.snap differ diff --git a/packages/orchestration/test/exos/chain-hub.test.ts b/packages/orchestration/test/exos/chain-hub.test.ts index 446c6c02bd4..e5cb3c05c07 100644 --- a/packages/orchestration/test/exos/chain-hub.test.ts +++ b/packages/orchestration/test/exos/chain-hub.test.ts @@ -4,12 +4,19 @@ import test from '@endo/ses-ava/prepare-endo.js'; import { makeNameHubKit } from '@agoric/vats'; import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import { makeChainHub } from '../../src/exos/chain-hub.js'; +import { E } from '@endo/far'; +import { makeChainHub, registerAssets } from '../../src/exos/chain-hub.js'; import { provideDurableZone } from '../supports.js'; -import { registerKnownChains } from '../../src/chain-info.js'; +import { + registerChainAssets, + registerKnownChains, +} from '../../src/chain-info.js'; import knownChains from '../../src/fetched-chain-info.js'; -import type { IBCConnectionInfo } from '../../src/cosmos-api.js'; +import type { + CosmosChainInfo, + IBCConnectionInfo, +} from '../../src/cosmos-api.js'; +import { assets as assetFixture } from '../assets.fixture.js'; const connection = { id: 'connection-1', @@ -87,3 +94,63 @@ test.serial('getConnectionInfo', async t => { // Look up the opposite direction t.deepEqual(await vt.when(chainHub.getConnectionInfo(b, a)), ba); }); + +test('getBrandInfo support', async t => { + const { chainHub } = setup(); + + const denom = 'utok1'; + const info1: CosmosChainInfo = { + chainId: 'chain1', + stakingTokens: [{ denom }], + }; + + chainHub.registerChain('chain1', info1); + const info = { + chainName: 'chain1', + baseName: 'chain1', + baseDenom: denom, + }; + chainHub.registerAsset('utok1', info); + + const actual = chainHub.lookupAsset('utok1'); + t.deepEqual(actual, info); +}); + +test('toward asset info in agoricNames (#9572)', async t => { + const { chainHub, nameAdmin, vt } = setup(); + // use fetched chain info + await registerKnownChains(nameAdmin); + + await vt.when(chainHub.getChainInfo('cosmoshub')); + + for (const name of ['kava', 'fxcore']) { + chainHub.registerChain(name, { chainId: name }); + } + + await registerChainAssets(nameAdmin, 'cosmoshub', assetFixture.cosmoshub); + const details = await E(E(nameAdmin).readonly()).lookup( + 'chainAssets', + 'cosmoshub', + ); + registerAssets(chainHub, 'cosmoshub', details); + + { + const actual = chainHub.lookupAsset('uatom'); + t.deepEqual(actual, { + chainName: 'cosmoshub', + baseName: 'cosmoshub', + baseDenom: 'uatom', + }); + } + + { + const actual = chainHub.lookupAsset( + 'ibc/F04D72CF9B5D9C849BB278B691CDFA2241813327430EC9CDC83F8F4CA4CDC2B0', + ); + t.deepEqual(actual, { + chainName: 'cosmoshub', + baseName: 'kava', + baseDenom: 'erc20/tether/usdt', + }); + } +}); diff --git a/packages/orchestration/test/facade.test.ts b/packages/orchestration/test/facade.test.ts index 5132770fa5a..5655d80895f 100644 --- a/packages/orchestration/test/facade.test.ts +++ b/packages/orchestration/test/facade.test.ts @@ -3,14 +3,17 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; import { reincarnate } from '@agoric/swingset-liveslots/tools/setup-vat-data.js'; +import { makeIssuerKit } from '@agoric/ertp'; import type { CosmosChainInfo, IBCConnectionInfo } from '../src/cosmos-api.js'; import type { Chain } from '../src/orchestration-api.js'; import { provideOrchestration } from '../src/utils/start-helper.js'; import { commonSetup, provideDurableZone } from './supports.js'; +import { denomHash } from '../src/utils/denomHash.js'; +import fetchedChainInfo from '../src/fetched-chain-info.js'; // Refresh with scripts/refresh-chain-info.ts const test = anyTest; -export const mockChainInfo: CosmosChainInfo = harden({ +const mockChainInfo: CosmosChainInfo = harden({ chainId: 'mock-1', icaEnabled: false, icqEnabled: false, @@ -18,7 +21,7 @@ export const mockChainInfo: CosmosChainInfo = harden({ ibcHooksEnabled: false, stakingTokens: [{ denom: 'umock' }], }); -export const mockChainConnection: IBCConnectionInfo = { +const mockChainConnection: IBCConnectionInfo = { id: 'connection-0', client_id: '07-tendermint-2', counterparty: { @@ -127,4 +130,104 @@ test.serial('faulty chain info', async t => { }); }); +test.serial('asset / denom info', async t => { + const { facadeServices, commonPrivateArgs } = await commonSetup(t); + + // XXX relax again + reincarnate({ relaxDurabilityRules: true }); + const { zcf } = await setupZCFTest(); + const zone = provideDurableZone('test'); + const vt = prepareSwingsetVowTools(zone); + const orchKit = provideOrchestration( + zcf, + zone.mapStore('test'), + { + agoricNames: facadeServices.agoricNames, + timerService: facadeServices.timerService, + storageNode: commonPrivateArgs.storageNode, + orchestrationService: facadeServices.orchestrationService, + localchain: facadeServices.localchain, + }, + commonPrivateArgs.marshaller, + ); + const { chainHub, orchestrate } = orchKit; + + chainHub.registerChain('agoric', fetchedChainInfo.agoric); + chainHub.registerChain(mockChainInfo.chainId, mockChainInfo); + chainHub.registerConnection( + 'agoric-3', + mockChainInfo.chainId, + mockChainConnection, + ); + + chainHub.registerAsset('utoken1', { + chainName: mockChainInfo.chainId, + baseName: mockChainInfo.chainId, + baseDenom: 'utoken1', + }); + + const { channelId } = mockChainConnection.transferChannel; + const agDenom = `ibc/${denomHash({ denom: 'utoken1', channelId })}`; + const { brand } = makeIssuerKit('Token1'); + t.log(`utoken1 over ${channelId}: ${agDenom}`); + chainHub.registerAsset(agDenom, { + chainName: 'agoric', + baseName: mockChainInfo.chainId, + baseDenom: 'utoken1', + brand, + }); + + const handle = orchestrate( + 'useDenoms', + { brand }, + // eslint-disable-next-line no-shadow + async (orc, { brand }) => { + const c1 = await orc.getChain(mockChainInfo.chainId); + + { + const actual = orc.getBrandInfo('utoken1'); + const info = await actual.chain.getChainInfo(); + t.deepEqual(info, mockChainInfo); + + t.deepEqual(actual, { + base: c1, + chain: c1, + baseDenom: 'utoken1', + brand: undefined, + }); + } + + const ag = await orc.getChain('agoric'); + { + const actual = orc.getBrandInfo(agDenom); + + t.deepEqual(actual, { + chain: ag, + base: c1, + baseDenom: 'utoken1', + brand, + }); + } + }, + ); + + await vt.when(handle()); + + chainHub.registerChain('anotherChain', mockChainInfo); + chainHub.registerConnection('agoric-3', 'anotherChain', mockChainConnection); + chainHub.registerAsset('utoken2', { + chainName: 'anotherChain', + baseName: 'anotherChain', + baseDenom: 'utoken2', + }); + + const missingGetChain = orchestrate('missing getChain', {}, async orc => { + const actual = orc.getBrandInfo('utoken2'); + }); + + await t.throwsAsync(vt.when(missingGetChain()), { + message: 'use getChain("anotherChain") before getBrandInfo("utoken2")', + }); +}); + test.todo('contract upgrade');