Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create new data access to allow for in-memory transactions. #1386

Merged
merged 47 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
776bb7d
feat: create pay before persist request flow
aimensahnoun May 6, 2024
d53c930
Merge branch 'master' of github.com:RequestNetwork/requestNetwork int…
aimensahnoun Jun 5, 2024
fc5e671
feat: add preparePaymentRequest
aimensahnoun Jun 5, 2024
4c41cdf
feat: add requestPayment to `createRequest` return
aimensahnoun Jun 5, 2024
e9d51c7
docs: document `persistRequest` method
aimensahnoun Jun 6, 2024
c2ec1c2
test: add creation and persistance tests for in-memory requests
aimensahnoun Jun 6, 2024
399217c
test: update transactiom-manager tests to check only relavant parts o…
aimensahnoun Jun 6, 2024
12631ac
test: updating request-client tests to check object containing simila…
aimensahnoun Jun 6, 2024
dee30bd
Merge branch 'master' into 1380-pay-before-persist
MantisClone Jul 9, 2024
a32f05c
Merge branch 'master' of github.com:RequestNetwork/requestNetwork int…
aimensahnoun Jul 12, 2024
0964179
Merge branch '1380-pay-before-persist' of github.com:RequestNetwork/r…
aimensahnoun Jul 12, 2024
6dffdfd
refactor: merge in memory variables into one object `inMemoryInfo`
aimensahnoun Jul 15, 2024
d3d9368
test: move `spyPersistTransaction` back to beforeEach
aimensahnoun Jul 16, 2024
a56c1aa
Merge branch 'master' of github.com:RequestNetwork/requestNetwork int…
aimensahnoun Jul 16, 2024
af2afa8
test: add logs to failing test
aimensahnoun Jul 16, 2024
186b163
test: add more logs to test
aimensahnoun Jul 16, 2024
8e91696
test: revert skip persistence
aimensahnoun Jul 16, 2024
613e6c3
test: add persistence back
aimensahnoun Jul 16, 2024
10ed848
test: add more logs to failing test
aimensahnoun Jul 16, 2024
638d730
test: increase failAtCall
aimensahnoun Jul 16, 2024
d4b611f
test: increase timeout
aimensahnoun Jul 16, 2024
0900c53
test: uncomment workflows
aimensahnoun Jul 16, 2024
91f0cc6
test: lower fail at call
aimensahnoun Jul 16, 2024
3dc465c
Revert to fail at call 6
MantisClone Jul 16, 2024
6df24aa
Revert debug log messages and extended timeout
MantisClone Jul 16, 2024
7ec1197
Throw error on unhandled request
MantisClone Jul 16, 2024
6b3513b
Add debug logging related to msw server
MantisClone Jul 16, 2024
8027e49
Print as strings
MantisClone Jul 16, 2024
0f0fe60
Fix debug logs
MantisClone Jul 16, 2024
6172c07
Try resetting handlers after each test
MantisClone Jul 16, 2024
ffc7559
Remove debug logs. Call `resetHandlers()` afterEach test
MantisClone Jul 16, 2024
e45294e
feat: skip refreshing if `skipePersistence` is active
aimensahnoun Jul 17, 2024
915b0a1
feat: emit confirmation on no-persist-http-data-access
aimensahnoun Jul 17, 2024
cf5884d
test: fix `in-memory request` tests
aimensahnoun Jul 17, 2024
8f50530
test: revert test changes
aimensahnoun Jul 17, 2024
e2dac67
fix: fix test
aimensahnoun Jul 17, 2024
55ec0b4
test: move in-memory reuqests to their own file
aimensahnoun Jul 17, 2024
8027739
test: move spyPersistTransatction to before all
aimensahnoun Jul 17, 2024
c3b08ad
refactor: update error message to be more clear
aimensahnoun Jul 17, 2024
11b3121
clarify error message
MantisClone Jul 17, 2024
ab44ab4
fix: skipPersistence doc
MantisClone Jul 17, 2024
7b71180
test: update test with updated error message
aimensahnoun Jul 17, 2024
a5d9668
fix: fix typo
aimensahnoun Jul 17, 2024
422e9e0
refactor: move `preparePaymentRequest` to the bottom of file
aimensahnoun Jul 17, 2024
3ccf360
refactor: rename `preparePaymentRequest` and `paymentRequest`
aimensahnoun Jul 17, 2024
157530f
chore: remove await from mockServer.close
aimensahnoun Jul 17, 2024
487dffe
chore: add warning to useMockStorage about overriding skipPersistence
aimensahnoun Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -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,
Expand All @@ -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';
MantisClone marked this conversation as resolved.
Show resolved Hide resolved

