Skip to content

Commit

Permalink
feat: getBrandInfo using ChainHub (#9754)
Browse files Browse the repository at this point in the history
closes #9211

## Description

  - feat: chainHub.registerAsset / lookupAsset
  - feat: Orchestrator.getBrandInfo
    - getChain() is now backed by a store
  - groundwork for asset info in agoricNames  - #9752

### Testing Considerations

unit tests cover happy path.

Low code-complexity doesn't seem to merit more.

### Security / Scaling Considerations

none, AFAICT

### Documentation Considerations

make `getBrandInfo()` work as documented, provided assets are registered with the ChainHub

### Upgrade Considerations

not yet deployed
  • Loading branch information
mergify[bot] committed Jul 26, 2024
2 parents bf791ed + ca79a58 commit 77fcd1a
Show file tree
Hide file tree
Showing 14 changed files with 507 additions and 32 deletions.
34 changes: 27 additions & 7 deletions packages/orchestration/src/chain-info.js
Original file line number Diff line number Diff line change
@@ -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 */

Expand Down Expand Up @@ -66,7 +72,21 @@ const knownChains = /** @satisfies {Record<string, ChainInfo>} */ (
/** @typedef {typeof knownChains} KnownChains */

/**
* @param {ERef<import('@agoric/vats').NameHubKit['nameAdmin']>} agoricNamesAdmin
* TODO(#9572): include this in registerChain
*
* @param {ERef<NameAdmin>} 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<NameAdmin>} agoricNamesAdmin
* @param {string} name
* @param {CosmosChainInfo} chainInfo
* @param {(...messages: string[]) => void} [log]
Expand All @@ -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;
Expand Down
25 changes: 24 additions & 1 deletion packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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<string, unknown>;

/**
* Info for a Cosmos-based chain.
*/
Expand Down
77 changes: 74 additions & 3 deletions packages/orchestration/src/exos/chain-hub.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
*/

/**
Expand All @@ -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<DenomDetail>} */
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
Expand Down Expand Up @@ -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),
});

/**
Expand All @@ -172,6 +192,12 @@ export const makeChainHub = (agoricNames, vowTools) => {
valueShape: IBCConnectionInfoShape,
});

/** @type {MapStore<string, DenomDetail>} */
const denomDetails = zone.mapStore('denom', {
keyShape: M.string(),
valueShape: DenomDetailShape,
});

const lookupChainInfo = vowTools.retriable(
zone,
'lookupChainInfo',
Expand Down Expand Up @@ -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<typeof makeChainHub>} 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 });
}
};
54 changes: 40 additions & 14 deletions packages/orchestration/src/exos/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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';
Expand All @@ -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');

Expand All @@ -47,6 +47,7 @@ export const OrchestratorI = M.interface('Orchestrator', {
* asyncFlowTools: AsyncFlowTools;
* chainHub: ChainHub;
* localchain: Remote<LocalChain>;
* chainByName: MapStore<string, HostInterface<Chain>>;
* makeRecorderKit: MakeRecorderKit;
* makeLocalChainFacade: MakeLocalChainFacade;
* makeRemoteChainFacade: MakeRemoteChainFacade;
Expand All @@ -62,25 +63,24 @@ export const prepareOrchestratorKit = (
{
chainHub,
localchain,
chainByName,
makeLocalChainFacade,
makeRemoteChainFacade,
vowTools: { watch },
vowTools: { watch, asVow },
},
) =>
zone.exoClassKit(
'Orchestrator',
{
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
},
),
Expand All @@ -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;
},
},
/**
Expand All @@ -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<Orchestrator['getChain']>} */
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<Orchestrator['getBrandInfo']>} */
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<Orchestrator['asAmount']>} */
asAmount: () => Fail`not yet implemented`,
},
},
Expand Down
13 changes: 12 additions & 1 deletion packages/orchestration/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
*/

Expand Down Expand Up @@ -79,6 +79,17 @@ export const IBCConnectionInfoShape = M.splitRecord({
transferChannel: IBCChannelInfoShape,
});

/** @type {TypedPattern<CosmosAssetInfo>} */
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<CosmosChainInfo>} */
export const CosmosChainInfoShape = M.splitRecord(
{
Expand Down
1 change: 1 addition & 0 deletions packages/orchestration/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading

0 comments on commit 77fcd1a

Please sign in to comment.