From f22e422820d46c55f71fa4a5f42922c801d51819 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 29 Jan 2024 17:06:47 -0600 Subject: [PATCH 1/6] chore: add chain-registry submodule --- .gitmodules | 3 +++ src/lib/chain-registry | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 src/lib/chain-registry diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d21c03b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/lib/chain-registry"] + path = src/lib/chain-registry + url = https://github.com/cosmos/chain-registry diff --git a/src/lib/chain-registry b/src/lib/chain-registry new file mode 160000 index 0000000..394a647 --- /dev/null +++ b/src/lib/chain-registry @@ -0,0 +1 @@ +Subproject commit 394a64789bd22289e92f2043302f81e81afc3c16 From 9268a229deb8424777df7420af14553746ea9e29 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 29 Jan 2024 17:07:01 -0600 Subject: [PATCH 2/6] chore: copy fs.js from endo/bundle-source 2023-09-07 17:29 d7ee2a80e --- src/lib/fs.js | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/lib/fs.js diff --git a/src/lib/fs.js b/src/lib/fs.js new file mode 100644 index 0000000..34ce9f4 --- /dev/null +++ b/src/lib/fs.js @@ -0,0 +1,148 @@ +let mutex = Promise.resolve(undefined); + +/** + * @param {string} fileName + * @param {{ + * fs: { + * promises: Pick + * }, + * path: Pick, + * }} powers + */ +export const makeFileReader = (fileName, { fs, path }) => { + const make = there => makeFileReader(there, { fs, path }); + + // fs.promises.exists isn't implemented in Node.js apparently because it's pure + // sugar. + const exists = fn => + fs.promises.stat(fn).then( + () => true, + e => { + if (e.code === 'ENOENT') { + return false; + } + throw e; + }, + ); + + const readText = async () => { + const promise = mutex; + let release = Function.prototype; + mutex = new Promise(resolve => { + release = resolve; + }); + await promise; + try { + return await fs.promises.readFile(fileName, 'utf-8'); + } finally { + release(undefined); + } + }; + + const maybeReadText = () => + readText().catch(error => { + if ( + error.message.startsWith('ENOENT: ') || + error.message.startsWith('EISDIR: ') + ) { + return undefined; + } + throw error; + }); + + return harden({ + toString: () => fileName, + readText, + maybeReadText, + neighbor: ref => make(path.resolve(fileName, ref)), + stat: () => fs.promises.stat(fileName), + absolute: () => path.normalize(fileName), + relative: there => path.relative(fileName, there), + exists: () => exists(fileName), + }); +}; + +/** + * @param {string} fileName + * @param {{ + * fs: Pick & + * { promises: Pick< + * import('fs/promises'), + * 'readFile' | 'stat' | 'writeFile' | 'mkdir' | 'rm' + * >, + * }, + * path: Pick, + * }} io + * @param {(there: string) => ReturnType} make + */ +export const makeFileWriter = ( + fileName, + { fs, path }, + make = there => makeFileWriter(there, { fs, path }, make), +) => { + const writeText = async (txt, opts) => { + const promise = mutex; + let release = Function.prototype; + mutex = new Promise(resolve => { + release = resolve; + }); + await promise; + try { + return await fs.promises.writeFile(fileName, txt, opts); + } finally { + release(undefined); + } + }; + + return harden({ + toString: () => fileName, + writeText, + readOnly: () => makeFileReader(fileName, { fs, path }), + neighbor: ref => make(path.resolve(fileName, ref)), + mkdir: opts => fs.promises.mkdir(fileName, opts), + rm: opts => fs.promises.rm(fileName, opts), + rename: newName => + fs.promises.rename( + fileName, + path.resolve(path.dirname(fileName), newName), + ), + }); +}; + +/** + * @param {string} fileName + * @param {{ + * fs: Pick & + * { promises: Pick< + * import('fs/promises'), + * 'readFile' | 'stat' | 'writeFile' | 'mkdir' | 'rm' + * >, + * }, + * path: Pick, + * }} io + * @param {number} pid + * @param {number} nonce + * @param {(there: string) => ReturnType} make + */ +export const makeAtomicFileWriter = ( + fileName, + { fs, path }, + pid = undefined, + nonce = undefined, + make = there => makeAtomicFileWriter(there, { fs, path }, pid, nonce, make), +) => { + const writer = makeFileWriter(fileName, { fs, path }, make); + return harden({ + ...writer, + atomicWriteText: async (txt, opts) => { + const scratchName = `${fileName}.${nonce || 'no-nonce'}.${ + pid || 'no-pid' + }.scratch`; + const scratchWriter = writer.neighbor(scratchName); + await scratchWriter.writeText(txt, opts); + const stats = await scratchWriter.readOnly().stat(); + await scratchWriter.rename(fileName); + return stats; + }, + }); +}; From 1b237eb0034049309a14c990bf32f6b47660d6d8 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 29 Jan 2024 17:07:32 -0600 Subject: [PATCH 3/6] chore: rename fs.js -> fs.ts --- src/lib/{fs.js => fs.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/lib/{fs.js => fs.ts} (100%) diff --git a/src/lib/fs.js b/src/lib/fs.ts similarity index 100% rename from src/lib/fs.js rename to src/lib/fs.ts From a57456292fc6361097afba39f3671e3c8d8271be Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 29 Jan 2024 17:17:17 -0600 Subject: [PATCH 4/6] chore(lib/fs): convert JSDoc to typescript --- src/lib/fs.ts | 125 ++++++++++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/src/lib/fs.ts b/src/lib/fs.ts index 34ce9f4..1fd3d9a 100644 --- a/src/lib/fs.ts +++ b/src/lib/fs.ts @@ -1,20 +1,28 @@ +import * as fsT from 'fs'; +import * as fspT from 'fs/promises'; +import * as pathT from 'path'; + +const { freeze } = Object; + let mutex = Promise.resolve(undefined); -/** - * @param {string} fileName - * @param {{ - * fs: { - * promises: Pick - * }, - * path: Pick, - * }} powers - */ -export const makeFileReader = (fileName, { fs, path }) => { - const make = there => makeFileReader(there, { fs, path }); +export const makeFileReader = ( + fileName: string, + { + fs, + path, + }: { + fs: { + promises: Pick; + }; + path: Pick; + } +) => { + const make = (there: string) => makeFileReader(there, { fs, path }); // fs.promises.exists isn't implemented in Node.js apparently because it's pure // sugar. - const exists = fn => + const exists = (fn: string) => fs.promises.stat(fn).then( () => true, e => { @@ -22,7 +30,7 @@ export const makeFileReader = (fileName, { fs, path }) => { return false; } throw e; - }, + } ); const readText = async () => { @@ -50,37 +58,35 @@ export const makeFileReader = (fileName, { fs, path }) => { throw error; }); - return harden({ + return freeze({ toString: () => fileName, readText, maybeReadText, - neighbor: ref => make(path.resolve(fileName, ref)), + neighbor: (ref: string) => make(path.resolve(fileName, ref)), stat: () => fs.promises.stat(fileName), absolute: () => path.normalize(fileName), - relative: there => path.relative(fileName, there), + relative: (there: string) => path.relative(fileName, there), exists: () => exists(fileName), }); }; -/** - * @param {string} fileName - * @param {{ - * fs: Pick & - * { promises: Pick< - * import('fs/promises'), - * 'readFile' | 'stat' | 'writeFile' | 'mkdir' | 'rm' - * >, - * }, - * path: Pick, - * }} io - * @param {(there: string) => ReturnType} make - */ export const makeFileWriter = ( - fileName, - { fs, path }, - make = there => makeFileWriter(there, { fs, path }, make), + fileName: string, + { + fs, + path, + }: { + fs: Pick & { + promises: Pick< + typeof fspT, + 'readFile' | 'stat' | 'writeFile' | 'mkdir' | 'rm' | 'rename' + >; + }; + path: Pick; + }, + make = (there: string) => makeFileWriter(there, { fs, path }, make) ) => { - const writeText = async (txt, opts) => { + const writeText = async (txt: string, opts: object) => { const promise = mutex; let release = Function.prototype; mutex = new Promise(resolve => { @@ -94,47 +100,44 @@ export const makeFileWriter = ( } }; - return harden({ + return freeze({ toString: () => fileName, writeText, readOnly: () => makeFileReader(fileName, { fs, path }), - neighbor: ref => make(path.resolve(fileName, ref)), - mkdir: opts => fs.promises.mkdir(fileName, opts), - rm: opts => fs.promises.rm(fileName, opts), - rename: newName => + neighbor: (ref: string) => make(path.resolve(fileName, ref)), + mkdir: (opts: object) => fs.promises.mkdir(fileName, opts), + rm: (opts: object) => fs.promises.rm(fileName, opts), + rename: (newName: string) => fs.promises.rename( fileName, - path.resolve(path.dirname(fileName), newName), + path.resolve(path.dirname(fileName), newName) ), }); }; -/** - * @param {string} fileName - * @param {{ - * fs: Pick & - * { promises: Pick< - * import('fs/promises'), - * 'readFile' | 'stat' | 'writeFile' | 'mkdir' | 'rm' - * >, - * }, - * path: Pick, - * }} io - * @param {number} pid - * @param {number} nonce - * @param {(there: string) => ReturnType} make - */ export const makeAtomicFileWriter = ( - fileName, - { fs, path }, - pid = undefined, - nonce = undefined, - make = there => makeAtomicFileWriter(there, { fs, path }, pid, nonce, make), + fileName: string, + { + fs, + path, + }: { + fs: Pick & { + promises: Pick< + typeof fspT, + 'readFile' | 'stat' | 'writeFile' | 'mkdir' | 'rm' | 'rename' + >; + }; + path: Pick; + }, + pid: number | undefined = undefined, + nonce: number | undefined = undefined, + make = (there: string) => + makeAtomicFileWriter(there, { fs, path }, pid, nonce, make) ) => { const writer = makeFileWriter(fileName, { fs, path }, make); - return harden({ + return freeze({ ...writer, - atomicWriteText: async (txt, opts) => { + atomicWriteText: async (txt: string, opts: object) => { const scratchName = `${fileName}.${nonce || 'no-nonce'}.${ pid || 'no-pid' }.scratch`; From 22752db9260f1d0c6577f3da5cf96703818b9fb5 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 29 Jan 2024 19:12:53 -0600 Subject: [PATCH 5/6] feat(lib/fs): rd.children() --- src/lib/fs.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/fs.ts b/src/lib/fs.ts index 1fd3d9a..4e7ce68 100644 --- a/src/lib/fs.ts +++ b/src/lib/fs.ts @@ -13,7 +13,7 @@ export const makeFileReader = ( path, }: { fs: { - promises: Pick; + promises: Pick; }; path: Pick; } @@ -58,15 +58,18 @@ export const makeFileReader = ( throw error; }); + const neighbor = (ref: string) => make(path.resolve(fileName, ref)); return freeze({ toString: () => fileName, readText, maybeReadText, - neighbor: (ref: string) => make(path.resolve(fileName, ref)), + neighbor, stat: () => fs.promises.stat(fileName), absolute: () => path.normalize(fileName), relative: (there: string) => path.relative(fileName, there), exists: () => exists(fileName), + children: () => + fs.promises.readdir(fileName).then(refs => refs.map(neighbor)), }); }; From 7f2e9979bd63cd75dc71783feea0083c1d264d14 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Mon, 29 Jan 2024 19:13:15 -0600 Subject: [PATCH 6/6] feat: show vbank info for Agoric IBC peer assets (WIP) using chain registry --- src/lib/bulk-asset.ts | 163 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100755 src/lib/bulk-asset.ts diff --git a/src/lib/bulk-asset.ts b/src/lib/bulk-asset.ts new file mode 100755 index 0000000..7a06b4d --- /dev/null +++ b/src/lib/bulk-asset.ts @@ -0,0 +1,163 @@ +#!/bin/env tsx +import { createRequire } from 'module'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import { makeFileReader } from './fs.ts'; + +const nodeRequire = createRequire(import.meta.url); +const asset = { + registry: nodeRequire.resolve('./chain-registry/README.md'), +}; + +type Rd = ReturnType; + +const getJSON = async (rd: Rd) => { + const txt = await rd.readText(); + return JSON.parse(txt); +}; + +const getIBCChannels = async (rd: Rd) => { + const ibc = rd.neighbor('./_IBC/'); + const channels = await ibc.children(); + return { ibc, channels }; +}; + +type ChainInfo = { + chain_name: string; + client_id: string; + connection_id: string; +}; + +type ChannelInfo = { + channel_id: string; + port_id: string; + client_id?: string; + connection_id?: string; +}; + +type ConnectionInfo = { + ['$schema']: string; + chain_1: ChainInfo; + chain_2: ChainInfo; + channels: Array<{ + chain_1: ChannelInfo; + chain_2: ChannelInfo; + ordering: 'ordered' | 'unordered'; + version: string; + tags?: { + status?: 'live' | 'upcoming' | 'killed'; + preferred?: boolean; + }; + }>; +}; + +const getIBCNeighbors = async (rd: Rd, name: string) => { + const { ibc, channels } = await getIBCChannels(rd); + const found = channels.filter( + ch => ibc.relative(ch.toString()).includes(name) // TODO word boundaries only + ); + // console.log(found.map(r => ibc.relative(r.toString()))); + + return Promise.all(found.map(ch => getJSON(ch) as Promise)); +}; + +type InterchainAssetParams = { + denom: string; + decimalPlaces: number; + issuerName: string; +}; + +const x = () => { + const p: InterchainAssetParams = { + decimalPlaces: 6, + denom: '@@@', + issuerName: 'whee', + }; + return p; +}; + +type DenomUnit = { denom: string; exponent: number }; + +type AssetT = { + denom_units: DenomUnit[]; + base: string; + display: string; + name: string; + symbol: string; + description?: string; +}; + +type AssetList = { + chain_name: string; + assets: AssetT[]; +}; + +export const pprint = (x: unknown) => JSON.stringify(x, null, 2); + +const assetInfo = (a: AssetT) => { + const { symbol: allegedName, denom_units } = a; + const decimalPlaces = denom_units.find(u => u.denom === a.display)?.exponent; + const assetKind = typeof decimalPlaces === 'number' ? 'nat' : undefined; + // if (decimalPlaces === undefined) console.log('@@@', pprint(a)); + return { + allegedName, + assetKind, + decimalPlaces, + }; +}; + +const chainInfo = async (rd: Rd, name: string) => { + const chain = await getJSON(rd.neighbor(`./${name}/chain.json`)); + const assetlist: AssetList = await getJSON( + rd.neighbor(`./${name}/assetlist.json`) + ); + return { chain, assetlist }; +}; + +const sha256 = (txt: string) => + crypto.createHash('sha256').update(txt).digest('hex'); + +const main = async (src = 'agoric') => { + // console.log('asset paths', asset); + const rd = makeFileReader(asset.registry, { fs, path }).neighbor('../'); + // console.log('chain registry rd', `${rd}`); + // console.log('chains', `${await rd.children()}`); + + const agChannels = await getIBCNeighbors(rd, src); + + for await (const peer of agChannels) { + if (peer.chain_1.chain_name !== src) throw Error(pprint(peer)); + const { chain_2, channels } = peer; + // if (!['osmosis', 'cosmoshub'].includes(chain_2.chain_name)) continue; // XXX + // console.log(pprint(chan)); + const other = await chainInfo(rd, chain_2.chain_name); + for (const chan of channels) { + const { channel_id, port_id } = chan.chain_1; + if (port_id !== 'transfer') continue; + + for (const asset of other.assetlist.assets) { + const info = assetInfo(asset); + const path = `${port_id}/${channel_id}/${asset.base}`; + const hash = sha256(path).toUpperCase(); + const denom = `ibc/${hash}`; + if (info.decimalPlaces === undefined) continue; + const ia: InterchainAssetParams = { + decimalPlaces: info.decimalPlaces, + denom, + issuerName: asset.symbol, + }; + console.log( + `${src}-${channel_id}->${chain_2.chain_name} $${asset.symbol}`, + path, + ia + ); + } + } + } +}; + +main().catch(err => { + console.error(err); + process.exit(1); +});