From b78f927ce15cbf5354a7bcf3573ca3a42460ab71 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 22 Jul 2024 18:31:31 -0500 Subject: [PATCH 1/5] chore: don't export from tests --- packages/orchestration/test/facade.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/orchestration/test/facade.test.ts b/packages/orchestration/test/facade.test.ts index 5132770fa5a..0adb161c880 100644 --- a/packages/orchestration/test/facade.test.ts +++ b/packages/orchestration/test/facade.test.ts @@ -10,7 +10,7 @@ import { commonSetup, provideDurableZone } from './supports.js'; const test = anyTest; -export const mockChainInfo: CosmosChainInfo = harden({ +const mockChainInfo: CosmosChainInfo = harden({ chainId: 'mock-1', icaEnabled: false, icqEnabled: false, @@ -18,7 +18,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: { From a5fab62fdc08ae48075ce5ba0ec36f852f87f43b Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 25 Jul 2024 11:15:29 -0500 Subject: [PATCH 2/5] feat: chainHub.registerAsset / lookupAsset --- packages/orchestration/src/exos/chain-hub.js | 53 ++++++++++++++++++- packages/orchestration/src/typeGuards.js | 2 +- packages/orchestration/src/types.ts | 1 + .../orchestration/test/exos/chain-hub.test.ts | 27 +++++++++- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index 7f235bac09c..0b78ac4932c 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'; @@ -11,8 +12,9 @@ import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {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,6 +24,20 @@ 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 */ @@ -146,6 +162,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 +190,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,6 +360,31 @@ 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; diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 800b2ced1ca..71253dfbd4f 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, ChainInfo, CosmosChainInfo, DenomAmount, DenomDetail} from './types.js'; * @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js'; */ 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/test/exos/chain-hub.test.ts b/packages/orchestration/test/exos/chain-hub.test.ts index 446c6c02bd4..cd163bfbaa7 100644 --- a/packages/orchestration/test/exos/chain-hub.test.ts +++ b/packages/orchestration/test/exos/chain-hub.test.ts @@ -4,12 +4,14 @@ 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 { provideDurableZone } from '../supports.js'; import { 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'; const connection = { id: 'connection-1', @@ -87,3 +89,24 @@ 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, vt } = 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); +}); From 7ac60a25148d745a59a20715e9fed199d3071d3f Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 25 Jul 2024 11:15:53 -0500 Subject: [PATCH 3/5] feat: Orchestrator.getBrandInfo --- .../orchestration/src/exos/orchestrator.js | 54 ++++++--- .../orchestration/src/utils/start-helper.js | 3 + packages/orchestration/test/facade.test.ts | 103 ++++++++++++++++++ 3 files changed, 146 insertions(+), 14 deletions(-) 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/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/facade.test.ts b/packages/orchestration/test/facade.test.ts index 0adb161c880..5655d80895f 100644 --- a/packages/orchestration/test/facade.test.ts +++ b/packages/orchestration/test/facade.test.ts @@ -3,10 +3,13 @@ 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; @@ -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'); From 829331a59a4c47efa4adbaab86eb03683dcdcc98 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 25 Jul 2024 16:42:10 -0500 Subject: [PATCH 4/5] chore: support for asset info in agoricNames --- packages/orchestration/src/chain-info.js | 34 +++- packages/orchestration/src/cosmos-api.ts | 25 ++- packages/orchestration/src/exos/chain-hub.js | 24 ++- packages/orchestration/src/typeGuards.js | 13 +- packages/orchestration/test/assets.fixture.ts | 145 ++++++++++++++++++ .../orchestration/test/exos/chain-hub.test.ts | 50 +++++- 6 files changed, 278 insertions(+), 13 deletions(-) create mode 100644 packages/orchestration/test/assets.fixture.ts 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 0b78ac4932c..24b280085b3 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -10,7 +10,7 @@ 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'; @@ -42,6 +42,8 @@ export const DenomDetailShape = M.splitRecord( 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 @@ -390,3 +392,23 @@ export const makeChainHub = (agoricNames, vowTools) => { 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/typeGuards.js b/packages/orchestration/src/typeGuards.js index 71253dfbd4f..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, DenomDetail} 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/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/exos/chain-hub.test.ts b/packages/orchestration/test/exos/chain-hub.test.ts index cd163bfbaa7..e5cb3c05c07 100644 --- a/packages/orchestration/test/exos/chain-hub.test.ts +++ b/packages/orchestration/test/exos/chain-hub.test.ts @@ -4,14 +4,19 @@ import test from '@endo/ses-ava/prepare-endo.js'; import { makeNameHubKit } from '@agoric/vats'; import { prepareSwingsetVowTools } from '@agoric/vow/vat.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 { CosmosChainInfo, IBCConnectionInfo, } from '../../src/cosmos-api.js'; +import { assets as assetFixture } from '../assets.fixture.js'; const connection = { id: 'connection-1', @@ -91,7 +96,7 @@ test.serial('getConnectionInfo', async t => { }); test('getBrandInfo support', async t => { - const { chainHub, vt } = setup(); + const { chainHub } = setup(); const denom = 'utok1'; const info1: CosmosChainInfo = { @@ -110,3 +115,42 @@ test('getBrandInfo support', async t => { 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', + }); + } +}); From ca79a5809a1315352274f2fb7015546e387388f3 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 25 Jul 2024 20:24:56 -0500 Subject: [PATCH 5/5] chore: regenerate snapshots with chainName collection --- .../examples/snapshots/sendAnywhere.test.ts.md | 1 + .../snapshots/sendAnywhere.test.ts.snap | Bin 954 -> 972 bytes .../examples/snapshots/unbondExample.test.ts.md | 4 ++++ .../snapshots/unbondExample.test.ts.snap | Bin 786 -> 858 bytes 4 files changed, 5 insertions(+) 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 785dda987fc272fc94708bed534a963109e78ead..85e446a9dcb5accc8b029a545bcdb49c9bd2a534 100644 GIT binary patch literal 972 zcmV;-12g}^X6AOLMkpEghdbM@- zkyo`qP9KX100000000A>R!wi*L>PW%ZLjT3Hy=$(NSh*80uCIIsvba`*c4V9HEp7X zq(VZiX6;$8yWW{)JZ354(jR~XCnVrPZXCG4Z$NwreghH+i4)=vAhb@rvBxEf>yzd8 zd7gRa{q&cEek}Y^^7x3UNX0Dd(=enVixN&J$xx`|@lg;^Mc-a{#ej|3Lmc9C0qg+y z7{HGJ{s8bVfU5+!L4Z}#Tp`Wf{zK*~Lguf_%gZ2b5F2C(tE90;3=nIDJoiQ_X+LJ( zUfPeNWa!HuxZJe6T_rx^GzP?)cYJ4ErEPxjd8^Aja z;PVFXdjq&?01ph{YXkVx0A4VGTPDCw;1g2|XrmlZhgd(Dz^^9Iwtxc*_{h>IO`YO= zZvnqpz_ljuZWH*X3H;pzuG_$ltdyBSyH%CRj6F! z@?4+-)|+ETg!@WJR@aSz==o^C|}~&uJ%+Cl273Z;E6r5^uLS)4Y!Fi;k1BsyKM$7=#dMo1COM6hr-=Kd?A!wt#mPCS zC$rw_NJ$?NBY#_3TB_^;^UN(_ooRLSg=Bo^06#l`e9W$O(32cgbdW?QSWvD}&{kIk^PXn|2WKU`tXSZ1ck{%15=Uw<1vSWMj_Pv3fw_*#vn@ uQl1Rz_tAQpeMUe^Ko-+Ji{IbOqvW0ZVtaI;nDi1wrTPyoULUoG3jhELG2j0H literal 954 zcmV;r14aBnRzV?K<+(NO9>B>t6 zZm#wv$RCRc00000000A>R!wi*L>PX?Uu$ogk0mK-+7!7Gr%E6pIB`Ln(j`$-B_uR0 zLa3UxXLsH8&a7s}UCOODKmu{=nR4R7iGKhGnm>VrdO`dH4hXFiZ`NZG#r4VZ`#jIQ z^Um|W;~#q6R76Ad^f}XsPFcT8`+eGHiQ;su212W+&*PYC`f2kY8x2e!;uF6EU>Csm z0A2ui3E*D<*9q`00al6EBHmv25sNe-v)7fC6_7=c2FNm2iM2*-5Vu9%_%M^So3d~} z>!yhsgm)7CaG3CToANki1#GdMrmWB6_rqz-&{#cSk&rQy7a%SXvQ2;@98mRyN1aqm z^3b!nkRJ)IC5?0*Ii8Cw3K#@I35VXJO~Dn5g{gdvjmyF9n+x@X~<=Jg|Ug z7Vw7!Y}mkOHt>@T{AmL>9N;4dc;o=zIz~aQazTCK{^kJ34iLD&$1d=-YfwCs()iT{ zUbw(@4;XpC&mQo%2fWoVs11|q+-?AO8o)OV;MjyO81QoRQpF1k#yM`Z<59xf#fj9l z!4-q965dal7Q6tg&)8+8p|iG?OtVY`ubaVUC*wKs9-l}$9$(0jC53BMg~~Rr&IBr8 zvoUr>|4<9b>ZY-AsHtXkb}{V%|IF=cCw-Oi=r-*$d7loMJn4xWbEwt~)uBn6Np=GH zU>?#-#iS$TUZx{4Vn>zJ3bH)QyL4Q9xjaj4!szfW9p@#;9p{+)yfvT+-_E)xj;QT8 zgMNg?ynG!R(YUsk*UwTBwRf|5z(zu|ddn{n^7)*FJ;ivuqw^E6 zO#|*rF-jD>Pjxh?U+k+#VzR?ElbojN6^eT~U-zC)Qgs39=|FeeQqm{HF5Z@xmn*~G zIrl1X&3ncE$_Kvlf!}@OPVp}Ks5F)OKiva&2uce801img5C8xG 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 90dff347a7304446288687837454c7a52b64a33f..55ffa320693b47b977b2ec221bc4e5fc22dd7000 100644 GIT binary patch literal 858 zcmV-g1Eu^yRzV>3L?!N@ftY1R(y zZnWLR!wizMi_p^_Ie#czLbQ|fXtAJ6l4OKP^0-haWZu#)rCQe#D`Xg}?0o8EtsB-GM7Q^$PNF?>iwq9%ZK0FMB? z2JivE{{YSq-~s^_Nw`45jn?m+SwhCI^Yil{Gaz-4IV=)yiTEJ31#;>T?4`p z_$>q;gutuNk=I>$@IC}ShQJSXpj~(9M;!WLbh^|B)6N;5Y$QFQ8l%&w+5#sWc`8&# za;w#daD1#j%yi-`{UU~03azSUu+q#_F1)1%hW7gtb>pE3r^^vapK)p&phR|uVqJGO ztv0-B8Yed`wYRSO+R`B{ zjim+J7v1ltRh{;98gCR0C2laLGiBp7Ehjd${|a>r*EhGe1aQu@6iR-$wg71G}l&5u1iMABG-K;fQJ2#e zPbM+lrXPz400000000A>R?BYFKomV=J9Y}BY1EX`2gqxL*sx;*p%4nBYSr{1R%qf( z6O+kIG@cO3iXyRw9TE$q_7hll)fIdJQrG+j6^ctzXChEcR@R(z?p)u;aSr!tTn4S^ z;E3r^bH*Y~l@238J5f{W=-{YcrJ_7g-;3t4H0M`j{hXAw0 zEf9CT_L2pfkm2LZ%nV2l!~@A=mRLn%gJcTi#>1|nHO~BvZjFag(|;D~7p+j#SE;CT zHlXE7oU;b2-}U>L5o30f1ya?Snm{;B$SMH_@Dz>qMNsCl7ek*CP&SZ4D;nq+IY}Tn zse&epw4yqcB8Gn@VCR+WMe*q^>*CaYBU!(ISDsqHh6Q}EfFBl6uz{)#9NNIC4JAz55xUoB=*(fUgiyNaDtj+2x