diff --git a/bases/shared/instagoric-server/constants.js b/bases/shared/instagoric-server/constants.js new file mode 100644 index 0000000..1543646 --- /dev/null +++ b/bases/shared/instagoric-server/constants.js @@ -0,0 +1,78 @@ +// @ts-check +import { fs } from 'zx'; +import { + createTempDir, + DataCache, + getDockerImageValue, + getMetricsRequest, + getNetworkConfig, + getRevisionValue, + getServices, +} from './utils'; + +export const BASE_AMOUNT = '25000000'; + +export const CLIENT_AMOUNT = + process.env.CLIENT_AMOUNT || '25000000uist,25000000ibc/toyusdc'; + +export const DELEGATE_AMOUNT = + process.env.DELEGATE_AMOUNT || + '75000000ubld,25000000ibc/toyatom,25000000ibc/toyellie,25000000ibc/toyusdc,25000000ibc/toyollie'; + +export const COMMANDS = { + 'SEND_BLD/IBC': 'send_bld_ibc', + SEND_AND_PROVISION_IST: 'send_ist_and_provision', + FUND_PROV_POOL: 'fund_provision_pool', + CUSTOM_DENOMS_LIST: 'custom_denoms_list', +}; + +export const PROVISIONING_POOL_ADDR = + 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346'; + +export const DOCKERTAG = process.env.DOCKERTAG; +export const DOCKERIMAGE = process.env.DOCKERIMAGE; + +export const FAUCET_KEYNAME = + process.env.FAUCET_KEYNAME || process.env.WHALE_KEYNAME || 'self'; + +export const AG0_MODE = (process.env.AG0_MODE || 'false') === 'true'; +export const agBinary = AG0_MODE ? 'ag0' : 'agd'; + +export const NETNAME = process.env.NETNAME || 'devnet'; +export const NETDOMAIN = process.env.NETDOMAIN || '.agoric.net'; + +export const RPC_PORT = 26657; + +export const TRANSACTION_STATUS = { + FAILED: 1000, + NOT_FOUND: 1001, + SUCCESSFUL: 1002, +}; + +export const FAKE = process.env.FAKE || process.argv[2] === '--fake'; +export const podname = process.env.POD_NAME || 'validator-primary'; +export const namespace = + process.env.NAMESPACE || + fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/namespace', { + encoding: 'utf8', + flag: 'r', + }); +export const INCLUDE_SEED = process.env.SEED_ENABLE || 'yes'; +export const NODE_ID = + process.env.NODE_ID || 'fb86a0993c694c981a28fa1ebd1fd692f345348b'; + +export const tmpDir = FAKE ? await createTempDir() : null; + +const { details: X } = globalThis.assert; +export const agoricHome = FAKE ? tmpDir : process.env.AGORIC_HOME; +assert(agoricHome, X`AGORIC_HOME not set`); + +export const chainId = process.env.CHAIN_ID; +assert(chainId, X`CHAIN_ID not set`); + +export const revision = getRevisionValue(); +export const dockerImage = await getDockerImageValue(namespace, podname); + +export const ipsCache = new DataCache(getServices, 0.1); +export const networkConfig = new DataCache(getNetworkConfig, 0.5); +export const metricsCache = new DataCache(getMetricsRequest, 0.1); diff --git a/bases/shared/instagoric-server/server.js b/bases/shared/instagoric-server/server.js index ab65e56..2cf8ebc 100644 --- a/bases/shared/instagoric-server/server.js +++ b/bases/shared/instagoric-server/server.js @@ -3,229 +3,50 @@ import './lockdown.js'; import process from 'process'; import express from 'express'; -import https from 'https'; -import tmp from 'tmp'; -import { $, fetch, fs, nothrow, sleep } from 'zx'; - +import { fs, sleep } from 'zx'; +import { + BASE_AMOUNT, + CLIENT_AMOUNT, + DELEGATE_AMOUNT, + COMMANDS, + PROVISIONING_POOL_ADDR, + DOCKERTAG, + DOCKERIMAGE, + FAUCET_KEYNAME, + NETNAME, + NETDOMAIN, + TRANSACTION_STATUS, + AG0_MODE, + FAKE, + chainId, + namespace, + revision, + ipsCache, + networkConfig, + metricsCache, + dockerImage, + tmpDir +} from './constants.js'; +import { + dockerComposeYaml, + getTransactionStatus, + pollForProvisioning, + sendFunds, + constructAmountToSend, + getDenoms, + logRequest, + addKey, +} from './utils.js'; import { makeSubscriptionKit } from '@agoric/notifier'; -const { details: X } = globalThis.assert; - -const BASE_AMOUNT = "25000000"; // Adding here to avoid ReferenceError for local server. Not needed for k8 let CLUSTER_NAME; -const CLIENT_AMOUNT = - process.env.CLIENT_AMOUNT || '25000000uist,25000000ibc/toyusdc'; -const DELEGATE_AMOUNT = - process.env.DELEGATE_AMOUNT || - '75000000ubld,25000000ibc/toyatom,25000000ibc/toyellie,25000000ibc/toyusdc,25000000ibc/toyollie'; - -const COMMANDS = { - "SEND_BLD/IBC": "send_bld_ibc", - "SEND_AND_PROVISION_IST": "send_ist_and_provision", - "FUND_PROV_POOL": "fund_provision_pool", - "CUSTOM_DENOMS_LIST": "custom_denoms_list", -}; - - -const PROVISIONING_POOL_ADDR = 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346'; - -const DOCKERTAG = process.env.DOCKERTAG; // Optional. -const DOCKERIMAGE = process.env.DOCKERIMAGE; // Optional. -const FAUCET_KEYNAME = - process.env.FAUCET_KEYNAME || process.env.WHALE_KEYNAME || 'self'; -const NETNAME = process.env.NETNAME || 'devnet'; -const NETDOMAIN = process.env.NETDOMAIN || '.agoric.net'; -const AG0_MODE = (process.env.AG0_MODE || 'false') === 'true'; -const agBinary = AG0_MODE ? 'ag0' : 'agd'; -const podname = process.env.POD_NAME || 'validator-primary'; -const INCLUDE_SEED = process.env.SEED_ENABLE || 'yes'; -const NODE_ID = - process.env.NODE_ID || 'fb86a0993c694c981a28fa1ebd1fd692f345348b'; -const RPC_PORT = 26657; -const TRANSACTION_STATUS = { - FAILED: 1000, - NOT_FOUND: 1001, - SUCCESSFUL: 1002, -}; - -const FAKE = process.env.FAKE || process.argv[2] === '--fake'; if (FAKE) { console.log('FAKE MODE'); - const tmpDir = await new Promise((resolve, reject) => { - tmp.dir({ prefix: 'faucet', postfix: 'home' }, (err, path) => { - if (err) { - return reject(err); - } - resolve(path); - }); - }); - // Create the temporary key. console.log(`Creating temporary key`, { tmpDir, FAUCET_KEYNAME }); - await $`${agBinary} --home=${tmpDir} keys --keyring-backend=test add ${FAUCET_KEYNAME}`; - process.env.AGORIC_HOME = tmpDir; -} - -const agoricHome = process.env.AGORIC_HOME; -assert(agoricHome, X`AGORIC_HOME not set`); - -const chainId = process.env.CHAIN_ID; -assert(chainId, X`CHAIN_ID not set`); - -let dockerImage; - -const namespace = - process.env.NAMESPACE || - fs.readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/namespace', { - encoding: 'utf8', - flag: 'r', - }); - -let revision; -if (FAKE) { - revision = 'fake_revision'; -} else { - revision = - process.env.AG0_MODE === 'true' - ? 'ag0' - : fs.readFileSync('/usr/src/agoric-sdk/packages/solo/public/git-revision.txt', { - encoding: 'utf8', - flag: 'r', - }).trim(); -} - -/** - * @param {string} relativeUrl - * @returns {Promise} - */ -const makeKubernetesRequest = async relativeUrl => { - const ca = await fs.readFile( - '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt', - 'utf8', - ); - const token = await fs.readFile( - '/var/run/secrets/kubernetes.io/serviceaccount/token', - 'utf8', - ); - const url = new URL( - relativeUrl, - 'https://kubernetes.default.svc.cluster.local', - ); - const response = await fetch(url.href, { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/json', - }, - agent: new https.Agent({ ca }), - }); - return response.json(); -}; - -const getMetricsRequest = async relativeUrl => { - const url = new URL('http://localhost:26661/metrics'); - const response = await fetch(url.href); - return response.text(); -}; - -// eslint-disable-next-line no-unused-vars -async function getNodeId(node) { - const response = await fetch( - `http://${node}.${namespace}.svc.cluster.local:26657/status`, - ); - return response.json(); -} - -async function getServices() { - if (FAKE) { - return new Map([ - ['validator-primary-ext', '1.1.1.1'], - ['seed-ext', '1.1.1.2'], - ]); - } - const services = await makeKubernetesRequest( - `/api/v1/namespaces/${namespace}/services/`, - ); - const map1 = new Map(); - for (const item of services.items) { - const ingress = item.status?.loadBalancer?.ingress; - if (ingress?.length > 0) { - map1.set(item.metadata.name, ingress[0].ip); - } - } - return map1; -} - -const getNetworkConfig = async () => { - const svc = await getServices(); - const file = FAKE - ? './resources/network_info.json' - : '/config/network/network_info.json'; - const buf = await fs.readFile(file, 'utf8'); - const ap = JSON.parse(buf); - ap.chainName = chainId; - ap.gci = `https://${NETNAME}.rpc${NETDOMAIN}:443/genesis`; - ap.peers[0] = ap.peers[0].replace( - 'validator-primary.instagoric.svc.cluster.local', - svc.get('validator-primary-ext') || - `${podname}.${namespace}.svc.cluster.local`, - ); - ap.peers[0] = ap.peers[0].replace( - 'fb86a0993c694c981a28fa1ebd1fd692f345348b', - `${NODE_ID}`, - ); - ap.rpcAddrs = [`https://${NETNAME}.rpc${NETDOMAIN}:443`]; - ap.apiAddrs = [`https://${NETNAME}.api${NETDOMAIN}:443`]; - if (INCLUDE_SEED === 'yes') { - ap.seeds[0] = ap.seeds[0].replace( - 'seed.instagoric.svc.cluster.local', - svc.get('seed-ext') || `seed.${namespace}.svc.cluster.local`, - ); - } else { - ap.seeds = []; - } - - return JSON.stringify(ap); -}; -class DataCache { - constructor(fetchFunction, minutesToLive = 10) { - this.millisecondsToLive = minutesToLive * 60 * 1000; - this.fetchFunction = fetchFunction; - this.cache = null; - this.getData = this.getData.bind(this); - this.resetCache = this.resetCache.bind(this); - this.isCacheExpired = this.isCacheExpired.bind(this); - this.fetchDate = new Date(0); - } - - isCacheExpired() { - return ( - this.fetchDate.getTime() + this.millisecondsToLive < new Date().getTime() - ); - } - - getData() { - if (!this.cache || this.isCacheExpired()) { - console.log('fetch'); - return this.fetchFunction().then(data => { - this.cache = data; - this.fetchDate = new Date(); - return data; - }); - } else { - console.log('cache hit'); - - return Promise.resolve(this.cache); - } - } - - resetCache() { - this.fetchDate = new Date(0); - } + await addKey(tmpDir, FAUCET_KEYNAME) } -const ipsCache = new DataCache(getServices, 0.1); -const networkConfig = new DataCache(getNetworkConfig, 0.5); -const metricsCache = new DataCache(getMetricsRequest, 0.1); const publicapp = express(); const privateapp = express(); @@ -233,27 +54,11 @@ const faucetapp = express(); const publicport = 8001; const privateport = 8002; const faucetport = 8003; -const logReq = (req, res, next) => { - const time = Date.now(); - res.on('finish', () => { - console.log( - JSON.stringify({ - time, - dur: Date.now() - time, - method: req.method, - forwarded: req.get('X-Forwarded-For'), - ip: req.ip, - url: req.originalUrl, - status: res.statusCode, - }), - ); - }); - next(); -}; -publicapp.use(logReq); -privateapp.use(logReq); -faucetapp.use(logReq); + +publicapp.use(logRequest); +privateapp.use(logRequest); +faucetapp.use(logRequest); publicapp.get('/', (req, res) => { const domain = NETDOMAIN; @@ -313,27 +118,7 @@ publicapp.get('/metrics-config', async (req, res) => { res.send(result); }); -const dockerComposeYaml = (dockerimage, dockertag, netname, netdomain) => `\ -version: "2.2" -services: - ag-solo: - image: ${dockerimage}:\${SDK_TAG:-${dockertag}} - ports: - - "\${HOST_PORT:-8000}:\${PORT:-8000}" - volumes: - - "ag-solo-state:/state" - - "$HOME/.agoric:/root/.agoric" - environment: - - "AG_SOLO_BASEDIR=/state/\${SOLO_HOME:-${dockertag}}" - entrypoint: ag-solo - command: - - setup - - --webhost=0.0.0.0 - - --webport=\${PORT:-8000} - - --netconfig=\${NETCONFIG_URL:-https://${netname}${netdomain}/network-config} -volumes: - ag-solo-state: -`; + publicapp.get('/docker-compose.yml', (req, res) => { res.setHeader( @@ -408,118 +193,8 @@ const addRequest = (address, request) => { publication.updateState(address); }; -/** - * Returns the status of a transaction against hash `txHash`. - * The status is one of the values from `TRANSACTION_STATUS` - * @param {string} txHash - * @returns {Promise} - */ -const getTransactionStatus = async txHash => { - let { exitCode, stderr, stdout } = await nothrow($`\ - ${agBinary} query tx ${txHash} \ - --chain-id=${chainId} \ - --home=${agoricHome} \ - --node=http://localhost:${RPC_PORT} \ - --output=json \ - --type=hash \ - `); - exitCode = exitCode ?? 1; - - // This check is brittle as this can also happen in case - // an invalid txhash was provided. So there is no reliable - // distinction between the case of invalid txhash and a - // transaction currently in the mempool. We could use search - // endpoint but that seems overkill to cover a case where - // the only the deliberate use of invalid hash can effect the user - if (exitCode && stderr.includes(`tx (${txHash}) not found`)) - return TRANSACTION_STATUS.NOT_FOUND; - - const code = Number(JSON.parse(stdout).code); - return code ? TRANSACTION_STATUS.FAILED : TRANSACTION_STATUS.SUCCESSFUL; -}; - -/** - * @param {string} address - * @param {string} clientType - * @param {string} txHash - * @returns {Promise} - */ -const pollForProvisioning = async (address, clientType, txHash) => { - const status = await getTransactionStatus(txHash); - status === TRANSACTION_STATUS.NOT_FOUND - ? setTimeout(() => pollForProvisioning(address, clientType, txHash), 2000) - : status === TRANSACTION_STATUS.SUCCESSFUL - ? await provisionAddress(address, clientType) - : console.log( - `Not provisioning address "${address}" of type "${clientType}" as transaction "${txHash}" failed`, - ); -}; - -/** - * @param {string} address - * @param {string} clientType - * @returns {Promise} - */ -const provisionAddress = async (address, clientType) => { - let { exitCode, stderr } = await nothrow($`\ - ${agBinary} tx swingset provision-one faucet_provision ${address} ${clientType} \ - --broadcast-mode=block \ - --chain-id=${chainId} \ - --from=${FAUCET_KEYNAME} \ - --keyring-backend=test \ - --keyring-dir=${agoricHome} \ - --node=http://localhost:${RPC_PORT} \ - --yes \ - `); - exitCode = exitCode ?? 1; - - if (exitCode) - console.log( - `Failed to provision address "${address}" of type "${clientType}" with error message: ${stderr}`, - ); -}; - -/** - * Send funds to `address`. - * It only waits for the transaction - * checks and doesn't wait for the - * transaction to actually be included - * in a block. The returned transaction - * hash can be used to get the current status - * of the transaction later - * @param {string} address - * @param {string} amount - * @returns {Promise<[number, string]>} - */ -const sendFunds = async (address, amount) => { - let { exitCode, stdout } = await nothrow($`\ - ${agBinary} tx bank send ${FAUCET_KEYNAME} ${address} ${amount} \ - --broadcast-mode=sync \ - --chain-id=${chainId} \ - --keyring-backend=test \ - --keyring-dir=${agoricHome} \ - --node=http://localhost:${RPC_PORT} \ - --output=json \ - --yes \ - `); - exitCode = exitCode ?? 1; - - if (exitCode) return [exitCode, '']; - return [exitCode, String(JSON.parse(stdout).txhash)]; -}; // Faucet worker. - -const constructAmountToSend = (amount, denoms) => denoms.map(denom => `${amount}${denom}`).join(','); - -const getDenoms = async () => { - // Not handling pagination as it is used for testing. Limit 100 shoud suffice - - const result = await $`${agBinary} query bank total --limit=100 -o json`; - const output = JSON.parse(result.stdout.trim()); - return output.supply.map((element) => element.denom); -} - const startFaucetWorker = async () => { console.log('Starting Faucet worker!'); @@ -776,14 +451,7 @@ faucetapp.listen(faucetport, () => { console.log(`faucetapp listening on port ${faucetport}`); }); -if (FAKE) { - dockerImage = 'asdf:unknown'; -} else { - const statefulSet = await makeKubernetesRequest( - `/apis/apps/v1/namespaces/${namespace}/statefulsets/${podname}`, - ); - dockerImage = statefulSet.spec.template.spec.containers[0].image; -} + publicapp.listen(publicport, () => { console.log(`publicapp listening on port ${publicport}`); }); diff --git a/bases/shared/instagoric-server/utils.js b/bases/shared/instagoric-server/utils.js new file mode 100644 index 0000000..0432d8e --- /dev/null +++ b/bases/shared/instagoric-server/utils.js @@ -0,0 +1,348 @@ +// @ts-check +import { $, fs, nothrow } from 'zx'; +import https from 'https'; +import tmp from 'tmp'; +import { + namespace, + FAKE, + chainId, + NETNAME, + NETDOMAIN, + podname, + NODE_ID, + INCLUDE_SEED, + agBinary, + agoricHome, + RPC_PORT, + TRANSACTION_STATUS, + FAUCET_KEYNAME, + AG0_MODE, +} from './constants'; + +/** + * @param {string} relativeUrl + * @returns {Promise} + */ +export const makeKubernetesRequest = async relativeUrl => { + const ca = await fs.readFile( + '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt', + 'utf8', + ); + const token = await fs.readFile( + '/var/run/secrets/kubernetes.io/serviceaccount/token', + 'utf8', + ); + const url = new URL( + relativeUrl, + 'https://kubernetes.default.svc.cluster.local', + ); + const response = await fetch(url.href, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + agent: new https.Agent({ ca }), + }); + return response.json(); +}; + +export const getMetricsRequest = async () => { + const url = new URL('http://localhost:26661/metrics'); + const response = await fetch(url.href); + return response.text(); +}; + +export const getNodeId = async node => { + const response = await fetch( + `http://${node}.${namespace}.svc.cluster.local:26657/status`, + ); + return response.json(); +}; + +export const getServices = async () => { + if (FAKE) { + return new Map([ + ['validator-primary-ext', '1.1.1.1'], + ['seed-ext', '1.1.1.2'], + ]); + } + const services = await makeKubernetesRequest( + `/api/v1/namespaces/${namespace}/services/`, + ); + const map1 = new Map(); + for (const item of services.items) { + const ingress = item.status?.loadBalancer?.ingress; + if (ingress?.length > 0) { + map1.set(item.metadata.name, ingress[0].ip); + } + } + return map1; +}; + +export const getNetworkConfig = async () => { + const svc = await getServices(); + const file = FAKE + ? './resources/network_info.json' + : '/config/network/network_info.json'; + const buf = await fs.readFile(file, 'utf8'); + const ap = JSON.parse(buf); + ap.chainName = chainId; + ap.gci = `https://${NETNAME}.rpc${NETDOMAIN}:443/genesis`; + ap.peers[0] = ap.peers[0].replace( + 'validator-primary.instagoric.svc.cluster.local', + svc.get('validator-primary-ext') || + `${podname}.${namespace}.svc.cluster.local`, + ); + ap.peers[0] = ap.peers[0].replace( + 'fb86a0993c694c981a28fa1ebd1fd692f345348b', + `${NODE_ID}`, + ); + ap.rpcAddrs = [`https://${NETNAME}.rpc${NETDOMAIN}:443`]; + ap.apiAddrs = [`https://${NETNAME}.api${NETDOMAIN}:443`]; + if (INCLUDE_SEED === 'yes') { + ap.seeds[0] = ap.seeds[0].replace( + 'seed.instagoric.svc.cluster.local', + svc.get('seed-ext') || `seed.${namespace}.svc.cluster.local`, + ); + } else { + ap.seeds = []; + } + + return JSON.stringify(ap); +}; + +export const dockerComposeYaml = ( + dockerimage, + dockertag, + netname, + netdomain, +) => `\ +version: "2.2" +services: + ag-solo: + image: ${dockerimage}:\${SDK_TAG:-${dockertag}} + ports: + - "\${HOST_PORT:-8000}:\${PORT:-8000}" + volumes: + - "ag-solo-state:/state" + - "$HOME/.agoric:/root/.agoric" + environment: + - "AG_SOLO_BASEDIR=/state/\${SOLO_HOME:-${dockertag}}" + entrypoint: ag-solo + command: + - setup + - --webhost=0.0.0.0 + - --webport=\${PORT:-8000} + - --netconfig=\${NETCONFIG_URL:-https://${netname}${netdomain}/network-config} +volumes: + ag-solo-state: +`; + +/** + * Returns the status of a transaction against hash `txHash`. + * The status is one of the values from `TRANSACTION_STATUS` + * @param {string} txHash + * @returns {Promise} + */ +export const getTransactionStatus = async txHash => { + let { exitCode, stderr, stdout } = await nothrow($`\ + ${agBinary} query tx ${txHash} \ + --chain-id=${chainId} \ + --home=${agoricHome} \ + --node=http://localhost:${RPC_PORT} \ + --output=json \ + --type=hash \ + `); + exitCode = exitCode ?? 1; + + // This check is brittle as this can also happen in case + // an invalid txhash was provided. So there is no reliable + // distinction between the case of invalid txhash and a + // transaction currently in the mempool. We could use search + // endpoint but that seems overkill to cover a case where + // the only the deliberate use of invalid hash can effect the user + if (exitCode && stderr.includes(`tx (${txHash}) not found`)) + return TRANSACTION_STATUS.NOT_FOUND; + + const code = Number(JSON.parse(stdout).code); + return code ? TRANSACTION_STATUS.FAILED : TRANSACTION_STATUS.SUCCESSFUL; +}; + +/** + * @param {string} address + * @param {string} clientType + * @returns {Promise} + */ +export const provisionAddress = async (address, clientType) => { + let { exitCode, stderr } = await nothrow($`\ + ${agBinary} tx swingset provision-one faucet_provision ${address} ${clientType} \ + --broadcast-mode=block \ + --chain-id=${chainId} \ + --from=${FAUCET_KEYNAME} \ + --keyring-backend=test \ + --keyring-dir=${agoricHome} \ + --node=http://localhost:${RPC_PORT} \ + --yes \ + `); + exitCode = exitCode ?? 1; + + if (exitCode) + console.log( + `Failed to provision address "${address}" of type "${clientType}" with error message: ${stderr}`, + ); +}; + +/** + * @param {string} address + * @param {string} clientType + * @param {string} txHash + * @returns {Promise} + */ +export const pollForProvisioning = async (address, clientType, txHash) => { + const status = await getTransactionStatus(txHash); + status === TRANSACTION_STATUS.NOT_FOUND + ? setTimeout(() => pollForProvisioning(address, clientType, txHash), 2000) + : status === TRANSACTION_STATUS.SUCCESSFUL + ? await provisionAddress(address, clientType) + : console.log( + `Not provisioning address "${address}" of type "${clientType}" as transaction "${txHash}" failed`, + ); +}; + +/** + * Send funds to `address`. + * It only waits for the transaction + * checks and doesn't wait for the + * transaction to actually be included + * in a block. The returned transaction + * hash can be used to get the current status + * of the transaction later + * @param {string} address + * @param {string} amount + * @returns {Promise<[number, string]>} + */ +export const sendFunds = async (address, amount) => { + let { exitCode, stdout } = await nothrow($`\ + ${agBinary} tx bank send ${FAUCET_KEYNAME} ${address} ${amount} \ + --broadcast-mode=sync \ + --chain-id=${chainId} \ + --keyring-backend=test \ + --keyring-dir=${agoricHome} \ + --node=http://localhost:${RPC_PORT} \ + --output=json \ + --yes \ + `); + exitCode = exitCode ?? 1; + + if (exitCode) return [exitCode, '']; + return [exitCode, String(JSON.parse(stdout).txhash)]; +}; + +export class DataCache { + constructor(fetchFunction, minutesToLive = 10) { + this.millisecondsToLive = minutesToLive * 60 * 1000; + this.fetchFunction = fetchFunction; + this.cache = null; + this.getData = this.getData.bind(this); + this.resetCache = this.resetCache.bind(this); + this.isCacheExpired = this.isCacheExpired.bind(this); + this.fetchDate = new Date(0); + } + + isCacheExpired() { + return ( + this.fetchDate.getTime() + this.millisecondsToLive < new Date().getTime() + ); + } + + getData() { + if (!this.cache || this.isCacheExpired()) { + console.log('fetch'); + return this.fetchFunction().then(data => { + this.cache = data; + this.fetchDate = new Date(); + return data; + }); + } else { + console.log('cache hit'); + + return Promise.resolve(this.cache); + } + } + + resetCache() { + this.fetchDate = new Date(0); + } +} + +export const constructAmountToSend = (amount, denoms) => + denoms.map(denom => `${amount}${denom}`).join(','); + +export const getDenoms = async () => { + // Not handling pagination as it is used for testing. Limit 100 shoud suffice + const result = await $`${agBinary} query bank total --limit=100 -o json`; + const output = JSON.parse(result.stdout.trim()); + return output.supply.map(element => element.denom); +}; + +export const logRequest = (req, res, next) => { + const time = Date.now(); + res.on('finish', () => { + console.log( + JSON.stringify({ + time, + dur: Date.now() - time, + method: req.method, + forwarded: req.get('X-Forwarded-For'), + ip: req.ip, + url: req.originalUrl, + status: res.statusCode, + }), + ); + }); + next(); +}; + +export const getRevisionValue = async () => { + if (FAKE) { + return 'fake_revision'; + } + + return AG0_MODE + ? 'ag0' + : fs + .readFileSync( + '/usr/src/agoric-sdk/packages/solo/public/git-revision.txt', + { + encoding: 'utf8', + flag: 'r', + }, + ) + .trim(); +}; + +export const getDockerImageValue = async (namespace, podname) => { + if (FAKE) { + return 'asdf:unknown'; + } + + const statefulSet = await makeKubernetesRequest( + `/apis/apps/v1/namespaces/${namespace}/statefulsets/${podname}`, + ); + return statefulSet.spec.template.spec.containers[0].image; +}; + +export const createTempDir = async () => { + return new Promise((resolve, reject) => { + tmp.dir({ prefix: 'faucet', postfix: 'home' }, (err, path) => { + if (err) { + return reject(err); + } + resolve(path); + }); + }); +}; + +export const addKey = async (directory, keyName) => { + await $`${agBinary} --home=${directory} keys --keyring-backend=test add ${keyName}`; +};