diff --git a/packages/data-access/src/combined-data-access.ts b/packages/data-access/src/combined-data-access.ts index c179f9ee9..304b9bb1c 100644 --- a/packages/data-access/src/combined-data-access.ts +++ b/packages/data-access/src/combined-data-access.ts @@ -1,4 +1,5 @@ import { DataAccessTypes } from '@requestnetwork/types'; +import { NoPersistDataWrite } from './no-persist-data-write'; export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess { constructor( @@ -16,6 +17,10 @@ export abstract class CombinedDataAccess implements DataAccessTypes.IDataAccess await this.reader.close(); } + skipPersistence(): 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 f4fc148b2..ed9c511aa 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 { NoPersistDataWrite } from './no-persist-data-write'; 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 000000000..979bac504 --- /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/request-client.js/src/api/request-network.ts b/packages/request-client.js/src/api/request-network.ts index d91f40b60..3bcbeb8ea 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.skipPersistence(); // 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.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.', ); @@ -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.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-config.ts b/packages/request-client.js/src/http-data-access-config.ts new file mode 100644 index 000000000..eb290bc22 --- /dev/null +++ b/packages/request-client.js/src/http-data-access-config.ts @@ -0,0 +1,111 @@ +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; + } = { + 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 99b5abf5c..e8a878201 100644 --- a/packages/request-client.js/src/http-data-access.ts +++ b/packages/request-client.js/src/http-data-access.ts @@ -1,30 +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'; - -// 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 { - /** - * 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 { + protected readonly dataAccessConfig: HttpDataAccessConfig; + private readonly transaction: HttpTransaction; /** * Creates an instance of HttpDataAccess. * @param httpConfig @see ClientTypes.IHttpDataAccessConfig for available options. @@ -34,29 +21,29 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { { httpConfig, nodeConnectionConfig, + skipPersistence, }: { httpConfig?: Partial; nodeConnectionConfig?: Partial; + skipPersistence?: boolean; } = { httpConfig: {}, nodeConnectionConfig: {}, + skipPersistence: true, }, ) { - // Get Request Client version to set it in the header - const requestClientVersion = packageJson.version; - 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 = skipPersistence + ? new NoPersistDataWrite() + : new HttpDataWrite(dataAccessConfig, transaction); + + super(reader, writer); + this.dataAccessConfig = dataAccessConfig; + this.transaction = transaction; + } /** * Initialize the module. Does nothing, exists only to implement IDataAccess * @@ -88,40 +75,7 @@ export default class HttpDataAccess implements DataAccessTypes.IDataAccess { channelId: string, topics?: string[], ): Promise { - // 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( - new EventEmitter() as DataAccessTypes.PersistTransactionEmitter, - 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); } /** @@ -131,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); } /** @@ -157,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); } /** @@ -178,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); } /** @@ -202,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); } /** @@ -217,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', {}); } /** @@ -234,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 000000000..ea27807e6 --- /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 000000000..57047be15 --- /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 f3c7ab277..f6edce152 100644 --- a/packages/request-client.js/src/http-metamask-data-access.ts +++ b/packages/request-client.js/src/http-metamask-data-access.ts @@ -3,7 +3,8 @@ import { requestHashSubmitterArtifact } from '@requestnetwork/smart-contracts'; import { ClientTypes, CurrencyTypes, DataAccessTypes, StorageTypes } 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 { NodeConnectionConfig } from './http-data-access-config'; /** * Exposes a Data-Access module over HTTP @@ -95,11 +96,10 @@ export default class HttpMetaMaskDataAccess extends HttpDataAccess { ); // 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 }, - ); + 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); @@ -184,14 +184,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-request-network.ts b/packages/request-client.js/src/http-request-network.ts index 78346f54a..92e7d139c 100644 --- a/packages/request-client.js/src/http-request-network.ts +++ b/packages/request-client.js/src/http-request-network.ts @@ -9,11 +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 { NoPersistHttpDataAccess } from './no-persist-http-data-access'; - +import { NodeConnectionConfig } from './http-data-access-config'; /** * Exposes RequestNetwork module configured to use http-data-access. */ @@ -58,12 +57,7 @@ export default class HttpRequestNetwork extends RequestNetwork { ) { const dataAccess: DataAccessTypes.IDataAccess = useMockStorage ? new MockDataAccess(new MockStorage()) - : skipPersistence - ? new NoPersistHttpDataAccess({ - httpConfig, - nodeConnectionConfig, - }) - : new HttpDataAccess({ httpConfig, nodeConnectionConfig }); + : new HttpDataAccess({ httpConfig, nodeConnectionConfig, skipPersistence }); if (!currencyManager) { currencyManager = CurrencyManager.getDefault(); 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 000000000..e153b83f6 --- /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 a5fbf6e35..dced3bc19 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/src/no-persist-http-data-access.ts b/packages/request-client.js/src/no-persist-http-data-access.ts deleted file mode 100644 index d2b3a4769..000000000 --- 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 }); - } - - 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; - } -} 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 ae914d051..fe1bc1abd 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(), + skipPersistence: 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 e2cf5164f..1e5a646b2 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,25 @@ -import { RequestNetwork } from '../src/index'; +import { RequestNetwork, RequestNetworkBase } from '../src/index'; import * as TestData from './data-test'; import { http, HttpResponse } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; import config from '../src/http-config-defaults'; +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; @@ -60,6 +76,25 @@ describe('handle in-memory request', () => { expect(spyPersistTransaction).not.toHaveBeenCalled(); }); + it('creates a request without persisting it with custom data access', async () => { + const myCustomDataAccess = new MyCustomDataAccess(); + + requestNetwork = new RequestNetworkBase({ + signatureProvider: TestData.fakeSignatureProvider, + dataAccess: myCustomDataAccess, + }); + + 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/transaction-manager/test/index.test.ts b/packages/transaction-manager/test/index.test.ts index 60106f03a..da8456aff 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; }), + skipPersistence: jest.fn().mockReturnValue(true), }; }); @@ -227,6 +228,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn().mockReturnValue(fakeMetaDataAccessPersistReturn), + skipPersistence: 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), + skipPersistence: 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), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -391,6 +395,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -441,6 +446,7 @@ describe('index', () => { .mockReturnValue(fakeMetaDataAccessGetReturnFirstHashWrong), initialize: jest.fn(), close: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), persistTransaction: jest.fn(), }; @@ -500,6 +506,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -557,6 +564,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -630,6 +638,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -717,6 +726,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -793,6 +803,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -871,6 +882,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -948,6 +960,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1096,6 +1109,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1170,6 +1184,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager( @@ -1238,6 +1253,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1317,6 +1333,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1386,6 +1403,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + skipPersistence: jest.fn().mockReturnValue(true), }; const transactionManager = new TransactionManager(fakeDataAccess); @@ -1465,6 +1483,7 @@ describe('index', () => { initialize: jest.fn(), close: jest.fn(), persistTransaction: jest.fn(), + 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 6b0bc688a..0974ed5dd 100644 --- a/packages/types/src/data-access-types.ts +++ b/packages/types/src/data-access-types.ts @@ -36,6 +36,7 @@ export interface IDataWrite { } export interface IDataAccess extends IDataRead, IDataWrite { + skipPersistence(): boolean; _getStatus?(): Promise; getLitCapacityDelegationAuthSig?: (delegateeAddress: string) => Promise; }