/**
* Entry point of the request-client.js library. Create requests, get requests, manipulate requests.
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -66,6 +71,79 @@ export default class RequestNetwork {
);
}

/**
* 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 preparePaymentRequest(
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,
};
}
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
MantisClone marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a request.
*
Expand All @@ -85,28 +163,66 @@ export default class RequestNetwork {
topics,
);

const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
const requestId = requestLogicCreateResult.result.requestId;
const isSkipingPersistence = this.dataAccess instanceof NoPersistHttpDataAccess;
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
// 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: isSkipingPersistence
? {
topics: requestLogicCreateResult.meta.transactionManagerMeta?.topics,
transactionData: transactionData,
paymentRequest: this.preparePaymentRequest(transactionData, requestId),
}
: null,
});
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
MantisClone marked this conversation as resolved.
Show resolved Hide resolved

if (!options?.skipRefresh) {
if (!options?.skipRefresh && !isSkipingPersistence) {
// 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(
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
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;
}
MantisClone marked this conversation as resolved.
Show resolved Hide resolved

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

const transactionData = requestLogicCreateResult.meta?.transactionManagerMeta.transactionData;
const requestId = requestLogicCreateResult.result.requestId;
const isSkipingPersistence = 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: isSkipingPersistence
? {
topics: requestLogicCreateResult.meta.transactionManagerMeta?.topics,
transactionData: transactionData,
paymentRequest: this.preparePaymentRequest(transactionData, requestId),
}
: null,
});

if (!options?.skipRefresh) {
if (!options?.skipRefresh && !isSkipingPersistence) {
// refresh the local request data
await request.refresh();
}
Expand Down
13 changes: 13 additions & 0 deletions packages/request-client.js/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 paymentRequest - Structured data primarily used for processing payments before the request is persisted.
*/
public readonly inMemoryInfo: RequestLogicTypes.IInMemoryInfo | null = null;
MantisClone marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates an instance of Request
*
Expand All @@ -98,6 +109,7 @@ export default class Request {
requestLogicCreateResult?: RequestLogicTypes.IReturnCreateRequest;
skipPaymentDetection?: boolean;
disableEvents?: boolean;
inMemoryInfo?: RequestLogicTypes.IInMemoryInfo | null;
},
) {
this.requestLogic = requestLogic;
Expand All @@ -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;
Expand Down
14 changes: 12 additions & 2 deletions packages/request-client.js/src/http-request-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,8 +24,9 @@ export default class HttpRequestNetwork extends RequestNetwork {
* @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.
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
* @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 to create a transaction without persisting it.
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
*/
constructor(
{
Expand All @@ -35,6 +37,7 @@ export default class HttpRequestNetwork extends RequestNetwork {
useMockStorage,
currencyManager,
paymentOptions,
skipPersistence,
}: {
decryptionProvider?: DecryptionProviderTypes.IDecryptionProvider;
httpConfig?: Partial<ClientTypes.IHttpDataAccessConfig>;
Expand All @@ -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) {
Expand Down
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';
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
MantisClone marked this conversation as resolved.
Show resolved Hide resolved

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
Expand Up @@ -18,7 +18,7 @@ afterAll(() => {
});

afterEach(() => {
mockServer.restoreHandlers();
mockServer.resetHandlers();
MantisClone marked this conversation as resolved.
Show resolved Hide resolved
});

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