From b8b592c1587fd2e17706a9ab7a6acfc8ac305c3d Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 17 Jun 2024 15:53:41 -0700 Subject: [PATCH 1/4] refactor: extract writeChainInfo --- .../orchestration/scripts/fetch-chain-info.ts | 101 +------------- packages/orchestration/src/utils/registry.js | 131 ++++++++++++++++++ 2 files changed, 133 insertions(+), 99 deletions(-) create mode 100644 packages/orchestration/src/utils/registry.js diff --git a/packages/orchestration/scripts/fetch-chain-info.ts b/packages/orchestration/scripts/fetch-chain-info.ts index d294f6d6c29..9634dfc884e 100755 --- a/packages/orchestration/scripts/fetch-chain-info.ts +++ b/packages/orchestration/scripts/fetch-chain-info.ts @@ -1,17 +1,9 @@ #!/usr/bin/env tsx /** @file Fetch canonical chain info to generate the minimum needed for agoricNames */ -import { - State as IBCChannelState, - Order, -} from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; -import { State as IBCConnectionState } from '@agoric/cosmic-proto/ibc/core/connection/v1/connection.js'; -import type { IBCChannelID, IBCConnectionID } from '@agoric/vats'; import { ChainRegistryClient } from '@chain-registry/client'; -import type { IBCInfo } from '@chain-registry/types'; -import assert from 'node:assert'; import fsp from 'node:fs/promises'; import prettier from 'prettier'; -import type { CosmosChainInfo, IBCConnectionInfo } from '../src/cosmos-api.js'; +import { convertChainInfo } from '../src/utils/registry.js'; // XXX script assumes it's run from the package path // XXX .json would be more apt; UNTIL https://github.com/endojs/endo/issues/2110 @@ -43,96 +35,7 @@ const client = new ChainRegistryClient({ // chain info, assets and ibc data will be downloaded dynamically by invoking fetchUrls method await client.fetchUrls(); -const chainInfo = {} as Record; - -function toConnectionEntry(ibcInfo: IBCInfo, name: string) { - // IbcInfo encodes the undirected edge as a tuple of (chain_1, chain_2) in alphabetical order - const fromChain1 = ibcInfo.chain_1.chain_name === name; - const [from, to] = fromChain1 - ? [ibcInfo.chain_1, ibcInfo.chain_2] - : [ibcInfo.chain_2, ibcInfo.chain_1]; - assert.equal(from.chain_name, name); - const transferChannels = ibcInfo.channels.filter( - c => - c.chain_1.port_id === 'transfer' && - // @ts-expect-error tags does not specify keys - c.tags?.preferred, - ); - if (transferChannels.length === 0) { - console.warn( - 'no transfer channel for [', - from.chain_name, - to.chain_name, - ']', - '(skipping)', - ); - return []; - } - if (transferChannels.length > 1) { - console.warn( - 'multiple preferred transfer channels [', - from.chain_name, - to.chain_name, - ']:', - transferChannels, - '(choosing first)', - ); - } - const [channel] = transferChannels; - const [channelFrom, channelTo] = fromChain1 - ? [channel.chain_2, channel.chain_1] - : [channel.chain_1, channel.chain_2]; - const record = { - id: from.connection_id as IBCConnectionID, - client_id: from.client_id, - counterparty: { - client_id: to.client_id, - connection_id: to.connection_id as IBCConnectionID, - prefix: { - key_prefix: 'FIXME', - }, - }, - state: IBCConnectionState.STATE_OPEN, // XXX presumably - transferChannel: { - channelId: channelFrom.channel_id as IBCChannelID, - portId: channelFrom.port_id, - counterPartyChannelId: channelTo.channel_id as IBCChannelID, - counterPartyPortId: channelTo.port_id, - // FIXME mapping, our guard expects a numerical enum - ordering: Order.ORDER_NONE_UNSPECIFIED, - state: IBCChannelState.STATE_OPEN, // XXX presumably - version: channel.version, - }, - } as IBCConnectionInfo; - const destChainId = chainInfo[to.chain_name].chainId; - return [destChainId, record] as const; -} - -for (const name of chainNames) { - console.log('processing info', name); - - const chain = client.getChain(name); - chainInfo[name] = { - chainId: chain.chain_id, - stakingTokens: chain.staking?.staking_tokens, - // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 - icqEnabled: name === 'osmosis', - }; -} - -// iterate this after chainInfo is filled out -for (const name of chainNames) { - console.log('processing connections', name); - - const ibcData = client.getChainIbcData(name); - const connections = Object.fromEntries( - ibcData - .map(datum => toConnectionEntry(datum, name)) - // sort alphabetically for consistency - .sort(([a], [b]) => a?.localeCompare(b)), - ); - chainInfo[name] = { ...chainInfo[name], connections }; -} +const chainInfo = await convertChainInfo(client); const record = JSON.stringify(chainInfo, null, 2); const src = `/** @file Generated by fetch-chain-info.ts */\nexport default /** @type {const} } */ (${record});`; diff --git a/packages/orchestration/src/utils/registry.js b/packages/orchestration/src/utils/registry.js new file mode 100644 index 00000000000..c6a4183624e --- /dev/null +++ b/packages/orchestration/src/utils/registry.js @@ -0,0 +1,131 @@ +import { + State as IBCChannelState, + Order, +} from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; +import { State as IBCConnectionState } from '@agoric/cosmic-proto/ibc/core/connection/v1/connection.js'; +import assert from 'node:assert'; + +/** + * @import {IBCChannelID, IBCConnectionID} from '@agoric/vats'; + * @import {Chain, IBCInfo} from '@chain-registry/types'; + * @import {ChainRegistryClient} from '@chain-registry/client'; + * @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js'; + */ + +/** + * @param {IBCInfo} ibcInfo + * @param {string} name + * @param {Record} chainInfo + * @returns {[string, IBCConnectionInfo] | []} + */ +function toConnectionEntry(ibcInfo, name, chainInfo) { + // IbcInfo encodes the undirected edge as a tuple of (chain_1, chain_2) in alphabetical order + const fromChain1 = ibcInfo.chain_1.chain_name === name; + const [from, to] = fromChain1 + ? [ibcInfo.chain_1, ibcInfo.chain_2] + : [ibcInfo.chain_2, ibcInfo.chain_1]; + assert.equal(from.chain_name, name); + const transferChannels = ibcInfo.channels.filter( + c => + c.chain_1.port_id === 'transfer' && + // @ts-expect-error tags does not specify keys + c.tags?.preferred, + ); + if (transferChannels.length === 0) { + console.warn( + 'no transfer channel for [', + from.chain_name, + to.chain_name, + ']', + '(skipping)', + ); + return []; + } + if (transferChannels.length > 1) { + console.warn( + 'multiple preferred transfer channels [', + from.chain_name, + to.chain_name, + ']:', + transferChannels, + '(choosing first)', + ); + } + const [channel] = transferChannels; + const [channelFrom, channelTo] = fromChain1 + ? [channel.chain_2, channel.chain_1] + : [channel.chain_1, channel.chain_2]; + const record = { + id: /** @type {IBCConnectionID} */ (from.connection_id), + client_id: from.client_id, + counterparty: { + client_id: to.client_id, + connection_id: /** @type {IBCConnectionID} */ (to.connection_id), + prefix: { + key_prefix: 'FIXME', + }, + }, + state: IBCConnectionState.STATE_OPEN, // XXX presumably + transferChannel: { + channelId: /** @type {IBCChannelID} */ (channelFrom.channel_id), + portId: channelFrom.port_id, + counterPartyChannelId: /** @type {IBCChannelID} */ (channelTo.channel_id), + counterPartyPortId: channelTo.port_id, + // FIXME mapping, our guard expects a numerical enum + ordering: Order.ORDER_NONE_UNSPECIFIED, + state: IBCChannelState.STATE_OPEN, // XXX presumably + version: channel.version, + }, + }; + const destChainId = chainInfo[to.chain_name].chainId; + return [destChainId, record]; +} + +/** + * Converts the given chain info to our local config format + * + * @param {Pick} registry + */ +export const convertChainInfo = async registry => { + /** @type {Record} */ + const chainInfo = {}; + + for (const chain of registry.chains) { + console.log('processing info', chain.chain_name); + chainInfo[chain.chain_name] = { + chainId: chain.chain_id, + stakingTokens: chain.staking?.staking_tokens, + // UNTIL https://github.com/Agoric/agoric-sdk/issues/9326 + icqEnabled: chain.chain_name === 'osmosis', + }; + } + + // XXX probably easier to keep ibc separate + const ibcLookup = {}; + for (const ibc of registry.ibcData) { + ibcLookup[ibc.chain_1.chain_name] ||= []; + ibcLookup[ibc.chain_2.chain_name] ||= []; + + ibcLookup[ibc.chain_1.chain_name].push(ibc); + ibcLookup[ibc.chain_2.chain_name].push(ibc); + } + + const chainNames = registry.chains.map(c => c.chain_name).sort(); + + // iterate this after chainInfo is filled out + for (const name of chainNames) { + console.log('processing connections', name); + + const ibcData = ibcLookup[name]; + const connections = Object.fromEntries( + ibcData + .map(datum => toConnectionEntry(datum, name, chainInfo)) + // sort alphabetically for consistency + .sort(([a], [b]) => (a && b ? a.localeCompare(b) : 0)), + ); + chainInfo[name] = { ...chainInfo[name], connections }; + } + + // return object with insertion in alphabetical order of chain name + return Object.fromEntries(chainNames.map(name => [name, chainInfo[name]])); +}; From ad6e2e3395521a78da087f2de3b38a5b784df3c3 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 17 Jun 2024 16:33:06 -0700 Subject: [PATCH 2/4] refactor: remove extra export --- packages/builders/scripts/inter-protocol/add-STARS.js | 4 ++-- packages/builders/scripts/smart-wallet/build-game1-start.js | 2 +- packages/builders/scripts/testing/add-LEMONS.js | 2 +- packages/builders/scripts/testing/add-OLIVES.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/builders/scripts/inter-protocol/add-STARS.js b/packages/builders/scripts/inter-protocol/add-STARS.js index 48809f22daa..50d2e6dbe9f 100644 --- a/packages/builders/scripts/inter-protocol/add-STARS.js +++ b/packages/builders/scripts/inter-protocol/add-STARS.js @@ -3,7 +3,7 @@ import { defaultProposalBuilder as vaultProposalBuilder } from './add-collateral import { defaultProposalBuilder as oraclesProposalBuilder } from './price-feed-core.js'; /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ -export const starsVaultProposalBuilder = async powers => { +const starsVaultProposalBuilder = async powers => { return vaultProposalBuilder(powers, { interchainAssetOptions: { // Values for the Stargaze token on Osmosis @@ -18,7 +18,7 @@ export const starsVaultProposalBuilder = async powers => { }; /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ -export const starsOraclesProposalBuilder = async powers => { +const starsOraclesProposalBuilder = async powers => { return oraclesProposalBuilder(powers, { AGORIC_INSTANCE_NAME: `STARS-USD price feed`, IN_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'STARS'], diff --git a/packages/builders/scripts/smart-wallet/build-game1-start.js b/packages/builders/scripts/smart-wallet/build-game1-start.js index 20ecee5b4d2..aee88216584 100644 --- a/packages/builders/scripts/smart-wallet/build-game1-start.js +++ b/packages/builders/scripts/smart-wallet/build-game1-start.js @@ -9,7 +9,7 @@ import { makeHelpers } from '@agoric/deploy-script-support'; import { getManifestForGame1 } from '@agoric/smart-wallet/test/start-game1-proposal.js'; /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ -export const game1ProposalBuilder = async ({ publishRef, install }) => { +const game1ProposalBuilder = async ({ publishRef, install }) => { return harden({ sourceSpec: '@agoric/smart-wallet/test/start-game1-proposal.js', getManifestCall: [ diff --git a/packages/builders/scripts/testing/add-LEMONS.js b/packages/builders/scripts/testing/add-LEMONS.js index f0fce8380f1..a17827f3684 100644 --- a/packages/builders/scripts/testing/add-LEMONS.js +++ b/packages/builders/scripts/testing/add-LEMONS.js @@ -4,7 +4,7 @@ import { defaultProposalBuilder as vaultProposalBuilder } from '../inter-protoco /** @file This is for use in tests in a3p-integration */ /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ -export const starsVaultProposalBuilder = async powers => { +const starsVaultProposalBuilder = async powers => { return vaultProposalBuilder(powers, { interchainAssetOptions: { denom: 'ibc/000C0FFEECAFE000', diff --git a/packages/builders/scripts/testing/add-OLIVES.js b/packages/builders/scripts/testing/add-OLIVES.js index 9b82d9242b7..011d70e3e3b 100644 --- a/packages/builders/scripts/testing/add-OLIVES.js +++ b/packages/builders/scripts/testing/add-OLIVES.js @@ -4,7 +4,7 @@ import { defaultProposalBuilder as vaultProposalBuilder } from '../inter-protoco /** @file This is for use in tests in a3p-integration */ /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ -export const stars2VaultProposalBuilder = async powers => { +const stars2VaultProposalBuilder = async powers => { return vaultProposalBuilder(powers, { interchainAssetOptions: { denom: 'ibc/111C0FFEECAFE111', From ca97f1e579817fd77008c143fda1e0efed915077 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 18 Jun 2024 08:28:44 -0700 Subject: [PATCH 3/4] test: connection vstorage --- .../boot/test/bootstrapTests/orchestration.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index 4bdc1fc7f4b..1e22fe8dea8 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -56,13 +56,14 @@ test.serial('config', async t => { 'chainConnection', 'cosmoshub-4_juno-1', ); + t.like(connection, { + state: 3, + transferChannel: { portId: 'transfer', state: 3 }, + }); - t.like( + t.deepEqual( readLatest(`published.agoricNames.chainConnection.cosmoshub-4_juno-1`), - { - state: 3, - transferChannel: { portId: 'transfer', state: 3 }, - }, + connection, ); await documentStorageSchema(t, storage, { From 2f625b96276311812433c0ebdd7e35ef5a0669d2 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Fri, 21 Jun 2024 07:49:55 -0700 Subject: [PATCH 4/4] test: revise chain info --- .../test/bootstrapTests/orchestration.test.ts | 34 +++++++++++- .../scripts/testing/append-chain-info.js | 50 +++++++++++++++++ .../scripts/testing/tweak-chain-info.js | 53 +++++++++++++++++++ .../src/proposals/revise-chain-info.js | 44 +++++++++++++++ 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/builders/scripts/testing/append-chain-info.js create mode 100644 packages/builders/scripts/testing/tweak-chain-info.js create mode 100644 packages/orchestration/src/proposals/revise-chain-info.js diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index 1e22fe8dea8..5deaca784a1 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -2,12 +2,12 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { Fail } from '@agoric/assert'; import { AmountMath } from '@agoric/ertp'; +import { documentStorageSchema } from '@agoric/internal/src/storage-test-utils.js'; import type { CosmosValidatorAddress } from '@agoric/orchestration'; import type { start as startStakeIca } from '@agoric/orchestration/src/examples/stakeIca.contract.js'; import type { Instance } from '@agoric/zoe/src/zoeService/utils.js'; import { M, matches } from '@endo/patterns'; import type { TestFn } from 'ava'; -import { documentStorageSchema } from '@agoric/internal/src/storage-test-utils.js'; import { makeWalletFactoryContext, type WalletFactoryTestContext, @@ -231,3 +231,35 @@ test.serial('stakeAtom - smart wallet', async t => { 'delegate fails with invalid validator', ); }); + +// XXX rely on .serial to be in sequence, and keep this one last +test.serial('revise chain info', async t => { + const { + buildProposal, + evalProposal, + runUtils: { EV }, + } = t.context; + + const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames'); + + await t.throwsAsync(EV(agoricNames).lookup('chain', 'hot'), { + message: '"nameKey" not found: "hot"', + }); + + // Revise chain info in agoricNames with the fixture in this script + await evalProposal( + buildProposal('@agoric/builders/scripts/testing/append-chain-info.js'), + ); + + const hotchain = await EV(agoricNames).lookup('chain', 'hot'); + t.deepEqual(hotchain, { allegedName: 'Hot New Chain', chainId: 'hot-1' }); + + const connection = await EV(agoricNames).lookup( + 'chainConnection', + 'cosmoshub-4_hot-1', + ); + t.like(connection, { + id: 'connection-99', + client_id: '07-tendermint-3', + }); +}); diff --git a/packages/builders/scripts/testing/append-chain-info.js b/packages/builders/scripts/testing/append-chain-info.js new file mode 100644 index 00000000000..f141fe49074 --- /dev/null +++ b/packages/builders/scripts/testing/append-chain-info.js @@ -0,0 +1,50 @@ +/// +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {Record} */ +const chainInfo = { + hot: { + allegedName: 'Hot New Chain', + chainId: 'hot-1', + connections: { + 'cosmoshub-4': { + id: 'connection-99', + client_id: '07-tendermint-3', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + prefix: { + key_prefix: '', + }, + }, + state: 3 /* IBCConnectionState.STATE_OPEN */, + transferChannel: { + portId: 'transfer', + channelId: 'channel-1', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: 1 /* Order.ORDER_UNORDERED */, + state: 3 /* IBCConnectionState.STATE_OPEN */, + version: 'ics20-1', + }, + }, + }, + }, +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async () => + harden({ + sourceSpec: '@agoric/orchestration/src/proposals/revise-chain-info.js', + getManifestCall: [ + 'getManifestForReviseChains', + { + chainInfo, + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('revise-chain-info', defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/testing/tweak-chain-info.js b/packages/builders/scripts/testing/tweak-chain-info.js new file mode 100644 index 00000000000..922300ea7c0 --- /dev/null +++ b/packages/builders/scripts/testing/tweak-chain-info.js @@ -0,0 +1,53 @@ +/// +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {Record} */ +const chainInfo = { + agoric: { + chainId: 'agoric-4', + }, + hot: { + allegedName: 'Hot New Chain', + chainId: 'hot-1', + connections: { + 'cosmoshub-4': { + id: 'connection-99', + client_id: '07-tendermint-3', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + prefix: { + key_prefix: '', + }, + }, + state: 3 /* IBCConnectionState.STATE_OPEN */, + transferChannel: { + portId: 'transfer', + channelId: 'channel-1', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: 1 /* Order.ORDER_UNORDERED */, + state: 3 /* IBCConnectionState.STATE_OPEN */, + version: 'ics20-1', + }, + }, + }, + }, +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async () => + harden({ + sourceSpec: '@agoric/orchestration/src/proposals/revise-chain-info.js', + getManifestCall: [ + 'getManifestForReviseChains', + { + chainInfo, + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('revise-chain-info', defaultProposalBuilder); +}; diff --git a/packages/orchestration/src/proposals/revise-chain-info.js b/packages/orchestration/src/proposals/revise-chain-info.js new file mode 100644 index 00000000000..b864bfedfc1 --- /dev/null +++ b/packages/orchestration/src/proposals/revise-chain-info.js @@ -0,0 +1,44 @@ +import { makeTracer } from '@agoric/internal'; +import { registerChain } from '../chain-info.js'; + +const trace = makeTracer('ReviseChainInfo', true); + +/** @import {CosmosChainInfo} from '../types.js'; */ + +/** + * This will add news values AND overwrite any existing values. Voters on a + * proposal of core-eval must be careful not to overwrite any values operating + * in production. + * + * @param {BootstrapPowers} powers + * @param {{ options: { chainInfo: Record } }} opt + */ +export const reviseChainInfo = async ( + { consume: { agoricNamesAdmin } }, + { options: { chainInfo } }, +) => { + trace('init-chainInfo'); + + assert(chainInfo, 'chainInfo is required'); + + trace(chainInfo); + + // Now register the names + for await (const [name, info] of Object.entries(chainInfo)) { + await registerChain(agoricNamesAdmin, name, info, trace); + } +}; +harden(reviseChainInfo); + +export const getManifestForReviseChains = (_powers, { chainInfo }) => ({ + manifest: { + [reviseChainInfo.name]: { + consume: { + agoricNamesAdmin: true, + }, + }, + }, + options: { + chainInfo, + }, +});