diff --git a/docs/classes/IExecConfig.md b/docs/classes/IExecConfig.md index a2f48003..71d64f48 100644 --- a/docs/classes/IExecConfig.md +++ b/docs/classes/IExecConfig.md @@ -29,6 +29,7 @@ const wallet = IExecWalletModule.fromConfig(config); - [resolveBridgeBackAddress](IExecConfig.md#resolvebridgebackaddress) - [resolveBridgedContractsClient](IExecConfig.md#resolvebridgedcontractsclient) - [resolveChainId](IExecConfig.md#resolvechainid) +- [resolveCompassURL](IExecConfig.md#resolvecompassurl) - [resolveContractsClient](IExecConfig.md#resolvecontractsclient) - [resolveEnsPublicResolverAddress](IExecConfig.md#resolveenspublicresolveraddress) - [resolveIexecGatewayURL](IExecConfig.md#resolveiexecgatewayurl) @@ -123,6 +124,18 @@ resolve the current chainId ___ +### resolveCompassURL + +▸ **resolveCompassURL**(): `Promise`<`undefined` \| `string`\> + +resolve the current Compass URL + +#### Returns + +`Promise`<`undefined` \| `string`\> + +___ + ### resolveContractsClient ▸ **resolveContractsClient**(): `Promise`<[`IExecContractsClient`](internal_.IExecContractsClient.md)\> diff --git a/docs/classes/errors.ApiCallError.md b/docs/classes/errors.ApiCallError.md index 0288384b..47a5ce15 100644 --- a/docs/classes/errors.ApiCallError.md +++ b/docs/classes/errors.ApiCallError.md @@ -20,6 +20,8 @@ ApiCallError encapsulates an error occurring during a call to an API such as a n ↳↳ [`IpfsGatewayCallError`](errors.IpfsGatewayCallError.md) + ↳↳ [`CompassCallError`](errors.CompassCallError.md) + ↳↳ [`WorkerpoolCallError`](errors.WorkerpoolCallError.md) ## Table of contents diff --git a/docs/classes/errors.CompassCallError.md b/docs/classes/errors.CompassCallError.md new file mode 100644 index 00000000..2d8993c3 --- /dev/null +++ b/docs/classes/errors.CompassCallError.md @@ -0,0 +1,71 @@ +[iexec](../README.md) / [Exports](../modules.md) / [errors](../modules/errors.md) / CompassCallError + +# Class: CompassCallError + +[errors](../modules/errors.md).CompassCallError + +CompassCallError encapsulates an error occurring during a call to the Compass API such as a network error or a server-side internal error. + +## Hierarchy + +- [`ApiCallError`](errors.ApiCallError.md) + + ↳ **`CompassCallError`** + +## Table of contents + +### Constructors + +- [constructor](errors.CompassCallError.md#constructor) + +### Properties + +- [cause](errors.CompassCallError.md#cause) +- [originalError](errors.CompassCallError.md#originalerror) + +## Constructors + +### constructor + +• **new CompassCallError**(`message`, `originalError`): [`CompassCallError`](errors.CompassCallError.md) + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `message` | `string` | A descriptive error message detailing the nature of the error. | +| `originalError` | `Error` | The original Error object that caused this API call error. | + +#### Returns + +[`CompassCallError`](errors.CompassCallError.md) + +#### Inherited from + +[ApiCallError](errors.ApiCallError.md).[constructor](errors.ApiCallError.md#constructor) + +## Properties + +### cause + +• **cause**: `Error` + +The original Error object that caused this API call error. + +#### Inherited from + +[ApiCallError](errors.ApiCallError.md).[cause](errors.ApiCallError.md#cause) + +___ + +### originalError + +• **originalError**: `Error` + +**`Deprecated`** + +use Error cause instead. + +#### Inherited from + +[ApiCallError](errors.ApiCallError.md).[originalError](errors.ApiCallError.md#originalerror) diff --git a/docs/interfaces/IExecConfigOptions.md b/docs/interfaces/IExecConfigOptions.md index cc07914c..cf28fba7 100644 --- a/docs/interfaces/IExecConfigOptions.md +++ b/docs/interfaces/IExecConfigOptions.md @@ -9,6 +9,7 @@ - [allowExperimentalNetworks](IExecConfigOptions.md#allowexperimentalnetworks) - [bridgeAddress](IExecConfigOptions.md#bridgeaddress) - [bridgedNetworkConf](IExecConfigOptions.md#bridgednetworkconf) +- [compassURL](IExecConfigOptions.md#compassurl) - [confirms](IExecConfigOptions.md#confirms) - [defaultTeeFramework](IExecConfigOptions.md#defaultteeframework) - [ensPublicResolverAddress](IExecConfigOptions.md#enspublicresolveraddress) @@ -61,6 +62,14 @@ override the bridged network configuration ___ +### compassURL + +• `Optional` **compassURL**: `string` + +override the compass URL to target a custom instance + +___ + ### confirms • `Optional` **confirms**: `number` diff --git a/docs/modules/errors.md b/docs/modules/errors.md index d7223ea8..7f14a902 100644 --- a/docs/modules/errors.md +++ b/docs/modules/errors.md @@ -8,6 +8,7 @@ - [ApiCallError](../classes/errors.ApiCallError.md) - [BridgeError](../classes/errors.BridgeError.md) +- [CompassCallError](../classes/errors.CompassCallError.md) - [ConfigurationError](../classes/errors.ConfigurationError.md) - [IpfsGatewayCallError](../classes/errors.IpfsGatewayCallError.md) - [MarketCallError](../classes/errors.MarketCallError.md) diff --git a/src/cli/cmd/iexec-task.js b/src/cli/cmd/iexec-task.js index 12fb8756..929a097d 100644 --- a/src/cli/cmd/iexec-task.js +++ b/src/cli/cmd/iexec-task.js @@ -206,13 +206,18 @@ debugTask const onchainData = await show(chain.contracts, taskid); const offchainData = await fetchTaskOffchainInfo( chain.contracts, + chain.compass, taskid, ).catch((e) => { spinner.warn(`Failed to fetch off-chain data: ${e.message}`); }); const appLogs = opts.logs - ? await fetchAllReplicatesLogs(chain.contracts, taskid).catch((e) => { + ? await fetchAllReplicatesLogs( + chain.contracts, + chain.compass, + taskid, + ).catch((e) => { spinner.warn(`Failed to fetch app logs: ${e.message}`); }) : undefined; diff --git a/src/cli/cmd/iexec-workerpool.js b/src/cli/cmd/iexec-workerpool.js index a3f14b12..d0bff138 100644 --- a/src/cli/cmd/iexec-workerpool.js +++ b/src/cli/cmd/iexec-workerpool.js @@ -212,7 +212,7 @@ show throw e; } }), - getWorkerpoolApiUrl(chain.contracts, addressOrIndex), + getWorkerpoolApiUrl(chain.contracts, chain.compass, addressOrIndex), ]); } else { showInfo = await showUserWorkerpool( @@ -228,7 +228,11 @@ show throw e; } }), - getWorkerpoolApiUrl(chain.contracts, showInfo.objAddress), + getWorkerpoolApiUrl( + chain.contracts, + chain.compass, + showInfo.objAddress, + ), ]); } const { workerpool, objAddress } = showInfo; diff --git a/src/cli/utils/fs.js b/src/cli/utils/fs.js index cfabb020..ac5348c4 100644 --- a/src/cli/utils/fs.js +++ b/src/cli/utils/fs.js @@ -33,6 +33,7 @@ const chainConfSchema = () => resultProxy: string(), ipfsGateway: string(), iexecGateway: string(), + compass: string(), pocoSubgraph: string(), voucherSubgraph: string(), native: boolean(), diff --git a/src/common/execution/debug.js b/src/common/execution/debug.js index 64847bc8..f0fd4df8 100644 --- a/src/common/execution/debug.js +++ b/src/common/execution/debug.js @@ -13,12 +13,13 @@ import { WORKERPOOL_URL_TEXT_RECORD_KEY } from '../utils/constant.js'; import { jsonApi, getAuthorization } from '../utils/api-utils.js'; import { checkSigner } from '../utils/utils.js'; import { getAddress } from '../wallet/address.js'; -import { WorkerpoolCallError } from '../utils/errors.js'; +import { CompassCallError, WorkerpoolCallError } from '../utils/errors.js'; const debug = Debug('iexec:execution:debug'); export const getWorkerpoolApiUrl = async ( contracts = throwIfMissing(), + compassUrl, workerpoolAddress, ) => { try { @@ -28,6 +29,21 @@ export const getWorkerpoolApiUrl = async ( .required() .label('workerpool address') .validate(workerpoolAddress); + + // Compass base workerpool API URL resolution + if (compassUrl) { + const json = await jsonApi.get({ + api: compassUrl, + endpoint: `/${contracts.chainId}/workerpools/${vAddress}`, + ApiCallErrorClass: CompassCallError, + }); + if (!json?.apiUrl) { + throw new Error(`No apiUrl found in compass response`); + } + return json.apiUrl; + } + + // ENS based workerpool API URL resolution const name = await lookupAddress(contracts, vAddress).catch(() => { /** return undefined */ }); @@ -53,6 +69,7 @@ export const getWorkerpoolApiUrl = async ( const getTaskOffchainApiUrl = async ( contracts = throwIfMissing(), + compassUrl, taskid = throwIfMissing(), ) => { try { @@ -63,7 +80,11 @@ const getTaskOffchainApiUrl = async ( if (!workerpool) { throw Error(`Cannot find task's workerpool`); } - const workerpoolApiUrl = await getWorkerpoolApiUrl(contracts, workerpool); + const workerpoolApiUrl = await getWorkerpoolApiUrl( + contracts, + compassUrl, + workerpool, + ); if (!workerpoolApiUrl) { throw Error(`Impossible to resolve API url for workerpool ${workerpool}`); } @@ -76,11 +97,16 @@ const getTaskOffchainApiUrl = async ( export const fetchTaskOffchainInfo = async ( contracts = throwIfMissing(), + compassURL, taskid = throwIfMissing(), ) => { try { const vTaskid = await bytes32Schema().validate(taskid); - const workerpoolApiUrl = await getTaskOffchainApiUrl(contracts, vTaskid); + const workerpoolApiUrl = await getTaskOffchainApiUrl( + contracts, + compassURL, + vTaskid, + ); const data = await jsonApi.get({ api: workerpoolApiUrl, endpoint: `/tasks/${vTaskid}`, @@ -106,12 +132,17 @@ export const fetchTaskOffchainInfo = async ( export const fetchAllReplicatesLogs = async ( contracts = throwIfMissing(), + compassURL, taskid = throwIfMissing(), ) => { try { checkSigner(contracts); const vTaskid = await bytes32Schema().validate(taskid); - const workerpoolApiUrl = await getTaskOffchainApiUrl(contracts, vTaskid); + const workerpoolApiUrl = await getTaskOffchainApiUrl( + contracts, + compassURL, + vTaskid, + ); const { dealid } = await taskShow(contracts, vTaskid); const { requester } = await dealShow(contracts, dealid); const userAddress = await getAddress(contracts); diff --git a/src/common/utils/config.js b/src/common/utils/config.js index ad8e13b8..08d2d8d2 100644 --- a/src/common/utils/config.js +++ b/src/common/utils/config.js @@ -18,6 +18,7 @@ const networkConfigs = [ resultProxy: 'https://result.v8-bellecour.iex.ec', ipfsGateway: 'https://ipfs-gateway.v8-bellecour.iex.ec', iexecGateway: 'https://api.market.v8-bellecour.iex.ec', + compass: undefined, // no compass using ENS pocoSubgraph: 'https://thegraph.iex.ec/subgraphs/name/bellecour/poco-v5', voucherHub: voucherHubBellecourAddress, voucherSubgraph: @@ -40,6 +41,7 @@ const networkConfigs = [ resultProxy: undefined, // no protocol running ipfsGateway: undefined, // no protocol running iexecGateway: undefined, // no protocol running + compass: undefined, // no protocol running pocoSubgraph: undefined, // no protocol running voucherHub: undefined, // no voucher voucherSubgraph: undefined, // no voucher @@ -55,14 +57,15 @@ const networkConfigs = [ name: 'arbitrum-sepolia-testnet', hub: '0x14B465079537655E1662F012e99EBa3863c8B9E0', host: 'https://sepolia-rollup.arbitrum.io/rpc', - ensRegistry: undefined, // TODO: not supported - ensPublicResolver: undefined, // TODO: not supported + ensRegistry: undefined, // not supported + ensPublicResolver: undefined, // 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', + compass: 'https://compass.arbitrum-sepolia-testnet.iex.ec', pocoSubgraph: 'https://thegraph.arbitrum-sepolia-testnet.iex.ec/api/subgraphs/id/2GCj8gzLCihsiEDq8cYvC5nUgK6VfwZ6hm3Wj8A3kcxz', voucherHub: undefined, // no voucher @@ -94,6 +97,7 @@ export const getChainDefaults = ( resultProxy, iexecGateway, ipfsGateway, + compass, pocoSubgraph, voucherHub, voucherSubgraph, @@ -115,6 +119,7 @@ export const getChainDefaults = ( resultProxy, iexecGateway, ipfsGateway, + compass, pocoSubgraph, voucherHub, voucherSubgraph, diff --git a/src/common/utils/errors.js b/src/common/utils/errors.js index 24704569..da9d2885 100644 --- a/src/common/utils/errors.js +++ b/src/common/utils/errors.js @@ -118,6 +118,13 @@ export class IpfsGatewayCallError extends ApiCallError { } } +export class CompassCallError extends ApiCallError { + constructor(message, originalError) { + super(`Compass API error: ${message}`, originalError); + this.name = this.constructor.name; + } +} + export class WorkerpoolCallError extends ApiCallError { constructor(message, ...args) { super(`Workerpool API error: ${message}`, ...args); diff --git a/src/lib/IExecConfig.d.ts b/src/lib/IExecConfig.d.ts index d00e3abd..e9b7a284 100644 --- a/src/lib/IExecConfig.d.ts +++ b/src/lib/IExecConfig.d.ts @@ -102,6 +102,12 @@ export interface IExecConfigOptions { * override the IExec market URL to target a custom instance */ iexecGatewayURL?: string; + + /** + * @experimental + * override the compass URL to target a custom instance + */ + compassURL?: string; /** * override the PoCo subgraph URL to target a custom instance */ @@ -186,6 +192,11 @@ export default class IExecConfig { * resolve the current IExec market URL */ resolveIexecGatewayURL(): Promise; + /** + * @experimental + * resolve the current Compass URL + */ + resolveCompassURL(): Promise; /** * resolve the current IPFS gateway URL */ diff --git a/src/lib/IExecConfig.js b/src/lib/IExecConfig.js index 7e109d7a..89ca24fc 100644 --- a/src/lib/IExecConfig.js +++ b/src/lib/IExecConfig.js @@ -29,6 +29,7 @@ export default class IExecConfig { resultProxyURL, ipfsGatewayURL, iexecGatewayURL, + compassURL, pocoSubgraphURL, voucherSubgraphURL, defaultTeeFramework, @@ -302,6 +303,11 @@ export default class IExecConfig { ); }; + this.resolveCompassURL = async () => { + const chainConfDefaults = await chainConfDefaultsPromise; + return compassURL || chainConfDefaults.compass; + }; + this.resolveIpfsGatewayURL = async () => { const { chainId } = await networkPromise; const chainConfDefaults = await chainConfDefaultsPromise; diff --git a/src/lib/IExecTaskModule.js b/src/lib/IExecTaskModule.js index d25a80cc..7e3995eb 100644 --- a/src/lib/IExecTaskModule.js +++ b/src/lib/IExecTaskModule.js @@ -25,9 +25,14 @@ export default class IExecTaskModule extends IExecModule { this.fetchLogs = async (taskid) => fetchAllReplicatesLogs( await this.config.resolveContractsClient(), + await this.config.resolveCompassURL(), taskid, ); this.fetchOffchainInfo = async (taskid) => - fetchTaskOffchainInfo(await this.config.resolveContractsClient(), taskid); + fetchTaskOffchainInfo( + await this.config.resolveContractsClient(), + await this.config.resolveCompassURL(), + taskid, + ); } } diff --git a/src/lib/IExecWorkerpoolModule.js b/src/lib/IExecWorkerpoolModule.js index 7305cc32..174e7c8e 100644 --- a/src/lib/IExecWorkerpoolModule.js +++ b/src/lib/IExecWorkerpoolModule.js @@ -36,6 +36,7 @@ export default class IExecWorkerpoolModule extends IExecModule { this.getWorkerpoolApiUrl = async (workerpoolAddress) => getWorkerpoolApiUrl( await this.config.resolveContractsClient(), + await this.config.resolveCompassURL(), workerpoolAddress, ); this.predictWorkerpoolAddress = async (workerpool) => diff --git a/src/lib/errors.d.ts b/src/lib/errors.d.ts index fcb18192..e90da877 100644 --- a/src/lib/errors.d.ts +++ b/src/lib/errors.d.ts @@ -149,6 +149,11 @@ export class MarketCallError extends ApiCallError {} */ export class IpfsGatewayCallError extends ApiCallError {} +/** + * CompassCallError encapsulates an error occurring during a call to the Compass API such as a network error or a server-side internal error. + */ +export class CompassCallError extends ApiCallError {} + /** * WorkerpoolCallError encapsulates an error occurring during a call to a workerpool API such as a network error or a server-side internal error. */ diff --git a/test/lib/e2e/IExecWorkerpoolModule.test.js b/test/lib/e2e/IExecWorkerpoolModule.test.js index 2082be15..1db108cb 100644 --- a/test/lib/e2e/IExecWorkerpoolModule.test.js +++ b/test/lib/e2e/IExecWorkerpoolModule.test.js @@ -3,9 +3,15 @@ import { describe, test, expect } from '@jest/globals'; import { BN } from 'bn.js'; import { deployRandomWorkerpool, getTestConfig } from '../lib-test-utils.js'; -import { TEST_CHAINS, getId, getRandomAddress } from '../../test-utils.js'; +import { + SERVICE_HTTP_500_URL, + SERVICE_UNREACHABLE_URL, + TEST_CHAINS, + getId, + getRandomAddress, +} from '../../test-utils.js'; import '../../jest-setup.js'; -import { errors } from '../../../src/lib/index.js'; +import { errors, IExec } from '../../../src/lib/index.js'; const iexecTestChain = TEST_CHAINS['bellecour-fork']; @@ -233,25 +239,76 @@ describe('workerpool', () => { }); describe('getWorkerpoolApiUrl()', () => { - test('resolves the url', async () => { - const { iexec: readOnlyIExec } = getTestConfig(iexecTestChain)({ - readOnly: true, + describe('on networks with ENS', () => { + test('resolves the url against ENS', async () => { + const { iexec: readOnlyIExec } = getTestConfig(iexecTestChain)({ + readOnly: true, + }); + const { iexec } = getTestConfig(iexecTestChain)(); + const { address } = await deployRandomWorkerpool(iexec); + const resNoApiUrl = + await readOnlyIExec.workerpool.getWorkerpoolApiUrl(address); + expect(resNoApiUrl).toBe(undefined); + const label = address.toLowerCase(); + const domain = 'pools.iexec.eth'; + const name = `${label}.${domain}`; + await iexec.ens.claimName(label, domain); + await iexec.ens.configureResolution(name, address); + const apiUrl = 'https://my-workerpool.com'; + await iexec.workerpool.setWorkerpoolApiUrl(address, apiUrl); + const resConfigured = + await readOnlyIExec.workerpool.getWorkerpoolApiUrl(address); + expect(resConfigured).toBe(apiUrl); + }); + }); + + describe('on networks relying on compass', () => { + test('resolves the url against Compass', async () => { + // TODO include compass in stack instead of using arbitrum-sepolia-testnet + const readOnlyIExec = new IExec( + { ethProvider: 'arbitrum-sepolia-testnet' }, + { allowExperimentalNetworks: true }, + ); + const apiUrl = await readOnlyIExec.workerpool.getWorkerpoolApiUrl( + '0x39C3CdD91A7F1c4Ed59108a9da4E79dE9A1C1b59', + ); + expect(typeof apiUrl).toBe('string'); + expect(apiUrl.startsWith('https://')).toBe(true); + }); + + test('fails with CompassCallError if Compass is not available', async () => { + const iexecCompassNotFound = new IExec( + { ethProvider: 'arbitrum-sepolia-testnet' }, + { + allowExperimentalNetworks: true, + compassURL: SERVICE_UNREACHABLE_URL, + }, + ); + await expect( + iexecCompassNotFound.workerpool.getWorkerpoolApiUrl( + getRandomAddress(), + ), + ).rejects.toThrow( + new errors.CompassCallError( + `Connection to ${SERVICE_UNREACHABLE_URL} failed with a network error`, + ), + ); + + const iexecCompassInternalError = new IExec( + { ethProvider: 'arbitrum-sepolia-testnet' }, + { allowExperimentalNetworks: true, compassURL: SERVICE_HTTP_500_URL }, + ); + await expect( + iexecCompassInternalError.workerpool.getWorkerpoolApiUrl( + getRandomAddress(), + ), + ).rejects.toThrow( + new errors.CompassCallError( + `Server at ${SERVICE_HTTP_500_URL} encountered an internal error`, + Error('Server internal error: 500 Internal Server Error'), + ), + ); }); - const { iexec } = getTestConfig(iexecTestChain)(); - const { address } = await deployRandomWorkerpool(iexec); - const resNoApiUrl = - await readOnlyIExec.workerpool.getWorkerpoolApiUrl(address); - expect(resNoApiUrl).toBe(undefined); - const label = address.toLowerCase(); - const domain = 'pools.iexec.eth'; - const name = `${label}.${domain}`; - await iexec.ens.claimName(label, domain); - await iexec.ens.configureResolution(name, address); - const apiUrl = 'https://my-workerpool.com'; - await iexec.workerpool.setWorkerpoolApiUrl(address, apiUrl); - const resConfigured = - await readOnlyIExec.workerpool.getWorkerpoolApiUrl(address); - expect(resConfigured).toBe(apiUrl); }); });