From b8f3b3411bd54c8c9587d2b52d05a9b023fa80df Mon Sep 17 00:00:00 2001 From: kidneyweak <35759909+kidneyweakx@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:06:51 +0800 Subject: [PATCH 1/7] refactor: replace all then to promise (#96) --- test/fabric/service/caService.test.ts | 130 ++++++++++++-------------- test/fabric/service/orderer.test.ts | 22 ++--- test/fabric/service/peer.test.ts | 24 ++--- 3 files changed, 73 insertions(+), 103 deletions(-) diff --git a/test/fabric/service/caService.test.ts b/test/fabric/service/caService.test.ts index b9c48ce9..bb2ccd01 100644 --- a/test/fabric/service/caService.test.ts +++ b/test/fabric/service/caService.test.ts @@ -202,49 +202,37 @@ describe('Fabric.CA', function () { fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) }) - it('up & down', (done) => { - caService.up(rcaArgv).then(() => { - const socket = net.connect(rcaArgv.basic.port, '127.0.0.1', () => { - caService.down({ caName: rcaArgv.basic.caName }) - .then(() => { - // TODO check container - done() - }) - .catch((err) => { - assert.fail(`ca down error: ${err.message}`) - }) + it('up & down', async () => { + await caService.up(rcaArgv) + await new Promise((resolve, reject) => { + const socket = net.connect(rcaArgv.basic.port, '127.0.0.1', async () => { + await caService.down({ caName: rcaArgv.basic.caName }) + resolve() }) - socket.on('error', (err) => { - assert.fail(`ca connect test error: ${err.message}`) + reject(new Error(`ca connect test error: ${err.message}`)) }) - }).catch((err) => { - assert.fail(`ca up error: ${err.message}`) }) }) }) describe('Fabric.CA.enrollICA', function () { - before((done) => { - caService.up(rcaArgv).then(async () => { - await sleep(1000) - const socket = net.connect(rcaArgv.basic.port, '127.0.0.1', () => { - done() + before(async () => { + await caService.up(rcaArgv) + await new Promise((resolve, reject) => { + const socket = net.connect(rcaArgv.basic.port, '127.0.0.1', async () => { + await sleep(1000) + resolve() }) socket.on('error', (err) => { - assert.fail(`rca connect test error: ${err.message}`) + reject(new Error(`rca connect test error: ${err.message}`)) }) - }).catch((err) => { - assert.fail(`rca up error: ${err.message}`) }) }) - after((done) => { - caService.down({ caName: rcaArgv.basic.caName }) - .then(() => { - fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) - done() - }).catch((err) => { assert.fail(`rca down error: ${err.message}`) }) + after(async () => { + await caService.down({ caName: rcaArgv.basic.caName }) + fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) }) it('enroll client', async () => { @@ -272,32 +260,32 @@ describe('Fabric.CA', function () { describe('Fabric.CA.enrollOrg', function () { this.timeout(60000) - before((done) => { - caService.up(rcaArgv).then(() => { + before(async () => { + await caService.up(rcaArgv) + await new Promise((resolve, reject) => { const socket = net.connect(rcaArgv.basic.port, '127.0.0.1', async () => { await sleep(1000) - caService.enroll(enrollRcaClientArgv).then(() => { - caService.register(registerIcaArgv).then(() => { - caService.up(icaArgv).then(() => { - const socket = net.connect(icaArgv.basic.port, '127.0.0.1', () => { - done() - }) - socket.on('error', (err) => { assert.fail(`ica connect test error: ${err.message}`) }) - }).catch((err) => { assert.fail(`ica up error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca register ica error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca enroll client error: ${err.message}`) }) + await caService.enroll(enrollRcaClientArgv) + await caService.register(registerIcaArgv) + await caService.up(icaArgv) + const socket = net.connect(icaArgv.basic.port, '127.0.0.1', () => { + resolve() + }) + socket.on('error', (err) => { + reject(new Error(`ica connect test error: ${err.message}`)) + }) + resolve() + }) + socket.on('error', (err) => { + reject(new Error(`rca connect test error: ${err.message}`)) }) - socket.on('error', (err) => { assert.fail(`rca connect test error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca up error: ${err.message}`) }) + }) }) - after((done) => { - caService.down({ caName: rcaArgv.basic.caName }).then(() => { - caService.down({ caName: icaArgv.basic.caName }).then(() => { - fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) - done() - }).catch((err) => { assert.fail(`ica down error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca down error: ${err.message}`) }) + after(async () => { + await caService.down({ caName: rcaArgv.basic.caName }) + await caService.down({ caName: icaArgv.basic.caName }) + fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) }) it('enroll && register orderer', async () => { @@ -382,32 +370,30 @@ describe('Fabric.CA', function () { describe('Fabric.CA.reenroll', function () { this.timeout(60000) - before((done) => { - caService.up(rcaArgv).then(() => { + before(async () => { + await caService.up(rcaArgv) + await new Promise((resolve, reject) => { const socket = net.connect(rcaArgv.basic.port, '127.0.0.1', async () => { await sleep(1000) - caService.enroll(enrollRcaClientArgv).then(() => { - caService.register(registerIcaArgv).then(() => { - caService.up(icaArgv).then(() => { - const socket = net.connect(icaArgv.basic.port, '127.0.0.1', () => { - done() - }) - socket.on('error', (err) => { assert.fail(`ica connect test error: ${err.message}`) }) - }).catch((err) => { assert.fail(`ica up error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca register ica error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca enroll client error: ${err.message}`) }) + await caService.enroll(enrollRcaClientArgv) + await caService.register(registerIcaArgv) + await caService.up(icaArgv) + const socket = net.connect(icaArgv.basic.port, '127.0.0.1', async () => { + await sleep(1000) + resolve() + }) + socket.on('error', (err) => { reject(new Error(`ica connect test error: ${err.message}`)) }) }) - socket.on('error', (err) => { assert.fail(`rca connect test error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca up error: ${err.message}`) }) - }) + socket.on('error', (err) => { + reject(new Error(`rca connect test error: ${err.message}`)) + }) + }) - after((done) => { - caService.down({ caName: rcaArgv.basic.caName }).then(() => { - caService.down({ caName: icaArgv.basic.caName }).then(() => { - fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) - done() - }).catch((err) => { assert.fail(`ica down error: ${err.message}`) }) - }).catch((err) => { assert.fail(`rca down error: ${err.message}`) }) + after(async () => { + await caService.down({ caName: rcaArgv.basic.caName }) + await caService.down({ caName: icaArgv.basic.caName }) + fs.rmSync(`${config.infraConfig.bdkPath}/${config.networkName}`, { recursive: true }) + }) }) it('reenroll orderer ca', async () => { diff --git a/test/fabric/service/orderer.test.ts b/test/fabric/service/orderer.test.ts index 8bd2f7c1..f8fd600b 100644 --- a/test/fabric/service/orderer.test.ts +++ b/test/fabric/service/orderer.test.ts @@ -32,27 +32,19 @@ describe('Orderer service:', function () { await minimumNetwork.deleteNetwork() }) - it('should start and shutdown docker container', (done) => { + it('should start and shutdown docker container', async () => { const ordererHostname = `${minimumNetwork.getOrderer().hostname}.${minimumNetwork.getOrderer().orgDomain}` const port = minimumNetwork.getOrderer().port - ordererService.up({ ordererHostname }).then(() => { - const socket = net.connect(port, '127.0.0.1', () => { - ordererService.down({ ordererHostname }) - .then(() => { - // TODO check container - done() - }) - .catch((err) => { - assert.fail(`orderer down error: ${err.message}`) - }) + await ordererService.up({ ordererHostname }) + await new Promise((resolve, reject) => { + const socket = net.connect(port, '127.0.0.1', async () => { + await ordererService.down({ ordererHostname }) + resolve() }) - socket.on('error', (err) => { - assert.fail(`orderer connect test error: ${err.message}`) + reject(new Error(`orderer down error: ${err.message}`)) }) - }).catch((err) => { - assert.fail(`orderer up error: ${err.message}`) }) }) }) diff --git a/test/fabric/service/peer.test.ts b/test/fabric/service/peer.test.ts index f602d762..cb144e1c 100644 --- a/test/fabric/service/peer.test.ts +++ b/test/fabric/service/peer.test.ts @@ -34,27 +34,19 @@ describe('Peer service:', function () { await minimumNetwork.deleteNetwork() }) - it('should start and shutdown docker container', (done) => { + it('should start and shutdown docker container', async () => { const peerHostname = `${minimumNetwork.getPeer().hostname}.${minimumNetwork.getPeer().orgDomain}` const port = minimumNetwork.getPeer().port - peerService.up({ peerHostname }).then(() => { - const socket = net.connect(port, '127.0.0.1', () => { - peerService.down({ peerHostname }) - .then(() => { - // TODO check container - done() - }) - .catch((err) => { - assert.fail(`peer down error: ${err.message}`) - }) + await peerService.up({ peerHostname }) + await new Promise((resolve, reject) => { + const socket = net.connect(port, '127.0.0.1', async () => { + await peerService.down({ peerHostname }) + resolve() }) - - socket.on('error', (err) => { - assert.fail(`peer connect test error: ${err.message}`) + socket.on('error', (err: any) => { + reject(new Error(`orderer down error: ${err.message}`)) }) - }).catch((err) => { - assert.fail(`peer up error: ${err.message}`) }) }) }) From 3d58125e19725ce9a3d1259a42b8fc7d739d98e7 Mon Sep 17 00:00:00 2001 From: Chengwei-Lin <39145342+LinXJ1204@users.noreply.github.com> Date: Wed, 17 Jan 2024 15:59:29 +0800 Subject: [PATCH 2/7] feat: bootnode mode (#97) * feat: bootnode mode * feat: assign specified node to be bootnode * fix: remove console log * fix: eslink and test * fix: adjust for pr comments --- .gitignore | 3 ++ src/quorum/command/network/create.ts | 43 ++++++++++++++++++- src/quorum/model/defaultNetworkConfig.ts | 2 + src/quorum/model/type/network.type.ts | 2 + .../docker-compose/memberDockerCompose.ts | 4 +- .../validatorDockerComposeYaml.ts | 4 +- src/quorum/service/network.ts | 17 +++++--- test/quorum/service/backup.test.ts | 2 + test/quorum/service/network.test.ts | 4 ++ 9 files changed, 70 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 78457c88..10c24a61 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ src/command/test.ts .scannerwork + +# Pnpm lockfile +pnpm-lock.yaml \ No newline at end of file diff --git a/src/quorum/command/network/create.ts b/src/quorum/command/network/create.ts index eaea7748..b9947cec 100644 --- a/src/quorum/command/network/create.ts +++ b/src/quorum/command/network/create.ts @@ -76,6 +76,47 @@ export const handler = async (argv: Arguments) => { }, ], { onCancel }) + const { isBootNode } = await prompts({ + type: 'select', + name: 'isBootNode', + message: 'Using bootnode?', + choices: [ + { + title: 'true', + value: true, + }, + { + title: 'false', + value: false, + }, + ], + initial: 1, + }) + + const createNode = (type: string, index: number, offset = 0) => ({ + title: `${type}${index}`, + value: `${index + offset}`, + }) + + const nodelist = [ + ...Array.from({ length: validatorNumber }, (_, i) => createNode('validator', i)), + ...Array.from({ length: memberNumber }, (_, i) => createNode('member', i, validatorNumber)), + ] + + const bootNodeList: boolean[] = Array(validatorNumber + memberNumber).fill(false) + if (isBootNode) { + const isbootNodeList: any = await prompts({ + type: 'multiselect', + name: 'isbootNodeList', + message: 'Choose bootnode', + choices: nodelist, + initial: '', + }) + Object.values(isbootNodeList).flat().forEach((node: any) => { + bootNodeList[node] = true + }) + } + const { walletOwner } = await prompts({ type: 'select', name: 'walletOwner', @@ -122,7 +163,7 @@ export const handler = async (argv: Arguments) => { amount: '1000000000000000000000000000', }] - return { chainId, validatorNumber, memberNumber, alloc } + return { chainId, validatorNumber, memberNumber, alloc, isBootNode, bootNodeList } } else { const { address, privateKey } = wallet.createWalletAddress(WalletType.ETHEREUM) return defaultNetworkConfig(address, privateKey) diff --git a/src/quorum/model/defaultNetworkConfig.ts b/src/quorum/model/defaultNetworkConfig.ts index 9426b8eb..4474844e 100644 --- a/src/quorum/model/defaultNetworkConfig.ts +++ b/src/quorum/model/defaultNetworkConfig.ts @@ -19,6 +19,8 @@ export function defaultNetworkConfig (address: string, privateKey: string) { account: address, amount: '1000000000000000000000000000', }], + isBootNode: false, + bootNodeList: [], } return networkConfig diff --git a/src/quorum/model/type/network.type.ts b/src/quorum/model/type/network.type.ts index 80f7b8a2..8732d653 100644 --- a/src/quorum/model/type/network.type.ts +++ b/src/quorum/model/type/network.type.ts @@ -18,6 +18,8 @@ export interface NetworkCreateType { validatorNumber: number memberNumber: number alloc: AllocType[] + isBootNode: boolean + bootNodeList: boolean[] } export interface NetworkGenerateType { diff --git a/src/quorum/model/yaml/docker-compose/memberDockerCompose.ts b/src/quorum/model/yaml/docker-compose/memberDockerCompose.ts index ee40fafe..68f4104f 100644 --- a/src/quorum/model/yaml/docker-compose/memberDockerCompose.ts +++ b/src/quorum/model/yaml/docker-compose/memberDockerCompose.ts @@ -1,7 +1,7 @@ import DockerComposeYaml from './dockerComposeYaml' class MemberDockerComposeYaml extends DockerComposeYaml { - public addMember (bdkPath: string, memberNum: number, rpcPort: number, chainId: number, peerPort: number) { + public addMember (bdkPath: string, memberNum: number, rpcPort: number, chainId: number, peerPort: number, bootnode: boolean, nodeEncode: string) { this.addNetwork('quorum', {}) this.addService(`member${memberNum}`, { image: 'quorumengineering/quorum:23.4.0', @@ -20,7 +20,7 @@ class MemberDockerComposeYaml extends DockerComposeYaml { volumes: [`${bdkPath}/member${memberNum}/data/:/data`], entrypoint: [ '/bin/sh', '-c', - `geth init --datadir /data /data/genesis.json; geth --datadir /data --networkid ${chainId} --nodiscover --verbosity 3 --syncmode full --nousb --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" --http.vhosts "*" --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins "*" --http.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --ws.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --port ${peerPort}`, + `geth init --datadir /data /data/genesis.json; geth --datadir /data --networkid ${chainId} ${(bootnode) ? '--bootnodes '.concat(nodeEncode) : '--nodiscover'} --verbosity 3 --syncmode full --nousb --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" --http.vhosts "*" --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins "*" --http.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --ws.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --port ${peerPort}`, ], }) } diff --git a/src/quorum/model/yaml/docker-compose/validatorDockerComposeYaml.ts b/src/quorum/model/yaml/docker-compose/validatorDockerComposeYaml.ts index e451b27a..20fc2257 100644 --- a/src/quorum/model/yaml/docker-compose/validatorDockerComposeYaml.ts +++ b/src/quorum/model/yaml/docker-compose/validatorDockerComposeYaml.ts @@ -1,7 +1,7 @@ import DockerComposeYaml from './dockerComposeYaml' class ValidatorDockerComposeYaml extends DockerComposeYaml { - public addValidator (bdkPath: string, validatorNum: number, rpcPort: number, chainId: number, peerPort: number) { + public addValidator (bdkPath: string, validatorNum: number, rpcPort: number, chainId: number, peerPort: number, bootnode: boolean, nodeEncode: string) { this.addNetwork('quorum', {}) this.addService(`validator${validatorNum}`, { image: 'quorumengineering/quorum:23.4.0', @@ -20,7 +20,7 @@ class ValidatorDockerComposeYaml extends DockerComposeYaml { volumes: [`${bdkPath}/validator${validatorNum}/data/:/data`], entrypoint: [ '/bin/sh', '-c', - `geth init --datadir /data /data/genesis.json; geth --datadir /data --networkid ${chainId} --nodiscover --verbosity 3 --syncmode full --nousb --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" --http.vhosts "*" --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins "*" --http.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --ws.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --port ${peerPort} `, + `geth init --datadir /data /data/genesis.json; geth --datadir /data --networkid ${chainId} --verbosity 3 --syncmode full --nousb --mine --miner.threads 1 --miner.gasprice 0 --emitcheckpoints --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" ${(bootnode) ? '--bootnodes '.concat(nodeEncode) : '--nodiscover'} --http.vhosts "*" --ws --ws.addr 0.0.0.0 --ws.port 8546 --ws.origins "*" --http.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --ws.api admin,trace,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul,qbft --port ${peerPort} `, ], }) } diff --git a/src/quorum/service/network.ts b/src/quorum/service/network.ts index 7c67f5fa..6d92eaa4 100644 --- a/src/quorum/service/network.ts +++ b/src/quorum/service/network.ts @@ -72,7 +72,7 @@ export default class Network extends AbstractService { this.bdkFile.copyPublicKeyToValidator(i) this.bdkFile.copyAddressToValidator(i) - validatorDockerComposeYaml.addValidator(bdkPath, i, 8545 + i * 2, networkCreateConfig.chainId, 30303 + i) + validatorDockerComposeYaml.addValidator(bdkPath, i, 8545 + i * 2, networkCreateConfig.chainId, 30303 + i, networkCreateConfig.bootNodeList[i], staticNodesJson[i]) } this.bdkFile.createValidatorDockerComposeYaml(validatorDockerComposeYaml) @@ -90,7 +90,7 @@ export default class Network extends AbstractService { this.bdkFile.copyPublicKeyToMember(i) this.bdkFile.copyAddressToMember(i) - memberDockerComposeYaml.addMember(bdkPath, i, 8645 + i * 2, networkCreateConfig.chainId, 30403 + i) + memberDockerComposeYaml.addMember(bdkPath, i, 8645 + i * 2, networkCreateConfig.chainId, 30403 + i, networkCreateConfig.bootNodeList[networkCreateConfig.validatorNumber + i], staticNodesJson[networkCreateConfig.validatorNumber + i]) } this.bdkFile.createMemberDockerComposeYaml(memberDockerComposeYaml) @@ -125,7 +125,9 @@ export default class Network extends AbstractService { this.bdkFile.copyPermissionedNodesJsonToValidator(nodeNum) const validatorDockerComposeYaml = new ValidatorDockerComposeYaml() - validatorDockerComposeYaml.addValidator(bdkPath, nodeNum, 8545 + nodeNum * 2, joinNodeConfig.genesisJson.config.chainId, 30303 + nodeNum) + + // TODO: add bootnode selection + validatorDockerComposeYaml.addValidator(bdkPath, nodeNum, 8545 + nodeNum * 2, joinNodeConfig.genesisJson.config.chainId, 30303 + nodeNum, false, '') this.bdkFile.createValidatorDockerComposeYaml(validatorDockerComposeYaml) await (new ValidatorInstance(this.config, this.infra).upOneService(`${joinNodeConfig.node}`)) @@ -156,7 +158,8 @@ export default class Network extends AbstractService { this.bdkFile.copyPermissionedNodesJsonToMember(nodeNum) const memberDockerComposeYaml = new MemberDockerComposeYaml() - memberDockerComposeYaml.addMember(bdkPath, nodeNum, 8645 + nodeNum * 2, joinNodeConfig.genesisJson.config.chainId, 30403 + nodeNum) + // TODO: add bootnode selection + memberDockerComposeYaml.addMember(bdkPath, nodeNum, 8645 + nodeNum * 2, joinNodeConfig.genesisJson.config.chainId, 30403 + nodeNum, false, '') this.bdkFile.createMemberDockerComposeYaml(memberDockerComposeYaml) await (new MemberInstance(this.config, this.infra).upOneService(`${joinNodeConfig.node}`)) @@ -285,7 +288,8 @@ export default class Network extends AbstractService { for (let i = 0; i < validatorNum + 1; i += 1) { this.bdkFile.copyStaticNodesJsonToValidator(i) this.bdkFile.copyPermissionedNodesJsonToValidator(i) - validatorDockerComposeYaml.addValidator(this.bdkFile.getBdkPath(), i, 8545 + i * 2, chainId, 30303 + i) + // TODO: add bootnode selection + validatorDockerComposeYaml.addValidator(this.bdkFile.getBdkPath(), i, 8545 + i * 2, chainId, 30303 + i, false, '') } this.bdkFile.createValidatorDockerComposeYaml(validatorDockerComposeYaml) @@ -353,7 +357,8 @@ export default class Network extends AbstractService { for (let i = 0; i < memberCount + 1; i += 1) { this.bdkFile.copyStaticNodesJsonToMember(i) this.bdkFile.copyPermissionedNodesJsonToMember(i) - memberDockerComposeYaml.addMember(this.bdkFile.getBdkPath(), i, 8645 + i * 2, chainId, 30403 + i) + // TODO: add bootnode selection + memberDockerComposeYaml.addMember(this.bdkFile.getBdkPath(), i, 8645 + i * 2, chainId, 30403 + i, false, '') } this.bdkFile.createMemberDockerComposeYaml(memberDockerComposeYaml) diff --git a/test/quorum/service/backup.test.ts b/test/quorum/service/backup.test.ts index 7079be01..0c7aeef8 100644 --- a/test/quorum/service/backup.test.ts +++ b/test/quorum/service/backup.test.ts @@ -24,6 +24,8 @@ describe('Quorum.Backup', function () { validatorNumber: 1, memberNumber: 1, alloc: [], + isBootNode: false, + bootNodeList: [], } await network.create(networkCreate) }) diff --git a/test/quorum/service/network.test.ts b/test/quorum/service/network.test.ts index 479242bf..ca30be37 100644 --- a/test/quorum/service/network.test.ts +++ b/test/quorum/service/network.test.ts @@ -30,6 +30,8 @@ describe.skip('Quorum.Network.Service', function () { account: address, amount: '1000000000000000000000000000', }], + isBootNode: false, + bootNodeList: [false, false, false, false], } before(async () => { @@ -85,6 +87,8 @@ describe.skip('Quorum.Network.Service', function () { memberNumber: 0, chainId: 1234, alloc: [], + isBootNode: false, + bootNodeList: [false, false], } await network.create(onlyMemberConfig) const upContainers = await docker.listContainers(dockerdOption) From 331810981c43692014f4150616a03da8d5c837df Mon Sep 17 00:00:00 2001 From: kidneyweak <35759909+kidneyweakx@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:51:05 +0800 Subject: [PATCH 3/7] feat: optimize create network process (#100) --- src/quorum/command/network/create.ts | 9 +- src/quorum/command/network/generate.ts | 9 +- src/quorum/instance/bdkFile.ts | 12 +- src/quorum/model/type/network.type.ts | 5 + src/quorum/service/network.ts | 156 ++++++++++++++----------- src/wallet/service/wallet.ts | 1 - 6 files changed, 116 insertions(+), 76 deletions(-) diff --git a/src/quorum/command/network/create.ts b/src/quorum/command/network/create.ts index b9947cec..5b8d34c9 100644 --- a/src/quorum/command/network/create.ts +++ b/src/quorum/command/network/create.ts @@ -2,6 +2,7 @@ import { ethers } from 'ethers' import prompts from 'prompts' import { Argv, Arguments } from 'yargs' import Network from '../../service/network' +import Backup from '../../service/backup' import { onCancel } from '../../../util/error' import { NetworkCreateType } from '../../model/type/network.type' import config from '../../config' @@ -26,11 +27,12 @@ export const builder = (yargs: Argv) => { export const handler = async (argv: Arguments) => { const network = new Network(config) + const backup = new Backup(config) const wallet = new Wallet() // check bdkPath files exist or not (include useless file e.g. .DS_Store) const confirm: boolean = await (async () => { network.createBdkFolder() - const fileList = network.getBdkFiles() + const fileList = network.getNetworkFiles() if (fileList.length !== 0) { const confirmDelete = (await prompts({ type: 'confirm', @@ -40,8 +42,11 @@ export const handler = async (argv: Arguments) => { }, { onCancel })).value if (confirmDelete) { const spinner = ora('Quorum Network Create ...').start() + // backup before remove + await backup.exportAll() + network.removeBdkFiles(fileList) - spinner.succeed('Remove all existing files!') + spinner.succeed('Backup and remove all existing files!') } return confirmDelete } else { diff --git a/src/quorum/command/network/generate.ts b/src/quorum/command/network/generate.ts index 20caf867..4f003e66 100644 --- a/src/quorum/command/network/generate.ts +++ b/src/quorum/command/network/generate.ts @@ -1,6 +1,7 @@ import prompts from 'prompts' import { Argv, Arguments } from 'yargs' import Network from '../../service/network' +import Backup from '../../service/backup' import { onCancel, ParamsError } from '../../../util/error' import { NetworkGenerateType } from '../../model/type/network.type' import config from '../../config' @@ -22,6 +23,7 @@ export const builder = (yargs: Argv) => { export const handler = async (argv: Arguments) => { const network = new Network(config) + const backup = new Backup(config) // check bdkPath files exist or not (include useless file e.g. .DS_Store) const confirm: boolean = await (async () => { network.createBdkFolder() @@ -34,9 +36,12 @@ export const handler = async (argv: Arguments) => { initial: false, }, { onCancel })).value if (confirmDelete) { - const spinner = ora('Quorum Network Generate ...').start() + const spinner = ora('Quorum Network Create ...').start() + // backup before remove + await backup.exportAll() + network.removeBdkFiles(fileList) - spinner.succeed('Remove all existing files!') + spinner.succeed('Backup and remove all existing files!') } return confirmDelete } else { diff --git a/src/quorum/instance/bdkFile.ts b/src/quorum/instance/bdkFile.ts index 0d01f31c..1978c52b 100644 --- a/src/quorum/instance/bdkFile.ts +++ b/src/quorum/instance/bdkFile.ts @@ -1,7 +1,7 @@ import ExplorerDockerComposeYaml from '../model/yaml/docker-compose/explorerDockerComposeYaml' import fs from 'fs-extra' import { Config } from '../config' -import { GenesisJsonType } from '../model/type/network.type' +import { GenesisJsonType, NetworkInfoItem } from '../model/type/network.type' import ValidatorDockerComposeYaml from '../model/yaml/docker-compose/validatorDockerComposeYaml' import MemberDockerComposeYaml from '../model/yaml/docker-compose/memberDockerCompose' import { PathError } from '../../util/error' @@ -50,6 +50,10 @@ export default class BdkFile { fs.writeFileSync(`${this.bdkPath}/artifacts/goQuorum/static-nodes.json`, JSON.stringify(staticNodesJson, null, 2)) } + public createNetworkInfoJson (networkInfoJson: Array) { + fs.writeFileSync(`${this.bdkPath}/network-info.json`, JSON.stringify(networkInfoJson, null, 2)) + } + public createPrivateKey (dir: string, privateKey: string) { fs.mkdirSync(`${this.bdkPath}/${dir}`, { recursive: true }) fs.writeFileSync(`${this.bdkPath}/${dir}/nodekey`, privateKey) @@ -217,6 +221,12 @@ export default class BdkFile { return JSON.parse(staticNodesJson) } + public getNetworkInfoJson () { + this.checkPathExist(this.bdkPath) + const networkInfoJson = fs.readFileSync(`${this.bdkPath}/network-info.json`, 'utf8') + return JSON.parse(networkInfoJson) + } + public getPermissionedNodesJson () { this.checkPathExist(this.bdkPath) const permissionedNodesJson = fs.readFileSync(`${this.bdkPath}/artifacts/goQuorum/permissioned-nodes.json`, 'utf8') diff --git a/src/quorum/model/type/network.type.ts b/src/quorum/model/type/network.type.ts index 8732d653..d5c9213c 100644 --- a/src/quorum/model/type/network.type.ts +++ b/src/quorum/model/type/network.type.ts @@ -98,3 +98,8 @@ export interface AddMemberRemoteType { discoveryPort: string ipAddress: string } + +export interface NetworkInfoItem { + label: string + value: string +} diff --git a/src/quorum/service/network.ts b/src/quorum/service/network.ts index 6d92eaa4..954bf8d0 100644 --- a/src/quorum/service/network.ts +++ b/src/quorum/service/network.ts @@ -1,6 +1,6 @@ import { ethers } from 'ethers' import RLP from 'rlp' -import { NetworkCreateType, NetworkGenerateType, JoinNodeType, AddValidatorRemoteType, AddMemberRemoteType } from '../model/type/network.type' +import { NetworkCreateType, NetworkGenerateType, JoinNodeType, AddValidatorRemoteType, AddMemberRemoteType, NetworkInfoItem } from '../model/type/network.type' import GenesisJsonYaml from '../model/yaml/network/gensisJson' import { AbstractService } from './Service.abstract' import ValidatorInstance from '../instance/validator' @@ -17,6 +17,7 @@ export default class Network extends AbstractService { */ public async create (networkCreateConfig: NetworkCreateType) { const validatorAddressList: Buffer[] = [] + const networkInfo: NetworkInfoItem[] = [] for (let i = 0; i < networkCreateConfig.validatorNumber; i += 1) { const { address } = this.createKey(`artifacts/validator${i}`) validatorAddressList.push(Buffer.from(address, 'hex')) @@ -44,7 +45,7 @@ export default class Network extends AbstractService { this.bdkFile.createGenesisJson(genesisJson) this.bdkFile.createDisallowedNodesJson([]) - const staticNodesJson = [] + const staticNodesJson: string[] = [] const bdkPath = this.bdkFile.getBdkPath() for (let i = 0; i < networkCreateConfig.validatorNumber; i += 1) { @@ -73,6 +74,7 @@ export default class Network extends AbstractService { this.bdkFile.copyAddressToValidator(i) validatorDockerComposeYaml.addValidator(bdkPath, i, 8545 + i * 2, networkCreateConfig.chainId, 30303 + i, networkCreateConfig.bootNodeList[i], staticNodesJson[i]) + this.createNetworkInfoJson(networkInfo, `http://validator${i}:${8545 + i * 2}`) } this.bdkFile.createValidatorDockerComposeYaml(validatorDockerComposeYaml) @@ -91,16 +93,19 @@ export default class Network extends AbstractService { this.bdkFile.copyAddressToMember(i) memberDockerComposeYaml.addMember(bdkPath, i, 8645 + i * 2, networkCreateConfig.chainId, 30403 + i, networkCreateConfig.bootNodeList[networkCreateConfig.validatorNumber + i], staticNodesJson[networkCreateConfig.validatorNumber + i]) + this.createNetworkInfoJson(networkInfo, `http://member${i}:${8645 + i * 2}`) } this.bdkFile.createMemberDockerComposeYaml(memberDockerComposeYaml) await (new MemberInstance(this.config, this.infra).up()) } + this.bdkFile.createNetworkInfoJson(networkInfo) } public async joinNode (joinNodeConfig: JoinNodeType) { + const networkInfo: NetworkInfoItem[] = this.bdkFile.getNetworkInfoJson() const nodeNum = Number(joinNodeConfig.node.replace(/(validator|member)+/g, '')) - const staticNodesJson = [] + const staticNodesJson: string[] = [] const bdkPath = this.bdkFile.getBdkPath() const enodeInfo = String(this.getNodeInfo(joinNodeConfig.node, 'enodeInfo')) const publicKey = String(this.getNodeInfo(joinNodeConfig.node, 'publicKey')) @@ -128,29 +133,14 @@ export default class Network extends AbstractService { // TODO: add bootnode selection validatorDockerComposeYaml.addValidator(bdkPath, nodeNum, 8545 + nodeNum * 2, joinNodeConfig.genesisJson.config.chainId, 30303 + nodeNum, false, '') + this.createNetworkInfoJson(networkInfo, `http://validator${nodeNum}:${8545 + nodeNum * 2}`) this.bdkFile.createValidatorDockerComposeYaml(validatorDockerComposeYaml) await (new ValidatorInstance(this.config, this.infra).upOneService(`${joinNodeConfig.node}`)) + this.bdkFile.createNetworkInfoJson(networkInfo) - let tryTime = 0 - while (await this.quorumCommand('istanbul.isValidator()', `${joinNodeConfig.node}`) !== 'true') { - if (tryTime !== 10) { - const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545') - const wallet = ethers.Wallet.createRandom().connect(provider) - const tx = { - to: '0x0000000000000000000000000000000000000000', - value: '0x0', - nonce: provider.getTransactionCount(wallet.getAddress(), 'latest'), - } - const receipt = await wallet.sendTransaction(tx) - await receipt.wait() + await this.istanbulIsValidator(joinNodeConfig.node) - tryTime += 1 - await sleep(500) - } else { - throw new TimeLimitError('[x] Time limit reached. Please check later.') - } - } return nodeNum } else { this.bdkFile.copyGenesisJsonToMember(nodeNum) @@ -160,9 +150,11 @@ export default class Network extends AbstractService { const memberDockerComposeYaml = new MemberDockerComposeYaml() // TODO: add bootnode selection memberDockerComposeYaml.addMember(bdkPath, nodeNum, 8645 + nodeNum * 2, joinNodeConfig.genesisJson.config.chainId, 30403 + nodeNum, false, '') + this.createNetworkInfoJson(networkInfo, `http://member${nodeNum}:${8645 + nodeNum * 2}`) this.bdkFile.createMemberDockerComposeYaml(memberDockerComposeYaml) await (new MemberInstance(this.config, this.infra).upOneService(`${joinNodeConfig.node}`)) + this.bdkFile.createNetworkInfoJson(networkInfo) let tryTime = 0 while (parseInt(await this.quorumCommand('net.peerCount', joinNodeConfig.node)) < 1) { @@ -230,7 +222,7 @@ export default class Network extends AbstractService { } public generate (networkGenerateConfig: NetworkGenerateType) { - const staticNodesJson = [] + const staticNodesJson: string[] = [] // Add node to static-nodes.json for (let i = 0; i < networkGenerateConfig.validatorNumber; i += 1) { @@ -259,6 +251,7 @@ export default class Network extends AbstractService { } public async addValidatorLocal () { + const networkInfo: NetworkInfoItem[] = await this.bdkFile.getNetworkInfoJson() // count validator number const validatorCount = parseInt(await this.quorumCommand('istanbul.getValidators().length', 'validator0')) const validatorNum = validatorCount @@ -290,8 +283,10 @@ export default class Network extends AbstractService { this.bdkFile.copyPermissionedNodesJsonToValidator(i) // TODO: add bootnode selection validatorDockerComposeYaml.addValidator(this.bdkFile.getBdkPath(), i, 8545 + i * 2, chainId, 30303 + i, false, '') + this.createNetworkInfoJson(networkInfo, `http://validator${i}:${8545 + i * 2}`) } this.bdkFile.createValidatorDockerComposeYaml(validatorDockerComposeYaml) + this.bdkFile.createNetworkInfoJson(networkInfo) // for loop to copy static-nodes.json to member const memberCount = await this.bdkFile.getExportFiles().filter(file => file.match(/(member)[0-9]+/g)).length @@ -301,30 +296,13 @@ export default class Network extends AbstractService { } await (new ValidatorInstance(this.config, this.infra).upOneService(`${newValidator}`)) + await this.istanbulIsValidator(newValidator) - let tryTime = 0 - while (await this.quorumCommand('istanbul.isValidator()', `${newValidator}`) !== 'true') { - if (tryTime !== 10) { - const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545') - const wallet = ethers.Wallet.createRandom().connect(provider) - const tx = { - to: '0x0000000000000000000000000000000000000000', - value: '0x0', - nonce: provider.getTransactionCount(wallet.getAddress(), 'latest'), - } - const receipt = await wallet.sendTransaction(tx) - await receipt.wait() - - tryTime += 1 - await sleep(500) - } else { - throw new TimeLimitError('[x] Time limit reached. Please check later.') - } - } return validatorNum } public async addMemberLocal () { + const networkInfo: NetworkInfoItem[] = await this.bdkFile.getNetworkInfoJson() // count member number const memberCount = (await this.bdkFile.getExportFiles().filter(file => file.match(/(member)[0-9]+/g))).length const memberNum = memberCount @@ -359,9 +337,10 @@ export default class Network extends AbstractService { this.bdkFile.copyPermissionedNodesJsonToMember(i) // TODO: add bootnode selection memberDockerComposeYaml.addMember(this.bdkFile.getBdkPath(), i, 8645 + i * 2, chainId, 30403 + i, false, '') + this.createNetworkInfoJson(networkInfo, `http://member${i}:${8645 + i * 2}`) } this.bdkFile.createMemberDockerComposeYaml(memberDockerComposeYaml) - + this.bdkFile.createNetworkInfoJson(networkInfo) await (new MemberInstance(this.config, this.infra).upOneService(`${newMember}`)) let tryTime = 0 @@ -377,29 +356,6 @@ export default class Network extends AbstractService { return memberNum } - public async upService (service: string) { - if (service.match(/validator[\w-]+/g)) { - await (new ValidatorInstance(this.config, this.infra).upOneService(service)) - } else if (service.match(/member[\w-]+/g)) { - await (new MemberInstance(this.config, this.infra).upOneService(service)) - } - } - - public async upAll () { - await (new ValidatorInstance(this.config, this.infra).up()) - await (new MemberInstance(this.config, this.infra).up()) - } - - public async down () { - await (new ValidatorInstance(this.config, this.infra).down()) - await (new MemberInstance(this.config, this.infra).down()) - } - - public async delete () { - await this.down() - this.removeBdkFiles(this.getNetworkFiles()) - } - public async checkNode (node: string, method: string) { const result = await this.quorumCommand(method, node) @@ -469,6 +425,32 @@ export default class Network extends AbstractService { return result } + /** + * @description network service + */ + public async upService (service: string) { + if (service.match(/validator[\w-]+/g)) { + await (new ValidatorInstance(this.config, this.infra).upOneService(service)) + } else if (service.match(/member[\w-]+/g)) { + await (new MemberInstance(this.config, this.infra).upOneService(service)) + } + } + + public async upAll () { + await (new ValidatorInstance(this.config, this.infra).up()) + await (new MemberInstance(this.config, this.infra).up()) + } + + public async down () { + await (new ValidatorInstance(this.config, this.infra).down()) + await (new MemberInstance(this.config, this.infra).down()) + } + + public async delete () { + await this.down() + this.removeBdkFiles(this.getNetworkFiles()) + } + /** @ignore */ private async quorumCommand (args: string, option: string) { const result = await this.infra.runCommand({ @@ -505,6 +487,42 @@ export default class Network extends AbstractService { return { privateKey, publicKey, address } } + /** @ignore */ + private createNetworkInfoJson (networkInfo: NetworkInfoItem[], endpoint: string) { + const match = endpoint.match(/((http[s]?):\/\/((validator|member)\d+)):(\d+)/) + if (!match) { + throw new Error('Invalid URL format') + } + const label = match[3] + const protocol = match[2] + const host = label.startsWith('validator') || label.startsWith('member') ? 'localhost' : label + const value = `${protocol}://${host}:${match[5]}` + networkInfo.push({ label, value }) + } + + /** @ignore */ + private async istanbulIsValidator (joinNode: string, retryTime = 10) { + let tryTime = 0 + while (await this.quorumCommand('istanbul.isValidator()', `${joinNode}`) !== 'true') { + if (tryTime !== retryTime) { + const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545') + const wallet = ethers.Wallet.createRandom().connect(provider) + const tx = { + to: '0x0000000000000000000000000000000000000000', + value: '0x0', + nonce: provider.getTransactionCount(wallet.getAddress(), 'latest'), + } + const receipt = await wallet.sendTransaction(tx) + await receipt.wait() + + tryTime += 1 + await sleep(500) + } else { + throw new TimeLimitError('[x] Time limit reached. Please check later.') + } + } + } + /** @ignore */ public createBdkFolder () { return this.bdkFile.createBdkFolder() @@ -517,11 +535,9 @@ export default class Network extends AbstractService { /** @ignore */ public getNetworkFiles () { - const array = [] - array.push(this.bdkFile.getExportFiles().filter(file => file.match(/(validator|member)+/g))) - array.push(this.bdkFile.getExportFiles().filter(file => file.match(/(artifacts)/g))) - const networkFilesList = array.flat() - + const networkFilesList = this.bdkFile.getExportFiles().filter( + file => file.match(/(validator|member|artifacts|network-info)+/g), + ) return networkFilesList } diff --git a/src/wallet/service/wallet.ts b/src/wallet/service/wallet.ts index f9d84741..aecd3e65 100644 --- a/src/wallet/service/wallet.ts +++ b/src/wallet/service/wallet.ts @@ -1,4 +1,3 @@ - import { ethers } from 'ethers' import { WalletCreateType, WalletType } from '../model/type/wallet.type' From 6768305be38c1cbcdab5955e0eb28c5ea658a9a1 Mon Sep 17 00:00:00 2001 From: JakeKuo Date: Mon, 29 Jan 2024 13:59:44 +0800 Subject: [PATCH 4/7] feat: transform bdk ui to quorum dashboard (#99) * feat: bdk ui dashboard * fix: modify reviewed code * fix(ui): network info json location --------- Co-authored-by: kidneyweak --- package-lock.json | 91 +++++++++++++++++++++++++++++ package.json | 1 + src/ui/components/dockerlogs.tsx | 50 ---------------- src/ui/components/nodeInfo.tsx | 32 ++++++++++ src/ui/components/peerInfo.tsx | 48 +++++++++++++++ src/ui/components/selectInput.tsx | 48 --------------- src/ui/components/status.tsx | 60 +++++++++++++++++++ src/ui/components/terminal.tsx | 56 ------------------ src/ui/models/type/ui.type.ts | 29 ++++----- src/ui/services/commandContext.ts | 77 ------------------------ src/ui/services/containerContext.ts | 32 ---------- src/ui/services/nodeContext.ts | 84 ++++++++++++++++++++++++++ src/ui/views/app.tsx | 55 +++++++++-------- 13 files changed, 362 insertions(+), 301 deletions(-) delete mode 100644 src/ui/components/dockerlogs.tsx create mode 100644 src/ui/components/nodeInfo.tsx create mode 100644 src/ui/components/peerInfo.tsx delete mode 100644 src/ui/components/selectInput.tsx create mode 100644 src/ui/components/status.tsx delete mode 100644 src/ui/components/terminal.tsx delete mode 100644 src/ui/services/commandContext.ts delete mode 100644 src/ui/services/containerContext.ts create mode 100644 src/ui/services/nodeContext.ts diff --git a/package-lock.json b/package-lock.json index c31bb2c9..8945f220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.0.5", "license": "Apache-2.0", "dependencies": { + "axios": "^1.6.5", "deep-object-diff": "^1.1.0", "dockerode": "^3.3.5", "dotenv": "^16.3.1", @@ -2354,6 +2355,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -2385,6 +2391,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2842,6 +2858,17 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comment-parser": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", @@ -3010,6 +3037,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -4006,6 +4041,25 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4028,6 +4082,19 @@ "node": ">=8.0.0" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -5597,6 +5664,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6536,6 +6622,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/package.json b/package.json index d405435d..fd4b0055 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "doc:create": "npx typedoc --readme none --out ./api-docs --packages ." }, "dependencies": { + "axios": "^1.6.5", "deep-object-diff": "^1.1.0", "dockerode": "^3.3.5", "dotenv": "^16.3.1", diff --git a/src/ui/components/dockerlogs.tsx b/src/ui/components/dockerlogs.tsx deleted file mode 100644 index 72399dda..00000000 --- a/src/ui/components/dockerlogs.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useState, useMemo, useEffect } from 'react' -import { Text, Box, Newline } from 'ink' -import ContainerContext from '../services/containerContext' -import { ContainerListProps } from '../models/type/ui.type' - -export default function DockerLogs () { - const containerContext = new ContainerContext() - const [containers, setContainers] = useState([]) - - useEffect(() => { - const fetchContainers = async () => { - try { - const result = await containerContext.getContainers() - if (result !== containers) setContainers(result) - } catch (error) { - console.error(`Error fetching containers: ${error}`) - } - } - - const interval = setInterval(fetchContainers, 3000) - return () => { - clearInterval(interval) - } - }, [containers]) - - const memoizedContainers = useMemo(() => containers, [containers]) - - return ( - - {memoizedContainers.map((container: any) => { - const statusColor = container.state === 'running' ? 'green' : 'red' - return ( - - - State: {container.state} - - Container ID: {container.id} - - - Name: {container.names.join(', ')} - - Image: {container.image} - - - - ) - })} - - ) -} diff --git a/src/ui/components/nodeInfo.tsx b/src/ui/components/nodeInfo.tsx new file mode 100644 index 00000000..b2a0612d --- /dev/null +++ b/src/ui/components/nodeInfo.tsx @@ -0,0 +1,32 @@ +import React, { useState, useEffect } from 'react' +import { Box, Text, Newline } from 'ink' +import { NodeDetails } from '../models/type/ui.type' +import { NodeContextService } from '../services/nodeContext' + +export default function NodeInfo (props: any) { + const apiUrl = props.apiUrl + const nodeInformationService = new NodeContextService(apiUrl) + const [nodeInfo, setNodeInfo] = useState() + + useEffect(() => { + const fetchData = async () => { + const res = await nodeInformationService.getNodeDetails() + setNodeInfo(res) + } + fetchData() + .then((response) => { return response }) + .catch((error) => { return error }) + }, [apiUrl]) + + return ( + + Node ID: {nodeInfo?.id} + + Node Name: {nodeInfo?.name} + + Enode: {nodeInfo?.enode} + + IP Address: {nodeInfo?.ip} + + ) +} diff --git a/src/ui/components/peerInfo.tsx b/src/ui/components/peerInfo.tsx new file mode 100644 index 00000000..11c6954b --- /dev/null +++ b/src/ui/components/peerInfo.tsx @@ -0,0 +1,48 @@ +import React, { useState, useEffect } from 'react' +import { Box, Text } from 'ink' +import { NodeContextService } from '../services/nodeContext' +import { PeerInformation } from '../models/type/ui.type' + +export default function PeerInfo (props: any) { + const apiUrl: string = props.apiUrl + const nodeInformationService = new NodeContextService(apiUrl) + const [jsonData, setJsonData] = useState([]) + + const fetchData = async () => { + const res: PeerInformation[] = await nodeInformationService.getNodePeers() + setJsonData(res) + } + useEffect(() => { + const intervalId = setInterval(() => { + fetchData() + .then((response) => { return response }) + .catch((error) => { return error }) + }, 2500) + + return () => clearInterval(intervalId) + }, [apiUrl, jsonData]) + + return ( + + + Peer Information + + + { jsonData?.map((item: PeerInformation, index: number) => { + return ( + + ID: {item.id} + + E-Node: {item.enode} + + Local Address: {item.network?.localAddress} + + Remote Address: {item.network?.remoteAddress} + + ) + }, + )} + + + ) +} diff --git a/src/ui/components/selectInput.tsx b/src/ui/components/selectInput.tsx deleted file mode 100644 index 9bff3f59..00000000 --- a/src/ui/components/selectInput.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useState } from 'react' -import { Text, Box } from 'ink' -import SelectInput from 'ink-select-input' -import CommandContext from '../services/commandContext' -import { debounce } from '../../util' - -export default function Select ({ setNetworkType }: any) { - const commandContext = new CommandContext() - - const [isCommandExecuting, setIsCommandExecuting] = useState(false) - const [items, setItems] = useState([ - { - label: 'Fabric', - value: 'bdk fabric', - }, - { - label: 'Quorum', - value: 'bdk quorum', - }, - ]) - - const selectChain = debounce((item: any) => { - setNetworkType(item.value) - }, 350) - - const handleCommand = async (item: any) => { - if (isCommandExecuting) return - setIsCommandExecuting(true) - await new Promise(resolve => setImmediate(resolve)) - - const commandList = await commandContext.makeItem(item.value) - if (commandList.length === 0) { - await commandContext.executeCommand(item.value) - } else setItems(commandList) - setIsCommandExecuting(false) - } - - return ( - <> - - Choose a type of network to deploy: - - - - - - ) -} diff --git a/src/ui/components/status.tsx b/src/ui/components/status.tsx new file mode 100644 index 00000000..eeae2e56 --- /dev/null +++ b/src/ui/components/status.tsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect, useLayoutEffect, memo } from 'react' +import { Box, Text, Newline } from 'ink' +import { NodeContextService } from '../services/nodeContext' +import { debounce } from '../../util' + +const NodeStatus = memo(function NodeStatus (props: any) { + const apiUrl = props.apiUrl + const nodeInformationService = new NodeContextService(apiUrl) + const [state, setState] = useState('shutdown') + const [stateColor, setStateColor] = useState('#FF000F') + const [block, setBlock] = useState(0) + const [peer, setPeer] = useState(0) + + const fetchData = async () => { + const res = await nodeInformationService.getBlocks() + setBlock(res) + } + + const fetchPeerData = async () => { + const res = await nodeInformationService.getPeers() + setPeer(res) + } + + const debouncedFetchData = debounce(fetchData, 2500) + const debouncedFetchPeerData = debounce(fetchPeerData, 2500) + + useLayoutEffect(() => { + const intervalId = setInterval(() => { + debouncedFetchData() + debouncedFetchPeerData() + }, 2500) + + return () => clearInterval(intervalId) + }, [apiUrl]) + + useEffect(() => { + if (block !== 0) { + setState('running') + setStateColor('#00FF19') + } else { + setState('shutdown') + setStateColor('#FF000F') + } + }, [block, peer]) + + return ( + + Node Status + + Status: {state} + + Blocks: {block} + + Peers: {peer} + + + ) +}, +) +export default NodeStatus diff --git a/src/ui/components/terminal.tsx b/src/ui/components/terminal.tsx deleted file mode 100644 index f68557b7..00000000 --- a/src/ui/components/terminal.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState, useMemo } from 'react' -import { Text, Box } from 'ink' -import CommandContext from '../services/commandContext' - -interface TerminalProps { - type: string -} - -export default function Terminal (props: TerminalProps) { - const commandContext = new CommandContext() - const [output, setOutput] = useState('') - const [loading, setLoading] = useState(false) - - useMemo(() => { - let isCancelled = false - - const fetchCommandHelp = async () => { - setLoading(true) - - try { - const result = await commandContext.getCommandHelp(props.type) - if (!isCancelled) { - setOutput(result) - } - } catch (error) { - console.error(`Error fetching command help: ${error}`) - } - - setLoading(false) - } - - setOutput('') - fetchCommandHelp().catch((error) => { - console.error(`Error fetching command help: ${error}`) - }) - - return () => { - isCancelled = true - } - }, [props.type]) - - return ( - - Command output: - - {loading ? ( - <> - ) : ( - - {output} - - )} - - - ) -} diff --git a/src/ui/models/type/ui.type.ts b/src/ui/models/type/ui.type.ts index c167c8cc..e9e6022c 100644 --- a/src/ui/models/type/ui.type.ts +++ b/src/ui/models/type/ui.type.ts @@ -1,18 +1,21 @@ -export interface ItemProps { - label: string - value: string +export interface NodeDetails { + id: string + name: string + enode: string + ip: string } -export interface CommandProps { - type: string +export interface PeerInformation { + id: string + enode: string + name: string + network: { + localAddress: string + remoteAddress: string + } } -export interface ContainerListProps { - id: string - names: string[] - image: string - status: string - state: string - created: number - ports?: string[] +export interface NodeListType { + value: string + label: string } diff --git a/src/ui/services/commandContext.ts b/src/ui/services/commandContext.ts deleted file mode 100644 index da6421b1..00000000 --- a/src/ui/services/commandContext.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { exec, execSync } from 'child_process' -import { ItemProps } from '../models/type/ui.type' - -export default class CommandContext { - public getCommandContext (command: string): Promise<(string | undefined)[]> { - return new Promise((resolve, reject) => { - exec(`${command} --help`, (error, stdout) => { - if (error) { - reject(error.message) - return - } - - const regex = /(?::|:|คอมมาน)\n((?:\s+.*\n)+)/ - const match = regex.exec(stdout.toString()) - - const commandsRegex = this.makeRegex(command) - if (match) { - const commandsText = match[1] - const commands = - commandsText - .match(commandsRegex) - ?.map( - (match) => `${command} ${match.trim().split(/\s+/).pop()}`, - ) ?? [] - resolve(commands) - } else { - resolve([]) - } - }) - }) - } - - public getCommandHelp (command: string): Promise { - return new Promise((resolve, reject) => { - exec(`${command} --help`, (error, stdout) => { - if (error) { - reject(error.message) - return - } - resolve(stdout.toString()) - }) - }) - } - - public executeCommand (command: string): Promise { - return new Promise((resolve, reject) => { - exec(command, (error, stdout) => { - if (error) { - reject(error.message) - return - } - resolve(stdout.toString()) - }) - }) - } - - public async makeItem (command: string): Promise { - const commands = await this.getCommandContext(command) - // map commands to key value pair and ignore undefined - const items = commands.map((command) => { - if (command) return { label: command, value: command } - }) as ItemProps[] - return items - } - - private checkInteractive (command: string): boolean { - const text = execSync(`${command} --help`).toString() - const regex = /interactive/ - const match = regex.exec(text) - if (match) return true - return false - } - - private makeRegex (command: string): RegExp { - return new RegExp(`\\s+${command}\\s+([A-Za-z0-9]+)`, 'g') - } -} diff --git a/src/ui/services/containerContext.ts b/src/ui/services/containerContext.ts deleted file mode 100644 index bea3520e..00000000 --- a/src/ui/services/containerContext.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Dockerode from 'dockerode' -import { logger } from '../../util/logger' -import { DockerError } from '../../util/error' -import { ContainerListProps } from '../models/type/ui.type' - -export default class ContainerContext { - private dockerode: Dockerode - - constructor () { - this.dockerode = new Dockerode({ socketPath: '/var/run/docker.sock' }) - this.dockerode - .version() - .then((res) => logger.silly(`Use docker version: ${res.Version}`)) - .catch((e) => { - throw new DockerError(`[x] command [docker]: ${e.message}`) - }) - } - - public async getContainers (): Promise { - const containers = await this.dockerode.listContainers({ - all: true, - }) - return Promise.resolve(containers.map(container => ({ - id: container.Id, - names: container.Names, - image: container.Image, - status: container.Status, - state: container.State, - created: container.Created, - }))) - } -} diff --git a/src/ui/services/nodeContext.ts b/src/ui/services/nodeContext.ts new file mode 100644 index 00000000..b7da1fcc --- /dev/null +++ b/src/ui/services/nodeContext.ts @@ -0,0 +1,84 @@ +import fs from 'fs-extra' +import { NodeDetails, PeerInformation } from '../models/type/ui.type' +import axios from 'axios' + +export class NodeContextService { + private apiUrl: string + private bdkPath: string + constructor (apiUrl: string) { + this.apiUrl = apiUrl + this.bdkPath = process.env.BDK_PATH || `${process.env.HOME}/.bdk/quorum/bdk-quorum-network` + } + + public getNodeList () { + const nodeList = fs.readFileSync(`${this.bdkPath}/network-info.json`, 'utf-8') + return JSON.parse(nodeList) + } + + public async getBlocks () { + try { + const response = await axios.post( + this.apiUrl, + this.makeJsonRpcParam('eth_blockNumber'), + ) + const blockNumberHex = response.data.result + return parseInt(blockNumberHex, 16) + } catch (error) { + return 0 + } + } + + public async getPeers () { + try { + const response = await axios.post( + this.apiUrl, + this.makeJsonRpcParam('net_peerCount'), + ) + const peerCountHex = response.data.result + return parseInt(peerCountHex, 16) + } catch (error) { + return 0 + } + } + + public async getNodeDetails (): Promise { + try { + const response = await axios.post( + this.apiUrl, + this.makeJsonRpcParam('admin_nodeInfo'), + ) + return response.data.result as NodeDetails + } catch (error) { + return { id: 'NaN', name: 'NaN', enode: 'NaN', ip: 'NaN' } + } + } + + public async getNodePeers (): Promise { + try { + const response = await axios.post( + this.apiUrl, + this.makeJsonRpcParam('admin_peers'), + ) + return response.data.result + } catch (error) { + return [{ + id: 'NaN', + enode: 'NaN', + name: 'NaN', + network: { + localAddress: 'NaN', + remoteAddress: 'NaN', + }, + }] + } + } + + private makeJsonRpcParam (method: string, id = 1) { + return { + jsonrpc: '2.0', + method: method, + params: [], + id: id, + } + } +} diff --git a/src/ui/views/app.tsx b/src/ui/views/app.tsx index 5d53e572..e3affde3 100644 --- a/src/ui/views/app.tsx +++ b/src/ui/views/app.tsx @@ -1,10 +1,12 @@ -import React, { useState, useEffect, lazy, Suspense } from 'react' -import { Box, Text, useApp, useInput, useStdout } from 'ink' +import React, { useState, useEffect } from 'react' +import { Box, Newline, Text, useApp, useInput, useStdout } from 'ink' import Logo from '../components/logo' -import DockerLogs from '../components/dockerlogs' -import Select from '../components/selectInput' - -const Terminal = lazy(() => import('../components/terminal')) +import SelectInput from 'ink-select-input' +import NodeStatus from '../components/status' +import NodeInfo from '../components/nodeInfo' +import PeerInfo from '../components/peerInfo' +import { NodeContextService } from '../services/nodeContext' +import { NodeListType } from '../models/type/ui.type' export default function App () { const { exit } = useApp() @@ -50,32 +52,35 @@ export default function App () { } }, []) - const [networkType, setNetworkType] = useState('bdk fabric') + const nodeService = new NodeContextService('http://localhost:8545') + const NodeList = nodeService.getNodeList() + + const [nodeType, setNodeType] = useState(NodeList[0].value) + const [nodeName, setNodeName] = useState(NodeList[0].label) + const selectNode = (item: NodeListType) => { + setNodeType(item.value) + setNodeName(item.label) + } return ( - - Loading ....}> - - - - Press q to exit + + + + + Select Node: + + + + Current node: {nodeName} {nodeType} + + - - - -