|
| 1 | +const fs = require('fs').promises; |
| 2 | +const repl = require('repl'); |
| 3 | +const path = require('path'); |
| 4 | +const { Readable } = require('stream'); |
| 5 | +const { readFileSync, createReadStream, constants } = require('fs'); |
| 6 | +const getopts = require('getopts'); |
| 7 | +const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api'); |
| 8 | +const Types = require('@polkadot/types'); |
| 9 | +const fetch = require('node-fetch'); |
| 10 | +const chalk = require('chalk'); |
| 11 | +const { buildCtx } = require('./util/scenario/ctx'); |
| 12 | + |
| 13 | +async function fileExists(path) { |
| 14 | + try { |
| 15 | + await fs.access(path, constants.R_OK); |
| 16 | + return true; |
| 17 | + } catch (e) { |
| 18 | + return false; |
| 19 | + } |
| 20 | +} |
| 21 | + |
| 22 | +function matchesLine(completion, line) { |
| 23 | + if (completion.initial && line.startsWith(completion.initial)) { |
| 24 | + return true; |
| 25 | + } |
| 26 | + |
| 27 | + return false; |
| 28 | +} |
| 29 | + |
| 30 | +function targetMatches(completion, line) { |
| 31 | + let words = line.split(/\s+/); |
| 32 | + let position = words.length - 1; // e.g. "deploy" = 0 "deploy " = 1 "deploy abc" = 1 |
| 33 | + let lastWord = words.length === 0 ? "" : words[words.length - 1]; |
| 34 | + let targets = completion.targets.filter(({pos}) => pos === position); |
| 35 | + |
| 36 | + let matching = targets.reduce((acc, {choices}) => { |
| 37 | + return [ |
| 38 | + ...acc, |
| 39 | + ...choices.filter((choice) => choice.startsWith(lastWord)) |
| 40 | + ]; |
| 41 | + }, []); |
| 42 | + |
| 43 | + if (lastWord.length > 0) { |
| 44 | + return [matching, lastWord]; |
| 45 | + } else { |
| 46 | + return [matching, line]; |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +function getCompleter(defaultCompleter, completions) { |
| 51 | + return function(line, callback) { |
| 52 | + const lineMatches = completions.filter((completion) => matchesLine(completion, line)); |
| 53 | + let [choices, text] = lineMatches.reduce(([accMatch, accText], completion) => { |
| 54 | + let [matches, text] = targetMatches(completion, line); |
| 55 | + |
| 56 | + if (matches && text.length < accText.length) { |
| 57 | + return [matches, text]; |
| 58 | + } else if (matches && text.length === accText.length) { |
| 59 | + return [ [ ...accMatch, ...matches ], accText]; |
| 60 | + } else { |
| 61 | + return [accMatch, accText]; |
| 62 | + } |
| 63 | + }, [[], line]); |
| 64 | + |
| 65 | + if (choices.length > 0) { |
| 66 | + callback(null, [choices, text]); |
| 67 | + } else { |
| 68 | + defaultCompleter(line, callback); |
| 69 | + } |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +function getCompletions(defaultCompleter, contracts) { |
| 74 | + let contractNames = Object.keys(contracts) |
| 75 | + let contractAddresses = Object.values(contracts).filter((x) => !!x); |
| 76 | + |
| 77 | + const completions = [ |
| 78 | + { |
| 79 | + initial: '.deploy', |
| 80 | + targets: [ |
| 81 | + { |
| 82 | + pos: 1, |
| 83 | + choices: contractNames |
| 84 | + } |
| 85 | + ] |
| 86 | + }, |
| 87 | + { |
| 88 | + initial: '.match', |
| 89 | + targets: [ |
| 90 | + { |
| 91 | + pos: 1, |
| 92 | + choices: contractAddresses |
| 93 | + }, |
| 94 | + { |
| 95 | + pos: 2, |
| 96 | + choices: contractNames |
| 97 | + } |
| 98 | + ] |
| 99 | + } |
| 100 | + ]; |
| 101 | + |
| 102 | + return getCompleter(defaultCompleter, completions); |
| 103 | +} |
| 104 | + |
| 105 | +function lowerCase(str) { |
| 106 | + if (str === "") { |
| 107 | + return ""; |
| 108 | + } else { |
| 109 | + return str[0].toLowerCase() + str.slice(1,); |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +async function wrapError(fn, r) { |
| 114 | + try { |
| 115 | + return await fn; |
| 116 | + } catch (err) { |
| 117 | + console.error(`Error: ${err}`); |
| 118 | + } finally { |
| 119 | + r.displayPrompt(); |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// async function getContracts(saddle) { |
| 124 | +// let contracts = await saddle.listContracts(); |
| 125 | +// let contractInsts = await Object.entries(contracts).reduce(async (acc, [contract, address]) => { |
| 126 | +// if (address) { |
| 127 | +// return { |
| 128 | +// ... await acc, |
| 129 | +// [contract]: await saddle.getContractAt(contract, address) |
| 130 | +// }; |
| 131 | +// } else { |
| 132 | +// return await acc; |
| 133 | +// } |
| 134 | +// }, {}); |
| 135 | + |
| 136 | +// return { |
| 137 | +// contracts, |
| 138 | +// contractInsts |
| 139 | +// }; |
| 140 | +// } |
| 141 | + |
| 142 | +function defineAction(r, fn) { |
| 143 | + return async (name) => { |
| 144 | + r.clearBufferedCommand(); |
| 145 | + await wrapError(fn(name), r); |
| 146 | + }; |
| 147 | +} |
| 148 | + |
| 149 | +// function defineCommands(r, { api, keyring, types }, saddle, network, contracts) { |
| 150 | +// r.defineCommand('validators', { |
| 151 | +// help: 'Show current validators', |
| 152 | +// action: defineAction(r, async () => { |
| 153 | +// let validators = await api.query.cash.validators.entries(); |
| 154 | +// validators.forEach(([substrateId, validatorKeys]) => { |
| 155 | +// let key = toSS58(keyring, substrateId.toHuman()[0]); |
| 156 | +// let value = Object.entries(validatorKeys.unwrap().toJSON()).map(([k, v]) => |
| 157 | +// `\t\t${k}=${v}`).join("\n"); |
| 158 | +// console.log(`\t${key}:\n${value}\n`); |
| 159 | +// }); |
| 160 | +// }) |
| 161 | +// }); |
| 162 | + |
| 163 | +// r.defineCommand('decode_call', { |
| 164 | +// help: 'Decode a call', |
| 165 | +// action: defineAction(r, async (name) => { |
| 166 | +// let call = new types.GenericCall(api.registry, name); |
| 167 | +// let { method, section, args } = call.toHuman(); |
| 168 | +// console.log(`Extrinsic Call:\n\t${section}.${method}(${args.map((a) => JSON.stringify(a)).join(",")})`); |
| 169 | +// }) |
| 170 | +// }); |
| 171 | + |
| 172 | +// r.defineCommand('block', { |
| 173 | +// help: 'Show current gateway block', |
| 174 | +// action: defineAction(r, async () => { |
| 175 | +// const blockHash = await api.rpc.chain.getBlockHash(); |
| 176 | +// const signedBlock = await api.rpc.chain.getBlock(blockHash); |
| 177 | +// let header = signedBlock.block.header; |
| 178 | +// console.log(`#${header.number}`); |
| 179 | +// }) |
| 180 | +// }); |
| 181 | + |
| 182 | +// r.defineCommand('exec', { |
| 183 | +// help: 'Sign and send a trx request from saddle eth addr', |
| 184 | +// action: defineAction(r, async (request) => { |
| 185 | +// let user = saddle.account;// get saddle user and make chain account |
| 186 | +// const nonce = await api.query.cash.nonces({eth: user}); |
| 187 | +// let req = `${nonce}:${request}` |
| 188 | +// let sig = await saddle.web3.eth.sign(req, user) |
| 189 | +// console.log("🎲", req) |
| 190 | +// let tx = api.tx.cash.execTrxRequest(request, {'Eth': [user, sig]}, nonce) |
| 191 | +// console.log("🏁", await tx.send()) |
| 192 | +// }) |
| 193 | +// }); |
| 194 | + |
| 195 | +// r.defineCommand('eth_network', { |
| 196 | +// help: 'Show given Ethereum network', |
| 197 | +// action: defineAction(r, async () => { |
| 198 | +// console.log(`Network: ${network}`); |
| 199 | +// }) |
| 200 | +// }); |
| 201 | + |
| 202 | +// r.defineCommand('eth_from', { |
| 203 | +// help: 'Show default from Ethereum address', |
| 204 | +// action: defineAction(r, async () => { |
| 205 | +// console.log(`From: ${saddle.network_config.default_account}`); |
| 206 | +// }) |
| 207 | +// }); |
| 208 | + |
| 209 | +// r.defineCommand('eth_deployed', { |
| 210 | +// help: 'Show given deployed Ethereum contracts', |
| 211 | +// action: defineAction(r, async () => { |
| 212 | +// Object.entries(contracts).forEach(([contract, deployed]) => { |
| 213 | +// console.log(`${contract}: ${deployed || ""}`); |
| 214 | +// }); |
| 215 | +// }) |
| 216 | +// }); |
| 217 | +// } |
| 218 | + |
| 219 | +// function defineContracts(r, saddle, contractInsts) { |
| 220 | +// Object.entries(contractInsts).forEach(([contract, contractInst]) => { |
| 221 | +// Object.defineProperty(r.context, lowerCase(contract), { |
| 222 | +// configurable: true, |
| 223 | +// enumerable: true, |
| 224 | +// value: contractInst |
| 225 | +// }); |
| 226 | +// }); |
| 227 | +// } |
| 228 | + |
| 229 | +async function loadChainConfig(chain) { |
| 230 | + return JSON.parse(await fs.readFile(path.join(__dirname, chain, 'chain-config.json'), 'utf8')); |
| 231 | +} |
| 232 | + |
| 233 | +async function loadTypes(version) { |
| 234 | + let releaseTypesFile = path.join(__dirname, '..', 'releases', `m${Number(version)}`, 'types.json'); |
| 235 | + let baseTypesFile = path.join(__dirname, '..', 'types.json'); |
| 236 | + if (await fileExists(releaseTypesFile)) { |
| 237 | + return JSON.parse(await fs.readFile(releaseTypesFile, 'utf8')); |
| 238 | + } else { |
| 239 | + console.warn(chalk.yellow(`Cannot find release m${version} types file at ${releaseTypesFile}, using base types.json. Please pull release m${version} with \`scripts/pull_release.sh m${version}\``)); |
| 240 | + return JSON.parse(await fs.readFile(baseTypesFile, 'utf8')); |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +async function rpc(chain, chainConfig, section, method, params=[]) { |
| 245 | + if (!chainConfig.rpc) { |
| 246 | + throw new Error(`No websocket config for chain ${chain}`); |
| 247 | + } |
| 248 | + |
| 249 | + let res = await fetch(chainConfig.rpc, { |
| 250 | + method: 'post', |
| 251 | + body: JSON.stringify({ |
| 252 | + jsonrpc: "2.0", |
| 253 | + id: 1, |
| 254 | + method: `${section}_${method}`, |
| 255 | + params |
| 256 | + }), |
| 257 | + headers: { 'Content-Type': 'application/json' }, |
| 258 | + }); |
| 259 | + |
| 260 | + let resJson = await res.json(); |
| 261 | + |
| 262 | + return resJson.result; |
| 263 | +} |
| 264 | + |
| 265 | +async function getRuntimeVersion(chain, chainConfig) { |
| 266 | + return await rpc(chain, chainConfig, "state", "getRuntimeVersion"); |
| 267 | +} |
| 268 | + |
| 269 | +function toSS58(keyring, arr) { |
| 270 | + return keyring.encodeAddress(arr); |
| 271 | +} |
| 272 | + |
| 273 | +function defineKeys(r, obj) { |
| 274 | + Object.entries(obj).forEach(([key, value]) => { |
| 275 | + Object.defineProperty(r.context, key, { |
| 276 | + configurable: false, |
| 277 | + enumerable: typeof(value) !== 'function', |
| 278 | + value |
| 279 | + }); |
| 280 | + }); |
| 281 | +} |
| 282 | + |
| 283 | +async function loadScenario(scenarioJson) { |
| 284 | + let scenInfo; |
| 285 | + try { |
| 286 | + scenInfo = JSON.parse(scenarioJson); |
| 287 | + } catch (e) { |
| 288 | + console.log(`Invalid scenario JSON: ${scenarioJson}`); |
| 289 | + throw e; |
| 290 | + } |
| 291 | + |
| 292 | + scenInfo.profile = 'release'; |
| 293 | + scenInfo.log_file = path.join(__dirname, 'repl.log'); |
| 294 | + |
| 295 | + let ctx = await buildCtx(scenInfo); |
| 296 | + let ethChain = ctx.chains.find('eth'); |
| 297 | + |
| 298 | + return ctx; |
| 299 | +} |
| 300 | + |
| 301 | +async function startConsole(input, chain, options, scenario) { |
| 302 | + let { |
| 303 | + verbose, |
| 304 | + websocket |
| 305 | + } = options; |
| 306 | + |
| 307 | + let ctx = await loadScenario(scenario); |
| 308 | + |
| 309 | + console.info(`Gateway console on chain ${chain}`); |
| 310 | + |
| 311 | + // Object.entries(contracts).forEach(([contract, deployed]) => { |
| 312 | + // if (deployed) { |
| 313 | + // console.log(`\t${lowerCase(contract)}: ${deployed}`); |
| 314 | + // } |
| 315 | + // }); |
| 316 | + |
| 317 | + let r = repl.start({ |
| 318 | + prompt: '> ', |
| 319 | + input: input, |
| 320 | + output: input ? process.stdout : undefined, |
| 321 | + terminal: input ? false : undefined |
| 322 | + }); |
| 323 | + if (typeof(r.setupHistory) === 'function') { |
| 324 | + r.setupHistory(path.join(process.cwd(), '.repl_history'), (err, repl) => null); |
| 325 | + } |
| 326 | + r.originalCompleter = r.completer; |
| 327 | + r.completer = getCompletions(r.completer, {}); |
| 328 | + |
| 329 | + // defineCommands(r, saddle, network, contracts); |
| 330 | + |
| 331 | + defineKeys(r, ctx); |
| 332 | + //defineKeys(r, { saddle }); |
| 333 | + //defineContracts(r, saddle, contractInsts); |
| 334 | + |
| 335 | + process.on('uncaughtException', () => console.log('Error')); |
| 336 | + |
| 337 | + r.on('exit', () => { |
| 338 | + process.exit(); |
| 339 | + }); |
| 340 | +} |
| 341 | + |
| 342 | +let input; |
| 343 | +const options = getopts(process.argv.slice(2), { |
| 344 | + alias: { |
| 345 | + script: "s", |
| 346 | + eval: "e", |
| 347 | + chain: "c", |
| 348 | + websocket: "w", |
| 349 | + verbose: "v", |
| 350 | + scenario: "x", |
| 351 | + info_file: "i" |
| 352 | + }, |
| 353 | +}); |
| 354 | + |
| 355 | +let chain; |
| 356 | +let scenario; |
| 357 | +if (options.info_file) { |
| 358 | + scenario = readFileSync(options.info_file); |
| 359 | + chain = 'scenario'; |
| 360 | +} |
| 361 | +if (options.scenario) { |
| 362 | + scenario = options.scenario; |
| 363 | + chain = 'scenario'; |
| 364 | +} else { |
| 365 | + chain = options.chain || 'testnet'; |
| 366 | +} |
| 367 | + |
| 368 | +if (options.script) { |
| 369 | + input = createReadStream(options.script); |
| 370 | +} else if (options.eval) { |
| 371 | + let evalArg = options.eval; |
| 372 | + let codes = Array.isArray(evalArg) ? evalArg.map((e) => e + ';\n') : [ evalArg ]; |
| 373 | + input = Readable.from(codes); |
| 374 | +} |
| 375 | + |
| 376 | +startConsole(input, chain, options, scenario); |
0 commit comments