diff --git a/devnets/devnet.init.mjs b/devnets/devnet.init.mjs index d983fc58554..de88dbcb658 100644 --- a/devnets/devnet.init.mjs +++ b/devnets/devnet.init.mjs @@ -1,96 +1,135 @@ -import { spawn, exec, execSync } from 'child_process' -import { existsSync, writeFileSync, chmodSync } from 'fs' +import { env } from 'node:process' +import { spawn, exec, execSync } from 'node:child_process' +import { existsSync, writeFileSync, chmodSync } from 'node:fs' -const run = command => { - if (process.env.Verbose) console.info('$', command) - const result = String(execSync(command)).trim() - if (process.env.Verbose) console.info(result) - return result -} +const { + VERBOSE = false, + + CHAIN_ID = `local-${DAEMON}`, + TOKEN = 'uscrt', + ACCOUNTS = 'Admin Alice Bob Charlie Mallory', + AMOUNT = `1000000000000000000${TOKEN}`, + + LCP_PORT = '1317', + RPC_ADDR = 'tcp://0.0.0.0:26657', + GRPC_ADDR = '0.0.0.0:9090', + GRPC_WEB_ADDR = '0.0.0.0:9091', + + DAEMON = 'secretd', + STATE_DIR = `/state/${CHAIN_ID}`, + STATE_UID = null, + STATE_GID = null, +} = env + +const daemonDir = `~/.${DAEMON}` +const configDir = `${daemonDir}/config` +const appToml = `${configDir}/app.toml` +const genesis = `${configDir}/genesis.json` +const nodeKey = `${configDir}/node_key.json` +const stateDir = `/state/${CHAIN_ID}` +const wallets = `${stateDir}/wallet` start() -function start ({ - lcpPort = process.env.lcpPort || '1317', - rpcAddr = process.env.rpcAddr || 'tcp://0.0.0.0:26657', - grpcAddr = process.env.grpcAddr || '0.0.0.0:9090', - grpcWebAddr = process.env.grpcWebAddr || '0.0.0.0:9091', - genesisJSON = '~/.secretd/config/genesis.json', -} = {}) { - if (!existsSync(genesisJSON)) { - console.info(`${genesisJSON} missing -> performing genesis`) - genesis() - } else { - console.info(`${genesisJSON} exists -> resuming devnet`) - } +function start () { + performGenesis() + spawnLcp() + launchNode() + console.info('Server exited.') +} +function spawnLcp () { console.info('Configuring the node to support lcp (CORS proxy)...') - run(`perl -i -pe 's;address = "tcp://0.0.0.0:1317";address = "tcp://0.0.0.0:1316";' .secretd/config/app.toml`) - run(`perl -i -pe 's/enable-unsafe-cors = false/enable-unsafe-cors = true/' .secretd/config/app.toml`) - const lcpArgs = [`--proxyUrl`, 'http://localhost:1316', `--port`, lcpPort, `--proxyPartial`, ``] - - console.info('Spawning lcp (CORS proxy)...') - if (process.env.Verbose) console.log(`$ lcp`, ...lcpArgs) + run(`perl -i -pe 's;address = "tcp://0.0.0.0:1317";address = "tcp://0.0.0.0:1316";' ${appToml}`) + run(`perl -i -pe 's/enable-unsafe-cors = false/enable-unsafe-cors = true/' ${appToml}`) + const lcpArgs = [ + `--proxyUrl`, 'http://localhost:1316', + `--port`, LCP_PORT, + `--proxyPartial`, `` + ] + console.info(`Spawning lcp (CORS proxy) on port ${LCP_PORT}`) + if (VERBOSE) console.log(`$ lcp`, ...lcpArgs) const lcp = spawn(`lcp`, lcpArgs, { stdio: 'inherit' }) +} +function launchNode () { console.info('Launching the node...') - const command = `source /opt/sgxsdk/environment && RUST_BACKTRACE=1 secretd start --bootstrap` - + ` --rpc.laddr ${rpcAddr}` - + ` --grpc.address ${grpcAddr}` - + ` --grpc-web.address ${grpcWebAddr}` + const command = `source /opt/sgxsdk/environment && RUST_BACKTRACE=1 ${DAEMON} start --bootstrap` + + ` --rpc.laddr ${RPC_ADDR}` + + ` --grpc.address ${GRPC_ADDR}` + + ` --grpc-web.address ${GRPC_WEB_ADDR}` console.info(`$`, command) execSync(command, { shell: '/bin/bash', stdio: 'inherit' }) - console.info('Server exited.') } -function genesis ({ - chainId = process.env.ChainId || 'fadroma-devnet', - stateDir = `/state/${chainId}`, - genesisAccounts = (process.env.GenesisAccounts || 'Admin Alice Bob Charlie Mallory').split(' '), - amount = "1000000000000000000uscrt", - uid = process.env._UID, - gid = process.env._GID -} = {}) { +function performGenesis () { + if (existsSync(genesis)) { + console.info(`Resuming devnet (${genesis} exists).`) + return + } + console.info(`Performing genesis because ${genesis} is missing.`) + preGenesisCleanup() + preGenesisConfig() + createGenesisTransaction() + bootstrapChain() + console.info('\nSprinkling holy water...') + console.info() +} + +function preGenesisCleanup () { console.info('\nEnsuring a clean slate...') - run(`rm -rf ~/.secretd ~/.secretcli /opt/secret/.sgx-secrets`) + run(`rm -rf ${daemonDir} ~/.secretcli /opt/secret/.sgx-secrets`) +} +function preGenesisConfig () { console.info('\nEstablishing initial config...') - run(`mkdir -p ${stateDir}/wallet`) - if (uid) run(`chown -R ${uid} ${stateDir}`) - if (gid) run(`chgrp -R ${gid} ${stateDir}`) - run(`secretd config chain-id "${chainId}"`) - run(`secretd config keyring-backend test`) - run(`secretd init fadroma-devnet --chain-id "${chainId}"`) - run(`cp ~/node_key.json ~/.secretd/config/node_key.json`) - run(`perl -i -pe 's/"stake"/ "uscrt"/g' ~/.secretd/config/genesis.json`) - - console.info('\nCreating genesis accounts', genesisAccounts) - for (const name of genesisAccounts) { - const mnemonic = run(`secretd keys add "${name}" 2>&1 | tail -n1`) - const address = run(`secretd keys show -a "${name}"`) - const identity = `${stateDir}/wallet/${name}.json` - writeFileSync(identity, JSON.stringify({ address, mnemonic })) - if (uid) run(`chown -R ${uid} ${identity}`) - if (gid) run(`chgrp -R ${gid} ${identity}`) - } - if (uid) run(`chown -R ${uid} ${stateDir}`) - if (gid) run(`chgrp -R ${gid} ${stateDir}`) + run(`mkdir -p ${wallets}`) + fixPermissions() + daemon(`config chain-id "${CHAIN_ID}"`) + daemon(`config keyring-backend test`) + daemon(`init fadroma-devnet --chain-id "${CHAIN_ID}"`) + run(`cp ~/node_key.json ${nodeKey}`) + run(`perl -i -pe 's/"stake"/ "${TOKEN}"/g' ${genesis}`) +} - console.info('\nAdding genesis accounts...') - for (const name of genesisAccounts) { - const address = run(`secretd keys show -a "${name}"`) - run(`secretd add-genesis-account "${address}" "${amount}"`) +function createGenesisTransaction () { + let accounts = ACCOUNTS.split(' ') + if (accounts.length === 0) accounts = ['Admin'] + console.info('\nCreating genesis accounts:') + for (const name of accounts) { + const mnemonic = daemon(`keys add "${name}" 2>&1 | tail -n1`) + const address = daemon(`keys show -a "${name}"`) + console.info(`\n- ${AMOUNT} ${address} (${name})`) + daemon(`add-genesis-account "${address}" "${AMOUNT}"`) + const identity = `${wallets}/${name}.json` + writeFileSync(identity, JSON.stringify({ address, mnemonic })) + fixPermissions(identity) } - + fixPermissions() console.info('\nCreating genesis transaction...') - run(`secretd gentx "${genesisAccounts[0]}" 1000000uscrt --chain-id ${chainId} --keyring-backend test`) + daemon(`gentx "${accounts[0]}" 1000000${TOKEN} --chain-id ${CHAIN_ID} --keyring-backend test`) +} +function bootstrapChain () { console.info('\nBootstrapping chain...') - run(`secretd collect-gentxs`) - run(`secretd validate-genesis`) - run(`secretd init-bootstrap`) - run(`secretd validate-genesis`) + daemon(`collect-gentxs`) + daemon(`validate-genesis`) + daemon(`init-bootstrap`) + daemon(`validate-genesis`) +} - console.info('\nSprinkling holy water...') - console.info() +function fixPermissions (path = stateDir) { + if (STATE_UID) run(`chown -R ${STATE_UID} ${stateDir}`) + if (STATE_GID) run(`chgrp -R ${STATE_GID} ${stateDir}`) +} + +function run (command) { + if (VERBOSE) console.info('$', command) + const result = String(execSync(command)).trim() + if (VERBOSE) console.info(result) + return result +} + +function daemon (command) { + return run(`${DAEMON} ${command}`) } diff --git a/fadroma-devnet.ts b/fadroma-devnet.ts index 767858e1da0..cf551f0f8e9 100644 --- a/fadroma-devnet.ts +++ b/fadroma-devnet.ts @@ -28,7 +28,7 @@ import type { Path } from '@hackbg/file' import { freePort, Endpoint, waitPort, isPortTaken } from '@hackbg/port' import * as Dock from '@hackbg/dock' -import { dirname } from 'node:path' +import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { randomBytes } from 'node:crypto' @@ -121,6 +121,8 @@ export class Devnet implements DevnetHandle { } } // Apply the rest of the configuration options + const defaultInit = resolve(dirname(fileURLToPath(import.meta.url)), 'devnets', 'devnet.init.mjs') + this.initScript = options.initScript! ?? defaultInit this.keepRunning = options.keepRunning ?? !this.deleteOnExit this.podman = options.podman ?? false this.platform = options.platform ?? 'scrt_1.9' @@ -128,7 +130,6 @@ export class Devnet implements DevnetHandle { this.launchTimeout = options.launchTimeout ?? 10 this.dontMountState = options.dontMountState ?? false this.accounts = options.accounts ?? this.accounts - this.initScript = options.initScript! ?? this.initScript this.readyPhrase = options.readyPhrase ?? Devnet.readyMessage[this.platform] this.protocol = options.protocol ?? 'http' this.host = options.host ?? 'localhost' @@ -163,19 +164,21 @@ export class Devnet implements DevnetHandle { /** Environment variables in the container. */ get spawnEnv () { - // Environment variables in devnet container const env: Record = { - Verbose: this.verbose ? 'yes' : '', - ChainId: this.chainId, - GenesisAccounts: this.accounts.join(' '), - _UID: String((process.getuid!)()), - _GID: String((process.getgid!)()), + CHAIN_ID: this.chainId, + ACCOUNTS: this.accounts.join(' '), + STATE_UID: String((process.getuid!)()), + STATE_GID: String((process.getgid!)()), } - // Which kind of API to expose at the default container port - switch (this.portMode) { - case 'lcp': env.lcpPort = String(this.port); break - case 'grpcWeb': env.grpcWebAddr = `0.0.0.0:${this.port}`; break - default: throw new DevnetError.PortMode(this.portMode) + if (this.verbose) { + env['VERBOSE'] = 'yes' + } + if (this.portMode === 'lcp') { + env['LCP_PORT'] = String(this.port) + } else if (this.portMode === 'grpcWeb') { + env['GRPC_WEB_ADDR'] = `0.0.0.0:${this.port}` + } else { + throw new DevnetError.PortMode(this.portMode) } return env } @@ -183,8 +186,12 @@ export class Devnet implements DevnetHandle { /** Options for the container. */ get spawnOptions () { const Binds: string[] = [] - if (this.initScript) Binds.push(`${this.initScript}:${this.initScriptMount}:ro`) - if (!this.dontMountState) Binds.push(`${$(this.stateDir).path}:/state/${this.chainId}:rw`) + if (this.initScript) { + Binds.push(`${this.initScript}:${this.initScriptMount}:ro`) + } + if (!this.dontMountState) { + Binds.push(`${$(this.stateDir).path}:/state/${this.chainId}:rw`) + } const NetworkMode = 'bridge' const PortBindings = {[`${this.port}/tcp`]: [{HostPort: `${this.port}`}]} const HostConfig = {Binds, NetworkMode, PortBindings} diff --git a/spec/Devnet.test.ts b/spec/Devnet.test.ts index 91079a4e03a..49d3d53a2e7 100644 --- a/spec/Devnet.test.ts +++ b/spec/Devnet.test.ts @@ -1,11 +1,13 @@ -//import './Devnet.spec.ts.md' - import { Devnet } from '@hackbg/fadroma' import * as assert from 'node:assert' import { getuid, getgid } from 'node:process' import $, { TextFile } from '@hackbg/file' import { Image, Container } from '@hackbg/dock' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +const initScript = resolve(dirname(fileURLToPath(import.meta.url)), 'devnet', 'devnet.init.mjs') + let devnet: any ;(async () => { @@ -20,6 +22,9 @@ let devnet: any await testDevnetHighLevel() + //@ts-ignore + await import('./Devnet.spec.ts.md') + })() async function testDevnetChainId () { @@ -94,7 +99,7 @@ async function testDevnetStateFile () { async function testDevnetUrl () { assert.ok( - devnet = new Devnet(), + devnet = new Devnet({ initScript }), "can construct with no options" ) @@ -112,6 +117,11 @@ async function testDevnetUrl () { async function testDevnetContainer () { + assert.ok( + devnet = new Devnet({ initScript }), + "can construct with explicitly enabled init script" + ) + assert.equal( devnet.initScriptMount, '/devnet.init.mjs', "devnet init script mounted at default location" @@ -119,12 +129,11 @@ async function testDevnetContainer () { assert.deepEqual( devnet.spawnEnv, { - Verbose: '', - ChainId: devnet.chainId, - GenesisAccounts: devnet.accounts.join(' '), - _UID: getuid!(), - _GID: getgid!(), - lcpPort: String(devnet.port) + CHAIN_ID: devnet.chainId, + ACCOUNTS: devnet.accounts.join(' '), + STATE_UID: String(getuid!()), + STATE_GID: String(getgid!()), + LCP_PORT: String(devnet.port) }, "devnet spawn environment" )