From a98fbc760bd1b7f8b3b989d8bb4cf19a4fc1e924 Mon Sep 17 00:00:00 2001 From: Pierre Jeanjacquot <26487010+PierreJeanjacquot@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:59:15 +0100 Subject: [PATCH] fix: add chain specific feature check errors --- src/common/ens/registration.js | 12 +++- src/common/ens/resolution.js | 7 ++ src/common/ens/text-record.js | 6 ++ src/common/execution/debug.js | 8 +++ src/common/execution/workerpool.js | 8 +++ src/common/market/order.js | 13 ++++ src/common/utils/config.js | 39 +++++++++++ src/common/voucher/voucher.js | 7 ++ src/common/voucher/voucherHub.js | 5 ++ src/common/wallet/bridge.js | 20 ++++++ test/lib/unit/config.test.js | 107 ++++++++++++++++++++++++++++- 11 files changed, 229 insertions(+), 3 deletions(-) diff --git a/src/common/ens/registration.js b/src/common/ens/registration.js index 296d407a..b0a5e501 100644 --- a/src/common/ens/registration.js +++ b/src/common/ens/registration.js @@ -19,6 +19,10 @@ import { checkSigner } from '../utils/utils.js'; import { NULL_ADDRESS, APP, DATASET, WORKERPOOL } from '../utils/constant.js'; import { getEnsRegistryAddress } from './registry.js'; import { getOwner, lookupAddress } from './resolution.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:ens:registration'); @@ -34,6 +38,7 @@ export const getDefaultDomain = async ( address, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); const vAddress = await addressSchema({ ethProvider: contracts.provider, }) @@ -66,6 +71,7 @@ export const registerFifsEns = async ( domain = FIFS_DOMAINS.default, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); checkSigner(contracts); const vDomain = await ensDomainSchema().validate(domain); const vLabel = await ensLabelSchema().validate(label); @@ -129,12 +135,13 @@ export const obsConfigureResolution = ( address, ) => new Observable((observer) => { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); + checkSigner(contracts); + const safeObserver = new SafeObserver(observer); let abort = false; - const configure = async () => { try { - checkSigner(contracts); const vAddress = address !== undefined ? await addressSchema({ @@ -367,6 +374,7 @@ export const configureResolution = async ( address, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); checkSigner(contracts); const vAddress = address !== undefined diff --git a/src/common/ens/resolution.js b/src/common/ens/resolution.js index dbf59388..85c62459 100644 --- a/src/common/ens/resolution.js +++ b/src/common/ens/resolution.js @@ -8,6 +8,10 @@ import { } from '../utils/validator.js'; import { wrapCall } from '../utils/errorWrappers.js'; import { getEnsRegistryAddress, checkEns } from './registry.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:ens:resolution'); @@ -16,6 +20,7 @@ export const getOwner = async ( name = throwIfMissing(), ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); const vName = await ensDomainSchema().validate(name); const nameHash = namehash(vName); const ensAddress = await getEnsRegistryAddress(contracts); @@ -36,6 +41,7 @@ export const resolveName = async ( name = throwIfMissing(), ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); const vName = await ensDomainSchema().validate(name); await checkEns(contracts); return await wrapCall(contracts.provider.resolveName(vName)); @@ -47,6 +53,7 @@ export const resolveName = async ( export const lookupAddress = async (contracts = throwIfMissing(), address) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); const vAddress = await addressSchema({ ethProvider: contracts.provider, }) diff --git a/src/common/ens/text-record.js b/src/common/ens/text-record.js index 23e262b2..82152434 100644 --- a/src/common/ens/text-record.js +++ b/src/common/ens/text-record.js @@ -11,6 +11,10 @@ import { getAddress } from '../wallet/address.js'; import { wrapSend, wrapWait, wrapCall } from '../utils/errorWrappers.js'; import { NULL_ADDRESS } from '../utils/constant.js'; import { getOwner } from './resolution.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:ens:text-record'); @@ -20,6 +24,7 @@ export const readTextRecord = async ( key, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); const vName = await ensDomainSchema().validate(name); const vKey = await textRecordKeySchema().validate(key); const node = namehash(vName); @@ -52,6 +57,7 @@ export const setTextRecord = async ( value = '', ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.ENS); const vName = await ensDomainSchema().validate(name); const vKey = await textRecordKeySchema().validate(key); const vValue = await textRecordValueSchema().validate(value); diff --git a/src/common/execution/debug.js b/src/common/execution/debug.js index c4cb0862..2de6ba23 100644 --- a/src/common/execution/debug.js +++ b/src/common/execution/debug.js @@ -14,6 +14,10 @@ import { jsonApi, getAuthorization } from '../utils/api-utils.js'; import { checkSigner } from '../utils/utils.js'; import { getAddress } from '../wallet/address.js'; import { CompassCallError, WorkerpoolCallError } from '../utils/errors.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:execution:debug'); @@ -32,6 +36,10 @@ export const getWorkerpoolApiUrl = async ( // Compass base workerpool API URL resolution if (compassUrl) { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.COMPASS, + ); const json = await jsonApi.get({ api: compassUrl, endpoint: `/${contracts.chainId}/workerpools/${vAddress}`, diff --git a/src/common/execution/workerpool.js b/src/common/execution/workerpool.js index 83d9e6bb..6a27522b 100644 --- a/src/common/execution/workerpool.js +++ b/src/common/execution/workerpool.js @@ -7,6 +7,10 @@ import { import { lookupAddress } from '../ens/resolution.js'; import { setTextRecord } from '../ens/text-record.js'; import { WORKERPOOL_URL_TEXT_RECORD_KEY } from '../utils/constant.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:execution:workerpool'); @@ -16,6 +20,10 @@ export const setWorkerpoolApiUrl = async ( url, ) => { try { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.WORKERPOOL_API_URL_REGISTRATION, + ); const vAddress = await addressSchema({ ethProvider: contracts.provider, }) diff --git a/src/common/market/order.js b/src/common/market/order.js index 72084bc3..2bc84b66 100644 --- a/src/common/market/order.js +++ b/src/common/market/order.js @@ -63,6 +63,10 @@ import { import { getVoucherHubContract } from '../utils/voucher-utils.js'; import { checkAllowance } from '../account/allowance.js'; import { fetchVoucherContract } from '../voucher/voucher.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:market:order'); @@ -818,6 +822,9 @@ export const estimateMatchOrders = async ({ .label('voucherAddress') .validate(voucherAddress), ]); + if (!vUseVoucher) { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.VOUCHER); + } const matchableVolume = await getMatchableVolume( contracts, vAppOrder, @@ -938,6 +945,12 @@ export const matchOrders = async ({ .label('voucherAddress') .validate(voucherAddress), ]); + if (vUseVoucher) { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.VOUCHER, + ); + } // check resulting tag await tagSchema() diff --git a/src/common/utils/config.js b/src/common/utils/config.js index 7f119729..43ff7603 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -2,6 +2,14 @@ import { EnsPlugin, Network } from 'ethers'; import { address as voucherHubBellecourAddress } from '../generated/@iexec/voucher-contracts/deployments/bellecour/VoucherHubERC1967Proxy.js'; import { TEE_FRAMEWORKS } from './constant.js'; +export const CHAIN_SPECIFIC_FEATURES = { + ENS: 'ENS', + WORKERPOOL_API_URL_REGISTRATION: 'Workerpool API Registration', + VOUCHER: 'iExec Voucher', + COMPASS: 'iExec Compass', + XRLC_BRIDGE: 'iExec xRLC Bridge', +}; + const networkConfigs = [ { id: 134, @@ -28,6 +36,7 @@ const networkConfigs = [ }, shouldRegisterNetwork: true, isExperimental: false, + notImplemented: [CHAIN_SPECIFIC_FEATURES.COMPASS], }, { id: 1, @@ -50,6 +59,10 @@ const networkConfigs = [ }, shouldRegisterNetwork: false, isExperimental: false, + notImplemented: [ + CHAIN_SPECIFIC_FEATURES.COMPASS, + CHAIN_SPECIFIC_FEATURES.VOUCHER, + ], }, { id: 421614, @@ -72,6 +85,12 @@ const networkConfigs = [ bridge: {}, // no bridge shouldRegisterNetwork: false, isExperimental: false, + notImplemented: [ + CHAIN_SPECIFIC_FEATURES.ENS, + CHAIN_SPECIFIC_FEATURES.WORKERPOOL_API_URL_REGISTRATION, + CHAIN_SPECIFIC_FEATURES.VOUCHER, + CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE, + ], }, { id: 42161, @@ -93,6 +112,12 @@ const networkConfigs = [ voucherSubgraph: undefined, // no voucher bridge: {}, // no bridge shouldRegisterNetwork: false, + notImplemented: [ + CHAIN_SPECIFIC_FEATURES.ENS, + CHAIN_SPECIFIC_FEATURES.WORKERPOOL_API_URL_REGISTRATION, + CHAIN_SPECIFIC_FEATURES.VOUCHER, + CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE, + ], }, ]; @@ -147,6 +172,20 @@ export const getChainDefaults = ( }; }; +export const checkImplementedOnChain = (chainId, featureName) => { + const networkConfig = networkConfigs.find( + (network) => `${network.id}` === `${chainId}`, + ); + if ( + networkConfig?.notImplemented && + networkConfig.notImplemented.includes(featureName) + ) { + throw new Error( + `${featureName} is not available on network ${networkConfig.name}`, + ); + } +}; + // Register unknown networks and their ENS settings for the ethers library networkConfigs.forEach((networkConfig) => { if ( diff --git a/src/common/voucher/voucher.js b/src/common/voucher/voucher.js index 1a5d8334..991d8c47 100644 --- a/src/common/voucher/voucher.js +++ b/src/common/voucher/voucher.js @@ -10,6 +10,10 @@ import { getVoucherContract, getVoucherSubgraphClient, } from '../utils/voucher-utils.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:voucher:voucher'); @@ -78,6 +82,7 @@ export const showUserVoucher = async ( owner = throwIfMissing(), ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.VOUCHER); const vOwner = await addressSchema({ ethProvider: contracts.provider, }) @@ -139,6 +144,7 @@ export const authorizeRequester = async ( requester, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.VOUCHER); checkSigner(contracts); const userAddress = await getAddress(contracts); const vRequester = await addressSchema({ @@ -180,6 +186,7 @@ export const revokeRequesterAuthorization = async ( requester, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.VOUCHER); checkSigner(contracts); const userAddress = await getAddress(contracts); const vRequester = await addressSchema({ diff --git a/src/common/voucher/voucherHub.js b/src/common/voucher/voucherHub.js index 61f11bdd..55d11e01 100644 --- a/src/common/voucher/voucherHub.js +++ b/src/common/voucher/voucherHub.js @@ -4,6 +4,10 @@ import { addressSchema, throwIfMissing } from '../utils/validator.js'; import { wrapCall } from '../utils/errorWrappers.js'; import { NULL_ADDRESS } from '../utils/constant.js'; import { getVoucherHubContract } from '../utils/voucher-utils.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:voucher:voucherHub'); @@ -13,6 +17,7 @@ export const fetchVoucherAddress = async ( owner, ) => { try { + checkImplementedOnChain(contracts.chainId, CHAIN_SPECIFIC_FEATURES.VOUCHER); const vOwner = await addressSchema({ ethProvider: contracts.provider }) .required() .label('owner') diff --git a/src/common/wallet/bridge.js b/src/common/wallet/bridge.js index a069c8b2..aebc704f 100644 --- a/src/common/wallet/bridge.js +++ b/src/common/wallet/bridge.js @@ -21,6 +21,10 @@ import { abi as HomeBridgeErcToNativeAbi } from './abi/HomeBridgeErcToNative.js' import { getAddress } from './address.js'; import { getRlcBalance } from './balance.js'; import { sendRLC } from './send.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, +} from '../utils/config.js'; const debug = Debug('iexec:wallet:bridge'); @@ -69,6 +73,10 @@ export const obsBridgeToSidechain = ( { sidechainBridgeAddress, bridgedContracts } = {}, ) => new Observable((observer) => { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE, + ); const safeObserver = new SafeObserver(observer); let abort; let stopWatchPromise; @@ -289,6 +297,10 @@ export const bridgeToSidechain = async ( nRlcAmount = throwIfMissing(), { sidechainBridgeAddress, bridgedContracts } = {}, ) => { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE, + ); checkSigner(contracts); let sendTxHash; let receiveTxHash; @@ -326,6 +338,10 @@ export const obsBridgeToMainchain = ( { mainchainBridgeAddress, bridgedContracts } = {}, ) => new Observable((observer) => { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE, + ); const safeObserver = new SafeObserver(observer); let abort; let stopWatchPromise; @@ -517,6 +533,10 @@ export const bridgeToMainchain = async ( nRlcAmount = throwIfMissing(), { mainchainBridgeAddress, bridgedContracts } = {}, ) => { + checkImplementedOnChain( + contracts.chainId, + CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE, + ); checkSigner(contracts); let sendTxHash; let receiveTxHash; diff --git a/test/lib/unit/config.test.js b/test/lib/unit/config.test.js index 3176cc0d..a15950e4 100644 --- a/test/lib/unit/config.test.js +++ b/test/lib/unit/config.test.js @@ -1,6 +1,11 @@ import { describe, test, expect } from '@jest/globals'; import { Network } from 'ethers'; -import { getChainDefaults, getId } from '../../../src/common/utils/config.js'; +import { + CHAIN_SPECIFIC_FEATURES, + checkImplementedOnChain, + getChainDefaults, + getId, +} from '../../../src/common/utils/config.js'; describe('getId()', () => { test('chain id as number returns id', () => { @@ -61,3 +66,103 @@ describe('Networks', () => { expect(networkFromName.name).toBe('bellecour'); }); }); + +describe('checkImplementedOnChain', () => { + describe(`feature ${CHAIN_SPECIFIC_FEATURES.ENS}`, () => { + const feature = CHAIN_SPECIFIC_FEATURES.ENS; + test('is implemented on bellecour', () => { + expect(() => checkImplementedOnChain(134, feature)).not.toThrow(); + }); + test('is implemented on mainnet', () => { + expect(() => checkImplementedOnChain(1, feature)).not.toThrow(); + }); + test('is not implemented on arbitrum-mainnet', () => { + expect(() => checkImplementedOnChain(42161, feature)).toThrow( + `${feature} is not available on network arbitrum-mainnet`, + ); + }); + test('is not implemented on arbitrum-sepolia-testnet', () => { + expect(() => checkImplementedOnChain(421614, feature)).toThrow( + `${feature} is not available on network arbitrum-sepolia-testnet`, + ); + }); + }); + describe(`feature ${CHAIN_SPECIFIC_FEATURES.WORKERPOOL_API_URL_REGISTRATION}`, () => { + const feature = CHAIN_SPECIFIC_FEATURES.WORKERPOOL_API_URL_REGISTRATION; + test('is implemented on bellecour', () => { + expect(() => checkImplementedOnChain(134, feature)).not.toThrow(); + }); + test('is implemented on mainnet', () => { + expect(() => checkImplementedOnChain(1, feature)).not.toThrow(); + }); + test('is not implemented on arbitrum-mainnet', () => { + expect(() => checkImplementedOnChain(42161, feature)).toThrow( + `${feature} is not available on network arbitrum-mainnet`, + ); + }); + test('is not implemented on arbitrum-sepolia-testnet', () => { + expect(() => checkImplementedOnChain(421614, feature)).toThrow( + `${feature} is not available on network arbitrum-sepolia-testnet`, + ); + }); + }); + describe(`feature ${CHAIN_SPECIFIC_FEATURES.VOUCHER}`, () => { + const feature = CHAIN_SPECIFIC_FEATURES.VOUCHER; + test('is implemented on bellecour', () => { + expect(() => checkImplementedOnChain(134, feature)).not.toThrow(); + }); + test('is not implemented on mainnet', () => { + expect(() => checkImplementedOnChain(1, feature)).toThrow( + `${feature} is not available on network mainnet`, + ); + }); + test('is not implemented on arbitrum-mainnet', () => { + expect(() => checkImplementedOnChain(42161, feature)).toThrow( + `${feature} is not available on network arbitrum-mainnet`, + ); + }); + test('is not implemented on arbitrum-sepolia-testnet', () => { + expect(() => checkImplementedOnChain(421614, feature)).toThrow( + `${feature} is not available on network arbitrum-sepolia-testnet`, + ); + }); + }); + describe(`feature ${CHAIN_SPECIFIC_FEATURES.COMPASS}`, () => { + const feature = CHAIN_SPECIFIC_FEATURES.COMPASS; + test('is not implemented on bellecour', () => { + expect(() => checkImplementedOnChain(134, feature)).toThrow( + `${feature} is not available on network bellecour`, + ); + }); + test('is not implemented on mainnet', () => { + expect(() => checkImplementedOnChain(1, feature)).toThrow( + `${feature} is not available on network mainnet`, + ); + }); + test('is implemented on arbitrum-mainnet', () => { + expect(() => checkImplementedOnChain(42161, feature)).not.toThrow(); + }); + test('is implemented on arbitrum-sepolia-testnet', () => { + expect(() => checkImplementedOnChain(421614, feature)).not.toThrow(); + }); + }); + describe(`feature ${CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE}`, () => { + const feature = CHAIN_SPECIFIC_FEATURES.XRLC_BRIDGE; + test('is implemented on bellecour', () => { + expect(() => checkImplementedOnChain(134, feature)).not.toThrow(); + }); + test('is implemented on mainnet', () => { + expect(() => checkImplementedOnChain(1, feature)).not.toThrow(); + }); + test('is not implemented on arbitrum-mainnet', () => { + expect(() => checkImplementedOnChain(42161, feature)).toThrow( + `${feature} is not available on network arbitrum-mainnet`, + ); + }); + test('is not implemented on arbitrum-sepolia-testnet', () => { + expect(() => checkImplementedOnChain(421614, feature)).toThrow( + `${feature} is not available on network arbitrum-sepolia-testnet`, + ); + }); + }); +});