From fe210f87863d93d7350fdfa00c26c8266582b887 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Mon, 9 Jun 2025 15:29:16 +0200 Subject: [PATCH 01/11] feat: add experimental networks default config --- src/common/utils/config.js | 191 +++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 91 deletions(-) diff --git a/src/common/utils/config.js b/src/common/utils/config.js index af39ea65..aa0b1ae2 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -2,100 +2,109 @@ import { Network, EnsPlugin } from 'ethers'; import { TEE_FRAMEWORKS } from './constant.js'; import { address as voucherHubBellecourAddress } from '../generated/@iexec/voucher-contracts/deployments/bellecour/VoucherHubERC1967Proxy.js'; -const hostMap = { - 1: 'mainnet', - 134: 'https://bellecour.iex.ec', -}; - -const ensMap = { - 1: { - // registry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', - publicResolver: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41', - }, - 134: { - registry: '0x5f5B93fca68c9C79318d1F3868A354EE67D8c006', - publicResolver: '0x1347d8a1840A810B990d0B774A6b7Bb8A1bd62BB', - }, -}; - -const voucherHubMap = { - 134: voucherHubBellecourAddress, -}; - -const networkMap = { - 134: { +const networkConfigs = [ + { + id: 134, name: 'bellecour', - chainId: 134, - ensAddress: ensMap[134].registry, + hub: undefined, // use default + host: 'https://bellecour.iex.ec', + ensRegistry: '0x5f5B93fca68c9C79318d1F3868A354EE67D8c006', // use ethers default '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' + ensPublicResolver: '0x1347d8a1840A810B990d0B774A6b7Bb8A1bd62BB', + sms: { + [TEE_FRAMEWORKS.SCONE]: 'https://sms.iex.ec', + [TEE_FRAMEWORKS.GRAMINE]: 'https://sms.gramine.v8-bellecour.iex.ec', + }, + resultProxy: 'https://result.v8-bellecour.iex.ec', + ipfsGateway: 'https://ipfs-gateway.v8-bellecour.iex.ec', + iexecGateway: 'https://api.market.v8-bellecour.iex.ec', + pocoSubgraph: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5', + voucherHub: voucherHubBellecourAddress, + voucherSubgraph: + 'https://thegraph.iex.ec/subgraphs/name/bellecour/iexec-voucher', + bridge: { + contract: '0x188A4376a1D818bF2434972Eb34eFd57102a19b7', + bridgedChainId: '1', + }, + shouldRegisterNetwork: true, + isExperimental: false, }, -}; - -const hubMap = {}; - -const smsMap = { - 134: { - [TEE_FRAMEWORKS.SCONE]: 'https://sms.iex.ec', - [TEE_FRAMEWORKS.GRAMINE]: 'https://sms.gramine.v8-bellecour.iex.ec', + { + id: 1, + name: 'mainnet', + hub: undefined, // use default + host: 'mainnet', + ensRegistry: undefined, // use ethers default + ensPublicResolver: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41', + sms: undefined, // no protocol running + resultProxy: undefined, // no protocol running + ipfsGateway: undefined, // no protocol running + iexecGateway: undefined, // no protocol running + pocoSubgraph: undefined, // no protocol running + voucherHub: undefined, // no voucher + voucherSubgraph: undefined, // no voucher + bridge: { + contract: '0x4e55c9B8953AB1957ad0A59D413631A66798c6a2', + bridgedChainId: '134', + }, + shouldRegisterNetwork: false, + isExperimental: false, }, +]; + +export const getId = (idOrName) => + networkConfigs.find( + ({ id, name }) => idOrName === name || `${idOrName}` === `${id}`, + )?.id; + +export const getChainDefaults = ({ id, allowExperimental = false }) => { + const { + host, + ensRegistry, + ensPublicResolver, + hub, + sms, + resultProxy, + iexecGateway, + ipfsGateway, + pocoSubgraph, + voucherHub, + voucherSubgraph, + bridge, + } = + networkConfigs + .filter( + ({ isExperimental }) => allowExperimental || isExperimental === false, + ) + .find((networkConfig) => `${id}` === `${networkConfig.id}`) || {}; + + return { + host, + ensRegistry, + ensPublicResolver, + hub, + sms, + resultProxy, + iexecGateway, + ipfsGateway, + pocoSubgraph, + voucherHub, + voucherSubgraph, + bridge, + }; }; -const resultProxyMap = { - 134: 'https://result.v8-bellecour.iex.ec', -}; - -const bridgeMap = { - 1: { - contract: '0x4e55c9B8953AB1957ad0A59D413631A66798c6a2', - bridgedChainId: '134', - }, - 134: { - contract: '0x188A4376a1D818bF2434972Eb34eFd57102a19b7', - bridgedChainId: '1', - }, -}; - -const ipfsGatewayMap = { - 134: 'https://ipfs-gateway.v8-bellecour.iex.ec', -}; - -const iexecGatewayMap = { - 134: 'https://api.market.v8-bellecour.iex.ec', -}; - -const pocoSubgraphMap = { - 134: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5', -}; - -const voucherSubgraphMap = { - 134: 'https://thegraph.iex.ec/subgraphs/name/bellecour/iexec-voucher', -}; - -const idMap = { - mainnet: 1, - bellecour: 134, -}; - -export const getId = (idOrName) => idMap[idOrName] || idOrName; - -export const getChainDefaults = ({ id }) => ({ - host: hostMap[id], - hub: hubMap[id], - sms: smsMap[id], - ensPublicResolver: ensMap[id] && ensMap[id].publicResolver, - voucherHub: voucherHubMap[id], - resultProxy: resultProxyMap[id], - ipfsGateway: ipfsGatewayMap[id], - iexecGateway: iexecGatewayMap[id], - pocoSubgraph: pocoSubgraphMap[id], - voucherSubgraph: voucherSubgraphMap[id], - bridge: bridgeMap[id], -}); - // register ethers unknown networks -if (Network.from(134).name === 'unknown') { - const bellecourNetwork = new Network(networkMap[134].name, 134).attachPlugin( - new EnsPlugin(ensMap[134].registry, 134), - ); - Network.register(bellecourNetwork.chainId, () => bellecourNetwork); - Network.register(bellecourNetwork.name, () => bellecourNetwork); -} +networkConfigs.forEach((networkConfig) => { + if ( + networkConfig.shouldRegisterNetwork && + Network.from(networkConfig.id).name === 'unknown' && + networkConfig.ensRegistry + ) { + const network = new Network( + networkConfig.name, + networkConfig.id, + ).attachPlugin(new EnsPlugin(networkConfig.ensRegistry, networkConfig.id)); + Network.register(network.chainId, () => network); + Network.register(network.name, () => network); + } +}); From 7312a068abff0858013b6d3a18a4ac3b96c78b24 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:55:42 +0200 Subject: [PATCH 02/11] test: reduce flakyness of tests based on RPC API providers --- test/lib/e2e/IExecAccountModule.test.js | 13 ++++++-- test/lib/e2e/IExecConfig.test.js | 43 +++++++------------------ test/lib/e2e/IExecEnsModule.test.js | 8 ++--- test/lib/e2e/IExecWalletModule.test.js | 14 ++++++-- test/lib/unit/validator.test.js | 32 +++++++++++------- test/test-utils.js | 8 +++++ 6 files changed, 64 insertions(+), 54 deletions(-) diff --git a/test/lib/e2e/IExecAccountModule.test.js b/test/lib/e2e/IExecAccountModule.test.js index 610939bc..38216cb3 100644 --- a/test/lib/e2e/IExecAccountModule.test.js +++ b/test/lib/e2e/IExecAccountModule.test.js @@ -4,7 +4,7 @@ import { describe, test, expect } from '@jest/globals'; import { BN } from 'bn.js'; import { ONE_ETH, ONE_RLC, getTestConfig } from '../lib-test-utils.js'; import { - INFURA_PROJECT_ID, + DEFAULT_PROVIDER_OPTIONS, TEST_CHAINS, getRandomAddress, setBalance, @@ -47,7 +47,9 @@ describe('account', () => { test('expose bridged balances (mainnet) on bellecour', async () => { const iexec = new IExec( { ethProvider: 'bellecour' }, - { providerOptions: { infura: INFURA_PROJECT_ID } }, + { + providerOptions: DEFAULT_PROVIDER_OPTIONS, + }, ); const res = await iexec.account.checkBridgedBalance(getRandomAddress()); expect(res.stake).toBeInstanceOf(BN); @@ -56,7 +58,12 @@ describe('account', () => { describe('token chain', () => { test('expose bridged balances (bellecour) on mainnet', async () => { - const iexec = new IExec({ ethProvider: 'mainnet' }); + const iexec = new IExec( + { ethProvider: 'mainnet' }, + { + providerOptions: DEFAULT_PROVIDER_OPTIONS, + }, + ); const res = await iexec.account.checkBridgedBalance(getRandomAddress()); expect(res.stake).toBeInstanceOf(BN); diff --git a/test/lib/e2e/IExecConfig.test.js b/test/lib/e2e/IExecConfig.test.js index 955ad576..0c6b9a6c 100644 --- a/test/lib/e2e/IExecConfig.test.js +++ b/test/lib/e2e/IExecConfig.test.js @@ -21,6 +21,7 @@ import { TEE_FRAMEWORKS, getRandomAddress, getRandomWallet, + DEFAULT_PROVIDER_OPTIONS, } from '../../test-utils.js'; import '../../jest-setup.js'; @@ -90,12 +91,7 @@ describe('[IExecConfig]', () => { const config = new IExecConfig( { ethProvider: 'mainnet' }, { - providerOptions: { - cloudflare: true, - alchemy: ALCHEMY_API_KEY, - etherscan: ETHERSCAN_API_KEY, - infura: INFURA_PROJECT_ID, - }, + providerOptions: DEFAULT_PROVIDER_OPTIONS, }, ); const { provider, signer, chainId } = @@ -140,12 +136,7 @@ describe('[IExecConfig]', () => { const config = new IExecConfig( { ethProvider: '1' }, { - providerOptions: { - cloudflare: true, - alchemy: ALCHEMY_API_KEY, - etherscan: ETHERSCAN_API_KEY, - infura: INFURA_PROJECT_ID, - }, + providerOptions: DEFAULT_PROVIDER_OPTIONS, }, ); const { provider, signer, chainId } = @@ -180,12 +171,7 @@ describe('[IExecConfig]', () => { const config = new IExecConfig( { ethProvider: 1 }, { - providerOptions: { - cloudflare: true, - alchemy: ALCHEMY_API_KEY, - etherscan: ETHERSCAN_API_KEY, - infura: INFURA_PROJECT_ID, - }, + providerOptions: DEFAULT_PROVIDER_OPTIONS, }, ); const { provider, signer, chainId } = @@ -417,12 +403,7 @@ describe('[IExecConfig]', () => { 'mainnet', wallet.privateKey, { - providers: { - cloudflare: true, - infura: INFURA_PROJECT_ID, - alchemy: ALCHEMY_API_KEY, - etherscan: ETHERSCAN_API_KEY, - }, + providers: DEFAULT_PROVIDER_OPTIONS, }, ), }); @@ -551,7 +532,12 @@ describe('[IExecConfig]', () => { describe('bridged chain provider', () => { test('IExecConfig({ ethProvider: "bellecour" })', async () => { - const config = new IExecConfig({ ethProvider: 'bellecour' }); + const config = new IExecConfig( + { ethProvider: 'bellecour' }, + { + providerOptions: DEFAULT_PROVIDER_OPTIONS, + }, + ); const { provider, signer, chainId } = await config.resolveBridgedContractsClient(); expect(signer).toBeUndefined(); @@ -599,12 +585,7 @@ describe('[IExecConfig]', () => { const config = new IExecConfig( { ethProvider: 'mainnet' }, { - providerOptions: { - cloudflare: true, - alchemy: ALCHEMY_API_KEY, - etherscan: ETHERSCAN_API_KEY, - infura: INFURA_PROJECT_ID, - }, + providerOptions: DEFAULT_PROVIDER_OPTIONS, }, ); const { provider, signer, chainId } = diff --git a/test/lib/e2e/IExecEnsModule.test.js b/test/lib/e2e/IExecEnsModule.test.js index c1852b9d..795b76d0 100644 --- a/test/lib/e2e/IExecEnsModule.test.js +++ b/test/lib/e2e/IExecEnsModule.test.js @@ -14,7 +14,7 @@ import { NULL_ADDRESS, getId, getRandomAddress, - INFURA_PROJECT_ID, + DEFAULT_PROVIDER_OPTIONS, } from '../../test-utils.js'; import '../../jest-setup.js'; import { IExec } from '../../../src/lib/index.js'; @@ -34,11 +34,7 @@ describe('ens', () => { const iexec = new IExec( { ethProvider: 'mainnet' }, { - providerOptions: { - cloudflare: 1, - infura: INFURA_PROJECT_ID, - quorum: 1, - }, + providerOptions: DEFAULT_PROVIDER_OPTIONS, }, ); const balance = await iexec.wallet.checkBalances('core.v5.iexec.eth'); diff --git a/test/lib/e2e/IExecWalletModule.test.js b/test/lib/e2e/IExecWalletModule.test.js index 8a1cdb7b..078b3a6e 100644 --- a/test/lib/e2e/IExecWalletModule.test.js +++ b/test/lib/e2e/IExecWalletModule.test.js @@ -4,7 +4,10 @@ import { describe, test, expect } from '@jest/globals'; import { BN } from 'bn.js'; import { ONE_ETH, ONE_RLC, getTestConfig } from '../lib-test-utils.js'; import { + ALCHEMY_API_KEY, + ETHERSCAN_API_KEY, INFURA_PROJECT_ID, + DEFAULT_PROVIDER_OPTIONS, TEST_CHAINS, getRandomAddress, getRandomWallet, @@ -67,7 +70,9 @@ describe('wallet', () => { test('expose bridged balances (mainnet) on bellecour', async () => { const iexec = new IExec( { ethProvider: 'bellecour' }, - { providerOptions: { infura: INFURA_PROJECT_ID } }, + { + providerOptions: DEFAULT_PROVIDER_OPTIONS, + }, ); const address = getRandomAddress(); const balance = await iexec.wallet.checkBridgedBalances(address); @@ -79,7 +84,12 @@ describe('wallet', () => { }); describe('token chain', () => { test('expose bridged balances (bellecour) on mainnet', async () => { - const iexec = new IExec({ ethProvider: 'mainnet' }); + const iexec = new IExec( + { ethProvider: 'mainnet' }, + { + providerOptions: DEFAULT_PROVIDER_OPTIONS, + }, + ); const address = getRandomAddress(); const balance = await iexec.wallet.checkBridgedBalances(address); expect(balance.nRLC).toStrictEqual(new BN(0)); diff --git a/test/lib/unit/validator.test.js b/test/lib/unit/validator.test.js index a3a8a68e..ab07e2c2 100644 --- a/test/lib/unit/validator.test.js +++ b/test/lib/unit/validator.test.js @@ -3,7 +3,12 @@ import { BN } from 'bn.js'; import { getDefaultProvider } from 'ethers'; import fsExtra from 'fs-extra'; import { join } from 'path'; -import { INFURA_PROJECT_ID, TEE_FRAMEWORKS } from '../../test-utils.js'; +import { + ALCHEMY_API_KEY, + ETHERSCAN_API_KEY, + INFURA_PROJECT_ID, + TEE_FRAMEWORKS, +} from '../../test-utils.js'; import { uint256Schema, weiAmountSchema, @@ -33,9 +38,12 @@ const { ValidationError } = errors; const { readFile } = fsExtra; -const mainnetHost = INFURA_PROJECT_ID - ? `https://mainnet.infura.io/v3/${INFURA_PROJECT_ID}` - : 'mainnet'; +const mainnetDefaultProvider = getDefaultProvider('mainnet', { + cloudflare: true, + alchemy: ALCHEMY_API_KEY || '-', + etherscan: ETHERSCAN_API_KEY || '-', + infura: INFURA_PROJECT_ID || '-', +}); describe('[positiveIntSchema]', () => { test('int', async () => { @@ -878,7 +886,7 @@ describe('[addressSchema]', () => { test('undefined', async () => { await expect( addressSchema({ - ethProvider: getDefaultProvider(mainnetHost), + ethProvider: mainnetDefaultProvider, }).validate(undefined), ).resolves.toBe(undefined); }); @@ -894,7 +902,7 @@ describe('[addressSchema]', () => { }); test('address (with ethProvider)', async () => { await expect( - addressSchema({ ethProvider: getDefaultProvider(mainnetHost) }).validate( + addressSchema({ ethProvider: mainnetDefaultProvider }).validate( '0x607F4C5BB672230e8672085532f7e901544a7375', ), ).resolves.toBe('0x607F4C5BB672230e8672085532f7e901544a7375'); @@ -910,14 +918,14 @@ describe('[addressSchema]', () => { }); test('ens (resolve ENS with ethProvider)', async () => { await expect( - addressSchema({ ethProvider: getDefaultProvider(mainnetHost) }).validate( + addressSchema({ ethProvider: mainnetDefaultProvider }).validate( 'rlc.iexec.eth', ), ).resolves.toBe('0x607F4C5BB672230e8672085532f7e901544a7375'); }, 10000); test('invalid ens (throw when ens is missing)', async () => { await expect( - addressSchema({ ethProvider: getDefaultProvider(mainnetHost) }).validate( + addressSchema({ ethProvider: mainnetDefaultProvider }).validate( 'pierre.iexec.eth', ), ).rejects.toThrow( @@ -935,7 +943,7 @@ describe('[addressOrAnySchema]', () => { test('undefined', async () => { await expect( addressOrAnySchema({ - ethProvider: getDefaultProvider(mainnetHost), + ethProvider: mainnetDefaultProvider, }).validate(undefined), ).resolves.toBe(undefined); }); @@ -957,7 +965,7 @@ describe('[addressOrAnySchema]', () => { test('address (with ethProvider)', async () => { await expect( addressOrAnySchema({ - ethProvider: getDefaultProvider(mainnetHost), + ethProvider: mainnetDefaultProvider, }).validate('0x607F4C5BB672230e8672085532f7e901544a7375'), ).resolves.toBe('0x607F4C5BB672230e8672085532f7e901544a7375'); }); @@ -975,14 +983,14 @@ describe('[addressOrAnySchema]', () => { test('ens (resolve ENS with ethProvider)', async () => { await expect( addressOrAnySchema({ - ethProvider: getDefaultProvider(mainnetHost), + ethProvider: mainnetDefaultProvider, }).validate('rlc.iexec.eth'), ).resolves.toBe('0x607F4C5BB672230e8672085532f7e901544a7375'); }, 10000); test('invalid ens (throw when ens is missing)', async () => { await expect( addressOrAnySchema({ - ethProvider: getDefaultProvider(mainnetHost), + ethProvider: mainnetDefaultProvider, }).validate('pierre.iexec.eth'), ).rejects.toThrow( new ValidationError('Unable to resolve ENS pierre.iexec.eth'), diff --git a/test/test-utils.js b/test/test-utils.js index f467cafb..b6b437e8 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -47,6 +47,14 @@ console.log('using env INFURA_PROJECT_ID', !!INFURA_PROJECT_ID); console.log('using env ETHERSCAN_API_KEY', !!ETHERSCAN_API_KEY); console.log('using env ALCHEMY_API_KEY', !!ALCHEMY_API_KEY); +export const DEFAULT_PROVIDER_OPTIONS = { + cloudflare: true, + alchemy: ALCHEMY_API_KEY, + etherscan: ETHERSCAN_API_KEY, + infura: INFURA_PROJECT_ID, + quorum: 1, +}; + export const SERVICE_HTTP_500_URL = DRONE ? 'http://service-internal-error:80' : 'http://localhost:5500'; From 6e903ad6b67a75c1ad9ef21e24a968505559bc29 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:11:32 +0200 Subject: [PATCH 03/11] test: add config module unit tests --- test/lib/unit/config.test.js | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/lib/unit/config.test.js diff --git a/test/lib/unit/config.test.js b/test/lib/unit/config.test.js new file mode 100644 index 00000000..0fb0b2b1 --- /dev/null +++ b/test/lib/unit/config.test.js @@ -0,0 +1,59 @@ +import { describe, test, expect } from '@jest/globals'; +import { Network } from 'ethers'; +import { getChainDefaults, getId } from '../../../src/common/utils/config.js'; + +describe('getId()', () => { + test('chain id as number returns id', () => { + expect(getId(134)).toBe(134); + }); + test('chain id as string returns id', () => { + expect(getId('134')).toBe(134); + }); + test('chain name returns id', () => { + expect(getId('bellecour')).toBe(134); + }); +}); + +describe('getChainDefaults', () => { + test('id 134 returns bellecour config', () => { + expect(getChainDefaults({ id: 134 })).toEqual({ + bridge: { + bridgedChainId: '1', + contract: '0x188A4376a1D818bF2434972Eb34eFd57102a19b7', + }, + ensPublicResolver: '0x1347d8a1840A810B990d0B774A6b7Bb8A1bd62BB', + ensRegistry: '0x5f5B93fca68c9C79318d1F3868A354EE67D8c006', + host: 'https://bellecour.iex.ec', + hub: undefined, + iexecGateway: 'https://api.market.v8-bellecour.iex.ec', + ipfsGateway: 'https://ipfs-gateway.v8-bellecour.iex.ec', + pocoSubgraph: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5', + resultProxy: 'https://result.v8-bellecour.iex.ec', + sms: { + gramine: 'https://sms.gramine.v8-bellecour.iex.ec', + scone: 'https://sms.iex.ec', + }, + voucherHub: '0x3137B6DF4f36D338b82260eDBB2E7bab034AFEda', + voucherSubgraph: + 'https://thegraph.iex.ec/subgraphs/name/bellecour/iexec-voucher', + }); + }); + test('unknown id returns empty object', () => { + expect(getChainDefaults({ id: 0 })).toEqual({}); + }); + test.skip('experimental networks are hidden by default', () => { + throw Error('TODO'); + }); +}); + +describe('Networks', () => { + test('ethers Networks is populated with bellecour', () => { + const networkFromId = Network.from(134); + expect(networkFromId.chainId).toBe(134n); + expect(networkFromId.name).toBe('bellecour'); + + const networkFromName = Network.from('bellecour'); + expect(networkFromName.chainId).toBe(134n); + expect(networkFromName.name).toBe('bellecour'); + }); +}); From 3ea6f88fe71d6efcfc77144a58fad400b101655a Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:36:30 +0200 Subject: [PATCH 04/11] feat: add allowExperimentalNetworks option in chain.json --- src/cli/utils/chains.js | 49 ++++++++++++++++-------------------- src/cli/utils/fs.js | 1 + src/common/utils/config.js | 2 ++ test/lib/unit/config.test.js | 1 + 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/cli/utils/chains.js b/src/cli/utils/chains.js index 94c4a5dd..80d2047a 100644 --- a/src/cli/utils/chains.js +++ b/src/cli/utils/chains.js @@ -1,5 +1,5 @@ import Debug from 'debug'; -import { getChainDefaults } from '../../common/utils/config.js'; +import { getId, getChainDefaults } from '../../common/utils/config.js'; import IExecContractsClient from '../../common/utils/IExecContractsClient.js'; import { EnhancedWallet } from '../../common/utils/signers.js'; import { loadChainConf } from './fs.js'; @@ -8,18 +8,6 @@ import { getReadOnlyProvider } from '../../common/utils/providers.js'; const debug = Debug('iexec:chains'); -const CHAIN_ALIASES_MAP = { - 1: 'mainnet', - 134: 'bellecour', -}; - -const CHAIN_NAME_MAP = { - 1: { id: '1' }, - mainnet: { id: '1' }, - 134: { id: '134' }, - bellecour: { id: '134' }, -}; - const createChainFromConf = ( chainName, chainConf, @@ -63,26 +51,29 @@ const createChainFromConf = ( }; export const loadChain = async ( - chainName, + chainNameOrId, { txOptions, spinner = Spinner() } = {}, ) => { try { const chainsConf = await loadChainConf(); - debug('chainsConf', chainsConf); const providerOptions = chainsConf.providers; let name; let loadedConf; - if (chainName) { - if (chainsConf.chains[chainName]) { - loadedConf = chainsConf.chains[chainName]; - name = chainName; + if (chainNameOrId) { + if (chainsConf.chains[chainNameOrId]) { + loadedConf = chainsConf.chains[chainNameOrId]; + name = chainNameOrId; } else { - const alias = CHAIN_ALIASES_MAP[chainName]; + const { name: alias } = getChainDefaults({ + id: getId(chainNameOrId), + allowExperimental: chainsConf.allowExperimentalNetworks, + }); if (alias && chainsConf.chains[alias]) { loadedConf = chainsConf.chains[alias]; name = alias; } - if (!name) throw Error(`Missing "${chainName}" chain in "chain.json"`); + if (!name) + throw Error(`Missing "${chainNameOrId}" chain in "chain.json"`); } } else if (chainsConf.default) { if (chainsConf.chains[chainsConf.default]) { @@ -96,11 +87,13 @@ export const loadChain = async ( throw Error('Missing chain parameter. Check your "chain.json" file'); const idConf = { - ...CHAIN_NAME_MAP[name], - ...(loadedConf.id && { id: loadedConf.id }), + id: loadedConf.id || getId(name), }; - const defaultConf = getChainDefaults(idConf); + const defaultConf = getChainDefaults({ + id: idConf.id, + allowExperimental: chainsConf.allowExperimentalNetworks, + }); debug('loading chain', name); debug('loadedConf', loadedConf); @@ -120,7 +113,10 @@ export const loadChain = async ( if (chainsConf.chains[bridgedChainNameOrId]) { bridgeLoadedConf = chainsConf.chains[bridgedChainNameOrId]; } else { - const alias = CHAIN_ALIASES_MAP[bridgedChainNameOrId]; + const { name: alias } = getChainDefaults({ + id: getId(bridgedChainNameOrId), + allowExperimental: chainsConf.allowExperimentalNetworks, + }); if (alias && chainsConf.chains[alias]) { bridgeLoadedConf = chainsConf.chains[alias]; } @@ -128,8 +124,7 @@ export const loadChain = async ( throw Error(`Missing "${name}" chain in "chain.json"`); } const bridgeIdConf = { - ...CHAIN_NAME_MAP[bridgedChainNameOrId], - ...(bridgeLoadedConf.id && { id: bridgeLoadedConf.id }), + id: bridgeLoadedConf.id || getId(bridgedChainNameOrId), }; const bridgeDefaultConf = getChainDefaults(bridgeIdConf); debug('bridgeLoadedConf', bridgeLoadedConf); diff --git a/src/cli/utils/fs.js b/src/cli/utils/fs.js index 705acaaf..cfabb020 100644 --- a/src/cli/utils/fs.js +++ b/src/cli/utils/fs.js @@ -51,6 +51,7 @@ const chainConfSchema = () => const chainsConfSchema = () => object({ default: string(), + allowExperimentalNetworks: boolean().default(false), chains: object() .test(async (chainsOjb) => { await Promise.all( diff --git a/src/common/utils/config.js b/src/common/utils/config.js index aa0b1ae2..0e963e05 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -58,6 +58,7 @@ export const getId = (idOrName) => export const getChainDefaults = ({ id, allowExperimental = false }) => { const { + name, host, ensRegistry, ensPublicResolver, @@ -78,6 +79,7 @@ export const getChainDefaults = ({ id, allowExperimental = false }) => { .find((networkConfig) => `${id}` === `${networkConfig.id}`) || {}; return { + name, host, ensRegistry, ensPublicResolver, diff --git a/test/lib/unit/config.test.js b/test/lib/unit/config.test.js index 0fb0b2b1..e30ab99a 100644 --- a/test/lib/unit/config.test.js +++ b/test/lib/unit/config.test.js @@ -27,6 +27,7 @@ describe('getChainDefaults', () => { hub: undefined, iexecGateway: 'https://api.market.v8-bellecour.iex.ec', ipfsGateway: 'https://ipfs-gateway.v8-bellecour.iex.ec', + name: 'bellecour', pocoSubgraph: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5', resultProxy: 'https://result.v8-bellecour.iex.ec', sms: { From dfb17a1764ed74729c01c34296cf22f86975485b Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:49:32 +0200 Subject: [PATCH 05/11] feat: add allowExperimentalNetworks option in IExecConfig --- docs/interfaces/IExecConfigOptions.md | 11 +++++++++++ src/lib/IExecConfig.d.ts | 6 ++++++ src/lib/IExecConfig.js | 7 ++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/interfaces/IExecConfigOptions.md b/docs/interfaces/IExecConfigOptions.md index 7a423f5f..cc07914c 100644 --- a/docs/interfaces/IExecConfigOptions.md +++ b/docs/interfaces/IExecConfigOptions.md @@ -6,6 +6,7 @@ ### Properties +- [allowExperimentalNetworks](IExecConfigOptions.md#allowexperimentalnetworks) - [bridgeAddress](IExecConfigOptions.md#bridgeaddress) - [bridgedNetworkConf](IExecConfigOptions.md#bridgednetworkconf) - [confirms](IExecConfigOptions.md#confirms) @@ -25,6 +26,16 @@ ## Properties +### allowExperimentalNetworks + +• `Optional` **allowExperimentalNetworks**: `boolean` + +if true allows using a provider connected to an experimental networks (default false) + +⚠️ experimental networks are networks on which the iExec's stack is partially deployed, experimental networks can be subject to instabilities or discontinuity. Access is provided without warranties. + +___ + ### bridgeAddress • `Optional` **bridgeAddress**: `string` diff --git a/src/lib/IExecConfig.d.ts b/src/lib/IExecConfig.d.ts index 04c4a045..d00e3abd 100644 --- a/src/lib/IExecConfig.d.ts +++ b/src/lib/IExecConfig.d.ts @@ -118,6 +118,12 @@ export interface IExecConfigOptions { * [ethers default provider](https://docs.ethers.io/v5/api/providers/#providers-getDefaultProvider) options */ providerOptions?: ProviderOptions | AnyRecord; + /** + * if true allows using a provider connected to an experimental networks (default false) + * + * ⚠️ experimental networks are networks on which the iExec's stack is partially deployed, experimental networks can be subject to instabilities or discontinuity. Access is provided without warranties. + */ + allowExperimentalNetworks?: boolean; } /** diff --git a/src/lib/IExecConfig.js b/src/lib/IExecConfig.js index c5a8252d..452adc23 100644 --- a/src/lib/IExecConfig.js +++ b/src/lib/IExecConfig.js @@ -33,6 +33,7 @@ export default class IExecConfig { voucherSubgraphURL, defaultTeeFramework, providerOptions, + allowExperimentalNetworks = false, } = {}, ) { if ( @@ -130,7 +131,10 @@ export default class IExecConfig { const chainConfDefaultsPromise = (async () => { const { chainId } = await networkPromise; - return getChainDefaults({ id: chainId }); + return getChainDefaults({ + id: chainId, + allowExperimental: allowExperimentalNetworks, + }); })(); chainConfDefaultsPromise.catch((err) => { @@ -182,6 +186,7 @@ export default class IExecConfig { } const bridgedChainConfDefaults = getChainDefaults({ id: bridgedChainId, + allowExperimental: allowExperimentalNetworks, }); const bridgedRpcUrl = bridgedNetworkConf.rpcURL !== undefined From fc48e2942de1e7a5d3d4ad7f4eef4222666bd4b3 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:50:31 +0200 Subject: [PATCH 06/11] feat: add experimental network arbitrum-sepolia-testnet --- src/common/utils/config.js | 21 +++++++++++++++++++++ test/lib/unit/config.test.js | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/common/utils/config.js b/src/common/utils/config.js index 0e963e05..2379fe93 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -49,6 +49,27 @@ const networkConfigs = [ shouldRegisterNetwork: false, isExperimental: false, }, + { + id: 421614, + name: 'arbitrum-sepolia-testnet', + hub: '0x14B465079537655E1662F012e99EBa3863c8B9E0', + host: 'https://sepolia-rollup.arbitrum.io/rpc', + ensRegistry: undefined, // TODO: not supported + ensPublicResolver: undefined, // TODO: not supported + sms: { + [TEE_FRAMEWORKS.SCONE]: 'https://sms.arbitrum-sepolia-testnet.iex.ec', + }, + resultProxy: undefined, // not exposed + ipfsGateway: 'https://ipfs-gateway.arbitrum-sepolia-testnet.iex.ec', + iexecGateway: 'https://api-market.arbitrum-sepolia-testnet.iex.ec', + pocoSubgraph: + 'https://thegraph.arbitrum-sepolia-testnet.iex.ec/api/subgraphs/id/2GCj8gzLCihsiEDq8cYvC5nUgK6VfwZ6hm3Wj8A3kcxz', + voucherHub: undefined, // no voucher + voucherSubgraph: undefined, // no voucher + bridge: {}, // no bridge + shouldRegisterNetwork: false, + isExperimental: true, + }, ]; export const getId = (idOrName) => diff --git a/test/lib/unit/config.test.js b/test/lib/unit/config.test.js index e30ab99a..a0c3312b 100644 --- a/test/lib/unit/config.test.js +++ b/test/lib/unit/config.test.js @@ -42,8 +42,11 @@ describe('getChainDefaults', () => { test('unknown id returns empty object', () => { expect(getChainDefaults({ id: 0 })).toEqual({}); }); - test.skip('experimental networks are hidden by default', () => { - throw Error('TODO'); + test('experimental networks are accessible with `allowExperimental:true` hidden by default', () => { + expect(getChainDefaults({ id: 421614 })).toEqual({}); + expect( + getChainDefaults({ id: 421614, allowExperimental: true }).host, + ).toBeDefined(); }); }); From 2872c5b4192f66b2eb4998e1dbf6bfa85ab90b28 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:54:18 +0200 Subject: [PATCH 07/11] feat: add allowExperimentalNetworks in getSignerFromPrivateKey --- docs/modules/utils.md | 1 + src/cli/cmd/iexec.js | 4 +- src/cli/utils/chains.js | 56 ++++++++++---- src/common/utils/config.js | 19 +++-- src/common/utils/providers.js | 15 +++- src/common/utils/signers.js | 17 ++++- src/lib/IExecConfig.js | 15 ++-- src/lib/utils.d.ts | 6 ++ test/lib/e2e/IExecConfig.test.js | 122 +++++++++++++++++++++++++++++++ test/lib/e2e/utils.test.js | 16 ++++ test/lib/unit/config.test.js | 10 +-- 11 files changed, 237 insertions(+), 44 deletions(-) diff --git a/docs/modules/utils.md b/docs/modules/utils.md index 8256fe17..3cd6ac77 100644 --- a/docs/modules/utils.md +++ b/docs/modules/utils.md @@ -183,6 +183,7 @@ const iexec = new IExec({ ethProvider }); | `host` | `string` | node RPC url | | `privateKey` | `string` | wallet private key | | `options?` | `Object` | - | +| `options.allowExperimentalNetworks?` | `boolean` | if true allows using a provider connected to an experimental networks (default false) ⚠️ experimental networks are networks on which the iExec's stack is partially deployed, experimental networks can be subject to instabilities or discontinuity. Access is provided without warranties. | | `options.gasPrice?` | `string` \| `number` \| `bigint` | gas price override | | `options.getTransactionCount?` | (`blockTag?`: `BlockTag`) => `Promise`<`number`\> | nonce override | | `options.providers` | [`ProviderOptions`](../interfaces/ProviderOptions.md) | providers options | diff --git a/src/cli/cmd/iexec.js b/src/cli/cmd/iexec.js index e2c0d4a1..41226536 100755 --- a/src/cli/cmd/iexec.js +++ b/src/cli/cmd/iexec.js @@ -123,9 +123,7 @@ infoCmd const chain = await loadChain(opts.chain, { spinner }); const host = - chain.host === getChainDefaults({ id: chain.id }).host - ? 'default' - : chain.host; + chain.host === getChainDefaults(chain.id).host ? 'default' : chain.host; spinner.info(`Ethereum host: ${host}`); spinner.start(info.checking('iExec contracts info')); diff --git a/src/cli/utils/chains.js b/src/cli/utils/chains.js index 80d2047a..189ebed7 100644 --- a/src/cli/utils/chains.js +++ b/src/cli/utils/chains.js @@ -11,12 +11,18 @@ const debug = Debug('iexec:chains'); const createChainFromConf = ( chainName, chainConf, - { bridgeConf, providerOptions, txOptions = {} } = {}, + { + bridgeConf, + providerOptions, + txOptions = {}, + allowExperimentalNetworks = false, + } = {}, ) => { try { const chain = { ...chainConf }; const provider = getReadOnlyProvider(chainConf.host, { providers: providerOptions, + allowExperimentalNetworks, }); chain.name = chainName; @@ -33,6 +39,7 @@ const createChainFromConf = ( chain.bridgedNetwork = { ...bridgeConf }; const bridgeProvider = getReadOnlyProvider(bridgeConf.host, { providers: providerOptions, + allowExperimentalNetworks, }); chain.bridgedNetwork.contracts = new IExecContractsClient({ provider: bridgeProvider, @@ -56,6 +63,7 @@ export const loadChain = async ( ) => { try { const chainsConf = await loadChainConf(); + const { allowExperimentalNetworks } = chainsConf; const providerOptions = chainsConf.providers; let name; let loadedConf; @@ -64,10 +72,14 @@ export const loadChain = async ( loadedConf = chainsConf.chains[chainNameOrId]; name = chainNameOrId; } else { - const { name: alias } = getChainDefaults({ - id: getId(chainNameOrId), - allowExperimental: chainsConf.allowExperimentalNetworks, - }); + const { name: alias } = getChainDefaults( + getId(chainNameOrId, { + allowExperimentalNetworks, + }), + { + allowExperimentalNetworks, + }, + ); if (alias && chainsConf.chains[alias]) { loadedConf = chainsConf.chains[alias]; name = alias; @@ -87,12 +99,15 @@ export const loadChain = async ( throw Error('Missing chain parameter. Check your "chain.json" file'); const idConf = { - id: loadedConf.id || getId(name), + id: + loadedConf.id || + getId(name, { + allowExperimentalNetworks, + }), }; - const defaultConf = getChainDefaults({ - id: idConf.id, - allowExperimental: chainsConf.allowExperimentalNetworks, + const defaultConf = getChainDefaults(idConf.id, { + allowExperimentalNetworks, }); debug('loading chain', name); @@ -113,10 +128,14 @@ export const loadChain = async ( if (chainsConf.chains[bridgedChainNameOrId]) { bridgeLoadedConf = chainsConf.chains[bridgedChainNameOrId]; } else { - const { name: alias } = getChainDefaults({ - id: getId(bridgedChainNameOrId), - allowExperimental: chainsConf.allowExperimentalNetworks, - }); + const { name: alias } = getChainDefaults( + getId(bridgedChainNameOrId, { + allowExperimentalNetworks, + }), + { + allowExperimentalNetworks, + }, + ); if (alias && chainsConf.chains[alias]) { bridgeLoadedConf = chainsConf.chains[alias]; } @@ -124,9 +143,15 @@ export const loadChain = async ( throw Error(`Missing "${name}" chain in "chain.json"`); } const bridgeIdConf = { - id: bridgeLoadedConf.id || getId(bridgedChainNameOrId), + id: + bridgeLoadedConf.id || + getId(bridgedChainNameOrId, { + allowExperimentalNetworks, + }), }; - const bridgeDefaultConf = getChainDefaults(bridgeIdConf); + const bridgeDefaultConf = getChainDefaults(bridgeIdConf.id, { + allowExperimentalNetworks, + }); debug('bridgeLoadedConf', bridgeLoadedConf); debug('bridgeDefaultConf', defaultConf); bridgeConf = { @@ -145,6 +170,7 @@ export const loadChain = async ( bridgeConf, providerOptions, txOptions, + allowExperimentalNetworks, }); spinner.info(`Using chain ${name} [chainId: ${chain.id}]`); return chain; diff --git a/src/common/utils/config.js b/src/common/utils/config.js index 2379fe93..d5cd3d14 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -72,12 +72,18 @@ const networkConfigs = [ }, ]; -export const getId = (idOrName) => - networkConfigs.find( - ({ id, name }) => idOrName === name || `${idOrName}` === `${id}`, - )?.id; +export const getId = (idOrName, { allowExperimentalNetworks = false } = {}) => + networkConfigs + .filter( + ({ isExperimental }) => + allowExperimentalNetworks || isExperimental === false, + ) + .find(({ id, name }) => idOrName === name || `${idOrName}` === `${id}`)?.id; -export const getChainDefaults = ({ id, allowExperimental = false }) => { +export const getChainDefaults = ( + id, + { allowExperimentalNetworks = false } = {}, +) => { const { name, host, @@ -95,7 +101,8 @@ export const getChainDefaults = ({ id, allowExperimental = false }) => { } = networkConfigs .filter( - ({ isExperimental }) => allowExperimental || isExperimental === false, + ({ isExperimental }) => + allowExperimentalNetworks || isExperimental === false, ) .find((networkConfig) => `${id}` === `${networkConfig.id}`) || {}; diff --git a/src/common/utils/providers.js b/src/common/utils/providers.js index c3b61c1f..71c66096 100644 --- a/src/common/utils/providers.js +++ b/src/common/utils/providers.js @@ -1,11 +1,18 @@ import { getDefaultProvider, JsonRpcProvider } from 'ethers'; import { getChainDefaults, getId } from './config.js'; -export const getReadOnlyProvider = (host, options = {}) => { - const providerOptions = options.providers || {}; +export const getReadOnlyProvider = ( + host, + { providers = {}, allowExperimentalNetworks = false } = {}, +) => { let resolvedHost = host; - const defaults = getChainDefaults({ id: getId(host) }); + const defaults = getChainDefaults( + getId(host, { allowExperimentalNetworks }), + { + allowExperimentalNetworks, + }, + ); if (defaults && defaults.host) { resolvedHost = defaults.host; @@ -24,7 +31,7 @@ export const getReadOnlyProvider = (host, options = {}) => { }); } // API provider - const { quorum, ...providersOptionsRest } = providerOptions; + const { quorum, ...providersOptionsRest } = providers; // disable non configured providers when at least 1 is configured const apiProvidersList = [ 'alchemy', diff --git a/src/common/utils/signers.js b/src/common/utils/signers.js index 368401e3..588d19b8 100644 --- a/src/common/utils/signers.js +++ b/src/common/utils/signers.js @@ -100,9 +100,18 @@ export class BrowserProviderSignerAdapter extends AbstractSigner { export const getSignerFromPrivateKey = ( host, privateKey, - { gasPrice, getTransactionCount, providers } = {}, -) => - new EnhancedWallet(privateKey, getReadOnlyProvider(host, { providers }), { + { gasPrice, getTransactionCount, - }); + providers, + allowExperimentalNetworks = false, + } = {}, +) => + new EnhancedWallet( + privateKey, + getReadOnlyProvider(host, { providers, allowExperimentalNetworks }), + { + gasPrice, + getTransactionCount, + }, + ); diff --git a/src/lib/IExecConfig.js b/src/lib/IExecConfig.js index 452adc23..786e478b 100644 --- a/src/lib/IExecConfig.js +++ b/src/lib/IExecConfig.js @@ -77,6 +77,7 @@ export default class IExecConfig { if (isRpcUrlProvider) { provider = getReadOnlyProvider(ethProvider, { providers: providerOptions, + allowExperimentalNetworks, }); } else if (isEthersAbstractSignerWithProvider) { provider = ethProvider.provider; @@ -131,9 +132,8 @@ export default class IExecConfig { const chainConfDefaultsPromise = (async () => { const { chainId } = await networkPromise; - return getChainDefaults({ - id: chainId, - allowExperimental: allowExperimentalNetworks, + return getChainDefaults(chainId, { + allowExperimentalNetworks, }); })(); @@ -143,12 +143,13 @@ export default class IExecConfig { const contractsPromise = (async () => { const { chainId } = await networkPromise; + const chainConfDefaults = await chainConfDefaultsPromise; try { return new IExecContractsClient({ chainId, provider, signer, - hubAddress, + hubAddress: hubAddress || chainConfDefaults.hub, useGas, confirms, isNative, @@ -184,9 +185,8 @@ export default class IExecConfig { `Missing chainId in bridgedNetworkConf and no default value for your chain ${chainId}`, ); } - const bridgedChainConfDefaults = getChainDefaults({ - id: bridgedChainId, - allowExperimental: allowExperimentalNetworks, + const bridgedChainConfDefaults = getChainDefaults(bridgedChainId, { + allowExperimentalNetworks, }); const bridgedRpcUrl = bridgedNetworkConf.rpcURL !== undefined @@ -228,6 +228,7 @@ export default class IExecConfig { chainId: bridgedConf.chainId, provider: getReadOnlyProvider(bridgedConf.rpcURL, { providers: providerOptions, + allowExperimentalNetworks, }), hubAddress: bridgedConf.hubAddress, confirms, diff --git a/src/lib/utils.d.ts b/src/lib/utils.d.ts index b3f98aad..d4ab254c 100644 --- a/src/lib/utils.d.ts +++ b/src/lib/utils.d.ts @@ -42,6 +42,12 @@ export const getSignerFromPrivateKey: ( * providers options */ providers: ProviderOptions; + /** + * if true allows using a provider connected to an experimental networks (default false) + * + * ⚠️ experimental networks are networks on which the iExec's stack is partially deployed, experimental networks can be subject to instabilities or discontinuity. Access is provided without warranties. + */ + allowExperimentalNetworks?: boolean; }, ) => EnhancedWallet; diff --git a/test/lib/e2e/IExecConfig.test.js b/test/lib/e2e/IExecConfig.test.js index 0c6b9a6c..ae8bc399 100644 --- a/test/lib/e2e/IExecConfig.test.js +++ b/test/lib/e2e/IExecConfig.test.js @@ -27,6 +27,7 @@ import '../../jest-setup.js'; import { utils, IExecConfig, errors } from '../../../src/lib/index.js'; import IExecContractsClient from '../../../src/common/utils/IExecContractsClient.js'; +import { getChainDefaults } from '../../../src/common/utils/config.js'; const iexecTestChain = TEST_CHAINS['bellecour-fork']; const unknownTestChain = TEST_CHAINS['custom-token-chain']; @@ -129,6 +130,31 @@ describe('[IExecConfig]', () => { ); expect(createConfig).toThrow(errors.ConfigurationError); }); + describe('allowExperimentalNetworks', () => { + test('throw with experimental chains when allowExperimentalNetworks is not enabled', () => { + const createConfig = () => + new IExecConfig({ ethProvider: 'arbitrum-sepolia-testnet' }); + expect(createConfig).toThrow( + Error('Invalid ethProvider: Invalid provider host name or url'), + ); + expect(createConfig).toThrow(errors.ConfigurationError); + }); + test('allows experimental chains when allowExperimentalNetworks is enabled', async () => { + const config = new IExecConfig( + { ethProvider: 'arbitrum-sepolia-testnet' }, + { allowExperimentalNetworks: true }, + ); + const { provider, signer, chainId } = + await config.resolveContractsClient(); + expect(signer).toBeUndefined(); + expect(provider).toBeDefined(); + expect(provider).toBeInstanceOf(JsonRpcProvider); + expect(chainId).toBe('421614'); + const network = await provider.getNetwork(); + expect(network.chainId).toBe(421614n); + expect(network.name).toBe('arbitrum-sepolia'); + }); + }); }); describe('read-only ethProvider from network chainId', () => { @@ -216,6 +242,30 @@ describe('[IExecConfig]', () => { ); expect(createConfig).toThrow(errors.ConfigurationError); }); + describe('allowExperimentalNetworks', () => { + test('throw with experimental chains when allowExperimentalNetworks is not enabled', () => { + const createConfig = () => new IExecConfig({ ethProvider: 421614 }); + expect(createConfig).toThrow( + Error('Invalid ethProvider: Invalid provider host name or url'), + ); + expect(createConfig).toThrow(errors.ConfigurationError); + }); + test('allows experimental chains when allowExperimentalNetworks is enabled', async () => { + const config = new IExecConfig( + { ethProvider: 421614 }, + { allowExperimentalNetworks: true }, + ); + const { provider, signer, chainId } = + await config.resolveContractsClient(); + expect(signer).toBeUndefined(); + expect(provider).toBeDefined(); + expect(provider).toBeInstanceOf(JsonRpcProvider); + expect(chainId).toBe('421614'); + const network = await provider.getNetwork(); + expect(network.chainId).toBe(421614n); + expect(network.name).toBe('arbitrum-sepolia'); + }); + }); }); describe('read-only ethProvider with API keys', () => { @@ -371,6 +421,38 @@ describe('[IExecConfig]', () => { network.getPlugin('org.ethers.plugins.network.Ens').address, ).toBe('0x5f5B93fca68c9C79318d1F3868A354EE67D8c006'); }); + describe('allowExperimentalNetworks', () => { + const experimentalNetworkRpcUrl = getChainDefaults(421614, { + allowExperimentalNetworks: true, + }).host; + + test('fail resolving config with experimental chains when allowExperimentalNetworks is not enabled', async () => { + const config = new IExecConfig({ + ethProvider: experimentalNetworkRpcUrl, + }); + + await expect(config.resolveContractsClient()).rejects.toThrow( + Error( + 'Failed to create contracts client: Missing iExec contract default address for chain 421614', + ), + ); + }); + test('allows experimental chains when allowExperimentalNetworks is enabled', async () => { + const config = new IExecConfig( + { ethProvider: experimentalNetworkRpcUrl }, + { allowExperimentalNetworks: true }, + ); + const { provider, signer, chainId } = + await config.resolveContractsClient(); + expect(signer).toBeUndefined(); + expect(provider).toBeDefined(); + expect(provider).toBeInstanceOf(JsonRpcProvider); + expect(chainId).toBe('421614'); + const network = await provider.getNetwork(); + expect(network.chainId).toBe(421614n); + expect(network.name).toBe('arbitrum-sepolia'); + }); + }); }); describe('signer provider from private key', () => { @@ -444,6 +526,46 @@ describe('[IExecConfig]', () => { network.getPlugin('org.ethers.plugins.network.Ens').address, ).toBe(iexecTestChain.defaults.ensRegistryAddress); }); + describe('allowExperimentalNetworks', () => { + const experimentalNetworkRpcUrl = getChainDefaults(421614, { + allowExperimentalNetworks: true, + }).host; + + test('fail resolving config with experimental chains when allowExperimentalNetworks is not enabled', async () => { + const injectedProvider = new InjectedProvider( + experimentalNetworkRpcUrl, + getRandomWallet().privateKey, + ); + const config = new IExecConfig({ + ethProvider: injectedProvider, + }); + + await expect(config.resolveContractsClient()).rejects.toThrow( + Error( + 'Failed to create contracts client: Missing iExec contract default address for chain 421614', + ), + ); + }); + test('allows experimental chains when allowExperimentalNetworks is enabled', async () => { + const injectedProvider = new InjectedProvider( + experimentalNetworkRpcUrl, + getRandomWallet().privateKey, + ); + const config = new IExecConfig( + { ethProvider: injectedProvider }, + { allowExperimentalNetworks: true }, + ); + const { provider, signer, chainId } = + await config.resolveContractsClient(); + expect(signer).toBeDefined(); + expect(provider).toBeDefined(); + expect(provider).toBeInstanceOf(BrowserProvider); + expect(chainId).toBe('421614'); + const network = await provider.getNetwork(); + expect(network.chainId).toBe(421614n); + expect(network.name).toBe('arbitrum-sepolia'); + }); + }); }); describe('ethers AbstractProvider', () => { diff --git a/test/lib/e2e/utils.test.js b/test/lib/e2e/utils.test.js index d0fa8571..8638ac56 100644 --- a/test/lib/e2e/utils.test.js +++ b/test/lib/e2e/utils.test.js @@ -601,5 +601,21 @@ describe('utils', () => { ).wallet.checkBalances(NULL_ADDRESS), ).resolves.toBeDefined(); }); + test('allowExperimentalNetworks option allow creating signer connected to an experimental network', async () => { + expect(() => + utils.getSignerFromPrivateKey( + 'arbitrum-sepolia-testnet', + getRandomWallet().privateKey, + ), + ).toThrowError('Invalid provider host name or url'); + + const signer = utils.getSignerFromPrivateKey( + 'arbitrum-sepolia-testnet', + getRandomWallet().privateKey, + { allowExperimentalNetworks: true }, + ); + const nonce = await signer.getNonce(); + expect(nonce).toBe(0); + }); }); }); diff --git a/test/lib/unit/config.test.js b/test/lib/unit/config.test.js index a0c3312b..b9100d45 100644 --- a/test/lib/unit/config.test.js +++ b/test/lib/unit/config.test.js @@ -16,7 +16,7 @@ describe('getId()', () => { describe('getChainDefaults', () => { test('id 134 returns bellecour config', () => { - expect(getChainDefaults({ id: 134 })).toEqual({ + expect(getChainDefaults(134)).toEqual({ bridge: { bridgedChainId: '1', contract: '0x188A4376a1D818bF2434972Eb34eFd57102a19b7', @@ -40,12 +40,12 @@ describe('getChainDefaults', () => { }); }); test('unknown id returns empty object', () => { - expect(getChainDefaults({ id: 0 })).toEqual({}); + expect(getChainDefaults(0)).toEqual({}); }); - test('experimental networks are accessible with `allowExperimental:true` hidden by default', () => { - expect(getChainDefaults({ id: 421614 })).toEqual({}); + test('experimental networks are accessible with `allowExperimentalNetworks:true` hidden by default', () => { + expect(getChainDefaults(421614)).toEqual({}); expect( - getChainDefaults({ id: 421614, allowExperimental: true }).host, + getChainDefaults(421614, { allowExperimentalNetworks: true }).host, ).toBeDefined(); }); }); From 85fe30f73bceab5828d3f52445d5003ef42598c0 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:24:50 +0200 Subject: [PATCH 08/11] refactor: move defaults hub address in config --- .../classes/internal_.IExecContractsClient.md | 4 +- src/common/utils/IExecContractsClient.d.ts | 6 +-- src/common/utils/IExecContractsClient.js | 27 ++----------- src/common/utils/config.js | 7 ++-- src/lib/IExecConfig.js | 40 +++++++++++-------- test/lib/e2e/IExecConfig.test.js | 6 +-- test/lib/unit/config.test.js | 2 +- 7 files changed, 41 insertions(+), 51 deletions(-) diff --git a/docs/classes/internal_.IExecContractsClient.md b/docs/classes/internal_.IExecContractsClient.md index f5b1da05..22cc0c53 100644 --- a/docs/classes/internal_.IExecContractsClient.md +++ b/docs/classes/internal_.IExecContractsClient.md @@ -44,9 +44,9 @@ Create a client for IExec contracts | Name | Type | Description | | :------ | :------ | :------ | | `args` | `Object` | - | -| `args.chainId` | `string` \| `number` | id of the chain to use (used to resolve IExec contract address) | +| `args.chainId` | `string` \| `number` | id of the chain | | `args.confirms?` | `number` | number of block to wait for transactions confirmation (default 1) | -| `args.hubAddress?` | `string` | override the IExec contract address to target a custom instance | +| `args.hubAddress` | `string` | IExec contract address | | `args.isNative?` | `boolean` | true if IExec contract use the chain native token | | `args.provider` | `Provider` | ethers Provider | | `args.signer?` | `Signer` | ethers Signer, required to sign transactions and messages | diff --git a/src/common/utils/IExecContractsClient.d.ts b/src/common/utils/IExecContractsClient.d.ts index c351ea9f..e44b05ea 100644 --- a/src/common/utils/IExecContractsClient.d.ts +++ b/src/common/utils/IExecContractsClient.d.ts @@ -14,13 +14,13 @@ export default class IExecContractsClient { */ signer?: Signer; /** - * id of the chain to use (used to resolve IExec contract address) + * id of the chain */ chainId: number | string; /** - * override the IExec contract address to target a custom instance + * IExec contract address */ - hubAddress?: string; + hubAddress: string; /** * if false set the gasPrice to 0 (default true) */ diff --git a/src/common/utils/IExecContractsClient.js b/src/common/utils/IExecContractsClient.js index 23a3e4ec..dd6bbe6a 100644 --- a/src/common/utils/IExecContractsClient.js +++ b/src/common/utils/IExecContractsClient.js @@ -1,7 +1,6 @@ import Debug from 'debug'; import { Contract } from 'ethers'; import { version as pocoVersion } from '../generated/@iexec/poco/package.js'; -import { networks as iexecProxyNetworks } from '../generated/@iexec/poco/ERC1538Proxy.js'; import iexecTokenDesc from '../generated/@iexec/poco/IexecInterfaceToken.js'; import iexecNativeDesc from '../generated/@iexec/poco/IexecInterfaceNative.js'; import appRegistryDesc from '../generated/@iexec/poco/AppRegistry.js'; @@ -20,17 +19,6 @@ const gasPriceByNetwork = { 134: 0n, }; -const getHubAddress = (chainId) => { - if ( - iexecProxyNetworks && - iexecProxyNetworks[chainId] && - iexecProxyNetworks[chainId].address - ) { - return iexecProxyNetworks[chainId].address; - } - throw Error(`Missing iExec contract default address for chain ${chainId}`); -}; - const getIsNative = (chainId) => nativeNetworks.includes(chainId); const getGasPriceOverride = (chainId) => gasPriceByNetwork[chainId]; @@ -76,19 +64,12 @@ const getContractsDescMap = (isNative) => ({ }, }); -const createClient = ({ - ethSigner, - ethProvider, - chainId, - globalHubAddress, - isNative, -}) => { +const createClient = ({ ethSigner, ethProvider, hubAddress, isNative }) => { const cachedAddresses = {}; + if (!hubAddress) throw Error('Missing iExec contract address'); const contractsDescMap = getContractsDescMap(isNative); - const hubAddress = globalHubAddress || getHubAddress(chainId); - const getContract = (objName, address) => { try { const { contractDesc } = contractsDescMap[objName]; @@ -184,6 +165,7 @@ class IExecContractsClient { } = {}) { const stringChainId = `${chainId}`; if (!provider) throw Error('missing provider key'); + if (!hubAddress) throw Error('missing hubAddress key'); if (!stringChainId) throw Error('missing chainId key'); if (!Number.isInteger(confirms) || confirms <= 0) throw Error('invalid confirms'); @@ -206,8 +188,7 @@ class IExecContractsClient { const client = createClient({ ethSigner: signer, ethProvider: provider, - chainId: stringChainId, - globalHubAddress: hubAddress, + hubAddress, isNative: native, }); diff --git a/src/common/utils/config.js b/src/common/utils/config.js index d5cd3d14..d10d74ad 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -1,14 +1,15 @@ import { Network, EnsPlugin } from 'ethers'; import { TEE_FRAMEWORKS } from './constant.js'; import { address as voucherHubBellecourAddress } from '../generated/@iexec/voucher-contracts/deployments/bellecour/VoucherHubERC1967Proxy.js'; +import { networks as iexecProxyNetworks } from '../generated/@iexec/poco/ERC1538Proxy.js'; const networkConfigs = [ { id: 134, name: 'bellecour', - hub: undefined, // use default + hub: iexecProxyNetworks[134].address, host: 'https://bellecour.iex.ec', - ensRegistry: '0x5f5B93fca68c9C79318d1F3868A354EE67D8c006', // use ethers default '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' + ensRegistry: '0x5f5B93fca68c9C79318d1F3868A354EE67D8c006', ensPublicResolver: '0x1347d8a1840A810B990d0B774A6b7Bb8A1bd62BB', sms: { [TEE_FRAMEWORKS.SCONE]: 'https://sms.iex.ec', @@ -31,7 +32,7 @@ const networkConfigs = [ { id: 1, name: 'mainnet', - hub: undefined, // use default + hub: iexecProxyNetworks[1].address, host: 'mainnet', ensRegistry: undefined, // use ethers default ensPublicResolver: '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41', diff --git a/src/lib/IExecConfig.js b/src/lib/IExecConfig.js index 786e478b..7e109d7a 100644 --- a/src/lib/IExecConfig.js +++ b/src/lib/IExecConfig.js @@ -144,12 +144,18 @@ export default class IExecConfig { const contractsPromise = (async () => { const { chainId } = await networkPromise; const chainConfDefaults = await chainConfDefaultsPromise; + const resolvedHubAddress = hubAddress || chainConfDefaults.hub; + if (!resolvedHubAddress) { + throw new ConfigurationError( + `hubAddress option not set and no default value for your chain ${chainId}`, + ); + } try { return new IExecContractsClient({ chainId, provider, signer, - hubAddress: hubAddress || chainConfDefaults.hub, + hubAddress: resolvedHubAddress, useGas, confirms, isNative, @@ -177,9 +183,7 @@ export default class IExecConfig { ); } const bridgedChainId = - bridgedNetworkConf.chainId !== undefined - ? bridgedNetworkConf.chainId - : chainConfDefaults.bridge && chainConfDefaults.bridge.bridgedChainId; + bridgedNetworkConf.chainId ?? chainConfDefaults.bridge?.bridgedChainId; if (!bridgedChainId) { throw new ConfigurationError( `Missing chainId in bridgedNetworkConf and no default value for your chain ${chainId}`, @@ -189,30 +193,28 @@ export default class IExecConfig { allowExperimentalNetworks, }); const bridgedRpcUrl = - bridgedNetworkConf.rpcURL !== undefined - ? bridgedNetworkConf.rpcURL - : bridgedChainConfDefaults.host; + bridgedNetworkConf.rpcURL ?? bridgedChainConfDefaults.host; if (!bridgedRpcUrl) { throw new ConfigurationError( `Missing rpcURL in bridgedNetworkConf and no default value for bridged chain ${bridgedChainId}`, ); } const bridgedBridgeAddress = - bridgedNetworkConf.bridgeAddress !== undefined - ? bridgedNetworkConf.bridgeAddress - : bridgedChainConfDefaults.bridge && - bridgedChainConfDefaults.bridge.contract; + bridgedNetworkConf.bridgeAddress ?? + bridgedChainConfDefaults.bridge?.contract; if (!bridgedBridgeAddress) { throw new ConfigurationError( `Missing bridgeAddress in bridgedNetworkConf and no default value for bridged chain ${bridgedChainId}`, ); } + const bridgedHubAddress = + bridgedNetworkConf.hubAddress ?? bridgedChainConfDefaults.hub; const contracts = await contractsPromise; return { chainId: bridgedChainId, rpcURL: bridgedRpcUrl, isNative: !contracts.isNative, - hubAddress: bridgedNetworkConf.hubAddress, + hubAddress: bridgedHubAddress, bridgeAddress: bridgedBridgeAddress, }; })(); @@ -223,16 +225,22 @@ export default class IExecConfig { const bridgedContractsPromise = (async () => { const bridgedConf = await bridgedConfPromise; + const { hubAddress, chainId, isNative, rpcURL } = bridgedConf; + if (!hubAddress) { + throw new ConfigurationError( + `Missing hubAddress in bridgedNetworkConf and no default value for bridged chain ${chainId}`, + ); + } try { return new IExecContractsClient({ - chainId: bridgedConf.chainId, - provider: getReadOnlyProvider(bridgedConf.rpcURL, { + chainId, + provider: getReadOnlyProvider(rpcURL, { providers: providerOptions, allowExperimentalNetworks, }), - hubAddress: bridgedConf.hubAddress, + hubAddress, confirms, - isNative: bridgedConf.isNative, + isNative, }); } catch (err) { throw new ConfigurationError( diff --git a/test/lib/e2e/IExecConfig.test.js b/test/lib/e2e/IExecConfig.test.js index ae8bc399..79f23fbf 100644 --- a/test/lib/e2e/IExecConfig.test.js +++ b/test/lib/e2e/IExecConfig.test.js @@ -433,7 +433,7 @@ describe('[IExecConfig]', () => { await expect(config.resolveContractsClient()).rejects.toThrow( Error( - 'Failed to create contracts client: Missing iExec contract default address for chain 421614', + 'hubAddress option not set and no default value for your chain 421614', ), ); }); @@ -542,7 +542,7 @@ describe('[IExecConfig]', () => { await expect(config.resolveContractsClient()).rejects.toThrow( Error( - 'Failed to create contracts client: Missing iExec contract default address for chain 421614', + 'hubAddress option not set and no default value for your chain 421614', ), ); }); @@ -800,7 +800,7 @@ describe('[IExecConfig]', () => { const promise = config.resolveContractsClient(); await expect(promise).rejects.toThrow( Error( - `Failed to create contracts client: Missing iExec contract default address for chain ${unknownTestChain.chainId}`, + `hubAddress option not set and no default value for your chain ${unknownTestChain.chainId}`, ), ); await expect(promise).rejects.toThrow(errors.ConfigurationError); diff --git a/test/lib/unit/config.test.js b/test/lib/unit/config.test.js index b9100d45..a0859b50 100644 --- a/test/lib/unit/config.test.js +++ b/test/lib/unit/config.test.js @@ -24,7 +24,7 @@ describe('getChainDefaults', () => { ensPublicResolver: '0x1347d8a1840A810B990d0B774A6b7Bb8A1bd62BB', ensRegistry: '0x5f5B93fca68c9C79318d1F3868A354EE67D8c006', host: 'https://bellecour.iex.ec', - hub: undefined, + hub: '0x3eca1B216A7DF1C7689aEb259fFB83ADFB894E7f', iexecGateway: 'https://api.market.v8-bellecour.iex.ec', ipfsGateway: 'https://ipfs-gateway.v8-bellecour.iex.ec', name: 'bellecour', From a871cf575575f77310a033caca8408efa11b63a8 Mon Sep 17 00:00:00 2001 From: pjt <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:47:24 +0200 Subject: [PATCH 09/11] docs: update comment Co-authored-by: Robin Le Caignec <72495599+Le-Caignec@users.noreply.github.com> --- src/common/utils/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils/config.js b/src/common/utils/config.js index d10d74ad..9e7ef2d8 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -124,7 +124,7 @@ export const getChainDefaults = ( }; }; -// register ethers unknown networks +// Register unknown networks and their ENS settings for the ethers library networkConfigs.forEach((networkConfig) => { if ( networkConfig.shouldRegisterNetwork && From 3ba252e886bd2a7d2c768317dc32425d3ce1f1ed Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:26:10 +0200 Subject: [PATCH 10/11] refactor: networks are not considered experimental by default --- src/common/utils/config.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common/utils/config.js b/src/common/utils/config.js index 9e7ef2d8..ad8e13b8 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -76,8 +76,7 @@ const networkConfigs = [ export const getId = (idOrName, { allowExperimentalNetworks = false } = {}) => networkConfigs .filter( - ({ isExperimental }) => - allowExperimentalNetworks || isExperimental === false, + ({ isExperimental }) => allowExperimentalNetworks || !isExperimental, ) .find(({ id, name }) => idOrName === name || `${idOrName}` === `${id}`)?.id; @@ -102,8 +101,7 @@ export const getChainDefaults = ( } = networkConfigs .filter( - ({ isExperimental }) => - allowExperimentalNetworks || isExperimental === false, + ({ isExperimental }) => allowExperimentalNetworks || !isExperimental, ) .find((networkConfig) => `${id}` === `${networkConfig.id}`) || {}; From 0125bcffd2aa8e7b9780497234365987d41c5c9d Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:49:09 +0200 Subject: [PATCH 11/11] fix: prevent unneeded errors when voucher is not deployed but not used --- src/common/market/order.js | 2 +- src/common/voucher/voucher.js | 6 +++--- src/common/voucher/voucherHub.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/market/order.js b/src/common/market/order.js index ba70a088..b71a41d1 100644 --- a/src/common/market/order.js +++ b/src/common/market/order.js @@ -783,7 +783,7 @@ const getMatchableVolume = async ( export const estimateMatchOrders = async ({ contracts = throwIfMissing(), - voucherHubAddress = throwIfMissing(), + voucherHubAddress, apporder, datasetorder = NULL_DATASETORDER, workerpoolorder, diff --git a/src/common/voucher/voucher.js b/src/common/voucher/voucher.js index c90d7d6f..6befc0fb 100644 --- a/src/common/voucher/voucher.js +++ b/src/common/voucher/voucher.js @@ -15,7 +15,7 @@ const debug = Debug('iexec:voucher:voucher'); export const fetchVoucherContract = async ( contracts = throwIfMissing(), - voucherHubAddress = throwIfMissing(), + voucherHubAddress, userAddress, { voucherAddress } = {}, ) => { @@ -135,7 +135,7 @@ export const showUserVoucher = async ( export const authorizeRequester = async ( contracts = throwIfMissing(), - voucherHubAddress = throwIfMissing(), + voucherHubAddress, requester, ) => { try { @@ -176,7 +176,7 @@ export const authorizeRequester = async ( export const revokeRequesterAuthorization = async ( contracts = throwIfMissing(), - voucherHubAddress = throwIfMissing(), + voucherHubAddress, requester, ) => { try { diff --git a/src/common/voucher/voucherHub.js b/src/common/voucher/voucherHub.js index 7fdfbe98..61f11bdd 100644 --- a/src/common/voucher/voucherHub.js +++ b/src/common/voucher/voucherHub.js @@ -9,7 +9,7 @@ const debug = Debug('iexec:voucher:voucherHub'); export const fetchVoucherAddress = async ( contracts = throwIfMissing(), - voucherHubAddress = throwIfMissing(), + voucherHubAddress, owner, ) => { try { @@ -31,7 +31,7 @@ export const fetchVoucherAddress = async ( export const isVoucherAddress = async ( contracts = throwIfMissing(), - voucherHubAddress = throwIfMissing(), + voucherHubAddress, voucherAddress, ) => { try {