From 161c863e1ebdfa1a1e73b6e022e1b8e98b41bca9 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Tue, 7 Jan 2025 17:40:09 +0700 Subject: [PATCH 01/11] feat: allow no persist for Custom Data Access --- .../data-access/src/combined-data-access.ts | 6 ++++- packages/data-access/src/data-write.ts | 1 + packages/data-access/src/mock-data-access.ts | 4 +-- .../src/api/request-network.ts | 9 +++---- .../request-client.js/src/http-data-access.ts | 5 ++++ .../src/http-metamask-data-access.ts | 2 +- .../src/http-request-network.ts | 6 +++-- .../src/no-persist-http-data-access.ts | 2 +- .../test/in-memory-request.test.ts | 25 ++++++++++++++++++- .../thegraph-data-access/src/NoopDataWrite.ts | 2 ++ packages/types/src/data-access-types.ts | 1 + 11 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/data-access/src/combined-data-access.ts b/packages/data-access/src/combined-data-access.ts index c179f9ee9c..4086ed1177 100644 --- a/packages/data-access/src/combined-data-access.ts +++ b/packages/data-access/src/combined-data-access.ts @@ -1,10 +1,14 @@ import { DataAccessTypes } from '@requestnetwork/types'; export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess { + public readonly persist: boolean; + constructor( protected reader: DataAccessTypes.IDataRead, protected writer: DataAccessTypes.IDataWrite, - ) {} + ) { + this.persist = writer.persist; + } async initialize(): Promise { await this.reader.initialize(); diff --git a/packages/data-access/src/data-write.ts b/packages/data-access/src/data-write.ts index b22edf73b7..6ab6c8017a 100644 --- a/packages/data-access/src/data-write.ts +++ b/packages/data-access/src/data-write.ts @@ -6,6 +6,7 @@ export class DataAccessWrite implements DataAccessTypes.IDataWrite { constructor( protected readonly storage: StorageTypes.IStorageWrite, private readonly pendingStore?: DataAccessTypes.IPendingStore, + public readonly persist: boolean = true, ) { this.pendingStore = pendingStore; } diff --git a/packages/data-access/src/mock-data-access.ts b/packages/data-access/src/mock-data-access.ts index 55b6e0d4fe..e421fef743 100644 --- a/packages/data-access/src/mock-data-access.ts +++ b/packages/data-access/src/mock-data-access.ts @@ -13,13 +13,13 @@ import { CombinedDataAccess } from './combined-data-access'; export class MockDataAccess extends CombinedDataAccess { private readonly dataIndex: InMemoryIndexer; - constructor(storage: StorageTypes.IStorage) { + constructor(storage: StorageTypes.IStorage, options: { persist: boolean } = { persist: true }) { const dataIndex = new InMemoryIndexer(storage); const pendingStore = new PendingStore(); super( new DataAccessRead(dataIndex, { network: 'mock', pendingStore }), - new DataAccessWrite(storage, pendingStore), + new DataAccessWrite(storage, pendingStore, options.persist), ); this.dataIndex = dataIndex; } diff --git a/packages/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index d91f40b606..003e3a0b61 100644 --- a/packages/request-client.js/src/api/request-network.ts +++ b/packages/request-client.js/src/api/request-network.ts @@ -23,7 +23,6 @@ import * as Types from '../types'; import ContentDataExtension from './content-data-extension'; import Request from './request'; import localUtils from './utils'; -import { NoPersistHttpDataAccess } from '../no-persist-http-data-access'; /** * Entry point of the request-client.js library. Create requests, get requests, manipulate requests. @@ -115,7 +114,7 @@ export default class RequestNetwork { const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData; const requestId = requestLogicCreateResult.result.requestId; - const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess; + const isSkippingPersistence = !this.dataAccess.persist; // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { contentDataExtension: this.contentData, @@ -149,7 +148,7 @@ export default class RequestNetwork { * @param request The Request object to persist. This must be a request that was created with skipPersistence enabled. * @returns A promise that resolves to the result of the persist transaction operation. * @throws {Error} If the request's `inMemoryInfo` is not provided, indicating it wasn't created with skipPersistence. - * @throws {Error} If the current data access instance does not support persistence (e.g., NoPersistHttpDataAccess). + * @throws {Error} If the current data access instance does not support persistence. */ public async persistRequest( request: Request, @@ -158,7 +157,7 @@ export default class RequestNetwork { throw new Error('Cannot persist request without inMemoryInfo.'); } - if (this.dataAccess instanceof NoPersistHttpDataAccess) { + if (!this.dataAccess.persist) { throw new Error( 'Cannot persist request when skipPersistence is enabled. To persist the request, create a new instance of RequestNetwork without skipPersistence being set to true.', ); @@ -198,7 +197,7 @@ export default class RequestNetwork { const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData; const requestId = requestLogicCreateResult.result.requestId; - const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess; + const isSkippingPersistence = !this.dataAccess.persist; // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 99b5abf5cb..8f37c66674 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -14,6 +14,7 @@ export type NodeConnectionConfig = { baseURL: string; headers: Record; nodeConnectionConfig?: Partial; + persist: boolean; } = { httpConfig: {}, nodeConnectionConfig: {}, + persist: true, }, ) { // Get Request Client version to set it in the header const requestClientVersion = packageJson.version; + this.persist = persist; this.httpConfig = { ...httpConfigDefaults, ...httpConfig, diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index f3c7ab2777..ac6d63c738 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -42,7 +42,7 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { httpConfig: {}, }, ) { - super({ httpConfig, nodeConnectionConfig }); + super({ httpConfig, nodeConnectionConfig, persist: true }); ethereumProviderUrl = ethereumProviderUrl ? ethereumProviderUrl : 'http://localhost:8545'; diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index 78346f54a3..b6b0a9d88b 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -57,13 +57,15 @@ export default class HttpRequestNetwork extends RequestNetwork { }, ) { const dataAccess: DataAccessTypes.IDataAccess = useMockStorage - ? new MockDataAccess(new MockStorage()) + ? new MockDataAccess(new MockStorage(), { + persist: !skipPersistence, + }) : skipPersistence ? new NoPersistHttpDataAccess({ httpConfig, nodeConnectionConfig, }) - : new HttpDataAccess({ httpConfig, nodeConnectionConfig }); + : new HttpDataAccess({ httpConfig, nodeConnectionConfig, persist: true }); if (!currencyManager) { currencyManager = CurrencyManager.getDefault(); diff --git a/packages/request-client.js/src/no-persist-http-data-access.ts b/packages/request-client.js/src/no-persist-http-data-access.ts index d2b3a4769a..d60e4fd6b2 100644 --- a/packages/request-client.js/src/no-persist-http-data-access.ts +++ b/packages/request-client.js/src/no-persist-http-data-access.ts @@ -15,7 +15,7 @@ export class NoPersistHttpDataAccess extends HttpDataAccess { nodeConnectionConfig: {}, }, ) { - super({ httpConfig, nodeConnectionConfig }); + super({ httpConfig, nodeConnectionConfig, persist: false }); } async persistTransaction( diff --git a/packages/request-client.js/test/in-memory-request.test.ts b/packages/request-client.js/test/in-memory-request.test.ts index e2cf5164fc..622e97e26f 100644 --- a/packages/request-client.js/test/in-memory-request.test.ts +++ b/packages/request-client.js/test/in-memory-request.test.ts @@ -1,9 +1,12 @@ -import { RequestNetwork } from '../src/index'; +import { RequestNetwork, RequestNetworkBase } from '../src/index'; import * as TestData from './data-test'; +import { DataAccessTypes } from '@requestnetwork/types'; import { http, HttpResponse } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; import config from '../src/http-config-defaults'; +import { MockDataAccess } from '@requestnetwork/data-access'; +import { MockStorage } from '../src/mock-storage'; describe('handle in-memory request', () => { let requestNetwork: RequestNetwork; @@ -60,6 +63,26 @@ describe('handle in-memory request', () => { expect(spyPersistTransaction).not.toHaveBeenCalled(); }); + it('creates a request without persisting it with custom data access', async () => { + const mockStorage = new MockStorage(); + const mockDataAccess = new MockDataAccess(mockStorage, { persist: false }); + + requestNetwork = new RequestNetworkBase({ + signatureProvider: TestData.fakeSignatureProvider, + dataAccess: mockDataAccess, + }); + + const request = await requestNetwork.createRequest(requestCreationParams); + + expect(request).toBeDefined(); + expect(request.requestId).toBeDefined(); + expect(request.inMemoryInfo).toBeDefined(); + expect(request.inMemoryInfo?.requestData).toBeDefined(); + expect(request.inMemoryInfo?.topics).toBeDefined(); + expect(request.inMemoryInfo?.transactionData).toBeDefined(); + expect(spyPersistTransaction).not.toHaveBeenCalled(); + }); + it('throws an error when trying to persist a request with skipPersistence as true', async () => { requestNetwork = new RequestNetwork({ skipPersistence: true, diff --git a/packages/thegraph-data-access/src/NoopDataWrite.ts b/packages/thegraph-data-access/src/NoopDataWrite.ts index 80e7847e5f..44d720d2fd 100644 --- a/packages/thegraph-data-access/src/NoopDataWrite.ts +++ b/packages/thegraph-data-access/src/NoopDataWrite.ts @@ -1,6 +1,8 @@ import { DataAccessTypes } from '@requestnetwork/types'; export class NoopDataWrite implements DataAccessTypes.IDataWrite { + constructor(public readonly persist: boolean = true) {} + async initialize(): Promise { // no-op } diff --git a/packages/types/src/data-access-types.ts b/packages/types/src/data-access-types.ts index 6b0bc688a2..6b3bf725ef 100644 --- a/packages/types/src/data-access-types.ts +++ b/packages/types/src/data-access-types.ts @@ -25,6 +25,7 @@ export interface IDataRead { } export interface IDataWrite { + persist: boolean; initialize: () => Promise; close: () => Promise; From 952184dca23058640ec481b0995f43491a17868f Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Tue, 7 Jan 2025 18:16:41 +0700 Subject: [PATCH 02/11] deprecate NoPersistHttpDataAccess + handle no persistence inside DataAccess class --- packages/data-access/src/data-write.ts | 15 +++++- packages/data-access/src/index.ts | 1 + .../src/no-persist-transaction-raw-data.ts | 17 +++++++ .../request-client.js/src/http-data-access.ts | 23 ++++++--- .../src/http-metamask-data-access.ts | 16 ++++++- .../src/http-request-network.ts | 8 +--- .../src/no-persist-http-data-access.ts | 48 ------------------- 7 files changed, 64 insertions(+), 64 deletions(-) create mode 100644 packages/data-access/src/no-persist-transaction-raw-data.ts delete mode 100644 packages/request-client.js/src/no-persist-http-data-access.ts diff --git a/packages/data-access/src/data-write.ts b/packages/data-access/src/data-write.ts index 6ab6c8017a..238b2c832b 100644 --- a/packages/data-access/src/data-write.ts +++ b/packages/data-access/src/data-write.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; import Block from './block'; import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; +import { getNoPersistTransactionRawData } from './no-persist-transaction-raw-data'; export class DataAccessWrite implements DataAccessTypes.IDataWrite { constructor( @@ -24,6 +25,19 @@ export class DataAccessWrite implements DataAccessTypes.IDataWrite { channelId: string, topics?: string[] | undefined, ): Promise { + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + + if (!this.persist) { + const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( + eventEmitter, + getNoPersistTransactionRawData(topics), + ); + + // Emit confirmation instantly since data is not going to be persisted + result.emit('confirmed', result); + return result; + } + const updatedBlock = Block.pushTransaction( Block.createEmptyBlock(), transaction, @@ -33,7 +47,6 @@ export class DataAccessWrite implements DataAccessTypes.IDataWrite { const storageResult = await this.storage.append(JSON.stringify(updatedBlock)); - const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; this.pendingStore?.add(channelId, { transaction, storageResult, topics: topics || [] }); const result: DataAccessTypes.IReturnPersistTransactionRaw = { diff --git a/packages/data-access/src/index.ts b/packages/data-access/src/index.ts index f4fc148b26..2d5b4731fe 100644 --- a/packages/data-access/src/index.ts +++ b/packages/data-access/src/index.ts @@ -5,3 +5,4 @@ export { DataAccessRead } from './data-read'; export { PendingStore } from './pending-store'; export { DataAccessBaseOptions } from './types'; export { MockDataAccess } from './mock-data-access'; +export { getNoPersistTransactionRawData } from './no-persist-transaction-raw-data'; diff --git a/packages/data-access/src/no-persist-transaction-raw-data.ts b/packages/data-access/src/no-persist-transaction-raw-data.ts new file mode 100644 index 0000000000..ff01b3a532 --- /dev/null +++ b/packages/data-access/src/no-persist-transaction-raw-data.ts @@ -0,0 +1,17 @@ +import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; + +export const getNoPersistTransactionRawData = ( + topics?: string[], +): DataAccessTypes.IReturnPersistTransactionRaw => { + return { + meta: { + topics: topics || [], + transactionStorageLocation: '', + storageMeta: { + state: StorageTypes.ContentState.PENDING, + timestamp: Date.now() / 1000, + }, + }, + result: {}, + }; +}; diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 8f37c66674..8863858dca 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -4,6 +4,7 @@ import httpConfigDefaults from './http-config-defaults'; import { normalizeKeccak256Hash, retry, validatePaginationParams } from '@requestnetwork/utils'; import { stringify } from 'qs'; import { utils } from 'ethers'; +import { getNoPersistTransactionRawData } from '@requestnetwork/data-access'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJson = require('../package.json'); @@ -39,7 +40,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { }: { httpConfig?: Partial; nodeConnectionConfig?: Partial; - persist: boolean; + persist?: boolean; } = { httpConfig: {}, nodeConnectionConfig: {}, @@ -48,7 +49,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { ) { // Get Request Client version to set it in the header const requestClientVersion = packageJson.version; - this.persist = persist; + this.persist = !!persist; this.httpConfig = { ...httpConfigDefaults, ...httpConfig, @@ -93,6 +94,19 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { channelId: string, topics?: string[], ): Promise { + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + + if (!this.persist) { + const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( + eventEmitter, + getNoPersistTransactionRawData(topics), + ); + + // Emit confirmation instantly since data is not going to be persisted + result.emit('confirmed', result); + return result; + } + // We don't retry this request since it may fail because of a slow Storage // For example, if the Ethereum network is slow and we retry the request three times // three data will be persisted at the end @@ -104,10 +118,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { ); // Create the return result with EventEmitter - const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( - new EventEmitter() as DataAccessTypes.PersistTransactionEmitter, - data, - ); + const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(eventEmitter, data); // Try to get the confirmation new Promise((r) => setTimeout(r, this.httpConfig.getConfirmationDeferDelay)) diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index ac6d63c738..fc07f4f9e1 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -1,4 +1,4 @@ -import { Block } from '@requestnetwork/data-access'; +import { Block, getNoPersistTransactionRawData } from '@requestnetwork/data-access'; import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; import { ClientTypes, CurrencyTypes, DataAccessTypes, StorageTypes } from '@requestnetwork/types'; import { ethers } from 'ethers'; @@ -73,6 +73,19 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { channelId: string, topics?: string[], ): Promise { + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + + if (!this.persist) { + const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( + eventEmitter, + getNoPersistTransactionRawData(topics), + ); + + // Emit confirmation instantly since data is not going to be persisted + result.emit('confirmed', result); + return result; + } + if (!this.networkName) { const network = await this.provider.getNetwork(); @@ -131,7 +144,6 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { } this.cache[channelId][ipfsHash] = { block, storageMeta }; - const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; const result: DataAccessTypes.IReturnPersistTransactionRaw = { meta: { storageMeta: { diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index b6b0a9d88b..19d91e6dee 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -12,7 +12,6 @@ import RequestNetwork from './api/request-network'; import HttpDataAccess, { NodeConnectionConfig } from './http-data-access'; import { MockDataAccess } from '@requestnetwork/data-access'; import { MockStorage } from './mock-storage'; -import { NoPersistHttpDataAccess } from './no-persist-http-data-access'; /** * Exposes RequestNetwork module configured to use http-data-access. @@ -60,12 +59,7 @@ export default class HttpRequestNetwork extends RequestNetwork { ? new MockDataAccess(new MockStorage(), { persist: !skipPersistence, }) - : skipPersistence - ? new NoPersistHttpDataAccess({ - httpConfig, - nodeConnectionConfig, - }) - : new HttpDataAccess({ httpConfig, nodeConnectionConfig, persist: true }); + : new HttpDataAccess({ httpConfig, nodeConnectionConfig, persist: !skipPersistence }); if (!currencyManager) { currencyManager = CurrencyManager.getDefault(); diff --git a/packages/request-client.js/src/no-persist-http-data-access.ts b/packages/request-client.js/src/no-persist-http-data-access.ts deleted file mode 100644 index d60e4fd6b2..0000000000 --- a/packages/request-client.js/src/no-persist-http-data-access.ts +++ /dev/null @@ -1,48 +0,0 @@ -import HttpDataAccess, { NodeConnectionConfig } from './http-data-access'; -import { ClientTypes, DataAccessTypes, StorageTypes } from '@requestnetwork/types'; -import { EventEmitter } from 'events'; - -export class NoPersistHttpDataAccess extends HttpDataAccess { - constructor( - { - httpConfig, - nodeConnectionConfig, - }: { - httpConfig?: Partial; - nodeConnectionConfig?: Partial; - } = { - httpConfig: {}, - nodeConnectionConfig: {}, - }, - ) { - super({ httpConfig, nodeConnectionConfig, persist: false }); - } - - async persistTransaction( - transactionData: DataAccessTypes.ITransaction, - channelId: string, - topics?: string[], - ): Promise { - const data: DataAccessTypes.IReturnPersistTransactionRaw = { - meta: { - topics: topics || [], - transactionStorageLocation: '', - storageMeta: { - state: StorageTypes.ContentState.PENDING, - timestamp: Date.now() / 1000, - }, - }, - result: {}, - }; - - const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( - new EventEmitter() as DataAccessTypes.PersistTransactionEmitter, - data, - ); - - // Emit confirmation instantly since data is not going to be persisted - result.emit('confirmed', result); - - return result; - } -} From b5375791fb781157a6926b4533747d9ff974be86 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Tue, 7 Jan 2025 18:31:46 +0700 Subject: [PATCH 03/11] fix test --- .../test/api/request-network.test.ts | 1 + .../transaction-manager/test/index.test.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/request-client.js/test/api/request-network.test.ts b/packages/request-client.js/test/api/request-network.test.ts index ae914d051f..af7d2f27c6 100644 --- a/packages/request-client.js/test/api/request-network.test.ts +++ b/packages/request-client.js/test/api/request-network.test.ts @@ -16,6 +16,7 @@ const mockDataAccess: DataAccessTypes.IDataAccess = { close: jest.fn(), persistTransaction: jest.fn(), getChannelsByMultipleTopics: jest.fn(), + persist: true, }; describe('api/request-network', () => { diff --git a/packages/transaction-manager/test/index.test.ts b/packages/transaction-manager/test/index.test.ts index 60106f03a3..44bb7e1a33 100644 --- a/packages/transaction-manager/test/index.test.ts +++ b/packages/transaction-manager/test/index.test.ts @@ -61,6 +61,7 @@ describe('index', () => { getTransactionsByChannelId: jest.fn().mockReturnValue(fakeMetaDataAccessGetReturn), initialize: jest.fn(), close: jest.fn(), + persist: true, persistTransaction: jest.fn((): any => { setTimeout( () => { @@ -226,6 +227,7 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnWithEncryptedTransaction), initialize: jest.fn(), close: jest.fn(), + persist: true, persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), }; @@ -272,6 +274,7 @@ describe('index', () => { getTransactionsByChannelId: jest.fn().mockReturnValue(fakeMetaDataAccessGetReturnEmpty), initialize: jest.fn(), close: jest.fn(), + persist: true, persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), }; @@ -315,6 +318,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), + persist: true, }; const transactionManager = new TransactionManager( @@ -391,6 +395,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -442,6 +447,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -499,6 +505,7 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnWithEncryptedTransaction), initialize: jest.fn(), close: jest.fn(), + persist: true, persistTransaction: jest.fn(), }; @@ -557,6 +564,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -630,6 +638,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -717,6 +726,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -793,6 +803,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -871,6 +882,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -948,6 +960,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -1096,6 +1109,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -1170,6 +1184,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( @@ -1238,6 +1253,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1317,6 +1333,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1386,6 +1403,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1465,6 +1483,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + persist: true, }; const transactionManager = new TransactionManager( From 08b410f18b0b6b787324671b0c0d78a87d842123 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Tue, 7 Jan 2025 19:08:14 +0700 Subject: [PATCH 04/11] fix default value --- packages/request-client.js/src/http-data-access.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 8863858dca..3fdc54cc9c 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -49,7 +49,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { ) { // Get Request Client version to set it in the header const requestClientVersion = packageJson.version; - this.persist = !!persist; + this.persist = persist || true; this.httpConfig = { ...httpConfigDefaults, ...httpConfig, @@ -97,6 +97,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; if (!this.persist) { + console.log('no persist'); const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( eventEmitter, getNoPersistTransactionRawData(topics), From 375908231a2606b8c7e7ffc42e8978779866a966 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Tue, 7 Jan 2025 20:00:26 +0700 Subject: [PATCH 05/11] fix request-client.js test --- packages/request-client.js/src/http-data-access.ts | 3 +-- packages/request-client.js/src/http-metamask-data-access.ts | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 3fdc54cc9c..0b411e7b73 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -49,7 +49,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { ) { // Get Request Client version to set it in the header const requestClientVersion = packageJson.version; - this.persist = persist || true; + this.persist = persist ?? true; this.httpConfig = { ...httpConfigDefaults, ...httpConfig, @@ -97,7 +97,6 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; if (!this.persist) { - console.log('no persist'); const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( eventEmitter, getNoPersistTransactionRawData(topics), diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index fc07f4f9e1..4b1b3027e9 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -33,16 +33,19 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { nodeConnectionConfig, web3, ethereumProviderUrl, + persist, }: { httpConfig?: Partial; nodeConnectionConfig?: NodeConnectionConfig; web3?: any; ethereumProviderUrl?: string; + persist?: boolean; } = { httpConfig: {}, + persist: true, }, ) { - super({ httpConfig, nodeConnectionConfig, persist: true }); + super({ httpConfig, nodeConnectionConfig, persist }); ethereumProviderUrl = ethereumProviderUrl ? ethereumProviderUrl : 'http://localhost:8545'; From 8c60f1a738956dcd79e177cd3cbbf2f5e5600166 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Fri, 10 Jan 2025 11:09:24 +0700 Subject: [PATCH 06/11] create NoPersistDataWrite class --- .../data-access/src/combined-data-access.ts | 11 +- packages/data-access/src/index.ts | 2 +- packages/data-access/src/mock-data-access.ts | 4 +- .../data-access/src/no-persist-data-write.ts | 36 +++ .../src/no-persist-transaction-raw-data.ts | 17 -- .../src/api/request-network.ts | 6 +- .../src/http-data-access-config.ts | 112 +++++++++ .../request-client.js/src/http-data-access.ts | 213 +++--------------- .../request-client.js/src/http-data-read.ts | 92 ++++++++ .../request-client.js/src/http-data-write.ts | 77 +++++++ .../src/http-metamask-data-access.ts | 152 ++----------- .../src/http-metamask-data-write.ts | 157 +++++++++++++ .../src/http-request-network.ts | 8 +- .../request-client.js/src/http-transaction.ts | 32 +++ packages/request-client.js/src/index.ts | 2 +- .../test/api/request-network.test.ts | 1 - .../test/in-memory-request.test.ts | 26 ++- .../thegraph-data-access/src/NoopDataWrite.ts | 2 - .../transaction-manager/test/index.test.ts | 19 -- packages/types/src/data-access-types.ts | 2 +- 20 files changed, 595 insertions(+), 376 deletions(-) create mode 100644 packages/data-access/src/no-persist-data-write.ts delete mode 100644 packages/data-access/src/no-persist-transaction-raw-data.ts create mode 100644 packages/request-client.js/src/http-data-access-config.ts create mode 100644 packages/request-client.js/src/http-data-read.ts create mode 100644 packages/request-client.js/src/http-data-write.ts create mode 100644 packages/request-client.js/src/http-metamask-data-write.ts create mode 100644 packages/request-client.js/src/http-transaction.ts diff --git a/packages/data-access/src/combined-data-access.ts b/packages/data-access/src/combined-data-access.ts index 4086ed1177..22c63f726e 100644 --- a/packages/data-access/src/combined-data-access.ts +++ b/packages/data-access/src/combined-data-access.ts @@ -1,14 +1,11 @@ import { DataAccessTypes } from '@requestnetwork/types'; +import { NoPersistDataWrite } from './no-persist-data-write'; export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess { - public readonly persist: boolean; - constructor( protected reader: DataAccessTypes.IDataRead, protected writer: DataAccessTypes.IDataWrite, - ) { - this.persist = writer.persist; - } + ) {} async initialize(): Promise { await this.reader.initialize(); @@ -20,6 +17,10 @@ export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess await this.reader.close(); } + isPersisting(): boolean { + return !(this.writer instanceof NoPersistDataWrite); + } + async getTransactionsByChannelId( channelId: string, updatedBetween?: DataAccessTypes.ITimestampBoundaries | undefined, diff --git a/packages/data-access/src/index.ts b/packages/data-access/src/index.ts index 2d5b4731fe..ed9c511aaf 100644 --- a/packages/data-access/src/index.ts +++ b/packages/data-access/src/index.ts @@ -5,4 +5,4 @@ export { DataAccessRead } from './data-read'; export { PendingStore } from './pending-store'; export { DataAccessBaseOptions } from './types'; export { MockDataAccess } from './mock-data-access'; -export { getNoPersistTransactionRawData } from './no-persist-transaction-raw-data'; +export { NoPersistDataWrite } from './no-persist-data-write'; diff --git a/packages/data-access/src/mock-data-access.ts b/packages/data-access/src/mock-data-access.ts index e421fef743..55b6e0d4fe 100644 --- a/packages/data-access/src/mock-data-access.ts +++ b/packages/data-access/src/mock-data-access.ts @@ -13,13 +13,13 @@ import { CombinedDataAccess } from './combined-data-access'; export class MockDataAccess extends CombinedDataAccess { private readonly dataIndex: InMemoryIndexer; - constructor(storage: StorageTypes.IStorage, options: { persist: boolean } = { persist: true }) { + constructor(storage: StorageTypes.IStorage) { const dataIndex = new InMemoryIndexer(storage); const pendingStore = new PendingStore(); super( new DataAccessRead(dataIndex, { network: 'mock', pendingStore }), - new DataAccessWrite(storage, pendingStore, options.persist), + new DataAccessWrite(storage, pendingStore), ); this.dataIndex = dataIndex; } diff --git a/packages/data-access/src/no-persist-data-write.ts b/packages/data-access/src/no-persist-data-write.ts new file mode 100644 index 0000000000..979bac5044 --- /dev/null +++ b/packages/data-access/src/no-persist-data-write.ts @@ -0,0 +1,36 @@ +import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; +import { EventEmitter } from 'events'; + +export class NoPersistDataWrite implements DataAccessTypes.IDataWrite { + async initialize(): Promise { + return; + } + + async close(): Promise { + return; + } + + async persistTransaction( + transaction: DataAccessTypes.ITransaction, + channelId: string, + topics?: string[] | undefined, + ): Promise { + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + + const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(eventEmitter, { + meta: { + topics: topics || [], + transactionStorageLocation: '', + storageMeta: { + state: StorageTypes.ContentState.PENDING, + timestamp: Date.now() / 1000, + }, + }, + result: {}, + }); + + // Emit confirmation instantly since data is not going to be persisted + result.emit('confirmed', result); + return result; + } +} diff --git a/packages/data-access/src/no-persist-transaction-raw-data.ts b/packages/data-access/src/no-persist-transaction-raw-data.ts deleted file mode 100644 index ff01b3a532..0000000000 --- a/packages/data-access/src/no-persist-transaction-raw-data.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; - -export const getNoPersistTransactionRawData = ( - topics?: string[], -): DataAccessTypes.IReturnPersistTransactionRaw => { - return { - meta: { - topics: topics || [], - transactionStorageLocation: '', - storageMeta: { - state: StorageTypes.ContentState.PENDING, - timestamp: Date.now() / 1000, - }, - }, - result: {}, - }; -}; diff --git a/packages/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index 003e3a0b61..0ae85d6bb9 100644 --- a/packages/request-client.js/src/api/request-network.ts +++ b/packages/request-client.js/src/api/request-network.ts @@ -114,7 +114,7 @@ export default class RequestNetwork { const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData; const requestId = requestLogicCreateResult.result.requestId; - const isSkippingPersistence = !this.dataAccess.persist; + const isSkippingPersistence = !this.dataAccess.isPersisting(); // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { contentDataExtension: this.contentData, @@ -157,7 +157,7 @@ export default class RequestNetwork { throw new Error('Cannot persist request without inMemoryInfo.'); } - if (!this.dataAccess.persist) { + if (!this.dataAccess.isPersisting()) { throw new Error( 'Cannot persist request when skipPersistence is enabled. To persist the request, create a new instance of RequestNetwork without skipPersistence being set to true.', ); @@ -197,7 +197,7 @@ export default class RequestNetwork { const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData; const requestId = requestLogicCreateResult.result.requestId; - const isSkippingPersistence = !this.dataAccess.persist; + const isSkippingPersistence = !this.dataAccess.isPersisting(); // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { diff --git a/packages/request-client.js/src/http-data-access-config.ts b/packages/request-client.js/src/http-data-access-config.ts new file mode 100644 index 0000000000..86c5b5d00a --- /dev/null +++ b/packages/request-client.js/src/http-data-access-config.ts @@ -0,0 +1,112 @@ +import { ClientTypes } from '@requestnetwork/types'; +import { retry } from '@requestnetwork/utils'; +import httpConfigDefaults from './http-config-defaults'; +import { stringify } from 'qs'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require('../package.json'); +export type NodeConnectionConfig = { baseURL: string; headers: Record }; + +export class HttpDataAccessConfig { + /** + * Configuration that overrides http-config-defaults, + * @see httpConfigDefaults for the default configuration. + */ + public httpConfig: ClientTypes.IHttpDataAccessConfig; + + /** + * Configuration that will be sent at each request. + */ + public nodeConnectionConfig: NodeConnectionConfig; + + constructor( + { + httpConfig, + nodeConnectionConfig, + }: { + httpConfig?: Partial; + nodeConnectionConfig?: Partial; + persist?: boolean; + } = { + httpConfig: {}, + nodeConnectionConfig: {}, + }, + ) { + const requestClientVersion = packageJson.version; + this.httpConfig = { + ...httpConfigDefaults, + ...httpConfig, + }; + this.nodeConnectionConfig = { + baseURL: 'http://localhost:3000', + headers: { + [this.httpConfig.requestClientVersionHeader]: requestClientVersion, + }, + ...nodeConnectionConfig, + }; + } + + /** + * Sends an HTTP GET request to the node and retries until it succeeds. + * Throws when the retry count reaches a maximum. + * + * @param url HTTP GET request url + * @param params HTTP GET request parameters + * @param retryConfig Maximum retry count, delay between retries, exponential backoff delay, and maximum exponential backoff delay + */ + public async fetchAndRetry( + path: string, + params: Record, + retryConfig: { + maxRetries?: number; + retryDelay?: number; + exponentialBackoffDelay?: number; + maxExponentialBackoffDelay?: number; + } = {}, + ): Promise { + retryConfig.maxRetries = retryConfig.maxRetries ?? this.httpConfig.httpRequestMaxRetry; + retryConfig.retryDelay = retryConfig.retryDelay ?? this.httpConfig.httpRequestRetryDelay; + retryConfig.exponentialBackoffDelay = + retryConfig.exponentialBackoffDelay ?? this.httpConfig.httpRequestExponentialBackoffDelay; + retryConfig.maxExponentialBackoffDelay = + retryConfig.maxExponentialBackoffDelay ?? + this.httpConfig.httpRequestMaxExponentialBackoffDelay; + return await retry(async () => await this.fetch('GET', path, params), retryConfig)(); + } + + public async fetch( + method: 'GET' | 'POST', + path: string, + params: Record | undefined, + body?: Record, + ): Promise { + const { baseURL, headers, ...options } = this.nodeConnectionConfig; + const url = new URL(path, baseURL); + if (params) { + // qs.parse doesn't handle well mixes of string and object params + for (const [key, value] of Object.entries(params)) { + if (typeof value === 'object') { + params[key] = JSON.stringify(value); + } + } + url.search = stringify(params); + } + const r = await fetch(url, { + method, + body: body ? JSON.stringify(body) : undefined, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + ...options, + }); + if (r.ok) { + return await r.json(); + } + + throw Object.assign(new Error(r.statusText), { + status: r.status, + statusText: r.statusText, + }); + } +} diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 0b411e7b73..7694fc6c3e 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -1,32 +1,17 @@ import { ClientTypes, DataAccessTypes } from '@requestnetwork/types'; -import { EventEmitter } from 'events'; -import httpConfigDefaults from './http-config-defaults'; -import { normalizeKeccak256Hash, retry, validatePaginationParams } from '@requestnetwork/utils'; -import { stringify } from 'qs'; import { utils } from 'ethers'; -import { getNoPersistTransactionRawData } from '@requestnetwork/data-access'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const packageJson = require('../package.json'); - -export type NodeConnectionConfig = { baseURL: string; headers: Record }; +import { CombinedDataAccess, NoPersistDataWrite } from '@requestnetwork/data-access'; +import { HttpDataRead } from './http-data-read'; +import { HttpDataWrite } from './http-data-write'; +import { HttpDataAccessConfig, NodeConnectionConfig } from './http-data-access-config'; +import { HttpTransaction } from './http-transaction'; /** * Exposes a Data-Access module over HTTP */ -export default class HttpDataAccess implements DataAccessTypes.IDataAccess { - public readonly persist: boolean; - /** - * Configuration that overrides http-config-defaults, - * @see httpConfigDefaults for the default configuration. - */ - protected httpConfig: ClientTypes.IHttpDataAccessConfig; - - /** - * Configuration that will be sent at each request. - */ - protected nodeConnectionConfig: NodeConnectionConfig; - +export default class HttpDataAccess extends CombinedDataAccess { + private readonly dataAccessConfig: HttpDataAccessConfig; + private readonly transaction: HttpTransaction; /** * Creates an instance of HttpDataAccess. * @param httpConfig @see ClientTypes.IHttpDataAccessConfig for available options. @@ -47,22 +32,18 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { persist: true, }, ) { - // Get Request Client version to set it in the header - const requestClientVersion = packageJson.version; - this.persist = persist ?? true; - this.httpConfig = { - ...httpConfigDefaults, - ...httpConfig, - }; - this.nodeConnectionConfig = { - baseURL: 'http://localhost:3000', - headers: { - [this.httpConfig.requestClientVersionHeader]: requestClientVersion, - }, - ...nodeConnectionConfig, - }; - } + const dataAccessConfig = new HttpDataAccessConfig({ httpConfig, nodeConnectionConfig }); + const transaction = new HttpTransaction(dataAccessConfig); + const reader = new HttpDataRead(dataAccessConfig); + const writer = persist + ? new HttpDataWrite(dataAccessConfig, transaction) + : new NoPersistDataWrite(); + super(reader, writer); + + this.dataAccessConfig = dataAccessConfig; + this.transaction = transaction; + } /** * Initialize the module. Does nothing, exists only to implement IDataAccess * @@ -94,50 +75,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { channelId: string, topics?: string[], ): Promise { - const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; - - if (!this.persist) { - const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( - eventEmitter, - getNoPersistTransactionRawData(topics), - ); - - // Emit confirmation instantly since data is not going to be persisted - result.emit('confirmed', result); - return result; - } - - // We don't retry this request since it may fail because of a slow Storage - // For example, if the Ethereum network is slow and we retry the request three times - // three data will be persisted at the end - const data = await this.fetch( - 'POST', - '/persistTransaction', - undefined, - { channelId, topics, transactionData }, - ); - - // Create the return result with EventEmitter - const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(eventEmitter, data); - - // Try to get the confirmation - new Promise((r) => setTimeout(r, this.httpConfig.getConfirmationDeferDelay)) - .then(async () => { - const confirmedData = await this.getConfirmedTransaction(transactionData); - // when found, emit the event 'confirmed' - result.emit('confirmed', confirmedData); - }) - .catch((e) => { - let error: Error = e; - if (e && 'status' in e && e.status === 404) { - error = new Error( - `Timeout while confirming the Request was persisted. It is likely that the Request will be confirmed eventually. Catch this error and use getConfirmedTransaction() to continue polling for confirmation. Adjusting the httpConfig settings on the RequestNetwork object to avoid future timeouts. Avoid calling persistTransaction() again to prevent creating a duplicate Request.`, - ); - } - result.emit('error', error); - }); - - return result; + return await this.writer.persistTransaction(transactionData, channelId, topics); } /** @@ -147,20 +85,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { public async getConfirmedTransaction( transactionData: DataAccessTypes.ITransaction, ): Promise { - const transactionHash: string = normalizeKeccak256Hash(transactionData).value; - - return await this.fetchAndRetry( - '/getConfirmedTransaction', - { - transactionHash, - }, - { - maxRetries: this.httpConfig.getConfirmationMaxRetry, - retryDelay: this.httpConfig.getConfirmationRetryDelay, - exponentialBackoffDelay: this.httpConfig.getConfirmationExponentialBackoffDelay, - maxExponentialBackoffDelay: this.httpConfig.getConfirmationMaxExponentialBackoffDelay, - }, - ); + return await this.transaction.getConfirmedTransaction(transactionData); } /** @@ -173,13 +98,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { channelId: string, timestampBoundaries?: DataAccessTypes.ITimestampBoundaries, ): Promise { - return await this.fetchAndRetry( - '/getTransactionsByChannelId', - { - channelId, - timestampBoundaries, - }, - ); + return await this.reader.getTransactionsByChannelId(channelId, timestampBoundaries); } /** @@ -194,16 +113,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { page?: number, pageSize?: number, ): Promise { - validatePaginationParams(page, pageSize); - - const params = { - topic, - updatedBetween, - ...(page !== undefined && { page }), - ...(pageSize !== undefined && { pageSize }), - }; - - return await this.fetchAndRetry('/getChannelsByTopic', params); + return await this.reader.getChannelsByTopic(topic, updatedBetween, page, pageSize); } /** @@ -218,14 +128,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { page?: number, pageSize?: number, ): Promise { - validatePaginationParams(page, pageSize); - - return await this.fetchAndRetry('/getChannelsByMultipleTopics', { - topics, - updatedBetween, - page, - pageSize, - }); + return await this.reader.getChannelsByMultipleTopics(topics, updatedBetween, page, pageSize); } /** @@ -233,7 +136,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { * */ public async _getStatus(): Promise { - return await this.fetchAndRetry('/information', {}); + return await this.dataAccessConfig.fetchAndRetry('/information', {}); } /** @@ -250,70 +153,8 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { if (!utils.isAddress(delegateeAddress)) { throw new Error('delegateeAddress must be a valid Ethereum address'); } - return await this.fetchAndRetry('/getLitCapacityDelegationAuthSig', { delegateeAddress }); - } - - /** - * Sends an HTTP GET request to the node and retries until it succeeds. - * Throws when the retry count reaches a maximum. - * - * @param url HTTP GET request url - * @param params HTTP GET request parameters - * @param retryConfig Maximum retry count, delay between retries, exponential backoff delay, and maximum exponential backoff delay - */ - protected async fetchAndRetry( - path: string, - params: Record, - retryConfig: { - maxRetries?: number; - retryDelay?: number; - exponentialBackoffDelay?: number; - maxExponentialBackoffDelay?: number; - } = {}, - ): Promise { - retryConfig.maxRetries = retryConfig.maxRetries ?? this.httpConfig.httpRequestMaxRetry; - retryConfig.retryDelay = retryConfig.retryDelay ?? this.httpConfig.httpRequestRetryDelay; - retryConfig.exponentialBackoffDelay = - retryConfig.exponentialBackoffDelay ?? this.httpConfig.httpRequestExponentialBackoffDelay; - retryConfig.maxExponentialBackoffDelay = - retryConfig.maxExponentialBackoffDelay ?? - this.httpConfig.httpRequestMaxExponentialBackoffDelay; - return await retry(async () => await this.fetch('GET', path, params), retryConfig)(); - } - - protected async fetch( - method: 'GET' | 'POST', - path: string, - params: Record | undefined, - body?: Record, - ): Promise { - const { baseURL, headers, ...options } = this.nodeConnectionConfig; - const url = new URL(path, baseURL); - if (params) { - // qs.parse doesn't handle well mixes of string and object params - for (const [key, value] of Object.entries(params)) { - if (typeof value === 'object') { - params[key] = JSON.stringify(value); - } - } - url.search = stringify(params); - } - const r = await fetch(url, { - method, - body: body ? JSON.stringify(body) : undefined, - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - ...options, - }); - if (r.ok) { - return await r.json(); - } - - throw Object.assign(new Error(r.statusText), { - status: r.status, - statusText: r.statusText, + return await this.dataAccessConfig.fetchAndRetry('/getLitCapacityDelegationAuthSig', { + delegateeAddress, }); } } diff --git a/packages/request-client.js/src/http-data-read.ts b/packages/request-client.js/src/http-data-read.ts new file mode 100644 index 0000000000..ea27807e62 --- /dev/null +++ b/packages/request-client.js/src/http-data-read.ts @@ -0,0 +1,92 @@ +import { DataAccessTypes } from '@requestnetwork/types'; +import { validatePaginationParams } from '@requestnetwork/utils'; +import { HttpDataAccessConfig } from './http-data-access-config'; + +export class HttpDataRead implements DataAccessTypes.IDataRead { + constructor(private readonly dataAccessConfig: HttpDataAccessConfig) {} + + /** + * Initialize the module. Does nothing, exists only to implement IDataAccess + * + * @returns nothing + */ + public async initialize(): Promise { + // no-op, nothing to do + return; + } + + /** + * Closes the module. Does nothing, exists only to implement IDataAccess + * + * @returns nothing + */ + public async close(): Promise { + // no-op, nothing to do + return; + } + + /** + * Gets the transactions for a channel from the node through HTTP. + * + * @param channelId The channel id to search for + * @param timestampBoundaries filter timestamp boundaries + */ + public async getTransactionsByChannelId( + channelId: string, + timestampBoundaries?: DataAccessTypes.ITimestampBoundaries, + ): Promise { + return await this.dataAccessConfig.fetchAndRetry( + '/getTransactionsByChannelId', + { + channelId, + timestampBoundaries, + }, + ); + } + + /** + * Gets all the transactions of channel indexed by topic from the node through HTTP. + * + * @param topic topic to search for + * @param updatedBetween filter timestamp boundaries + */ + public async getChannelsByTopic( + topic: string, + updatedBetween?: DataAccessTypes.ITimestampBoundaries, + page?: number, + pageSize?: number, + ): Promise { + validatePaginationParams(page, pageSize); + + const params = { + topic, + updatedBetween, + ...(page !== undefined && { page }), + ...(pageSize !== undefined && { pageSize }), + }; + + return await this.dataAccessConfig.fetchAndRetry('/getChannelsByTopic', params); + } + + /** + * Gets all the transactions of channel indexed by multiple topics from the node through HTTP. + * + * @param topics topics to search for + * @param updatedBetween filter timestamp boundaries + */ + public async getChannelsByMultipleTopics( + topics: string[], + updatedBetween?: DataAccessTypes.ITimestampBoundaries, + page?: number, + pageSize?: number, + ): Promise { + validatePaginationParams(page, pageSize); + + return await this.dataAccessConfig.fetchAndRetry('/getChannelsByMultipleTopics', { + topics, + updatedBetween, + page, + pageSize, + }); + } +} diff --git a/packages/request-client.js/src/http-data-write.ts b/packages/request-client.js/src/http-data-write.ts new file mode 100644 index 0000000000..57047be15d --- /dev/null +++ b/packages/request-client.js/src/http-data-write.ts @@ -0,0 +1,77 @@ +import { DataAccessTypes } from '@requestnetwork/types'; +import { HttpDataAccessConfig } from './http-data-access-config'; +import { EventEmitter } from 'events'; +import { HttpTransaction } from './http-transaction'; + +export class HttpDataWrite implements DataAccessTypes.IDataWrite { + constructor( + private readonly dataAccessConfig: HttpDataAccessConfig, + private readonly transaction: HttpTransaction, + ) {} + + /** + * Initialize the module. Does nothing, exists only to implement IDataAccess + * + * @returns nothing + */ + public async initialize(): Promise { + // no-op, nothing to do + return; + } + + /** + * Closes the module. Does nothing, exists only to implement IDataAccess + * + * @returns nothing + */ + public async close(): Promise { + // no-op, nothing to do + return; + } + + /** + * Persists a new transaction on a node through HTTP. + * + * @param transactionData The transaction data + * @param topics The topics used to index the transaction + */ + public async persistTransaction( + transactionData: DataAccessTypes.ITransaction, + channelId: string, + topics?: string[], + ): Promise { + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + + // We don't retry this request since it may fail because of a slow Storage + // For example, if the Ethereum network is slow and we retry the request three times + // three data will be persisted at the end + const data = await this.dataAccessConfig.fetch( + 'POST', + '/persistTransaction', + undefined, + { channelId, topics, transactionData }, + ); + + // Create the return result with EventEmitter + const result: DataAccessTypes.IReturnPersistTransaction = Object.assign(eventEmitter, data); + + // Try to get the confirmation + new Promise((r) => setTimeout(r, this.dataAccessConfig.httpConfig.getConfirmationDeferDelay)) + .then(async () => { + const confirmedData = await this.transaction.getConfirmedTransaction(transactionData); + // when found, emit the event 'confirmed' + result.emit('confirmed', confirmedData); + }) + .catch((e) => { + let error: Error = e; + if (e && 'status' in e && e.status === 404) { + error = new Error( + `Timeout while confirming the Request was persisted. It is likely that the Request will be confirmed eventually. Catch this error and use getConfirmedTransaction() to continue polling for confirmation. Adjusting the httpConfig settings on the RequestNetwork object to avoid future timeouts. Avoid calling persistTransaction() again to prevent creating a duplicate Request.`, + ); + } + result.emit('error', error); + }); + + return result; + } +} diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index 4b1b3027e9..cf9425a612 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -1,14 +1,14 @@ -import { Block, getNoPersistTransactionRawData } from '@requestnetwork/data-access'; -import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; -import { ClientTypes, CurrencyTypes, DataAccessTypes, StorageTypes } from '@requestnetwork/types'; +import { CombinedDataAccess } from '@requestnetwork/data-access'; +import { ClientTypes, CurrencyTypes, DataAccessTypes } from '@requestnetwork/types'; import { ethers } from 'ethers'; -import { EventEmitter } from 'events'; -import HttpDataAccess, { NodeConnectionConfig } from './http-data-access'; - +import HttpDataAccess from './http-data-access'; +import { HttpDataAccessConfig, NodeConnectionConfig } from './http-data-access-config'; +import { HttpDataRead } from './http-data-read'; +import { HttpMetaMaskDataWrite } from './http-metamask-data-write'; /** * Exposes a Data-Access module over HTTP */ -export default class HttpMetaMaskDataAccess extends HttpDataAccess { +export default class HttpMetaMaskDataAccess extends CombinedDataAccess { /** * Cache block persisted directly (in case the node did not have the time to retrieve it) * (public for easier testing) @@ -22,6 +22,9 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { private provider: ethers.providers.JsonRpcProvider | ethers.providers.Web3Provider; private networkName: CurrencyTypes.EvmChainName = 'private'; + private readonly dataAccessConfig: HttpDataAccessConfig; + private readonly dataAccess: HttpDataAccess; + /** * Creates an instance of HttpDataAccess. * @param httpConfig Http config that will be used by the underlying http-data-access. @see ClientTypes.IHttpDataAccessConfig @@ -33,26 +36,31 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { nodeConnectionConfig, web3, ethereumProviderUrl, - persist, }: { httpConfig?: Partial; nodeConnectionConfig?: NodeConnectionConfig; web3?: any; ethereumProviderUrl?: string; - persist?: boolean; } = { httpConfig: {}, - persist: true, }, ) { - super({ httpConfig, nodeConnectionConfig, persist }); - ethereumProviderUrl = ethereumProviderUrl ? ethereumProviderUrl : 'http://localhost:8545'; // Creates a local or default provider - this.provider = web3 + const provider = web3 ? new ethers.providers.Web3Provider(web3) : new ethers.providers.JsonRpcProvider({ url: ethereumProviderUrl }); + + const dataAccessConfig = new HttpDataAccessConfig({ httpConfig, nodeConnectionConfig }); + const reader = new HttpDataRead(dataAccessConfig); + const writer = new HttpMetaMaskDataWrite(dataAccessConfig, provider); + + super(reader, writer); + + this.dataAccessConfig = dataAccessConfig; + this.dataAccess = new HttpDataAccess(dataAccessConfig); + this.provider = provider; } /** @@ -76,117 +84,7 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { channelId: string, topics?: string[], ): Promise { - const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; - - if (!this.persist) { - const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( - eventEmitter, - getNoPersistTransactionRawData(topics), - ); - - // Emit confirmation instantly since data is not going to be persisted - result.emit('confirmed', result); - return result; - } - - if (!this.networkName) { - const network = await this.provider.getNetwork(); - - this.networkName = - network.chainId === 1 ? 'mainnet' : network.chainId === 4 ? 'rinkeby' : 'private'; - } - const submitterContract = requestHashSubmitterArtifact.connect( - this.networkName, - this.provider.getSigner(), - ); - - // We don't use the node to persist the transaction, but we will Do it ourselves - - // create a block and add the transaction in it - const block: DataAccessTypes.IBlock = Block.pushTransaction( - Block.createEmptyBlock(), - transactionData, - channelId, - topics, - ); - - // store the block on ipfs and get the the ipfs hash and size - const { ipfsHash, ipfsSize } = await this.fetch<{ ipfsHash: string; ipfsSize: number }>( - 'POST', - '/ipfsAdd', - { data: block }, - ); - - // get the fee required to submit the hash - const fee = await submitterContract.getFeesAmount(ipfsSize); - - // submit the hash to ethereum - const tx = await submitterContract.submitHash( - ipfsHash, - /* eslint-disable no-magic-numbers */ - ethers.utils.hexZeroPad(ethers.utils.hexlify(ipfsSize), 32), - { value: fee }, - ); - - const ethBlock = await this.provider.getBlock(tx.blockNumber ?? -1); - - // create the storage meta from the transaction receipt - const storageMeta: StorageTypes.IEthereumMetadata = { - blockConfirmation: tx.confirmations, - blockNumber: tx.blockNumber ?? -1, - blockTimestamp: ethBlock.timestamp, - fee: fee.toString(), - networkName: this.networkName, - smartContractAddress: tx.to ?? '', - transactionHash: tx.hash, - }; - - // Add the block to the cache - if (!this.cache[channelId]) { - this.cache[channelId] = {}; - } - this.cache[channelId][ipfsHash] = { block, storageMeta }; - - const result: DataAccessTypes.IReturnPersistTransactionRaw = { - meta: { - storageMeta: { - ethereum: storageMeta, - ipfs: { size: ipfsSize }, - state: StorageTypes.ContentState.PENDING, - timestamp: storageMeta.blockTimestamp, - }, - topics: topics || [], - transactionStorageLocation: ipfsHash, - }, - result: {}, - }; - - // When the ethereum transaction is mined, emit an event 'confirmed' - void tx.wait().then((txConfirmed) => { - // emit the event to tell the request transaction is confirmed - eventEmitter.emit('confirmed', { - meta: { - storageMeta: { - ethereum: { - blockConfirmation: txConfirmed.confirmations, - blockNumber: txConfirmed.blockNumber, - blockTimestamp: ethBlock.timestamp, - fee: fee.toString(), - networkName: this.networkName, - smartContractAddress: txConfirmed.to, - transactionHash: txConfirmed.transactionHash, - }, - state: StorageTypes.ContentState.CONFIRMED, - timestamp: ethBlock.timestamp, - }, - topics: topics || [], - transactionStorageLocation: ipfsHash, - }, - result: {}, - }); - }); - - return Object.assign(eventEmitter, result); + return this.writer.persistTransaction(transactionData, channelId, topics); } /** @@ -199,14 +97,14 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { channelId: string, timestampBoundaries?: DataAccessTypes.ITimestampBoundaries, ): Promise { - const data = await this.fetchAndRetry( + const data = await this.dataAccessConfig.fetchAndRetry( '/getTransactionsByChannelId', { params: { channelId, timestampBoundaries }, }, { - maxRetries: this.httpConfig.httpRequestMaxRetry, - retryDelay: this.httpConfig.httpRequestRetryDelay, + maxRetries: this.dataAccessConfig.httpConfig.httpRequestMaxRetry, + retryDelay: this.dataAccessConfig.httpConfig.httpRequestRetryDelay, }, ); diff --git a/packages/request-client.js/src/http-metamask-data-write.ts b/packages/request-client.js/src/http-metamask-data-write.ts new file mode 100644 index 0000000000..59acd48cb1 --- /dev/null +++ b/packages/request-client.js/src/http-metamask-data-write.ts @@ -0,0 +1,157 @@ +import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; +import { HttpDataAccessConfig } from './http-data-access-config'; +import { CurrencyTypes } from '@requestnetwork/types'; +import { ethers } from 'ethers'; +import { EventEmitter } from 'events'; +import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; +import { Block } from '@requestnetwork/data-access'; + +export class HttpMetaMaskDataWrite implements DataAccessTypes.IDataWrite { + /** + * Cache block persisted directly (in case the node did not have the time to retrieve it) + * (public for easier testing) + */ + public cache: { + [channelId: string]: { + [ipfsHash: string]: { block: DataAccessTypes.IBlock; storageMeta: any } | null; + }; + } = {}; + + constructor( + private readonly dataAccessConfig: HttpDataAccessConfig, + private provider: ethers.providers.JsonRpcProvider | ethers.providers.Web3Provider, + private networkName: CurrencyTypes.EvmChainName = 'private', + ) {} + + /** + * Initialize the module. Does nothing, exists only to implement IDataAccess + * + * @returns nothing + */ + public async initialize(): Promise { + // no-op, nothing to do + return; + } + + /** + * Closes the module. Does nothing, exists only to implement IDataAccess + * + * @returns nothing + */ + public async close(): Promise { + // no-op, nothing to do + return; + } + + /** + * Persists a new transaction using the node only for IPFS but persisting on ethereum through local provider + * + * @param transactionData The transaction data + * @param topics The topics used to index the transaction + */ + public async persistTransaction( + transactionData: DataAccessTypes.ITransaction, + channelId: string, + topics?: string[], + ): Promise { + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + + if (!this.networkName) { + const network = await this.provider.getNetwork(); + + this.networkName = + network.chainId === 1 ? 'mainnet' : network.chainId === 4 ? 'rinkeby' : 'private'; + } + const submitterContract = requestHashSubmitterArtifact.connect( + this.networkName, + this.provider.getSigner(), + ); + + // We don't use the node to persist the transaction, but we will Do it ourselves + + // create a block and add the transaction in it + const block: DataAccessTypes.IBlock = Block.pushTransaction( + Block.createEmptyBlock(), + transactionData, + channelId, + topics, + ); + + // store the block on ipfs and get the the ipfs hash and size + const { ipfsHash, ipfsSize } = await this.dataAccessConfig.fetch<{ + ipfsHash: string; + ipfsSize: number; + }>('POST', '/ipfsAdd', { data: block }); + + // get the fee required to submit the hash + const fee = await submitterContract.getFeesAmount(ipfsSize); + + // submit the hash to ethereum + const tx = await submitterContract.submitHash( + ipfsHash, + /* eslint-disable no-magic-numbers */ + ethers.utils.hexZeroPad(ethers.utils.hexlify(ipfsSize), 32), + { value: fee }, + ); + + const ethBlock = await this.provider.getBlock(tx.blockNumber ?? -1); + + // create the storage meta from the transaction receipt + const storageMeta: StorageTypes.IEthereumMetadata = { + blockConfirmation: tx.confirmations, + blockNumber: tx.blockNumber ?? -1, + blockTimestamp: ethBlock.timestamp, + fee: fee.toString(), + networkName: this.networkName, + smartContractAddress: tx.to ?? '', + transactionHash: tx.hash, + }; + + // Add the block to the cache + if (!this.cache[channelId]) { + this.cache[channelId] = {}; + } + this.cache[channelId][ipfsHash] = { block, storageMeta }; + + const result: DataAccessTypes.IReturnPersistTransactionRaw = { + meta: { + storageMeta: { + ethereum: storageMeta, + ipfs: { size: ipfsSize }, + state: StorageTypes.ContentState.PENDING, + timestamp: storageMeta.blockTimestamp, + }, + topics: topics || [], + transactionStorageLocation: ipfsHash, + }, + result: {}, + }; + + // When the ethereum transaction is mined, emit an event 'confirmed' + void tx.wait().then((txConfirmed) => { + // emit the event to tell the request transaction is confirmed + eventEmitter.emit('confirmed', { + meta: { + storageMeta: { + ethereum: { + blockConfirmation: txConfirmed.confirmations, + blockNumber: txConfirmed.blockNumber, + blockTimestamp: ethBlock.timestamp, + fee: fee.toString(), + networkName: this.networkName, + smartContractAddress: txConfirmed.to, + transactionHash: txConfirmed.transactionHash, + }, + state: StorageTypes.ContentState.CONFIRMED, + timestamp: ethBlock.timestamp, + }, + topics: topics || [], + transactionStorageLocation: ipfsHash, + }, + result: {}, + }); + }); + + return Object.assign(eventEmitter, result); + } +} diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index 19d91e6dee..d281dc1f2f 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -9,10 +9,10 @@ import { } from '@requestnetwork/types'; import { PaymentNetworkOptions } from '@requestnetwork/payment-detection'; import RequestNetwork from './api/request-network'; -import HttpDataAccess, { NodeConnectionConfig } from './http-data-access'; +import HttpDataAccess from './http-data-access'; import { MockDataAccess } from '@requestnetwork/data-access'; import { MockStorage } from './mock-storage'; - +import { NodeConnectionConfig } from './http-data-access-config'; /** * Exposes RequestNetwork module configured to use http-data-access. */ @@ -56,9 +56,7 @@ export default class HttpRequestNetwork extends RequestNetwork { }, ) { const dataAccess: DataAccessTypes.IDataAccess = useMockStorage - ? new MockDataAccess(new MockStorage(), { - persist: !skipPersistence, - }) + ? new MockDataAccess(new MockStorage()) : new HttpDataAccess({ httpConfig, nodeConnectionConfig, persist: !skipPersistence }); if (!currencyManager) { diff --git a/packages/request-client.js/src/http-transaction.ts b/packages/request-client.js/src/http-transaction.ts new file mode 100644 index 0000000000..e153b83f62 --- /dev/null +++ b/packages/request-client.js/src/http-transaction.ts @@ -0,0 +1,32 @@ +import { HttpDataAccessConfig } from './http-data-access-config'; +import { DataAccessTypes } from '@requestnetwork/types'; +import { normalizeKeccak256Hash } from '@requestnetwork/utils'; + +export class HttpTransaction { + constructor(private readonly dataAccessConfig: HttpDataAccessConfig) {} + + /** + * Gets a transaction from the node through HTTP. + * @param transactionData The transaction data + */ + public async getConfirmedTransaction( + transactionData: DataAccessTypes.ITransaction, + ): Promise { + const transactionHash: string = normalizeKeccak256Hash(transactionData).value; + + return await this.dataAccessConfig.fetchAndRetry( + '/getConfirmedTransaction', + { + transactionHash, + }, + { + maxRetries: this.dataAccessConfig.httpConfig.getConfirmationMaxRetry, + retryDelay: this.dataAccessConfig.httpConfig.getConfirmationRetryDelay, + exponentialBackoffDelay: + this.dataAccessConfig.httpConfig.getConfirmationExponentialBackoffDelay, + maxExponentialBackoffDelay: + this.dataAccessConfig.httpConfig.getConfirmationMaxExponentialBackoffDelay, + }, + ); + } +} diff --git a/packages/request-client.js/src/index.ts b/packages/request-client.js/src/index.ts index a5fbf6e35a..dced3bc196 100644 --- a/packages/request-client.js/src/index.ts +++ b/packages/request-client.js/src/index.ts @@ -6,8 +6,8 @@ import { default as RequestNetwork } from './http-request-network'; import { default as RequestNetworkBase } from './api/request-network'; import { default as HttpMetaMaskDataAccess } from './http-metamask-data-access'; import { default as HttpDataAccess } from './http-data-access'; -import { NodeConnectionConfig } from './http-data-access'; import * as Types from './types'; +import { NodeConnectionConfig } from './http-data-access-config'; export { PaymentReferenceCalculator, diff --git a/packages/request-client.js/test/api/request-network.test.ts b/packages/request-client.js/test/api/request-network.test.ts index af7d2f27c6..ae914d051f 100644 --- a/packages/request-client.js/test/api/request-network.test.ts +++ b/packages/request-client.js/test/api/request-network.test.ts @@ -16,7 +16,6 @@ const mockDataAccess: DataAccessTypes.IDataAccess = { close: jest.fn(), persistTransaction: jest.fn(), getChannelsByMultipleTopics: jest.fn(), - persist: true, }; describe('api/request-network', () => { diff --git a/packages/request-client.js/test/in-memory-request.test.ts b/packages/request-client.js/test/in-memory-request.test.ts index 622e97e26f..973274d35b 100644 --- a/packages/request-client.js/test/in-memory-request.test.ts +++ b/packages/request-client.js/test/in-memory-request.test.ts @@ -1,12 +1,27 @@ import { RequestNetwork, RequestNetworkBase } from '../src/index'; import * as TestData from './data-test'; -import { DataAccessTypes } from '@requestnetwork/types'; import { http, HttpResponse } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; import config from '../src/http-config-defaults'; -import { MockDataAccess } from '@requestnetwork/data-access'; -import { MockStorage } from '../src/mock-storage'; +import { + CombinedDataAccess, + DataAccessRead, + NoPersistDataWrite, + PendingStore, +} from '@requestnetwork/data-access'; + +class MyCustomDataAccess extends CombinedDataAccess { + constructor() { + const pendingStore = new PendingStore(); + super( + new DataAccessRead({} as any, { network: 'mock', pendingStore }), + new NoPersistDataWrite(), + ); + } +} +{ +} describe('handle in-memory request', () => { let requestNetwork: RequestNetwork; @@ -64,12 +79,11 @@ describe('handle in-memory request', () => { }); it('creates a request without persisting it with custom data access', async () => { - const mockStorage = new MockStorage(); - const mockDataAccess = new MockDataAccess(mockStorage, { persist: false }); + const myCustomDataAccess = new MyCustomDataAccess(); requestNetwork = new RequestNetworkBase({ signatureProvider: TestData.fakeSignatureProvider, - dataAccess: mockDataAccess, + dataAccess: myCustomDataAccess, }); const request = await requestNetwork.createRequest(requestCreationParams); diff --git a/packages/thegraph-data-access/src/NoopDataWrite.ts b/packages/thegraph-data-access/src/NoopDataWrite.ts index 44d720d2fd..80e7847e5f 100644 --- a/packages/thegraph-data-access/src/NoopDataWrite.ts +++ b/packages/thegraph-data-access/src/NoopDataWrite.ts @@ -1,8 +1,6 @@ import { DataAccessTypes } from '@requestnetwork/types'; export class NoopDataWrite implements DataAccessTypes.IDataWrite { - constructor(public readonly persist: boolean = true) {} - async initialize(): Promise { // no-op } diff --git a/packages/transaction-manager/test/index.test.ts b/packages/transaction-manager/test/index.test.ts index 44bb7e1a33..60106f03a3 100644 --- a/packages/transaction-manager/test/index.test.ts +++ b/packages/transaction-manager/test/index.test.ts @@ -61,7 +61,6 @@ describe('index', () => { getTransactionsByChannelId: jest.fn().mockReturnValue(fakeMetaDataAccessGetReturn), initialize: jest.fn(), close: jest.fn(), - persist: true, persistTransaction: jest.fn((): any => { setTimeout( () => { @@ -227,7 +226,6 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnWithEncryptedTransaction), initialize: jest.fn(), close: jest.fn(), - persist: true, persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), }; @@ -274,7 +272,6 @@ describe('index', () => { getTransactionsByChannelId: jest.fn().mockReturnValue(fakeMetaDataAccessGetReturnEmpty), initialize: jest.fn(), close: jest.fn(), - persist: true, persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), }; @@ -318,7 +315,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), - persist: true, }; const transactionManager = new TransactionManager( @@ -395,7 +391,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -447,7 +442,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -505,7 +499,6 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnWithEncryptedTransaction), initialize: jest.fn(), close: jest.fn(), - persist: true, persistTransaction: jest.fn(), }; @@ -564,7 +557,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -638,7 +630,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -726,7 +717,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -803,7 +793,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -882,7 +871,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -960,7 +948,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -1109,7 +1096,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -1184,7 +1170,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( @@ -1253,7 +1238,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1333,7 +1317,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1403,7 +1386,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1483,7 +1465,6 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - persist: true, }; const transactionManager = new TransactionManager( diff --git a/packages/types/src/data-access-types.ts b/packages/types/src/data-access-types.ts index 6b3bf725ef..47067590a1 100644 --- a/packages/types/src/data-access-types.ts +++ b/packages/types/src/data-access-types.ts @@ -25,7 +25,6 @@ export interface IDataRead { } export interface IDataWrite { - persist: boolean; initialize: () => Promise; close: () => Promise; @@ -37,6 +36,7 @@ export interface IDataWrite { } export interface IDataAccess extends IDataRead, IDataWrite { + isPersisting(): boolean; _getStatus?(): Promise; getLitCapacityDelegationAuthSig?: (delegateeAddress: string) => Promise; } From 48d7654d8b06096619c5c5d76194c31b2d378da9 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Fri, 10 Jan 2025 11:11:22 +0700 Subject: [PATCH 07/11] fix test --- .../transaction-manager/test/index.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/transaction-manager/test/index.test.ts b/packages/transaction-manager/test/index.test.ts index 60106f03a3..93e7426d2b 100644 --- a/packages/transaction-manager/test/index.test.ts +++ b/packages/transaction-manager/test/index.test.ts @@ -74,6 +74,7 @@ describe('index', () => { ); return fakeMetaDataAccessPersistReturn; }), + isPersisting: jest.fn().mockReturnValue(true), }; }); @@ -227,6 +228,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -273,6 +275,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -315,6 +318,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -391,6 +395,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -441,6 +446,7 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnFirstHashWrong), initialize: jest.fn(), close: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), persistTransaction: jest.fn(), }; @@ -500,6 +506,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -557,6 +564,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -630,6 +638,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -717,6 +726,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -793,6 +803,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -871,6 +882,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -948,6 +960,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1096,6 +1109,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1170,6 +1184,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1238,6 +1253,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1317,6 +1333,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1386,6 +1403,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1465,6 +1483,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( From dcf76d956c84a64b51b59c543df857808740fdb3 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Fri, 10 Jan 2025 11:12:53 +0700 Subject: [PATCH 08/11] remove persist flag --- packages/data-access/src/data-write.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/data-access/src/data-write.ts b/packages/data-access/src/data-write.ts index 238b2c832b..b22edf73b7 100644 --- a/packages/data-access/src/data-write.ts +++ b/packages/data-access/src/data-write.ts @@ -1,13 +1,11 @@ import { EventEmitter } from 'events'; import Block from './block'; import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; -import { getNoPersistTransactionRawData } from './no-persist-transaction-raw-data'; export class DataAccessWrite implements DataAccessTypes.IDataWrite { constructor( protected readonly storage: StorageTypes.IStorageWrite, private readonly pendingStore?: DataAccessTypes.IPendingStore, - public readonly persist: boolean = true, ) { this.pendingStore = pendingStore; } @@ -25,19 +23,6 @@ export class DataAccessWrite implements DataAccessTypes.IDataWrite { channelId: string, topics?: string[] | undefined, ): Promise { - const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; - - if (!this.persist) { - const result: DataAccessTypes.IReturnPersistTransaction = Object.assign( - eventEmitter, - getNoPersistTransactionRawData(topics), - ); - - // Emit confirmation instantly since data is not going to be persisted - result.emit('confirmed', result); - return result; - } - const updatedBlock = Block.pushTransaction( Block.createEmptyBlock(), transaction, @@ -47,6 +32,7 @@ export class DataAccessWrite implements DataAccessTypes.IDataWrite { const storageResult = await this.storage.append(JSON.stringify(updatedBlock)); + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; this.pendingStore?.add(channelId, { transaction, storageResult, topics: topics || [] }); const result: DataAccessTypes.IReturnPersistTransactionRaw = { From 77570b5b466b2e2bc1811901accfa82a0b1e3223 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Fri, 10 Jan 2025 11:21:14 +0700 Subject: [PATCH 09/11] remove http-metamask-data-access-write --- .../request-client.js/src/http-data-access.ts | 2 +- .../src/http-metamask-data-access.ts | 129 +++++++++++--- .../src/http-metamask-data-write.ts | 157 ------------------ 3 files changed, 109 insertions(+), 179 deletions(-) delete mode 100644 packages/request-client.js/src/http-metamask-data-write.ts diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 7694fc6c3e..d45ced354e 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -10,7 +10,7 @@ import { HttpTransaction } from './http-transaction'; * Exposes a Data-Access module over HTTP */ export default class HttpDataAccess extends CombinedDataAccess { - private readonly dataAccessConfig: HttpDataAccessConfig; + protected readonly dataAccessConfig: HttpDataAccessConfig; private readonly transaction: HttpTransaction; /** * Creates an instance of HttpDataAccess. diff --git a/packages/request-client.js/src/http-metamask-data-access.ts b/packages/request-client.js/src/http-metamask-data-access.ts index cf9425a612..f6edce1525 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -1,14 +1,15 @@ -import { CombinedDataAccess } from '@requestnetwork/data-access'; -import { ClientTypes, CurrencyTypes, DataAccessTypes } from '@requestnetwork/types'; +import { Block } from '@requestnetwork/data-access'; +import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; +import { ClientTypes, CurrencyTypes, DataAccessTypes, StorageTypes } from '@requestnetwork/types'; import { ethers } from 'ethers'; +import { EventEmitter } from 'events'; import HttpDataAccess from './http-data-access'; -import { HttpDataAccessConfig, NodeConnectionConfig } from './http-data-access-config'; -import { HttpDataRead } from './http-data-read'; -import { HttpMetaMaskDataWrite } from './http-metamask-data-write'; +import { NodeConnectionConfig } from './http-data-access-config'; + /** * Exposes a Data-Access module over HTTP */ -export default class HttpMetaMaskDataAccess extends CombinedDataAccess { +export default class HttpMetaMaskDataAccess extends HttpDataAccess { /** * Cache block persisted directly (in case the node did not have the time to retrieve it) * (public for easier testing) @@ -22,9 +23,6 @@ export default class HttpMetaMaskDataAccess extends CombinedDataAccess { private provider: ethers.providers.JsonRpcProvider | ethers.providers.Web3Provider; private networkName: CurrencyTypes.EvmChainName = 'private'; - private readonly dataAccessConfig: HttpDataAccessConfig; - private readonly dataAccess: HttpDataAccess; - /** * Creates an instance of HttpDataAccess. * @param httpConfig Http config that will be used by the underlying http-data-access. @see ClientTypes.IHttpDataAccessConfig @@ -45,22 +43,14 @@ export default class HttpMetaMaskDataAccess extends CombinedDataAccess { httpConfig: {}, }, ) { + super({ httpConfig, nodeConnectionConfig }); + ethereumProviderUrl = ethereumProviderUrl ? ethereumProviderUrl : 'http://localhost:8545'; // Creates a local or default provider - const provider = web3 + this.provider = web3 ? new ethers.providers.Web3Provider(web3) : new ethers.providers.JsonRpcProvider({ url: ethereumProviderUrl }); - - const dataAccessConfig = new HttpDataAccessConfig({ httpConfig, nodeConnectionConfig }); - const reader = new HttpDataRead(dataAccessConfig); - const writer = new HttpMetaMaskDataWrite(dataAccessConfig, provider); - - super(reader, writer); - - this.dataAccessConfig = dataAccessConfig; - this.dataAccess = new HttpDataAccess(dataAccessConfig); - this.provider = provider; } /** @@ -84,7 +74,104 @@ export default class HttpMetaMaskDataAccess extends CombinedDataAccess { channelId: string, topics?: string[], ): Promise { - return this.writer.persistTransaction(transactionData, channelId, topics); + if (!this.networkName) { + const network = await this.provider.getNetwork(); + + this.networkName = + network.chainId === 1 ? 'mainnet' : network.chainId === 4 ? 'rinkeby' : 'private'; + } + const submitterContract = requestHashSubmitterArtifact.connect( + this.networkName, + this.provider.getSigner(), + ); + + // We don't use the node to persist the transaction, but we will Do it ourselves + + // create a block and add the transaction in it + const block: DataAccessTypes.IBlock = Block.pushTransaction( + Block.createEmptyBlock(), + transactionData, + channelId, + topics, + ); + + // store the block on ipfs and get the the ipfs hash and size + const { ipfsHash, ipfsSize } = await this.dataAccessConfig.fetch<{ + ipfsHash: string; + ipfsSize: number; + }>('POST', '/ipfsAdd', { data: block }); + + // get the fee required to submit the hash + const fee = await submitterContract.getFeesAmount(ipfsSize); + + // submit the hash to ethereum + const tx = await submitterContract.submitHash( + ipfsHash, + /* eslint-disable no-magic-numbers */ + ethers.utils.hexZeroPad(ethers.utils.hexlify(ipfsSize), 32), + { value: fee }, + ); + + const ethBlock = await this.provider.getBlock(tx.blockNumber ?? -1); + + // create the storage meta from the transaction receipt + const storageMeta: StorageTypes.IEthereumMetadata = { + blockConfirmation: tx.confirmations, + blockNumber: tx.blockNumber ?? -1, + blockTimestamp: ethBlock.timestamp, + fee: fee.toString(), + networkName: this.networkName, + smartContractAddress: tx.to ?? '', + transactionHash: tx.hash, + }; + + // Add the block to the cache + if (!this.cache[channelId]) { + this.cache[channelId] = {}; + } + this.cache[channelId][ipfsHash] = { block, storageMeta }; + + const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; + const result: DataAccessTypes.IReturnPersistTransactionRaw = { + meta: { + storageMeta: { + ethereum: storageMeta, + ipfs: { size: ipfsSize }, + state: StorageTypes.ContentState.PENDING, + timestamp: storageMeta.blockTimestamp, + }, + topics: topics || [], + transactionStorageLocation: ipfsHash, + }, + result: {}, + }; + + // When the ethereum transaction is mined, emit an event 'confirmed' + void tx.wait().then((txConfirmed) => { + // emit the event to tell the request transaction is confirmed + eventEmitter.emit('confirmed', { + meta: { + storageMeta: { + ethereum: { + blockConfirmation: txConfirmed.confirmations, + blockNumber: txConfirmed.blockNumber, + blockTimestamp: ethBlock.timestamp, + fee: fee.toString(), + networkName: this.networkName, + smartContractAddress: txConfirmed.to, + transactionHash: txConfirmed.transactionHash, + }, + state: StorageTypes.ContentState.CONFIRMED, + timestamp: ethBlock.timestamp, + }, + topics: topics || [], + transactionStorageLocation: ipfsHash, + }, + result: {}, + }); + }); + + return Object.assign(eventEmitter, result); } /** diff --git a/packages/request-client.js/src/http-metamask-data-write.ts b/packages/request-client.js/src/http-metamask-data-write.ts deleted file mode 100644 index 59acd48cb1..0000000000 --- a/packages/request-client.js/src/http-metamask-data-write.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { DataAccessTypes, StorageTypes } from '@requestnetwork/types'; -import { HttpDataAccessConfig } from './http-data-access-config'; -import { CurrencyTypes } from '@requestnetwork/types'; -import { ethers } from 'ethers'; -import { EventEmitter } from 'events'; -import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; -import { Block } from '@requestnetwork/data-access'; - -export class HttpMetaMaskDataWrite implements DataAccessTypes.IDataWrite { - /** - * Cache block persisted directly (in case the node did not have the time to retrieve it) - * (public for easier testing) - */ - public cache: { - [channelId: string]: { - [ipfsHash: string]: { block: DataAccessTypes.IBlock; storageMeta: any } | null; - }; - } = {}; - - constructor( - private readonly dataAccessConfig: HttpDataAccessConfig, - private provider: ethers.providers.JsonRpcProvider | ethers.providers.Web3Provider, - private networkName: CurrencyTypes.EvmChainName = 'private', - ) {} - - /** - * Initialize the module. Does nothing, exists only to implement IDataAccess - * - * @returns nothing - */ - public async initialize(): Promise { - // no-op, nothing to do - return; - } - - /** - * Closes the module. Does nothing, exists only to implement IDataAccess - * - * @returns nothing - */ - public async close(): Promise { - // no-op, nothing to do - return; - } - - /** - * Persists a new transaction using the node only for IPFS but persisting on ethereum through local provider - * - * @param transactionData The transaction data - * @param topics The topics used to index the transaction - */ - public async persistTransaction( - transactionData: DataAccessTypes.ITransaction, - channelId: string, - topics?: string[], - ): Promise { - const eventEmitter = new EventEmitter() as DataAccessTypes.PersistTransactionEmitter; - - if (!this.networkName) { - const network = await this.provider.getNetwork(); - - this.networkName = - network.chainId === 1 ? 'mainnet' : network.chainId === 4 ? 'rinkeby' : 'private'; - } - const submitterContract = requestHashSubmitterArtifact.connect( - this.networkName, - this.provider.getSigner(), - ); - - // We don't use the node to persist the transaction, but we will Do it ourselves - - // create a block and add the transaction in it - const block: DataAccessTypes.IBlock = Block.pushTransaction( - Block.createEmptyBlock(), - transactionData, - channelId, - topics, - ); - - // store the block on ipfs and get the the ipfs hash and size - const { ipfsHash, ipfsSize } = await this.dataAccessConfig.fetch<{ - ipfsHash: string; - ipfsSize: number; - }>('POST', '/ipfsAdd', { data: block }); - - // get the fee required to submit the hash - const fee = await submitterContract.getFeesAmount(ipfsSize); - - // submit the hash to ethereum - const tx = await submitterContract.submitHash( - ipfsHash, - /* eslint-disable no-magic-numbers */ - ethers.utils.hexZeroPad(ethers.utils.hexlify(ipfsSize), 32), - { value: fee }, - ); - - const ethBlock = await this.provider.getBlock(tx.blockNumber ?? -1); - - // create the storage meta from the transaction receipt - const storageMeta: StorageTypes.IEthereumMetadata = { - blockConfirmation: tx.confirmations, - blockNumber: tx.blockNumber ?? -1, - blockTimestamp: ethBlock.timestamp, - fee: fee.toString(), - networkName: this.networkName, - smartContractAddress: tx.to ?? '', - transactionHash: tx.hash, - }; - - // Add the block to the cache - if (!this.cache[channelId]) { - this.cache[channelId] = {}; - } - this.cache[channelId][ipfsHash] = { block, storageMeta }; - - const result: DataAccessTypes.IReturnPersistTransactionRaw = { - meta: { - storageMeta: { - ethereum: storageMeta, - ipfs: { size: ipfsSize }, - state: StorageTypes.ContentState.PENDING, - timestamp: storageMeta.blockTimestamp, - }, - topics: topics || [], - transactionStorageLocation: ipfsHash, - }, - result: {}, - }; - - // When the ethereum transaction is mined, emit an event 'confirmed' - void tx.wait().then((txConfirmed) => { - // emit the event to tell the request transaction is confirmed - eventEmitter.emit('confirmed', { - meta: { - storageMeta: { - ethereum: { - blockConfirmation: txConfirmed.confirmations, - blockNumber: txConfirmed.blockNumber, - blockTimestamp: ethBlock.timestamp, - fee: fee.toString(), - networkName: this.networkName, - smartContractAddress: txConfirmed.to, - transactionHash: txConfirmed.transactionHash, - }, - state: StorageTypes.ContentState.CONFIRMED, - timestamp: ethBlock.timestamp, - }, - topics: topics || [], - transactionStorageLocation: ipfsHash, - }, - result: {}, - }); - }); - - return Object.assign(eventEmitter, result); - } -} From f68fbff2d1de87bcf2e6fbb39621f3cb358ec2c5 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Fri, 10 Jan 2025 12:06:33 +0700 Subject: [PATCH 10/11] fix --- packages/request-client.js/src/http-data-access-config.ts | 1 - packages/request-client.js/src/http-data-access.ts | 7 ++++--- .../request-client.js/test/api/request-network.test.ts | 1 + packages/request-client.js/test/in-memory-request.test.ts | 2 -- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/request-client.js/src/http-data-access-config.ts b/packages/request-client.js/src/http-data-access-config.ts index 86c5b5d00a..eb290bc229 100644 --- a/packages/request-client.js/src/http-data-access-config.ts +++ b/packages/request-client.js/src/http-data-access-config.ts @@ -26,7 +26,6 @@ export class HttpDataAccessConfig { }: { httpConfig?: Partial; nodeConnectionConfig?: Partial; - persist?: boolean; } = { httpConfig: {}, nodeConnectionConfig: {}, diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index d45ced354e..30e9af7ad9 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -35,9 +35,10 @@ export default class HttpDataAccess extends CombinedDataAccess { const dataAccessConfig = new HttpDataAccessConfig({ httpConfig, nodeConnectionConfig }); const transaction = new HttpTransaction(dataAccessConfig); const reader = new HttpDataRead(dataAccessConfig); - const writer = persist - ? new HttpDataWrite(dataAccessConfig, transaction) - : new NoPersistDataWrite(); + const writer = + persist === false + ? new NoPersistDataWrite() + : new HttpDataWrite(dataAccessConfig, transaction); super(reader, writer); diff --git a/packages/request-client.js/test/api/request-network.test.ts b/packages/request-client.js/test/api/request-network.test.ts index ae914d051f..1bccf46050 100644 --- a/packages/request-client.js/test/api/request-network.test.ts +++ b/packages/request-client.js/test/api/request-network.test.ts @@ -16,6 +16,7 @@ const mockDataAccess: DataAccessTypes.IDataAccess = { close: jest.fn(), persistTransaction: jest.fn(), getChannelsByMultipleTopics: jest.fn(), + isPersisting: jest.fn().mockReturnValue(true), }; describe('api/request-network', () => { diff --git a/packages/request-client.js/test/in-memory-request.test.ts b/packages/request-client.js/test/in-memory-request.test.ts index 973274d35b..1e5a646b2f 100644 --- a/packages/request-client.js/test/in-memory-request.test.ts +++ b/packages/request-client.js/test/in-memory-request.test.ts @@ -20,8 +20,6 @@ class MyCustomDataAccess extends CombinedDataAccess { ); } } -{ -} describe('handle in-memory request', () => { let requestNetwork: RequestNetwork; From 7f363728d4f0e4049cc799c11f4e683f4d9f2c09 Mon Sep 17 00:00:00 2001 From: Kevin Dave Date: Fri, 10 Jan 2025 21:34:51 +0700 Subject: [PATCH 11/11] change naming --- .../data-access/src/combined-data-access.ts | 4 +- .../src/api/request-network.ts | 6 +-- .../request-client.js/src/http-data-access.ts | 13 +++---- .../src/http-request-network.ts | 2 +- .../test/api/request-network.test.ts | 2 +- .../transaction-manager/test/index.test.ts | 38 +++++++++---------- packages/types/src/data-access-types.ts | 2 +- 7 files changed, 33 insertions(+), 34 deletions(-) diff --git a/packages/data-access/src/combined-data-access.ts b/packages/data-access/src/combined-data-access.ts index 22c63f726e..304b9bb1c5 100644 --- a/packages/data-access/src/combined-data-access.ts +++ b/packages/data-access/src/combined-data-access.ts @@ -17,8 +17,8 @@ export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess await this.reader.close(); } - isPersisting(): boolean { - return !(this.writer instanceof NoPersistDataWrite); + skipPersistence(): boolean { + return this.writer instanceof NoPersistDataWrite; } async getTransactionsByChannelId( diff --git a/packages/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index 0ae85d6bb9..3bcbeb8eac 100644 --- a/packages/request-client.js/src/api/request-network.ts +++ b/packages/request-client.js/src/api/request-network.ts @@ -114,7 +114,7 @@ export default class RequestNetwork { const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData; const requestId = requestLogicCreateResult.result.requestId; - const isSkippingPersistence = !this.dataAccess.isPersisting(); + const isSkippingPersistence = this.dataAccess.skipPersistence(); // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { contentDataExtension: this.contentData, @@ -157,7 +157,7 @@ export default class RequestNetwork { throw new Error('Cannot persist request without inMemoryInfo.'); } - if (!this.dataAccess.isPersisting()) { + if (this.dataAccess.skipPersistence()) { throw new Error( 'Cannot persist request when skipPersistence is enabled. To persist the request, create a new instance of RequestNetwork without skipPersistence being set to true.', ); @@ -197,7 +197,7 @@ export default class RequestNetwork { const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData; const requestId = requestLogicCreateResult.result.requestId; - const isSkippingPersistence = !this.dataAccess.isPersisting(); + const isSkippingPersistence = this.dataAccess.skipPersistence(); // create the request object const request = new Request(requestId, this.requestLogic, this.currencyManager, { diff --git a/packages/request-client.js/src/http-data-access.ts b/packages/request-client.js/src/http-data-access.ts index 30e9af7ad9..e8a8782013 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -21,24 +21,23 @@ export default class HttpDataAccess extends CombinedDataAccess { { httpConfig, nodeConnectionConfig, - persist, + skipPersistence, }: { httpConfig?: Partial; nodeConnectionConfig?: Partial; - persist?: boolean; + skipPersistence?: boolean; } = { httpConfig: {}, nodeConnectionConfig: {}, - persist: true, + skipPersistence: true, }, ) { const dataAccessConfig = new HttpDataAccessConfig({ httpConfig, nodeConnectionConfig }); const transaction = new HttpTransaction(dataAccessConfig); const reader = new HttpDataRead(dataAccessConfig); - const writer = - persist === false - ? new NoPersistDataWrite() - : new HttpDataWrite(dataAccessConfig, transaction); + const writer = skipPersistence + ? new NoPersistDataWrite() + : new HttpDataWrite(dataAccessConfig, transaction); super(reader, writer); diff --git a/packages/request-client.js/src/http-request-network.ts b/packages/request-client.js/src/http-request-network.ts index d281dc1f2f..92e7d139c2 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -57,7 +57,7 @@ export default class HttpRequestNetwork extends RequestNetwork { ) { const dataAccess: DataAccessTypes.IDataAccess = useMockStorage ? new MockDataAccess(new MockStorage()) - : new HttpDataAccess({ httpConfig, nodeConnectionConfig, persist: !skipPersistence }); + : new HttpDataAccess({ httpConfig, nodeConnectionConfig, skipPersistence }); if (!currencyManager) { currencyManager = CurrencyManager.getDefault(); diff --git a/packages/request-client.js/test/api/request-network.test.ts b/packages/request-client.js/test/api/request-network.test.ts index 1bccf46050..fe1bc1abda 100644 --- a/packages/request-client.js/test/api/request-network.test.ts +++ b/packages/request-client.js/test/api/request-network.test.ts @@ -16,7 +16,7 @@ const mockDataAccess: DataAccessTypes.IDataAccess = { close: jest.fn(), persistTransaction: jest.fn(), getChannelsByMultipleTopics: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; describe('api/request-network', () => { diff --git a/packages/transaction-manager/test/index.test.ts b/packages/transaction-manager/test/index.test.ts index 93e7426d2b..da8456affc 100644 --- a/packages/transaction-manager/test/index.test.ts +++ b/packages/transaction-manager/test/index.test.ts @@ -74,7 +74,7 @@ describe('index', () => { ); return fakeMetaDataAccessPersistReturn; }), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; }); @@ -228,7 +228,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -275,7 +275,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -318,7 +318,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -395,7 +395,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -446,7 +446,7 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnFirstHashWrong), initialize: jest.fn(), close: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), persistTransaction: jest.fn(), }; @@ -506,7 +506,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -564,7 +564,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -638,7 +638,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -726,7 +726,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -803,7 +803,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -882,7 +882,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -960,7 +960,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1109,7 +1109,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1184,7 +1184,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1253,7 +1253,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1333,7 +1333,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1403,7 +1403,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1483,7 +1483,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), - isPersisting: jest.fn().mockReturnValue(true), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( diff --git a/packages/types/src/data-access-types.ts b/packages/types/src/data-access-types.ts index 47067590a1..0974ed5dd8 100644 --- a/packages/types/src/data-access-types.ts +++ b/packages/types/src/data-access-types.ts @@ -36,7 +36,7 @@ export interface IDataWrite { } export interface IDataAccess extends IDataRead, IDataWrite { - isPersisting(): boolean; + skipPersistence(): boolean; _getStatus?(): Promise; getLitCapacityDelegationAuthSig?: (delegateeAddress: string) => Promise; }