From cf21d2bfee6c85e964418a05ea7b176b8ae976fe Mon Sep 17 00:00:00 2001 From: BenRey <44082144+Ben-Rey@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:02:02 +0200 Subject: [PATCH] Add test to operationManager (#573) --- packages/massa-web3/package.json | 1 + .../src/experimental/accountOperation.ts | 8 +- .../experimental/basicElements/operation.ts | 14 +- .../basicElements/operationManager.ts | 11 +- .../src/experimental/smartContract.ts | 5 +- packages/massa-web3/src/web3/BaseClient.ts | 2 + .../unit/mock/blockchainClient.mock.ts | 29 +++ .../unit/operationManager.spec.ts | 180 +++++++++++++++++- 8 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 packages/massa-web3/test/experimental/unit/mock/blockchainClient.mock.ts diff --git a/packages/massa-web3/package.json b/packages/massa-web3/package.json index 1068d0af..da3f8efe 100644 --- a/packages/massa-web3/package.json +++ b/packages/massa-web3/package.json @@ -18,6 +18,7 @@ "generate": "npm-run-all generate:*", "test": "jest --detectOpenHandles --forceExit ./test/experimental/unit", "test:cov": "jest --detectOpenHandles --coverage --forceExit ./test/experimental/unit", + "test:cov:watch": "jest --detectOpenHandles --coverage --watch ./test/experimental/unit", "test:integration": "jest --detectOpenHandles --forceExit ./test/experimental/integration", "test:all": "npm run test && npm run test:integration", "test:watch": "jest --watch", diff --git a/packages/massa-web3/src/experimental/accountOperation.ts b/packages/massa-web3/src/experimental/accountOperation.ts index 1cdb032e..093b908d 100644 --- a/packages/massa-web3/src/experimental/accountOperation.ts +++ b/packages/massa-web3/src/experimental/accountOperation.ts @@ -34,8 +34,8 @@ export class AccountOperation { const operation = new OperationManager(this.account.privateKey, this.client) const details: RollOperation = { fee: opts?.fee ?? (await this.client.getMinimalFee()), - expirePeriod: await calculateExpirePeriod( - this.client, + expirePeriod: calculateExpirePeriod( + await this.client.fetchPeriod(), opts?.periodToLive ), type, @@ -97,8 +97,8 @@ export class AccountOperation { const operation = new OperationManager(this.account.privateKey, this.client) const details: TransferOperation = { fee: opts?.fee ?? (await this.client.getMinimalFee()), - expirePeriod: await calculateExpirePeriod( - this.client, + expirePeriod: calculateExpirePeriod( + await this.client.fetchPeriod(), opts?.periodToLive ), type: OperationType.Transaction, diff --git a/packages/massa-web3/src/experimental/basicElements/operation.ts b/packages/massa-web3/src/experimental/basicElements/operation.ts index 7c675321..dd54f9b4 100644 --- a/packages/massa-web3/src/experimental/basicElements/operation.ts +++ b/packages/massa-web3/src/experimental/basicElements/operation.ts @@ -1,4 +1,4 @@ -import { EventFilter, SCOutputEvent } from '../client' +import { SCOutputEvent } from '../client' import { BlockchainClient } from '../client' /** @@ -89,12 +89,10 @@ export class Operation { return Promise.reject(new Error('Operation not found')) } - const filter = { + return this.client.getEvents({ operationId: this.id, isFinal: true, - } as EventFilter - - return this.client.getEvents(filter) + }) } /** @@ -107,12 +105,10 @@ export class Operation { return Promise.reject(new Error('Operation not found')) } - const filter = { + return this.client.getEvents({ operationId: this.id, isFinal: false, - } as EventFilter - - return this.client.getEvents(filter) + }) } } diff --git a/packages/massa-web3/src/experimental/basicElements/operationManager.ts b/packages/massa-web3/src/experimental/basicElements/operationManager.ts index 4693a09f..f446d1b4 100644 --- a/packages/massa-web3/src/experimental/basicElements/operationManager.ts +++ b/packages/massa-web3/src/experimental/basicElements/operationManager.ts @@ -262,23 +262,22 @@ export class OperationManager { * Calculates the expire period. * * @remarks - * This function fetches the current period from the blockchain and adds the periodToLive to it. * If the periodToLive is too big, the node will silently reject the operation. * This is why the periodToLive is limited to an upper value. * - * @param client - The blockchain client. + * @param period - The current period. * @param periodToLive - The period to live. * * @returns The expire period. * @throws An error if the periodToLive is too low or too big. */ -export async function calculateExpirePeriod( - client: BlockchainClient, +export function calculateExpirePeriod( + period: number, periodToLive: number = 10 -): Promise { +): number { // Todo: adjust max value if (periodToLive < 1 || periodToLive > 100) { throw new Error('periodToLive must be between 1 and 100') } - return (await client.fetchPeriod()) + periodToLive + return period + periodToLive } diff --git a/packages/massa-web3/src/experimental/smartContract.ts b/packages/massa-web3/src/experimental/smartContract.ts index 1ea08ee7..5a9478e3 100644 --- a/packages/massa-web3/src/experimental/smartContract.ts +++ b/packages/massa-web3/src/experimental/smartContract.ts @@ -57,7 +57,10 @@ export class ByteCode { const operation = new OperationManager(privateKey, client) const details: ExecuteOperation = { fee: opts?.fee ?? (await client.getMinimalFee()), - expirePeriod: await calculateExpirePeriod(client, opts?.periodToLive), + expirePeriod: calculateExpirePeriod( + await client.fetchPeriod(), + opts?.periodToLive + ), type: OperationType.ExecuteSmartContractBytecode, coins: opts?.coins ?? 0n, // TODO: implement max gas diff --git a/packages/massa-web3/src/web3/BaseClient.ts b/packages/massa-web3/src/web3/BaseClient.ts index f7f88847..524cecb1 100755 --- a/packages/massa-web3/src/web3/BaseClient.ts +++ b/packages/massa-web3/src/web3/BaseClient.ts @@ -386,6 +386,8 @@ export class BaseClient { rollsAmountEncoded, ]) } + default: + throw new Error('operation type not supported') } } } diff --git a/packages/massa-web3/test/experimental/unit/mock/blockchainClient.mock.ts b/packages/massa-web3/test/experimental/unit/mock/blockchainClient.mock.ts new file mode 100644 index 00000000..16d1f935 --- /dev/null +++ b/packages/massa-web3/test/experimental/unit/mock/blockchainClient.mock.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { OperationStatus } from '../../../../src/experimental/basicElements/operation' +import { + BlockchainClient, + EventFilter, + SCOutputEvent, + SendOperationInput, +} from '../../../../src/experimental/client/interfaces' + +import { NodeStatus } from '../../../../src/experimental/generated/client' + +export const blockchainClientMock: BlockchainClient = { + sendOperation: jest.fn((data: SendOperationInput) => + Promise.resolve('operationId') + ), + fetchPeriod: jest.fn(() => Promise.resolve(1)), + getOperationStatus: jest.fn((operationId: string) => + Promise.resolve(OperationStatus.Success) + ), + getBalance: jest.fn((address: string, speculative?: boolean) => + Promise.resolve(BigInt(1000)) + ), + getEvents: jest.fn((filter: EventFilter) => + Promise.resolve([] as SCOutputEvent[]) + ), + getChainId: jest.fn(() => Promise.resolve(BigInt(1))), + getMinimalFee: jest.fn(() => Promise.resolve(BigInt(1))), + status: {} as NodeStatus, +} diff --git a/packages/massa-web3/test/experimental/unit/operationManager.spec.ts b/packages/massa-web3/test/experimental/unit/operationManager.spec.ts index 39fa8a4e..a17751da 100644 --- a/packages/massa-web3/test/experimental/unit/operationManager.spec.ts +++ b/packages/massa-web3/test/experimental/unit/operationManager.spec.ts @@ -8,17 +8,17 @@ import { } from '../../../src/index' import { getOperationBufferToSign } from '../../../src/web3/accounts/Web3Account' import { BaseClient, PERIOD_OFFSET } from '../../../src/web3/BaseClient' - +import { blockchainClientMock } from './mock/blockchainClient.mock' import { ExecuteOperation, OperationManager, OperationType, + RollOperation, + Signature, TransferOperation, + calculateExpirePeriod, } from '../../../src/experimental/basicElements' -import { - PrivateKey, - Address as XPAddress, -} from '../../../src/experimental/basicElements' +import { PrivateKey, Address } from '../../../src/experimental/basicElements' import 'dotenv/config' const clientConfig: IClientConfig = { @@ -39,7 +39,7 @@ describe('Unit tests', () => { type: OperationType.Transaction, expirePeriod: 2, amount: 3n, - recipientAddress: XPAddress.fromString( + recipientAddress: Address.fromString( 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53' ), } @@ -61,6 +61,31 @@ describe('Unit tests', () => { ) }) + test('serialize - sell roll', async () => { + const sellRoll: RollOperation = { + type: OperationType.RollSell, + expirePeriod: 2, + fee: 1n, + amount: 3n, + } + + const transactionData: ITransactionData = { + fee: 1n, + amount: 3n, + recipientAddress: 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53', + } + + expect(OperationManager.serialize(sellRoll)).toEqual( + Uint8Array.from( + new BaseClient(clientConfig).compactBytesForOperation( + transactionData, + OperationTypeId.RollSell, + 2 + ) + ) + ) + }) + test('serialize - execute', async () => { const execute: ExecuteOperation = { fee: 1n, @@ -95,13 +120,23 @@ describe('Unit tests', () => { ) }) + test('serialize - throw if OperationType is not supported', async () => { + const operation = { + type: -1, + } + + expect(() => + OperationManager.serialize(operation as ExecuteOperation) + ).toThrow('Operation type not supported') + }) + test('canonicalize', async () => { const transfer: TransferOperation = { fee: 1n, type: OperationType.Transaction, expirePeriod: 2, amount: 3n, - recipientAddress: XPAddress.fromString( + recipientAddress: Address.fromString( 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53' ), } @@ -168,4 +203,135 @@ describe('Unit tests', () => { ) ) }) + + test('deserialize - transfer', async () => { + const transfer: TransferOperation = { + fee: 1n, + type: OperationType.Transaction, + expirePeriod: 2, + amount: 3n, + recipientAddress: Address.fromString( + 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53' + ), + } + + const serialized = OperationManager.serialize(transfer) + const deserialized = OperationManager.deserialize(serialized) + + expect(deserialized).toEqual(transfer) + }) + + test('deserialize - roll sell', async () => { + const sellRoll: RollOperation = { + type: OperationType.RollSell, + expirePeriod: 2, + fee: 1n, + amount: 3n, + } + + const serialized = OperationManager.serialize(sellRoll) + const deserialized = OperationManager.deserialize(serialized) + + expect(deserialized).toEqual(sellRoll) + }) + + test('sign', async () => { + const transfer: TransferOperation = { + fee: 1n, + type: OperationType.Transaction, + expirePeriod: 2, + amount: 3n, + recipientAddress: Address.fromString( + 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53' + ), + } + + const privateKey = await PrivateKey.generate() + const operationManager = new OperationManager(privateKey) + const signature = await operationManager.sign(1n, transfer) + + expect(signature).toBeInstanceOf(Signature) + }) + + test('send throw if blockchainClient is not defined', async () => { + const transfer: TransferOperation = { + fee: 1n, + type: OperationType.Transaction, + expirePeriod: 2, + amount: 3n, + recipientAddress: Address.fromString( + 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53' + ), + } + + const privateKey = await PrivateKey.generate() + const operationManager = new OperationManager(privateKey) + + await expect(operationManager.send(transfer)).rejects.toThrow( + 'blockchainClient is mandatory to send operations' + ) + }) + + test('send', async () => { + const transfer: TransferOperation = { + fee: 1n, + type: OperationType.Transaction, + expirePeriod: 2, + amount: 3n, + recipientAddress: Address.fromString( + 'AU1wN8rn4SkwYSTDF3dHFY4U28KtsqKL1NnEjDZhHnHEy6cEQm53' + ), + } + + const privateKey = await PrivateKey.generate() + + const operationManager = new OperationManager( + privateKey, + blockchainClientMock + ) + + const operationId = await operationManager.send(transfer) + + expect(blockchainClientMock.getChainId).toHaveBeenCalled() + expect(blockchainClientMock.sendOperation).toHaveBeenCalledWith({ + data: OperationManager.serialize(transfer), + publicKey: (await privateKey.getPublicKey()).toString(), + signature: (await operationManager.sign(1n, transfer)).toString(), + }) + expect(operationId).toBe('operationId') + }) +}) + +describe('calculateExpirePeriod', () => { + test('returns correct expire period', () => { + const period = 1 + const periodToLive = 10 + const expectedExpirePeriod = period + periodToLive + const expirePeriod = calculateExpirePeriod(period, periodToLive) + expect(expirePeriod).toBe(expectedExpirePeriod) + }) + + test('returns correct expire period', () => { + const period = 1 + const defaultPeriodToLive = 10 + const expectedExpirePeriod = period + defaultPeriodToLive + const expirePeriod = calculateExpirePeriod(period) + expect(expirePeriod).toBe(expectedExpirePeriod) + }) + + test('throws error if periodToLive is less than 1', () => { + const period = 1 + const periodToLive = 0 + expect(() => calculateExpirePeriod(period, periodToLive)).toThrow( + 'periodToLive must be between 1 and 100' + ) + }) + test('throws error if periodToLive is greater than 100', () => { + const period = 1 + const periodToLive = 101 + + expect(() => calculateExpirePeriod(period, periodToLive)).toThrow( + 'periodToLive must be between 1 and 100' + ) + }) })