Skip to content

Commit

Permalink
feat: Create new data access to allow for in-memory transactions. (#1386
Browse files Browse the repository at this point in the history
)

Co-authored-by: MantisClone <david.huntmateo@request.network>
aimensahnoun and MantisClone authored Jul 17, 2024
1 parent f12ca2e commit 0320158
Showing 9 changed files with 505 additions and 148 deletions.
174 changes: 148 additions & 26 deletions packages/request-client.js/src/api/request-network.ts
Original file line number Diff line number Diff line change
@@ -4,10 +4,12 @@ import { RequestLogic } from '@requestnetwork/request-logic';
import { TransactionManager } from '@requestnetwork/transaction-manager';
import {
AdvancedLogicTypes,
ClientTypes,
CurrencyTypes,
DataAccessTypes,
DecryptionProviderTypes,
EncryptionTypes,
ExtensionTypes,
IdentityTypes,
PaymentTypes,
RequestLogicTypes,
@@ -20,6 +22,7 @@ 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.
@@ -31,6 +34,7 @@ export default class RequestNetwork {
private requestLogic: RequestLogicTypes.IRequestLogic;
private transaction: TransactionTypes.ITransactionManager;
private advancedLogic: AdvancedLogicTypes.IAdvancedLogic;
private dataAccess: DataAccessTypes.IDataAccess;

private contentData: ContentDataExtension;
private currencyManager: CurrencyTypes.ICurrencyManager;
@@ -55,6 +59,7 @@ export default class RequestNetwork {
paymentOptions?: Partial<PaymentNetworkOptions>;
}) {
this.currencyManager = currencyManager || CurrencyManager.getDefault();
this.dataAccess = dataAccess;
this.advancedLogic = new AdvancedLogic(this.currencyManager);
this.transaction = new TransactionManager(dataAccess, decryptionProvider);
this.requestLogic = new RequestLogic(this.transaction, signatureProvider, this.advancedLogic);
@@ -85,28 +90,66 @@ export default class RequestNetwork {
topics,
);

const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
const requestId = requestLogicCreateResult.result.requestId;
const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess;
// create the request object
const request = new Request(
requestLogicCreateResult.result.requestId,
this.requestLogic,
this.currencyManager,
{
contentDataExtension: this.contentData,
paymentNetwork,
requestLogicCreateResult,
skipPaymentDetection: parameters.disablePaymentDetection,
disableEvents: parameters.disableEvents,
},
);
const request = new Request(requestId, this.requestLogic, this.currencyManager, {
contentDataExtension: this.contentData,
paymentNetwork,
requestLogicCreateResult,
skipPaymentDetection: parameters.disablePaymentDetection,
disableEvents: parameters.disableEvents,
// inMemoryInfo is only used when skipPersistence is enabled
inMemoryInfo: isSkippingPersistence
? {
topics: requestLogicCreateResult.meta.transactionManagerMeta?.topics,
transactionData: transactionData,
requestData: this.prepareRequestDataForPayment(transactionData, requestId),
}
: null,
});

if (!options?.skipRefresh) {
if (!options?.skipRefresh && !isSkippingPersistence) {
// refresh the local request data
await request.refresh();
}

return request;
}

/**
* Persists an in-memory request to the data-access layer.
*
* This method is used to persist requests that were initially created with skipPersistence enabled.
*
* @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).
*/
public async persistRequest(
request: Request,
): Promise<DataAccessTypes.IReturnPersistTransaction> {
if (!request.inMemoryInfo) {
throw new Error('Cannot persist request without inMemoryInfo.');
}

if (this.dataAccess instanceof NoPersistHttpDataAccess) {
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.',
);
}
const result: DataAccessTypes.IReturnPersistTransaction =
await this.dataAccess.persistTransaction(
request.inMemoryInfo.transactionData,
request.requestId,
request.inMemoryInfo.topics,
);

return result;
}

/**
* Creates an encrypted request.
*
@@ -129,21 +172,27 @@ export default class RequestNetwork {
topics,
);

const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
const requestId = requestLogicCreateResult.result.requestId;
const isSkippingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess;

// create the request object
const request = new Request(
requestLogicCreateResult.result.requestId,
this.requestLogic,
this.currencyManager,
{
contentDataExtension: this.contentData,
paymentNetwork,
requestLogicCreateResult,
skipPaymentDetection: parameters.disablePaymentDetection,
disableEvents: parameters.disableEvents,
},
);
const request = new Request(requestId, this.requestLogic, this.currencyManager, {
contentDataExtension: this.contentData,
paymentNetwork,
requestLogicCreateResult,
skipPaymentDetection: parameters.disablePaymentDetection,
disableEvents: parameters.disableEvents,
inMemoryInfo: isSkippingPersistence
? {
topics: requestLogicCreateResult.meta.transactionManagerMeta?.topics,
transactionData: transactionData,
requestData: this.prepareRequestDataForPayment(transactionData, requestId),
}
: null,
});

if (!options?.skipRefresh) {
if (!options?.skipRefresh && !isSkippingPersistence) {
// refresh the local request data
await request.refresh();
}
@@ -437,4 +486,77 @@ export default class RequestNetwork {

return { requestParameters: copiedRequestParameters, topics, paymentNetwork };
}

/**
* Prepares a payment request structure from transaction data.
*
* This method is used to create a request structure similar to a persisted request,
* allowing users to pay before the request is persisted. This is useful in scenarios
* where a request is created, paid, and then persisted, as opposed to the normal flow
* of creating, persisting, and then paying the request.
*
* @param transactionData The transaction data containing the request information
* @param requestId The ID of the request
* @returns The prepared payment request structure or undefined if transaction data is missing
*/
private prepareRequestDataForPayment(
transactionData: DataAccessTypes.ITransaction,
requestId: string,
): ClientTypes.IRequestData {
const requestData = JSON.parse(transactionData.data as string).data;
const originalExtensionsData = requestData.parameters.extensionsData;
const newExtensions: RequestLogicTypes.IExtensionStates = {};

for (const extension of originalExtensionsData) {
if (extension.id !== ExtensionTypes.OTHER_ID.CONTENT_DATA) {
newExtensions[extension.id] = {
events: [
{
name: extension.action,
parameters: {
paymentAddress: extension.parameters.paymentAddress,
salt: extension.parameters.salt,
},
timestamp: requestData.parameters.timestamp,
},
],
id: extension.id,
type: ExtensionTypes.TYPE.PAYMENT_NETWORK,
values: {
salt: extension.parameters.salt,
receivedPaymentAmount: '0',
receivedRefundAmount: '0',
sentPaymentAmount: '0',
sentRefundAmount: '0',
paymentAddress: extension.parameters.paymentAddress,
},
version: extension.version,
};
}
}

return {
requestId: requestId,
currency: requestData.parameters.currency.type,
meta: null,
balance: null,
expectedAmount: requestData.parameters.expectedAmount,
contentData: requestData.parameters.extensionsData.find(
(ext: ExtensionTypes.IAction) => ext.id === ExtensionTypes.OTHER_ID.CONTENT_DATA,
)?.parameters.content,
currencyInfo: {
type: requestData.parameters.currency.type,
network: requestData.parameters.currency.network,
value: requestData.parameters.currency.value || '',
},
pending: null,
extensions: newExtensions,
extensionsData: requestData.parameters.extensionsData,
timestamp: requestData.parameters.timestamp,
version: requestData.parameters.version,
creator: requestData.parameters.creator,
state: requestData.parameters.state,
events: requestData.parameters.events,
};
}
}
13 changes: 13 additions & 0 deletions packages/request-client.js/src/api/request.ts
Original file line number Diff line number Diff line change
@@ -78,6 +78,17 @@ export default class Request {
*/
private currencyManager: CurrencyTypes.ICurrencyManager;

/**
* Information for an in-memory request, including transaction data, topics, and payment request data.
* This is used for requests that haven't been persisted yet, allowing for operations like payments
* before the request is stored in the data access layer.
*
* @property transactionData - Transaction data necessary for persisting the request later on.
* @property topics - Topics of the request, used for indexing and retrieval when persisting.
* @property requestData - Structured data primarily used for processing payments before the request is persisted.
*/
public readonly inMemoryInfo: RequestLogicTypes.IInMemoryInfo | null = null;

/**
* Creates an instance of Request
*
@@ -98,6 +109,7 @@ export default class Request {
requestLogicCreateResult?: RequestLogicTypes.IReturnCreateRequest;
skipPaymentDetection?: boolean;
disableEvents?: boolean;
inMemoryInfo?: RequestLogicTypes.IInMemoryInfo | null;
},
) {
this.requestLogic = requestLogic;
@@ -108,6 +120,7 @@ export default class Request {
this.skipPaymentDetection = options?.skipPaymentDetection || false;
this.disableEvents = options?.disableEvents || false;
this.currencyManager = currencyManager;
this.inMemoryInfo = options?.inMemoryInfo || null;

if (options?.requestLogicCreateResult && !this.disableEvents) {
const originalEmitter = options.requestLogicCreateResult;
16 changes: 13 additions & 3 deletions packages/request-client.js/src/http-request-network.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ 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.
@@ -21,10 +22,11 @@ export default class HttpRequestNetwork extends RequestNetwork {
*
* @param options.httpConfig Http config that will be used by the underlying data-access. @see ClientTypes.IHttpDataAccessConfig for available options.
* @param options.nodeConnectionConfig Configuration options to connect to the node.
* @param options.useMockStorage When true, will use a mock storage in memory. Meant to simplify local development and should never be used in production.
* @param options.useMockStorage When true, will use a mock storage in memory. Meant to simplify local development and should never be used in production. Overrides `skipPersistence` when both are true.
* @param options.signatureProvider Module to handle the signature. If not given it will be impossible to create new transaction (it requires to sign).
* @param options.currencies custom currency list
* @param options.currencyManager custom currency manager (will override `currencies`)
* @param options.currencies custom currency list.
* @param options.currencyManager custom currency manager (will override `currencies`).
* @param options.skipPersistence allows creating a transaction without immediate persistence.
*/
constructor(
{
@@ -35,6 +37,7 @@ export default class HttpRequestNetwork extends RequestNetwork {
useMockStorage,
currencyManager,
paymentOptions,
skipPersistence,
}: {
decryptionProvider?: DecryptionProviderTypes.IDecryptionProvider;
httpConfig?: Partial<ClientTypes.IHttpDataAccessConfig>;
@@ -43,13 +46,20 @@ export default class HttpRequestNetwork extends RequestNetwork {
useMockStorage?: boolean;
currencyManager?: CurrencyTypes.ICurrencyManager;
paymentOptions?: Partial<PaymentNetworkOptions>;
skipPersistence?: boolean;
} = {
httpConfig: {},
useMockStorage: false,
skipPersistence: false,
},
) {
const dataAccess: DataAccessTypes.IDataAccess = useMockStorage
? new MockDataAccess(new MockStorage())
: skipPersistence
? new NoPersistHttpDataAccess({
httpConfig,
nodeConnectionConfig,
})
: new HttpDataAccess({ httpConfig, nodeConnectionConfig });

if (!currencyManager) {
48 changes: 48 additions & 0 deletions packages/request-client.js/src/no-persist-http-data-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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<ClientTypes.IHttpDataAccessConfig>;
nodeConnectionConfig?: Partial<NodeConnectionConfig>;
} = {
httpConfig: {},
nodeConnectionConfig: {},
},
) {
super({ httpConfig, nodeConnectionConfig });
}

async persistTransaction(
transactionData: DataAccessTypes.ITransaction,
channelId: string,
topics?: string[],
): Promise<DataAccessTypes.IReturnPersistTransaction> {
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;
}
}
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ afterAll(() => {
});

afterEach(() => {
mockServer.restoreHandlers();
mockServer.resetHandlers();
});

describe('HttpRequestNetwork', () => {
Loading

0 comments on commit 0320158

Please sign in to comment.