Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 22 additions & 1 deletion packages/advanced-logic/src/extensions/abstract-extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExtensionTypes, IdentityTypes, RequestLogicTypes } from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';
import { areEqualIdentities, deepCopy } from '@requestnetwork/utils';

export interface ICreationContext {
extensionsState: RequestLogicTypes.IExtensionStates;
Expand Down Expand Up @@ -67,6 +67,17 @@ export abstract class AbstractExtension<TCreationParameters> implements Extensio
throw Error(`This extension has already been created`);
}

// New extensions can supply payment instructions or content.
// After request creation, only the payee may add them.
if (!this.isRequestCreation(requestState)) {
if (!requestState.payee) {
throw Error(`The request must have a payee`);
}
if (!areEqualIdentities(actionSigner, requestState.payee)) {
throw Error(`The signer must be the payee`);
}
}

copiedExtensionState[extensionAction.id] = this.applyCreation(extensionAction, timestamp, {
extensionsState,
extensionAction,
Expand Down Expand Up @@ -141,4 +152,14 @@ export abstract class AbstractExtension<TCreationParameters> implements Extensio
_extensionAction: ExtensionTypes.IAction,
// eslint-disable-next-line @typescript-eslint/no-empty-function
): void {}

/**
* Whether the action being applied is part of the request creation action.
* Every request action pushes an event before extensions are applied, so the request is
* being created only if its last event is the creation event.
*/
private isRequestCreation(requestState: RequestLogicTypes.IRequest): boolean {
const lastEvent = requestState.events[requestState.events.length - 1];
return !lastEvent || lastEvent.name === RequestLogicTypes.ACTION_NAME.CREATE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,12 @@ export default class MetaPaymentNetwork<
}
});
} else if (extensionAction.action === ExtensionTypes.PnMeta.ACTION.APPLY_ACTION_TO_PN) {
const { pnIdentifier } = extensionAction.parameters;
const { pnIdentifier, action } = extensionAction.parameters;

// Sub-payment networks can only be created with the meta payment network
if (action === ExtensionTypes.PnMeta.ACTION.CREATE) {
throw new Error('Invalid action');
}

const subPnState: ExtensionTypes.IState =
request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.META]?.values?.[pnIdentifier];
Expand Down
37 changes: 36 additions & 1 deletion packages/advanced-logic/test/extensions/content-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExtensionTypes } from '@requestnetwork/types';
import { ExtensionTypes, RequestLogicTypes } from '@requestnetwork/types';
import { deepCopy } from '@requestnetwork/utils';

