Skip to content

Commit

Permalink
feat(chainHub): getChainInfoByAddress helper
Browse files Browse the repository at this point in the history
- takes a ChainAddress.value, parses the bech32Prefix, and looks up the corresponding ChainInfo
  • Loading branch information
0xpatrickdev committed Oct 31, 2024
1 parent 6b3d31a commit d6c487c
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 1 deletion.
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
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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Generated by [AVA](https://avajs.dev).
chainHub: {
ChainHub_kindHandle: 'Alleged: kind',
ChainHub_singleton: 'Alleged: ChainHub',
bech32PrefixToChainName: {
agoric: 'agoric',
},
brandDenom: {},
chainInfos: {
agoric: {
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Generated by [AVA](https://avajs.dev).
chainHub: {
ChainHub_kindHandle: 'Alleged: kind',
ChainHub_singleton: 'Alleged: ChainHub',
bech32PrefixToChainName: {
agoric: 'agoric',
cosmos: 'cosmoshub',
},
brandDenom: {
'Alleged: BLD brand': 'ubld',
},
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ Generated by [AVA](https://avajs.dev).
chainHub: {
ChainHub_kindHandle: 'Alleged: kind',
ChainHub_singleton: 'Alleged: ChainHub',
bech32PrefixToChainName: {
agoric: 'agoric',
osmo: 'osmosis',
stride: 'stride',
},
brandDenom: {},
chainInfos: {
agoric: {
Expand Down
Binary file not shown.
32 changes: 32 additions & 0 deletions packages/orchestration/test/exos/chain-hub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,35 @@ test.serial('toward asset info in agoricNames (#9572)', async t => {
});
}
});

test.serial('getChainInfoByAddress', async t => {
const { chainHub, nameAdmin, vt } = setup();
// use fetched chain info
await registerKnownChains(nameAdmin);

// call getChainInfo so ChainHub performs agoricNames lookup that populates its local cache
await vt.asPromise(chainHub.getChainInfo('osmosis'));

const MOCK_ICA_ADDRESS =
'osmo1ht7u569vpuryp6utadsydcne9ckeh2v8dkd38v5hptjl3u2ewppqc6kzgd';
t.like(chainHub.getChainInfoByAddress(MOCK_ICA_ADDRESS), {
chainId: 'osmosis-1',
bech32Prefix: 'osmo',
});

t.throws(
() =>
chainHub.getChainInfoByAddress(MOCK_ICA_ADDRESS.replace('osmo1', 'foo1')),
{
message: 'Chain info not found for bech32Prefix "foo"',
},
);

t.throws(() => chainHub.getChainInfoByAddress('notbech32'), {
message: 'No separator character for "notbech32"',
});

t.throws(() => chainHub.getChainInfoByAddress('1notbech32'), {
message: 'Missing prefix for "1notbech32"',
});
});
31 changes: 31 additions & 0 deletions packages/orchestration/test/utils/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
makeICAChannelAddress,
makeICQChannelAddress,
findAddressField,
getBech32Prefix,
} from '../../src/utils/address.js';

test('makeICAChannelAddress', t => {
Expand Down Expand Up @@ -107,3 +108,33 @@ test('makeICQChannelAddress', t => {
'makeICQChannelAddress not hardened against malformed version. use `validateRemoteIbcAddress` to detect this, or expect IBC ProtocolImpl to throw',
);
});

const bech32 = test.macro({
title: (_, input: string, expected: string | null) =>
expected !== null
? `can extract ${expected} prefix from ${input}`
: `throws error for invalid address ${input}`,
exec: (t, input: string, expected: string | null, error?: string) => {
if (expected !== null) {
t.is(getBech32Prefix(input), expected);
} else {
t.throws(() => getBech32Prefix(input), { message: error });
}
},
});

test(bech32, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', 'bc');
test(bech32, 'cosmos1n4f2eqt2gm5mh6gevf8aw2wrf75q25yru09yvn', 'cosmos');
test(bech32, '111qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', '11');
test(
bech32,
'qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
null,
'No separator character for "qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"',
);
test(
bech32,
'1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
null,
'Missing prefix for "1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"',
);

0 comments on commit d6c487c

Please sign in to comment.