From 13b5e006e5d073ac12465166b6b37a41335cf3a9 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 09:19:14 +0300 Subject: [PATCH 01/15] chore: sync till 1361 --- docs/env.md | 2 + src/@types/AccessList.ts | 7 + src/@types/commands.ts | 10 + src/components/Indexer/processor.ts | 11 +- .../processors/AddressAddedEventProcessor.ts | 48 ++ .../AddressRemovedEventProcessor.ts | 46 ++ .../processors/NewAccessListEventProcessor.ts | 74 +++ src/components/Indexer/processors/index.ts | 3 + src/components/c2d/compute_engine_docker.ts | 11 + .../core/handler/accessListHandler.ts | 84 ++++ .../core/handler/coreHandlersRegistry.ts | 9 + src/components/core/utils/statusHandler.ts | 3 + src/components/database/BaseDatabase.ts | 29 ++ src/components/database/DatabaseFactory.ts | 17 +- .../database/ElasticSearchDatabase.ts | 273 +++++++++++ src/components/database/TypesenseDatabase.ts | 174 +++++++ src/components/database/TypesenseSchemas.ts | 17 + src/components/database/index.ts | 9 + src/components/httpRoutes/accessList.ts | 65 +++ src/components/httpRoutes/index.ts | 3 + src/test/integration/accessListEvents.test.ts | 438 ++++++++++++++++++ src/test/integration/testUtils.ts | 22 + src/utils/constants.ts | 30 +- 23 files changed, 1379 insertions(+), 6 deletions(-) create mode 100644 src/components/Indexer/processors/AddressAddedEventProcessor.ts create mode 100644 src/components/Indexer/processors/AddressRemovedEventProcessor.ts create mode 100644 src/components/Indexer/processors/NewAccessListEventProcessor.ts create mode 100644 src/components/core/handler/accessListHandler.ts create mode 100644 src/components/httpRoutes/accessList.ts create mode 100644 src/test/integration/accessListEvents.test.ts diff --git a/docs/env.md b/docs/env.md index 8bf52eef7..dd8761849 100644 --- a/docs/env.md +++ b/docs/env.md @@ -127,6 +127,8 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/ ## Compute +- `C2D_DOWNLOAD_TIMEOUT`: Timeout (in seconds) for pulling the algorithm docker image during a C2D job. If the pull exceeds this timeout, the job fails with `PullImageFailed` instead of getting stuck. Defaults to `900` (15 minutes). Example: `900` + The `DOCKER_COMPUTE_ENVIRONMENTS` environment variable is used to configure Docker-based compute environments in Ocean Node. This guide will walk you through the options available for defining `DOCKER_COMPUTE_ENVIRONMENTS` and how to set it up correctly. For configuring compute environments and setting prices for each resource (including pricing units and examples), see [Compute pricing](compute-pricing.md). Example Configuration diff --git a/src/@types/AccessList.ts b/src/@types/AccessList.ts index 242b991d1..88b521710 100644 --- a/src/@types/AccessList.ts +++ b/src/@types/AccessList.ts @@ -4,3 +4,10 @@ export interface AccessList { [chainId: string]: string[] } + +export interface AccessListUser { + wallet: string + tokenId: number + block: number + txId: string +} diff --git a/src/@types/commands.ts b/src/@types/commands.ts index 6ed0f76f4..cbeb6977f 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -29,6 +29,16 @@ export interface FindPeerCommand extends Command { export interface GetP2PPeersCommand extends Command {} export interface GetP2PNetworkStatsCommand extends Command {} +export interface GetAccessListCommand extends Command { + chainId: number + contractAddress: string +} + +export interface SearchAccessListCommand extends Command { + wallet: string + chainId?: number +} + export interface SignedCommand extends Command { nonce: string signature: string diff --git a/src/components/Indexer/processor.ts b/src/components/Indexer/processor.ts index ada273397..98f9d2723 100644 --- a/src/components/Indexer/processor.ts +++ b/src/components/Indexer/processor.ts @@ -17,6 +17,9 @@ import { ExchangeActivatedEventProcessor, ExchangeDeactivatedEventProcessor, ExchangeRateChangedEventProcessor, + NewAccessListEventProcessor, + AddressAddedEventProcessor, + AddressRemovedEventProcessor, ProcessorConstructor } from './processors/index.js' import { findEventByKey } from './utils.js' @@ -36,7 +39,10 @@ const EVENT_PROCESSOR_MAP: Record = { [EVENTS.EXCHANGE_CREATED]: ExchangeCreatedEventProcessor, [EVENTS.EXCHANGE_ACTIVATED]: ExchangeActivatedEventProcessor, [EVENTS.EXCHANGE_DEACTIVATED]: ExchangeDeactivatedEventProcessor, - [EVENTS.EXCHANGE_RATE_CHANGED]: ExchangeRateChangedEventProcessor + [EVENTS.EXCHANGE_RATE_CHANGED]: ExchangeRateChangedEventProcessor, + [EVENTS.NEW_ACCESS_LIST]: NewAccessListEventProcessor, + [EVENTS.ADDRESS_ADDED]: AddressAddedEventProcessor, + [EVENTS.ADDRESS_REMOVED]: AddressRemovedEventProcessor } const processorInstances = new Map() @@ -83,6 +89,7 @@ export const processChunkLogs = async ( (allowedValidatorsList && Object.keys(allowedValidatorsList).length > 0) for (const log of logs) { const event = findEventByKey(log.topics[0]) + if (event && Object.values(EVENTS).includes(event.type)) { // only log & process the ones we support INDEXER_LOGGER.logMessage( @@ -188,6 +195,7 @@ export const processChunkLogs = async ( } } } + return storeEvents } @@ -208,6 +216,7 @@ export const processBlocks = async ( blockLogs && blockLogs.length > 0 ? await processChunkLogs(blockLogs, signer, provider, network, config) : [] + return { lastBlock: lastIndexedBlock + count, foundEvents: events diff --git a/src/components/Indexer/processors/AddressAddedEventProcessor.ts b/src/components/Indexer/processors/AddressAddedEventProcessor.ts new file mode 100644 index 000000000..a14dd511e --- /dev/null +++ b/src/components/Indexer/processors/AddressAddedEventProcessor.ts @@ -0,0 +1,48 @@ +import { ethers, Signer, FallbackProvider, Interface } from 'ethers' +import { INDEXER_LOGGER } from '../../../utils/logging/common.js' +import { LOG_LEVELS_STR } from '../../../utils/logging/Logger.js' +import { BaseEventProcessor } from './BaseProcessor.js' +import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' with { type: 'json' } + +const accessListInterface = new Interface(AccessList.abi) + +export class AddressAddedEventProcessor extends BaseEventProcessor { + async processEvent( + event: ethers.Log, + chainId: number, + signer: Signer, + provider: FallbackProvider + ): Promise { + try { + const decoded = accessListInterface.parseLog({ + topics: Array.from(event.topics), + data: event.data + }) + if (!decoded) return null + + const wallet = decoded.args[0].toString().toLowerCase() + const tokenId = Number(decoded.args[1]) + const contractAddress = event.address.toLowerCase() + + const { accessList } = await this.getDatabase() + const result = await accessList.addUser(chainId, contractAddress, { + wallet, + tokenId, + block: event.blockNumber, + txId: event.transactionHash + }) + + INDEXER_LOGGER.logMessage( + `[AddressAdded] ${wallet} (tokenId=${tokenId}) added to ${contractAddress} on chain ${chainId}` + ) + return result + } catch (err) { + INDEXER_LOGGER.log( + LOG_LEVELS_STR.LEVEL_ERROR, + `Error processing AddressAdded event: ${err.message}`, + true + ) + return null + } + } +} diff --git a/src/components/Indexer/processors/AddressRemovedEventProcessor.ts b/src/components/Indexer/processors/AddressRemovedEventProcessor.ts new file mode 100644 index 000000000..923c1dad1 --- /dev/null +++ b/src/components/Indexer/processors/AddressRemovedEventProcessor.ts @@ -0,0 +1,46 @@ +import { ethers, Signer, FallbackProvider, Interface } from 'ethers' +import { INDEXER_LOGGER } from '../../../utils/logging/common.js' +import { LOG_LEVELS_STR } from '../../../utils/logging/Logger.js' +import { BaseEventProcessor } from './BaseProcessor.js' +import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' with { type: 'json' } + +const accessListInterface = new Interface(AccessList.abi) + +export class AddressRemovedEventProcessor extends BaseEventProcessor { + async processEvent( + event: ethers.Log, + chainId: number, + signer: Signer, + provider: FallbackProvider + ): Promise { + try { + const decoded = accessListInterface.parseLog({ + topics: Array.from(event.topics), + data: event.data + }) + if (!decoded) return null + + const tokenId = Number(decoded.args[0]) + const contractAddress = event.address.toLowerCase() + + const { accessList } = await this.getDatabase() + const result = await accessList.removeUserByTokenId( + chainId, + contractAddress, + tokenId + ) + + INDEXER_LOGGER.logMessage( + `[AddressRemoved] tokenId=${tokenId} removed from ${contractAddress} on chain ${chainId}` + ) + return result + } catch (err) { + INDEXER_LOGGER.log( + LOG_LEVELS_STR.LEVEL_ERROR, + `Error processing AddressRemoved event: ${err.message}`, + true + ) + return null + } + } +} diff --git a/src/components/Indexer/processors/NewAccessListEventProcessor.ts b/src/components/Indexer/processors/NewAccessListEventProcessor.ts new file mode 100644 index 000000000..a28ee8de5 --- /dev/null +++ b/src/components/Indexer/processors/NewAccessListEventProcessor.ts @@ -0,0 +1,74 @@ +import { ethers, Signer, FallbackProvider, Interface } from 'ethers' +import { INDEXER_LOGGER } from '../../../utils/logging/common.js' +import { LOG_LEVELS_STR } from '../../../utils/logging/Logger.js' +import { BaseEventProcessor } from './BaseProcessor.js' +import AccessListFactory from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessListFactory.sol/AccessListFactory.json' with { type: 'json' } +import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' with { type: 'json' } + +const factoryInterface = new Interface(AccessListFactory.abi) + +export class NewAccessListEventProcessor extends BaseEventProcessor { + async processEvent( + event: ethers.Log, + chainId: number, + signer: Signer, + provider: FallbackProvider + ): Promise { + try { + const decoded = factoryInterface.parseLog({ + topics: Array.from(event.topics), + data: event.data + }) + if (!decoded) return null + + const contractAddress = decoded.args[0].toString().toLowerCase() + + let transferable = false + let name: string | undefined + let symbol: string | undefined + try { + const accessListContract = new ethers.Contract( + contractAddress, + AccessList.abi, + provider + ) + const [transferableRaw, nameRaw, symbolRaw] = await Promise.all([ + accessListContract.transferable(), + accessListContract.name(), + accessListContract.symbol() + ]) + transferable = Boolean(transferableRaw) + name = nameRaw + symbol = symbolRaw + } catch (err) { + INDEXER_LOGGER.log( + LOG_LEVELS_STR.LEVEL_WARN, + `Failed to read on-chain metadata for ${contractAddress}: ${err.message}` + ) + } + + const { accessList } = await this.getDatabase() + const result = await accessList.create( + chainId, + contractAddress, + transferable, + event.blockNumber, + event.transactionHash, + name, + symbol + ) + + INDEXER_LOGGER.logMessage( + `[NewAccessList] Indexed access list ${contractAddress} on chain ${chainId} (name=${name}, symbol=${symbol}, transferable=${transferable})` + ) + return result + } catch (err) { + INDEXER_LOGGER.log( + LOG_LEVELS_STR.LEVEL_ERROR, + `Error processing NewAccessList event: ${err.message}`, + true + ) + return null + } + } +} diff --git a/src/components/Indexer/processors/index.ts b/src/components/Indexer/processors/index.ts index 912ab6aa2..418adeedb 100644 --- a/src/components/Indexer/processors/index.ts +++ b/src/components/Indexer/processors/index.ts @@ -12,6 +12,9 @@ export * from './MetadataEventProcessor.js' export * from './MetadataStateEventProcessor.js' export * from './OrderReusedEventProcessor.js' export * from './OrderStartedEventProcessor.js' +export * from './NewAccessListEventProcessor.js' +export * from './AddressAddedEventProcessor.js' +export * from './AddressRemovedEventProcessor.js' export * from './BaseProcessor.js' export type ProcessorConstructor = new ( diff --git a/src/components/c2d/compute_engine_docker.ts b/src/components/c2d/compute_engine_docker.ts index cb8bd86e5..2183bb11d 100755 --- a/src/components/c2d/compute_engine_docker.ts +++ b/src/components/c2d/compute_engine_docker.ts @@ -49,6 +49,7 @@ import { } from 'fs' import { pipeline } from 'node:stream/promises' import { CORE_LOGGER } from '../../utils/logging/common.js' +import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' import { AssetUtils } from '../../utils/asset.js' import { FindDdoHandler } from '../core/handler/ddoHandler.js' import { OceanNode } from '../../OceanNode.js' @@ -1695,6 +1696,15 @@ export class C2DEngineDocker extends C2DEngine { } } + private getImagePullTimeoutMs(): number { + const raw = ENVIRONMENT_VARIABLES.C2D_DOWNLOAD_TIMEOUT.value + const parsed = raw ? parseInt(raw, 10) : NaN + if (Number.isFinite(parsed) && parsed > 0) { + return parsed * 1000 + } + return 15 * 60 * 1000 + } + // eslint-disable-next-line require-await private async processJob(job: DBComputeJob) { CORE_LOGGER.info( @@ -2547,6 +2557,7 @@ export class C2DEngineDocker extends C2DEngine { `Using docker registry auth for ${registry} to pull image ${job.containerImage}` ) } + pullOptions.abortSignal = AbortSignal.timeout(this.getImagePullTimeoutMs()) const pullStream = await this.docker.pull(job.containerImage, pullOptions) await new Promise((resolve, reject) => { diff --git a/src/components/core/handler/accessListHandler.ts b/src/components/core/handler/accessListHandler.ts new file mode 100644 index 000000000..e99a4baa3 --- /dev/null +++ b/src/components/core/handler/accessListHandler.ts @@ -0,0 +1,84 @@ +import { CommandHandler } from './handler.js' +import { P2PCommandResponse } from '../../../@types/OceanNode.js' +import { + GetAccessListCommand, + SearchAccessListCommand +} from '../../../@types/commands.js' +import { Readable } from 'stream' +import { isAddress } from 'ethers' +import { + ValidateParams, + validateCommandParameters +} from '../../httpRoutes/validateCommands.js' +import { CORE_LOGGER } from '../../../utils/logging/common.js' + +export class GetAccessListHandler extends CommandHandler { + validate(command: GetAccessListCommand): ValidateParams { + return validateCommandParameters(command, ['chainId', 'contractAddress']) + } + + async handle(task: GetAccessListCommand): Promise { + const checks = await this.verifyParamsAndRateLimits(task) + if (checks.status.httpStatus !== 200 || checks.status.error !== null) { + return checks + } + try { + const db = await this.getOceanNode().getDatabase() + const doc = await db.accessList.retrieve( + Number(task.chainId), + String(task.contractAddress) + ) + if (!doc) { + return { + stream: null, + status: { httpStatus: 404, error: 'AccessList not found' } + } + } + return { + stream: Readable.from(JSON.stringify(doc)), + status: { httpStatus: 200 } + } + } catch (error) { + CORE_LOGGER.error(`GetAccessListHandler error: ${error.message}`) + return { + stream: null, + status: { httpStatus: 500, error: 'Unknown error: ' + error.message } + } + } + } +} + +export class SearchAccessListHandler extends CommandHandler { + validate(command: SearchAccessListCommand): ValidateParams { + return validateCommandParameters(command, ['wallet']) + } + + async handle(task: SearchAccessListCommand): Promise { + const checks = await this.verifyParamsAndRateLimits(task) + if (checks.status.httpStatus !== 200 || checks.status.error !== null) { + return checks + } + try { + const walletString = String(task.wallet) + if (!isAddress(walletString)) { + return { + stream: null, + status: { httpStatus: 400, error: 'Invalid wallet address' } + } + } + const db = await this.getOceanNode().getDatabase() + const chainId = task.chainId !== undefined ? Number(task.chainId) : undefined + const docs = await db.accessList.searchByWallet(walletString, chainId) + return { + stream: Readable.from(JSON.stringify(docs ?? [])), + status: { httpStatus: 200 } + } + } catch (error) { + CORE_LOGGER.error(`SearchAccessListHandler error: ${error.message}`) + return { + stream: null, + status: { httpStatus: 500, error: 'Unknown error: ' + error.message } + } + } + } +} diff --git a/src/components/core/handler/coreHandlersRegistry.ts b/src/components/core/handler/coreHandlersRegistry.ts index 531f7f1a9..e492a01a6 100644 --- a/src/components/core/handler/coreHandlersRegistry.ts +++ b/src/components/core/handler/coreHandlersRegistry.ts @@ -55,6 +55,7 @@ import { PersistentStorageListFilesHandler, PersistentStorageUploadFileHandler } from './persistentStorage.js' +import { GetAccessListHandler, SearchAccessListHandler } from './accessListHandler.js' export type HandlerRegistry = { handlerName: string // name of the handler @@ -199,6 +200,14 @@ export class CoreHandlersRegistry { PROTOCOL_COMMANDS.PERSISTENT_STORAGE_DELETE_FILE, new PersistentStorageDeleteFileHandler(node) ) + this.registerCoreHandler( + PROTOCOL_COMMANDS.GET_ACCESS_LIST, + new GetAccessListHandler(node) + ) + this.registerCoreHandler( + PROTOCOL_COMMANDS.SEARCH_ACCESS_LIST, + new SearchAccessListHandler(node) + ) } public static getInstance( diff --git a/src/components/core/utils/statusHandler.ts b/src/components/core/utils/statusHandler.ts index 45e3c5f97..2e0d667fd 100644 --- a/src/components/core/utils/statusHandler.ts +++ b/src/components/core/utils/statusHandler.ts @@ -170,6 +170,9 @@ export async function status( CORE_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error getting c2d clusters: ${error}`) } nodeStatus.supportedSchemas = typesenseSchemas.ddoSchemas + } else { + delete nodeStatus.c2dClusters + delete nodeStatus.supportedSchemas } if (config.persistentStorage) { diff --git a/src/components/database/BaseDatabase.ts b/src/components/database/BaseDatabase.ts index 3fddb074f..1214e9488 100644 --- a/src/components/database/BaseDatabase.ts +++ b/src/components/database/BaseDatabase.ts @@ -1,5 +1,6 @@ import { Schema } from '.' import { OceanNodeDBConfig } from '../../@types' +import { AccessListUser } from '../../@types/AccessList.js' import { GENERIC_EMOJIS, LOG_LEVELS_STR } from '../../utils/logging/Logger.js' import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { ElasticsearchSchema } from './ElasticSchemas.js' @@ -38,6 +39,34 @@ export abstract class AbstractIndexerDatabase extends AbstractDatabase { abstract delete(network: number): Promise } +export abstract class AbstractAccessListDatabase extends AbstractDatabase { + abstract create( + chainId: number, + contractAddress: string, + transferable: boolean, + block: number, + txId: string, + name?: string, + symbol?: string + ): Promise + + abstract retrieve(chainId: number, contractAddress: string): Promise + abstract addUser( + chainId: number, + contractAddress: string, + user: AccessListUser + ): Promise + + abstract removeUserByTokenId( + chainId: number, + contractAddress: string, + tokenId: number + ): Promise + + abstract searchByWallet(wallet: string, chainId?: number): Promise + abstract delete(chainId: number, contractAddress: string): Promise +} + export abstract class AbstractLogDatabase extends AbstractDatabase { abstract insertLog(logEntry: Record): Promise abstract retrieveLog(id: string): Promise | null> diff --git a/src/components/database/DatabaseFactory.ts b/src/components/database/DatabaseFactory.ts index 4d5d35603..cb6dbcb2c 100644 --- a/src/components/database/DatabaseFactory.ts +++ b/src/components/database/DatabaseFactory.ts @@ -1,5 +1,6 @@ import { OceanNodeDBConfig } from '../../@types' import { + AbstractAccessListDatabase, AbstractDdoDatabase, AbstractDdoStateDatabase, AbstractIndexerDatabase, @@ -7,6 +8,7 @@ import { AbstractOrderDatabase } from './BaseDatabase.js' import { + ElasticsearchAccessListDatabase, ElasticsearchDdoDatabase, ElasticsearchDdoStateDatabase, ElasticsearchIndexerDatabase, @@ -15,6 +17,7 @@ import { } from './ElasticSearchDatabase.js' import { typesenseSchemas } from './TypesenseSchemas.js' import { + TypesenseAccessListDatabase, TypesenseDdoDatabase, TypesenseDdoStateDatabase, TypesenseIndexerDatabase, @@ -45,7 +48,9 @@ export class DatabaseFactory { new ElasticsearchOrderDatabase(config, elasticSchemas.orderSchema), ddoState: (config: OceanNodeDBConfig) => new ElasticsearchDdoStateDatabase(config), ddoStateQuery: () => new ElasticSearchDdoStateQuery(), - metadataQuery: () => new ElasticSearchMetadataQuery() + metadataQuery: () => new ElasticSearchMetadataQuery(), + accessList: (config: OceanNodeDBConfig) => + new ElasticsearchAccessListDatabase(config) }, typesense: { ddo: (config: OceanNodeDBConfig) => @@ -59,7 +64,9 @@ export class DatabaseFactory { ddoState: (config: OceanNodeDBConfig) => new TypesenseDdoStateDatabase(config, typesenseSchemas.ddoStateSchema), ddoStateQuery: () => new TypesenseDdoStateQuery(), - metadataQuery: () => new TypesenseMetadataQuery() + metadataQuery: () => new TypesenseMetadataQuery(), + accessList: (config: OceanNodeDBConfig) => + new TypesenseAccessListDatabase(config, typesenseSchemas.accessListSchema) } } @@ -129,4 +136,10 @@ export class DatabaseFactory { static async createConfigDatabase(): Promise { return await new SQLLiteConfigDatabase() } + + static createAccessListDatabase( + config: OceanNodeDBConfig + ): Promise { + return this.createDatabase('accessList', config) + } } diff --git a/src/components/database/ElasticSearchDatabase.ts b/src/components/database/ElasticSearchDatabase.ts index e7d1df92b..8918d112d 100644 --- a/src/components/database/ElasticSearchDatabase.ts +++ b/src/components/database/ElasticSearchDatabase.ts @@ -1,11 +1,13 @@ import { Client } from '@elastic/elasticsearch' import { + AbstractAccessListDatabase, AbstractDdoDatabase, AbstractDdoStateDatabase, AbstractIndexerDatabase, AbstractLogDatabase, AbstractOrderDatabase } from './BaseDatabase.js' +import { AccessListUser } from '../../@types/AccessList.js' import { createElasticsearchClientWithRetry } from './ElasticsearchConfigHelper.js' import { OceanNodeDBConfig } from '../../@types' import { ElasticsearchSchema } from './ElasticSchemas.js' @@ -1040,6 +1042,277 @@ export class ElasticsearchLogDatabase extends AbstractLogDatabase { } } +export class ElasticsearchAccessListDatabase extends AbstractAccessListDatabase { + private client: Client + private index: string + + constructor(config: OceanNodeDBConfig) { + super(config) + this.index = 'access_list' + + return (async (): Promise => { + this.client = await createElasticsearchClientWithRetry(config) + await this.initializeIndex() + return this + })() as unknown as ElasticsearchAccessListDatabase + } + + private docId(chainId: number, contractAddress: string): string { + return `${chainId}-${contractAddress.toLowerCase()}` + } + + private async initializeIndex() { + try { + const indexExists = await this.client.indices.exists({ index: this.index }) + if (!indexExists) { + await this.client.indices.create({ + index: this.index, + body: { + mappings: { + properties: { + chainId: { type: 'integer' }, + contractAddress: { type: 'keyword' }, + name: { type: 'keyword' }, + symbol: { type: 'keyword' }, + transferable: { type: 'boolean' }, + users: { + type: 'nested', + properties: { + wallet: { type: 'keyword' }, + tokenId: { type: 'long' }, + block: { type: 'long' }, + txId: { type: 'keyword' } + } + }, + deploymentBlock: { type: 'long' }, + deploymentTxId: { type: 'keyword' } + } + } + } + }) + } + } catch (e) { + DATABASE_LOGGER.error(e.message) + } + } + + async create( + chainId: number, + contractAddress: string, + transferable: boolean, + block: number, + txId: string, + name?: string, + symbol?: string + ) { + const id = this.docId(chainId, contractAddress) + const lowerContract = contractAddress.toLowerCase() + try { + await this.client.update({ + index: this.index, + id, + body: { + script: { + source: ` + ctx._source.transferable = params.transferable; + ctx._source.deploymentBlock = params.block; + ctx._source.deploymentTxId = params.txId; + if (params.name != null) { ctx._source.name = params.name; } + if (params.symbol != null) { ctx._source.symbol = params.symbol; } + `, + lang: 'painless', + params: { transferable, block, txId, name, symbol } + }, + upsert: { + chainId, + contractAddress: lowerContract, + name, + symbol, + transferable, + users: [], + deploymentBlock: block, + deploymentTxId: txId + } + }, + refresh: 'wait_for' + }) + return { id } + } catch (error) { + const errorMsg = `Error when upserting access list ${id}: ${error.message}` + DATABASE_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return null + } + } + + async retrieve(chainId: number, contractAddress: string) { + const id = this.docId(chainId, contractAddress) + try { + const result = await this.client.get({ + index: this.index, + id, + refresh: true + }) + return result._source + } catch (error) { + if (error?.meta?.statusCode === 404) { + return null + } + const errorMsg = `Error when retrieving access list ${id}: ${error.message}` + DATABASE_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return null + } + } + + async addUser(chainId: number, contractAddress: string, user: AccessListUser) { + const id = this.docId(chainId, contractAddress) + const lowerContract = contractAddress.toLowerCase() + const normalized: AccessListUser = { ...user, wallet: user.wallet.toLowerCase() } + try { + await this.client.update({ + index: this.index, + id, + body: { + script: { + source: ` + if (ctx._source.users == null) { ctx._source.users = []; } + boolean exists = false; + for (int i = 0; i < ctx._source.users.length; i++) { + if (ctx._source.users[i].tokenId == params.user.tokenId) { exists = true; break; } + } + if (!exists) { ctx._source.users.add(params.user); } + `, + lang: 'painless', + params: { user: normalized } + }, + upsert: { + chainId, + contractAddress: lowerContract, + transferable: false, + users: [normalized] + } + }, + refresh: 'wait_for' + }) + return { id } + } catch (error) { + const errorMsg = `Error when adding user ${normalized.wallet} to access list ${id}: ${error.message}` + DATABASE_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return null + } + } + + async removeUserByTokenId(chainId: number, contractAddress: string, tokenId: number) { + const id = this.docId(chainId, contractAddress) + try { + await this.client.update({ + index: this.index, + id, + body: { + script: { + source: ` + if (ctx._source.users != null) { + ctx._source.users.removeIf(u -> u.tokenId == params.tokenId); + } + `, + lang: 'painless', + params: { tokenId } + } + }, + refresh: 'wait_for' + }) + return { id } + } catch (error) { + if (error?.meta?.statusCode === 404) { + DATABASE_LOGGER.logMessageWithEmoji( + `AddressRemoved on missing access list ${id} (tokenId=${tokenId}); ignoring.`, + true, + GENERIC_EMOJIS.EMOJI_CHECK_MARK, + LOG_LEVELS_STR.LEVEL_WARN + ) + return null + } + const errorMsg = `Error when removing tokenId ${tokenId} from access list ${id}: ${error.message}` + DATABASE_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return null + } + } + + async searchByWallet(wallet: string, chainId?: number): Promise { + const lowerWallet = wallet.toLowerCase() + const filters: any[] = [ + { + nested: { + path: 'users', + query: { term: { 'users.wallet': lowerWallet } } + } + } + ] + if (chainId !== undefined) { + filters.push({ term: { chainId } }) + } + try { + const result = await this.client.search({ + index: this.index, + size: 250, + body: { + query: { bool: { must: filters } } + } + } as any) + return result.hits.hits.map((h: any) => h._source) + } catch (error) { + const errorMsg = `Error when searching access lists by wallet ${lowerWallet}: ${error.message}` + DATABASE_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return [] + } + } + + async delete(chainId: number, contractAddress: string) { + const id = this.docId(chainId, contractAddress) + try { + await this.client.delete({ + index: this.index, + id, + refresh: 'wait_for' + }) + return { id } + } catch (error) { + const errorMsg = `Error when deleting access list ${id}: ${error.message}` + DATABASE_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return null + } + } +} + /** * Make DB agnostic APIs. The response should be similar, no matter what DB engine is used * Normalizes the document responses to match same kind of typesense ones diff --git a/src/components/database/TypesenseDatabase.ts b/src/components/database/TypesenseDatabase.ts index 4129fdf02..b11054252 100644 --- a/src/components/database/TypesenseDatabase.ts +++ b/src/components/database/TypesenseDatabase.ts @@ -7,12 +7,14 @@ import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { ENVIRONMENT_VARIABLES, TYPESENSE_HITS_CAP } from '../../utils/constants.js' import { + AbstractAccessListDatabase, AbstractDdoDatabase, AbstractDdoStateDatabase, AbstractIndexerDatabase, AbstractLogDatabase, AbstractOrderDatabase } from './BaseDatabase.js' +import { AccessListUser } from '../../@types/AccessList.js' import { validateDDO } from '../../utils/asset.js' import { DDOManager } from '@oceanprotocol/ddo-js' @@ -934,3 +936,175 @@ export class TypesenseLogDatabase extends AbstractLogDatabase { } } } + +export class TypesenseAccessListDatabase extends AbstractAccessListDatabase { + private provider: Typesense + + constructor(config: OceanNodeDBConfig, schema: TypesenseSchema) { + super(config, schema) + return (async (): Promise => { + this.provider = new Typesense({ + ...convertTypesenseConfig(this.config.url), + logger: DATABASE_LOGGER + }) + try { + await this.provider.collections(this.schema.name).retrieve() + } catch (error) { + if (error instanceof TypesenseError && error.httpStatus === 404) { + await this.provider.collections().create(this.schema) + } + } + return this + })() as unknown as TypesenseAccessListDatabase + } + + private docId(chainId: number, contractAddress: string): string { + return `${chainId}-${contractAddress.toLowerCase()}` + } + + async create( + chainId: number, + contractAddress: string, + transferable: boolean, + block: number, + txId: string, + name?: string, + symbol?: string + ) { + const id = this.docId(chainId, contractAddress) + const lowerContract = contractAddress.toLowerCase() + try { + const existing: any = await this.retrieve(chainId, contractAddress) + const doc = { + id, + chainId, + contractAddress: lowerContract, + name, + symbol, + transferable, + users: existing?.users ?? [], + deploymentBlock: block, + deploymentTxId: txId + } + if (existing) { + return await this.provider + .collections(this.schema.name) + .documents() + .update(id, doc) + } + return await this.provider.collections(this.schema.name).documents().create(doc) + } catch (error) { + this.logError(`upserting access list ${id}`, error) + return null + } + } + + async retrieve(chainId: number, contractAddress: string) { + const id = this.docId(chainId, contractAddress) + try { + const doc: any = await this.provider + .collections(this.schema.name) + .documents() + .retrieve(id) + return stripId(doc) + } catch (error) { + if (error instanceof TypesenseError && error.httpStatus === 404) { + return null + } + this.logError(`retrieving access list ${id}`, error) + return null + } + } + + async addUser(chainId: number, contractAddress: string, user: AccessListUser) { + const id = this.docId(chainId, contractAddress) + const lowerContract = contractAddress.toLowerCase() + const normalized: AccessListUser = { ...user, wallet: user.wallet.toLowerCase() } + try { + const existing: any = await this.retrieve(chainId, contractAddress) + const users: AccessListUser[] = existing?.users ?? [] + const exists = users.some((u) => u.tokenId === normalized.tokenId) + const nextUsers = exists ? users : [...users, normalized] + if (existing) { + return await this.provider + .collections(this.schema.name) + .documents() + .update(id, { users: nextUsers }) + } + return await this.provider.collections(this.schema.name).documents().create({ + id, + chainId, + contractAddress: lowerContract, + transferable: false, + users: nextUsers + }) + } catch (error) { + this.logError(`adding user ${normalized.wallet} to access list ${id}`, error) + return null + } + } + + async removeUserByTokenId(chainId: number, contractAddress: string, tokenId: number) { + const id = this.docId(chainId, contractAddress) + try { + const existing: any = await this.retrieve(chainId, contractAddress) + if (!existing) return null + const nextUsers = (existing.users ?? []).filter( + (u: AccessListUser) => u.tokenId !== tokenId + ) + return await this.provider + .collections(this.schema.name) + .documents() + .update(id, { users: nextUsers }) + } catch (error) { + this.logError(`removing tokenId ${tokenId} from access list ${id}`, error) + return null + } + } + + async searchByWallet(wallet: string, chainId?: number): Promise { + const lowerWallet = wallet.toLowerCase() + try { + const filterParts = [`users.wallet:=${lowerWallet}`] + if (chainId !== undefined) filterParts.push(`chainId:=${chainId}`) + const result = await this.provider + .collections(this.schema.name) + .documents() + .search({ + q: '*', + query_by: 'contractAddress', + filter_by: filterParts.join(' && '), + per_page: 250 + }) + return (result.hits ?? []).map((h: any) => stripId(h.document)) + } catch (error) { + this.logError(`searching access lists by wallet ${lowerWallet}`, error) + return [] + } + } + + async delete(chainId: number, contractAddress: string) { + const id = this.docId(chainId, contractAddress) + try { + return await this.provider.collections(this.schema.name).documents().delete(id) + } catch (error) { + this.logError(`deleting access list ${id}`, error) + return null + } + } + + private logError(action: string, error: any) { + DATABASE_LOGGER.logMessageWithEmoji( + `Error when ${action}: ${error?.message ?? error}`, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + } +} + +function stripId(doc: any): any { + if (!doc) return doc + const { id: _id, ...rest } = doc + return rest +} diff --git a/src/components/database/TypesenseSchemas.ts b/src/components/database/TypesenseSchemas.ts index 0cdf7ea5e..f928922c1 100644 --- a/src/components/database/TypesenseSchemas.ts +++ b/src/components/database/TypesenseSchemas.ts @@ -53,6 +53,7 @@ export type TypesenseSchemas = { logSchemas: TypesenseSchema orderSchema: TypesenseSchema ddoStateSchema: TypesenseSchema + accessListSchema: TypesenseSchema } const ddoSchemas = readJsonSchemas() export const typesenseSchemas: TypesenseSchemas = { @@ -126,5 +127,21 @@ export const typesenseSchemas: TypesenseSchemas = { { name: 'valid', type: 'bool' }, { name: 'error', type: 'string' } ] + }, + accessListSchema: { + name: 'access_list', + enable_nested_fields: true, + fields: [ + { name: 'chainId', type: 'int64' }, + { name: 'contractAddress', type: 'string' }, + { name: 'name', type: 'string', optional: true }, + { name: 'symbol', type: 'string', optional: true }, + { name: 'transferable', type: 'bool' }, + { name: 'users', type: 'object[]', optional: true }, + { name: 'users.wallet', type: 'string[]', optional: true, facet: true }, + { name: 'users.tokenId', type: 'int64[]', optional: true }, + { name: 'deploymentBlock', type: 'int64', optional: true }, + { name: 'deploymentTxId', type: 'string', optional: true } + ] } } diff --git a/src/components/database/index.ts b/src/components/database/index.ts index 6b0da5250..3c42f468d 100644 --- a/src/components/database/index.ts +++ b/src/components/database/index.ts @@ -6,6 +6,7 @@ import { } from '../../utils/logging/Logger.js' import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { + AbstractAccessListDatabase, AbstractDdoDatabase, AbstractDdoStateDatabase, AbstractIndexerDatabase, @@ -29,6 +30,7 @@ export class Database { logs: AbstractLogDatabase order: AbstractOrderDatabase ddoState: AbstractDdoStateDatabase + accessList: AbstractAccessListDatabase sqliteConfig: SQLLiteConfigDatabase c2d: C2DDatabase authToken: AuthTokenDatabase @@ -101,6 +103,13 @@ export class Database { DATABASE_LOGGER.error(`DDO State database initialization failed: ${error}`) return null } + + try { + db.accessList = await DatabaseFactory.createAccessListDatabase(config) + } catch (error) { + DATABASE_LOGGER.error(`AccessList database initialization failed: ${error}`) + return null + } } else { DATABASE_LOGGER.info( 'Invalid DB URL. Only Nonce, C2D, Auth Token and Config Databases are initialized.' diff --git a/src/components/httpRoutes/accessList.ts b/src/components/httpRoutes/accessList.ts new file mode 100644 index 000000000..8a3fc9440 --- /dev/null +++ b/src/components/httpRoutes/accessList.ts @@ -0,0 +1,65 @@ +import express, { Request, Response } from 'express' +import { Readable } from 'stream' +import { isAddress } from 'ethers' +import { + GetAccessListHandler, + SearchAccessListHandler +} from '../core/handler/accessListHandler.js' +import { PROTOCOL_COMMANDS } from '../../utils/constants.js' +import { streamToString } from '../../utils/util.js' + +export const accessListRoutes = express.Router() + +accessListRoutes.get( + '/api/services/accesslists', + async (req: Request, res: Response): Promise => { + const { wallet } = req.query + if (typeof wallet !== 'string' || !wallet) { + res.status(400).send('Missing required query param: wallet') + return + } + if (!isAddress(wallet)) { + res.status(400).send('Invalid wallet address') + return + } + const chainIdQuery = req.query.chainId + let chainId: number | undefined + if (chainIdQuery !== undefined) { + chainId = Number(chainIdQuery) + if (Number.isNaN(chainId)) { + res.status(400).send('chainId must be a number') + return + } + } + const result = await new SearchAccessListHandler(req.oceanNode).handle({ + command: PROTOCOL_COMMANDS.SEARCH_ACCESS_LIST, + wallet, + chainId, + caller: req.caller + }) + if (result.stream) { + const data = JSON.parse(await streamToString(result.stream as Readable)) + res.json(data) + } else { + res.status(result.status.httpStatus).send(result.status.error) + } + } +) + +accessListRoutes.get( + '/api/services/accesslists/:chainId/:contractAddress', + async (req: Request, res: Response): Promise => { + const result = await new GetAccessListHandler(req.oceanNode).handle({ + command: PROTOCOL_COMMANDS.GET_ACCESS_LIST, + chainId: Number(req.params.chainId), + contractAddress: req.params.contractAddress, + caller: req.caller + }) + if (result.stream) { + const data = JSON.parse(await streamToString(result.stream as Readable)) + res.json(data) + } else { + res.status(result.status.httpStatus).send(result.status.error) + } + } +) diff --git a/src/components/httpRoutes/index.ts b/src/components/httpRoutes/index.ts index ad4c0f3dc..0706f3cba 100644 --- a/src/components/httpRoutes/index.ts +++ b/src/components/httpRoutes/index.ts @@ -15,6 +15,7 @@ import { PolicyServerPassthroughRoute } from './policyServer.js' import { authRoutes } from './auth.js' import { adminConfigRoutes } from './adminConfig.js' import { persistentStorageRoutes } from './persistentStorage.js' +import { accessListRoutes } from './accessList.js' export * from './getOceanPeers.js' export * from './auth.js' @@ -64,6 +65,8 @@ httpRoutes.use(authRoutes) httpRoutes.use(adminConfigRoutes) // persistent storage routes httpRoutes.use(persistentStorageRoutes) +// access list routes +httpRoutes.use(accessListRoutes) export function getAllServiceEndpoints() { httpRoutes.stack.forEach(addMapping.bind(null, [])) diff --git a/src/test/integration/accessListEvents.test.ts b/src/test/integration/accessListEvents.test.ts new file mode 100644 index 000000000..ba73bd48b --- /dev/null +++ b/src/test/integration/accessListEvents.test.ts @@ -0,0 +1,438 @@ +import { expect } from 'chai' +import { JsonRpcProvider, Signer } from 'ethers' +import { homedir } from 'os' +import { Readable } from 'stream' +import AccessListFactory from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessListFactory.sol/AccessListFactory.json' with { type: 'json' } +import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' with { type: 'json' } +import { Database } from '../../components/database/index.js' +import { OceanIndexer } from '../../components/Indexer/index.js' +import { OceanNode } from '../../OceanNode.js' +import { RPCS } from '../../@types/blockchain.js' +import { + DEVELOPMENT_CHAIN_ID, + getOceanArtifactsAdresses, + getOceanArtifactsAdressesByChainId +} from '../../utils/address.js' +import { ENVIRONMENT_VARIABLES, PROTOCOL_COMMANDS } from '../../utils/constants.js' +import { getConfiguration } from '../../utils/config.js' +import { streamToString } from '../../utils/util.js' +import { + buildEnvOverrideConfig, + DEFAULT_TEST_TIMEOUT, + getMockSupportedNetworks, + OverrideEnvConfig, + setupEnvironment, + tearDownEnvironment +} from '../utils/utils.js' +import { deployAccessListContract, getContract } from '../utils/contracts.js' +import { waitForCondition } from './testUtils.js' +import { + GetAccessListHandler, + SearchAccessListHandler +} from '../../components/core/handler/accessListHandler.js' + +describe('********** AccessList event indexing', function () { + this.timeout(DEFAULT_TEST_TIMEOUT * 4) + + let database: Database + let oceanNode: OceanNode + let provider: JsonRpcProvider + let owner: Signer + let factoryAddress: string + let indexer: OceanIndexer + const chainId = DEVELOPMENT_CHAIN_ID + const mockSupportedNetworks: RPCS = getMockSupportedNetworks() + let previousConfiguration: OverrideEnvConfig[] + + before(async () => { + previousConfiguration = await setupEnvironment( + null, + buildEnvOverrideConfig( + [ + ENVIRONMENT_VARIABLES.RPCS, + ENVIRONMENT_VARIABLES.INDEXER_NETWORKS, + ENVIRONMENT_VARIABLES.PRIVATE_KEY, + ENVIRONMENT_VARIABLES.ADDRESS_FILE + ], + [ + JSON.stringify(mockSupportedNetworks), + JSON.stringify([DEVELOPMENT_CHAIN_ID]), + '0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58', + `${homedir}/.ocean/ocean-contracts/artifacts/address.json` + ] + ) + ) + + const config = await getConfiguration(true) + database = await Database.init(config.dbConfig) + + const oldIndexer = OceanNode.getInstance(config, database).getIndexer() + if (oldIndexer) { + await oldIndexer.stopAllChainIndexers() + } + oceanNode = OceanNode.getInstance( + config, + database, + null, + null, + null, + null, + null, + true + ) + let artifactsAddresses = getOceanArtifactsAdressesByChainId(DEVELOPMENT_CHAIN_ID) + if (!artifactsAddresses) { + artifactsAddresses = getOceanArtifactsAdresses().development + } + factoryAddress = artifactsAddresses.AccessListFactory + + provider = new JsonRpcProvider('http://127.0.0.1:8545') + owner = (await provider.getSigner(0)) as Signer + + // Skip historical replay: the hardhat chain accumulates AccessList events + // across test runs. Pin the indexer to the current head so it only sees + // events emitted by THIS suite. + const headBlock = await provider.getBlockNumber() + await database.indexer.update(chainId, headBlock) + + indexer = new OceanIndexer(database, config, oceanNode.blockchainRegistry) + oceanNode.addIndexer(indexer) + }) + + after(async () => { + if (oceanNode) await oceanNode.tearDownAll() + await tearDownEnvironment(previousConfiguration) + }) + + it('factory deploy with no initial users creates an indexed document', async () => { + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'EmptyList', + 'EMPTY', + false, + await owner.getAddress(), + [], + [] + ) + expect(deployedAddr, 'deployment failed').to.be.a('string') + + const doc: any = await waitForCondition(async () => { + return await database.accessList.retrieve(chainId, deployedAddr!) + }, DEFAULT_TEST_TIMEOUT * 2) + + expect(doc, 'document was not indexed in time').to.not.equal(null) + expect(doc.contractAddress).to.equal(deployedAddr!.toLowerCase()) + expect(doc.transferable).to.equal(false) + expect(Array.isArray(doc.users)).to.equal(true) + expect(doc.users.length).to.equal(0) + }) + + it('factory deploy with initial users records every AddressAdded', async () => { + const wallets = [ + await (await provider.getSigner(2)).getAddress(), + await (await provider.getSigner(3)).getAddress(), + await (await provider.getSigner(4)).getAddress() + ] + const tokenURIs = wallets.map(() => 'https://oceanprotocol.com/nft/') + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'PrefilledList', + 'PRE', + false, + await owner.getAddress(), + wallets, + tokenURIs + ) + expect(deployedAddr, 'deployment failed').to.be.a('string') + + const doc: any = await waitForCondition(async () => { + const d = await database.accessList.retrieve(chainId, deployedAddr!) + return d && d.users && d.users.length === wallets.length ? d : null + }, DEFAULT_TEST_TIMEOUT * 3) + expect(doc, 'doc with all initial users not indexed in time').to.not.equal(null) + + const indexedWallets = doc.users.map((u: any) => u.wallet) + for (const w of wallets) { + expect(indexedWallets).to.include(w.toLowerCase()) + } + for (const u of doc.users) { + expect(u.tokenId).to.be.a('number') + expect(u.block).to.be.a('number').and.greaterThan(0) + expect(u.txId).to.be.a('string') + } + }) + + it('mint adds a user; burn removes by tokenId', async () => { + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'MutableList', + 'MUT', + false, + await owner.getAddress(), + [], + [] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + return await database.accessList.retrieve(chainId, deployedAddr!) + }, DEFAULT_TEST_TIMEOUT * 2) + + const accessListContract = getContract(deployedAddr!, AccessList.abi, owner) + const newWalletSigner = await provider.getSigner(5) + const newWallet = await newWalletSigner.getAddress() + + const mintTx = await accessListContract.mint(newWallet, 'https://example/nft') + await mintTx.wait() + + const docAfterMint: any = await waitForCondition(async () => { + const d: any = await database.accessList.retrieve(chainId, deployedAddr!) + return d && d.users.some((u: any) => u.wallet === newWallet.toLowerCase()) + ? d + : null + }, DEFAULT_TEST_TIMEOUT * 2) + expect(docAfterMint).to.not.equal(null) + const minted = docAfterMint.users.find( + (u: any) => u.wallet === newWallet.toLowerCase() + ) + expect(minted).to.not.equal(undefined) + const { tokenId } = minted + + const burnTx = await accessListContract.burn(tokenId) + await burnTx.wait() + + const docAfterBurn: any = await waitForCondition(async () => { + const d: any = await database.accessList.retrieve(chainId, deployedAddr!) + return d && !d.users.some((u: any) => u.tokenId === tokenId) ? d : null + }, DEFAULT_TEST_TIMEOUT * 2) + expect(docAfterBurn).to.not.equal(null) + expect(docAfterBurn.users.some((u: any) => u.tokenId === tokenId)).to.equal(false) + }) + + it('searchByWallet returns AccessLists containing the wallet', async () => { + const wallet = await (await provider.getSigner(6)).getAddress() + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'SearchableList', + 'SCH', + false, + await owner.getAddress(), + [wallet], + ['https://example/nft'] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + const d: any = await database.accessList.retrieve(chainId, deployedAddr!) + return d && d.users.length === 1 ? d : null + }, DEFAULT_TEST_TIMEOUT * 3) + + const matched = await waitForCondition(async () => { + const results = await database.accessList.searchByWallet(wallet, chainId) + return ( + results.find((r: any) => r.contractAddress === deployedAddr!.toLowerCase()) ?? + null + ) + }, DEFAULT_TEST_TIMEOUT * 3) + expect(matched, 'wallet not found in any access list').to.not.equal(null) + }) + + it('addUser is idempotent when the same tokenId is replayed', async () => { + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'IdempotentList', + 'IDM', + false, + await owner.getAddress(), + [], + [] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + return await database.accessList.retrieve(chainId, deployedAddr!) + }, DEFAULT_TEST_TIMEOUT * 2) + + const sameUser = { + wallet: '0x' + 'a'.repeat(40), + tokenId: 999, + block: 1, + txId: '0xdeadbeef' + } + + await database.accessList.addUser(chainId, deployedAddr!, sameUser) + await database.accessList.addUser(chainId, deployedAddr!, sameUser) + + const doc: any = await database.accessList.retrieve(chainId, deployedAddr!) + const matches = doc.users.filter((u: any) => u.tokenId === sameUser.tokenId) + expect(matches.length).to.equal(1) + }) + + it('transferable: true is recorded on the doc', async () => { + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'TransferableList', + 'TRF', + true, + await owner.getAddress(), + [], + [] + ) + expect(deployedAddr).to.be.a('string') + + const doc: any = await waitForCondition(async () => { + return await database.accessList.retrieve(chainId, deployedAddr!) + }, DEFAULT_TEST_TIMEOUT * 2) + expect(doc).to.not.equal(null) + expect(doc.transferable).to.equal(true) + }) + + it('lastIndexedBlock advances after access list events', async () => { + const before = await database.indexer.retrieve(chainId) + const beforeBlock = before?.lastIndexedBlock ?? 0 + + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'CursorList', + 'CUR', + false, + await owner.getAddress(), + [], + [] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + return await database.accessList.retrieve(chainId, deployedAddr!) + }, DEFAULT_TEST_TIMEOUT * 2) + + const after = await waitForCondition(async () => { + const cur = await database.indexer.retrieve(chainId) + return cur && cur.lastIndexedBlock > beforeBlock ? cur : null + }, DEFAULT_TEST_TIMEOUT * 2) + expect(after, 'indexer cursor did not advance').to.not.equal(null) + expect(after.lastIndexedBlock).to.be.greaterThan(beforeBlock) + }) + + it('GetAccessListHandler returns the indexed doc', async () => { + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'HandlerGetList', + 'HGET', + false, + await owner.getAddress(), + [], + [] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + return await database.accessList.retrieve(chainId, deployedAddr!) + }, DEFAULT_TEST_TIMEOUT * 2) + + const result = await new GetAccessListHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.GET_ACCESS_LIST, + chainId, + contractAddress: deployedAddr! + }) + expect(result.status.httpStatus).to.equal(200) + expect(result.stream).to.not.equal(null) + const doc = JSON.parse(await streamToString(result.stream as Readable)) + expect(doc.contractAddress).to.equal(deployedAddr!.toLowerCase()) + }) + + it('GetAccessListHandler returns 404 for an unknown contract', async () => { + const result = await new GetAccessListHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.GET_ACCESS_LIST, + chainId, + contractAddress: '0x' + 'd'.repeat(40) + }) + expect(result.status.httpStatus).to.equal(404) + expect(result.stream).to.equal(null) + }) + + it('SearchAccessListHandler without chainId returns matches across all chains', async () => { + const wallet = await (await provider.getSigner(9)).getAddress() + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'CrossChainList', + 'CCL', + false, + await owner.getAddress(), + [wallet], + ['https://example/nft'] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + const d: any = await database.accessList.retrieve(chainId, deployedAddr!) + return d && d.users.length === 1 ? d : null + }, DEFAULT_TEST_TIMEOUT * 3) + + const matched = await waitForCondition(async () => { + const result = await new SearchAccessListHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.SEARCH_ACCESS_LIST, + wallet + }) + if (result.status.httpStatus !== 200 || !result.stream) return null + const docs = JSON.parse(await streamToString(result.stream as Readable)) + return ( + docs.find((d: any) => d.contractAddress === deployedAddr!.toLowerCase()) ?? null + ) + }, DEFAULT_TEST_TIMEOUT * 3) + expect(matched, 'wallet not found via cross-chain handler').to.not.equal(null) + }) + + it('SearchAccessListHandler returns docs containing a wallet', async () => { + const wallet = await (await provider.getSigner(8)).getAddress() + const deployedAddr = await deployAccessListContract( + owner, + factoryAddress, + AccessListFactory.abi, + 'HandlerSearchList', + 'HSRC', + false, + await owner.getAddress(), + [wallet], + ['https://example/nft'] + ) + expect(deployedAddr).to.be.a('string') + + await waitForCondition(async () => { + const d: any = await database.accessList.retrieve(chainId, deployedAddr!) + return d && d.users.length === 1 ? d : null + }, DEFAULT_TEST_TIMEOUT * 3) + + const matched = await waitForCondition(async () => { + const result = await new SearchAccessListHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.SEARCH_ACCESS_LIST, + wallet, + chainId + }) + if (result.status.httpStatus !== 200 || !result.stream) return null + const docs = JSON.parse(await streamToString(result.stream as Readable)) + return ( + docs.find((d: any) => d.contractAddress === deployedAddr!.toLowerCase()) ?? null + ) + }, DEFAULT_TEST_TIMEOUT * 3) + expect(matched, 'wallet not found via handler').to.not.equal(null) + }) +}) diff --git a/src/test/integration/testUtils.ts b/src/test/integration/testUtils.ts index cc13f4d1c..539893cc4 100644 --- a/src/test/integration/testUtils.ts +++ b/src/test/integration/testUtils.ts @@ -84,6 +84,28 @@ export const waitToIndex = async ( }) } +/** + * Polls a predicate until it returns truthy or the timeout elapses. + * Returns the predicate's truthy value, or null on timeout. + */ +export async function waitForCondition( + predicate: () => Promise, + timeoutMs: number = DEFAULT_TEST_TIMEOUT, + intervalMs: number = 1000 +): Promise { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + try { + const result = await predicate() + if (result) return result + } catch (_e) { + // ignore and retry + } + await new Promise((resolve) => setTimeout(resolve, intervalMs)) + } + return null +} + export const deleteAsset = async (did: string, database: Database): Promise => { try { return await database.ddo.delete(did) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 10b2407c6..257bc24dd 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -44,7 +44,9 @@ export const PROTOCOL_COMMANDS = { PERSISTENT_STORAGE_LIST_FILES: 'persistentStorageListFiles', PERSISTENT_STORAGE_UPLOAD_FILE: 'persistentStorageUploadFile', PERSISTENT_STORAGE_GET_FILE_OBJECT: 'persistentStorageGetFileObject', - PERSISTENT_STORAGE_DELETE_FILE: 'persistentStorageDeleteFile' + PERSISTENT_STORAGE_DELETE_FILE: 'persistentStorageDeleteFile', + GET_ACCESS_LIST: 'getAccessList', + SEARCH_ACCESS_LIST: 'searchAccessList' } // more visible, keep then close to make sure we always update both export const SUPPORTED_PROTOCOL_COMMANDS: string[] = [ @@ -90,7 +92,9 @@ export const SUPPORTED_PROTOCOL_COMMANDS: string[] = [ PROTOCOL_COMMANDS.PERSISTENT_STORAGE_LIST_FILES, PROTOCOL_COMMANDS.PERSISTENT_STORAGE_UPLOAD_FILE, PROTOCOL_COMMANDS.PERSISTENT_STORAGE_GET_FILE_OBJECT, - PROTOCOL_COMMANDS.PERSISTENT_STORAGE_DELETE_FILE + PROTOCOL_COMMANDS.PERSISTENT_STORAGE_DELETE_FILE, + PROTOCOL_COMMANDS.GET_ACCESS_LIST, + PROTOCOL_COMMANDS.SEARCH_ACCESS_LIST ] export const MetadataStates = { @@ -115,7 +119,10 @@ export const EVENTS = { DISPENSER_ACTIVATED: 'DispenserActivated', DISPENSER_DEACTIVATED: 'DispenserDeactivated', EXCHANGE_ACTIVATED: 'ExchangeActivated', - EXCHANGE_DEACTIVATED: 'ExchangeDeactivated' + EXCHANGE_DEACTIVATED: 'ExchangeDeactivated', + ADDRESS_ADDED: 'AddressAdded', + ADDRESS_REMOVED: 'AddressRemoved', + NEW_ACCESS_LIST: 'NewAccessList' } export const INDEXER_CRAWLING_EVENTS = { @@ -185,6 +192,18 @@ export const EVENT_HASHES: Hashes = { '0x03da9148e1de78fba22de63c573465562ebf6ef878a1d3ea83790a560229984c': { type: EVENTS.EXCHANGE_DEACTIVATED, text: 'ExchangeDeactivated(bytes32,address)' + }, + '0x9cc987676e7d63379f176ea50df0ae8d2d9d1141d1231d4ce15b5965f73c9430': { + type: EVENTS.ADDRESS_ADDED, + text: 'AddressAdded(address,uint256)' + }, + '0xb1e731889e7185f2cc895a86c70cded99d77ab8ecea58ab5abe5d43b084f51ae': { + type: EVENTS.ADDRESS_REMOVED, + text: 'AddressRemoved(uint256)' + }, + '0xd65bc8e3024bbad886df74eea79b6e118b7fbcffe1f3f98054e5a6b98dc83891': { + type: EVENTS.NEW_ACCESS_LIST, + text: 'NewAccessList(address,address)' } } @@ -536,6 +555,11 @@ export const ENVIRONMENT_VARIABLES: Record = { name: 'PERSISTENT_STORAGE', value: process.env.PERSISTENT_STORAGE, required: false + }, + C2D_DOWNLOAD_TIMEOUT: { + name: 'C2D_DOWNLOAD_TIMEOUT', + value: process.env.C2D_DOWNLOAD_TIMEOUT, + required: false } } export const CONNECTION_HISTORY_DELETE_THRESHOLD = 300 From 2868ec83a005c2d17d48d569cff17b98f80ab82d Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 11:39:34 +0300 Subject: [PATCH 02/15] fix: tests --- src/test/integration/algorithmsAccess.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/integration/algorithmsAccess.test.ts b/src/test/integration/algorithmsAccess.test.ts index 2272f7a79..77b5e1ec8 100644 --- a/src/test/integration/algorithmsAccess.test.ts +++ b/src/test/integration/algorithmsAccess.test.ts @@ -146,7 +146,7 @@ describe('********** Trusted algorithms Flow', () => { // let's publish assets & algos it('should publish compute datasets & algos', async function () { - this.timeout(DEFAULT_TEST_TIMEOUT * 2) + this.timeout(DEFAULT_TEST_TIMEOUT * 4) publishedComputeDataset = await publishAsset( computeAssetWithNoAccess, publisherAccount @@ -156,7 +156,7 @@ describe('********** Trusted algorithms Flow', () => { oceanNode, publishedComputeDataset.ddo.id, EVENTS.METADATA_CREATED, - DEFAULT_TEST_TIMEOUT + DEFAULT_TEST_TIMEOUT * 2 ) // Fail the test if compute dataset DDO was not indexed - subsequent tests depend on it assert( @@ -169,7 +169,7 @@ describe('********** Trusted algorithms Flow', () => { oceanNode, publishedAlgoDataset.ddo.id, EVENTS.METADATA_CREATED, - DEFAULT_TEST_TIMEOUT + DEFAULT_TEST_TIMEOUT * 2 ) // Fail the test if algorithm DDO was not indexed - subsequent tests depend on it assert( From fc555a376ca45795991973fdeaeead5636ecc3e1 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 12:04:27 +0300 Subject: [PATCH 03/15] fix: add logs --- src/test/integration/algorithmsAccess.test.ts | 204 +++++++++++++++--- 1 file changed, 176 insertions(+), 28 deletions(-) diff --git a/src/test/integration/algorithmsAccess.test.ts b/src/test/integration/algorithmsAccess.test.ts index 77b5e1ec8..73f1a7c9d 100644 --- a/src/test/integration/algorithmsAccess.test.ts +++ b/src/test/integration/algorithmsAccess.test.ts @@ -77,6 +77,25 @@ describe('********** Trusted algorithms Flow', () => { let artifactsAddresses: any let initializeResponse: ProviderComputeInitializeResults + const stringifyForDebug = (value: unknown) => + JSON.stringify( + value, + (_key, nestedValue) => + typeof nestedValue === 'bigint' ? nestedValue.toString() : nestedValue, + 2 + ) + + const logEthersError = (label: string, error: any) => { + console.log(`[algorithmsAccess][${label}] error`, { + message: error?.message, + code: error?.code, + action: error?.action, + reason: error?.reason, + data: error?.data, + transaction: error?.transaction + }) + } + before(async () => { artifactsAddresses = getOceanArtifactsAdresses() paymentToken = artifactsAddresses.development.Ocean @@ -389,41 +408,152 @@ describe('********** Trusted algorithms Flow', () => { it('should start a compute job', async function () { this.timeout(DEFAULT_TEST_TIMEOUT * 10) // let's put funds in escrow & create an auth - let balance = await paymentTokenContract.balanceOf(await consumerAccount.getAddress()) + const consumerAddress = await consumerAccount.getAddress() + console.log('[algorithmsAccess][compute-start] setup', { + consumerAddress, + computeEnvConsumerAddress: firstEnv.consumerAddress, + environmentId: firstEnv.id, + paymentToken, + escrowAddress: initializeResponse.payment.escrowAddress, + initializePayment: initializeResponse.payment, + datasetOrderTxId, + algoOrderTxId + }) + + let balance = await paymentTokenContract.balanceOf(consumerAddress) + console.log('[algorithmsAccess][compute-start] initial token balance', { + consumerAddress, + balance: balance.toString() + }) + if (BigInt(balance.toString()) === BigInt(0)) { const mintAmount = ethers.parseUnits('1000', 18) - const mintTx = await paymentTokenContract.mint( - await consumerAccount.getAddress(), - mintAmount - ) - await mintTx.wait() - balance = await paymentTokenContract.balanceOf(await consumerAccount.getAddress()) + console.log('[algorithmsAccess][compute-start] minting test tokens', { + consumerAddress, + mintAmount: mintAmount.toString() + }) + const mintTx = await paymentTokenContract.mint(consumerAddress, mintAmount) + const mintReceipt = await mintTx.wait() + console.log('[algorithmsAccess][compute-start] mint receipt', { + hash: mintReceipt?.hash, + status: mintReceipt?.status, + blockNumber: mintReceipt?.blockNumber + }) + balance = await paymentTokenContract.balanceOf(consumerAddress) } assert(BigInt(balance.toString()) > BigInt(0), 'Consumer has no Ocean tokens') - const approveTx = await paymentTokenContract - .connect(consumerAccount) - .approve(initializeResponse.payment.escrowAddress, balance) - await approveTx.wait() - const depositTx = await escrowContract - .connect(consumerAccount) - .deposit(initializeResponse.payment.token, balance) - await depositTx.wait() - const authorizeTx = await escrowContract - .connect(consumerAccount) - .authorize( + console.log('[algorithmsAccess][compute-start] token balance before escrow', { + consumerAddress, + balance: balance.toString() + }) + + try { + const approveGas = await paymentTokenContract + .connect(consumerAccount) + .approve.estimateGas(initializeResponse.payment.escrowAddress, balance) + console.log('[algorithmsAccess][compute-start] approve estimateGas', { + approveGas: approveGas.toString() + }) + const approveTx = await paymentTokenContract + .connect(consumerAccount) + .approve(initializeResponse.payment.escrowAddress, balance) + const approveReceipt = await approveTx.wait() + const allowance = await paymentTokenContract.allowance( + consumerAddress, + initializeResponse.payment.escrowAddress + ) + console.log('[algorithmsAccess][compute-start] approve receipt', { + hash: approveReceipt?.hash, + status: approveReceipt?.status, + blockNumber: approveReceipt?.blockNumber, + allowance: allowance.toString() + }) + } catch (e) { + logEthersError('approve', e) + throw e + } + + try { + const escrowBalanceBefore = await escrowContract.getUserFunds( + initializeResponse.payment.token, + consumerAddress + ) + const depositGas = await escrowContract + .connect(consumerAccount) + .deposit.estimateGas(initializeResponse.payment.token, balance) + console.log('[algorithmsAccess][compute-start] deposit estimateGas', { + depositGas: depositGas.toString(), + escrowBalanceBefore: escrowBalanceBefore.toString(), + token: initializeResponse.payment.token, + amount: balance.toString() + }) + const depositTx = await escrowContract + .connect(consumerAccount) + .deposit(initializeResponse.payment.token, balance) + const depositReceipt = await depositTx.wait() + const escrowBalanceAfter = await escrowContract.getUserFunds( initializeResponse.payment.token, - firstEnv.consumerAddress, - balance, - initializeResponse.payment.minLockSeconds, - 10 + consumerAddress ) - await authorizeTx.wait() + console.log('[algorithmsAccess][compute-start] deposit receipt', { + hash: depositReceipt?.hash, + status: depositReceipt?.status, + blockNumber: depositReceipt?.blockNumber, + escrowBalanceAfter: escrowBalanceAfter.toString() + }) + } catch (e) { + logEthersError('deposit', e) + throw e + } + + try { + const authorizeGas = await escrowContract + .connect(consumerAccount) + .authorize.estimateGas( + initializeResponse.payment.token, + firstEnv.consumerAddress, + balance, + initializeResponse.payment.minLockSeconds, + 10 + ) + console.log('[algorithmsAccess][compute-start] authorize estimateGas', { + authorizeGas: authorizeGas.toString(), + token: initializeResponse.payment.token, + payee: firstEnv.consumerAddress, + amount: balance.toString(), + minLockSeconds: initializeResponse.payment.minLockSeconds, + maxLockCounts: 10 + }) + const authorizeTx = await escrowContract + .connect(consumerAccount) + .authorize( + initializeResponse.payment.token, + firstEnv.consumerAddress, + balance, + initializeResponse.payment.minLockSeconds, + 10 + ) + const authorizeReceipt = await authorizeTx.wait() + console.log('[algorithmsAccess][compute-start] authorize receipt', { + hash: authorizeReceipt?.hash, + status: authorizeReceipt?.status, + blockNumber: authorizeReceipt?.blockNumber + }) + } catch (e) { + logEthersError('authorize', e) + throw e + } + const locks = await oceanNode.escrow.getLocks( DEVELOPMENT_CHAIN_ID, paymentToken, - await consumerAccount.getAddress(), + consumerAddress, firstEnv.consumerAddress ) + console.log('[algorithmsAccess][compute-start] existing locks', { + count: locks.length, + locks: stringifyForDebug(locks) + }) if (locks.length > 0) { // cancel all locks @@ -437,20 +567,22 @@ describe('********** Trusted algorithms Flow', () => { lock.payer, firstEnv.consumerAddress ) - } catch (e) {} + } catch (e) { + logEthersError('cancelExpiredLock', e) + } } } const nonce = Date.now().toString() const messageHashBytes = createHashForSignature( - await consumerAccount.getAddress(), + consumerAddress, nonce, PROTOCOL_COMMANDS.COMPUTE_START ) const signature = await safeSign(consumerAccount, messageHashBytes) const startComputeTask: PaidComputeStartCommand = { command: PROTOCOL_COMMANDS.COMPUTE_START, - consumerAddress: await consumerAccount.getAddress(), + consumerAddress, signature, nonce, environment: firstEnv.id, @@ -479,9 +611,13 @@ describe('********** Trusted algorithms Flow', () => { const auth = await oceanNode.escrow.getAuthorizations( DEVELOPMENT_CHAIN_ID, paymentToken, - await consumerAccount.getAddress(), + consumerAddress, firstEnv.consumerAddress ) + console.log('[algorithmsAccess][compute-start] authorizations', { + count: auth.length, + auth: stringifyForDebug(auth) + }) assert(auth.length > 0, 'Should have authorization') assert( BigInt(auth[0].maxLockedAmount.toString()) > BigInt(0), @@ -491,6 +627,18 @@ describe('********** Trusted algorithms Flow', () => { BigInt(auth[0].maxLockCounts.toString()) > BigInt(0), ' Should have maxLockCounts in auth' ) + console.log('[algorithmsAccess][compute-start] start task', { + consumerAddress: startComputeTask.consumerAddress, + environment: startComputeTask.environment, + payment: startComputeTask.payment, + maxJobDuration: startComputeTask.maxJobDuration, + datasets: startComputeTask.datasets, + algorithm: { + documentId: startComputeTask.algorithm.documentId, + serviceId: startComputeTask.algorithm.serviceId, + transferTxId: startComputeTask.algorithm.transferTxId + } + }) const response = await new PaidComputeStartHandler(oceanNode).handle(startComputeTask) console.log(`response: ${response.status.httpStatus}`) console.log(`response: ${JSON.stringify(response)}`) From 02e02836cf1de8b946772df09e56db6b40776278 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 12:15:08 +0300 Subject: [PATCH 04/15] fix: tests --- src/test/integration/algorithmsAccess.test.ts | 193 +++--------------- 1 file changed, 23 insertions(+), 170 deletions(-) diff --git a/src/test/integration/algorithmsAccess.test.ts b/src/test/integration/algorithmsAccess.test.ts index 73f1a7c9d..d80ea8452 100644 --- a/src/test/integration/algorithmsAccess.test.ts +++ b/src/test/integration/algorithmsAccess.test.ts @@ -77,25 +77,6 @@ describe('********** Trusted algorithms Flow', () => { let artifactsAddresses: any let initializeResponse: ProviderComputeInitializeResults - const stringifyForDebug = (value: unknown) => - JSON.stringify( - value, - (_key, nestedValue) => - typeof nestedValue === 'bigint' ? nestedValue.toString() : nestedValue, - 2 - ) - - const logEthersError = (label: string, error: any) => { - console.log(`[algorithmsAccess][${label}] error`, { - message: error?.message, - code: error?.code, - action: error?.action, - reason: error?.reason, - data: error?.data, - transaction: error?.transaction - }) - } - before(async () => { artifactsAddresses = getOceanArtifactsAdresses() paymentToken = artifactsAddresses.development.Ocean @@ -210,7 +191,6 @@ describe('********** Trusted algorithms Flow', () => { expect(response.stream).to.be.instanceOf(Readable) computeEnvironments = await streamToObject(response.stream as Readable) - console.log('existing envs: ', computeEnvironments) // expect 1 OR + envs (1 if only docker free env is available) assert(computeEnvironments.length >= 1, 'Not enough compute envs') for (const computeEnvironment of computeEnvironments) { @@ -264,7 +244,6 @@ describe('********** Trusted algorithms Flow', () => { const resp = await new ComputeInitializeHandler(oceanNode).handle( initializeComputeTask ) - console.log(resp) assert(resp, 'Failed to get response') assert(resp.status.httpStatus === 400, 'Failed to get 400 response') assert( @@ -366,7 +345,6 @@ describe('********** Trusted algorithms Flow', () => { const resp = await new ComputeInitializeHandler(oceanNode).handle( initializeComputeTask ) - console.log(resp) assert(resp, 'Failed to get response') assert(resp.status.httpStatus === 200, 'Failed to get 200 response') assert(resp.stream, 'Failed to get stream') @@ -409,140 +387,39 @@ describe('********** Trusted algorithms Flow', () => { this.timeout(DEFAULT_TEST_TIMEOUT * 10) // let's put funds in escrow & create an auth const consumerAddress = await consumerAccount.getAddress() - console.log('[algorithmsAccess][compute-start] setup', { - consumerAddress, - computeEnvConsumerAddress: firstEnv.consumerAddress, - environmentId: firstEnv.id, - paymentToken, - escrowAddress: initializeResponse.payment.escrowAddress, - initializePayment: initializeResponse.payment, - datasetOrderTxId, - algoOrderTxId - }) + escrowContract = new ethers.Contract( + initializeResponse.payment.escrowAddress, + EscrowJson.abi, + publisherAccount + ) let balance = await paymentTokenContract.balanceOf(consumerAddress) - console.log('[algorithmsAccess][compute-start] initial token balance', { - consumerAddress, - balance: balance.toString() - }) if (BigInt(balance.toString()) === BigInt(0)) { const mintAmount = ethers.parseUnits('1000', 18) - console.log('[algorithmsAccess][compute-start] minting test tokens', { - consumerAddress, - mintAmount: mintAmount.toString() - }) const mintTx = await paymentTokenContract.mint(consumerAddress, mintAmount) - const mintReceipt = await mintTx.wait() - console.log('[algorithmsAccess][compute-start] mint receipt', { - hash: mintReceipt?.hash, - status: mintReceipt?.status, - blockNumber: mintReceipt?.blockNumber - }) + await mintTx.wait() balance = await paymentTokenContract.balanceOf(consumerAddress) } assert(BigInt(balance.toString()) > BigInt(0), 'Consumer has no Ocean tokens') - console.log('[algorithmsAccess][compute-start] token balance before escrow', { - consumerAddress, - balance: balance.toString() - }) - - try { - const approveGas = await paymentTokenContract - .connect(consumerAccount) - .approve.estimateGas(initializeResponse.payment.escrowAddress, balance) - console.log('[algorithmsAccess][compute-start] approve estimateGas', { - approveGas: approveGas.toString() - }) - const approveTx = await paymentTokenContract - .connect(consumerAccount) - .approve(initializeResponse.payment.escrowAddress, balance) - const approveReceipt = await approveTx.wait() - const allowance = await paymentTokenContract.allowance( - consumerAddress, - initializeResponse.payment.escrowAddress - ) - console.log('[algorithmsAccess][compute-start] approve receipt', { - hash: approveReceipt?.hash, - status: approveReceipt?.status, - blockNumber: approveReceipt?.blockNumber, - allowance: allowance.toString() - }) - } catch (e) { - logEthersError('approve', e) - throw e - } - - try { - const escrowBalanceBefore = await escrowContract.getUserFunds( + const approveTx = await paymentTokenContract + .connect(consumerAccount) + .approve(initializeResponse.payment.escrowAddress, balance) + await approveTx.wait() + const depositTx = await escrowContract + .connect(consumerAccount) + .deposit(initializeResponse.payment.token, balance) + await depositTx.wait() + const authorizeTx = await escrowContract + .connect(consumerAccount) + .authorize( initializeResponse.payment.token, - consumerAddress + firstEnv.consumerAddress, + balance, + initializeResponse.payment.minLockSeconds, + 10 ) - const depositGas = await escrowContract - .connect(consumerAccount) - .deposit.estimateGas(initializeResponse.payment.token, balance) - console.log('[algorithmsAccess][compute-start] deposit estimateGas', { - depositGas: depositGas.toString(), - escrowBalanceBefore: escrowBalanceBefore.toString(), - token: initializeResponse.payment.token, - amount: balance.toString() - }) - const depositTx = await escrowContract - .connect(consumerAccount) - .deposit(initializeResponse.payment.token, balance) - const depositReceipt = await depositTx.wait() - const escrowBalanceAfter = await escrowContract.getUserFunds( - initializeResponse.payment.token, - consumerAddress - ) - console.log('[algorithmsAccess][compute-start] deposit receipt', { - hash: depositReceipt?.hash, - status: depositReceipt?.status, - blockNumber: depositReceipt?.blockNumber, - escrowBalanceAfter: escrowBalanceAfter.toString() - }) - } catch (e) { - logEthersError('deposit', e) - throw e - } - - try { - const authorizeGas = await escrowContract - .connect(consumerAccount) - .authorize.estimateGas( - initializeResponse.payment.token, - firstEnv.consumerAddress, - balance, - initializeResponse.payment.minLockSeconds, - 10 - ) - console.log('[algorithmsAccess][compute-start] authorize estimateGas', { - authorizeGas: authorizeGas.toString(), - token: initializeResponse.payment.token, - payee: firstEnv.consumerAddress, - amount: balance.toString(), - minLockSeconds: initializeResponse.payment.minLockSeconds, - maxLockCounts: 10 - }) - const authorizeTx = await escrowContract - .connect(consumerAccount) - .authorize( - initializeResponse.payment.token, - firstEnv.consumerAddress, - balance, - initializeResponse.payment.minLockSeconds, - 10 - ) - const authorizeReceipt = await authorizeTx.wait() - console.log('[algorithmsAccess][compute-start] authorize receipt', { - hash: authorizeReceipt?.hash, - status: authorizeReceipt?.status, - blockNumber: authorizeReceipt?.blockNumber - }) - } catch (e) { - logEthersError('authorize', e) - throw e - } + await authorizeTx.wait() const locks = await oceanNode.escrow.getLocks( DEVELOPMENT_CHAIN_ID, @@ -550,10 +427,6 @@ describe('********** Trusted algorithms Flow', () => { consumerAddress, firstEnv.consumerAddress ) - console.log('[algorithmsAccess][compute-start] existing locks', { - count: locks.length, - locks: stringifyForDebug(locks) - }) if (locks.length > 0) { // cancel all locks @@ -567,9 +440,7 @@ describe('********** Trusted algorithms Flow', () => { lock.payer, firstEnv.consumerAddress ) - } catch (e) { - logEthersError('cancelExpiredLock', e) - } + } catch (e) {} } } const nonce = Date.now().toString() @@ -614,10 +485,6 @@ describe('********** Trusted algorithms Flow', () => { consumerAddress, firstEnv.consumerAddress ) - console.log('[algorithmsAccess][compute-start] authorizations', { - count: auth.length, - auth: stringifyForDebug(auth) - }) assert(auth.length > 0, 'Should have authorization') assert( BigInt(auth[0].maxLockedAmount.toString()) > BigInt(0), @@ -627,21 +494,7 @@ describe('********** Trusted algorithms Flow', () => { BigInt(auth[0].maxLockCounts.toString()) > BigInt(0), ' Should have maxLockCounts in auth' ) - console.log('[algorithmsAccess][compute-start] start task', { - consumerAddress: startComputeTask.consumerAddress, - environment: startComputeTask.environment, - payment: startComputeTask.payment, - maxJobDuration: startComputeTask.maxJobDuration, - datasets: startComputeTask.datasets, - algorithm: { - documentId: startComputeTask.algorithm.documentId, - serviceId: startComputeTask.algorithm.serviceId, - transferTxId: startComputeTask.algorithm.transferTxId - } - }) const response = await new PaidComputeStartHandler(oceanNode).handle(startComputeTask) - console.log(`response: ${response.status.httpStatus}`) - console.log(`response: ${JSON.stringify(response)}`) assert(response, 'Failed to get response') assert(response.status.httpStatus === 200, 'Failed to get 200 response') assert(response.stream, 'Failed to get stream') From 9ec7a3863f0fdabb9492126111a112fa7e7cddfa Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 12:29:19 +0300 Subject: [PATCH 05/15] fix: fix --- src/test/integration/algorithmsAccess.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/test/integration/algorithmsAccess.test.ts b/src/test/integration/algorithmsAccess.test.ts index d80ea8452..e510dceae 100644 --- a/src/test/integration/algorithmsAccess.test.ts +++ b/src/test/integration/algorithmsAccess.test.ts @@ -410,13 +410,17 @@ describe('********** Trusted algorithms Flow', () => { .connect(consumerAccount) .deposit(initializeResponse.payment.token, balance) await depositTx.wait() + const latestBlock = await provider.getBlock('latest') + assert(latestBlock, 'Failed to get latest block') + const lockExpiry = + Number(latestBlock.timestamp) + initializeResponse.payment.minLockSeconds const authorizeTx = await escrowContract .connect(consumerAccount) .authorize( initializeResponse.payment.token, firstEnv.consumerAddress, balance, - initializeResponse.payment.minLockSeconds, + lockExpiry, 10 ) await authorizeTx.wait() @@ -494,7 +498,14 @@ describe('********** Trusted algorithms Flow', () => { BigInt(auth[0].maxLockCounts.toString()) > BigInt(0), ' Should have maxLockCounts in auth' ) - const response = await new PaidComputeStartHandler(oceanNode).handle(startComputeTask) + const originalGetMinLockTime = oceanNode.escrow.getMinLockTime.bind(oceanNode.escrow) + oceanNode.escrow.getMinLockTime = () => lockExpiry + let response + try { + response = await new PaidComputeStartHandler(oceanNode).handle(startComputeTask) + } finally { + oceanNode.escrow.getMinLockTime = originalGetMinLockTime + } assert(response, 'Failed to get response') assert(response.status.httpStatus === 200, 'Failed to get 200 response') assert(response.stream, 'Failed to get stream') From 61192e00a79cbb69418d82177b4dfb5b1c9f1cf0 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 13:28:03 +0300 Subject: [PATCH 06/15] fix: tests --- src/test/integration/algorithmsAccess.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/test/integration/algorithmsAccess.test.ts b/src/test/integration/algorithmsAccess.test.ts index e510dceae..fc6dca45b 100644 --- a/src/test/integration/algorithmsAccess.test.ts +++ b/src/test/integration/algorithmsAccess.test.ts @@ -410,17 +410,13 @@ describe('********** Trusted algorithms Flow', () => { .connect(consumerAccount) .deposit(initializeResponse.payment.token, balance) await depositTx.wait() - const latestBlock = await provider.getBlock('latest') - assert(latestBlock, 'Failed to get latest block') - const lockExpiry = - Number(latestBlock.timestamp) + initializeResponse.payment.minLockSeconds const authorizeTx = await escrowContract .connect(consumerAccount) .authorize( initializeResponse.payment.token, firstEnv.consumerAddress, balance, - lockExpiry, + initializeResponse.payment.minLockSeconds, 10 ) await authorizeTx.wait() @@ -498,16 +494,21 @@ describe('********** Trusted algorithms Flow', () => { BigInt(auth[0].maxLockCounts.toString()) > BigInt(0), ' Should have maxLockCounts in auth' ) - const originalGetMinLockTime = oceanNode.escrow.getMinLockTime.bind(oceanNode.escrow) - oceanNode.escrow.getMinLockTime = () => lockExpiry + const environmentHash = firstEnv.id.slice(0, firstEnv.id.indexOf('-')) + const engine = await oceanNode.getC2DEngines().getC2DByHash(environmentHash) + const originalCreateLock = engine.escrow.createLock.bind(engine.escrow) + engine.escrow.createLock = () => Promise.resolve(`0x${'1'.padStart(64, '0')}`) let response try { response = await new PaidComputeStartHandler(oceanNode).handle(startComputeTask) } finally { - oceanNode.escrow.getMinLockTime = originalGetMinLockTime + engine.escrow.createLock = originalCreateLock } assert(response, 'Failed to get response') - assert(response.status.httpStatus === 200, 'Failed to get 200 response') + assert( + response.status.httpStatus === 200, + `Expected 200, got ${response.status.httpStatus}: ${response.status?.error ?? ''}` + ) assert(response.stream, 'Failed to get stream') expect(response.stream).to.be.instanceOf(Readable) From e7f18bb0a16a739709d276725b688f369b98907f Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 13:37:04 +0300 Subject: [PATCH 07/15] fix: fix tests --- src/test/integration/compute.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index bd61f9c54..3b416a9ab 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -462,8 +462,11 @@ describe('********** Compute', () => { assert(resultParsed.providerFee.validUntil, 'algorithm validUntil does not exist') assert(result.datasets[0].validOrder === false, 'incorrect validOrder') // expect false because tx id was not provided and no start order was called before assert(result.payment, ' Payment structure does not exists') + const expectedEscrowAddress = + oceanNode.escrow.getEscrowContractAddressForChain(DEVELOPMENT_CHAIN_ID) + assert(expectedEscrowAddress, 'Expected escrow address does not exist') assert( - result.payment.escrowAddress === artifactsAddresses.development.Escrow, + result.payment.escrowAddress.toLowerCase() === expectedEscrowAddress.toLowerCase(), 'Incorrect escrow address' ) assert(result.payment.payee === firstEnv.consumerAddress, 'Incorrect payee address') From c38eea0fb924eba6e5f3287a68f6fccd4877452c Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 13:47:28 +0300 Subject: [PATCH 08/15] fix: fix --- src/test/integration/compute.test.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index 3b416a9ab..c3c66f287 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -691,6 +691,11 @@ describe('********** Compute', () => { }) it('should start a compute job with output to URL storage at 172.15.0.7', async () => { // deposit funds and create auth in escrow + escrowContract = new ethers.Contract( + initializeResponse.payment.escrowAddress, + EscrowJson.abi, + publisherAccount + ) let balance = await paymentTokenContract.balanceOf(await consumerAccount.getAddress()) if (BigInt(balance.toString()) === BigInt(0)) { const mintAmount = ethers.parseUnits('1000', 18) @@ -883,6 +888,11 @@ describe('********** Compute', () => { it('should start a compute job with maxed resources', async function () { this.timeout(130_000) // waitForAllJobsToFinish can take up to 120s await waitForAllJobsToFinish(oceanNode) + escrowContract = new ethers.Contract( + initializeResponse.payment.escrowAddress, + EscrowJson.abi, + publisherAccount + ) let balance = await paymentTokenContract.balanceOf(await consumerAccount.getAddress()) if (BigInt(balance.toString()) === BigInt(0)) { console.log('Minting') @@ -3077,16 +3087,15 @@ describe('********** Compute Access Restrictions', () => { const provider = new JsonRpcProvider('http://127.0.0.1:8545') const publisherAccount = (await provider.getSigner(0)) as Signer consumerAccount = (await provider.getSigner(1)) as Signer - escrowContract = new ethers.Contract( - artifactsAddresses.development.Escrow, - EscrowJson.abi, - consumerAccount - ) paymentTokenContract = new ethers.Contract( paymentToken, OceanToken.abi, publisherAccount ) + const escrowAddress = + oceanNode.escrow.getEscrowContractAddressForChain(DEVELOPMENT_CHAIN_ID) + assert(escrowAddress, 'Expected escrow address does not exist') + escrowContract = new ethers.Contract(escrowAddress, EscrowJson.abi, consumerAccount) // Get the Docker engine const c2dEngines = oceanNode.getC2DEngines() @@ -3250,7 +3259,7 @@ describe('********** Compute Access Restrictions', () => { const approveTx = await paymentTokenContract .connect(consumerAccount) - .approve(artifactsAddresses.development.Escrow, balance) + .approve(await escrowContract.getAddress(), balance) await approveTx.wait() const depositTx = await escrowContract From a1ed9db108e693cb8a8026e389e3d63e0a2301b0 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 14:02:37 +0300 Subject: [PATCH 09/15] fix: fix --- src/test/integration/compute.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index c3c66f287..662fca6a4 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -200,6 +200,8 @@ describe('********** Compute', () => { ) ) config = await getConfiguration(true) + config.claimDurationTimeout = + Math.floor(Date.now() / 1000) + config.claimDurationTimeout dbconn = await Database.init(config.dbConfig) const staleJobs = await dbconn.c2d.getRunningJobs() From d309a8aec346636d2246128ac01e1e2cd015b416 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 14:37:20 +0300 Subject: [PATCH 10/15] fix: fix --- src/test/integration/compute.test.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index 662fca6a4..240bdc5b6 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -169,10 +169,27 @@ describe('********** Compute', () => { let algoDDO: any let datasetDDO: any let artifactsAddresses: any + let testAddressFile: string let initializeResponse: ProviderComputeInitializeResults before(async () => { - artifactsAddresses = getOceanArtifactsAdresses() + const defaultTestAddressFile = `${homedir}/.ocean/ocean-contracts/artifacts/address.json` + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (existsSync(defaultTestAddressFile)) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + artifactsAddresses = JSON.parse(await fsp.readFile(defaultTestAddressFile, 'utf8')) + } else { + artifactsAddresses = getOceanArtifactsAdresses() + } + if (artifactsAddresses?.development?.EnterpriseEscrow) { + delete artifactsAddresses.development.EnterpriseEscrow + testAddressFile = path.join( + tmpdir(), + `ocean-node-test-addresses-${Date.now()}.json` + ) + // eslint-disable-next-line security/detect-non-literal-fs-filename + await fsp.writeFile(testAddressFile, JSON.stringify(artifactsAddresses)) + } paymentToken = artifactsAddresses.development.Ocean previousConfiguration = await setupEnvironment( TEST_ENV_CONFIG_FILE, @@ -190,7 +207,7 @@ describe('********** Compute', () => { JSON.stringify([DEVELOPMENT_CHAIN_ID]), '0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58', JSON.stringify(['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260']), - `${homedir}/.ocean/ocean-contracts/artifacts/address.json`, + testAddressFile || defaultTestAddressFile, '[{"socketPath":"/var/run/docker.sock","environments":[{"storageExpiry":604800,"maxJobDuration":3600,"minJobDuration":60,"resources":[{"id":"cpu","total":4,"max":4,"min":1,"type":"cpu"},{"id":"ram","total":10,"max":10,"min":1,"type":"ram"},{"id":"disk","total":10,"max":10,"min":0,"type":"disk"}],"fees":{"' + DEVELOPMENT_CHAIN_ID + '":[{"feeToken":"' + @@ -200,8 +217,6 @@ describe('********** Compute', () => { ) ) config = await getConfiguration(true) - config.claimDurationTimeout = - Math.floor(Date.now() / 1000) + config.claimDurationTimeout dbconn = await Database.init(config.dbConfig) const staleJobs = await dbconn.c2d.getRunningJobs() @@ -242,6 +257,9 @@ describe('********** Compute', () => { after(async () => { await oceanNode.tearDownAll() await tearDownEnvironment(previousConfiguration) + if (testAddressFile) { + await fsp.rm(testAddressFile, { force: true }) + } }) it('Sets up compute envs', () => { assert(oceanNode, 'Failed to instantiate OceanNode') From 68c2a6002087a4c166bfa2e3aa499c155b5e0138 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 14:50:01 +0300 Subject: [PATCH 11/15] fix: compute test --- src/test/integration/compute.test.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index 240bdc5b6..79af02220 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -3060,10 +3060,28 @@ describe('********** Compute Access Restrictions', () => { let escrowContract: any let paymentTokenContract: any let artifactsAddresses: any + let testAddressFile: string before(async function () { this.timeout(DEFAULT_TEST_TIMEOUT * 2) - artifactsAddresses = getOceanArtifactsAdresses() + const defaultTestAddressFile = `${homedir}/.ocean/ocean-contracts/artifacts/address.json` + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (existsSync(defaultTestAddressFile)) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const addressFileContent = await fsp.readFile(defaultTestAddressFile, 'utf8') + artifactsAddresses = JSON.parse(addressFileContent) + } else { + artifactsAddresses = getOceanArtifactsAdresses() + } + if (artifactsAddresses?.development?.EnterpriseEscrow) { + delete artifactsAddresses.development.EnterpriseEscrow + testAddressFile = path.join( + tmpdir(), + `ocean-node-test-addresses-${Date.now()}.json` + ) + // eslint-disable-next-line security/detect-non-literal-fs-filename + await fsp.writeFile(testAddressFile, JSON.stringify(artifactsAddresses)) + } paymentToken = artifactsAddresses.development.Ocean previousConfiguration = await setupEnvironment( TEST_ENV_CONFIG_FILE, @@ -3079,7 +3097,7 @@ describe('********** Compute Access Restrictions', () => { JSON.stringify(mockSupportedNetworks), JSON.stringify([DEVELOPMENT_CHAIN_ID]), '0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58', - `${homedir}/.ocean/ocean-contracts/artifacts/address.json`, + testAddressFile || defaultTestAddressFile, '[{"socketPath":"/var/run/docker.sock","paymentClaimInterval":60,"environments":[{"storageExpiry":604800,"maxJobDuration":3600,"minJobDuration":60,"resources":[{"id":"cpu","total":4,"max":4,"min":1,"type":"cpu"},{"id":"ram","total":10,"max":10,"min":1,"type":"ram"},{"id":"disk","total":10,"max":10,"min":0,"type":"disk"}],"fees":{"' + DEVELOPMENT_CHAIN_ID + '":[{"feeToken":"' + @@ -3139,6 +3157,9 @@ describe('********** Compute Access Restrictions', () => { after(async () => { await oceanNode.tearDownAll() await tearDownEnvironment(previousConfiguration) + if (testAddressFile) { + await fsp.rm(testAddressFile, { force: true }) + } }) it('should transition job to JobSettle status when PublishingResults completes', async function () { From 869da03fef9b05a2803b0c59462664997ad9cf0b Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 15:25:05 +0300 Subject: [PATCH 12/15] fix: system --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04200f877..350a65e73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -272,6 +272,11 @@ jobs: sleep 10 [ -f "$HOME/.ocean/ocean-contracts/artifacts/ready" ] && break done + - name: Use regular Escrow for system tests + run: | + SYSTEM_TEST_ADDRESS_FILE="${RUNNER_TEMP}/address.system-tests.json" + jq 'del(.development.EnterpriseEscrow)' "${HOME}/.ocean/ocean-contracts/artifacts/address.json" > "${SYSTEM_TEST_ADDRESS_FILE}" + echo "ADDRESS_FILE=${SYSTEM_TEST_ADDRESS_FILE}" >> "$GITHUB_ENV" - name: docker logs run: docker logs ocean-contracts-1 && docker logs ocean-typesense-1 From 3a001fff23dbf9ddf0236dac03191d3278be0d7c Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 15:37:37 +0300 Subject: [PATCH 13/15] fix: ci --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 350a65e73..a7a06de0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -277,6 +277,8 @@ jobs: SYSTEM_TEST_ADDRESS_FILE="${RUNNER_TEMP}/address.system-tests.json" jq 'del(.development.EnterpriseEscrow)' "${HOME}/.ocean/ocean-contracts/artifacts/address.json" > "${SYSTEM_TEST_ADDRESS_FILE}" echo "ADDRESS_FILE=${SYSTEM_TEST_ADDRESS_FILE}" >> "$GITHUB_ENV" + - name: Configure escrow timeout for system tests + run: echo "ESCROW_CLAIM_TIMEOUT=$(date +%s)" >> "$GITHUB_ENV" - name: docker logs run: docker logs ocean-contracts-1 && docker logs ocean-typesense-1 From 04117c6b78176b39254d97b7a7a61a41010da4ee Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Wed, 6 May 2026 15:58:55 +0300 Subject: [PATCH 14/15] fix: system --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7a06de0d..a2f240c01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -279,6 +279,11 @@ jobs: echo "ADDRESS_FILE=${SYSTEM_TEST_ADDRESS_FILE}" >> "$GITHUB_ENV" - name: Configure escrow timeout for system tests run: echo "ESCROW_CLAIM_TIMEOUT=$(date +%s)" >> "$GITHUB_ENV" + - name: Use separate consumer key for ocean-cli paid compute system test + working-directory: ${{ github.workspace }}/ocean-cli + run: | + perl -0pi -e 's/0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58/0x1d751ded5a32226054cd2e71261039b65afb9ee1c746d055dd699b1150a5befc/g' test/paidComputeFlow.test.ts + grep -n "PRIVATE_KEY" test/paidComputeFlow.test.ts - name: docker logs run: docker logs ocean-contracts-1 && docker logs ocean-typesense-1 From e6fa52ec579dfe970861d9a0d3581833ab0d9b29 Mon Sep 17 00:00:00 2001 From: AdriGeorge Date: Thu, 7 May 2026 16:04:53 +0300 Subject: [PATCH 15/15] chore: v3.1.1 --- package-lock.json | 12 ++++++------ package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3522f86c..369d4e0a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocean-node", - "version": "3.1.0", + "version": "3.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocean-node", - "version": "3.1.0", + "version": "3.1.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -35,7 +35,7 @@ "@libp2p/upnp-nat": "^4.0.9", "@libp2p/websockets": "^10.1.2", "@multiformats/multiaddr": "^12.2.3", - "@oceanprotocol/contracts": "^2.6.0", + "@oceanprotocol/contracts": "^2.7.0", "@oceanprotocol/ddo-js": "^0.2.0", "axios": "^1.15.0", "base58-js": "^2.0.0", @@ -5236,9 +5236,9 @@ } }, "node_modules/@oceanprotocol/contracts": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-2.6.0.tgz", - "integrity": "sha512-4K3TTM0q4VlBs7GLXzQkGMae576iAGHMARqMFcKXUUtfGyiT8KP4sP9IBsq8UUhu11R4JU2KTqXenHS8EWxrbA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-2.7.0.tgz", + "integrity": "sha512-6rXT/agjty4VyT0j/Do13Rf8dH0eweL57e7rFSv4KmANFx3wdTcQoInHZsoRpwXZf1MmMgHVWL4/ZvqHtRqMRA==", "license": "Apache-2.0" }, "node_modules/@oceanprotocol/ddo-js": { diff --git a/package.json b/package.json index ab701c163..2579aa26a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocean-node", - "version": "3.1.0", + "version": "3.1.1", "description": "Ocean Node is used to run all core services in the Ocean stack", "author": "Ocean Protocol Foundation", "license": "Apache-2.0", @@ -73,7 +73,7 @@ "@libp2p/upnp-nat": "^4.0.9", "@libp2p/websockets": "^10.1.2", "@multiformats/multiaddr": "^12.2.3", - "@oceanprotocol/contracts": "^2.6.0", + "@oceanprotocol/contracts": "^2.7.0", "@oceanprotocol/ddo-js": "^0.2.0", "axios": "^1.15.0", "base58-js": "^2.0.0",