From 4953d44569819f7a8f2094b76c402b4639aaa444 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Tue, 3 Dec 2024 13:06:19 +0100 Subject: [PATCH 1/9] List community RPC nodes --- .../validatorgroup/rpc-urls-l2.test.ts | 132 ++++++++++++++++++ .../commands/validatorgroup/rpc-urls.test.ts | 132 ++++++++++++++++++ .../src/commands/validatorgroup/rpc-urls.ts | 90 ++++++++++++ 3 files changed, 354 insertions(+) create mode 100644 packages/cli/src/commands/validatorgroup/rpc-urls-l2.test.ts create mode 100644 packages/cli/src/commands/validatorgroup/rpc-urls.test.ts create mode 100644 packages/cli/src/commands/validatorgroup/rpc-urls.ts diff --git a/packages/cli/src/commands/validatorgroup/rpc-urls-l2.test.ts b/packages/cli/src/commands/validatorgroup/rpc-urls-l2.test.ts new file mode 100644 index 000000000..88f3fd27f --- /dev/null +++ b/packages/cli/src/commands/validatorgroup/rpc-urls-l2.test.ts @@ -0,0 +1,132 @@ +import { newKitFromWeb3 } from '@celo/contractkit' +import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' +import { testWithAnvilL2, withImpersonatedAccount } from '@celo/dev-utils/lib/anvil-test' +import { ClaimTypes, IdentityMetadataWrapper } from '@celo/metadata-claims' +import { ux } from '@oclif/core' +import { setupGroupAndAffiliateValidator } from '../../test-utils/chain-setup' +import { stripAnsiCodesAndTxHashes, testLocallyWithWeb3Node } from '../../test-utils/cliUtils' +import RpcUrls from './rpc-urls' + +testWithAnvilL2('validatorgroup:rpc-urls cmd', async (web3) => { + jest.spyOn(IdentityMetadataWrapper, 'fetchFromURL').mockImplementation(async (_, url) => { + const validatorAddress = url.split('/').pop() + + return new IdentityMetadataWrapper({ + claims: [ + { + type: ClaimTypes.RPC_URL, + timestamp: Date.now(), + rpcUrl: `https://example.com:8545/${validatorAddress}`, + }, + ], + } as any) // that data is enough + }) + + const setMetadataUrlForValidator = async ( + accountsWrapper: AccountsWrapper, + validator: string + ) => { + await withImpersonatedAccount( + web3, + validator, + async () => { + await accountsWrapper + .setMetadataURL(`https://example.com/metadata/${validator}`) + .sendAndWaitForReceipt({ + from: validator, + }) + }, + 1_000_000_000_000_000n + ) + } + + const EXISTING_VALIDATORS = [ + '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65', + '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc', + '0x976EA74026E726554dB657fA54763abd0C3a0aa9', + ] + + beforeEach(async () => { + const kit = newKitFromWeb3(web3) + const accountsWrapper = await kit.contracts.getAccounts() + + const [nonElectedGroupAddress, validatorAddress] = await web3.eth.getAccounts() + + await setupGroupAndAffiliateValidator(kit, nonElectedGroupAddress, validatorAddress) + await accountsWrapper + .setName('Test group') + .sendAndWaitForReceipt({ from: nonElectedGroupAddress }) + + for (const validator of [...EXISTING_VALIDATORS, validatorAddress]) { + await setMetadataUrlForValidator(accountsWrapper, validator) + } + }) + + it('shows the RPC URLs of the elected validator groups', async () => { + const logMock = jest.spyOn(console, 'log') + const writeMock = jest.spyOn(ux.write, 'stdout') + + await testLocallyWithWeb3Node(RpcUrls, ['--csv'], web3) + + expect(writeMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes))) + .toMatchInlineSnapshot(` + [ + [ + "Validator Group Name,RPC URL,Validator Address + ", + ], + [ + "cLabs,https://example.com:8545/0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65,0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 + ", + ], + [ + "cLabs,https://example.com:8545/0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc,0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc + ", + ], + [ + "cLabs,https://example.com:8545/0x976EA74026E726554dB657fA54763abd0C3a0aa9,0x976EA74026E726554dB657fA54763abd0C3a0aa9 + ", + ], + ] + `) + expect( + logMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes)) + ).toMatchInlineSnapshot(`[]`) + }) + + it('shows all the RPC URLs', async () => { + const logMock = jest.spyOn(console, 'log') + const writeMock = jest.spyOn(ux.write, 'stdout') + + await testLocallyWithWeb3Node(RpcUrls, ['--all', '--csv'], web3) + + expect(writeMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes))) + .toMatchInlineSnapshot(` + [ + [ + "Validator Group Name,RPC URL,Validator Address + ", + ], + [ + "cLabs,https://example.com:8545/0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65,0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 + ", + ], + [ + "cLabs,https://example.com:8545/0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc,0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc + ", + ], + [ + "cLabs,https://example.com:8545/0x976EA74026E726554dB657fA54763abd0C3a0aa9,0x976EA74026E726554dB657fA54763abd0C3a0aa9 + ", + ], + [ + "Test group,https://example.com:8545/0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb,0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb + ", + ], + ] + `) + expect( + logMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes)) + ).toMatchInlineSnapshot(`[]`) + }) +}) diff --git a/packages/cli/src/commands/validatorgroup/rpc-urls.test.ts b/packages/cli/src/commands/validatorgroup/rpc-urls.test.ts new file mode 100644 index 000000000..611d4f282 --- /dev/null +++ b/packages/cli/src/commands/validatorgroup/rpc-urls.test.ts @@ -0,0 +1,132 @@ +import { newKitFromWeb3 } from '@celo/contractkit' +import { AccountsWrapper } from '@celo/contractkit/lib/wrappers/Accounts' +import { testWithAnvilL1, withImpersonatedAccount } from '@celo/dev-utils/lib/anvil-test' +import { ClaimTypes, IdentityMetadataWrapper } from '@celo/metadata-claims' +import { ux } from '@oclif/core' +import { setupGroupAndAffiliateValidator } from '../../test-utils/chain-setup' +import { stripAnsiCodesAndTxHashes, testLocallyWithWeb3Node } from '../../test-utils/cliUtils' +import RpcUrls from './rpc-urls' + +testWithAnvilL1('validatorgroup:rpc-urls cmd', async (web3) => { + jest.spyOn(IdentityMetadataWrapper, 'fetchFromURL').mockImplementation(async (_, url) => { + const validatorAddress = url.split('/').pop() + + return new IdentityMetadataWrapper({ + claims: [ + { + type: ClaimTypes.RPC_URL, + timestamp: Date.now(), + rpcUrl: `https://example.com:8545/${validatorAddress}`, + }, + ], + } as any) // that data is enough + }) + + const setMetadataUrlForValidator = async ( + accountsWrapper: AccountsWrapper, + validator: string + ) => { + await withImpersonatedAccount( + web3, + validator, + async () => { + await accountsWrapper + .setMetadataURL(`https://example.com/metadata/${validator}`) + .sendAndWaitForReceipt({ + from: validator, + }) + }, + 1_000_000_000_000_000n + ) + } + + const EXISTING_VALIDATORS = [ + '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65', + '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc', + '0x976EA74026E726554dB657fA54763abd0C3a0aa9', + ] + + beforeEach(async () => { + const kit = newKitFromWeb3(web3) + const accountsWrapper = await kit.contracts.getAccounts() + + const [nonElectedGroupAddress, validatorAddress] = await web3.eth.getAccounts() + + await setupGroupAndAffiliateValidator(kit, nonElectedGroupAddress, validatorAddress) + await accountsWrapper + .setName('Test group') + .sendAndWaitForReceipt({ from: nonElectedGroupAddress }) + + for (const validator of [...EXISTING_VALIDATORS, validatorAddress]) { + await setMetadataUrlForValidator(accountsWrapper, validator) + } + }) + + it('shows the RPC URLs of the elected validator groups', async () => { + const logMock = jest.spyOn(console, 'log') + const writeMock = jest.spyOn(ux.write, 'stdout') + + await testLocallyWithWeb3Node(RpcUrls, ['--csv'], web3) + + expect(writeMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes))) + .toMatchInlineSnapshot(` + [ + [ + "Validator Group Name,RPC URL,Validator Address + ", + ], + [ + "cLabs,https://example.com:8545/0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65,0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 + ", + ], + [ + "cLabs,https://example.com:8545/0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc,0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc + ", + ], + [ + "cLabs,https://example.com:8545/0x976EA74026E726554dB657fA54763abd0C3a0aa9,0x976EA74026E726554dB657fA54763abd0C3a0aa9 + ", + ], + ] + `) + expect( + logMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes)) + ).toMatchInlineSnapshot(`[]`) + }) + + it('shows all the RPC URLs', async () => { + const logMock = jest.spyOn(console, 'log') + const writeMock = jest.spyOn(ux.write, 'stdout') + + await testLocallyWithWeb3Node(RpcUrls, ['--all', '--csv'], web3) + + expect(writeMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes))) + .toMatchInlineSnapshot(` + [ + [ + "Validator Group Name,RPC URL,Validator Address + ", + ], + [ + "cLabs,https://example.com:8545/0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65,0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 + ", + ], + [ + "cLabs,https://example.com:8545/0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc,0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc + ", + ], + [ + "cLabs,https://example.com:8545/0x976EA74026E726554dB657fA54763abd0C3a0aa9,0x976EA74026E726554dB657fA54763abd0C3a0aa9 + ", + ], + [ + "Test group,https://example.com:8545/0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb,0x6Ecbe1DB9EF729CBe972C83Fb886247691Fb6beb + ", + ], + ] + `) + expect( + logMock.mock.calls.map((args) => args.map(stripAnsiCodesAndTxHashes)) + ).toMatchInlineSnapshot(`[]`) + }) +}) diff --git a/packages/cli/src/commands/validatorgroup/rpc-urls.ts b/packages/cli/src/commands/validatorgroup/rpc-urls.ts new file mode 100644 index 000000000..0cb28ab98 --- /dev/null +++ b/packages/cli/src/commands/validatorgroup/rpc-urls.ts @@ -0,0 +1,90 @@ +import { concurrentMap, StrongAddress } from '@celo/base' +import { ClaimTypes, IdentityMetadataWrapper } from '@celo/metadata-claims' +import { Flags, ux } from '@oclif/core' +import { BaseCommand } from '../../base' + +export default class RpcUrls extends BaseCommand { + static description = + 'Displays a list of community RPC nodes for the currently elected validator groups' + + static aliases: string[] = [ + 'validator:rpc-urls', + 'validatorgroup:rpc-urls', + 'network:community-rpc-nodes', + 'validator:community-rpc-nodes', + 'validatorgroup:community-rpc-nodes', + ] + + static flags = { + ...BaseCommand.flags, + ...(ux.table.flags() as object), + all: Flags.boolean({ + description: + 'Display all community RC nodes, not just the ones from currently elected validator groups', + required: false, + default: false, + }), + } + + async run() { + const res = await this.parse(RpcUrls) + const kit = await this.getKit() + const isL2 = await this.isCel2() + const epochManagerWrapper = await kit.contracts.getEpochManager() + const validatorsWrapper = await kit.contracts.getValidators() + const accountsWrapper = await kit.contracts.getAccounts() + + let validatorAddresses: StrongAddress[] = [] + + if (res.flags.all) { + validatorAddresses = (await validatorsWrapper.getRegisteredValidators()).map( + (v) => v.address as StrongAddress + ) + } else { + if (isL2) { + validatorAddresses = (await epochManagerWrapper.getElectedAccounts()) as StrongAddress[] + } else { + validatorAddresses = (await validatorsWrapper.currentValidatorAccountsSet()).map( + (v) => v.account as StrongAddress + ) + } + } + + const metadataURLs = await concurrentMap(5, validatorAddresses, async (address) => { + const metadataURL = await accountsWrapper.getMetadataURL(address) + + if (!metadataURL) { + return undefined + } + + const metadata = await IdentityMetadataWrapper.fetchFromURL(accountsWrapper, metadataURL) + + return metadata.findClaim(ClaimTypes.RPC_URL)?.rpcUrl + }) + + // TODO cache so we don't have to fetch the same validator group name multiple times + const validatorGroupNames = await concurrentMap(5, validatorAddresses, async (address) => { + return ( + await validatorsWrapper.getValidatorGroup( + await validatorsWrapper.getValidatorsGroup(address) + ) + ).name + }) + + ux.table( + validatorAddresses + .map((address, idx) => ({ + validatorGroupName: validatorGroupNames[idx], + rpcUrl: metadataURLs[idx], + validatorAddress: address, + })) + .filter((row) => row.rpcUrl), + { + validatorGroupName: { header: 'Validator Group Name' }, + rpcUrl: { header: 'RPC URL' }, + validatorAddress: { header: 'Validator Address' }, + }, + res.flags + ) + } +} From fc275e234f1f6d113a8496d05116d766b0b18d53 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Tue, 3 Dec 2024 14:15:18 +0100 Subject: [PATCH 2/9] typo + optimisations + update PR template --- .github/pull_request_template.md | 5 ++++ .../src/commands/validatorgroup/rpc-urls.ts | 27 ++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ceb6e1150..0086404a2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,6 +10,11 @@ _Describe any minor or "drive-by" changes here. If none delete this section_ _An explanation of how the changes were tested or an explanation as to why they don't need to be._ + +### How to QA + +_List of steps required to QA the change if applicable_ + ### Related issues - Fixes #[issue number here] diff --git a/packages/cli/src/commands/validatorgroup/rpc-urls.ts b/packages/cli/src/commands/validatorgroup/rpc-urls.ts index 0cb28ab98..c1958a885 100644 --- a/packages/cli/src/commands/validatorgroup/rpc-urls.ts +++ b/packages/cli/src/commands/validatorgroup/rpc-urls.ts @@ -8,8 +8,8 @@ export default class RpcUrls extends BaseCommand { 'Displays a list of community RPC nodes for the currently elected validator groups' static aliases: string[] = [ + 'network:rpc-urls', 'validator:rpc-urls', - 'validatorgroup:rpc-urls', 'network:community-rpc-nodes', 'validator:community-rpc-nodes', 'validatorgroup:community-rpc-nodes', @@ -20,7 +20,7 @@ export default class RpcUrls extends BaseCommand { ...(ux.table.flags() as object), all: Flags.boolean({ description: - 'Display all community RC nodes, not just the ones from currently elected validator groups', + 'Display all community RPC nodes, not just the ones from currently elected validator groups', required: false, default: false, }), @@ -50,6 +50,18 @@ export default class RpcUrls extends BaseCommand { } } + const validatorToGroup: { [key: StrongAddress]: string } = Object.fromEntries( + await concurrentMap(5, validatorAddresses, async (address) => { + return [address, await validatorsWrapper.getValidatorsGroup(address)] + }) + ) + + const validatorGroupNames = Object.fromEntries( + await concurrentMap(5, [...new Set(Object.values(validatorToGroup))], async (address) => { + return [address, (await validatorsWrapper.getValidatorGroup(address)).name] + }) + ) + const metadataURLs = await concurrentMap(5, validatorAddresses, async (address) => { const metadataURL = await accountsWrapper.getMetadataURL(address) @@ -62,19 +74,10 @@ export default class RpcUrls extends BaseCommand { return metadata.findClaim(ClaimTypes.RPC_URL)?.rpcUrl }) - // TODO cache so we don't have to fetch the same validator group name multiple times - const validatorGroupNames = await concurrentMap(5, validatorAddresses, async (address) => { - return ( - await validatorsWrapper.getValidatorGroup( - await validatorsWrapper.getValidatorsGroup(address) - ) - ).name - }) - ux.table( validatorAddresses .map((address, idx) => ({ - validatorGroupName: validatorGroupNames[idx], + validatorGroupName: validatorGroupNames[validatorToGroup[address]], rpcUrl: metadataURLs[idx], validatorAddress: address, })) From 192f5467431b6e3522eae493457d2d31e8186215 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Tue, 3 Dec 2024 14:24:02 +0100 Subject: [PATCH 3/9] docs --- docs/command-line-interface/network.md | 166 +++++++++++++++++ docs/command-line-interface/validator.md | 166 +++++++++++++++++ docs/command-line-interface/validatorgroup.md | 168 ++++++++++++++++++ 3 files changed, 500 insertions(+) diff --git a/docs/command-line-interface/network.md b/docs/command-line-interface/network.md index 7eb63285e..f6bf05e66 100644 --- a/docs/command-line-interface/network.md +++ b/docs/command-line-interface/network.md @@ -3,11 +3,95 @@ View details about the network, like contracts and parameters +* [`celocli network:community-rpc-nodes`](#celocli-networkcommunity-rpc-nodes) * [`celocli network:contracts`](#celocli-networkcontracts) * [`celocli network:info`](#celocli-networkinfo) * [`celocli network:parameters`](#celocli-networkparameters) +* [`celocli network:rpc-urls`](#celocli-networkrpc-urls) * [`celocli network:whitelist`](#celocli-networkwhitelist) +## `celocli network:community-rpc-nodes` + +Displays a list of community RPC nodes for the currently elected validator groups + +``` +USAGE + $ celocli network:community-rpc-nodes [-k | --useLedger | ] [-n ] [--gasCurrency + 0x1234567890123456789012345678901234567890] [--ledgerAddresses ] + [--globalHelp] [--columns | -x] [--filter ] [--no-header | [--csv | + --no-truncate]] [--output csv|json|yaml | | ] [--sort ] [--all] + +FLAGS + -k, --privateKey= + Use a private key to sign local transactions with + + -n, --node= + URL of the node to run commands against or an alias + + -x, --extended + show extra columns + + --all + Display all community RPC nodes, not just the ones from currently elected validator + groups + + --columns= + only show provided columns (comma-separated) + + --csv + output is csv format [alias: --output=csv] + + --filter= + filter property by partial string matching, ex: name=foo + + --gasCurrency=0x1234567890123456789012345678901234567890 + Use a specific gas currency for transaction fees (defaults to CELO if no gas + currency is supplied). It must be a whitelisted token. + + --globalHelp + View all available global flags + + --ledgerAddresses= + [default: 1] If --useLedger is set, this will get the first N addresses for local + signing + + --no-header + hide table header from output + + --no-truncate + do not truncate output to fit screen + + --output=