import ContentData from '../../src/extensions/content-data';
Expand Down Expand Up @@ -29,6 +29,41 @@ describe('content-data', () => {
// 'previous request state must not change'
expect(requestCreatedNoExtensionBefore).toEqual(TestData.requestCreatedNoExtension);
});
describe('after the request creation', () => {
const requestAfterCreation: RequestLogicTypes.IRequest = deepCopy(
TestData.requestCreatedNoExtension,
);
requestAfterCreation.events.push({
actionSigner: TestData.otherIdRaw.identity,
name: RequestLogicTypes.ACTION_NAME.ADD_EXTENSIONS_DATA,
parameters: { extensionsDataLength: 1 },
timestamp: TestData.arbitraryTimestamp,
});

it('can create state if signed by the payee', () => {
expect(
contentData.applyActionToExtension(
{},
TestData.createContentDataExtensionData,
requestAfterCreation,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
),
).toEqual(TestData.expectedCreatedContentDataState);
});

it('cannot create state if not signed by the payee', () => {
expect(() =>
contentData.applyActionToExtension(
{},
TestData.createContentDataExtensionData,
requestAfterCreation,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
),
).toThrowError('The signer must be the payee');
});
});
it('cannot create state if already state', () => {
expect(() =>
contentData.applyActionToExtension(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,68 @@ describe('extensions/payment-network/erc20/fee-proxy-contract', () => {
).toEqual(DataERC20FeeCreate.extensionFullState);
});

describe('after the request creation', () => {
const requestAfterCreation: RequestLogicTypes.IRequest = deepCopy(
DataERC20FeeCreate.requestStateNoExtensions,
);
requestAfterCreation.events.push({
actionSigner: TestData.otherIdRaw.identity,
name: RequestLogicTypes.ACTION_NAME.ADD_EXTENSIONS_DATA,
parameters: { extensionsDataLength: 1 },
timestamp: TestData.arbitraryTimestamp,
});

it('can applyActionToExtensions of creation signed by the payee', () => {
expect(
erc20FeeProxyContract.applyActionToExtension(
requestAfterCreation.extensions,
DataERC20FeeCreate.actionCreationFull,
requestAfterCreation,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
),
).toEqual(DataERC20FeeCreate.extensionFullState);
});

it('cannot applyActionToExtensions of creation signed by someone else than the payee', () => {
expect(() => {
erc20FeeProxyContract.applyActionToExtension(
requestAfterCreation.extensions,
DataERC20FeeCreate.actionCreationFull,
requestAfterCreation,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('The signer must be the payee');
});

it('cannot applyActionToExtensions of creation signed by the payer', () => {
expect(() => {
erc20FeeProxyContract.applyActionToExtension(
requestAfterCreation.extensions,
DataERC20FeeCreate.actionCreationFull,
requestAfterCreation,
TestData.payerRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('The signer must be the payee');
});

it('cannot applyActionToExtensions of creation without payee', () => {
const requestNoPayee: RequestLogicTypes.IRequest = deepCopy(requestAfterCreation);
delete requestNoPayee.payee;
expect(() => {
erc20FeeProxyContract.applyActionToExtension(
requestNoPayee.extensions,
DataERC20FeeCreate.actionCreationFull,
requestNoPayee,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('The request must have a payee');
});
});

it('cannot applyActionToExtensions of creation with a previous state', () => {
// 'must throw'
expect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,27 @@ describe('extensions/payment-network/meta', () => {
).toEqual(MetaCreate.extensionFullStateMultipleAnyToErc20);
});

it('cannot applyActionToExtensions of creation after the request creation if not signed by the payee', () => {
const requestAfterCreation: RequestLogicTypes.IRequest = deepCopy(
MetaCreate.requestStateNoExtensions,
);
requestAfterCreation.events.push({
actionSigner: TestData.otherIdRaw.identity,
name: RequestLogicTypes.ACTION_NAME.ADD_EXTENSIONS_DATA,
parameters: { extensionsDataLength: 1 },
timestamp: TestData.arbitraryTimestamp,
});
expect(() => {
metaPn.applyActionToExtension(
requestAfterCreation.extensions,
MetaCreate.actionCreationMultipleAnyToErc20,
requestAfterCreation,
TestData.otherIdRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('The signer must be the payee');
});

it('cannot applyActionToExtensions of creation with a previous state', () => {
expect(() => {
metaPn.applyActionToExtension(
Expand Down Expand Up @@ -359,6 +380,25 @@ describe('extensions/payment-network/meta', () => {
}).toThrowError(`No payment network with identifier wrongId`);
});

it('cannot applyActionToExtensions applyApplyActionToExtension to create a sub-pn', () => {
expect(() => {
metaPn.applyActionToExtension(
MetaCreate.requestFullStateCreated.extensions,
{
...MetaCreate.actionApplyActionToPn,
parameters: {
action: ExtensionTypes.PnMeta.ACTION.CREATE,
pnIdentifier: MetaCreate.salt1,
parameters: MetaCreate.baseParams(MetaCreate.salt1),
},
},
MetaCreate.requestFullStateCreated,
TestData.payeeRaw.identity,
TestData.arbitraryTimestamp,
);
}).toThrowError('Invalid action');
});

it('cannot applyActionToExtensions applyApplyActionToExtension when the action does not exists on the sub pn', () => {
const actionWithInvalidPnAction = deepCopy(MetaCreate.actionApplyActionToPn);
actionWithInvalidPnAction.parameters.action = 'wrongAction' as ExtensionTypes.ACTION;
Expand Down
1 change: 1 addition & 0 deletions packages/request-logic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@requestnetwork/currency": "0.34.0",
"@types/jest": "29.5.6",
"@types/semver": "7.3.4",
"jest": "29.5.0",
Expand Down
79 changes: 79 additions & 0 deletions packages/request-logic/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { EventEmitter } from 'events';

import { AdvancedLogic } from '@requestnetwork/advanced-logic';
import { CurrencyManager } from '@requestnetwork/currency';
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
import * as MultiFormat from '@requestnetwork/multi-format';
import { AdvancedLogicTypes, RequestLogicTypes, TransactionTypes } from '@requestnetwork/types';

Expand Down Expand Up @@ -1015,6 +1017,83 @@ describe('index', () => {
});

describe('getRequestFromId', () => {
it('ignores an ACCEPT with an unauthorized new extension and allows a later valid ACCEPT', async () => {
const advancedLogic = new AdvancedLogic(CurrencyManager.getDefault());
const rejectedAccept = sign(
{
name: RequestLogicTypes.ACTION_NAME.ACCEPT,
parameters: {
requestId,
extensionsData: [
advancedLogic.extensions.contentData.createCreationAction({
content: { invoiceNumber: 'test-invoice' },
}),
],
},
version: CURRENT_VERSION,
},
TestData.payerRaw.signatureParams,
);
const transactions: TransactionTypes.IReturnGetTransactions['result']['transactions'] = [
{
state: TransactionTypes.TransactionState.CONFIRMED,
timestamp: 1,
transaction: { data: JSON.stringify(action) },
},
{
state: TransactionTypes.TransactionState.CONFIRMED,
timestamp: 2,
transaction: { data: JSON.stringify(rejectedAccept) },
},
];
const transactionManager: TransactionTypes.ITransactionManager = {
...fakeTransactionManager,
getTransactionsByChannelId: async () => ({
meta: { ignoredTransactions: [] },
result: { transactions },
}),
};
const requestLogic = new RequestLogic(transactionManager, undefined, advancedLogic);

const rejected = await requestLogic.getRequestFromId(requestId);

expect(rejected.result.request?.state).toBe(RequestLogicTypes.STATE.CREATED);
expect(rejected.result.request?.extensions).toEqual({});
expect(rejected.result.request?.events.map((event) => event.name)).toEqual([
RequestLogicTypes.ACTION_NAME.CREATE,
]);
expect(rejected.meta.ignoredTransactions).toEqual([
expect.objectContaining({
reason: 'The signer must be the payee',
transaction: expect.objectContaining({ action: rejectedAccept }),
}),
]);

const validAccept = sign(
{
name: RequestLogicTypes.ACTION_NAME.ACCEPT,
parameters: { requestId },
version: CURRENT_VERSION,
},
TestData.payerRaw.signatureParams,
);
transactions.push({
state: TransactionTypes.TransactionState.CONFIRMED,
timestamp: 3,
transaction: { data: JSON.stringify(validAccept) },
});

const accepted = await requestLogic.getRequestFromId(requestId);

expect(accepted.result.request?.state).toBe(RequestLogicTypes.STATE.ACCEPTED);
expect(accepted.result.request?.extensions).toEqual({});
expect(accepted.result.request?.events.map((event) => event.name)).toEqual([
RequestLogicTypes.ACTION_NAME.CREATE,
RequestLogicTypes.ACTION_NAME.ACCEPT,
]);
expect(accepted.meta.ignoredTransactions).toEqual(rejected.meta.ignoredTransactions);
});

it('can getRequestFromId', async () => {
const actionCreate: RequestLogicTypes.IAction = sign(
{
Expand Down
Loading