|
| 1 | +import { Fail, makeError } from '@endo/errors'; |
| 2 | +import { E } from '@endo/far'; |
| 3 | +import { M } from '@endo/patterns'; |
| 4 | + |
| 5 | +import { VowShape } from '@agoric/vow'; |
| 6 | +// eslint-disable-next-line no-restricted-syntax |
| 7 | +import { heapVowTools } from '@agoric/vow/vat.js'; |
| 8 | +import { makeHeapZone } from '@agoric/zone'; |
| 9 | +import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; |
| 10 | + |
| 11 | +// FIXME test thoroughly whether heap suffices for ChainHub |
| 12 | +// eslint-disable-next-line no-restricted-syntax |
| 13 | +const { allVows, watch } = heapVowTools; |
| 14 | + |
| 15 | +/** |
| 16 | + * @import {NameHub} from '@agoric/vats'; |
| 17 | + * @import {Vow} from '@agoric/vow'; |
| 18 | + * @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js'; |
| 19 | + * @import {ChainInfo, KnownChains} from '../chain-info.js'; |
| 20 | + * @import {Remote} from '@agoric/internal'; |
| 21 | + * @import {Zone} from '@agoric/zone'; |
| 22 | + */ |
| 23 | + |
| 24 | +/** |
| 25 | + * @template {string} K |
| 26 | + * @typedef {K extends keyof KnownChains |
| 27 | + * ? Omit<KnownChains[K], 'connections'> |
| 28 | + * : ChainInfo} ActualChainInfo |
| 29 | + */ |
| 30 | + |
| 31 | +/** agoricNames key for ChainInfo hub */ |
| 32 | +export const CHAIN_KEY = 'chain'; |
| 33 | +/** namehub for connection info */ |
| 34 | +export const CONNECTIONS_KEY = 'chainConnection'; |
| 35 | + |
| 36 | +/** |
| 37 | + * Character used in a connection tuple key to separate the two chain ids. Valid |
| 38 | + * because a chainId can contain only alphanumerics and dash. |
| 39 | + * |
| 40 | + * Vstorage keys can be only alphanumerics, dash or underscore. That leaves |
| 41 | + * underscore as the only valid separator. |
| 42 | + * |
| 43 | + * @see {@link https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md} |
| 44 | + */ |
| 45 | +const CHAIN_ID_SEPARATOR = '_'; |
| 46 | + |
| 47 | +/** |
| 48 | + * The entries of the top-level namehubs in agoricNames are reflected to |
| 49 | + * vstorage. But only the top level. So we combine the 2 chain ids into 1 key. |
| 50 | + * Connections are directionless, so we sort the ids. |
| 51 | + * |
| 52 | + * @param {string} chainId1 |
| 53 | + * @param {string} chainId2 |
| 54 | + */ |
| 55 | +export const connectionKey = (chainId1, chainId2) => { |
| 56 | + if ( |
| 57 | + chainId1.includes(CHAIN_ID_SEPARATOR) || |
| 58 | + chainId2.includes(CHAIN_ID_SEPARATOR) |
| 59 | + ) { |
| 60 | + Fail`invalid chain id ${chainId1} or ${chainId2}`; |
| 61 | + } |
| 62 | + return [chainId1, chainId2].sort().join(CHAIN_ID_SEPARATOR); |
| 63 | +}; |
| 64 | + |
| 65 | +const ChainIdArgShape = M.or( |
| 66 | + M.string(), |
| 67 | + M.splitRecord( |
| 68 | + { |
| 69 | + chainId: M.string(), |
| 70 | + }, |
| 71 | + undefined, |
| 72 | + M.any(), |
| 73 | + ), |
| 74 | +); |
| 75 | + |
| 76 | +const ChainHubI = M.interface('ChainHub', { |
| 77 | + registerChain: M.call(M.string(), CosmosChainInfoShape).returns(), |
| 78 | + getChainInfo: M.call(M.string()).returns(VowShape), |
| 79 | + registerConnection: M.call( |
| 80 | + M.string(), |
| 81 | + M.string(), |
| 82 | + IBCConnectionInfoShape, |
| 83 | + ).returns(), |
| 84 | + getConnectionInfo: M.call(ChainIdArgShape, ChainIdArgShape).returns(VowShape), |
| 85 | + getChainsAndConnection: M.call(M.string(), M.string()).returns(VowShape), |
| 86 | +}); |
| 87 | + |
| 88 | +/** |
| 89 | + * Make a new ChainHub in the zone (or in the heap if no zone is provided). |
| 90 | + * |
| 91 | + * The resulting object is an Exo singleton. It has no precious state. It's only |
| 92 | + * state is a cache of queries to agoricNames and whatever info was provided in |
| 93 | + * registration calls. When you need a newer version you can simply make a hub |
| 94 | + * hub and repeat the registrations. |
| 95 | + * |
| 96 | + * @param {Remote<NameHub>} agoricNames |
| 97 | + * @param {Zone} [zone] |
| 98 | + */ |
| 99 | +export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { |
| 100 | + /** @type {MapStore<string, CosmosChainInfo>} */ |
| 101 | + const chainInfos = zone.mapStore('chainInfos', { |
| 102 | + keyShape: M.string(), |
| 103 | + valueShape: CosmosChainInfoShape, |
| 104 | + }); |
| 105 | + /** @type {MapStore<string, IBCConnectionInfo>} */ |
| 106 | + const connectionInfos = zone.mapStore('connectionInfos', { |
| 107 | + keyShape: M.string(), |
| 108 | + valueShape: IBCConnectionInfoShape, |
| 109 | + }); |
| 110 | + |
| 111 | + const chainHub = zone.exo('ChainHub', ChainHubI, { |
| 112 | + /** |
| 113 | + * Register a new chain. The name will override a name in well known chain |
| 114 | + * names. |
| 115 | + * |
| 116 | + * If a durable zone was not provided, registration will not survive a |
| 117 | + * reincarnation of the vat. Then if the chain is not yet in the well known |
| 118 | + * names at that point, it will have to be registered again. In an unchanged |
| 119 | + * contract `start` the call will happen again naturally. |
| 120 | + * |
| 121 | + * @param {string} name |
| 122 | + * @param {CosmosChainInfo} chainInfo |
| 123 | + */ |
| 124 | + registerChain(name, chainInfo) { |
| 125 | + chainInfos.init(name, chainInfo); |
| 126 | + }, |
| 127 | + /** |
| 128 | + * @template {string} K |
| 129 | + * @param {K} chainName |
| 130 | + * @returns {Vow<ActualChainInfo<K>>} |
| 131 | + */ |
| 132 | + getChainInfo(chainName) { |
| 133 | + // Either from registerChain or memoized remote lookup() |
| 134 | + if (chainInfos.has(chainName)) { |
| 135 | + return /** @type {Vow<ActualChainInfo<K>>} */ ( |
| 136 | + watch(chainInfos.get(chainName)) |
| 137 | + ); |
| 138 | + } |
| 139 | + |
| 140 | + return watch(E(agoricNames).lookup(CHAIN_KEY, chainName), { |
| 141 | + onFulfilled: chainInfo => { |
| 142 | + chainInfos.init(chainName, chainInfo); |
| 143 | + return chainInfo; |
| 144 | + }, |
| 145 | + onRejected: _cause => { |
| 146 | + throw makeError(`chain not found:${chainName}`); |
| 147 | + }, |
| 148 | + }); |
| 149 | + }, |
| 150 | + /** |
| 151 | + * @param {string} chainId1 |
| 152 | + * @param {string} chainId2 |
| 153 | + * @param {IBCConnectionInfo} connectionInfo |
| 154 | + */ |
| 155 | + registerConnection(chainId1, chainId2, connectionInfo) { |
| 156 | + const key = connectionKey(chainId1, chainId2); |
| 157 | + connectionInfos.init(key, connectionInfo); |
| 158 | + }, |
| 159 | + |
| 160 | + /** |
| 161 | + * @param {string | { chainId: string }} chain1 |
| 162 | + * @param {string | { chainId: string }} chain2 |
| 163 | + * @returns {Vow<IBCConnectionInfo>} |
| 164 | + */ |
| 165 | + cgetConnectionInfo(chain1, chain2) { |
| 166 | + const chainId1 = typeof chain1 === 'string' ? chain1 : chain1.chainId; |
| 167 | + const chainId2 = typeof chain2 === 'string' ? chain2 : chain2.chainId; |
| 168 | + const key = connectionKey(chainId1, chainId2); |
| 169 | + if (connectionInfos.has(key)) { |
| 170 | + return watch(connectionInfos.get(key)); |
| 171 | + } |
| 172 | + |
| 173 | + return watch(E(agoricNames).lookup(CONNECTIONS_KEY, key), { |
| 174 | + onFulfilled: connectionInfo => { |
| 175 | + connectionInfos.init(key, connectionInfo); |
| 176 | + return connectionInfo; |
| 177 | + }, |
| 178 | + onRejected: _cause => { |
| 179 | + throw makeError(`connection not found: ${chainId1}<->${chainId2}`); |
| 180 | + }, |
| 181 | + }); |
| 182 | + }, |
| 183 | + |
| 184 | + /** |
| 185 | + * @template {string} C1 |
| 186 | + * @template {string} C2 |
| 187 | + * @param {C1} chainName1 |
| 188 | + * @param {C2} chainName2 |
| 189 | + * @returns {Vow< |
| 190 | + * [ActualChainInfo<C1>, ActualChainInfo<C2>, IBCConnectionInfo] |
| 191 | + * >} |
| 192 | + */ |
| 193 | + getChainsAndConnection(chainName1, chainName2) { |
| 194 | + return watch( |
| 195 | + allVows([ |
| 196 | + chainHub.getChainInfo(chainName1), |
| 197 | + chainHub.getChainInfo(chainName2), |
| 198 | + ]), |
| 199 | + { |
| 200 | + onFulfilled: ([chain1, chain2]) => { |
| 201 | + return watch(chainHub.getConnectionInfo(chain2, chain1), { |
| 202 | + onFulfilled: connectionInfo => { |
| 203 | + return [chain1, chain2, connectionInfo]; |
| 204 | + }, |
| 205 | + }); |
| 206 | + }, |
| 207 | + }, |
| 208 | + ); |
| 209 | + }, |
| 210 | + }); |
| 211 | + |
| 212 | + return chainHub; |
| 213 | +}; |
| 214 | +/** @typedef {ReturnType<typeof makeChainHub>} ChainHub */ |
0 commit comments