Skip to content

Commit

Permalink
feat(orchestration): lookup ChainInfo from address value (#10354)
Browse files Browse the repository at this point in the history
closes: #10349

## Description
- add `bech32Prefix?: string` to `CosmosChainInfo` and populate data with `yarn codegen`
- add `getChainInfoByAddress` lookup method to `ChainHub`

### Security Considerations

If multiple cosmos chains use the same `bech32Prefix`, this approach is not sound. In practice, this doesn't seem to be an issue. Developers also control their own ChainHubs and choose whether they want to populate it with data from `agoricNames`. The potential impact is loss of funds from a misrouted transfer.

### Scaling Considerations
- Adds another exo to keep track of `bech32Prefix:chainName` mapping.
- Adds more data to agoricNames (negligible)

### Documentation Considerations
Typedoc will include these changes

### Testing Considerations
Includes unit tests and updates snapshot tests

### Upgrade Considerations
Library code, part of an NPM orch release.
  • Loading branch information
mergify[bot] authored Nov 1, 2024
2 parents a6b3352 + 8ba4699 commit aba3e48
Show file tree
Hide file tree
Showing 20 changed files with 194 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,55 @@ Generated by [AVA](https://avajs.dev).
[
[
'published.agoricNames.chain.agoric',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"agoric-3\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ubld\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"agoric\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"agoric-3\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ubld\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.celestia',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"celestia\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"utia\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"celestia\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"celestia\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"utia\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.cosmoshub',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"cosmoshub-4\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uatom\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"cosmos\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"cosmoshub-4\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uatom\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.dydx',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"dydx-mainnet-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"adydx\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"dydx\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"dydx-mainnet-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"adydx\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.juno',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"juno-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ujuno\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"juno\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"juno-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ujuno\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.neutron',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"neutron-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"untrn\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"neutron\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"neutron-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"untrn\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.noble',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"noble-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"noble\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"noble-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.omniflixhub',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"omniflixhub-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uflix\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"omniflix\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"omniflixhub-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uflix\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.osmosis',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"osmosis-1\\\\\\",\\\\\\"icqEnabled\\\\\\":true,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uosmo\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"osmo\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"osmosis-1\\\\\\",\\\\\\"icqEnabled\\\\\\":true,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uosmo\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.secretnetwork',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"secret-4\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uscrt\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"secret\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"secret-4\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uscrt\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.stargaze',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"stargaze-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ustars\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"stars\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"stargaze-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ustars\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.stride',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"stride-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ustrd\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"stride\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"stride-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"ustrd\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chain.umee',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"chainId\\\\\\":\\\\\\"umee-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uumee\\\\\\"}]}\\",\\"slots\\":[]}"]}',
'{"blockHeight":"0","values":["{\\"body\\":\\"{\\\\\\"bech32Prefix\\\\\\":\\\\\\"umee\\\\\\",\\\\\\"chainId\\\\\\":\\\\\\"umee-1\\\\\\",\\\\\\"icqEnabled\\\\\\":false,\\\\\\"stakingTokens\\\\\\":[{\\\\\\"denom\\\\\\":\\\\\\"uumee\\\\\\"}]}\\",\\"slots\\":[]}"]}',
],
[
'published.agoricNames.chainConnection.agoric-3_cosmoshub-4',
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ Generated by [AVA](https://avajs.dev).
},
},
{
bech32Prefix: Object @match:string {
payload: [],
},
connections: Object @match:recordOf {
payload: [
Object @match:any {
Expand Down
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/orchestration/src/cosmos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export interface CosmosAssetInfo extends Record<string, unknown> {
* Info for a Cosmos-based chain.
*/
export type CosmosChainInfo = Readonly<{
/** can be used to lookup chainInfo (chainId) from an address value */
bech32Prefix?: string;
chainId: string;

connections?: Record<string, IBCConnectionInfo>; // chainId or wellKnownName
Expand Down
25 changes: 25 additions & 0 deletions packages/orchestration/src/exos/chain-hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BrandShape } from '@agoric/ertp/src/typeGuards.js';

import { VowShape } from '@agoric/vow';
import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js';
import { getBech32Prefix } from '../utils/address.js';

/**
* @import {NameHub} from '@agoric/vats';
Expand Down Expand Up @@ -180,6 +181,7 @@ const ChainHubI = M.interface('ChainHub', {
registerAsset: M.call(M.string(), DenomDetailShape).returns(),
getAsset: M.call(M.string()).returns(M.or(DenomDetailShape, M.undefined())),
getDenom: M.call(BrandShape).returns(M.or(M.string(), M.undefined())),
getChainInfoByAddress: M.call(M.string()).returns(CosmosChainInfoShape),
});

/**
Expand Down Expand Up @@ -216,6 +218,11 @@ export const makeChainHub = (zone, agoricNames, vowTools) => {
keyShape: BrandShape,
valueShape: M.string(),
});
/** @type {MapStore<string, string>} */
const bech32PrefixToChainName = zone.mapStore('bech32PrefixToChainName', {
keyShape: M.string(),
valueShape: M.string(),
});

const lookupChainInfo = vowTools.retryable(
zone,
Expand All @@ -230,6 +237,9 @@ export const makeChainHub = (zone, agoricNames, vowTools) => {
// TODO consider makeAtomicProvider for vows
if (!chainInfos.has(chainName)) {
chainInfos.init(chainName, chainInfo);
if (chainInfo.bech32Prefix) {
bech32PrefixToChainName.init(chainInfo.bech32Prefix, chainName);
}
}
return chainInfo;
} catch (e) {
Expand Down Expand Up @@ -316,6 +326,9 @@ export const makeChainHub = (zone, agoricNames, vowTools) => {
*/
registerChain(name, chainInfo) {
chainInfos.init(name, chainInfo);
if (chainInfo.bech32Prefix) {
bech32PrefixToChainName.init(chainInfo.bech32Prefix, name);
}
},
/**
* @template {string} K
Expand Down Expand Up @@ -425,6 +438,18 @@ export const makeChainHub = (zone, agoricNames, vowTools) => {
}
return undefined;
},
/**
* @param {string} address bech32 address
* @returns {CosmosChainInfo}
*/
getChainInfoByAddress(address) {
const prefix = getBech32Prefix(address);
if (!bech32PrefixToChainName.has(prefix)) {
throw makeError(`Chain info not found for bech32Prefix ${q(prefix)}`);
}
const chainName = bech32PrefixToChainName.get(prefix);
return chainInfos.get(chainName);
},
});

return chainHub;
Expand Down
49 changes: 31 additions & 18 deletions packages/orchestration/src/fetched-chain-info.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @file Generated by fetch-chain-info.ts */
export default /** @type {const} } */ ({
agoric: {
bech32Prefix: 'agoric',
chainId: 'agoric-3',
stakingTokens: [
{
Expand Down Expand Up @@ -138,6 +139,7 @@ export default /** @type {const} } */ ({
},
},
celestia: {
bech32Prefix: 'celestia',
chainId: 'celestia',
stakingTokens: [
{
Expand Down Expand Up @@ -239,6 +241,7 @@ export default /** @type {const} } */ ({
},
},
cosmoshub: {
bech32Prefix: 'cosmos',
chainId: 'cosmoshub-4',
stakingTokens: [
{
Expand All @@ -247,24 +250,6 @@ export default /** @type {const} } */ ({
],
icqEnabled: false,
connections: {
'stargaze-1': {
id: 'connection-918',
client_id: '07-tendermint-1188',
counterparty: {
client_id: '07-tendermint-320',
connection_id: 'connection-256',
},
state: 3,
transferChannel: {
channelId: 'channel-730',
portId: 'transfer',
counterPartyChannelId: 'channel-239',
counterPartyPortId: 'transfer',
ordering: 0,
state: 3,
version: 'ics20-1',
},
},
'agoric-3': {
id: 'connection-649',
client_id: '07-tendermint-927',
Expand Down Expand Up @@ -391,6 +376,24 @@ export default /** @type {const} } */ ({
version: 'ics20-1',
},
},
'stargaze-1': {
id: 'connection-918',
client_id: '07-tendermint-1188',
counterparty: {
client_id: '07-tendermint-320',
connection_id: 'connection-256',
},
state: 3,
transferChannel: {
channelId: 'channel-730',
portId: 'transfer',
counterPartyChannelId: 'channel-239',
counterPartyPortId: 'transfer',
ordering: 0,
state: 3,
version: 'ics20-1',
},
},
'stride-1': {
id: 'connection-635',
client_id: '07-tendermint-913',
Expand All @@ -412,6 +415,7 @@ export default /** @type {const} } */ ({
},
},
dydx: {
bech32Prefix: 'dydx',
chainId: 'dydx-mainnet-1',
stakingTokens: [
{
Expand Down Expand Up @@ -513,6 +517,7 @@ export default /** @type {const} } */ ({
},
},
juno: {
bech32Prefix: 'juno',
chainId: 'juno-1',
stakingTokens: [
{
Expand Down Expand Up @@ -650,6 +655,7 @@ export default /** @type {const} } */ ({
},
},
neutron: {
bech32Prefix: 'neutron',
chainId: 'neutron-1',
stakingTokens: [
{
Expand Down Expand Up @@ -823,6 +829,7 @@ export default /** @type {const} } */ ({
},
},
noble: {
bech32Prefix: 'noble',
chainId: 'noble-1',
icqEnabled: false,
connections: {
Expand Down Expand Up @@ -1009,6 +1016,7 @@ export default /** @type {const} } */ ({
},
},
omniflixhub: {
bech32Prefix: 'omniflix',
chainId: 'omniflixhub-1',
stakingTokens: [
{
Expand Down Expand Up @@ -1092,6 +1100,7 @@ export default /** @type {const} } */ ({
},
},
osmosis: {
bech32Prefix: 'osmo',
chainId: 'osmosis-1',
stakingTokens: [
{
Expand Down Expand Up @@ -1319,6 +1328,7 @@ export default /** @type {const} } */ ({
},
},
secretnetwork: {
bech32Prefix: 'secret',
chainId: 'secret-4',
stakingTokens: [
{
Expand Down Expand Up @@ -1510,6 +1520,7 @@ export default /** @type {const} } */ ({
},
},
stargaze: {
bech32Prefix: 'stars',
chainId: 'stargaze-1',
stakingTokens: [
{
Expand Down Expand Up @@ -1665,6 +1676,7 @@ export default /** @type {const} } */ ({
},
},
stride: {
bech32Prefix: 'stride',
chainId: 'stride-1',
stakingTokens: [
{
Expand Down Expand Up @@ -1856,6 +1868,7 @@ export default /** @type {const} } */ ({
},
},
umee: {
bech32Prefix: 'umee',
chainId: 'umee-1',
stakingTokens: [
{
Expand Down
1 change: 1 addition & 0 deletions packages/orchestration/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const CosmosChainInfoShape = M.splitRecord(
chainId: M.string(),
},
{
bech32Prefix: M.string(),
connections: M.record(),
stakingTokens: M.arrayOf({ denom: M.string() }),
// UNTIL https://github.com/Agoric/agoric-sdk/issues/9326
Expand Down
19 changes: 18 additions & 1 deletion packages/orchestration/src/utils/address.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fail } from '@endo/errors';
import { Fail, q } from '@endo/errors';

/**
* @import {IBCConnectionID} from '@agoric/vats';
Expand Down Expand Up @@ -84,3 +84,20 @@ export const findAddressField = remoteAddressString => {
}
};
harden(findAddressField);

/**
* Extracts the human readable part (HRP), aka `bech32Prefix`, from an address.
*
* see
* [bech32.js](https://github.com/bitcoinjs/bech32/blob/5ceb0e3d4625561a459c85643ca6947739b2d83c/src/index.ts#L146)
* for reference implementation
*
* @param {string} address
*/
export const getBech32Prefix = address => {
assert(address, 'address is required');
const split = address.lastIndexOf('1');
if (split === -1) return Fail`No separator character for ${q(address)}`;
if (split === 0) return Fail`Missing prefix for ${q(address)}`;
return address.slice(0, split);
};
4 changes: 3 additions & 1 deletion packages/orchestration/src/utils/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const convertChainInfo = async registry => {
for (const chain of registry.chains) {
console.log('processing info', chain.chain_name);
chainInfo[chain.chain_name] = {
bech32Prefix: chain.bech32_prefix,
chainId: chain.chain_id,
stakingTokens: chain.staking?.staking_tokens,
// UNTIL https://github.com/Agoric/agoric-sdk/issues/9326
Expand Down Expand Up @@ -117,8 +118,9 @@ export const convertChainInfo = async registry => {
const connections = Object.fromEntries(
ibcData
.map(datum => toConnectionEntry(datum, name, chainInfo))
.filter(entry => entry.length > 0)
// sort alphabetically for consistency
.sort(([a], [b]) => (a && b ? a.localeCompare(b) : 0)),
.sort(([a], [b]) => a.localeCompare(b)),
);
chainInfo[name] = { ...chainInfo[name], connections };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ Generated by [AVA](https://avajs.dev).
chainHub: {
ChainHub_kindHandle: 'Alleged: kind',
ChainHub_singleton: 'Alleged: ChainHub',
bech32PrefixToChainName: {
agoric: 'agoric',
},
brandDenom: {},
chainInfos: {
agoric: {
bech32Prefix: 'agoric',
chainId: 'agoric-3',
icqEnabled: false,
stakingTokens: [
Expand Down
Binary file not shown.
Loading

0 comments on commit aba3e48

Please sign in to comment.