From 70f4192431b1f28180570d23449e23df36cb7c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= <50665615+flopez7@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:27:59 +0200 Subject: [PATCH 01/16] [Subgraph] Wrong balance (#3405) Co-authored-by: portuu3 --- .../human_protocol_sdk/constants.py | 12 +- .../human-protocol-sdk/src/constants.ts | 12 +- .../subgraph/src/mapping/EscrowTemplate.ts | 26 ++- .../subgraph/tests/escrow/escrow.test.ts | 151 +++++++++++++++++- .../subgraph/tests/escrow/fixtures.ts | 10 +- 5 files changed, 189 insertions(+), 22 deletions(-) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py index 00b433d0ac..a9433ff89a 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py @@ -36,7 +36,7 @@ class OperatorCategory(Enum): "https://api.studio.thegraph.com/query/74256/ethereum/version/latest" ), "subgraph_url_api_key": ( - "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmZEF1exsjDwjDXy1kmN3MbdZKxfkkoTj2MbEPUyhLfEG3" + "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmUe2Zpp9uZmYRLsgiEJQA87CCna4ZaRGUiQ7cU1f58dVV" ), "hmt_address": "0xd1ba9BAC957322D6e8c07a160a3A8dA11A0d2867", "factory_address": "0xD9c75a1Aa4237BB72a41E5E26bd8384f10c1f55a", @@ -52,7 +52,7 @@ class OperatorCategory(Enum): "https://api.studio.thegraph.com/query/74256/sepolia/version/latest" ), "subgraph_url_api_key": ( - "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmeHhtntEYGdgTqHQTJu5mUXHS8JPt3RiwidLYeeEu6VpP" + "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmbPBqzyRt3zAtF6BtY52P8wn7Gc3qdjWuKLHdargK8WZp" ), "hmt_address": "0x792abbcC99c01dbDec49c9fa9A828a186Da45C33", "factory_address": "0x5987A5558d961ee674efe4A8c8eB7B1b5495D3bf", @@ -68,7 +68,7 @@ class OperatorCategory(Enum): "https://api.studio.thegraph.com/query/74256/bsc/version/latest" ), "subgraph_url_api_key": ( - "hthttps://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmWsXVhdFuZZcXDjXrv1QLSBYcjNShazFQmNUdpAErjQqD" + "hthttps://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmYP2yo1NGNWGf585uwyRifERv6ZJbJqse3q9Xv1izrR5D" ), "hmt_address": "0x711Fd6ab6d65A98904522d4e3586F492B989c527", "factory_address": "0x92FD968AcBd521c232f5fB8c33b342923cC72714", @@ -84,7 +84,7 @@ class OperatorCategory(Enum): "https://api.studio.thegraph.com/query/74256/bsc-testnet/version/latest" ), "subgraph_url_api_key": ( - "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmPv7asd21BA5LZJxjDPfoL5ZJkEGMvahrdMqgNQnP5sxn" + "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmekNGfmcY6trj3m471kQLWg3YSKM7UaRJ3Ef3MBxWfu6o" ), "hmt_address": "0xE3D74BBFa45B4bCa69FF28891fBE392f4B4d4e4d", "factory_address": "0x2bfA592DBDaF434DDcbb893B1916120d181DAD18", @@ -102,7 +102,7 @@ class OperatorCategory(Enum): "https://api.studio.thegraph.com/query/74256/polygon/version/latest" ), "subgraph_url_api_key": ( - "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmZSsJn5TERyEfRrrbY926hLHD321ijoMynrxWxc3boRa6" + "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmTv69h7rW9SMSfLJN4WHzMA4qYttX2GzzfM5Drey4mCRp" ), "hmt_address": "0xc748B2A084F8eFc47E086ccdDD9b7e67aEb571BF", "factory_address": "0xBDBfD2cC708199C5640C6ECdf3B0F4A4C67AdfcB", @@ -120,7 +120,7 @@ class OperatorCategory(Enum): "https://api.studio.thegraph.com/query/74256/amoy/version/latest" ), "subgraph_url_api_key": ( - "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmYWc4ciJbAvTvcjoBzRSTWZrf1xD8WPRqEk3xtJZUKZqY" + "https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmUe97VzETuP1zQ6oD2trtu9RvSxpRSUrv4uhF1y5dzQDN" ), "hmt_address": "0x792abbcC99c01dbDec49c9fa9A828a186Da45C33", "factory_address": "0xAFf5a986A530ff839d49325A5dF69F96627E8D29", diff --git a/packages/sdk/typescript/human-protocol-sdk/src/constants.ts b/packages/sdk/typescript/human-protocol-sdk/src/constants.ts index 15bbc397ed..0351a400ae 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/constants.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/constants.ts @@ -36,7 +36,7 @@ export const NETWORKS: { subgraphUrl: 'https://api.studio.thegraph.com/query/74256/ethereum/version/latest', subgraphUrlApiKey: - 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmZEF1exsjDwjDXy1kmN3MbdZKxfkkoTj2MbEPUyhLfEG3', + 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmUe2Zpp9uZmYRLsgiEJQA87CCna4ZaRGUiQ7cU1f58dVV', oldSubgraphUrl: '', oldFactoryAddress: '', }, @@ -51,7 +51,7 @@ export const NETWORKS: { subgraphUrl: 'https://api.studio.thegraph.com/query/74256/sepolia/version/latest', subgraphUrlApiKey: - 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmeHhtntEYGdgTqHQTJu5mUXHS8JPt3RiwidLYeeEu6VpP', + 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmbPBqzyRt3zAtF6BtY52P8wn7Gc3qdjWuKLHdargK8WZp', oldSubgraphUrl: '', oldFactoryAddress: '', }, @@ -66,7 +66,7 @@ export const NETWORKS: { subgraphUrl: 'https://api.studio.thegraph.com/query/74256/bsc/version/latest', subgraphUrlApiKey: - 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmWsXVhdFuZZcXDjXrv1QLSBYcjNShazFQmNUdpAErjQqD', + 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmYP2yo1NGNWGf585uwyRifERv6ZJbJqse3q9Xv1izrR5D', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsc', oldFactoryAddress: '0xc88bC422cAAb2ac8812de03176402dbcA09533f4', }, @@ -81,7 +81,7 @@ export const NETWORKS: { subgraphUrl: 'https://api.studio.thegraph.com/query/74256/bsc-testnet/version/latest', subgraphUrlApiKey: - 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmPv7asd21BA5LZJxjDPfoL5ZJkEGMvahrdMqgNQnP5sxn', + 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmekNGfmcY6trj3m471kQLWg3YSKM7UaRJ3Ef3MBxWfu6o', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsctest', oldFactoryAddress: '0xaae6a2646c1f88763e62e0cd08ad050ea66ac46f', @@ -97,7 +97,7 @@ export const NETWORKS: { subgraphUrl: 'https://api.studio.thegraph.com/query/74256/polygon/version/latest', subgraphUrlApiKey: - 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmZSsJn5TERyEfRrrbY926hLHD321ijoMynrxWxc3boRa6', + 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmTv69h7rW9SMSfLJN4WHzMA4qYttX2GzzfM5Drey4mCRp', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/polygon', oldFactoryAddress: '0x45eBc3eAE6DA485097054ae10BA1A0f8e8c7f794', @@ -113,7 +113,7 @@ export const NETWORKS: { subgraphUrl: 'https://api.studio.thegraph.com/query/74256/amoy/version/latest', subgraphUrlApiKey: - 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmYWc4ciJbAvTvcjoBzRSTWZrf1xD8WPRqEk3xtJZUKZqY', + 'https://gateway-arbitrum.network.thegraph.com/api/[SUBGRAPH_API_KEY]/deployments/id/QmUe97VzETuP1zQ6oD2trtu9RvSxpRSUrv4uhF1y5dzQDN', oldSubgraphUrl: '', oldFactoryAddress: '', }, diff --git a/packages/sdk/typescript/subgraph/src/mapping/EscrowTemplate.ts b/packages/sdk/typescript/subgraph/src/mapping/EscrowTemplate.ts index a95556a16b..cbfaeeaf3c 100644 --- a/packages/sdk/typescript/subgraph/src/mapping/EscrowTemplate.ts +++ b/packages/sdk/typescript/subgraph/src/mapping/EscrowTemplate.ts @@ -639,11 +639,7 @@ export function handleCompleted(event: Completed): void { // Update escrow entity const escrowEntity = Escrow.load(dataSource.address()); if (escrowEntity) { - escrowEntity.status = 'Complete'; - escrowEntity.save(); - eventEntity.launcher = escrowEntity.launcher; - - createTransaction( + const transaction = createTransaction( event, 'complete', event.transaction.from, @@ -651,6 +647,26 @@ export function handleCompleted(event: Completed): void { null, Address.fromBytes(escrowEntity.address) ); + if ( + escrowEntity.balance && + escrowEntity.balance.gt(ZERO_BI) && + escrowEntity.token != HMT_ADDRESS + ) { + const internalTransaction = new InternalTransaction(toEventId(event)); + internalTransaction.from = escrowEntity.address; + internalTransaction.to = escrowEntity.launcher; + internalTransaction.value = escrowEntity.balance; + internalTransaction.transaction = transaction.id; + internalTransaction.method = 'transfer'; + internalTransaction.escrow = escrowEntity.address; + internalTransaction.token = escrowEntity.token; + internalTransaction.save(); + + escrowEntity.balance = ZERO_BI; + } + escrowEntity.status = 'Complete'; + escrowEntity.save(); + eventEntity.launcher = escrowEntity.launcher; } eventEntity.save(); } diff --git a/packages/sdk/typescript/subgraph/tests/escrow/escrow.test.ts b/packages/sdk/typescript/subgraph/tests/escrow/escrow.test.ts index 52ffc384eb..22e01f2143 100644 --- a/packages/sdk/typescript/subgraph/tests/escrow/escrow.test.ts +++ b/packages/sdk/typescript/subgraph/tests/escrow/escrow.test.ts @@ -93,7 +93,7 @@ describe('Escrow', () => { const escrow = new Escrow(escrowAddress); escrow.address = escrowAddress; - escrow.token = Address.zero(); + escrow.token = tokenAddress; escrow.factoryAddress = Address.zero(); escrow.launcher = launcherAddress; escrow.canceler = launcherAddress; @@ -1318,7 +1318,106 @@ describe('Escrow', () => { }); test('Should properly handle Completed event', () => { - const newCompleted = createCompletedEvent(operatorAddress); + const newCompleted = createCompletedEvent( + operatorAddress, + BigInt.fromI32(12) + ); + + handleCompleted(newCompleted); + + const id = toEventId(newCompleted).toHex(); + + // EscrowStatusEvent + assert.fieldEquals( + 'EscrowStatusEvent', + id, + 'block', + newCompleted.block.number.toString() + ); + assert.fieldEquals( + 'EscrowStatusEvent', + id, + 'timestamp', + newCompleted.block.timestamp.toString() + ); + assert.fieldEquals( + 'EscrowStatusEvent', + id, + 'txHash', + newCompleted.transaction.hash.toHex() + ); + assert.fieldEquals( + 'EscrowStatusEvent', + id, + 'escrowAddress', + escrowAddressString + ); + assert.fieldEquals( + 'EscrowStatusEvent', + id, + 'sender', + operatorAddressString + ); + assert.fieldEquals('EscrowStatusEvent', id, 'status', 'Complete'); + assert.fieldEquals( + 'EscrowStatusEvent', + id, + 'launcher', + launcherAddressString + ); + + // Escrow + assert.fieldEquals('Escrow', escrowAddress.toHex(), 'status', 'Complete'); + assert.fieldEquals( + 'Transaction', + newCompleted.transaction.hash.toHex(), + 'txHash', + newCompleted.transaction.hash.toHex() + ); + assert.fieldEquals( + 'Transaction', + newCompleted.transaction.hash.toHex(), + 'method', + 'complete' + ); + assert.fieldEquals( + 'Transaction', + newCompleted.transaction.hash.toHex(), + 'block', + newCompleted.block.number.toString() + ); + assert.fieldEquals( + 'Transaction', + newCompleted.transaction.hash.toHex(), + 'from', + newCompleted.transaction.from.toHex() + ); + assert.fieldEquals( + 'Transaction', + newCompleted.transaction.hash.toHex(), + 'to', + escrowAddressString + ); + + // InternalTransaction + const internalTxId = toEventId(newCompleted).toHex(); + assert.notInStore('InternalTransaction', internalTxId); + + // Escrow balance should be 0 after completion + assert.fieldEquals('Escrow', escrowAddress.toHex(), 'balance', '0'); + }); + + test('Should properly handle Completed event and create InternalTransaction if escrow has balance', () => { + const escrow = Escrow.load(escrowAddress); + if (escrow) { + escrow.balance = BigInt.fromI32(1234); + escrow.save(); + } + + const newCompleted = createCompletedEvent( + operatorAddress, + BigInt.fromI32(13) + ); handleCompleted(newCompleted); @@ -1395,6 +1494,44 @@ describe('Escrow', () => { 'to', escrowAddressString ); + + // InternalTransaction + const internalTxId = toEventId(newCompleted).toHex(); + + assert.fieldEquals( + 'InternalTransaction', + internalTxId, + 'from', + escrowAddressString + ); + assert.fieldEquals( + 'InternalTransaction', + internalTxId, + 'to', + launcherAddressString + ); + assert.fieldEquals('InternalTransaction', internalTxId, 'value', '1234'); + assert.fieldEquals( + 'InternalTransaction', + internalTxId, + 'method', + 'transfer' + ); + assert.fieldEquals( + 'InternalTransaction', + internalTxId, + 'escrow', + escrowAddressString + ); + assert.fieldEquals( + 'InternalTransaction', + internalTxId, + 'transaction', + newCompleted.transaction.hash.toHex() + ); + + // Escrow balance should be 0 after completion + assert.fieldEquals('Escrow', escrowAddress.toHex(), 'balance', '0'); }); test('Should properly handle Withdraw event', () => { @@ -1680,8 +1817,14 @@ describe('Escrow', () => { }); test('Should properly calculate completed event in statstics', () => { - const newCompleted1 = createCompletedEvent(operatorAddress); - const newCompleted2 = createCompletedEvent(operatorAddress); + const newCompleted1 = createCompletedEvent( + operatorAddress, + BigInt.fromI32(12) + ); + const newCompleted2 = createCompletedEvent( + operatorAddress, + BigInt.fromI32(13) + ); handleCompleted(newCompleted1); handleCompleted(newCompleted2); diff --git a/packages/sdk/typescript/subgraph/tests/escrow/fixtures.ts b/packages/sdk/typescript/subgraph/tests/escrow/fixtures.ts index 7546898636..dbc765a4a7 100644 --- a/packages/sdk/typescript/subgraph/tests/escrow/fixtures.ts +++ b/packages/sdk/typescript/subgraph/tests/escrow/fixtures.ts @@ -237,8 +237,16 @@ export function createCancelledEvent(sender: Address): Cancelled { return newCancelledEvent; } -export function createCompletedEvent(sender: Address): Completed { +export function createCompletedEvent( + sender: Address, + timestamp: BigInt +): Completed { const newCompletedEvent = changetype(newMockEvent()); + newCompletedEvent.transaction.hash = generateUniqueHash( + sender.toString(), + timestamp, + newCompletedEvent.transaction.nonce + ); newCompletedEvent.transaction.from = sender; From 0f78b3ccb5f058fe7cbe54516dbfa1822735e3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= <50665615+flopez7@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:49:12 +0200 Subject: [PATCH 02/16] [Human App] Implement JWT authentication guard and strategy to validate the token (#3394) --- packages/apps/human-app/server/jest.config.ts | 8 +- .../server/scripts/generate-env-doc.ts | 1 - .../human-app/server/src/app.controller.ts | 2 + .../apps/human-app/server/src/app.module.ts | 86 +++++++++++-------- .../src/common/config/params-decorators.ts | 39 --------- .../server/src/common/constants/cache.ts | 1 + .../server/src/common/constants/index.ts | 1 + .../server/src/common/decorators/index.ts | 11 +++ .../server/src/common/guards/jwt.auth.ts | 40 +++++++++ .../src/common/guards/strategy/index.ts | 1 + .../src/common/guards/strategy/jwt.http.ts | 67 +++++++++++++++ .../server/src/common/interfaces/jwt.ts | 6 ++ .../src/common/utils/jwt-token.model.ts | 12 +-- .../integrations/kv-store/kv-store.gateway.ts | 60 +++++++++++-- .../src/modules/abuse/abuse.controller.ts | 22 ++--- .../abuse/spec/abuse.controller.spec.ts | 9 +- .../email-confirmation.controller.ts | 27 +++--- .../email-verification.controller.spec.ts | 5 +- .../modules/h-captcha/h-captcha.controller.ts | 77 +++++++---------- .../spec/h-captcha.controller.spec.ts | 19 ++-- .../h-captcha/spec/h-captcha.fixtures.ts | 7 +- .../src/modules/health/health.controller.ts | 2 + .../job-assignment.controller.ts | 39 ++++----- .../spec/job-assignment.controller.spec.ts | 39 +++++---- .../jobs-discovery.controller.ts | 22 ++--- .../spec/jobs-discovery.controller.spec.ts | 42 ++++----- .../kyc-procedure/kyc-procedure.controller.ts | 21 +++-- .../spec/kyc-procedure.controller.spec.ts | 9 +- .../server/src/modules/nda/nda.controller.ts | 21 ++--- .../modules/nda/spec/nda.controller.spec.ts | 13 +-- .../oracle-discovery.controller.ts | 34 +++----- .../password-reset.controller.ts | 19 ++-- .../prepare-signature.controller.ts | 15 ++-- .../register-address.controller.ts | 23 ++--- .../spec/register-address.controller.spec.ts | 5 +- .../spec/statistics.controller.spec.ts | 10 +-- .../statistics/statistics.controller.ts | 38 +++----- .../token-refresh/token-refresh.controller.ts | 21 ++--- .../ui-configuration.controller.ts | 3 + .../user-operator/operator.controller.ts | 52 +++++------ .../spec/worker.controller.spec.ts | 19 ++-- .../modules/user-worker/worker.controller.ts | 30 +++---- 42 files changed, 524 insertions(+), 454 deletions(-) delete mode 100644 packages/apps/human-app/server/src/common/config/params-decorators.ts create mode 100644 packages/apps/human-app/server/src/common/constants/index.ts create mode 100644 packages/apps/human-app/server/src/common/guards/jwt.auth.ts create mode 100644 packages/apps/human-app/server/src/common/guards/strategy/index.ts create mode 100644 packages/apps/human-app/server/src/common/guards/strategy/jwt.http.ts create mode 100644 packages/apps/human-app/server/src/common/interfaces/jwt.ts diff --git a/packages/apps/human-app/server/jest.config.ts b/packages/apps/human-app/server/jest.config.ts index ce3be09dd0..b4808588b1 100644 --- a/packages/apps/human-app/server/jest.config.ts +++ b/packages/apps/human-app/server/jest.config.ts @@ -1,15 +1,17 @@ process.env['GIT_HASH'] = 'test_value_hardcoded_in_jest_config'; +import { createDefaultPreset } from 'ts-jest'; + +const jestTsPreset = createDefaultPreset({}); + module.exports = { + ...jestTsPreset, coverageDirectory: '../coverage', collectCoverageFrom: ['**/*.(t|j)s'], moduleFileExtensions: ['js', 'json', 'ts'], rootDir: 'src', testEnvironment: 'node', testRegex: '.*\\.spec\\.ts$', - transform: { - '^.+\\.(t|j)s$': 'ts-jest', - }, moduleNameMapper: { '^uuid$': require.resolve('uuid'), }, diff --git a/packages/apps/human-app/server/scripts/generate-env-doc.ts b/packages/apps/human-app/server/scripts/generate-env-doc.ts index 2b1817c9ce..d477e8d47c 100644 --- a/packages/apps/human-app/server/scripts/generate-env-doc.ts +++ b/packages/apps/human-app/server/scripts/generate-env-doc.ts @@ -112,7 +112,6 @@ function processConfigFiles() { 'common-config.module.ts', 'gateway-config.service.ts', 'gateway-config.types.ts', - 'params-decorators.ts', 'spec', ].includes(file), ); diff --git a/packages/apps/human-app/server/src/app.controller.ts b/packages/apps/human-app/server/src/app.controller.ts index f6cc9bb67c..bfe63b0c9c 100644 --- a/packages/apps/human-app/server/src/app.controller.ts +++ b/packages/apps/human-app/server/src/app.controller.ts @@ -1,9 +1,11 @@ import { Controller, Get, Redirect } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; +import { Public } from './common/decorators'; @Controller() export class AppController { @Get('/') + @Public() @Redirect('/swagger', 301) @ApiExcludeEndpoint() public swagger(): string { diff --git a/packages/apps/human-app/server/src/app.module.ts b/packages/apps/human-app/server/src/app.module.ts index 37292b384a..6099bfa28e 100644 --- a/packages/apps/human-app/server/src/app.module.ts +++ b/packages/apps/human-app/server/src/app.module.ts @@ -1,51 +1,54 @@ -import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { ConfigModule } from '@nestjs/config'; -import { HttpModule } from '@nestjs/axios'; -import { WorkerModule } from './modules/user-worker/worker.module'; -import { ReputationOracleModule } from './integrations/reputation-oracle/reputation-oracle.module'; -import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; -import { OperatorModule } from './modules/user-operator/operator.module'; -import { OperatorController } from './modules/user-operator/operator.controller'; -import { WorkerController } from './modules/user-worker/worker.controller'; -import { CommonConfigModule } from './common/config/common-config.module'; -import { CacheFactoryConfig } from './common/config/cache-factory.config'; +import { AutomapperModule } from '@automapper/nestjs'; +import { ChainId } from '@human-protocol/sdk'; +import { HttpModule } from '@nestjs/axios'; import { CacheModule } from '@nestjs/cache-manager'; -import { OracleDiscoveryController } from './modules/oracle-discovery/oracle-discovery.controller'; -import { OracleDiscoveryModule } from './modules/oracle-discovery/oracle-discovery.module'; -import { JobsDiscoveryModule } from './modules/jobs-discovery/jobs-discovery.module'; -import { JobsDiscoveryController } from './modules/jobs-discovery/jobs-discovery.controller'; -import { JobAssignmentController } from './modules/job-assignment/job-assignment.controller'; -import { JobAssignmentModule } from './modules/job-assignment/job-assignment.module'; -import { StatisticsModule } from './modules/statistics/statistics.module'; -import { StatisticsController } from './modules/statistics/statistics.controller'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; +import Joi from 'joi'; +import { AppController } from './app.controller'; +import { CacheFactoryConfig } from './common/config/cache-factory.config'; +import { CommonConfigModule } from './common/config/common-config.module'; +import { EnvironmentConfigService } from './common/config/environment-config.service'; +import { JwtAuthGuard } from './common/guards/jwt.auth'; +import { JwtHttpStrategy } from './common/guards/strategy'; +import { InterceptorModule } from './common/interceptors/interceptor.module'; +import { ForbidUnauthorizedHostMiddleware } from './common/middleware/host-check.middleware'; +import { EscrowUtilsModule } from './integrations/escrow/escrow-utils.module'; import { ExchangeOracleModule } from './integrations/exchange-oracle/exchange-oracle.module'; +import { HCaptchaLabelingModule } from './integrations/h-captcha-labeling/h-captcha-labeling.module'; import { KvStoreModule } from './integrations/kv-store/kv-store.module'; +import { ReputationOracleModule } from './integrations/reputation-oracle/reputation-oracle.module'; +import { AbuseController } from './modules/abuse/abuse.controller'; +import { AbuseModule } from './modules/abuse/abuse.module'; +import { CronJobModule } from './modules/cron-job/cron-job.module'; import { EmailConfirmationModule } from './modules/email-confirmation/email-confirmation.module'; -import { PasswordResetModule } from './modules/password-reset/password-reset.module'; +import { HCaptchaController } from './modules/h-captcha/h-captcha.controller'; +import { HCaptchaModule } from './modules/h-captcha/h-captcha.module'; +import { HealthModule } from './modules/health/health.module'; +import { JobAssignmentController } from './modules/job-assignment/job-assignment.controller'; +import { JobAssignmentModule } from './modules/job-assignment/job-assignment.module'; +import { JobsDiscoveryController } from './modules/jobs-discovery/jobs-discovery.controller'; +import { JobsDiscoveryModule } from './modules/jobs-discovery/jobs-discovery.module'; import { KycProcedureModule } from './modules/kyc-procedure/kyc-procedure.module'; +import { NDAController } from './modules/nda/nda.controller'; +import { NDAModule } from './modules/nda/nda.module'; +import { OracleDiscoveryController } from './modules/oracle-discovery/oracle-discovery.controller'; +import { OracleDiscoveryModule } from './modules/oracle-discovery/oracle-discovery.module'; +import { PasswordResetModule } from './modules/password-reset/password-reset.module'; import { PrepareSignatureModule } from './modules/prepare-signature/prepare-signature.module'; -import { HCaptchaModule } from './modules/h-captcha/h-captcha.module'; -import { HCaptchaLabelingModule } from './integrations/h-captcha-labeling/h-captcha-labeling.module'; -import { HCaptchaController } from './modules/h-captcha/h-captcha.controller'; -import { EscrowUtilsModule } from './integrations/escrow/escrow-utils.module'; -import Joi from 'joi'; -import { ChainId } from '@human-protocol/sdk'; import { RegisterAddressController } from './modules/register-address/register-address.controller'; import { RegisterAddressModule } from './modules/register-address/register-address.module'; -import { InterceptorModule } from './common/interceptors/interceptor.module'; -import { TokenRefreshModule } from './modules/token-refresh/token-refresh.module'; +import { StatisticsController } from './modules/statistics/statistics.controller'; +import { StatisticsModule } from './modules/statistics/statistics.module'; import { TokenRefreshController } from './modules/token-refresh/token-refresh.controller'; -import { CronJobModule } from './modules/cron-job/cron-job.module'; -import { EnvironmentConfigService } from './common/config/environment-config.service'; -import { ForbidUnauthorizedHostMiddleware } from './common/middleware/host-check.middleware'; -import { HealthModule } from './modules/health/health.module'; +import { TokenRefreshModule } from './modules/token-refresh/token-refresh.module'; import { UiConfigurationModule } from './modules/ui-configuration/ui-configuration.module'; -import { NDAModule } from './modules/nda/nda.module'; -import { NDAController } from './modules/nda/nda.controller'; -import { AbuseController } from './modules/abuse/abuse.controller'; -import { AbuseModule } from './modules/abuse/abuse.module'; +import { OperatorController } from './modules/user-operator/operator.controller'; +import { OperatorModule } from './modules/user-operator/operator.module'; +import { WorkerController } from './modules/user-worker/worker.controller'; +import { WorkerModule } from './modules/user-worker/worker.module'; const JOI_BOOLEAN_STRING_SCHEMA = Joi.string().valid('true', 'false'); @@ -146,7 +149,14 @@ const JOI_BOOLEAN_STRING_SCHEMA = Joi.string().valid('true', 'false'); AbuseController, ], exports: [HttpModule], - providers: [EnvironmentConfigService], + providers: [ + EnvironmentConfigService, + JwtHttpStrategy, + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/packages/apps/human-app/server/src/common/config/params-decorators.ts b/packages/apps/human-app/server/src/common/config/params-decorators.ts deleted file mode 100644 index 52f2e46b90..0000000000 --- a/packages/apps/human-app/server/src/common/config/params-decorators.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - BadRequestException, - createParamDecorator, - ExecutionContext, - UnauthorizedException, - Logger, -} from '@nestjs/common'; -import { jwtDecode } from 'jwt-decode'; -import { JwtUserData } from '../utils/jwt-token.model'; - -const logger = new Logger('JwtPayloadDecorator'); - -export const Authorization = createParamDecorator( - (_data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - const token = request.headers['authorization']; - if (token) { - return token; - } - throw new UnauthorizedException(); - }, -); - -export const JwtPayload = createParamDecorator( - (_data: unknown, ctx: ExecutionContext): any => { - const request = ctx.switchToHttp().getRequest(); - const token = request.headers['authorization']?.split(' ')[1]; - if (!token) { - throw new UnauthorizedException(); - } - try { - const decoded = jwtDecode(token); - return decoded as JwtUserData; - } catch (error) { - logger.error(`Error in decoding token: ${token}`, error); - throw new BadRequestException(); - } - }, -); diff --git a/packages/apps/human-app/server/src/common/constants/cache.ts b/packages/apps/human-app/server/src/common/constants/cache.ts index 9e3d3a6931..004f9bf37b 100644 --- a/packages/apps/human-app/server/src/common/constants/cache.ts +++ b/packages/apps/human-app/server/src/common/constants/cache.ts @@ -4,3 +4,4 @@ export const ORACLE_URL_CACHE_KEY = 'oracle:url'; export const DAILY_HMT_SPENT_CACHE_KEY = 'daily:hmt-spent'; export const ORACLE_STATISTICS_CACHE_KEY = 'statistics:oracle'; export const WORKER_STATISTICS_CACHE_KEY = 'statistics:worker'; +export const REPUTATION_ORACLE_PUBLIC_KEY = 'reputation:pubkey'; diff --git a/packages/apps/human-app/server/src/common/constants/index.ts b/packages/apps/human-app/server/src/common/constants/index.ts new file mode 100644 index 0000000000..7de7973ce8 --- /dev/null +++ b/packages/apps/human-app/server/src/common/constants/index.ts @@ -0,0 +1 @@ +export const JWT_KVSTORE_KEY = 'jwt_public_key'; diff --git a/packages/apps/human-app/server/src/common/decorators/index.ts b/packages/apps/human-app/server/src/common/decorators/index.ts index 14790857ae..fec5f72991 100644 --- a/packages/apps/human-app/server/src/common/decorators/index.ts +++ b/packages/apps/human-app/server/src/common/decorators/index.ts @@ -1 +1,12 @@ +import { Reflector } from '@nestjs/core'; + export * from './enums'; + +/** + * Decorator for HTTP endpoints to bypass JWT auth guard + * where JWT auth not needed + */ +export const Public = Reflector.createDecorator({ + key: 'isPublic', + transform: () => true, +}); diff --git a/packages/apps/human-app/server/src/common/guards/jwt.auth.ts b/packages/apps/human-app/server/src/common/guards/jwt.auth.ts new file mode 100644 index 0000000000..0dec177d5b --- /dev/null +++ b/packages/apps/human-app/server/src/common/guards/jwt.auth.ts @@ -0,0 +1,40 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { JwtUserData } from '../utils/jwt-token.model'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate { + constructor(private readonly reflector: Reflector) { + super(); + } + + public async canActivate(context: ExecutionContext): Promise { + // Check for public routes first + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) { + return true; + } + + // Try to authenticate with JWT + await super.canActivate(context); + + const request = context.switchToHttp().getRequest(); + const user = request.user as JwtUserData; + if (!user) { + throw new UnauthorizedException('User not found in request'); + } + request.token = request.headers['authorization']; + + return true; + } +} diff --git a/packages/apps/human-app/server/src/common/guards/strategy/index.ts b/packages/apps/human-app/server/src/common/guards/strategy/index.ts new file mode 100644 index 0000000000..e38b86c866 --- /dev/null +++ b/packages/apps/human-app/server/src/common/guards/strategy/index.ts @@ -0,0 +1 @@ +export * from './jwt.http'; diff --git a/packages/apps/human-app/server/src/common/guards/strategy/jwt.http.ts b/packages/apps/human-app/server/src/common/guards/strategy/jwt.http.ts new file mode 100644 index 0000000000..56e70eb47e --- /dev/null +++ b/packages/apps/human-app/server/src/common/guards/strategy/jwt.http.ts @@ -0,0 +1,67 @@ +import { Injectable, Req, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import * as jwt from 'jsonwebtoken'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; +import { JwtUserData } from '../../../common/utils/jwt-token.model'; +import { KvStoreGateway } from '../../../integrations/kv-store/kv-store.gateway'; + +@Injectable() +export class JwtHttpStrategy extends PassportStrategy(Strategy, 'jwt-http') { + constructor( + private readonly configService: EnvironmentConfigService, + private readonly kvStoreGateway: KvStoreGateway, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKeyProvider: async ( + _request: any, + rawJwtToken: any, + done: any, + ) => { + try { + const payload = jwt.decode(rawJwtToken); + const chainId = this.configService.chainIdsEnabled[0]; + const address = (payload as any).reputation_network; + const pubKey = await this.kvStoreGateway.getReputationOraclePublicKey( + chainId, + address, + ); + done(null, pubKey); + } catch (error) { + console.error(error); + done(error); + } + }, + passReqToCallback: true, + }); + } + + public async validate( + @Req() _request: any, + payload: { + user_id: string; + status: string; + wallet_address: string; + reputation_network: string; + qualifications?: string[]; + site_key?: string; + email?: string; + }, + ): Promise { + if (!payload.user_id) { + throw new UnauthorizedException('Invalid token: missing user id'); + } + + return { + user_id: payload.user_id, + wallet_address: payload.wallet_address, + status: payload.status, + reputation_network: payload.reputation_network, + qualifications: payload.qualifications, + site_key: payload.site_key, + email: payload.email, + }; + } +} diff --git a/packages/apps/human-app/server/src/common/interfaces/jwt.ts b/packages/apps/human-app/server/src/common/interfaces/jwt.ts new file mode 100644 index 0000000000..e5cfcce604 --- /dev/null +++ b/packages/apps/human-app/server/src/common/interfaces/jwt.ts @@ -0,0 +1,6 @@ +import { JwtUserData } from '../utils/jwt-token.model'; + +export interface RequestWithUser extends Request { + user: JwtUserData; + token: string; +} diff --git a/packages/apps/human-app/server/src/common/utils/jwt-token.model.ts b/packages/apps/human-app/server/src/common/utils/jwt-token.model.ts index bad884b4b6..85da60b3c1 100644 --- a/packages/apps/human-app/server/src/common/utils/jwt-token.model.ts +++ b/packages/apps/human-app/server/src/common/utils/jwt-token.model.ts @@ -6,17 +6,13 @@ export class JwtUserData { @AutoMap() wallet_address: string; @AutoMap() - email: string; - @AutoMap() - kyc_status: 'approved' | 'none'; + status: string; @AutoMap() reputation_network: string; @AutoMap() - qualifications: string[]; - @AutoMap() - site_key: string; + email?: string; @AutoMap() - iat: number; + qualifications?: string[]; @AutoMap() - exp: number; + site_key?: string; } diff --git a/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts b/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts index f15928a830..5341d6f44a 100644 --- a/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts +++ b/packages/apps/human-app/server/src/integrations/kv-store/kv-store.gateway.ts @@ -1,10 +1,19 @@ -import { HttpException, Inject, Injectable } from '@nestjs/common'; -import { EnvironmentConfigService } from '../../common/config/environment-config.service'; -import { ethers } from 'ethers'; -import { ChainId, KVStoreKeys, KVStoreUtils } from '@human-protocol/sdk'; +import { + ChainId, + KVStoreKeys, + KVStoreUtils, + StorageClient, +} from '@human-protocol/sdk'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { HttpException, Inject, Injectable } from '@nestjs/common'; import { Cache } from 'cache-manager'; -import { ORACLE_URL_CACHE_KEY } from '../../common/constants/cache'; +import { ethers } from 'ethers'; +import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { JWT_KVSTORE_KEY } from '../../common/constants'; +import { + ORACLE_URL_CACHE_KEY, + REPUTATION_ORACLE_PUBLIC_KEY, +} from '../../common/constants/cache'; @Injectable() export class KvStoreGateway { @@ -88,4 +97,45 @@ export class KvStoreGateway { return jobTypes; } } + + async getReputationOraclePublicKey( + chainId: ChainId, + address: string, + ): Promise { + const key = `${REPUTATION_ORACLE_PUBLIC_KEY}:${chainId}:${address}`; + const cachedData: string | undefined = await this.cacheManager.get(key); + if (cachedData) { + return cachedData; + } + + let publicKey: string; + try { + const url = await KVStoreUtils.getFileUrlAndVerifyHash( + chainId, + address, + JWT_KVSTORE_KEY, + ); + publicKey = (await StorageClient.downloadFileFromUrl(url)) as string; + } catch (e) { + if (e.toString().includes('Error: Invalid address')) { + throw new HttpException( + `Unable to retrieve public key from address: ${address}`, + 400, + ); + } else { + throw new Error(`Error while fetching public key from kv-store: ${e}`); + } + } + + if (!publicKey || publicKey === '') { + throw new HttpException( + `Unable to retrieve public key from address: ${address}`, + 400, + ); + } else { + // Guardar en caché sin TTL (persistente) + await this.cacheManager.set(key, publicKey, 0); + return publicKey; + } + } } diff --git a/packages/apps/human-app/server/src/modules/abuse/abuse.controller.ts b/packages/apps/human-app/server/src/modules/abuse/abuse.controller.ts index a2f336a3f6..3af5366983 100644 --- a/packages/apps/human-app/server/src/modules/abuse/abuse.controller.ts +++ b/packages/apps/human-app/server/src/modules/abuse/abuse.controller.ts @@ -1,25 +1,18 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { - Body, - Controller, - Get, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; +import { Body, Controller, Get, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags, } from '@nestjs/swagger'; -import { Authorization } from '../../common/config/params-decorators'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { AbuseService } from './abuse.service'; import { - ReportedAbuseResponse, ReportAbuseCommand, ReportAbuseDto, + ReportedAbuseResponse, } from './model/abuse.model'; @ApiBearerAuth() @@ -39,17 +32,16 @@ export class AbuseController { status: 200, description: 'Abuse report successfully submitted', }) - @UsePipes(new ValidationPipe()) public async reportAbuse( @Body() AbuseDto: ReportAbuseDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const AbuseCommand = this.mapper.map( AbuseDto, ReportAbuseDto, ReportAbuseCommand, ); - AbuseCommand.token = token; + AbuseCommand.token = req.token; return this.service.reportAbuse(AbuseCommand); } @@ -63,8 +55,8 @@ export class AbuseController { type: ReportedAbuseResponse, }) public async getUserAbuseReports( - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { - return this.service.getUserAbuseReports(token); + return this.service.getUserAbuseReports(req.token); } } diff --git a/packages/apps/human-app/server/src/modules/abuse/spec/abuse.controller.spec.ts b/packages/apps/human-app/server/src/modules/abuse/spec/abuse.controller.spec.ts index 29ced5ac79..2a97e4970b 100644 --- a/packages/apps/human-app/server/src/modules/abuse/spec/abuse.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/abuse/spec/abuse.controller.spec.ts @@ -12,6 +12,7 @@ import { TOKEN, } from './abuse.fixtures'; import { AbuseProfile } from '../abuse.mapper.profile'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; describe('AbuseController', () => { let controller: AbuseController; @@ -42,7 +43,9 @@ describe('AbuseController', () => { const dto = reportAbuseDtoFixture; const command = reportAbuseCommandFixture; - await controller.reportAbuse(dto, TOKEN); + await controller.reportAbuse(dto, { + token: TOKEN, + } as RequestWithUser); expect(abuseServiceMock.reportAbuse).toHaveBeenCalledWith(command); }); @@ -56,7 +59,9 @@ describe('AbuseController', () => { reportedAbuseResponseFixture, ); - const result = await controller.getUserAbuseReports(token); + const result = await controller.getUserAbuseReports({ + token: TOKEN, + } as RequestWithUser); expect(abuseServiceMock.getUserAbuseReports).toHaveBeenCalledWith(token); expect(result).toEqual(reportedAbuseResponseFixture); diff --git a/packages/apps/human-app/server/src/modules/email-confirmation/email-confirmation.controller.ts b/packages/apps/human-app/server/src/modules/email-confirmation/email-confirmation.controller.ts index d3ef5eb0f7..ca6367b918 100644 --- a/packages/apps/human-app/server/src/modules/email-confirmation/email-confirmation.controller.ts +++ b/packages/apps/human-app/server/src/modules/email-confirmation/email-confirmation.controller.ts @@ -1,14 +1,10 @@ -import { - Body, - Controller, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; -import { EmailConfirmationService } from './email-confirmation.service'; -import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Public } from '../../common/decorators'; +import { RequestWithUser } from '../../common/interfaces/jwt'; +import { EmailConfirmationService } from './email-confirmation.service'; import { EmailVerificationCommand, EmailVerificationDto, @@ -17,8 +13,8 @@ import { ResendEmailVerificationCommand, ResendEmailVerificationDto, } from './model/resend-email-verification.model'; -import { Authorization } from '../../common/config/params-decorators'; +@ApiTags('Email-Confirmation') @Controller('/email-confirmation') export class EmailConfirmationController { constructor( @@ -26,12 +22,11 @@ export class EmailConfirmationController { @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('Email-Confirmation') - @Post('/email-verification') @ApiOperation({ summary: 'Endpoint to verify the user email address', }) - @UsePipes(new ValidationPipe()) + @Public() + @Post('/email-verification') public async verifyEmail( @Body() emailVerificationDto: EmailVerificationDto, ): Promise { @@ -43,23 +38,21 @@ export class EmailConfirmationController { return this.service.processEmailVerification(emailVerificationCommand); } - @ApiTags('Email-Confirmation') @Post('/resend-email-verification') @ApiOperation({ summary: 'Endpoint to resend the email verification link', }) @ApiBearerAuth() - @UsePipes(new ValidationPipe()) public async resendEmailVerification( @Body() resendEmailVerificationDto: ResendEmailVerificationDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const resendEmailVerificationCommand = this.mapper.map( resendEmailVerificationDto, ResendEmailVerificationDto, ResendEmailVerificationCommand, ); - resendEmailVerificationCommand.token = token; + resendEmailVerificationCommand.token = req.token; return this.service.processResendEmailVerification( resendEmailVerificationCommand, ); diff --git a/packages/apps/human-app/server/src/modules/email-confirmation/spec/email-verification.controller.spec.ts b/packages/apps/human-app/server/src/modules/email-confirmation/spec/email-verification.controller.spec.ts index d8c359c2de..8a00a7a1fe 100644 --- a/packages/apps/human-app/server/src/modules/email-confirmation/spec/email-verification.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/email-confirmation/spec/email-verification.controller.spec.ts @@ -17,6 +17,7 @@ import { emailVerificationCommandFixture, emailVerificationDtoFixture, } from './email-verification.fixtures'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; describe('EmailConfirmationController', () => { let controller: EmailConfirmationController; @@ -62,7 +63,9 @@ describe('EmailConfirmationController', () => { it('should call the processResendEmailVerification method of the service with the correct arguments', async () => { const dto = resendEmailVerificationDtoFixture; const command = resendEmailVerificationCommandFixture; - await controller.resendEmailVerification(dto, emailVerificationToken); + await controller.resendEmailVerification(dto, { + token: emailVerificationToken, + } as RequestWithUser); expect(service.processResendEmailVerification).toHaveBeenCalledWith( command, ); diff --git a/packages/apps/human-app/server/src/modules/h-captcha/h-captcha.controller.ts b/packages/apps/human-app/server/src/modules/h-captcha/h-captcha.controller.ts index 7624c5d2eb..74c41e8ca0 100644 --- a/packages/apps/human-app/server/src/modules/h-captcha/h-captcha.controller.ts +++ b/packages/apps/human-app/server/src/modules/h-captcha/h-captcha.controller.ts @@ -1,98 +1,85 @@ -import { - Body, - Controller, - Get, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; -import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Get, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { RequestWithUser } from '../../common/interfaces/jwt'; +import { JwtUserData } from '../../common/utils/jwt-token.model'; import { HCaptchaService } from './h-captcha.service'; -import { - VerifyTokenCommand, - VerifyTokenDto, - VerifyTokenResponse, -} from './model/verify-token.model'; import { DailyHmtSpentCommand, DailyHmtSpentResponse, } from './model/daily-hmt-spent.model'; -import { - Authorization, - JwtPayload, -} from '../../common/config/params-decorators'; -import { JwtUserData } from '../../common/utils/jwt-token.model'; import { EnableLabelingCommand, EnableLabelingResponse, } from './model/enable-labeling.model'; import { UserStatsCommand, UserStatsResponse } from './model/user-stats.model'; +import { + VerifyTokenCommand, + VerifyTokenDto, + VerifyTokenResponse, +} from './model/verify-token.model'; +@ApiTags('h-captcha') +@ApiBearerAuth() @Controller('/labeling/h-captcha') export class HCaptchaController { constructor( private readonly service: HCaptchaService, @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('h-captcha') + @Post('/enable') @ApiOperation({ summary: 'Enables h-captcha labeling' }) - @ApiBearerAuth() - @UsePipes(new ValidationPipe()) public async enableLabeling( - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const command = { - token: token, + token: req.token, } as EnableLabelingCommand; return this.service.enableLabeling(command); } - @ApiTags('h-captcha') @Post('/verify') @ApiOperation({ summary: 'Sends solution for verification' }) - @ApiBearerAuth() - @UsePipes(new ValidationPipe()) public async verifyToken( @Body() dto: VerifyTokenDto, - @JwtPayload() jwtPayload: JwtUserData, - @Authorization() jwtToken: string, + @Request() req: RequestWithUser, ): Promise { - const command = this.mapper.map( - jwtPayload, - JwtUserData, - VerifyTokenCommand, - ); + if (!req.user.site_key) { + throw new Error('Missing site key'); + } + const command = this.mapper.map(req.user, JwtUserData, VerifyTokenCommand); command.response = dto.token; - command.jwtToken = jwtToken; + command.jwtToken = req.token; return await this.service.verifyToken(command); } - @ApiTags('h-captcha') + @Get('/daily-hmt-spent') - @ApiBearerAuth() @ApiOperation({ summary: 'Gets global daily HMT spent' }) - @UsePipes(new ValidationPipe()) public async getDailyHmtSpent( - @JwtPayload() jwtPayload: JwtUserData, + @Request() req: RequestWithUser, ): Promise { + if (!req.user.site_key) { + throw new Error('Missing site key'); + } const command = this.mapper.map( - jwtPayload, + req.user, JwtUserData, DailyHmtSpentCommand, ); return this.service.getDailyHmtSpent(command); } - @ApiTags('h-captcha') + @Get('/user-stats') - @ApiBearerAuth() @ApiOperation({ summary: 'Gets stats per user' }) - @UsePipes(new ValidationPipe()) public async getUserStats( - @JwtPayload() jwtPayload: JwtUserData, + @Request() req: RequestWithUser, ): Promise { - const command = this.mapper.map(jwtPayload, JwtUserData, UserStatsCommand); + if (!req.user.email || !req.user.site_key) { + throw new Error('Missing email or site key'); + } + const command = this.mapper.map(req.user, JwtUserData, UserStatsCommand); return this.service.getUserStats(command); } } diff --git a/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.controller.spec.ts b/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.controller.spec.ts index 1fef61d5b4..6ccb55a9cf 100644 --- a/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.controller.spec.ts @@ -14,6 +14,7 @@ import { verifyTokenCommandFixture, verifyTokenDtoFixture, } from './h-captcha.fixtures'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; describe('HCaptchaController', () => { let controller: HCaptchaController; @@ -41,7 +42,7 @@ describe('HCaptchaController', () => { expect(controller).toBeDefined(); }); it('should call getUserStats with proper arguments', async () => { - const dto = jwtUserDataFixture; + const dto = { user: jwtUserDataFixture } as RequestWithUser; const command = hCaptchaUserStatsCommandFixture; await controller.getUserStats(dto); expect(service.getUserStats).toHaveBeenCalledWith(command); @@ -49,14 +50,20 @@ describe('HCaptchaController', () => { it('should call verifyToken with proper arguments', async () => { const dto = verifyTokenDtoFixture; - const jwtPayload = jwtUserDataFixture; + const jwtPayload = { + user: jwtUserDataFixture, + token: JWT_TOKEN, + } as RequestWithUser; const command = verifyTokenCommandFixture; - await controller.verifyToken(dto, jwtPayload, JWT_TOKEN); + await controller.verifyToken(dto, jwtPayload); expect(service.verifyToken).toHaveBeenCalledWith(command); }); it('should call getDailyHmtSpent with proper arguments', async () => { - const dto = jwtUserDataFixture; + const dto = { + user: jwtUserDataFixture, + token: JWT_TOKEN, + } as RequestWithUser; const command = dailyHmtSpentCommandFixture; await controller.getDailyHmtSpent(dto); expect(service.getDailyHmtSpent).toHaveBeenCalledWith(command); @@ -64,7 +71,9 @@ describe('HCaptchaController', () => { it('should call enableLabeling with proper arguments', async () => { const command = enableLabelingCommandFixture; - await controller.enableLabeling(JWT_TOKEN); + await controller.enableLabeling({ + token: JWT_TOKEN, + } as RequestWithUser); expect(service.enableLabeling).toHaveBeenCalledWith(command); }); }); diff --git a/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.fixtures.ts b/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.fixtures.ts index 5696a6d2cd..d114d7c6b1 100644 --- a/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.fixtures.ts +++ b/packages/apps/human-app/server/src/modules/h-captcha/spec/h-captcha.fixtures.ts @@ -19,13 +19,12 @@ import { EnableLabelingCommand, EnableLabelingResponse, } from '../model/enable-labeling.model'; +const STATUS = 'active'; const EMAIL = 'some_email@example.com'; const ID = 'jwt_token_id'; const H_CAPTCHA_SITE_KEY = 'some_h_captcha_site_key'; const TOKEN_TO_VERIFY = 'some_hcaptcha_token'; const REPUTATION_NETWORK = 'some_reputation_network_address'; -const IAT = 2137; -const EXP = 7312; const POLYGON_WALLET_ADDR = '0xAf6E2cB084314Fbe50228e697d2B1b8553DDEd25'; const DAILY_HMT_SPENT = 100; const SOLVED = 10; @@ -73,12 +72,10 @@ export const jwtUserDataFixture: JwtUserData = { user_id: ID, wallet_address: POLYGON_WALLET_ADDR, email: EMAIL, - kyc_status: 'approved', + status: STATUS, qualifications: [], site_key: H_CAPTCHA_SITE_KEY, reputation_network: REPUTATION_NETWORK, - iat: IAT, - exp: EXP, }; export const hCaptchaUserStatsCommandFixture: UserStatsCommand = { diff --git a/packages/apps/human-app/server/src/modules/health/health.controller.ts b/packages/apps/human-app/server/src/modules/health/health.controller.ts index 96f3837db7..48d8dab2f7 100644 --- a/packages/apps/human-app/server/src/modules/health/health.controller.ts +++ b/packages/apps/human-app/server/src/modules/health/health.controller.ts @@ -10,10 +10,12 @@ import { } from '@nestjs/terminus'; import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { Public } from '../../common/decorators'; import { PingResponseDto } from './dto/ping-response.dto'; import { CacheManagerHealthIndicator } from './indicators/cache-manager.health'; @ApiTags('Health') +@Public() @Controller('health') export class HealthController { constructor( diff --git a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts index bb4caa6a8d..72c1a2d3e3 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/job-assignment.controller.ts @@ -1,3 +1,5 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; import { Body, Controller, @@ -5,27 +7,25 @@ import { Post, Put, Query, - UsePipes, - ValidationPipe, + Request, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { JobAssignmentService } from './job-assignment.service'; import { - JobAssignmentDto, JobAssignmentCommand, + JobAssignmentDto, JobAssignmentResponse, - JobsFetchParamsDto, JobsFetchParamsCommand, + JobsFetchParamsDto, JobsFetchResponse, - ResignJobDto, - ResignJobCommand, RefreshJobDto, + ResignJobCommand, + ResignJobDto, } from './model/job-assignment.model'; -import { Authorization } from '../../common/config/params-decorators'; @ApiTags('Job-Assignment') +@ApiBearerAuth() @Controller('/assignment') export class JobAssignmentController { constructor( @@ -37,66 +37,61 @@ export class JobAssignmentController { @ApiOperation({ summary: 'Request to assign a job to a logged user', }) - @ApiBearerAuth() - @UsePipes(new ValidationPipe()) public async assignJob( @Body() jobAssignmentDto: JobAssignmentDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const jobAssignmentCommand = this.mapper.map( jobAssignmentDto, JobAssignmentDto, JobAssignmentCommand, ); - jobAssignmentCommand.token = token; + jobAssignmentCommand.token = req.token; return this.service.processJobAssignment(jobAssignmentCommand); } @Get('/job') - @ApiBearerAuth() @ApiOperation({ summary: 'Request to get jobs assigned to a logged user', }) public async getAssignedJobs( @Query() jobsAssignmentParamsDto: JobsFetchParamsDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const jobsAssignmentParamsCommand = this.mapper.map( jobsAssignmentParamsDto, JobsFetchParamsDto, JobsFetchParamsCommand, ); - jobsAssignmentParamsCommand.token = token; + jobsAssignmentParamsCommand.token = req.token; return this.service.processGetAssignedJobs(jobsAssignmentParamsCommand); } @Post('/resign-job') - @ApiBearerAuth() @ApiOperation({ summary: 'Request to resign from assigment', }) public async resignAssigment( @Body() dto: ResignJobDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ) { const command = this.mapper.map(dto, ResignJobDto, ResignJobCommand); - command.token = token; + command.token = req.token; return this.service.resignJob(command); } @Put('/refresh') - @ApiBearerAuth() @ApiOperation({ summary: 'Request to refresh assigments data', }) public async refreshAssigments( @Body() dto: RefreshJobDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ) { const command = new JobsFetchParamsCommand(); command.oracleAddress = dto.oracle_address; - command.token = token; + command.token = req.token; return this.service.updateAssignmentsCache(command); } } diff --git a/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts b/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts index 313ab9e14b..ae69ce4e12 100644 --- a/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/job-assignment/spec/job-assignment.controller.spec.ts @@ -1,7 +1,11 @@ -import { JobAssignmentService } from '../job-assignment.service'; -import { JobAssignmentController } from '../job-assignment.controller'; +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { jobAssignmentServiceMock } from './job-assignment.service.mock'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; +import { JobAssignmentController } from '../job-assignment.controller'; +import { JobAssignmentProfile } from '../job-assignment.mapper.profile'; +import { JobAssignmentService } from '../job-assignment.service'; import { JobAssignmentCommand, JobAssignmentDto, @@ -10,21 +14,18 @@ import { RefreshJobDto, } from '../model/job-assignment.model'; import { - jobAssignmentDtoFixture, + EXCHANGE_ORACLE_ADDRESS, jobAssignmentCommandFixture, + jobAssignmentDtoFixture, jobAssignmentResponseFixture, - jobsFetchParamsDtoFixture, + jobAssignmentToken, jobsFetchParamsCommandFixture, + jobsFetchParamsDtoFixture, jobsFetchResponseFixture, - jobAssignmentToken, refreshJobDtoFixture, - EXCHANGE_ORACLE_ADDRESS, TOKEN, } from './job-assignment.fixtures'; -import { AutomapperModule } from '@automapper/nestjs'; -import { classes } from '@automapper/classes'; -import { JobAssignmentProfile } from '../job-assignment.mapper.profile'; -import { HttpService } from '@nestjs/axios'; +import { jobAssignmentServiceMock } from './job-assignment.service.mock'; const httpServiceMock = { request: jest.fn().mockImplementation((options) => { @@ -74,7 +75,9 @@ describe('JobAssignmentController', () => { it('should call service processJobAssignment method with proper fields set', async () => { const dto: JobAssignmentDto = jobAssignmentDtoFixture; const command: JobAssignmentCommand = jobAssignmentCommandFixture; - await controller.assignJob(dto, jobAssignmentToken); + await controller.assignJob(dto, { + token: jobAssignmentToken, + } as RequestWithUser); expect(jobAssignmentService.processJobAssignment).toHaveBeenCalledWith( command, ); @@ -83,7 +86,9 @@ describe('JobAssignmentController', () => { it('should return the result of service processJobAssignment method', async () => { const dto: JobAssignmentDto = jobAssignmentDtoFixture; const command: JobAssignmentCommand = jobAssignmentCommandFixture; - const result = await controller.assignJob(dto, jobAssignmentToken); + const result = await controller.assignJob(dto, { + token: jobAssignmentToken, + } as RequestWithUser); expect(result).toEqual( jobAssignmentServiceMock.processJobAssignment(command), ); @@ -92,7 +97,9 @@ describe('JobAssignmentController', () => { it('should call service processGetAssignedJobs method with proper fields set', async () => { const dto: JobsFetchParamsDto = jobsFetchParamsDtoFixture; const command: JobsFetchParamsCommand = jobsFetchParamsCommandFixture; - await controller.getAssignedJobs(dto, jobAssignmentToken); + await controller.getAssignedJobs(dto, { + token: jobAssignmentToken, + } as RequestWithUser); expect(jobAssignmentService.processGetAssignedJobs).toHaveBeenCalledWith( command, ); @@ -100,7 +107,9 @@ describe('JobAssignmentController', () => { it('should call service refreshAssigments method with proper fields set', async () => { const dto: RefreshJobDto = refreshJobDtoFixture; - await controller.refreshAssigments(dto, jobAssignmentToken); + await controller.refreshAssigments(dto, { + token: jobAssignmentToken, + } as RequestWithUser); expect(jobAssignmentService.updateAssignmentsCache).toHaveBeenCalledWith({ oracleAddress: EXCHANGE_ORACLE_ADDRESS, token: TOKEN, diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts index d74d817285..75a951eb8b 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/jobs-discovery.controller.ts @@ -1,9 +1,12 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; import { Controller, Get, HttpException, HttpStatus, Query, + Request, } from '@nestjs/common'; import { ApiBearerAuth, @@ -11,22 +14,17 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; +import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { JobsDiscoveryService } from './jobs-discovery.service'; import { JobsDiscoveryParamsCommand, JobsDiscoveryParamsDto, JobsDiscoveryResponse, } from './model/jobs-discovery.model'; -import { - Authorization, - JwtPayload, -} from '../../common/config/params-decorators'; -import { JwtUserData } from '../../common/utils/jwt-token.model'; -import { EnvironmentConfigService } from '../../common/config/environment-config.service'; @Controller() +@ApiBearerAuth() @ApiTags('Jobs-Discovery') export class JobsDiscoveryController { constructor( @@ -36,15 +34,13 @@ export class JobsDiscoveryController { ) {} @Get('/jobs') - @ApiBearerAuth() @ApiOperation({ summary: 'Retrieve a list of jobs for given Exchange Oracle', }) @ApiOkResponse({ type: JobsDiscoveryResponse, description: 'List of jobs' }) public async getJobs( @Query() jobsDiscoveryParamsDto: JobsDiscoveryParamsDto, - @JwtPayload() jwtPayload: JwtUserData, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { if (!this.environmentConfigService.jobsDiscoveryFlag) { throw new HttpException( @@ -58,8 +54,8 @@ export class JobsDiscoveryController { JobsDiscoveryParamsDto, JobsDiscoveryParamsCommand, ); - jobsDiscoveryParamsCommand.token = token; - jobsDiscoveryParamsCommand.data.qualifications = jwtPayload.qualifications; + jobsDiscoveryParamsCommand.token = req.token; + jobsDiscoveryParamsCommand.data.qualifications = req.user.qualifications; return await this.service.processJobsDiscovery(jobsDiscoveryParamsCommand); } } diff --git a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts index 87a0ddfc7e..fb4e14b4bf 100644 --- a/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/jobs-discovery/spec/jobs-discovery.controller.spec.ts @@ -1,22 +1,21 @@ -import { JobsDiscoveryService } from '../jobs-discovery.service'; -import { JobsDiscoveryController } from '../jobs-discovery.controller'; +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { ChainId } from '@human-protocol/sdk'; +import { HttpService } from '@nestjs/axios'; +import { HttpException, HttpStatus } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { jobsDiscoveryServiceMock } from './jobs-discovery.service.mock'; +import { CommonConfigModule } from '../../../common/config/common-config.module'; +import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; +import { JobsDiscoveryController } from '../jobs-discovery.controller'; +import { JobsDiscoveryProfile } from '../jobs-discovery.mapper.profile'; +import { JobsDiscoveryService } from '../jobs-discovery.service'; import { - jobsDiscoveryParamsCommandFixture, dtoFixture, - jobDiscoveryToken, + jobsDiscoveryParamsCommandFixture, responseFixture, } from './jobs-discovery.fixtures'; -import { AutomapperModule } from '@automapper/nestjs'; -import { classes } from '@automapper/classes'; -import { JobsDiscoveryProfile } from '../jobs-discovery.mapper.profile'; -import { HttpService } from '@nestjs/axios'; -import { CommonConfigModule } from '../../../common/config/common-config.module'; -import { ConfigModule } from '@nestjs/config'; -import { EnvironmentConfigService } from '../../../common/config/environment-config.service'; -import { HttpException, HttpStatus } from '@nestjs/common'; -import { ChainId } from '@human-protocol/sdk'; +import { jobsDiscoveryServiceMock } from './jobs-discovery.service.mock'; describe('JobsDiscoveryController', () => { let controller: JobsDiscoveryController; @@ -73,11 +72,10 @@ describe('JobsDiscoveryController', () => { it('should call service processJobsDiscovery method with proper fields set', async () => { const dto = dtoFixture; const command = jobsDiscoveryParamsCommandFixture; - await controller.getJobs( - dto, - { qualifications: [] } as any, - jobDiscoveryToken, - ); + await controller.getJobs(dto, { + user: { qualifications: [] }, + token: command.token, + } as any); command.data.qualifications = []; expect(jobsDiscoveryService.processJobsDiscovery).toHaveBeenCalledWith( command, @@ -88,11 +86,7 @@ describe('JobsDiscoveryController', () => { const dto = dtoFixture; (configServiceMock as any).jobsDiscoveryFlag = false; await expect( - controller.getJobs( - dto, - { qualifications: [] } as any, - jobDiscoveryToken, - ), + controller.getJobs(dto, { user: { qualifications: [] } } as any), ).rejects.toThrow( new HttpException('Jobs discovery is disabled', HttpStatus.FORBIDDEN), ); diff --git a/packages/apps/human-app/server/src/modules/kyc-procedure/kyc-procedure.controller.ts b/packages/apps/human-app/server/src/modules/kyc-procedure/kyc-procedure.controller.ts index a5e721fb47..f763113987 100644 --- a/packages/apps/human-app/server/src/modules/kyc-procedure/kyc-procedure.controller.ts +++ b/packages/apps/human-app/server/src/modules/kyc-procedure/kyc-procedure.controller.ts @@ -1,31 +1,30 @@ -import { Controller, Get, Post } from '@nestjs/common'; -import { KycProcedureService } from './kyc-procedure.service'; +import { Controller, Get, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { RequestWithUser } from '../../common/interfaces/jwt'; +import { KycProcedureService } from './kyc-procedure.service'; import { KycProcedureStartResponse } from './model/kyc-start.model'; -import { Authorization } from '../../common/config/params-decorators'; +@ApiTags('Kyc-Procedure') +@ApiBearerAuth() @Controller('/kyc') export class KycProcedureController { constructor(private readonly service: KycProcedureService) {} - @ApiTags('Kyc-Procedure') @Post('/start') - @ApiBearerAuth() @ApiOperation({ summary: 'Endpoint to start Kyc process for the user', }) public async startKycProcedure( - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { - return this.service.processStartKycProcedure(token); + return this.service.processStartKycProcedure(req.token); } - @ApiTags('Kyc-Procedure') + @Get('/on-chain') - @ApiBearerAuth() @ApiOperation({ summary: 'Endpoint to get a signed address for the KYC process.', }) - public async onChainKyc(@Authorization() token: string): Promise { - return this.service.processKycOnChain(token); + public async onChainKyc(@Request() req: RequestWithUser): Promise { + return this.service.processKycOnChain(req.token); } } diff --git a/packages/apps/human-app/server/src/modules/kyc-procedure/spec/kyc-procedure.controller.spec.ts b/packages/apps/human-app/server/src/modules/kyc-procedure/spec/kyc-procedure.controller.spec.ts index 49f57935b7..4ccf70859e 100644 --- a/packages/apps/human-app/server/src/modules/kyc-procedure/spec/kyc-procedure.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/kyc-procedure/spec/kyc-procedure.controller.spec.ts @@ -1,8 +1,9 @@ +import { expect, it, jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; import { KycProcedureController } from '../kyc-procedure.controller'; import { KycProcedureService } from '../kyc-procedure.service'; -import { Test, TestingModule } from '@nestjs/testing'; import { serviceMock } from './kyc-procedure.service.mock'; -import { expect, it, jest } from '@jest/globals'; describe('KycProcedureController', () => { let controller: KycProcedureController; @@ -30,12 +31,12 @@ describe('KycProcedureController', () => { service, 'processStartKycProcedure', ); - await controller.startKycProcedure('token'); + await controller.startKycProcedure({ token: 'token' } as RequestWithUser); expect(startKycProcedureSpy).toHaveBeenCalledWith('token'); }); it('should call processKycOnChain method of KycProcedureService', async () => { const kycService = jest.spyOn(service, 'processKycOnChain'); - await controller.onChainKyc('token'); + await controller.onChainKyc({ token: 'token' } as RequestWithUser); expect(kycService).toHaveBeenCalledWith('token'); }); }); diff --git a/packages/apps/human-app/server/src/modules/nda/nda.controller.ts b/packages/apps/human-app/server/src/modules/nda/nda.controller.ts index ed886df9b1..f91a015ee4 100644 --- a/packages/apps/human-app/server/src/modules/nda/nda.controller.ts +++ b/packages/apps/human-app/server/src/modules/nda/nda.controller.ts @@ -1,22 +1,13 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { - Body, - Controller, - Get, - HttpCode, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Authorization } from '../../common/config/params-decorators'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { GetNDACommand, SignNDACommand, SignNDADto } from './model/nda.model'; import { NDAService } from './nda.service'; @Controller('/nda') @ApiTags('NDA') -@UsePipes(new ValidationPipe()) @ApiBearerAuth() export class NDAController { @InjectMapper() private readonly mapper: Mapper; @@ -34,9 +25,9 @@ export class NDAController { 'Retrieves the latest NDA URL that users must sign to join the oracle', }) @Get('/') - async getLatestNDA(@Authorization() token: string) { + async getLatestNDA(@Request() req: RequestWithUser) { const command = new GetNDACommand(); - command.token = token; + command.token = req.token; return this.ndaService.getLatestNDA(command); } @@ -47,9 +38,9 @@ export class NDAController { }) @HttpCode(200) @Post('sign') - async signNDA(@Body() dto: SignNDADto, @Authorization() token: string) { + async signNDA(@Body() dto: SignNDADto, @Request() req: RequestWithUser) { const command = this.mapper.map(dto, SignNDADto, SignNDACommand); - command.token = token; + command.token = req.token; await this.ndaService.signNDA(command); return { message: 'NDA signed successfully' }; } diff --git a/packages/apps/human-app/server/src/modules/nda/spec/nda.controller.spec.ts b/packages/apps/human-app/server/src/modules/nda/spec/nda.controller.spec.ts index 333e9c79ca..8e9ebf5685 100644 --- a/packages/apps/human-app/server/src/modules/nda/spec/nda.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/nda/spec/nda.controller.spec.ts @@ -1,16 +1,17 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; import { NDAController } from '../nda.controller'; +import { SignNDAProfile } from '../nda.mapper.profile'; import { NDAService } from '../nda.service'; -import { ndaServiceMock } from './nda.service.mock'; import { NDA_TOKEN, signNDACommandFixture, signNDADtoFixture, } from './nda.fixtures'; -import { SignNDAProfile } from '../nda.mapper.profile'; +import { ndaServiceMock } from './nda.service.mock'; describe('NDAController', () => { let controller: NDAController; @@ -40,13 +41,13 @@ describe('NDAController', () => { it('should call service signNDA method with proper fields set', async () => { const dto = signNDADtoFixture; const command = signNDACommandFixture; - await controller.signNDA(dto, NDA_TOKEN); + await controller.signNDA(dto, { token: NDA_TOKEN } as RequestWithUser); expect(ndaServiceMock.signNDA).toHaveBeenCalledWith(command); }); it('should call service getLatestNDA method with proper fields set', async () => { const token = NDA_TOKEN; - await controller.getLatestNDA(token); + await controller.getLatestNDA({ token: NDA_TOKEN } as RequestWithUser); expect(ndaServiceMock.getLatestNDA).toHaveBeenCalledWith({ token }); }); }); diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts index 2e315ad0b1..5b062b6b8f 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts @@ -1,30 +1,24 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; import { Controller, Get, HttpException, HttpStatus, Query, - UsePipes, - ValidationPipe, + Request, } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { - ApiBearerAuth, - ApiOkResponse, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; -import { OracleDiscoveryService } from './oracle-discovery.service'; -import { + DiscoveredOracle, GetOraclesCommand, GetOraclesQuery, - DiscoveredOracle, } from './model/oracle-discovery.model'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; -import { EnvironmentConfigService } from '../../common/config/environment-config.service'; -import { JwtPayload } from '../../common/config/params-decorators'; -import { JwtUserData } from '../../common/utils/jwt-token.model'; +import { OracleDiscoveryService } from './oracle-discovery.service'; +@ApiTags('Oracle-Discovery') @Controller() export class OracleDiscoveryController { constructor( @@ -33,17 +27,14 @@ export class OracleDiscoveryController { @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('Oracle-Discovery') - @ApiBearerAuth() @Get('/oracles') @ApiOperation({ summary: 'Oracles discovery' }) @ApiOkResponse({ type: Array, description: 'List of oracles', }) - @UsePipes(new ValidationPipe()) public async getOracles( - @JwtPayload() jwtPayload: JwtUserData, + @Request() req: RequestWithUser, @Query() query: GetOraclesQuery, ): Promise { if (!this.environmentConfigService.jobsDiscoveryFlag) { @@ -55,8 +46,9 @@ export class OracleDiscoveryController { const command = this.mapper.map(query, GetOraclesQuery, GetOraclesCommand); const oracles = await this.oracleDiscoveryService.getOracles(command); - const isAudinoAvailableForUser = - jwtPayload.qualifications.includes('audino'); + const isAudinoAvailableForUser = (req?.user?.qualifications ?? []).includes( + 'audino', + ); /** * TODO: remove filtering logic when Audino available for everyone diff --git a/packages/apps/human-app/server/src/modules/password-reset/password-reset.controller.ts b/packages/apps/human-app/server/src/modules/password-reset/password-reset.controller.ts index f593d87ee7..80fcce0f5a 100644 --- a/packages/apps/human-app/server/src/modules/password-reset/password-reset.controller.ts +++ b/packages/apps/human-app/server/src/modules/password-reset/password-reset.controller.ts @@ -1,14 +1,8 @@ -import { - Body, - Controller, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; -import { PasswordResetService } from './password-reset.service'; -import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Post } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Public } from '../../common/decorators'; import { ForgotPasswordCommand, ForgotPasswordDto, @@ -17,7 +11,10 @@ import { RestorePasswordCommand, RestorePasswordDto, } from './model/restore-password.model'; +import { PasswordResetService } from './password-reset.service'; +@ApiTags('Password-Reset') +@Public() @Controller('/password-reset') export class PasswordResetController { constructor( @@ -25,12 +22,10 @@ export class PasswordResetController { @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('Password-Reset') @Post('/forgot-password') @ApiOperation({ summary: 'Endpoint to initiate the password reset process', }) - @UsePipes(new ValidationPipe()) public async forgotPassword( @Body() forgotPasswordDto: ForgotPasswordDto, ): Promise { @@ -42,12 +37,10 @@ export class PasswordResetController { return await this.service.processForgotPassword(forgotPasswordCommand); } - @ApiTags('Password-Reset') @Post('/restore-password') @ApiOperation({ summary: 'Endpoint to restore the user password after reset', }) - @UsePipes(new ValidationPipe()) public async restorePassword(@Body() dto: RestorePasswordDto): Promise { const command = this.mapper.map( dto, diff --git a/packages/apps/human-app/server/src/modules/prepare-signature/prepare-signature.controller.ts b/packages/apps/human-app/server/src/modules/prepare-signature/prepare-signature.controller.ts index 26fd177e9f..9e8bfbb76c 100644 --- a/packages/apps/human-app/server/src/modules/prepare-signature/prepare-signature.controller.ts +++ b/packages/apps/human-app/server/src/modules/prepare-signature/prepare-signature.controller.ts @@ -1,13 +1,8 @@ -import { - Body, - Controller, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; -import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Post } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Public } from '../../common/decorators'; import { PrepareSignatureCommand, PrepareSignatureDto, @@ -15,6 +10,8 @@ import { } from './model/prepare-signature.model'; import { PrepareSignatureService } from './prepare-signature.service'; +@ApiTags('Prepare-Signature') +@Public() @Controller('/prepare-signature') export class PrepareSignatureController { constructor( @@ -22,13 +19,11 @@ export class PrepareSignatureController { @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('Prepare-Signature') @Post('/') @ApiOperation({ summary: 'Endpoint for generating typed structured data objects compliant with EIP-712. The generated object should be convertible to a string format to ensure compatibility with signature mechanisms', }) - @UsePipes(new ValidationPipe()) public async prepareSignature( @Body() prepareSignatureDto: PrepareSignatureDto, ): Promise { diff --git a/packages/apps/human-app/server/src/modules/register-address/register-address.controller.ts b/packages/apps/human-app/server/src/modules/register-address/register-address.controller.ts index bb6f706f71..3a569e42df 100644 --- a/packages/apps/human-app/server/src/modules/register-address/register-address.controller.ts +++ b/packages/apps/human-app/server/src/modules/register-address/register-address.controller.ts @@ -1,21 +1,17 @@ -import { - Body, - Controller, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; +import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Authorization } from '../../common/config/params-decorators'; -import { RegisterAddressService } from './register-address.service'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { RegisterAddressCommand, RegisterAddressDto, RegisterAddressResponse, } from './model/register-address.model'; -import { Mapper } from '@automapper/core'; +import { RegisterAddressService } from './register-address.service'; +@ApiTags('Register Address') +@ApiBearerAuth() @Controller('/user/register-address') export class RegisterAddressController { @InjectMapper() private readonly mapper: Mapper; @@ -27,23 +23,20 @@ export class RegisterAddressController { this.mapper = mapper; } - @ApiTags('Register Address') @Post('/') @ApiOperation({ summary: 'Register Blockchain Address', }) - @ApiBearerAuth() - @UsePipes(new ValidationPipe()) public async registerAddress( @Body() dto: RegisterAddressDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const command = this.mapper.map( dto, RegisterAddressDto, RegisterAddressCommand, ); - command.token = token; + command.token = req.token; return this.service.registerBlockchainAddress(command); } } diff --git a/packages/apps/human-app/server/src/modules/register-address/spec/register-address.controller.spec.ts b/packages/apps/human-app/server/src/modules/register-address/spec/register-address.controller.spec.ts index 4748e24a25..c28bed1646 100644 --- a/packages/apps/human-app/server/src/modules/register-address/spec/register-address.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/register-address/spec/register-address.controller.spec.ts @@ -12,6 +12,7 @@ import { registerAddressResponseFixture, } from './register-address.fixtures'; import { RegisterAddressProfile } from '../register-address.mapper.profile'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; describe('RegisterAddressController', () => { let controller: RegisterAddressController; @@ -58,7 +59,9 @@ describe('RegisterAddressController', () => { it('should call service registerBlockchainAddress method with proper fields set', async () => { const dto = registerAddressDtoFixture; const command = registerAddressCommandFixture; - await controller.registerAddress(dto, REGISTER_ADDRESS_TOKEN); + await controller.registerAddress(dto, { + token: REGISTER_ADDRESS_TOKEN, + } as RequestWithUser); expect(service.registerBlockchainAddress).toHaveBeenCalledWith(command); }); }); diff --git a/packages/apps/human-app/server/src/modules/statistics/spec/statistics.controller.spec.ts b/packages/apps/human-app/server/src/modules/statistics/spec/statistics.controller.spec.ts index ea49d85f42..184ccd3e81 100644 --- a/packages/apps/human-app/server/src/modules/statistics/spec/statistics.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/statistics/spec/statistics.controller.spec.ts @@ -16,6 +16,7 @@ import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; import { StatisticsProfile } from '../statistics.mapper.profile'; import { jwtUserDataFixture } from '../../h-captcha/spec/h-captcha.fixtures'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; describe('StatisticsController', () => { let controller: StatisticsController; @@ -63,11 +64,10 @@ describe('StatisticsController', () => { const dto: UserStatisticsDto = { oracle_address: statisticsExchangeOracleAddress, }; - const result = await controller.getUserStatistics( - dto, - jwtUserDataFixture, - statisticsToken, - ); + const result = await controller.getUserStatistics(dto, { + user: jwtUserDataFixture, + token: statisticsToken, + } as RequestWithUser); expect(statisticsServiceMock.getUserStats).toHaveBeenCalledWith( generalUserStatsCommandFixture, diff --git a/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts b/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts index 2941ccb81c..e20631f483 100644 --- a/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts +++ b/packages/apps/human-app/server/src/modules/statistics/statistics.controller.ts @@ -1,12 +1,8 @@ -import { - Controller, - Get, - Query, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, Get, Query, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { StatisticsService } from './statistics.service'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { OracleStatisticsCommand, OracleStatisticsDto, @@ -17,24 +13,20 @@ import { UserStatisticsDto, UserStatisticsResponse, } from './model/user-statistics.model'; -import { - Authorization, - JwtPayload, -} from '../../common/config/params-decorators'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; -import { JwtUserData } from '../../common/utils/jwt-token.model'; +import { StatisticsService } from './statistics.service'; +import { Public } from '../../common/decorators'; +@ApiTags('Statistics') @Controller() export class StatisticsController { constructor( private readonly service: StatisticsService, @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('Statistics') - @Get('/stats') + @ApiOperation({ summary: 'General Oracle Statistics' }) - @UsePipes(new ValidationPipe()) + @Public() + @Get('/stats') public getOracleStatistics( @Query() dto: OracleStatisticsDto, ): Promise { @@ -47,22 +39,20 @@ export class StatisticsController { } @ApiTags('Statistics') - @Get('stats/assignment') @ApiOperation({ summary: 'Statistics for requesting user' }) @ApiBearerAuth() - @UsePipes(new ValidationPipe()) + @Get('stats/assignment') public getUserStatistics( @Query() dto: UserStatisticsDto, - @JwtPayload() payload: JwtUserData, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const command = this.mapper.map( dto, UserStatisticsDto, UserStatisticsCommand, ); - command.token = token; - command.walletAddress = payload.wallet_address; + command.token = req.token; + command.walletAddress = req.user.wallet_address; return this.service.getUserStats(command); } } diff --git a/packages/apps/human-app/server/src/modules/token-refresh/token-refresh.controller.ts b/packages/apps/human-app/server/src/modules/token-refresh/token-refresh.controller.ts index 5e87a77606..bb7892dd68 100644 --- a/packages/apps/human-app/server/src/modules/token-refresh/token-refresh.controller.ts +++ b/packages/apps/human-app/server/src/modules/token-refresh/token-refresh.controller.ts @@ -1,20 +1,17 @@ -import { - Body, - Controller, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; -import { TokenRefreshService } from './token-refresh.service'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Public } from '../../common/decorators'; import { TokenRefreshCommand, TokenRefreshDto, + TokenRefreshResponse, } from './model/token-refresh.model'; -import { TokenRefreshResponse } from './model/token-refresh.model'; +import { TokenRefreshService } from './token-refresh.service'; +@ApiTags('Refresh-Token') +@Public() @Controller() export class TokenRefreshController { constructor( @@ -22,10 +19,8 @@ export class TokenRefreshController { @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('Refresh-Token') @Post('/auth/refresh') @ApiOperation({ summary: 'Refresh token' }) - @UsePipes(new ValidationPipe()) public refreshToken( @Body() dto: TokenRefreshDto, ): Promise { diff --git a/packages/apps/human-app/server/src/modules/ui-configuration/ui-configuration.controller.ts b/packages/apps/human-app/server/src/modules/ui-configuration/ui-configuration.controller.ts index 8c419c5fcf..291bb66b9e 100644 --- a/packages/apps/human-app/server/src/modules/ui-configuration/ui-configuration.controller.ts +++ b/packages/apps/human-app/server/src/modules/ui-configuration/ui-configuration.controller.ts @@ -1,14 +1,17 @@ import { Controller, Get } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { EnvironmentConfigService } from '../../common/config/environment-config.service'; +import { Public } from '../../common/decorators'; import { UiConfigResponseDto } from './ui-configuration.dto'; @Controller() +@Public() @ApiTags('UI-Configuration') export class UiConfigurationController { constructor( private readonly environmentConfigService: EnvironmentConfigService, ) {} + @Get('/ui-config') @ApiOperation({ summary: 'Retrieve UI configuration' }) @ApiOkResponse({ diff --git a/packages/apps/human-app/server/src/modules/user-operator/operator.controller.ts b/packages/apps/human-app/server/src/modules/user-operator/operator.controller.ts index 6b3f62c246..d66c3c0550 100644 --- a/packages/apps/human-app/server/src/modules/user-operator/operator.controller.ts +++ b/packages/apps/human-app/server/src/modules/user-operator/operator.controller.ts @@ -1,18 +1,17 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { - Body, - Controller, - HttpCode, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; +import { Body, Controller, HttpCode, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; - -import { Authorization } from '../../common/config/params-decorators'; - -import { OperatorService } from './operator.service'; +import { Public } from '../../common/decorators'; +import { RequestWithUser } from '../../common/interfaces/jwt'; +import { + DisableOperatorCommand, + DisableOperatorDto, +} from './model/disable-operator.model'; +import { + EnableOperatorCommand, + EnableOperatorDto, +} from './model/enable-operator.model'; import { SignupOperatorCommand, SignupOperatorDto, @@ -23,26 +22,20 @@ import { SigninOperatorResponse, SignupOperatorResponse, } from './model/operator-signin.model'; -import { - DisableOperatorCommand, - DisableOperatorDto, -} from './model/disable-operator.model'; -import { - EnableOperatorCommand, - EnableOperatorDto, -} from './model/enable-operator.model'; +import { OperatorService } from './operator.service'; +@ApiTags('User-Operator') @Controller() export class OperatorController { constructor( private readonly service: OperatorService, @InjectMapper() private readonly mapper: Mapper, ) {} - @ApiTags('User-Operator') + @Post('/auth/web3/signup') @HttpCode(200) @ApiOperation({ summary: 'Operator signup' }) - @UsePipes(new ValidationPipe()) + @Public() async signupOperator( @Body() signupOperatorDto: SignupOperatorDto, ): Promise { @@ -54,11 +47,10 @@ export class OperatorController { return this.service.signupOperator(signupOperatorCommand); } - @ApiTags('User-Operator') @Post('/auth/web3/signin') @HttpCode(200) @ApiOperation({ summary: 'Operator signin' }) - @UsePipes(new ValidationPipe()) + @Public() async signinOperator( @Body() dto: SigninOperatorDto, ): Promise { @@ -70,45 +62,41 @@ export class OperatorController { return this.service.signinOperator(command); } - @ApiTags('User-Operator') @Post('/disable-operator') @HttpCode(200) @ApiOperation({ summary: 'Endpoint to disable an operator', }) @ApiBearerAuth() - @UsePipes(new ValidationPipe()) async disableOperator( @Body() disableOperatorDto: DisableOperatorDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const disableOperatorCommand = this.mapper.map( disableOperatorDto, DisableOperatorDto, DisableOperatorCommand, ); - disableOperatorCommand.token = token; + disableOperatorCommand.token = req.token; await this.service.disableOperator(disableOperatorCommand); } - @ApiTags('User-Operator') @Post('/enable-operator') @HttpCode(200) @ApiOperation({ summary: 'Endpoint to enable an operator', }) @ApiBearerAuth() - @UsePipes(new ValidationPipe()) async enable( @Body() enableOperatorDto: EnableOperatorDto, - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { const enableOperatorCommand = this.mapper.map( enableOperatorDto, EnableOperatorDto, EnableOperatorCommand, ); - enableOperatorCommand.token = token; + enableOperatorCommand.token = req.token; await this.service.enableOperator(enableOperatorCommand); } } diff --git a/packages/apps/human-app/server/src/modules/user-worker/spec/worker.controller.spec.ts b/packages/apps/human-app/server/src/modules/user-worker/spec/worker.controller.spec.ts index 1d2b71caab..6505d929ed 100644 --- a/packages/apps/human-app/server/src/modules/user-worker/spec/worker.controller.spec.ts +++ b/packages/apps/human-app/server/src/modules/user-worker/spec/worker.controller.spec.ts @@ -1,17 +1,18 @@ -import { WorkerController } from '../worker.controller'; -import { WorkerService } from '../worker.service'; +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RequestWithUser } from '../../../common/interfaces/jwt'; import { RegistrationInExchangeOracleDto, SignupWorkerCommand, SignupWorkerDto, } from '../model/worker-registration.model'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AutomapperModule } from '@automapper/nestjs'; -import { classes } from '@automapper/classes'; -import { WorkerProfile } from '../worker.mapper.profile'; -import { workerServiceMock } from './worker.service.mock'; import { SigninWorkerDto } from '../model/worker-signin.model'; +import { WorkerController } from '../worker.controller'; +import { WorkerProfile } from '../worker.mapper.profile'; +import { WorkerService } from '../worker.service'; import { workerToken } from './worker.fixtures'; +import { workerServiceMock } from './worker.service.mock'; describe('WorkerController', () => { let controller: WorkerController; @@ -79,7 +80,9 @@ describe('WorkerController', () => { oracle_address: '0x34df642', h_captcha_token: 'h_captcha_token', }; - await controller.createRegistrationInExchangeOracle(dto, workerToken); + await controller.createRegistrationInExchangeOracle(dto, { + token: workerToken, + } as RequestWithUser); const expectedCommand = { oracleAddress: dto.oracle_address, hCaptchaToken: dto.h_captcha_token, diff --git a/packages/apps/human-app/server/src/modules/user-worker/worker.controller.ts b/packages/apps/human-app/server/src/modules/user-worker/worker.controller.ts index 86ebfbebef..875c856451 100644 --- a/packages/apps/human-app/server/src/modules/user-worker/worker.controller.ts +++ b/packages/apps/human-app/server/src/modules/user-worker/worker.controller.ts @@ -1,19 +1,13 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { - Body, - Controller, - Get, - Post, - UsePipes, - ValidationPipe, -} from '@nestjs/common'; +import { Body, Controller, Get, Post, Request } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Authorization } from '../../common/config/params-decorators'; +import { Public } from '../../common/decorators'; +import { RequestWithUser } from '../../common/interfaces/jwt'; import { - RegistrationInExchangeOracleResponse, RegistrationInExchangeOracleCommand, RegistrationInExchangeOracleDto, + RegistrationInExchangeOracleResponse, RegistrationInExchangeOraclesResponse, SignupWorkerCommand, SignupWorkerDto, @@ -32,9 +26,10 @@ export class WorkerController { private readonly service: WorkerService, @InjectMapper() private readonly mapper: Mapper, ) {} + @Post('/auth/signup') @ApiOperation({ summary: 'Worker signup' }) - @UsePipes(new ValidationPipe()) + @Public() public signupWorker(@Body() signupWorkerDto: SignupWorkerDto): Promise { const signupWorkerCommand = this.mapper.map( signupWorkerDto, @@ -46,7 +41,7 @@ export class WorkerController { @Post('/auth/signin') @ApiOperation({ summary: 'Worker signin' }) - @UsePipes(new ValidationPipe()) + @Public() public signinWorker( @Body() signinWorkerDto: SigninWorkerDto, ): Promise { @@ -61,17 +56,17 @@ export class WorkerController { @ApiBearerAuth() @Post('/exchange-oracle-registration') @ApiOperation({ summary: 'Registers a worker in Exchange Oracle' }) - @UsePipes(new ValidationPipe()) public createRegistrationInExchangeOracle( @Body() registrationInExchangeOracleDto: RegistrationInExchangeOracleDto, - @Authorization() token: string, + + @Request() req: RequestWithUser, ): Promise { const registrationInExchangeOracle = this.mapper.map( registrationInExchangeOracleDto, RegistrationInExchangeOracleDto, RegistrationInExchangeOracleCommand, ); - registrationInExchangeOracle.token = token; + registrationInExchangeOracle.token = req.token; return this.service.registrationInExchangeOracle( registrationInExchangeOracle, @@ -81,10 +76,9 @@ export class WorkerController { @ApiBearerAuth() @Get('/exchange-oracle-registration') @ApiOperation({ summary: 'Retrieves oracles registered by the worker' }) - @UsePipes(new ValidationPipe()) public getRegistrationInExchangeOracles( - @Authorization() token: string, + @Request() req: RequestWithUser, ): Promise { - return this.service.getRegistrationInExchangeOracles(token); + return this.service.getRegistrationInExchangeOracles(req.token); } } From 6806b17348f9fb161f455dfb27332c03ce6c9b9a Mon Sep 17 00:00:00 2001 From: Nikolai Muhhin Date: Thu, 26 Jun 2025 09:58:21 +0300 Subject: [PATCH 03/16] Issue 3385: Extract stripe module (#3386) Co-authored-by: Nikolai Muhhin Co-authored-by: Nikolai Muhhin Co-authored-by: portuu3 <61605646+portuu3@users.noreply.github.com> --- .gitignore | 4 +- .../apps/job-launcher/server/.env.example | 9 +- .../server/src/common/config/config.module.ts | 6 +- .../server/src/common/config/env-schema.ts | 10 +- .../config/payment-provider-config.service.ts | 59 ++ .../common/config/stripe-config.service.ts | 50 - .../server/src/common/enums/payment.ts | 6 - ...9498615107-RenameStripeCustomerIdColumn.ts | 28 + .../src/modules/job/job.service.spec.ts | 2 +- .../server/src/modules/job/job.service.ts | 4 +- .../src/modules/payment/payment.interface.ts | 54 ++ .../src/modules/payment/payment.module.ts | 13 +- .../src/modules/payment/payment.repository.ts | 2 +- .../modules/payment/payment.service.spec.ts | 596 ++++-------- .../src/modules/payment/payment.service.ts | 328 ++----- .../providers/payment-provider.abstract.ts | 115 +++ .../payment/providers/stripe/fixtures.ts | 101 ++ .../providers/stripe/stripe.service.spec.ts | 918 ++++++++++++++++++ .../providers/stripe/stripe.service.ts | 427 ++++++++ .../server/src/modules/user/fixtures.ts | 2 +- .../server/src/modules/user/user.entity.ts | 2 +- .../webhook/webhook.controller.spec.ts | 12 +- .../job-launcher/server/test/constants.ts | 16 +- 23 files changed, 2030 insertions(+), 734 deletions(-) create mode 100644 packages/apps/job-launcher/server/src/common/config/payment-provider-config.service.ts delete mode 100644 packages/apps/job-launcher/server/src/common/config/stripe-config.service.ts create mode 100644 packages/apps/job-launcher/server/src/database/migrations/1749498615107-RenameStripeCustomerIdColumn.ts create mode 100644 packages/apps/job-launcher/server/src/modules/payment/providers/payment-provider.abstract.ts create mode 100644 packages/apps/job-launcher/server/src/modules/payment/providers/stripe/fixtures.ts create mode 100644 packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.spec.ts create mode 100644 packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.ts diff --git a/.gitignore b/.gitignore index 70399e1fe6..d9ac258354 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ yarn-error.log* # IDE - IntelliJ .idea/ +*.iml # OS .DS_Store @@ -48,4 +49,5 @@ dist hardhat-dependency-compiler # cache -cache \ No newline at end of file +cache + diff --git a/packages/apps/job-launcher/server/.env.example b/packages/apps/job-launcher/server/.env.example index 73585bf6e8..d892a45d20 100644 --- a/packages/apps/job-launcher/server/.env.example +++ b/packages/apps/job-launcher/server/.env.example @@ -83,10 +83,11 @@ HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000 # Stripe -STRIPE_SECRET_KEY=disabled -STRIPE_APP_NAME=Launcher Server Local -STRIPE_APP_VERSION=1.0.0 -STRIPE_APP_INFO_URL=http://local.app +PAYMENT_PROVIDER_SECRET_KEY=disabled +PAYMENT_PROVIDER_APP_NAME=Launcher Server Local +PAYMENT_PROVIDER_APP_VERSION=1.0.0 +PAYMENT_PROVIDER_APP_INFO_URL=http://local.app +PAYMENT_PROVIDER_API_VERSION=2022-11-15 # Sendgrid SENDGRID_API_KEY=sendgrid-disabled diff --git a/packages/apps/job-launcher/server/src/common/config/config.module.ts b/packages/apps/job-launcher/server/src/common/config/config.module.ts index 1f39191864..82692b8851 100644 --- a/packages/apps/job-launcher/server/src/common/config/config.module.ts +++ b/packages/apps/job-launcher/server/src/common/config/config.module.ts @@ -9,7 +9,7 @@ import { NetworkConfigService } from './network-config.service'; import { PGPConfigService } from './pgp-config.service'; import { S3ConfigService } from './s3-config.service'; import { SendgridConfigService } from './sendgrid-config.service'; -import { StripeConfigService } from './stripe-config.service'; +import { PaymentProviderConfigService } from './payment-provider-config.service'; import { Web3ConfigService } from './web3-config.service'; import { SlackConfigService } from './slack-config.service'; import { VisionConfigService } from './vision-config.service'; @@ -23,7 +23,7 @@ import { VisionConfigService } from './vision-config.service'; DatabaseConfigService, Web3ConfigService, S3ConfigService, - StripeConfigService, + PaymentProviderConfigService, SendgridConfigService, CvatConfigService, PGPConfigService, @@ -38,7 +38,7 @@ import { VisionConfigService } from './vision-config.service'; DatabaseConfigService, Web3ConfigService, S3ConfigService, - StripeConfigService, + PaymentProviderConfigService, SendgridConfigService, CvatConfigService, PGPConfigService, diff --git a/packages/apps/job-launcher/server/src/common/config/env-schema.ts b/packages/apps/job-launcher/server/src/common/config/env-schema.ts index 6cb4fa35e0..50e853d2dc 100644 --- a/packages/apps/job-launcher/server/src/common/config/env-schema.ts +++ b/packages/apps/job-launcher/server/src/common/config/env-schema.ts @@ -59,11 +59,11 @@ export const envValidator = Joi.object({ S3_BUCKET: Joi.string(), S3_USE_SSL: Joi.string(), // Stripe - STRIPE_SECRET_KEY: Joi.string().required(), - STRIPE_API_VERSION: Joi.string(), - STRIPE_APP_NAME: Joi.string(), - STRIPE_APP_VERSION: Joi.string(), - STRIPE_APP_INFO_URL: Joi.string(), + PAYMENT_PROVIDER_SECRET_KEY: Joi.string().required(), + PAYMENT_PROVIDER_API_VERSION: Joi.string(), + PAYMENT_PROVIDER_APP_NAME: Joi.string(), + PAYMENT_PROVIDER_APP_VERSION: Joi.string(), + PAYMENT_PROVIDER_APP_INFO_URL: Joi.string(), // SendGrid SENDGRID_API_KEY: Joi.string().required(), SENDGRID_FROM_EMAIL: Joi.string(), diff --git a/packages/apps/job-launcher/server/src/common/config/payment-provider-config.service.ts b/packages/apps/job-launcher/server/src/common/config/payment-provider-config.service.ts new file mode 100644 index 0000000000..c28a642d2e --- /dev/null +++ b/packages/apps/job-launcher/server/src/common/config/payment-provider-config.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class PaymentProviderConfigService { + constructor(private configService: ConfigService) {} + + /** + * The secret key used for authenticating requests to the payment providers API. + * Required + */ + get secretKey(): string { + return this.configService.getOrThrow('PAYMENT_PROVIDER_SECRET_KEY'); + } + + /** + * The version of the payment providers to use for requests. + * Default: '2022-11-15' + */ + get apiVersion(): string { + return this.configService.get( + 'PAYMENT_PROVIDER_API_VERSION', + '2022-11-15', + ); + } + + /** + * The name of the application interacting with the payment providers API. + * Default: 'Fortune' + */ + get appName(): string { + return this.configService.get( + 'PAYMENT_PROVIDER_APP_NAME', + 'Fortune', + ); + } + + /** + * The version of the application interacting with the payment providers API. + * Default: '0.0.1' + */ + get appVersion(): string { + return this.configService.get( + 'PAYMENT_PROVIDER_APP_VERSION', + '0.0.1', + ); + } + + /** + * The URL of the application's information page. + * Default: 'https://hmt.ai' + */ + get appInfoURL(): string { + return this.configService.get( + 'PAYMENT_PROVIDER_APP_INFO_URL', + 'https://hmt.ai', + ); + } +} diff --git a/packages/apps/job-launcher/server/src/common/config/stripe-config.service.ts b/packages/apps/job-launcher/server/src/common/config/stripe-config.service.ts deleted file mode 100644 index 513d64c64d..0000000000 --- a/packages/apps/job-launcher/server/src/common/config/stripe-config.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -@Injectable() -export class StripeConfigService { - constructor(private configService: ConfigService) {} - - /** - * The secret key used for authenticating requests to the Stripe API. - * Required - */ - get secretKey(): string { - return this.configService.getOrThrow('STRIPE_SECRET_KEY'); - } - - /** - * The version of the Stripe API to use for requests. - * Default: '2022-11-15' - */ - get apiVersion(): string { - return this.configService.get('STRIPE_API_VERSION', '2022-11-15'); - } - - /** - * The name of the application interacting with the Stripe API. - * Default: 'Fortune' - */ - get appName(): string { - return this.configService.get('STRIPE_APP_NAME', 'Fortune'); - } - - /** - * The version of the application interacting with the Stripe API. - * Default: '0.0.1' - */ - get appVersion(): string { - return this.configService.get('STRIPE_APP_VERSION', '0.0.1'); - } - - /** - * The URL of the application's information page. - * Default: 'https://hmt.ai' - */ - get appInfoURL(): string { - return this.configService.get( - 'STRIPE_APP_INFO_URL', - 'https://hmt.ai', - ); - } -} diff --git a/packages/apps/job-launcher/server/src/common/enums/payment.ts b/packages/apps/job-launcher/server/src/common/enums/payment.ts index 2db8a1325e..9277a96b7f 100644 --- a/packages/apps/job-launcher/server/src/common/enums/payment.ts +++ b/packages/apps/job-launcher/server/src/common/enums/payment.ts @@ -36,12 +36,6 @@ export enum PaymentStatus { SUCCEEDED = 'succeeded', } -export enum StripePaymentStatus { - CANCELED = 'canceled', - REQUIRES_PAYMENT_METHOD = 'requires_payment_method', - SUCCEEDED = 'succeeded', -} - export enum PaymentSortField { CREATED_AT = 'created_at', AMOUNT = 'amount', diff --git a/packages/apps/job-launcher/server/src/database/migrations/1749498615107-RenameStripeCustomerIdColumn.ts b/packages/apps/job-launcher/server/src/database/migrations/1749498615107-RenameStripeCustomerIdColumn.ts new file mode 100644 index 0000000000..ff6bb88c1b --- /dev/null +++ b/packages/apps/job-launcher/server/src/database/migrations/1749498615107-RenameStripeCustomerIdColumn.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameStripeCustomerIdColumn1749498615107 implements MigrationInterface { + + name = 'RenameStripeCustomerIdColumn1749498615107'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "hmt"."users" + RENAME COLUMN "stripe_customer_id" TO "payment_provider_id" + `); + await queryRunner.query(` + ALTER TABLE "hmt"."users" + RENAME CONSTRAINT "UQ_5ffbe395603641c29e8ce9b4c97" TO "UQ_721ffe5f6051eb5c6ac35321213" + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "hmt"."users" + RENAME CONSTRAINT "UQ_721ffe5f6051eb5c6ac35321213" TO "UQ_5ffbe395603641c29e8ce9b4c97" + `); + await queryRunner.query(` + ALTER TABLE "hmt"."users" + RENAME COLUMN "payment_provider_id" TO "stripe_customer_id" + `); + } +} diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts index ea98af8fcb..66072ac3ff 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts @@ -469,7 +469,7 @@ describe('JobService', () => { const fortuneJobDto: JobFortuneDto = createFortuneJobDto(); await expect( jobService.createJob( - createUser({ stripeCustomerId: null }), + createUser({ paymentProviderId: null }), FortuneJobType.FORTUNE, fortuneJobDto, ), diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 93d0e9f284..e18688b745 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -146,9 +146,9 @@ export class JobService { const whitelisted = await this.whitelistService.isUserWhitelisted(user.id); if (!whitelisted) { if ( - !user.stripeCustomerId || + !user.paymentProviderId || !(await this.paymentService.getDefaultPaymentMethod( - user.stripeCustomerId, + user.paymentProviderId, )) ) throw new ValidationError(ErrorJob.NotActiveCard); diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.interface.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.interface.ts index a9b4d8650f..b2648a08c2 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.interface.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.interface.ts @@ -1,6 +1,60 @@ import { PaymentEntity } from './payment.entity'; +import { PaymentStatus, VatType } from '../../common/enums/payment'; export interface ListResult { entities: PaymentEntity[]; itemCount: number; } + +export interface PaymentMethod { + id: string; + brand: string; + last4: string; + expMonth: number; + expYear: number; + default: boolean; +} + +export interface BillingAddress { + line1?: string; + city?: string; + country?: string; + postalCode?: string; +} + +export interface CustomerData { + email: string; + name?: string; + address?: BillingAddress; + defaultPaymentMethod?: string; +} + +export interface TaxId { + id: string; + type: VatType; + value: string; +} + +export interface Invoice { + id: string; + paymentId: string | null; + status?: string; + amountDue: number; + currency: string; +} + +export interface CardSetup { + customerId: string; + paymentMethod: string; +} + +export interface PaymentData { + customer: string; + id: string; + clientSecret: string | null; + status: PaymentStatus | null; + amount: number; + amountReceived: number; + currency: string; + latestCharge: string; +} diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.module.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.module.ts index 6667226e1f..522df94b37 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.module.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.module.ts @@ -15,6 +15,8 @@ import { UserEntity } from '../user/user.entity'; import { JobRepository } from '../job/job.repository'; import { UserRepository } from '../user/user.repository'; import { RateModule } from '../rate/rate.module'; +import { StripeService } from './providers/stripe/stripe.service'; +import { PaymentProvider } from './providers/payment-provider.abstract'; @Module({ imports: [ @@ -39,7 +41,16 @@ import { RateModule } from '../rate/rate.module'; }), ], controllers: [PaymentController], - providers: [PaymentService, PaymentRepository, JobRepository, UserRepository], + providers: [ + PaymentService, + PaymentRepository, + JobRepository, + UserRepository, + { + provide: PaymentProvider, + useClass: StripeService, + }, + ], exports: [PaymentService, PaymentRepository], }) export class PaymentModule {} diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts index a7da7743fb..1753cd296f 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.repository.ts @@ -4,7 +4,7 @@ import { DataSource, In, LessThan, MoreThan } from 'typeorm'; import { PaymentStatus } from '../../common/enums/payment'; import { BaseRepository } from '../../database/base.repository'; import { PaymentEntity } from './payment.entity'; -import { ListResult } from '../payment/payment.interface'; +import { ListResult } from './payment.interface'; import { GetPaymentsDto } from './payment.dto'; import { convertToDatabaseSortDirection } from '../../database/database.utils'; diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts index 2a7a83146a..7677b18080 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts @@ -12,7 +12,6 @@ import { ConflictException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import { ethers } from 'ethers'; -import Stripe from 'stripe'; import { MOCK_ADDRESS, MOCK_PAYMENT_ID, @@ -22,7 +21,6 @@ import { } from '../../../test/constants'; import { NetworkConfigService } from '../../common/config/network-config.service'; import { ServerConfigService } from '../../common/config/server-config.service'; -import { StripeConfigService } from '../../common/config/stripe-config.service'; import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; import { ErrorPayment, @@ -30,14 +28,12 @@ import { ErrorSignature, } from '../../common/constants/errors'; import { SortDirection } from '../../common/enums/collection'; -import { Country } from '../../common/enums/job'; import { PaymentCurrency, PaymentSortField, PaymentSource, PaymentStatus, PaymentType, - StripePaymentStatus, VatType, } from '../../common/enums/payment'; import { @@ -55,13 +51,15 @@ import { GetPaymentsDto, UserBalanceDto } from './payment.dto'; import { PaymentEntity } from './payment.entity'; import { PaymentRepository } from './payment.repository'; import { PaymentService } from './payment.service'; +import { PaymentProvider } from './providers/payment-provider.abstract'; +import { Invoice, PaymentData } from './payment.interface'; describe('PaymentService', () => { - let stripe: Stripe; let paymentService: PaymentService; - let paymentRepository: PaymentRepository; - let userRepository: UserRepository; - let rateService: RateService; + let paymentProvider: jest.Mocked; + let paymentRepository: jest.Mocked; + let userRepository: jest.Mocked; + let rateService: jest.Mocked; const signerMock = { address: MOCK_ADDRESS, @@ -71,6 +69,7 @@ describe('PaymentService', () => { beforeEach(async () => { const moduleRef = await Test.createTestingModule({ providers: [ + PaymentService, { provide: ConfigService, useValue: { @@ -83,8 +82,6 @@ describe('PaymentService', () => { }), }, }, - PaymentService, - StripeConfigService, { provide: PaymentRepository, useValue: createMock(), @@ -111,82 +108,31 @@ describe('PaymentService', () => { getRate: jest.fn().mockResolvedValue(1), }, }, + { + provide: PaymentProvider, + useValue: createMock(), + }, NetworkConfigService, ServerConfigService, ], }).compile(); paymentService = moduleRef.get(PaymentService); + paymentProvider = moduleRef.get(PaymentProvider); paymentRepository = moduleRef.get(PaymentRepository); userRepository = moduleRef.get(UserRepository); rateService = moduleRef.get(RateService); - - stripe = { - customers: { - create: jest.fn(), - update: jest.fn(), - listPaymentMethods: jest.fn(), - listTaxIds: jest.fn(), - createTaxId: jest.fn(), - retrieve: jest.fn(), - }, - paymentIntents: { - create: jest.fn(), - retrieve: jest.fn(), - update: jest.fn(), - confirm: jest.fn(), - }, - setupIntents: { - create: jest.fn(), - retrieve: jest.fn(), - }, - paymentMethods: { - retrieve: jest.fn(), - detach: jest.fn(), - }, - charges: { - retrieve: jest.fn(), - }, - invoices: { - create: jest.fn(), - finalizeInvoice: jest.fn(), - }, - invoiceItems: { - create: jest.fn(), - }, - } as any; - - paymentService['stripe'] = stripe; }); describe('createFiatPayment', () => { - let createInvoiceMock: any, - createInvoiceItemMock: any, - finalizeInvoiceMock: any, - retrievePaymentIntentMock: any, - updatePaymentIntentMock: any, - findOneMock: any; + let findOneMock: any; beforeEach(() => { findOneMock = jest.spyOn(paymentRepository, 'findOneByTransaction'); - createInvoiceMock = jest.spyOn(stripe.invoices, 'create'); - createInvoiceItemMock = jest.spyOn(stripe.invoiceItems, 'create'); - finalizeInvoiceMock = jest.spyOn(stripe.invoices, 'finalizeInvoice'); - retrievePaymentIntentMock = jest.spyOn(stripe.paymentIntents, 'retrieve'); - updatePaymentIntentMock = jest.spyOn(stripe.paymentIntents, 'update'); }); afterEach(() => { - expect(createInvoiceMock).toHaveBeenCalledTimes(1); - expect(createInvoiceItemMock).toHaveBeenCalledTimes(1); - expect(finalizeInvoiceMock).toHaveBeenCalledTimes(1); - expect(retrievePaymentIntentMock).toHaveBeenCalledTimes(1); - expect(updatePaymentIntentMock).toHaveBeenCalledTimes(1); - createInvoiceMock.mockRestore(); - createInvoiceItemMock.mockRestore(); - finalizeInvoiceMock.mockRestore(); - retrievePaymentIntentMock.mockRestore(); - updatePaymentIntentMock.mockRestore(); + jest.restoreAllMocks(); }); it('should create a fiat payment successfully', async () => { @@ -198,56 +144,49 @@ describe('PaymentService', () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; const paymentIntent = { id: 'pi_123', - client_secret: 'clientSecret123', - }; + clientSecret: 'clientSecret123', + } as PaymentData; const invoice = { id: 'id', - payment_intent: paymentIntent.id, - }; + paymentId: paymentIntent.id, + } as Invoice; + + paymentProvider.createInvoice.mockResolvedValue(invoice as any); + paymentProvider.assignPaymentMethod.mockResolvedValue( + paymentIntent as any, + ); - createInvoiceMock.mockResolvedValue(invoice as any); - finalizeInvoiceMock.mockResolvedValue(invoice as any); - retrievePaymentIntentMock.mockResolvedValue(paymentIntent as any); - jest - .spyOn(stripe.paymentIntents, 'retrieve') - .mockResolvedValue(paymentIntent as any); jest .spyOn(paymentRepository, 'findOneByTransaction') .mockResolvedValue(null); + jest .spyOn(paymentRepository, 'createUnique') .mockResolvedValue(undefined as any); const result = await paymentService.createFiatPayment(user as any, dto); - expect(result).toEqual(paymentIntent.client_secret); - expect(stripe.invoices.create).toHaveBeenCalledWith({ - currency: PaymentCurrency.USD, - customer: 'cus_123', - auto_advance: false, - payment_settings: { - payment_method_types: ['card'], - }, - }); - expect(stripe.invoiceItems.create).toHaveBeenCalledWith({ - customer: 'cus_123', - amount: 10000, - invoice: invoice.id, - description: 'Top up', - }); - expect(stripe.paymentIntents.update).toHaveBeenCalledWith('pi_123', { - payment_method: 'pm_123', - }); + expect(result).toEqual(paymentIntent.clientSecret); + expect(paymentProvider.createInvoice).toHaveBeenCalledWith( + 'cus_123', + 10000, + PaymentCurrency.USD, + 'Top up', + ); + expect(paymentProvider.assignPaymentMethod).toHaveBeenCalledWith( + 'pi_123', + 'pm_123', + false, + ); }); it('should throw a bad request exception if transaction already exist', async () => { - 0; const dto = { amount: 100, currency: PaymentCurrency.USD, @@ -256,7 +195,7 @@ describe('PaymentService', () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; const paymentIntent = { @@ -269,12 +208,10 @@ describe('PaymentService', () => { payment_intent: paymentIntent.id, }; - createInvoiceMock.mockResolvedValue(invoice as any); - finalizeInvoiceMock.mockResolvedValue(invoice as any); - retrievePaymentIntentMock.mockResolvedValue(paymentIntent as any); - jest - .spyOn(stripe.paymentIntents, 'retrieve') - .mockResolvedValue(paymentIntent as any); + paymentProvider.createInvoice.mockResolvedValue(invoice as any); + paymentProvider.assignPaymentMethod.mockResolvedValue( + paymentIntent as any, + ); findOneMock.mockResolvedValue({ transaction: paymentIntent.client_secret, @@ -286,45 +223,13 @@ describe('PaymentService', () => { new ConflictError(ErrorPayment.TransactionAlreadyExists), ); }); - - it('should throw a bad request exception if the invoice creation fails', async () => { - 0; - const dto = { - amount: 100, - currency: PaymentCurrency.USD, - paymentMethodId: 'pm_123', - }; - - const user = { - id: 1, - stripeCustomerId: 'cus_123', - }; - - const paymentIntent = { - id: 'pi_123', - }; - - const invoice = { - id: 'id', - payment_intent: paymentIntent.id, - }; - - createInvoiceMock.mockResolvedValue(invoice as any); - finalizeInvoiceMock.mockResolvedValue(invoice as any); - retrievePaymentIntentMock.mockResolvedValue(paymentIntent as any); - - await expect( - paymentService.createFiatPayment(user as any, dto), - ).rejects.toThrow(new ServerError(ErrorPayment.ClientSecretDoesNotExist)); - }); }); describe('confirmFiatPayment', () => { - let retrievePaymentIntentMock: any, findOneMock: any; + let findOneMock: any; beforeEach(() => { findOneMock = jest.spyOn(paymentRepository, 'findOneByTransaction'); - retrievePaymentIntentMock = jest.spyOn(stripe.paymentIntents, 'retrieve'); }); afterEach(() => { @@ -338,13 +243,15 @@ describe('PaymentService', () => { }; const paymentData = { - status: StripePaymentStatus.SUCCEEDED, + status: PaymentStatus.SUCCEEDED, amount: 100, - amount_received: 100, + amountReceived: 100, currency: PaymentCurrency.USD, }; - retrievePaymentIntentMock.mockResolvedValue(paymentData); + paymentProvider.retrievePaymentIntent.mockResolvedValue( + paymentData as any, + ); const paymentEntity: Partial = { userId: userId, @@ -357,50 +264,46 @@ describe('PaymentService', () => { const result = await paymentService.confirmFiatPayment(userId, dto); expect(result).toBe(true); + expect(paymentProvider.retrievePaymentIntent).toHaveBeenCalledWith( + MOCK_PAYMENT_ID, + ); + expect(paymentRepository.updateOne).toHaveBeenCalledWith({ + userId, + amount: 1, + currency: PaymentCurrency.USD, + status: PaymentStatus.SUCCEEDED, + }); }); - it('should handle payment cancellation', async () => { + it('should throw a not found exception if payment not found', async () => { const userId = 1; const dto = { paymentId: MOCK_PAYMENT_ID, }; - const paymentData = { - status: StripePaymentStatus.CANCELED, - amount: 100, - amount_received: 0, - currency: PaymentCurrency.USD, - }; - - retrievePaymentIntentMock.mockResolvedValue(paymentData); - - const paymentEntity: Partial = { - userId: userId, - status: PaymentStatus.PENDING, - amount: 0, - currency: PaymentCurrency.USD, - }; - findOneMock.mockResolvedValue(paymentEntity); + findOneMock.mockResolvedValue(null); await expect( paymentService.confirmFiatPayment(userId, dto), - ).rejects.toThrow(new ConflictError(ErrorPayment.NotSuccess)); + ).rejects.toThrow(new NotFoundError(ErrorPayment.NotFound)); }); - it('should handle payment requiring a payment method', async () => { + it('should throw a conflict exception if payment status is not pending', async () => { const userId = 1; const dto = { paymentId: MOCK_PAYMENT_ID, }; const paymentData = { - status: StripePaymentStatus.REQUIRES_PAYMENT_METHOD, + status: PaymentStatus.FAILED, amount: 100, - amount_received: 0, + amountReceived: 0, currency: PaymentCurrency.USD, }; - retrievePaymentIntentMock.mockResolvedValue(paymentData); + paymentProvider.retrievePaymentIntent.mockResolvedValue( + paymentData as any, + ); const paymentEntity: Partial = { userId: userId, @@ -424,11 +327,13 @@ describe('PaymentService', () => { const paymentData = { status: 'unknown_status', amount: 100, - amount_received: 0, + amountReceived: 0, currency: PaymentCurrency.USD, }; - retrievePaymentIntentMock.mockResolvedValue(paymentData); + paymentProvider.retrievePaymentIntent.mockResolvedValue( + paymentData as any, + ); const paymentEntity: Partial = { userId: userId, @@ -449,7 +354,9 @@ describe('PaymentService', () => { paymentId: MOCK_PAYMENT_ID, }; - retrievePaymentIntentMock.mockResolvedValue(null); + paymentProvider.retrievePaymentIntent.mockResolvedValue( + null as unknown as PaymentData, + ); await expect( paymentService.confirmFiatPayment(userId, dto), @@ -847,41 +754,33 @@ describe('PaymentService', () => { const user = { id: 1, email: 'test@hmt.ai', - stripeCustomerId: null, + paymentProviderId: null, }; - const paymentIntent = { - client_secret: 'clientSecret123', - }; + const client_secret = 'clientSecret123'; + const customerId = 'cus_123'; - jest - .spyOn(stripe.customers, 'create') - .mockResolvedValue({ id: 'cus_123' } as any); - jest - .spyOn(stripe.setupIntents, 'create') - .mockResolvedValue(paymentIntent as any); + paymentProvider.createCustomer.mockResolvedValue(customerId); + paymentProvider.setupCard.mockResolvedValue(client_secret); const result = await paymentService.createCustomerAndAssignCard( user as any, ); - expect(result).toEqual(paymentIntent.client_secret); - expect(stripe.customers.create).toHaveBeenCalledWith({ - email: user.email, - }); - expect(stripe.setupIntents.create).toHaveBeenCalledWith({ - automatic_payment_methods: { enabled: true }, - customer: 'cus_123', - }); + expect(result).toEqual(client_secret); + expect(paymentProvider.createCustomer).toHaveBeenCalledWith(user.email); }); it('should throw a bad request exception if the customer creation fails', async () => { const user = { id: 1, email: 'test@hmt.ai', - stripeCustomerId: undefined, + paymentProviderId: undefined, }; - jest.spyOn(stripe.customers, 'create').mockRejectedValue(new Error()); + + paymentProvider.createCustomer.mockRejectedValue( + new ServerError(ErrorPayment.CustomerNotCreated), + ); await expect( paymentService.createCustomerAndAssignCard(user as any), @@ -894,15 +793,16 @@ describe('PaymentService', () => { email: 'test@hmt.ai', }; - jest - .spyOn(stripe.customers, 'create') - .mockResolvedValue({ id: 1 } as any); - - jest.spyOn(stripe.setupIntents, 'create').mockRejectedValue(new Error()); + paymentProvider.createCustomer.mockResolvedValue('cus_123'); + paymentProvider.setupCard.mockRejectedValue( + new ServerError(ErrorPayment.CardNotAssigned), + ); await expect( paymentService.createCustomerAndAssignCard(user as any), - ).rejects.toThrow(ErrorPayment.CardNotAssigned); + ).rejects.toThrow(new ServerError(ErrorPayment.CardNotAssigned)); + + expect(paymentProvider.createCustomer).toHaveBeenCalledWith(user.email); }); it('should throw a bad request exception if the client secret does not exists', async () => { @@ -911,36 +811,32 @@ describe('PaymentService', () => { email: 'test@hmt.ai', }; - jest - .spyOn(stripe.customers, 'create') - .mockResolvedValue({ id: 1 } as any); - jest - .spyOn(stripe.setupIntents, 'create') - .mockResolvedValue(undefined as any); + paymentProvider.createCustomer.mockResolvedValue('cus_123'); + paymentProvider.setupCard.mockRejectedValue( + new ServerError(ErrorPayment.ClientSecretDoesNotExist), + ); await expect( paymentService.createCustomerAndAssignCard(user as any), - ).rejects.toThrow(ErrorPayment.ClientSecretDoesNotExist); + ).rejects.toThrow(new ServerError(ErrorPayment.ClientSecretDoesNotExist)); }); }); describe('confirmCard', () => { - it('should confirm a card and update user stripeCustomerId successfully', async () => { + it('should confirm a card and update user paymentProviderId successfully', async () => { const user = { id: 1, email: 'test@hmt.ai', - stripeCustomerId: null, + paymentProviderId: null, }; const setupMock = { - customer: 'cus_123', - payment_method: 'pm_123', + customerId: 'cus_123', + paymentMethod: 'pm_123', }; - jest - .spyOn(stripe.setupIntents, 'retrieve') - .mockResolvedValue(setupMock as any); - jest.spyOn(stripe.customers, 'update').mockResolvedValue(null as any); + paymentProvider.retrieveCardSetup.mockResolvedValue(setupMock as any); + paymentProvider.updateCustomer.mockResolvedValue(null as any); jest .spyOn(userRepository, 'updateOne') .mockResolvedValue(undefined as any); @@ -953,14 +849,14 @@ describe('PaymentService', () => { expect(result).toBeTruthy(); expect(userRepository.updateOne).toHaveBeenCalledWith( expect.objectContaining({ - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }), ); - expect(stripe.setupIntents.retrieve).toHaveBeenCalledWith('setup_123'); - expect(stripe.customers.update).toHaveBeenCalledWith('cus_123', { - invoice_settings: { - default_payment_method: 'pm_123', - }, + expect(paymentProvider.retrieveCardSetup).toHaveBeenCalledWith( + 'setup_123', + ); + expect(paymentProvider.updateCustomer).toHaveBeenCalledWith('cus_123', { + defaultPaymentMethod: 'pm_123', }); }); @@ -970,16 +866,14 @@ describe('PaymentService', () => { email: 'test@hmt.ai', }; - jest - .spyOn(stripe.setupIntents, 'retrieve') - .mockResolvedValue(undefined as any); + paymentProvider.retrieveCardSetup.mockResolvedValue(undefined as any); await expect( paymentService.confirmCard(user as any, { setupId: '1', defaultCard: false, }), - ).rejects.toThrow(ErrorPayment.SetupNotFound); + ).rejects.toThrow(new ServerError(ErrorPayment.SetupNotFound)); }); }); @@ -987,7 +881,7 @@ describe('PaymentService', () => { const user = { id: faker.number.int(), email: faker.internet.email(), - stripeCustomerId: faker.word.sample(), + paymentProviderId: faker.word.sample(), }; const jobEntity = { @@ -1004,53 +898,34 @@ describe('PaymentService', () => { const invoiceId = faker.word.sample(); const paymentMethodId = faker.word.sample(); - it('should charge user credit card and create slash payments successfully', async () => { + it('should create slash successfully', async () => { jest.spyOn(userRepository, 'findById').mockResolvedValueOnce(user as any); - jest - .spyOn(stripe.paymentIntents, 'retrieve') - .mockResolvedValueOnce(paymentIntent as any); - jest - .spyOn(stripe.paymentIntents, 'confirm') - .mockResolvedValueOnce(paymentIntent as any); - jest - .spyOn(stripe.invoices, 'create') - .mockResolvedValueOnce({ id: invoiceId } as any); - jest - .spyOn(stripe.invoiceItems, 'create') - .mockResolvedValueOnce({} as any); - jest - .spyOn(stripe.invoices, 'finalizeInvoice') - .mockResolvedValueOnce({ payment_intent: paymentIntent.id } as any); - jest.spyOn(stripe.customers, 'retrieve').mockResolvedValueOnce({ - invoice_settings: { default_payment_method: paymentMethodId }, + + paymentProvider.createInvoice.mockResolvedValueOnce({ + id: invoiceId, + paymentId: paymentIntent, } as any); + paymentProvider.assignPaymentMethod.mockResolvedValueOnce( + paymentIntent as any, + ); + paymentProvider.getDefaultPaymentMethod.mockResolvedValueOnce( + paymentMethodId, + ); const result = await paymentService.createSlash(jobEntity as any); expect(result).toBe(undefined); - expect(stripe.invoices.create).toHaveBeenCalledWith({ - customer: user.stripeCustomerId, - currency: PaymentCurrency.USD, - auto_advance: false, - payment_settings: { - payment_method_types: ['card'], - }, - }); - expect(stripe.invoiceItems.create).toHaveBeenCalledWith({ - customer: user.stripeCustomerId, - amount: expect.any(Number), - invoice: invoiceId, - description: 'Slash Job Id ' + jobEntity.id, - }); - expect(stripe.invoices.finalizeInvoice).toHaveBeenCalledWith(invoiceId); - expect(stripe.paymentIntents.confirm).toHaveBeenCalledWith( - paymentIntent.id, - { - payment_method: paymentMethodId, - off_session: true, - }, + expect(paymentProvider.createInvoice).toHaveBeenCalledWith( + user.paymentProviderId, + expect.any(Number), + PaymentCurrency.USD, + 'Slash Job Id ' + jobEntity.id, + ); + expect(paymentProvider.assignPaymentMethod).toHaveBeenCalledWith( + paymentIntent, + paymentMethodId, + true, ); - expect(paymentRepository.createUnique).toHaveBeenCalledTimes(2); }); it('should fail if user does not have payment info', async () => { @@ -1065,25 +940,23 @@ describe('PaymentService', () => { it('should fail if stripe create payment intent fails', async () => { jest.spyOn(userRepository, 'findById').mockResolvedValueOnce(user as any); - jest - .spyOn(stripe.invoices, 'create') - .mockResolvedValueOnce({ id: invoiceId } as any); - jest - .spyOn(stripe.invoiceItems, 'create') - .mockResolvedValueOnce({} as any); - jest - .spyOn(stripe.invoices, 'finalizeInvoice') - .mockResolvedValueOnce({ payment_intent: paymentIntent.id } as any); - jest.spyOn(stripe.customers, 'retrieve').mockResolvedValueOnce({ - invoice_settings: { default_payment_method: paymentMethodId }, + + paymentProvider.createInvoice.mockResolvedValueOnce({ + id: invoiceId, } as any); - jest - .spyOn(stripe.paymentIntents, 'confirm') - .mockRejectedValue(new Error()); + paymentProvider.getDefaultPaymentMethod.mockResolvedValueOnce( + paymentMethodId, + ); + + paymentProvider.assignPaymentMethod.mockRejectedValue( + new ServerError(ErrorPayment.PaymentMethodAssociationFailed), + ); await expect( paymentService.createSlash(jobEntity as any), - ).rejects.toThrow(ErrorPayment.PaymentMethodAssociationFailed); + ).rejects.toThrow( + new ServerError(ErrorPayment.PaymentMethodAssociationFailed), + ); }); }); @@ -1091,22 +964,22 @@ describe('PaymentService', () => { it('should list user payment methods successfully', async () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; - const paymentMethods = { - data: [ - { id: 'pm_123', card: { brand: 'visa', last4: '4242' } }, - { id: 'pm_456', card: { brand: 'mastercard', last4: '5555' } }, - ], - }; + const paymentMethods = [ + { + id: 'pm_123', + brand: 'visa', + last4: '4242', + }, + { id: 'pm_456', brand: 'mastercard', last4: '5555' }, + ]; - jest - .spyOn(stripe.customers, 'listPaymentMethods') - .mockResolvedValueOnce(paymentMethods as any); - jest - .spyOn(paymentService as any, 'getDefaultPaymentMethod') - .mockResolvedValueOnce('pm_123'); + paymentProvider.listPaymentMethods.mockResolvedValue( + paymentMethods as any, + ); + paymentProvider.getDefaultPaymentMethod.mockResolvedValue('pm_123'); const result = await paymentService.listUserPaymentMethods(user as any); @@ -1130,37 +1003,36 @@ describe('PaymentService', () => { it('should delete a payment method successfully', async () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; - jest - .spyOn(stripe.paymentMethods, 'retrieve') - .mockResolvedValue({ id: 'pm_123' } as any); - jest - .spyOn(paymentService as any, 'getDefaultPaymentMethod') - .mockResolvedValue('pm_456'); + paymentProvider.retrievePaymentMethod.mockResolvedValue({ + id: 'pm_123', + } as any); + paymentProvider.getDefaultPaymentMethod.mockResolvedValue('pm_456'); jest .spyOn(paymentService as any, 'isPaymentMethodInUse') .mockResolvedValue(false); - jest.spyOn(stripe.paymentMethods, 'detach').mockResolvedValue({} as any); + paymentProvider.detachPaymentMethod.mockResolvedValue({} as any); await paymentService.deletePaymentMethod(user as any, 'pm_123'); - expect(stripe.paymentMethods.detach).toHaveBeenCalledWith('pm_123'); + expect(paymentProvider.detachPaymentMethod).toHaveBeenCalledWith( + 'pm_123', + ); }); it('should throw an error when trying to delete the default payment method in use', async () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; - jest - .spyOn(stripe.paymentMethods, 'retrieve') - .mockResolvedValue({ id: 'pm_123' } as any); - jest - .spyOn(paymentService as any, 'getDefaultPaymentMethod') - .mockResolvedValue('pm_123'); + paymentProvider.retrievePaymentMethod.mockResolvedValue({ + id: 'pm_123', + default: true, + } as any); + paymentProvider.getDefaultPaymentMethod.mockResolvedValue('pm_123'); jest .spyOn(paymentService as any, 'isPaymentMethodInUse') .mockResolvedValue(true); @@ -1175,40 +1047,29 @@ describe('PaymentService', () => { it('should get user billing info successfully', async () => { const user = { id: 1, - stripeCustomerId: 'cus_123', - }; - - const taxIds = { - data: [{ type: VatType.EU_VAT, value: 'DE123456789' }], + paymentProviderId: 'cus_123', }; const customer = { name: 'John Doe', email: 'john@example.com', address: { - country: Country.DE, - postal_code: '12345', + country: 'de', + postalCode: '12345', city: 'Berlin', - line1: 'Street 1', + line: 'Street 1', }, }; - jest - .spyOn(stripe.customers, 'listTaxIds') - .mockResolvedValue(taxIds as any); - jest - .spyOn(stripe.customers, 'retrieve') - .mockResolvedValue(customer as any); + paymentProvider.retrieveBillingInfo.mockResolvedValue(customer as any); const result = await paymentService.getUserBillingInfo(user as any); expect(result).toEqual({ name: 'John Doe', email: 'john@example.com', - vat: 'DE123456789', - vatType: VatType.EU_VAT, address: { - country: Country.DE, + country: 'de', postalCode: '12345', city: 'Berlin', line: 'Street 1', @@ -1221,7 +1082,7 @@ describe('PaymentService', () => { it('should update user billing info successfully', async () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; const updateBillingInfoDto = { @@ -1237,31 +1098,28 @@ describe('PaymentService', () => { }, }; - jest - .spyOn(stripe.customers, 'listTaxIds') - .mockResolvedValue({ data: [] } as any); - jest.spyOn(stripe.customers, 'createTaxId').mockResolvedValue({} as any); - jest.spyOn(stripe.customers, 'update').mockResolvedValue({} as any); + paymentProvider.updateCustomer.mockResolvedValue({} as any); await paymentService.updateUserBillingInfo( user as any, updateBillingInfoDto, ); - expect(stripe.customers.createTaxId).toHaveBeenCalledWith('cus_123', { - type: VatType.EU_VAT, - value: 'DE123456789', - }); - expect(stripe.customers.update).toHaveBeenCalledWith('cus_123', { - name: 'John Doe', - email: 'john@example.com', - address: { - country: 'DE', - postal_code: '12345', - city: 'Berlin', - line1: 'Street 1', + expect(paymentProvider.updateBillingInfo).toHaveBeenCalledWith( + 'cus_123', + { + name: 'John Doe', + email: 'john@example.com', + address: { + country: 'DE', + postalCode: '12345', + city: 'Berlin', + line: 'Street 1', + }, + vat: 'DE123456789', + vatType: VatType.EU_VAT, }, - }); + ); }); }); @@ -1269,15 +1127,15 @@ describe('PaymentService', () => { it('should change the default payment method successfully', async () => { const user = { id: 1, - stripeCustomerId: 'cus_123', + paymentProviderId: 'cus_123', }; - jest.spyOn(stripe.customers, 'update').mockResolvedValue({} as any); + paymentProvider.updateCustomer.mockResolvedValue({} as any); await paymentService.changeDefaultPaymentMethod(user as any, 'pm_123'); - expect(stripe.customers.update).toHaveBeenCalledWith('cus_123', { - invoice_settings: { default_payment_method: 'pm_123' }, + expect(paymentProvider.updateCustomer).toHaveBeenCalledWith('cus_123', { + defaultPaymentMethod: 'pm_123', }); }); }); @@ -1422,58 +1280,28 @@ describe('PaymentService', () => { }); describe('getReceipt', () => { - let retrievePaymentIntentMock: jest.SpyInstance; - let retrieveChargeMock: jest.SpyInstance; - - beforeEach(() => { - retrievePaymentIntentMock = jest.spyOn(stripe.paymentIntents, 'retrieve'); - retrieveChargeMock = jest.spyOn(stripe.charges, 'retrieve'); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should return the receipt URL if payment intent and charge exist', async () => { + it('should get receipt successfully', async () => { const paymentId = 'pi_123'; - const user = { stripeCustomerId: 'cus_123' } as any; + const user = { paymentProviderId: 'cus_123' } as any; - retrievePaymentIntentMock.mockResolvedValue({ - customer: 'cus_123', - latest_charge: 'ch_123', - } as any); - - retrieveChargeMock.mockResolvedValue({ - receipt_url: 'https://receipt.url', - } as any); + paymentProvider.getReceiptUrl.mockResolvedValue('https://receipt.url'); const result = await paymentService.getReceipt(paymentId, user); - expect(result).toEqual('https://receipt.url'); - expect(retrievePaymentIntentMock).toHaveBeenCalledWith(paymentId); - expect(retrieveChargeMock).toHaveBeenCalledWith('ch_123'); - }); - - it('should throw a NOT_FOUND error if payment intent does not exist', async () => { - const paymentId = 'pi_123'; - const user = { stripeCustomerId: 'cus_123' } as any; - - retrievePaymentIntentMock.mockResolvedValue(null); - await expect(paymentService.getReceipt(paymentId, user)).rejects.toThrow( - new NotFoundError(ErrorPayment.NotFound), + expect(result).toBe('https://receipt.url'); + expect(paymentProvider.getReceiptUrl).toHaveBeenCalledWith( + 'pi_123', + 'cus_123', ); }); - it('should throw a NOT_FOUND error if charge does not exist', async () => { + it('should throw a NOT_FOUND error if receipt URL is not found', async () => { const paymentId = 'pi_123'; - const user = { stripeCustomerId: 'cus_123' } as any; - - retrievePaymentIntentMock.mockResolvedValue({ - customer: 'cus_123', - latest_charge: 'ch_123', - } as any); + const user = { paymentProviderId: 'cus_123' } as any; - retrieveChargeMock.mockResolvedValue(null); + paymentProvider.getReceiptUrl.mockRejectedValue( + new NotFoundError(ErrorPayment.NotFound), + ); await expect(paymentService.getReceipt(paymentId, user)).rejects.toThrow( new NotFoundError(ErrorPayment.NotFound), diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts index 0809be590b..6454eef906 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts @@ -5,10 +5,8 @@ import { } from '@human-protocol/core/typechain-types'; import { Injectable, Logger } from '@nestjs/common'; import { ethers, formatUnits } from 'ethers'; -import Stripe from 'stripe'; import { NetworkConfigService } from '../../common/config/network-config.service'; import { ServerConfigService } from '../../common/config/server-config.service'; -import { StripeConfigService } from '../../common/config/stripe-config.service'; import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; import { ErrorPayment } from '../../common/constants/errors'; import { CoingeckoTokenId } from '../../common/constants/payment'; @@ -18,14 +16,11 @@ import { PaymentSource, PaymentStatus, PaymentType, - StripePaymentStatus, - VatType, } from '../../common/enums/payment'; import { add, div, eq, lt, mul } from '../../common/utils/decimal'; import { verifySignature } from '../../common/utils/signature'; import { Web3Service } from '../web3/web3.service'; import { - AddressDto, BillingInfoDto, CardConfirmDto, CardDto, @@ -50,11 +45,11 @@ import { JobRepository } from '../job/job.repository'; import { RateService } from '../rate/rate.service'; import { UserEntity } from '../user/user.entity'; import { UserRepository } from '../user/user.repository'; +import { PaymentProvider } from './providers/payment-provider.abstract'; @Injectable() export class PaymentService { private readonly logger = new Logger(PaymentService.name); - private stripe: Stripe; constructor( private readonly networkConfigService: NetworkConfigService, @@ -62,70 +57,26 @@ export class PaymentService { private readonly paymentRepository: PaymentRepository, private readonly userRepository: UserRepository, private readonly jobRepository: JobRepository, - private stripeConfigService: StripeConfigService, - private serverConfigService: ServerConfigService, - private rateService: RateService, - ) { - this.stripe = new Stripe(this.stripeConfigService.secretKey, { - apiVersion: this.stripeConfigService.apiVersion as any, - appInfo: { - name: this.stripeConfigService.appName, - version: this.stripeConfigService.appVersion, - url: this.stripeConfigService.appInfoURL, - }, - }); - } + private readonly serverConfigService: ServerConfigService, + private readonly rateService: RateService, + private readonly paymentProvider: PaymentProvider, + ) {} public async createCustomerAndAssignCard(user: UserEntity): Promise { - // Creates a new Stripe customer if the user does not already have one. - // It then initiates a SetupIntent to link a payment method (card) to the customer. - let setupIntent: Stripe.Response; - let customerId = user.stripeCustomerId; - - if (!user.stripeCustomerId) { - try { - // Create a new customer in Stripe and assign the ID to the user. - customerId = ( - await this.stripe.customers.create({ - email: user.email, - }) - ).id; - } catch (error) { - this.logger.log(error.message, PaymentService.name); - throw new ServerError(ErrorPayment.CustomerNotCreated); - } - } - try { - // Create a SetupIntent to manage and confirm card setup. - setupIntent = await this.stripe.setupIntents.create({ - automatic_payment_methods: { - enabled: true, - }, - customer: customerId ?? undefined, - }); - } catch (error) { - this.logger.log(error.message, PaymentService.name); - throw new ServerError(ErrorPayment.CardNotAssigned); - } + let customerId = user.paymentProviderId; - // Ensure the SetupIntent contains a client secret for completing the card setup process. - if (!setupIntent?.client_secret) { - this.logger.log( - ErrorPayment.ClientSecretDoesNotExist, - PaymentService.name, - ); - throw new ServerError(ErrorPayment.ClientSecretDoesNotExist); + if (!customerId) { + customerId = await this.paymentProvider.createCustomer(user.email); } - return setupIntent.client_secret; + return await this.paymentProvider.setupCard(customerId); } public async confirmCard( user: UserEntity, data: CardConfirmDto, ): Promise { - // Confirms the card setup using the Stripe SetupIntent and sets it as the default payment method if requested. - const setup = await this.stripe.setupIntents.retrieve(data.setupId); + const setup = await this.paymentProvider.retrieveCardSetup(data.setupId); if (!setup) { this.logger.log(ErrorPayment.SetupNotFound, PaymentService.name); @@ -133,23 +84,18 @@ export class PaymentService { } let defaultPaymentMethod: string | null = null; - if (!user.stripeCustomerId) { - // Assign the Stripe customer ID to the user if it does not exist yet. - user.stripeCustomerId = setup.customer as string; + if (!user.paymentProviderId) { + user.paymentProviderId = setup.customerId as string; await this.userRepository.updateOne(user); } else { - // Check if the user already has a default payment method. defaultPaymentMethod = await this.getDefaultPaymentMethod( - user.stripeCustomerId, + user.paymentProviderId, ); } if (data.defaultCard || !defaultPaymentMethod) { - // Update Stripe customer settings to use this payment method by default. - await this.stripe.customers.update(user.stripeCustomerId, { - invoice_settings: { - default_payment_method: setup.payment_method, - }, + await this.paymentProvider.updateCustomer(user.paymentProviderId, { + defaultPaymentMethod: setup.paymentMethod as string, }); } @@ -163,19 +109,19 @@ export class PaymentService { const { amount, currency, paymentMethodId } = dto; const amountInCents = Math.ceil(mul(amount, 100)); - if (!user.stripeCustomerId) { + if (!user.paymentProviderId) { throw new NotFoundError(ErrorPayment.CustomerNotFound); } - const invoice = await this.createInvoice( - user.stripeCustomerId, + const invoice = await this.paymentProvider.createInvoice( + user.paymentProviderId, amountInCents, currency, 'Top up', ); - const paymentIntent = await this.handleStripePaymentIntent( - invoice.payment_intent as string, + const paymentIntent = await this.paymentProvider.assignPaymentMethod( + invoice.paymentId as string, paymentMethodId, false, // on-session payment ); @@ -200,17 +146,17 @@ export class PaymentService { transaction: paymentIntent.id, status: PaymentStatus.PENDING, }); + await this.paymentRepository.createUnique(newPaymentEntity); - return paymentIntent.client_secret!; + return paymentIntent.clientSecret!; } public async confirmFiatPayment( userId: number, data: PaymentFiatConfirmDto, ): Promise { - // Confirms a fiat payment based on the PaymentIntent ID and updates its status in the system. - const paymentData = await this.stripe.paymentIntents.retrieve( + const paymentData = await this.paymentProvider.retrievePaymentIntent( data.paymentId, ); @@ -227,25 +173,23 @@ export class PaymentService { !paymentEntity || paymentEntity.userId !== userId || paymentEntity.status !== PaymentStatus.PENDING || - !eq(paymentEntity.amount, div(paymentData.amount_received, 100)) || + !eq(paymentEntity.amount, div(paymentData.amountReceived, 100)) || paymentEntity.currency !== paymentData.currency ) { throw new NotFoundError(ErrorPayment.NotFound); } - if ( - paymentData?.status === StripePaymentStatus.CANCELED || - paymentData?.status === StripePaymentStatus.REQUIRES_PAYMENT_METHOD - ) { + if (paymentData.status === PaymentStatus.FAILED) { paymentEntity.status = PaymentStatus.FAILED; await this.paymentRepository.updateOne(paymentEntity); throw new ConflictError(ErrorPayment.NotSuccess); - } else if (paymentData?.status !== StripePaymentStatus.SUCCEEDED) { + } else if (paymentData.status !== PaymentStatus.SUCCEEDED) { return false; // TODO: Handling other cases } // Update the payment entity to reflect successful payment. paymentEntity.status = PaymentStatus.SUCCEEDED; + await this.paymentRepository.updateOne(paymentEntity); return true; @@ -257,9 +201,11 @@ export class PaymentService { signature: string, ): Promise { this.web3Service.validateChainId(dto.chainId); + const network = this.networkConfigService.networks.find( (item) => item.chainId === dto.chainId, ); + const provider = new ethers.JsonRpcProvider(network?.rpcUrl); const transaction = await provider.getTransactionReceipt( @@ -338,6 +284,7 @@ export class PaymentService { transaction: dto.transactionHash, status: PaymentStatus.SUCCEEDED, }); + await this.paymentRepository.createUnique(newPaymentEntity); return true; @@ -352,12 +299,10 @@ export class PaymentService { currency, ); - const balance = paymentEntities.reduce( + return paymentEntities.reduce( (sum, payment) => add(sum, Number(payment.amount)), 0, ); - - return balance; } public async createRefundPayment(dto: PaymentRefund) { @@ -388,97 +333,33 @@ export class PaymentService { return mul(amount, rate); } - private async createInvoice( - customerId: string, - amountInCents: number, - currency: string, - description: string, - ): Promise { - let invoice = await this.stripe.invoices.create({ - customer: customerId, - currency: currency, - auto_advance: false, - payment_settings: { - payment_method_types: ['card'], - }, - }); - - await this.stripe.invoiceItems.create({ - customer: customerId, - amount: amountInCents, - invoice: invoice.id, - description: description, - }); - - // Finalize the invoice to prepare it for payment. - invoice = await this.stripe.invoices.finalizeInvoice(invoice.id); - - if (!invoice.payment_intent) { - throw new ServerError(ErrorPayment.IntentNotCreated); - } - - return invoice; - } - - private async handleStripePaymentIntent( - paymentIntentId: string, - paymentMethodId: string, - offSession: boolean, - ): Promise { - try { - if (offSession) { - // Use confirm for off-session payments - await this.stripe.paymentIntents.confirm(paymentIntentId, { - payment_method: paymentMethodId, - off_session: true, - }); - } else { - // Use update for on-session payments - await this.stripe.paymentIntents.update(paymentIntentId, { - payment_method: paymentMethodId, - }); - } - } catch { - throw new ServerError(ErrorPayment.PaymentMethodAssociationFailed); - } - - const paymentIntent = - await this.stripe.paymentIntents.retrieve(paymentIntentId); - - if (!paymentIntent?.client_secret) { - throw new ServerError(ErrorPayment.ClientSecretDoesNotExist); - } - - return paymentIntent; - } - public async createSlash(job: JobEntity): Promise { const amount = this.serverConfigService.abuseAmount; const currency = PaymentCurrency.USD; const user = await this.userRepository.findById(job.userId); - if (!user || !user.stripeCustomerId) { + if (!user || !user.paymentProviderId) { throw new NotFoundError(ErrorPayment.CustomerNotFound); } const amountInCents = Math.ceil(mul(amount, 100)); - const invoice = await this.createInvoice( - user.stripeCustomerId, + const invoice = await this.paymentProvider.createInvoice( + user.paymentProviderId, amountInCents, currency, 'Slash Job Id ' + job.id, ); const defaultPaymentMethod = await this.getDefaultPaymentMethod( - user.stripeCustomerId, + user.paymentProviderId, ); if (!defaultPaymentMethod) { throw new ServerError(ErrorPayment.NotDefaultPaymentMethod); } - const paymentIntent = await this.handleStripePaymentIntent( - invoice.payment_intent as string, + const paymentIntent = await this.paymentProvider.assignPaymentMethod( + invoice.paymentId as string, defaultPaymentMethod, true, // off-session payment ); @@ -494,6 +375,7 @@ export class PaymentService { transaction: paymentIntent.id, status: PaymentStatus.SUCCEEDED, }); + await this.paymentRepository.createUnique(newPaymentEntity); Object.assign(newPaymentEntity, { @@ -507,6 +389,7 @@ export class PaymentService { status: PaymentStatus.SUCCEEDED, jobId: job.id, }); + await this.paymentRepository.createUnique(newPaymentEntity); } @@ -536,31 +419,26 @@ export class PaymentService { async listUserPaymentMethods(user: UserEntity): Promise { const cards: CardDto[] = []; - if (!user.stripeCustomerId) { + if (!user.paymentProviderId) { return cards; } - // List all the payment methods (cards) associated with the user's Stripe account - const paymentMethods = await this.stripe.customers.listPaymentMethods( - user.stripeCustomerId, - { - type: 'card', - limit: 100, - }, + // List all the payment methods (cards) associated with the user's account + const paymentMethods = await this.paymentProvider.listPaymentMethods( + user.paymentProviderId, ); - // Get the default payment method for the user const defaultPaymentMethod = await this.getDefaultPaymentMethod( - user.stripeCustomerId, + user.paymentProviderId, ); - for (const paymentMethod of paymentMethods.data) { + for (const paymentMethod of paymentMethods) { const card = new CardDto(); card.id = paymentMethod.id; - card.brand = paymentMethod.card?.brand as string; - card.last4 = paymentMethod.card?.last4 as string; - card.expMonth = paymentMethod.card?.exp_month as number; - card.expYear = paymentMethod.card?.exp_year as number; + card.brand = paymentMethod.brand; + card.last4 = paymentMethod.last4; + card.expMonth = paymentMethod.expMonth; + card.expYear = paymentMethod.expYear; card.default = defaultPaymentMethod === paymentMethod.id; cards.push(card); } @@ -570,103 +448,48 @@ export class PaymentService { async deletePaymentMethod(user: UserEntity, paymentMethodId: string) { // Retrieve the payment method to be detached const paymentMethod = - await this.stripe.paymentMethods.retrieve(paymentMethodId); + await this.paymentProvider.retrievePaymentMethod(paymentMethodId); // Check if the payment method is the default one and in use for the user if ( - user.stripeCustomerId && - paymentMethod.id === - (await this.getDefaultPaymentMethod(user.stripeCustomerId)) && + user.paymentProviderId && + paymentMethod.default && (await this.isPaymentMethodInUse(user.id)) ) { throw new ConflictError(ErrorPayment.PaymentMethodInUse); } // Detach the payment method from the user's account - return this.stripe.paymentMethods.detach(paymentMethodId); + return this.paymentProvider.detachPaymentMethod(paymentMethodId); } async getUserBillingInfo(user: UserEntity): Promise { - if (!user.stripeCustomerId) { - return null; - } - - // Retrieve the customer's tax IDs and customer information - const taxIds = await this.stripe.customers.listTaxIds( - user.stripeCustomerId, + return await this.paymentProvider.retrieveBillingInfo( + user.paymentProviderId, ); - - const customer = (await this.stripe.customers.retrieve( - user.stripeCustomerId, - )) as Stripe.Customer; - - const userBillingInfo = new BillingInfoDto(); - if (customer.address) { - const address = new AddressDto(); - address.country = (customer.address.country as string).toLowerCase(); - address.postalCode = customer.address.postal_code as string; - address.city = customer.address.city as string; - address.line = customer.address.line1 as string; - userBillingInfo.address = address; - } - userBillingInfo.name = customer.name as string; - userBillingInfo.email = customer.email as string; - userBillingInfo.vat = taxIds.data[0]?.value; - userBillingInfo.vatType = taxIds.data[0]?.type as VatType; - return userBillingInfo; } async updateUserBillingInfo( user: UserEntity, updateBillingInfoDto: BillingInfoDto, ) { - if (!user.stripeCustomerId) { + if (!user.paymentProviderId) { throw new NotFoundError(ErrorPayment.CustomerNotFound); } - // If the VAT or VAT type has changed, update it in Stripe - const existingTaxIds = await this.stripe.customers.listTaxIds( - user.stripeCustomerId, - ); - // Delete any existing tax IDs before adding the new one - for (const taxId of existingTaxIds.data) { - await this.stripe.customers.deleteTaxId(user.stripeCustomerId, taxId.id); - } - - // Create the new VAT tax ID - if (updateBillingInfoDto.vat && updateBillingInfoDto.vatType) { - await this.stripe.customers.createTaxId(user.stripeCustomerId, { - type: updateBillingInfoDto.vatType, - value: updateBillingInfoDto.vat, - }); - } - - // If there are changes to the address, name, or email, update them - if ( - updateBillingInfoDto.address || - updateBillingInfoDto.name || - updateBillingInfoDto.email - ) { - return this.stripe.customers.update(user.stripeCustomerId, { - address: { - line1: updateBillingInfoDto.address?.line, - city: updateBillingInfoDto.address?.city, - country: updateBillingInfoDto.address?.country, - postal_code: updateBillingInfoDto.address?.postalCode, - }, - name: updateBillingInfoDto.name, - email: updateBillingInfoDto.email, - }); - } + return await this.paymentProvider.updateBillingInfo( + user.paymentProviderId, + updateBillingInfoDto, + ); } async changeDefaultPaymentMethod(user: UserEntity, cardId: string) { - if (!user.stripeCustomerId) { + if (!user.paymentProviderId) { throw new NotFoundError(ErrorPayment.CustomerNotFound); } - // Update the user's default payment method in Stripe - return this.stripe.customers.update(user.stripeCustomerId, { - invoice_settings: { default_payment_method: cardId }, + + return this.paymentProvider.updateCustomer(user.paymentProviderId, { + defaultPaymentMethod: cardId, }); } @@ -675,10 +498,7 @@ export class PaymentService { throw new NotFoundError(ErrorPayment.CustomerNotFound); } - // Retrieve the customer from Stripe and return the default payment method - const customer = await this.stripe.customers.retrieve(customerId); - return (customer as Stripe.Customer).invoice_settings - .default_payment_method as string; + return await this.paymentProvider.getDefaultPaymentMethod(customerId); } private async isPaymentMethodInUse(userId: number): Promise { @@ -721,22 +541,10 @@ export class PaymentService { } async getReceipt(paymentId: string, user: UserEntity): Promise { - // Retrieve the payment intent using the provided payment ID - const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentId); - - if (!paymentIntent || paymentIntent.customer !== user.stripeCustomerId) { - throw new NotFoundError(ErrorPayment.NotFound); - } - - // Retrieve the charge for the payment intent and ensure it has a receipt URL - const charge = await this.stripe.charges.retrieve( - paymentIntent.latest_charge as string, + return await this.paymentProvider.getReceiptUrl( + paymentId, + user.paymentProviderId, ); - if (!charge || !charge.receipt_url) { - throw new NotFoundError(ErrorPayment.NotFound); - } - - return charge.receipt_url; } public async getUserBalance(userId: number): Promise { diff --git a/packages/apps/job-launcher/server/src/modules/payment/providers/payment-provider.abstract.ts b/packages/apps/job-launcher/server/src/modules/payment/providers/payment-provider.abstract.ts new file mode 100644 index 0000000000..6adca84442 --- /dev/null +++ b/packages/apps/job-launcher/server/src/modules/payment/providers/payment-provider.abstract.ts @@ -0,0 +1,115 @@ +import { + CardSetup, + CustomerData, + Invoice, + PaymentData, + PaymentMethod, +} from '../payment.interface'; +import { BillingInfoDto } from '../payment.dto'; +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export abstract class PaymentProvider { + protected readonly logger: Logger = new Logger(this.constructor.name); + + /** + * Create a new customer in the payment provider system + * @param email Customer's email address + * @returns Customer ID + */ + abstract createCustomer(email: string): Promise; + + /** + * Setup payment card in the payment provider system + * @param customerId Customer ID + * @returns Customer ID + */ + abstract setupCard(customerId: string): Promise; + + /** + * Create an invoice for a customer + * @param customerId Customer ID + * @param amountInCents Amount in cents + * @param currency Currency code + * @param description Invoice description + * @returns Created invoice + */ + abstract createInvoice( + customerId: string, + amountInCents: number, + currency: string, + description: string, + ): Promise; + + /** + * Assign a payment method and confirm the payment intent + * @param paymentIntentId Payment intent ID + * @param paymentMethodId Payment method ID + * @param offSession Whether the payment is off-session + * @returns Updated payment intent + */ + abstract assignPaymentMethod( + paymentIntentId: string, + paymentMethodId: string, + offSession: boolean, + ): Promise; + + /** + * Get the default payment method for a customer + * @param customerId Customer ID + * @returns Payment method ID or null + */ + abstract getDefaultPaymentMethod(customerId: string): Promise; + + /** + * List all payment methods for a customer + * @param customerId Customer ID + * @returns Array of payment methods + */ + abstract listPaymentMethods(customerId: string): Promise; + + /** + * Update customer information + * @param customerId Customer ID + * @param data Customer data to update + * @returns Updated customer data + */ + abstract updateCustomer( + customerId: string, + data: Partial, + ): Promise; + + abstract retrieveCardSetup(setupId: string): Promise; + + /** + * Retrieve a payment method + * @param paymentMethodId Payment method ID + * @returns Payment method data + */ + abstract retrievePaymentMethod( + paymentMethodId: string, + ): Promise; + + /** + * Detach a payment method from a customer + * @param paymentMethodId Payment method ID + * @returns Detached payment method + */ + abstract detachPaymentMethod(paymentMethodId: string): Promise; + + abstract getReceiptUrl( + paymentId: string, + customerId: string | null, + ): Promise; + + abstract retrieveBillingInfo( + customerId: string | null, + ): Promise; + + abstract updateBillingInfo( + customerId: string, + data: BillingInfoDto, + ): Promise; + + abstract retrievePaymentIntent(paymentId: string): Promise; +} diff --git a/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/fixtures.ts b/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/fixtures.ts new file mode 100644 index 0000000000..b6c817c722 --- /dev/null +++ b/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/fixtures.ts @@ -0,0 +1,101 @@ +import { faker } from '@faker-js/faker'; +import { PaymentCurrency, VatType } from '../../../../common/enums/payment'; +import { AddressDto, BillingInfoDto } from '../../payment.dto'; +import { StripePaymentStatus } from './stripe.service'; + +export const createMockSetupIntent = () => ({ + id: faker.string.alphanumeric(24), + client_secret: faker.string.alphanumeric(32), + customer: faker.string.alphanumeric(24), + payment_method: faker.string.alphanumeric(24), + status: 'requires_payment_method', + created: faker.number.int(), +}); + +export const createMockPaymentIntent = (overrides: Partial = {}) => ({ + id: faker.string.alphanumeric(24), + client_secret: faker.string.alphanumeric(32), + status: StripePaymentStatus.REQUIRES_PAYMENT_METHOD, + amount: faker.number.int({ min: 1000, max: 100000 }), + amount_received: 0, + currency: PaymentCurrency.USD, + customer: faker.string.alphanumeric(24), + latest_charge: faker.string.alphanumeric(24), + created: faker.number.int(), + ...overrides, +}); + +export const createMockCustomer = (overrides: Partial = {}) => ({ + id: faker.string.alphanumeric(24), + name: faker.person.fullName(), + email: faker.internet.email(), + address: { + line1: faker.location.streetAddress(), + city: faker.location.city(), + country: faker.location.countryCode(), + postal_code: faker.location.zipCode(), + }, + invoice_settings: { + default_payment_method: faker.string.alphanumeric(24), + }, + created: faker.number.int(), + ...overrides, +}); + +export const createMockPaymentMethod = (overrides: Partial = {}) => ({ + id: faker.string.alphanumeric(24), + card: { + brand: faker.helpers.arrayElement(['visa', 'mastercard', 'amex']), + last4: faker.string.numeric(4), + exp_month: faker.number.int({ min: 1, max: 12 }), + exp_year: faker.number.int({ min: 2024, max: 2030 }), + }, + customer: faker.string.alphanumeric(24), + created: faker.number.int(), + ...overrides, +}); + +export const createMockInvoice = (overrides: Partial = {}) => ({ + id: faker.string.alphanumeric(24), + payment_intent: faker.string.alphanumeric(24), + status: 'draft', + amount_due: faker.number.int({ min: 1000, max: 100000 }), + currency: PaymentCurrency.USD, + customer: faker.string.alphanumeric(24), + created: faker.number.int(), + ...overrides, +}); + +export const createMockCharge = (overrides: Partial = {}) => ({ + id: faker.string.alphanumeric(24), + receipt_url: faker.internet.url(), + amount: faker.number.int({ min: 1000, max: 100000 }), + currency: PaymentCurrency.USD, + created: faker.number.int(), + ...overrides, +}); + +export const createMockTaxId = (overrides: Partial = {}) => ({ + id: faker.string.alphanumeric(24), + type: faker.helpers.arrayElement(Object.values(VatType)), + value: faker.string.alphanumeric(10), + created: faker.number.int(), + ...overrides, +}); + +export const createMockBillingInfoDto = ( + overrides: Partial = {}, +): BillingInfoDto => { + const dto = new BillingInfoDto(); + dto.name = faker.person.fullName(); + dto.email = faker.internet.email(); + dto.address = new AddressDto(); + dto.address.line = faker.location.streetAddress(); + dto.address.city = faker.location.city(); + dto.address.country = faker.location.countryCode().toLowerCase(); + dto.address.postalCode = faker.location.zipCode(); + dto.vat = faker.string.alphanumeric(10); + dto.vatType = faker.helpers.arrayElement(Object.values(VatType)); + + return Object.assign(dto, overrides); +}; diff --git a/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.spec.ts b/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.spec.ts new file mode 100644 index 0000000000..fed082f457 --- /dev/null +++ b/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.spec.ts @@ -0,0 +1,918 @@ +jest.mock('stripe'); + +import { faker } from '@faker-js/faker'; +import { PaymentData } from '../../payment.interface'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Logger } from '@nestjs/common'; +import { StripePaymentStatus, StripeService } from './stripe.service'; +import { PaymentProviderConfigService } from '../../../../common/config/payment-provider-config.service'; +import Stripe from 'stripe'; +import { NotFoundError, ServerError } from '../../../../common/errors'; +import { ErrorPayment } from '../../../../common/constants/errors'; +import { + PaymentCurrency, + PaymentStatus, + VatType, +} from '../../../../common/enums/payment'; +import { + createMockBillingInfoDto, + createMockCharge, + createMockCustomer, + createMockInvoice, + createMockPaymentIntent, + createMockPaymentMethod, + createMockSetupIntent, + createMockTaxId, +} from './fixtures'; + +describe('StripeService', () => { + let service: StripeService; + let stripeMock: jest.Mocked; + let loggerSpy: jest.SpyInstance; + + const mockStripeConfigService = { + secretKey: 'test_key', + apiVersion: '2023-10-16', + appName: 'test-app', + appVersion: '1.0.0', + appInfoURL: 'https://test.com', + }; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + StripeService, + { + provide: PaymentProviderConfigService, + useValue: mockStripeConfigService, + }, + ], + }).compile(); + + service = module.get(StripeService); + + // Create a properly structured mock for Stripe + stripeMock = { + customers: { + create: jest.fn(), + update: jest.fn(), + retrieve: jest.fn(), + listPaymentMethods: jest.fn(), + listTaxIds: jest.fn(), + deleteTaxId: jest.fn(), + createTaxId: jest.fn(), + }, + setupIntents: { + create: jest.fn(), + retrieve: jest.fn(), + }, + paymentIntents: { + confirm: jest.fn(), + update: jest.fn(), + retrieve: jest.fn(), + }, + invoices: { + create: jest.fn(), + finalizeInvoice: jest.fn(), + }, + invoiceItems: { + create: jest.fn(), + }, + paymentMethods: { + detach: jest.fn(), + retrieve: jest.fn(), + }, + charges: { + retrieve: jest.fn(), + }, + } as unknown as jest.Mocked; + + (service as any).stripe = stripeMock; + loggerSpy = jest.spyOn(Logger.prototype, 'log'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createCustomer', () => { + it('should create a customer successfully', async () => { + const mockCustomer = { id: faker.string.uuid() }; + stripeMock.customers.create = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const email = faker.internet.email(); + const result = await service.createCustomer(email); + + expect(result).toBe(mockCustomer.id); + expect(stripeMock.customers.create).toHaveBeenCalledWith({ + email, + }); + }); + + it('should handle errors when creating customer', async () => { + stripeMock.customers.create = jest + .fn() + .mockRejectedValue(new Error('Stripe error')); + + const email = faker.internet.email(); + + await expect(service.createCustomer(email)).rejects.toThrow( + new ServerError(ErrorPayment.CustomerNotCreated), + ); + expect(loggerSpy).toHaveBeenCalled(); + }); + }); + + describe('setupCard', () => { + const mockSetupIntent = createMockSetupIntent(); + + it('should create setup intent successfully', async () => { + stripeMock.setupIntents.create = jest + .fn() + .mockResolvedValueOnce(mockSetupIntent); + + const customerId = faker.string.uuid(); + const result = await service.setupCard(customerId); + + expect(result).toBe(mockSetupIntent.client_secret); + expect(stripeMock.setupIntents.create).toHaveBeenCalledWith({ + automatic_payment_methods: { enabled: true }, + customer: customerId, + }); + }); + + it('should handle null customerId', async () => { + stripeMock.setupIntents.create = jest + .fn() + .mockResolvedValueOnce(mockSetupIntent); + + await service.setupCard(null); + + expect(stripeMock.setupIntents.create).toHaveBeenCalledWith({ + automatic_payment_methods: { enabled: true }, + customer: undefined, + }); + }); + + it('should handle missing client secret', async () => { + stripeMock.setupIntents.create = jest.fn().mockResolvedValueOnce({}); + + const customerId = faker.string.uuid(); + + await expect(service.setupCard(customerId)).rejects.toThrow( + new ServerError(ErrorPayment.ClientSecretDoesNotExist), + ); + }); + }); + + describe('assignPaymentMethod', () => { + const mockPaymentIntent = createMockPaymentIntent(); + + it('should assign off-session payment method', async () => { + stripeMock.paymentIntents.confirm = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + + const paymentIntentId = faker.string.uuid(); + const paymentMethodId = faker.string.uuid(); + + const result = await service.assignPaymentMethod( + paymentIntentId, + paymentMethodId, + true, + ); + + expect(stripeMock.paymentIntents.confirm).toHaveBeenCalledWith( + paymentIntentId, + { + payment_method: paymentMethodId, + off_session: true, + }, + ); + expect(result).toEqual({ + id: mockPaymentIntent.id, + clientSecret: mockPaymentIntent.client_secret, + status: PaymentStatus.FAILED, + amount: mockPaymentIntent.amount, + amountReceived: mockPaymentIntent.amount_received, + currency: mockPaymentIntent.currency, + customer: mockPaymentIntent.customer, + latestCharge: mockPaymentIntent.latest_charge, + } as PaymentData); + }); + + it('should assign on-session payment method', async () => { + stripeMock.paymentIntents.update = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + + const paymentIntentId = faker.string.uuid(); + const paymentMethodId = faker.string.uuid(); + + const result = await service.assignPaymentMethod( + paymentIntentId, + paymentMethodId, + false, + ); + + expect(stripeMock.paymentIntents.update).toHaveBeenCalledWith( + paymentIntentId, + { + payment_method: paymentMethodId, + }, + ); + expect(result).toEqual({ + id: mockPaymentIntent.id, + clientSecret: mockPaymentIntent.client_secret, + status: PaymentStatus.FAILED, + amount: mockPaymentIntent.amount, + amountReceived: mockPaymentIntent.amount_received, + currency: mockPaymentIntent.currency, + customer: mockPaymentIntent.customer, + latestCharge: mockPaymentIntent.latest_charge, + } as PaymentData); + }); + }); + + describe('createInvoice', () => { + const mockInvoice = createMockInvoice(); + + it('should create invoice successfully', async () => { + const customerId = faker.string.uuid(); + + stripeMock.invoices.create = jest + .fn() + .mockResolvedValueOnce({ id: customerId }); + stripeMock.invoiceItems.create = jest.fn().mockResolvedValueOnce({}); + stripeMock.invoices.finalizeInvoice = jest + .fn() + .mockResolvedValueOnce(mockInvoice); + + const result = await service.createInvoice( + customerId, + 1000, + PaymentCurrency.USD, + 'Test invoice', + ); + + expect(stripeMock.invoices.create).toHaveBeenCalled(); + expect(stripeMock.invoiceItems.create).toHaveBeenCalled(); + expect(stripeMock.invoices.finalizeInvoice).toHaveBeenCalled(); + + const { id, payment_intent, status, currency, amount_due } = mockInvoice; + + expect(result).toEqual({ + id, + paymentId: payment_intent, + status, + currency, + amountDue: amount_due, + }); + }); + + it('should throw error when payment intent is missing', async () => { + const customerId = faker.string.uuid(); + const invoiceId = faker.string.uuid(); + + stripeMock.invoices.create = jest + .fn() + .mockResolvedValueOnce({ id: invoiceId }); + stripeMock.invoiceItems.create = jest.fn().mockResolvedValueOnce({}); + stripeMock.invoices.finalizeInvoice = jest + .fn() + .mockResolvedValueOnce({ id: invoiceId }); + + await expect( + service.createInvoice( + customerId, + 1000, + PaymentCurrency.USD, + 'Test invoice', + ), + ).rejects.toThrow(new ServerError(ErrorPayment.IntentNotCreated)); + }); + }); + + describe('retrievePaymentIntent', () => { + it('should retrieve payment intent successfully', async () => { + const mockPaymentIntent = createMockPaymentIntent({ + status: StripePaymentStatus.SUCCEEDED, + amount_received: 1000, + }); + + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + + const result = await service.retrievePaymentIntent(mockPaymentIntent.id); + + expect(result).toEqual({ + id: mockPaymentIntent.id, + clientSecret: mockPaymentIntent.client_secret, + status: PaymentStatus.SUCCEEDED, + amount: mockPaymentIntent.amount, + amountReceived: mockPaymentIntent.amount_received, + currency: mockPaymentIntent.currency, + customer: mockPaymentIntent.customer, + latestCharge: mockPaymentIntent.latest_charge, + } as PaymentData); + expect(stripeMock.paymentIntents.retrieve).toHaveBeenCalledWith( + mockPaymentIntent.id, + ); + }); + + it('should handle different payment statuses', async () => { + const statuses = [ + { + stripe: StripePaymentStatus.REQUIRES_PAYMENT_METHOD, + expected: PaymentStatus.FAILED, + }, + { + stripe: StripePaymentStatus.SUCCEEDED, + expected: PaymentStatus.SUCCEEDED, + }, + { + stripe: StripePaymentStatus.CANCELED, + expected: PaymentStatus.FAILED, + }, + ]; + + for (const { stripe, expected } of statuses) { + const mockPaymentIntent = createMockPaymentIntent({ status: stripe }); + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + + const result = await service.retrievePaymentIntent( + mockPaymentIntent.id, + ); + + expect(result.status).toBe(expected); + } + }); + + it('should handle missing client secret', async () => { + const mockPaymentIntent = createMockPaymentIntent({ + client_secret: null, + }); + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + + const result = await service.retrievePaymentIntent(mockPaymentIntent.id); + + expect(result.clientSecret).toBeNull(); + }); + }); + + describe('getDefaultPaymentMethod', () => { + it('should return default payment method ID when available', async () => { + const mockCustomer = createMockCustomer(); + const defaultPaymentMethod = faker.string.alphanumeric(); + + (mockCustomer as any).invoice_settings = { + default_payment_method: defaultPaymentMethod, + }; + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const result = await service.getDefaultPaymentMethod(mockCustomer.id); + + expect(result).toBe(defaultPaymentMethod); + expect(stripeMock.customers.retrieve).toHaveBeenCalledWith( + mockCustomer.id, + ); + }); + + it('should return null when no default payment method', async () => { + const mockCustomer = createMockCustomer(); + (mockCustomer as any).invoice_settings = { + default_payment_method: null, + }; + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const result = await service.getDefaultPaymentMethod(mockCustomer.id); + + expect(result).toBeNull(); + }); + + it('should return null when customer has no invoice settings', async () => { + const mockCustomer = createMockCustomer(); + (mockCustomer as any).invoice_settings = undefined; + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const result = await service.getDefaultPaymentMethod(mockCustomer.id); + + expect(result).toBeNull(); + }); + }); + + describe('listPaymentMethods', () => { + it('should return list of payment methods', async () => { + const mockCustomer = createMockCustomer(); + + const mockPaymentMethods = [ + createMockPaymentMethod(), + createMockPaymentMethod(), + ]; + + mockPaymentMethods[0].id = 'pm_1'; + mockPaymentMethods[0].card = { + brand: 'visa', + last4: '4242', + exp_month: 12, + exp_year: 2024, + }; + mockPaymentMethods[1].id = 'pm_2'; + mockPaymentMethods[1].card = { + brand: 'mastercard', + last4: '5555', + exp_month: 6, + exp_year: 2025, + }; + + stripeMock.customers.listPaymentMethods = jest + .fn() + .mockResolvedValueOnce({ data: mockPaymentMethods }); + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const result = await service.listPaymentMethods(mockCustomer.id); + + expect(result).toEqual([ + { + id: 'pm_1', + brand: 'visa', + last4: '4242', + expMonth: 12, + expYear: 2024, + default: false, + }, + { + id: 'pm_2', + brand: 'mastercard', + last4: '5555', + expMonth: 6, + expYear: 2025, + default: false, + }, + ]); + expect(stripeMock.customers.listPaymentMethods).toHaveBeenCalledWith( + mockCustomer.id, + { type: 'card', limit: 100 }, + ); + }); + + it('should return empty array when no payment methods', async () => { + const mockCustomer = createMockCustomer(); + + stripeMock.customers.listPaymentMethods = jest + .fn() + .mockResolvedValueOnce({ data: [] }); + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const result = await service.listPaymentMethods(mockCustomer.id); + + expect(result).toEqual([]); + }); + + it('should handle payment methods without card details', async () => { + const mockCustomer = createMockCustomer(); + + const mockPaymentMethods = [createMockPaymentMethod()]; + (mockPaymentMethods[0] as any).card = null; + + stripeMock.customers.listPaymentMethods = jest + .fn() + .mockResolvedValueOnce({ data: mockPaymentMethods }); + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const result = await service.listPaymentMethods(mockCustomer.id); + + expect(result[0]).toEqual({ + id: mockPaymentMethods[0].id, + brand: undefined, + last4: undefined, + expMonth: undefined, + expYear: undefined, + default: false, + }); + }); + }); + + describe('retrieveCardSetup', () => { + it('should retrieve card setup successfully', async () => { + const mockSetupIntent = createMockSetupIntent(); + + stripeMock.setupIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockSetupIntent); + + const result = await service.retrieveCardSetup(mockSetupIntent.id); + + expect(result).toEqual({ + customerId: mockSetupIntent.customer, + paymentMethod: mockSetupIntent.payment_method, + }); + expect(stripeMock.setupIntents.retrieve).toHaveBeenCalledWith( + mockSetupIntent.id, + ); + }); + + it('should handle setup intent without customer', async () => { + const mockSetupIntent = createMockSetupIntent(); + (mockSetupIntent as any).customer = null; + + stripeMock.setupIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockSetupIntent); + + const result = await service.retrieveCardSetup(mockSetupIntent.id); + + expect(result).toEqual({ + customerId: null, + paymentMethod: mockSetupIntent.payment_method, + }); + }); + + it('should handle setup intent without payment method', async () => { + const mockSetupIntent = createMockSetupIntent(); + (mockSetupIntent as any).payment_method = null; + + stripeMock.setupIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockSetupIntent); + + const result = await service.retrieveCardSetup(mockSetupIntent.id); + + expect(result).toEqual({ + customerId: mockSetupIntent.customer, + paymentMethod: null, + }); + }); + }); + + describe('updateCustomer', () => { + it('should update customer successfully', async () => { + const mockCustomer = createMockCustomer(); + stripeMock.customers.update = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + + const updateData = { + name: 'Updated Name', + address: { + line1: '123 Street', + city: 'City', + }, + }; + + const result = await service.updateCustomer(mockCustomer.id, updateData); + + const { line1, city, country, postal_code } = mockCustomer.address; + + expect(result).toEqual({ + email: mockCustomer.email, + name: mockCustomer.name, + address: { + line1, + city, + country, + postalCode: postal_code, + }, + defaultPaymentMethod: + mockCustomer.invoice_settings.default_payment_method, + }); + + expect(stripeMock.customers.update).toHaveBeenCalledWith( + mockCustomer.id, + updateData, + ); + }); + }); + + describe('retrievePaymentMethod', () => { + it('should retrieve payment method successfully', async () => { + const mockCustomer = createMockCustomer(); + const mockPaymentMethod = createMockPaymentMethod(); + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + stripeMock.paymentMethods.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentMethod); + + const result = await service.retrievePaymentMethod(mockPaymentMethod.id); + + expect(result).toEqual({ + id: mockPaymentMethod.id, + brand: mockPaymentMethod.card.brand, + last4: mockPaymentMethod.card.last4, + expMonth: mockPaymentMethod.card.exp_month, + expYear: mockPaymentMethod.card.exp_year, + default: false, + }); + expect(stripeMock.paymentMethods.retrieve).toHaveBeenCalledWith( + mockPaymentMethod.id, + ); + }); + }); + + describe('detachPaymentMethod', () => { + it('should detach payment method successfully', async () => { + const mockPaymentMethod = createMockPaymentMethod(); + + stripeMock.paymentMethods.detach = jest + .fn() + .mockResolvedValueOnce(mockPaymentMethod); + + const result = await service.detachPaymentMethod(mockPaymentMethod.id); + + expect(result).toEqual({ + id: mockPaymentMethod.id, + brand: mockPaymentMethod.card.brand, + last4: mockPaymentMethod.card.last4, + expMonth: mockPaymentMethod.card.exp_month, + expYear: mockPaymentMethod.card.exp_year, + default: false, + }); + expect(stripeMock.paymentMethods.detach).toHaveBeenCalledWith( + mockPaymentMethod.id, + ); + }); + }); + + describe('getReceiptUrl', () => { + it('should return receipt URL for valid payment', async () => { + const customerId = faker.string.uuid(); + + const mockPaymentIntent = createMockPaymentIntent({ + customer: customerId, + latest_charge: 'ch_123', + }); + const mockCharge = { + receipt_url: faker.internet.email(), + }; + + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + stripeMock.charges.retrieve = jest.fn().mockResolvedValueOnce(mockCharge); + + const result = await service.getReceiptUrl( + mockPaymentIntent.id, + customerId, + ); + + expect(result).toBe(mockCharge.receipt_url); + expect(stripeMock.paymentIntents.retrieve).toHaveBeenCalledWith( + mockPaymentIntent.id, + ); + expect(stripeMock.charges.retrieve).toHaveBeenCalledWith( + mockPaymentIntent.latest_charge, + ); + }); + + it('should throw NotFoundError when payment intent not found', async () => { + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(null); + + await expect( + service.getReceiptUrl(faker.string.uuid(), faker.string.uuid()), + ).rejects.toThrow(new NotFoundError(ErrorPayment.NotFound)); + }); + + it('should throw NotFoundError when customer ID does not match', async () => { + const customerId = faker.string.uuid(); + + const mockPaymentIntent = createMockPaymentIntent({ + latest_charge: faker.string.uuid(), + }); + + const mockCharge = { + receipt_url: null, + }; + + stripeMock.charges.retrieve = jest.fn().mockResolvedValueOnce(mockCharge); + + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + + await expect( + service.getReceiptUrl(mockPaymentIntent.id, customerId), + ).rejects.toThrow(new NotFoundError(ErrorPayment.NotFound)); + }); + + it('should throw NotFoundError when receipt URL is missing', async () => { + const customerId = faker.string.uuid(); + + const mockPaymentIntent = createMockPaymentIntent({ + customer: customerId, + latest_charge: faker.string.uuid(), + }); + const mockCharge = createMockCharge({ + receipt_url: null, + }); + + stripeMock.paymentIntents.retrieve = jest + .fn() + .mockResolvedValueOnce(mockPaymentIntent); + stripeMock.charges.retrieve = jest.fn().mockResolvedValueOnce(mockCharge); + + await expect( + service.getReceiptUrl(mockPaymentIntent.id, customerId), + ).rejects.toThrow(new NotFoundError(ErrorPayment.NotFound)); + }); + }); + + describe('retrieveBillingInfo', () => { + it('should return null when customerId is null', async () => { + const result = await service.retrieveBillingInfo(null); + expect(result).toBeNull(); + }); + + it('should return complete billing info when all data is available', async () => { + const mockCustomer = createMockCustomer(); + const mockTaxIds = [ + { + id: 'txi_123', + type: VatType.EU_VAT, + value: 'DE123456789', + }, + ]; + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + stripeMock.customers.listTaxIds = jest + .fn() + .mockResolvedValueOnce({ data: mockTaxIds }); + + const result = await service.retrieveBillingInfo(mockCustomer.id); + + expect(result).toEqual({ + name: mockCustomer.name, + email: mockCustomer.email, + address: { + line: mockCustomer.address.line1, + city: mockCustomer.address.city, + country: mockCustomer.address.country.toLowerCase(), + postalCode: mockCustomer.address.postal_code, + }, + vat: 'DE123456789', + vatType: VatType.EU_VAT, + }); + }); + + it('should return partial billing info when some data is missing', async () => { + const mockCustomer = createMockCustomer({ + address: undefined, + }); + + stripeMock.customers.retrieve = jest + .fn() + .mockResolvedValueOnce(mockCustomer); + stripeMock.customers.listTaxIds = jest + .fn() + .mockResolvedValueOnce({ data: [] }); + + const result = await service.retrieveBillingInfo(mockCustomer.id); + + expect(result).toEqual({ + name: mockCustomer.name, + email: mockCustomer.email, + address: undefined, + vat: undefined, + vatType: undefined, + }); + }); + }); + + describe('updateBillingInfo', () => { + it('should update all billing information', async () => { + const mockTaxId = createMockTaxId(); + + const mockExistingTaxIds = [mockTaxId]; + + const mockUpdatedCustomer = createMockCustomer(); + + stripeMock.customers.listTaxIds = jest + .fn() + .mockResolvedValueOnce({ data: mockExistingTaxIds }); + stripeMock.customers.deleteTaxId = jest.fn().mockResolvedValueOnce({}); + stripeMock.customers.createTaxId = jest + .fn() + .mockResolvedValueOnce(mockTaxId); + stripeMock.customers.update = jest + .fn() + .mockResolvedValueOnce(mockUpdatedCustomer); + + const mockUpdateBillingInfo = createMockBillingInfoDto(); + const { name, email, address } = mockUpdateBillingInfo; + const { city, country, postalCode, line } = address ?? {}; + + await service.updateBillingInfo( + mockUpdatedCustomer.id, + mockUpdateBillingInfo, + ); + + expect(stripeMock.customers.deleteTaxId).toHaveBeenCalledWith( + mockUpdatedCustomer.id, + mockExistingTaxIds[0].id, + ); + expect(stripeMock.customers.createTaxId).toHaveBeenCalledWith( + mockUpdatedCustomer.id, + { + type: mockUpdateBillingInfo.vatType, + value: mockUpdateBillingInfo.vat, + }, + ); + expect(stripeMock.customers.update).toHaveBeenCalledWith( + mockUpdatedCustomer.id, + { + name, + email, + address: { + city, + country, + line1: line, + postal_code: postalCode, + }, + }, + ); + }); + + it('should handle update without VAT information', async () => { + const mockTaxId = createMockTaxId(); + + const mockExistingTaxIds = [mockTaxId]; + + const mockUpdatedCustomer = createMockCustomer(); + + stripeMock.customers.listTaxIds = jest + .fn() + .mockResolvedValueOnce({ data: mockExistingTaxIds }); + stripeMock.customers.deleteTaxId = jest.fn().mockResolvedValueOnce({}); + stripeMock.customers.update = jest + .fn() + .mockResolvedValueOnce(mockUpdatedCustomer); + + const mockUpdateBillingInfo = createMockBillingInfoDto({ + vat: undefined, + vatType: undefined, + }); + + const { name, email, address } = mockUpdateBillingInfo; + const { city, country, postalCode, line } = address ?? {}; + + await service.updateBillingInfo( + mockUpdatedCustomer.id, + mockUpdateBillingInfo, + ); + + expect(stripeMock.customers.deleteTaxId).toHaveBeenCalledWith( + mockUpdatedCustomer.id, + mockTaxId.id, + ); + expect(stripeMock.customers.createTaxId).not.toHaveBeenCalled(); + expect(stripeMock.customers.update).toHaveBeenCalledWith( + mockUpdatedCustomer.id, + { + name, + email, + address: { + city, + country, + line1: line, + postal_code: postalCode, + }, + }, + ); + }); + }); +}); diff --git a/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.ts b/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.ts new file mode 100644 index 0000000000..b40a39d655 --- /dev/null +++ b/packages/apps/job-launcher/server/src/modules/payment/providers/stripe/stripe.service.ts @@ -0,0 +1,427 @@ +import { Injectable } from '@nestjs/common'; +import Stripe from 'stripe'; +import { PaymentProviderConfigService } from '../../../../common/config/payment-provider-config.service'; +import { NotFoundError, ServerError } from '../../../../common/errors'; +import { ErrorPayment } from '../../../../common/constants/errors'; +import { PaymentStatus, VatType } from '../../../../common/enums/payment'; +import { + CardSetup, + CustomerData, + Invoice, + PaymentData, + PaymentMethod, + TaxId, +} from '../../payment.interface'; +import { PaymentProvider } from '../payment-provider.abstract'; +import { AddressDto, BillingInfoDto } from '../../payment.dto'; + +export enum StripePaymentStatus { + CANCELED = 'canceled', + REQUIRES_PAYMENT_METHOD = 'requires_payment_method', + SUCCEEDED = 'succeeded', +} + +@Injectable() +export class StripeService extends PaymentProvider { + private stripe: Stripe; + + constructor(private stripeConfigService: PaymentProviderConfigService) { + super(); + + this.stripe = new Stripe(this.stripeConfigService.secretKey, { + apiVersion: this.stripeConfigService.apiVersion as any, + appInfo: { + name: this.stripeConfigService.appName, + version: this.stripeConfigService.appVersion, + url: this.stripeConfigService.appInfoURL, + }, + }); + } + + async createCustomer(email: string): Promise { + try { + const customer = await this.stripe.customers.create({ email }); + return customer.id; + } catch (error) { + this.logger.log(error.message, StripeService.name); + throw new ServerError(ErrorPayment.CustomerNotCreated); + } + } + + async setupCard(customerId: string | null): Promise { + let setupIntent: Stripe.Response; + + try { + setupIntent = await this.stripe.setupIntents.create({ + automatic_payment_methods: { enabled: true }, + customer: customerId ?? undefined, + }); + } catch (error) { + this.logger.log(error.message, StripeService.name); + throw new ServerError(ErrorPayment.CardNotAssigned); + } + + if (!setupIntent?.client_secret) { + this.logger.log( + ErrorPayment.ClientSecretDoesNotExist, + StripeService.name, + ); + throw new ServerError(ErrorPayment.ClientSecretDoesNotExist); + } + + return setupIntent.client_secret; + } + + async createInvoice( + customerId: string, + amountInCents: number, + currency: string, + description: string, + ): Promise { + let invoice = await this.stripe.invoices.create({ + customer: customerId, + currency: currency, + auto_advance: false, + payment_settings: { + payment_method_types: ['card'], + }, + }); + + await this.stripe.invoiceItems.create({ + customer: customerId, + amount: amountInCents, + invoice: invoice.id, + description: description, + }); + + invoice = await this.stripe.invoices.finalizeInvoice(invoice.id); + + if (!invoice.payment_intent) { + throw new ServerError(ErrorPayment.IntentNotCreated); + } + + return { + id: invoice.id, + paymentId: invoice.payment_intent as string, + status: invoice.status?.toString(), + amountDue: invoice.amount_due, + currency: invoice.currency, + }; + } + + async assignPaymentMethod( + paymentIntentId: string, + paymentMethodId: string, + offSession: boolean, + ): Promise { + try { + if (offSession) { + await this.stripe.paymentIntents.confirm(paymentIntentId, { + payment_method: paymentMethodId, + off_session: true, + }); + } else { + await this.stripe.paymentIntents.update(paymentIntentId, { + payment_method: paymentMethodId, + }); + } + } catch { + throw new ServerError(ErrorPayment.PaymentMethodAssociationFailed); + } + + const paymentIntent = await this.retrievePaymentIntent(paymentIntentId); + + if (!paymentIntent?.clientSecret) { + throw new ServerError(ErrorPayment.ClientSecretDoesNotExist); + } + + return paymentIntent; + } + + async getReceiptUrl(paymentId: string, customerId: string): Promise { + const paymentIntent = await this.retrievePaymentIntent(paymentId); + + if (!paymentIntent || paymentIntent.customer !== customerId) { + throw new NotFoundError(ErrorPayment.NotFound); + } + + const charge = await this.retrieveCharge( + paymentIntent.latestCharge as string, + ); + + if (!charge || !charge.receipt_url) { + throw new NotFoundError(ErrorPayment.NotFound); + } + + return charge.receipt_url; + } + + async retrieveBillingInfo( + customerId: string | null, + ): Promise { + if (!customerId) { + return null; + } + + const taxIds = await this.listCustomerTaxIds(customerId); + + const customer = await this.retrieveCustomer(customerId); + + const userBillingInfo = new BillingInfoDto(); + + if (customer.address) { + const address = new AddressDto(); + address.country = (customer.address.country as string).toLowerCase(); + address.postalCode = customer.address.postalCode as string; + address.city = customer.address.city as string; + address.line = customer.address.line1 as string; + userBillingInfo.address = address; + } + + userBillingInfo.name = customer.name as string; + userBillingInfo.email = customer.email as string; + userBillingInfo.vat = taxIds[0]?.value; + userBillingInfo.vatType = taxIds[0]?.type as VatType; + + return userBillingInfo; + } + + async updateBillingInfo( + customerId: string, + data: BillingInfoDto, + ): Promise { + const existingTaxIds = await this.listCustomerTaxIds(customerId); + + for (const taxId of existingTaxIds) { + await this.deleteTaxId(customerId, taxId.id); + } + + // Create the new VAT tax ID + if (data.vat && data.vatType) { + await this.createTaxId(customerId, data.vatType, data.vat); + } + + // If there are changes to the address, name, or email, update them + if (data.address || data.name || data.email) { + return this.updateCustomer(customerId, { + address: { + line1: data.address?.line, + city: data.address?.city, + country: data.address?.country, + postalCode: data.address?.postalCode, + }, + name: data.name, + email: data.email, + }); + } + + return this.retrieveCustomer(customerId); + } + + async retrievePaymentIntent(paymentIntentId: string): Promise { + const paymentIntent = + await this.stripe.paymentIntents.retrieve(paymentIntentId); + + if (!paymentIntent) { + throw new NotFoundError(ErrorPayment.NotFound); + } + + let status: PaymentStatus | null; + if ( + paymentIntent.status === StripePaymentStatus.CANCELED || + paymentIntent?.status === StripePaymentStatus.REQUIRES_PAYMENT_METHOD + ) { + status = PaymentStatus.FAILED; + } else if (paymentIntent?.status !== StripePaymentStatus.SUCCEEDED) { + status = null; // handle other statuses + } else { + status = PaymentStatus.SUCCEEDED; + } + + return { + id: paymentIntent.id, + customer: paymentIntent.customer as string, + clientSecret: paymentIntent.client_secret, + status, + amount: paymentIntent.amount, + amountReceived: paymentIntent.amount_received, + currency: paymentIntent.currency, + latestCharge: paymentIntent.latest_charge as string, + }; + } + + async getDefaultPaymentMethod(customerId: string): Promise { + const customer = await this.retrieveCustomer(customerId); + return customer.defaultPaymentMethod ?? null; + } + + async listPaymentMethods(customerId: string): Promise { + const paymentMethods = await this.stripe.customers.listPaymentMethods( + customerId, + { type: 'card', limit: 100 }, + ); + + const defaultPaymentMethod = await this.getDefaultPaymentMethod(customerId); + + return paymentMethods.data.map((method) => ({ + id: method.id, + brand: method.card?.brand as string, + last4: method.card?.last4 as string, + expMonth: method.card?.exp_month as number, + expYear: method.card?.exp_year as number, + default: defaultPaymentMethod === method.id, + })); + } + + async detachPaymentMethod(paymentMethodId: string): Promise { + const paymentMethod = + await this.stripe.paymentMethods.detach(paymentMethodId); + + return { + id: paymentMethod.id, + brand: paymentMethod.card?.brand as string, + last4: paymentMethod.card?.last4 as string, + expMonth: paymentMethod.card?.exp_month as number, + expYear: paymentMethod.card?.exp_year as number, + default: false, + }; + } + + async retrievePaymentMethod(paymentMethodId: string): Promise { + const paymentMethod = + await this.stripe.paymentMethods.retrieve(paymentMethodId); + + const defaultPaymentMethod = await this.getDefaultPaymentMethod( + paymentMethod.customer as string, + ); + + return { + id: paymentMethod.id, + brand: paymentMethod.card?.brand as string, + last4: paymentMethod.card?.last4 as string, + expMonth: paymentMethod.card?.exp_month as number, + expYear: paymentMethod.card?.exp_year as number, + default: defaultPaymentMethod === paymentMethod.id, + }; + } + + async updateCustomer( + customerId: string, + data: Partial, + ): Promise { + const { email, name, address, defaultPaymentMethod } = data; + const { line1, city, country, postalCode } = address ?? {}; + + const updatePayload = defaultPaymentMethod + ? { + invoice_settings: { + default_payment_method: data.defaultPaymentMethod, + }, + } + : { + email, + name, + address: { + line1, + city, + country, + postal_code: postalCode, + }, + }; + + const customer = await this.stripe.customers.update( + customerId, + updatePayload, + ); + + return { + email: customer.email!, + name: customer.name ?? undefined, + address: customer.address + ? { + line1: customer.address.line1 ?? undefined, + city: customer.address.city ?? undefined, + country: customer.address.country ?? undefined, + postalCode: customer.address.postal_code ?? undefined, + } + : undefined, + defaultPaymentMethod: customer.invoice_settings + ? (customer.invoice_settings.default_payment_method as string) + : undefined, + }; + } + + private async retrieveCustomer(customerId: string): Promise { + const customer = (await this.stripe.customers.retrieve( + customerId, + )) as Stripe.Customer; + + return { + email: customer.email!, + name: customer.name ?? undefined, + address: customer.address + ? { + line1: customer.address.line1 ?? undefined, + city: customer.address.city ?? undefined, + country: customer.address.country ?? undefined, + postalCode: customer.address.postal_code ?? undefined, + } + : undefined, + defaultPaymentMethod: customer.invoice_settings + ? (customer.invoice_settings.default_payment_method as string) + : undefined, + }; + } + + private async listCustomerTaxIds(customerId: string): Promise { + const taxIds = await this.stripe.customers.listTaxIds(customerId); + + return taxIds.data.map((taxId) => ({ + id: taxId.id, + type: taxId.type as VatType, + value: taxId.value, + })); + } + + private async createTaxId( + customerId: string, + type: VatType, + value: string, + ): Promise { + const taxId = await this.stripe.customers.createTaxId(customerId, { + type, + value, + }); + return { + id: taxId.id, + type: taxId.type as VatType, + value: taxId.value, + }; + } + + private async deleteTaxId( + customerId: string, + taxIdId: string, + ): Promise { + await this.stripe.customers.deleteTaxId(customerId, taxIdId); + } + + async retrieveCardSetup(setupIntentId: string): Promise { + const setupIntent = await this.stripe.setupIntents.retrieve(setupIntentId); + + return { + customerId: setupIntent.customer as string, + paymentMethod: setupIntent.payment_method as string, + }; + } + + private async retrieveCharge( + chargeId: string, + ): Promise<{ receipt_url: string }> { + const charge = await this.stripe.charges.retrieve(chargeId); + if (!charge.receipt_url) { + throw new ServerError(ErrorPayment.NotFound); + } + return { receipt_url: charge.receipt_url }; + } +} diff --git a/packages/apps/job-launcher/server/src/modules/user/fixtures.ts b/packages/apps/job-launcher/server/src/modules/user/fixtures.ts index 06b69d59f2..53af331af7 100644 --- a/packages/apps/job-launcher/server/src/modules/user/fixtures.ts +++ b/packages/apps/job-launcher/server/src/modules/user/fixtures.ts @@ -9,7 +9,7 @@ export const createUser = (overrides: Partial = {}): UserEntity => { user.password = faker.internet.password(); user.type = faker.helpers.arrayElement(Object.values(UserType)); user.status = faker.helpers.arrayElement(Object.values(UserStatus)); - user.stripeCustomerId = faker.string.uuid(); + user.paymentProviderId = faker.string.uuid(); user.jobs = []; user.payments = []; user.apiKey = null; diff --git a/packages/apps/job-launcher/server/src/modules/user/user.entity.ts b/packages/apps/job-launcher/server/src/modules/user/user.entity.ts index c63f108411..59b5a9117a 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.entity.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.entity.ts @@ -29,7 +29,7 @@ export class UserEntity extends BaseEntity implements IUser { public status: UserStatus; @Column({ type: 'varchar', nullable: true, unique: true }) - public stripeCustomerId: string | null; + public paymentProviderId: string | null; @OneToMany(() => JobEntity, (job) => job.user) public jobs: JobEntity[]; diff --git a/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts b/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts index 35c82ff14d..22305582c9 100644 --- a/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts @@ -27,9 +27,9 @@ import { MOCK_S3_SECRET_KEY, MOCK_S3_USE_SSL, MOCK_SECRET, - MOCK_STRIPE_API_VERSION, - MOCK_STRIPE_APP_INFO_URL, - MOCK_STRIPE_SECRET_KEY, + MOCK_PAYMENT_PROVIDER_API_VERSION, + MOCK_PAYMENT_PROVIDER_APP_INFO_URL, + MOCK_PAYMENT_PROVIDER_SECRET_KEY, } from '../../../test/constants'; import { ServerConfigService } from '../../common/config/server-config.service'; import { Web3ConfigService } from '../../common/config/web3-config.service'; @@ -62,9 +62,9 @@ describe('WebhookController', () => { FORTUNE_EXCHANGE_ORACLE_ADDRESS: MOCK_ADDRESS, FORTUNE_RECORDING_ORACLE_ADDRESS: MOCK_ADDRESS, WEB3_PRIVATE_KEY: MOCK_PRIVATE_KEY, - STRIPE_SECRET_KEY: MOCK_STRIPE_SECRET_KEY, - STRIPE_API_VERSION: MOCK_STRIPE_API_VERSION, - STRIPE_APP_INFO_URL: MOCK_STRIPE_APP_INFO_URL, + PAYMENT_PROVIDER_SECRET_KEY: MOCK_PAYMENT_PROVIDER_SECRET_KEY, + PAYMENT_PROVIDER_API_VERSION: MOCK_PAYMENT_PROVIDER_API_VERSION, + PAYMENT_PROVIDER_APP_INFO_URL: MOCK_PAYMENT_PROVIDER_APP_INFO_URL, HCAPTCHA_SITE_KEY: MOCK_HCAPTCHA_SITE_KEY, HCAPTCHA_RECORDING_ORACLE_URI: MOCK_RECORDING_ORACLE_URL, HCAPTCHA_REPUTATION_ORACLE_URI: MOCK_REPUTATION_ORACLE_URL, diff --git a/packages/apps/job-launcher/server/test/constants.ts b/packages/apps/job-launcher/server/test/constants.ts index 809f24265b..d54e1a00c9 100644 --- a/packages/apps/job-launcher/server/test/constants.ts +++ b/packages/apps/job-launcher/server/test/constants.ts @@ -66,11 +66,11 @@ export const MOCK_JOB_ID = 1; export const MOCK_SENDGRID_API_KEY = 'SG.xxxxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; -export const MOCK_STRIPE_SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxx'; +export const MOCK_PAYMENT_PROVIDER_SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxx'; export const MOCK_COINGECKO_API_KEY = 'xxxxxxxxxxxxxxxxxxxxxx'; -export const MOCK_STRIPE_API_VERSION = '2022-11-15'; -export const MOCK_STRIPE_APP_NAME = 'Name'; -export const MOCK_STRIPE_APP_INFO_URL = 'https://test-app-url.com'; +export const MOCK_PAYMENT_PROVIDER_API_VERSION = '2022-11-15'; +export const MOCK_PAYMENT_PROVIDER_APP_NAME = 'Name'; +export const MOCK_PAYMENT_PROVIDER_APP_INFO_URL = 'https://test-app-url.com'; export const MOCK_SENDGRID_FROM_EMAIL = 'info@hmt.ai'; export const MOCK_SENDGRID_FROM_NAME = 'John Doe'; export const MOCK_S3_ENDPOINT = 'localhost'; @@ -248,10 +248,10 @@ export const mockConfig: any = { PGP_PASSPHRASE: MOCK_PGP_PASSPHRASE, REPUTATION_ORACLE_ADDRESS: MOCK_ADDRESS, WEB3_PRIVATE_KEY: MOCK_PRIVATE_KEY, - STRIPE_SECRET_KEY: MOCK_STRIPE_SECRET_KEY, - STRIPE_API_VERSION: MOCK_STRIPE_API_VERSION, - STRIPE_APP_NAME: MOCK_STRIPE_APP_NAME, - STRIPE_APP_INFO_URL: MOCK_STRIPE_APP_INFO_URL, + PAYMENT_PROVIDER_SECRET_KEY: MOCK_PAYMENT_PROVIDER_SECRET_KEY, + PAYMENT_PROVIDER_API_VERSION: MOCK_PAYMENT_PROVIDER_API_VERSION, + PAYMENT_PROVIDER_APP_NAME: MOCK_PAYMENT_PROVIDER_APP_NAME, + PAYMENT_PROVIDER_APP_INFO_URL: MOCK_PAYMENT_PROVIDER_APP_INFO_URL, CVAT_EXCHANGE_ORACLE_ADDRESS: MOCK_ADDRESS, CVAT_RECORDING_ORACLE_ADDRESS: MOCK_ADDRESS, HCAPTCHA_SITE_KEY: MOCK_HCAPTCHA_SITE_KEY, From 4b03aa79dd7d1d9416b0296949ae1a953d7a8de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20L=C3=B3pez?= <50665615+flopez7@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:21:26 +0200 Subject: [PATCH 04/16] [Dashboard] Wrong token balance (#3411) Co-authored-by: kirill --- .../model/addressDetailsSchema.ts | 20 ++---- .../searchResults/ui/EscrowAddress.tsx | 48 +++++++------- .../features/searchResults/ui/HmtBalance.tsx | 20 ++---- .../features/searchResults/ui/StakeInfo.tsx | 32 ++------- .../features/searchResults/ui/TokenAmount.tsx | 42 ++++++++++++ .../searchResults/ui/WalletAddress.tsx | 19 +----- .../src/modules/details/details.service.ts | 65 +++++++++++++++++-- .../src/modules/details/details.spec.ts | 8 +++ .../src/modules/details/dto/escrow.dto.ts | 12 +++- 9 files changed, 160 insertions(+), 106 deletions(-) create mode 100644 packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx diff --git a/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts b/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts index 42d7f3881d..10b6b53ddd 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts +++ b/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts @@ -38,23 +38,11 @@ export type AddressDetailsWallet = z.infer; const escrowSchema = z.object({ chainId: z.number().optional().nullable(), address: z.string().optional().nullable(), - balance: z - .string() - .optional() - .nullable() - .transform(transformOptionalTokenAmount), + balance: z.string().optional().nullable(), token: z.string().optional().nullable(), factoryAddress: z.string().optional().nullable(), - totalFundedAmount: z - .string() - .optional() - .nullable() - .transform(transformOptionalTokenAmount), - amountPaid: z - .string() - .optional() - .nullable() - .transform(transformOptionalTokenAmount), + totalFundedAmount: z.string().optional().nullable(), + amountPaid: z.string().optional().nullable(), status: z.string().optional().nullable(), manifest: z.string().optional().nullable(), launcher: z.string().optional().nullable(), @@ -62,6 +50,8 @@ const escrowSchema = z.object({ recordingOracle: z.string().optional().nullable(), reputationOracle: z.string().optional().nullable(), finalResultsUrl: z.string().nullable(), + tokenSymbol: z.string().optional().nullable(), + tokenDecimals: z.number().optional().nullable(), }); export type AddressDetailsEscrow = z.infer; diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx index 8801d5d77a..5270129696 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx @@ -10,6 +10,7 @@ import { AddressDetailsEscrow } from '../model/addressDetailsSchema'; import HmtBalance from './HmtBalance'; import TitleSectionWrapper from './TitleSectionWrapper'; +import TokenAmount from './TokenAmount'; type Props = { data: AddressDetailsEscrow; @@ -17,7 +18,6 @@ type Props = { const EscrowAddress: FC = ({ data }) => { const { - token, balance, factoryAddress, totalFundedAmount, @@ -27,16 +27,26 @@ const EscrowAddress: FC = ({ data }) => { exchangeOracle, recordingOracle, reputationOracle, + tokenSymbol, } = data; + const isHmt = tokenSymbol === 'HMT'; return ( - {token} + {tokenSymbol} {balance !== undefined && balance !== null ? ( - + {isHmt ? ( + + ) : ( + + )} ) : null} = ({ data }) => { {factoryAddress} - - {totalFundedAmount} - - HMT - - + - - {amountPaid} - - HMT - - + diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx index aa4f482b53..0d9d00c134 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx @@ -4,16 +4,15 @@ import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import useHmtPrice from '@/shared/api/useHmtPrice'; -import { useIsMobile } from '@/shared/hooks/useBreakpoints'; -import FormattedNumber from '@/shared/ui/FormattedNumber'; + +import TokenAmount from './TokenAmount'; type Props = { - balance?: number | null; + balance?: number | string | null; }; const HmtBalance: FC = ({ balance }) => { const { data, isError, isPending } = useHmtPrice(); - const isMobile = useIsMobile(); if (isError) { return N/A; @@ -29,16 +28,9 @@ const HmtBalance: FC = ({ balance }) => { return ( - - - - - {`HMT($${balanceInDollars})`} + + + {`($${balanceInDollars})`} ); diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx index 449d26c9ea..8731f44cfc 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx @@ -3,43 +3,21 @@ import { FC } from 'react'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { useIsMobile } from '@/shared/hooks/useBreakpoints'; -import FormattedNumber from '@/shared/ui/FormattedNumber'; import SectionWrapper from '@/shared/ui/SectionWrapper'; +import TokenAmount from './TokenAmount'; + type Props = { amountStaked?: number | null; amountLocked?: number | null; amountWithdrawable?: number | null; }; -const renderAmount = (amount: number | null | undefined, isMobile: boolean) => { - return ( - - - - - - HMT - - - ); -}; - const StakeInfo: FC = ({ amountStaked, amountLocked, amountWithdrawable, }) => { - const isMobile = useIsMobile(); if (!amountStaked && !amountLocked && !amountWithdrawable) return null; return ( @@ -53,7 +31,7 @@ const StakeInfo: FC = ({ Staked Tokens - {renderAmount(amountStaked, isMobile)} + )} {Number.isFinite(amountLocked) && ( @@ -61,7 +39,7 @@ const StakeInfo: FC = ({ Locked Tokens - {renderAmount(amountLocked, isMobile)} + )} {Number.isFinite(amountWithdrawable) && ( @@ -69,7 +47,7 @@ const StakeInfo: FC = ({ Withdrawable Tokens - {renderAmount(amountWithdrawable, isMobile)} + )} diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx new file mode 100644 index 0000000000..8d57079fb1 --- /dev/null +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react'; + +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +import { useIsMobile } from '@/shared/hooks/useBreakpoints'; +import FormattedNumber from '@/shared/ui/FormattedNumber'; + +type Props = { + amount: number | string | null | undefined; + tokenSymbol?: string | null | undefined; + alreadyParsed?: boolean; +}; + +const TokenAmount: FC = ({ + amount, + tokenSymbol = 'HMT', + alreadyParsed = false, +}) => { + const isMobile = useIsMobile(); + + return ( + + + + + + {tokenSymbol} + + + ); +}; + +export default TokenAmount; diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx index 5275c53c20..bc74edf214 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx @@ -3,8 +3,6 @@ import { FC } from 'react'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { useIsMobile } from '@/shared/hooks/useBreakpoints'; -import FormattedNumber from '@/shared/ui/FormattedNumber'; import SectionWrapper from '@/shared/ui/SectionWrapper'; import { @@ -18,6 +16,7 @@ import KVStore from './KvStore'; import ReputationScore from './ReputationScore'; import StakeInfo from './StakeInfo'; import TitleSectionWrapper from './TitleSectionWrapper'; +import TokenAmount from './TokenAmount'; type Props = { data: AddressDetailsWallet | AddressDetailsOperator; @@ -31,7 +30,6 @@ const WalletAddress: FC = ({ data }) => { reputation, amountWithdrawable, } = data; - const isMobile = useIsMobile(); const isWallet = 'totalHMTAmountReceived' in data; return ( @@ -56,20 +54,7 @@ const WalletAddress: FC = ({ data }) => { title="Earned Payouts" tooltip="Total amount earned by participating in jobs" > - - - - - HMT - + )} diff --git a/packages/apps/dashboard/server/src/modules/details/details.service.ts b/packages/apps/dashboard/server/src/modules/details/details.service.ts index 361e674c10..1be4efe63b 100644 --- a/packages/apps/dashboard/server/src/modules/details/details.service.ts +++ b/packages/apps/dashboard/server/src/modules/details/details.service.ts @@ -1,5 +1,11 @@ import { plainToInstance } from 'class-transformer'; -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + Logger, + Inject, +} from '@nestjs/common'; +import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { ChainId, EscrowUtils, @@ -39,6 +45,7 @@ import { KVStoreDataDto } from './dto/details-response.dto'; export class DetailsService { private readonly logger = new Logger(DetailsService.name); constructor( + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, private readonly configService: EnvironmentConfigService, private readonly httpService: HttpService, private readonly networkConfig: NetworkConfigService, @@ -48,18 +55,37 @@ export class DetailsService { chainId: ChainId, address: string, ): Promise { + const network = this.networkConfig.networks.find( + (network) => network.chainId === chainId, + ); + if (!network) throw new BadRequestException('Invalid chainId provided'); + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const escrowData = await EscrowUtils.getEscrow(chainId, address); if (escrowData) { const escrowDto: EscrowDto = plainToInstance(EscrowDto, escrowData, { excludeExtraneousValues: true, }); + + const { decimals, symbol } = await this.getTokenData( + chainId, + escrowData.token, + provider, + ); + + escrowDto.balance = ethers.formatUnits(escrowData.balance, decimals); + escrowDto.totalFundedAmount = ethers.formatUnits( + escrowData.totalFundedAmount, + decimals, + ); + escrowDto.amountPaid = ethers.formatUnits( + escrowData.amountPaid, + decimals, + ); + escrowDto.tokenSymbol = symbol; + escrowDto.tokenDecimals = decimals; return escrowDto; } - const network = this.networkConfig.networks.find( - (network) => network.chainId === chainId, - ); - if (!network) throw new BadRequestException('Invalid chainId provided'); - const provider = new ethers.JsonRpcProvider(network.rpcUrl); const stakingClient = await StakingClient.build(provider); const stakingData = await stakingClient.getStakerInfo(address); @@ -111,7 +137,10 @@ export class DetailsService { return walletDto; } - private async getHmtBalance(chainId: ChainId, hmtAddress: string) { + private async getHmtBalance( + chainId: ChainId, + hmtAddress: string, + ): Promise { const network = this.networkConfig.networks.find( (network) => network.chainId === chainId, ); @@ -403,4 +432,26 @@ export class DetailsService { return data; } + + private async getTokenData( + chainId: ChainId, + tokenAddress: string, + provider: ethers.JsonRpcProvider, + ): Promise<{ decimals: number; symbol: string }> { + const tokenCacheKey = `token:${chainId}:${tokenAddress.toLowerCase()}`; + let data = await this.cacheManager.get<{ + decimals: number; + symbol: string; + }>(tokenCacheKey); + if (!data) { + const erc20Contract = HMToken__factory.connect(tokenAddress, provider); + const [decimals, symbol] = await Promise.all([ + erc20Contract.decimals(), + erc20Contract.symbol(), + ]); + data = { decimals: Number(decimals), symbol }; + await this.cacheManager.set(tokenCacheKey, data); + } + return data; + } } diff --git a/packages/apps/dashboard/server/src/modules/details/details.spec.ts b/packages/apps/dashboard/server/src/modules/details/details.spec.ts index ead21997de..8fdfdc2b5f 100644 --- a/packages/apps/dashboard/server/src/modules/details/details.spec.ts +++ b/packages/apps/dashboard/server/src/modules/details/details.spec.ts @@ -13,6 +13,7 @@ import { OrderDirection, } from '@human-protocol/sdk'; import { OperatorsOrderBy } from '../../common/enums/operator'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; jest.mock('@human-protocol/sdk', () => ({ ...jest.requireActual('@human-protocol/sdk'), @@ -57,6 +58,13 @@ describe('DetailsService', () => { .mockResolvedValue([ChainId.MAINNET]), }, }, + { + provide: CACHE_MANAGER, + useValue: { + get: jest.fn(), + set: jest.fn(), + }, + }, Logger, ], }).compile(); diff --git a/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts b/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts index 4f48910aa5..69e8e7bc00 100644 --- a/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts +++ b/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsEnum, IsString, IsUrl } from 'class-validator'; +import { IsEnum, IsNumber, IsString, IsUrl } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { ChainId } from '@human-protocol/sdk'; @@ -73,6 +73,16 @@ export class EscrowDto { @IsUrl() @Expose() public finalResultsUrl?: string; + + @ApiProperty({ example: 'HMT' }) + @IsString() + @Expose() + public tokenSymbol: string; + + @ApiProperty({ example: 18 }) + @IsNumber() + @Expose() + public tokenDecimals: number; } export class EscrowPaginationDto { From e39a1f8659f3635c5058b3004cb8931735164986 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:50:44 +0200 Subject: [PATCH 05/16] chore(deps-dev): bump sass from 1.87.0 to 1.89.2 (#3396) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/apps/dashboard/client/package.json | 2 +- packages/apps/staking/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/apps/dashboard/client/package.json b/packages/apps/dashboard/client/package.json index 8e0eef6f20..9d783df3ef 100644 --- a/packages/apps/dashboard/client/package.json +++ b/packages/apps/dashboard/client/package.json @@ -60,7 +60,7 @@ "eslint-plugin-react-refresh": "^0.4.11", "globals": "^16.2.0", "prettier": "^3.4.2", - "sass": "^1.85.0", + "sass": "^1.89.2", "typescript": "^5.6.3", "typescript-eslint": "^8.33.0", "vite": "^6.2.4" diff --git a/packages/apps/staking/package.json b/packages/apps/staking/package.json index f5336e073d..50cebcdbd2 100644 --- a/packages/apps/staking/package.json +++ b/packages/apps/staking/package.json @@ -54,7 +54,7 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.11", "prettier": "^3.4.2", - "sass": "^1.85.0", + "sass": "^1.89.2", "typescript": "^5.6.3", "vite": "^6.2.4", "vite-plugin-node-polyfills": "^0.23.0" diff --git a/yarn.lock b/yarn.lock index e663727004..f2fd7d885c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3932,7 +3932,7 @@ __metadata: react-number-format: "npm:^5.4.3" react-router-dom: "npm:^6.23.1" recharts: "npm:^2.13.0-alpha.4" - sass: "npm:^1.85.0" + sass: "npm:^1.89.2" simplebar-react: "npm:^3.2.5" styled-components: "npm:^6.1.11" swiper: "npm:^11.1.3" @@ -4552,7 +4552,7 @@ __metadata: react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-router-dom: "npm:^6.24.1" - sass: "npm:^1.85.0" + sass: "npm:^1.89.2" serve: "npm:^14.2.4" typescript: "npm:^5.6.3" viem: "npm:2.x" @@ -25499,9 +25499,9 @@ __metadata: languageName: node linkType: hard -"sass@npm:^1.85.0": - version: 1.87.0 - resolution: "sass@npm:1.87.0" +"sass@npm:^1.89.2": + version: 1.89.2 + resolution: "sass@npm:1.89.2" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -25512,7 +25512,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/bd245faf14e4783dc547765350cf05817edaac0d6d6f6e4da8ab751f3eb3cc3873afd563c0ce416a24aa6c9c4e9023b05096447fc006660a01f76adffb54fbc6 + checksum: 10c0/752ccc7581b0c6395f63918116c20924e99943a86d79e94f5c4a0d41b1e981fe1f0ecd1ee82fff21496f81dbc91f68fb35a498166562ec8ec53e7aad7c3dbd9d languageName: node linkType: hard From ce064ff104667d91ef2e1600788a0074df6bb6d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:03:48 +0200 Subject: [PATCH 06/16] chore(deps): bump @mui/x-data-grid from 7.29.2 to 8.5.2 (#3399) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/apps/dashboard/client/package.json | 2 +- yarn.lock | 76 ++++++++++++++++++--- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/packages/apps/dashboard/client/package.json b/packages/apps/dashboard/client/package.json index 9d783df3ef..254001622b 100644 --- a/packages/apps/dashboard/client/package.json +++ b/packages/apps/dashboard/client/package.json @@ -24,7 +24,7 @@ "@mui/material": "^5.15.18", "@mui/styled-engine-sc": "6.4.0", "@mui/system": "^5.15.14", - "@mui/x-data-grid": "^7.23.2", + "@mui/x-data-grid": "^8.5.2", "@mui/x-date-pickers": "^7.23.6", "@tanstack/react-query": "^5.67.2", "@types/react-router-dom": "^5.3.3", diff --git a/yarn.lock b/yarn.lock index f2fd7d885c..6c43344a11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2166,6 +2166,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.27.6": + version: 7.27.6 + resolution: "@babel/runtime@npm:7.27.6" + checksum: 10c0/89726be83f356f511dcdb74d3ea4d873a5f0cf0017d4530cb53aa27380c01ca102d573eff8b8b77815e624b1f8c24e7f0311834ad4fb632c90a770fda00bd4c8 + languageName: node + linkType: hard + "@babel/template@npm:^7.27.1, @babel/template@npm:^7.3.3": version: 7.27.2 resolution: "@babel/template@npm:7.27.2" @@ -3906,7 +3913,7 @@ __metadata: "@mui/material": "npm:^5.15.18" "@mui/styled-engine-sc": "npm:6.4.0" "@mui/system": "npm:^5.15.14" - "@mui/x-data-grid": "npm:^7.23.2" + "@mui/x-data-grid": "npm:^8.5.2" "@mui/x-date-pickers": "npm:^7.23.6" "@tanstack/react-query": "npm:^5.67.2" "@types/react": "npm:^18.2.66" @@ -5802,6 +5809,20 @@ __metadata: languageName: node linkType: hard +"@mui/types@npm:^7.4.3": + version: 7.4.3 + resolution: "@mui/types@npm:7.4.3" + dependencies: + "@babel/runtime": "npm:^7.27.1" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/8078ed0c63211377af4cf244e0b8a94d15748253139a330f6c7b983b755a57fa89bdba5d8b9ca4c30944b1567115eab3cbb9b9869c14489b0ad3249e858c9fa1 + languageName: node + linkType: hard + "@mui/types@npm:~7.2.15": version: 7.2.24 resolution: "@mui/types@npm:7.2.24" @@ -5854,17 +5875,36 @@ __metadata: languageName: node linkType: hard -"@mui/x-data-grid@npm:^7.23.2": - version: 7.29.2 - resolution: "@mui/x-data-grid@npm:7.29.2" +"@mui/utils@npm:^7.1.1": + version: 7.1.1 + resolution: "@mui/utils@npm:7.1.1" dependencies: - "@babel/runtime": "npm:^7.25.7" - "@mui/utils": "npm:^5.16.6 || ^6.0.0 || ^7.0.0" - "@mui/x-internals": "npm:7.29.0" + "@babel/runtime": "npm:^7.27.1" + "@mui/types": "npm:^7.4.3" + "@types/prop-types": "npm:^15.7.14" clsx: "npm:^2.1.1" prop-types: "npm:^15.8.1" - reselect: "npm:^5.1.1" - use-sync-external-store: "npm:^1.0.0" + react-is: "npm:^19.1.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d2563c4be785a94d55d32df7e51be530bb56a21ffbf4d1ca084c51a8b598ad7ee6d8bb30dd4171691c3b669eea6d8da07280aedd8d96fc539cfd0e0e5b9a48a1 + languageName: node + linkType: hard + +"@mui/x-data-grid@npm:^8.5.2": + version: 8.5.2 + resolution: "@mui/x-data-grid@npm:8.5.2" + dependencies: + "@babel/runtime": "npm:^7.27.6" + "@mui/utils": "npm:^7.1.1" + "@mui/x-internals": "npm:8.5.2" + clsx: "npm:^2.1.1" + prop-types: "npm:^15.8.1" + use-sync-external-store: "npm:^1.5.0" peerDependencies: "@emotion/react": ^11.9.0 "@emotion/styled": ^11.8.1 @@ -5877,7 +5917,7 @@ __metadata: optional: true "@emotion/styled": optional: true - checksum: 10c0/6898abe0733f6fc1a5f41c2cd41037988fc2466ffaf2ce0d31cd22054b696e81d48aa788305cb4e7f54c757f6fee2ad8568d8b042954d65cbb23f70317c28861 + checksum: 10c0/deb5e7e1f9391b92d85acdb39eb1bc0fd85cbad748be839b132b62dedd3fcce5aae1919b4630a76f156ebcbd8402114feff97b6cc118f484883d722d31976b47 languageName: node linkType: hard @@ -5941,6 +5981,20 @@ __metadata: languageName: node linkType: hard +"@mui/x-internals@npm:8.5.2": + version: 8.5.2 + resolution: "@mui/x-internals@npm:8.5.2" + dependencies: + "@babel/runtime": "npm:^7.27.6" + "@mui/utils": "npm:^7.1.1" + reselect: "npm:^5.1.1" + peerDependencies: + "@mui/system": ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/03d2105d82624b0064c618a6bfc289c124f7fd8bff2f11fe5ceb9504514fb4cf59be906b5649b3537f21b86c54dbcbf4406e55d529a8a399d9fbc510f3bd2bf9 + languageName: node + linkType: hard + "@multiformats/dns@npm:^1.0.3": version: 1.0.6 resolution: "@multiformats/dns@npm:1.0.6" @@ -28378,7 +28432,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: From 6f43d980a396e593c2f41c819aab81c568a6eb50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:14:26 +0200 Subject: [PATCH 07/16] chore(deps): bump @tanstack/react-query-persist-client from 5.75.5 to 5.80.7 (#3398) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../exchange-oracle/client/package.json | 2 +- .../apps/job-launcher/client/package.json | 2 +- packages/apps/staking/package.json | 2 +- yarn.lock | 34 ++++++++++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/apps/fortune/exchange-oracle/client/package.json b/packages/apps/fortune/exchange-oracle/client/package.json index e6c3b2b24d..31f5326e90 100644 --- a/packages/apps/fortune/exchange-oracle/client/package.json +++ b/packages/apps/fortune/exchange-oracle/client/package.json @@ -34,7 +34,7 @@ "@mui/material": "^5.16.7", "@tanstack/query-sync-storage-persister": "^5.68.0", "@tanstack/react-query": "^5.67.2", - "@tanstack/react-query-persist-client": "^5.67.2", + "@tanstack/react-query-persist-client": "^5.80.7", "axios": "^1.7.2", "ethers": "^6.13.5", "react": "^18.3.1", diff --git a/packages/apps/job-launcher/client/package.json b/packages/apps/job-launcher/client/package.json index 36bbddc8c3..e4ed76e7c7 100644 --- a/packages/apps/job-launcher/client/package.json +++ b/packages/apps/job-launcher/client/package.json @@ -18,7 +18,7 @@ "@stripe/stripe-js": "^4.2.0", "@tanstack/query-sync-storage-persister": "^5.68.0", "@tanstack/react-query": "^5.67.2", - "@tanstack/react-query-persist-client": "^5.67.2", + "@tanstack/react-query-persist-client": "^5.80.7", "axios": "^1.1.3", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.12", diff --git a/packages/apps/staking/package.json b/packages/apps/staking/package.json index 50cebcdbd2..57b96a50c5 100644 --- a/packages/apps/staking/package.json +++ b/packages/apps/staking/package.json @@ -33,7 +33,7 @@ "@mui/material": "^5.16.7", "@tanstack/query-sync-storage-persister": "^5.68.0", "@tanstack/react-query": "^5.67.2", - "@tanstack/react-query-persist-client": "^5.67.2", + "@tanstack/react-query-persist-client": "^5.80.7", "axios": "^1.7.2", "ethers": "^6.13.5", "react": "^18.3.1", diff --git a/yarn.lock b/yarn.lock index 6c43344a11..2a4f6ef4a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4060,7 +4060,7 @@ __metadata: "@mui/material": "npm:^5.16.7" "@tanstack/query-sync-storage-persister": "npm:^5.68.0" "@tanstack/react-query": "npm:^5.67.2" - "@tanstack/react-query-persist-client": "npm:^5.67.2" + "@tanstack/react-query-persist-client": "npm:^5.80.7" "@types/react": "npm:^18.3.12" "@types/react-dom": "npm:^18.3.0" "@types/react-router-dom": "npm:^5.3.3" @@ -4321,7 +4321,7 @@ __metadata: "@stripe/stripe-js": "npm:^4.2.0" "@tanstack/query-sync-storage-persister": "npm:^5.68.0" "@tanstack/react-query": "npm:^5.67.2" - "@tanstack/react-query-persist-client": "npm:^5.67.2" + "@tanstack/react-query-persist-client": "npm:^5.80.7" "@types/file-saver": "npm:^2.0.7" "@types/react": "npm:^18.3.12" "@types/react-dom": "npm:^18.2.25" @@ -4543,7 +4543,7 @@ __metadata: "@mui/material": "npm:^5.16.7" "@tanstack/query-sync-storage-persister": "npm:^5.68.0" "@tanstack/react-query": "npm:^5.67.2" - "@tanstack/react-query-persist-client": "npm:^5.67.2" + "@tanstack/react-query-persist-client": "npm:^5.80.7" "@types/react": "npm:^18.3.12" "@types/react-dom": "npm:^18.3.0" "@types/react-router-dom": "npm:^5.3.3" @@ -9218,6 +9218,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:5.80.7": + version: 5.80.7 + resolution: "@tanstack/query-core@npm:5.80.7" + checksum: 10c0/bd96393e1a94aebc4d10da05e03de89ddce09d7266d0fcff2785480c544425d732f9fb03ecb93c96520980f57655f143452b9701f9bcb620cd07a04077018583 + languageName: node + linkType: hard + "@tanstack/query-devtools@npm:5.74.7": version: 5.74.7 resolution: "@tanstack/query-devtools@npm:5.74.7" @@ -9234,6 +9241,15 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-persist-client-core@npm:5.80.7": + version: 5.80.7 + resolution: "@tanstack/query-persist-client-core@npm:5.80.7" + dependencies: + "@tanstack/query-core": "npm:5.80.7" + checksum: 10c0/fb2d51a0d5d85d05b38b73504aa5de1320698b2e88819d579cba247e356faef1b6b1279fddce9ef5baf35ffd4bf6b3fec6e167a38709fed9e3a22aa96eacf6a2 + languageName: node + linkType: hard + "@tanstack/query-sync-storage-persister@npm:^5.68.0": version: 5.75.5 resolution: "@tanstack/query-sync-storage-persister@npm:5.75.5" @@ -9256,15 +9272,15 @@ __metadata: languageName: node linkType: hard -"@tanstack/react-query-persist-client@npm:^5.67.2": - version: 5.75.5 - resolution: "@tanstack/react-query-persist-client@npm:5.75.5" +"@tanstack/react-query-persist-client@npm:^5.80.7": + version: 5.80.7 + resolution: "@tanstack/react-query-persist-client@npm:5.80.7" dependencies: - "@tanstack/query-persist-client-core": "npm:5.75.5" + "@tanstack/query-persist-client-core": "npm:5.80.7" peerDependencies: - "@tanstack/react-query": ^5.75.5 + "@tanstack/react-query": ^5.80.7 react: ^18 || ^19 - checksum: 10c0/1fe684b70f756c667f1bccd827d9124e68c1335008aae8e14dfa9cd32c4a03ce0a72b761c55885033e4e889b3f6b1df0217c1f37662ecd85b3ad839c79b365af + checksum: 10c0/5159256a9afb2aae46e336d5a38f2d1f89c5e5f384f731853575a5d8216165d60731348bac5857bd75233779a0efbfdafb80bca15dd5c6eecd92dce4c2d6e9f9 languageName: node linkType: hard From cd08125547d4bf4cb5a0717a14badab3b1341ab0 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Thu, 26 Jun 2025 17:56:34 +0300 Subject: [PATCH 08/16] fix: sdk cd (#3420) --- .github/workflows/cd-core.yaml | 9 ++++----- .github/workflows/cd-node-sdk.yaml | 17 +++++++---------- .../typescript/human-protocol-sdk/package.json | 2 +- yarn.lock | 4 ++-- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cd-core.yaml b/.github/workflows/cd-core.yaml index 370fdd6a80..8fe2d7797c 100644 --- a/.github/workflows/cd-core.yaml +++ b/.github/workflows/cd-core.yaml @@ -24,8 +24,7 @@ jobs: file: ./packages/core/package.json field: version value: ${{ github.event.release.tag_name }} - - uses: JS-DevTools/npm-publish@v3 - with: - package: ./packages/core/package.json - access: public - token: ${{ secrets.NPM_TOKEN }} + - name: Publish package + run: yarn workspace @human-protocol/core npm publish --access public + env: + YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/cd-node-sdk.yaml b/.github/workflows/cd-node-sdk.yaml index 508d24689c..dc066b215b 100644 --- a/.github/workflows/cd-node-sdk.yaml +++ b/.github/workflows/cd-node-sdk.yaml @@ -23,14 +23,14 @@ jobs: run: yarn install --immutable - name: Build core package run: yarn build:core - - name: Change Node.js SDK version + - name: Change Node.js SDK version from release tag uses: jossef/action-set-json-field@v2 if: ${{ github.event_name != 'workflow_dispatch' }} with: file: ./packages/sdk/typescript/human-protocol-sdk/package.json field: version value: ${{ github.event.release.tag_name }} - - name: Change Node.js SDK version + - name: Change Node.js SDK version from workflow input uses: jossef/action-set-json-field@v2 if: ${{ github.event_name == 'workflow_dispatch' }} with: @@ -38,11 +38,8 @@ jobs: field: version value: ${{ github.event.inputs.release_version }} - name: Build SDK package - run: yarn build - working-directory: ./packages/sdk/typescript/human-protocol-sdk - - uses: JS-DevTools/npm-publish@v3 - name: Publish - with: - package: ./packages/sdk/typescript/human-protocol-sdk/package.json - access: public - token: ${{ secrets.NPM_TOKEN }} + run: yarn workspace @human-protocol/sdk build + - name: Publish package + run: yarn workspace @human-protocol/sdk npm publish --access public + env: + YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/sdk/typescript/human-protocol-sdk/package.json b/packages/sdk/typescript/human-protocol-sdk/package.json index c1a59d5d76..2f03e86181 100644 --- a/packages/sdk/typescript/human-protocol-sdk/package.json +++ b/packages/sdk/typescript/human-protocol-sdk/package.json @@ -38,7 +38,7 @@ ] }, "dependencies": { - "@human-protocol/core": "workspace:*", + "@human-protocol/core": "workspace:x", "axios": "^1.4.0", "ethers": "~6.13.5", "graphql": "^16.8.1", diff --git a/yarn.lock b/yarn.lock index 2a4f6ef4a9..aa4f0c7503 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3860,7 +3860,7 @@ __metadata: languageName: node linkType: hard -"@human-protocol/core@workspace:*, @human-protocol/core@workspace:packages/core": +"@human-protocol/core@workspace:*, @human-protocol/core@workspace:packages/core, @human-protocol/core@workspace:x": version: 0.0.0-use.local resolution: "@human-protocol/core@workspace:packages/core" dependencies: @@ -4515,7 +4515,7 @@ __metadata: version: 0.0.0-use.local resolution: "@human-protocol/sdk@workspace:packages/sdk/typescript/human-protocol-sdk" dependencies: - "@human-protocol/core": "workspace:*" + "@human-protocol/core": "workspace:x" axios: "npm:^1.4.0" eslint: "npm:^8.55.0" ethers: "npm:~6.13.5" From f952530366cca0d1407ab4027aa18a786baae869 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:08:13 +0300 Subject: [PATCH 09/16] chore(deps): bump @reown/appkit-adapter-wagmi from 1.7.4 to 1.7.10 (#3410) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: kirill --- packages/apps/human-app/frontend/package.json | 8 +- yarn.lock | 892 ++++++++++++++---- 2 files changed, 732 insertions(+), 168 deletions(-) diff --git a/packages/apps/human-app/frontend/package.json b/packages/apps/human-app/frontend/package.json index 42c9c07bd5..8180ab4879 100644 --- a/packages/apps/human-app/frontend/package.json +++ b/packages/apps/human-app/frontend/package.json @@ -27,8 +27,8 @@ "@mui/material": "^5.16.7", "@mui/system": "^5.15.14", "@mui/x-date-pickers": "^7.23.6", - "@reown/appkit": "^1.7.4", - "@reown/appkit-adapter-wagmi": "^1.7.4", + "@reown/appkit": "^1.7.11", + "@reown/appkit-adapter-wagmi": "^1.7.11", "@synaps-io/verify-sdk": "^4.0.45", "@tanstack/react-query": "^5.75.5", "@wagmi/core": "^2.17.1", @@ -50,9 +50,9 @@ "react-number-format": "^5.4.3", "react-router-dom": "^6.22.0", "serve": "^14.2.4", - "viem": "^2.29.1", + "viem": "^2.31.4", "vite-plugin-svgr": "^4.2.0", - "wagmi": "^2.15.2", + "wagmi": "^2.15.6", "zod": "^3.22.4", "zustand": "^4.5.0" }, diff --git a/yarn.lock b/yarn.lock index aa4f0c7503..c0928c81b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:^1.10.1, @adraffy/ens-normalize@npm:^1.8.8": +"@adraffy/ens-normalize@npm:^1.10.1, @adraffy/ens-normalize@npm:^1.11.0, @adraffy/ens-normalize@npm:^1.8.8": version: 1.11.0 resolution: "@adraffy/ens-normalize@npm:1.11.0" checksum: 10c0/5111d0f1a273468cb5661ed3cf46ee58de8f32f84e2ebc2365652e66c1ead82649df94c736804e2b9cfa831d30ef24e1cc3575d970dbda583416d3a98d8870a6 @@ -2251,6 +2251,18 @@ __metadata: languageName: node linkType: hard +"@coinbase/wallet-sdk@npm:4.3.3": + version: 4.3.3 + resolution: "@coinbase/wallet-sdk@npm:4.3.3" + dependencies: + "@noble/hashes": "npm:^1.4.0" + clsx: "npm:^1.2.1" + eventemitter3: "npm:^5.0.1" + preact: "npm:^10.24.2" + checksum: 10c0/528cbc62f42c151c45c61c4c73e120d6b98d88f5858edbc8cf50f3d96030103b5b0ae53415e2aa80d455a1be660d1f0dc73672aa64636359bd55aa25b0faea60 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -4194,8 +4206,8 @@ __metadata: "@mui/material": "npm:^5.16.7" "@mui/system": "npm:^5.15.14" "@mui/x-date-pickers": "npm:^7.23.6" - "@reown/appkit": "npm:^1.7.4" - "@reown/appkit-adapter-wagmi": "npm:^1.7.4" + "@reown/appkit": "npm:^1.7.11" + "@reown/appkit-adapter-wagmi": "npm:^1.7.11" "@synaps-io/verify-sdk": "npm:^4.0.45" "@tanstack/eslint-plugin-query": "npm:^5.60.1" "@tanstack/react-query": "npm:^5.75.5" @@ -4238,11 +4250,11 @@ __metadata: react-router-dom: "npm:^6.22.0" serve: "npm:^14.2.4" typescript: "npm:^5.6.3" - viem: "npm:^2.29.1" + viem: "npm:^2.31.4" vite: "npm:^6.2.4" vite-plugin-svgr: "npm:^4.2.0" vitest: "npm:^3.1.1" - wagmi: "npm:^2.15.2" + wagmi: "npm:^2.15.6" zod: "npm:^3.22.4" zustand: "npm:^4.5.0" languageName: unknown @@ -5306,7 +5318,7 @@ __metadata: languageName: node linkType: hard -"@lit/reactive-element@npm:^2.0.0, @lit/reactive-element@npm:^2.0.4, @lit/reactive-element@npm:^2.1.0": +"@lit/reactive-element@npm:^2.0.0, @lit/reactive-element@npm:^2.1.0": version: 2.1.0 resolution: "@lit/reactive-element@npm:2.1.0" dependencies: @@ -5593,6 +5605,13 @@ __metadata: languageName: node linkType: hard +"@msgpack/msgpack@npm:3.1.2": + version: 3.1.2 + resolution: "@msgpack/msgpack@npm:3.1.2" + checksum: 10c0/4fee6dbea70a485d3a787ac76dd43687f489d662f22919237db1f2abbc3c88070c1d3ad78417ce6e764bcd041051680284654021f52068e0aff82d570cb942d5 + languageName: node + linkType: hard + "@mswjs/interceptors@npm:^0.38.5": version: 0.38.6 resolution: "@mswjs/interceptors@npm:0.38.6" @@ -6438,7 +6457,7 @@ __metadata: languageName: node linkType: hard -"@noble/ciphers@npm:^1.0.0": +"@noble/ciphers@npm:1.3.0, @noble/ciphers@npm:^1.0.0, @noble/ciphers@npm:^1.3.0": version: 1.3.0 resolution: "@noble/ciphers@npm:1.3.0" checksum: 10c0/3ba6da645ce45e2f35e3b2e5c87ceba86b21dfa62b9466ede9edfb397f8116dae284f06652c0cd81d99445a2262b606632e868103d54ecc99fd946ae1af8cd37 @@ -6490,6 +6509,24 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.9.1": + version: 1.9.1 + resolution: "@noble/curves@npm:1.9.1" + dependencies: + "@noble/hashes": "npm:1.8.0" + checksum: 10c0/39c84dbfecdca80cfde2ecea4b06ef2ec1255a4df40158d22491d1400057a283f57b2b26c8b1331006e6e061db791f31d47764961c239437032e2f45e8888c1e + languageName: node + linkType: hard + +"@noble/curves@npm:1.9.2, @noble/curves@npm:^1.9.1": + version: 1.9.2 + resolution: "@noble/curves@npm:1.9.2" + dependencies: + "@noble/hashes": "npm:1.8.0" + checksum: 10c0/21d049ae4558beedbf5da0004407b72db84360fa29d64822d82dc9e80251e1ecb46023590cc4b20e70eed697d1b87279b4911dc39f8694c51c874289cfc8e9a7 + languageName: node + linkType: hard + "@noble/curves@npm:^1.6.0, @noble/curves@npm:^1.7.0, @noble/curves@npm:~1.9.0": version: 1.9.0 resolution: "@noble/curves@npm:1.9.0" @@ -6541,7 +6578,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.1.5, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:^1.6.1, @noble/hashes@npm:~1.8.0": +"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.1.5, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0, @noble/hashes@npm:^1.6.1, @noble/hashes@npm:^1.8.0, @noble/hashes@npm:~1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77 @@ -7561,28 +7598,39 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-adapter-wagmi@npm:^1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-adapter-wagmi@npm:1.7.4" - dependencies: - "@reown/appkit": "npm:1.7.4" - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-controllers": "npm:1.7.4" - "@reown/appkit-polyfills": "npm:1.7.4" - "@reown/appkit-scaffold-ui": "npm:1.7.4" - "@reown/appkit-utils": "npm:1.7.4" - "@reown/appkit-wallet": "npm:1.7.4" +"@reown/appkit-adapter-wagmi@npm:^1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-adapter-wagmi@npm:1.7.11" + dependencies: + "@reown/appkit": "npm:1.7.11" + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-controllers": "npm:1.7.11" + "@reown/appkit-polyfills": "npm:1.7.11" + "@reown/appkit-scaffold-ui": "npm:1.7.11" + "@reown/appkit-utils": "npm:1.7.11" + "@reown/appkit-wallet": "npm:1.7.11" "@wagmi/connectors": "npm:>=5.7.11" - "@walletconnect/universal-provider": "npm:2.20.1" + "@walletconnect/universal-provider": "npm:2.21.3" valtio: "npm:1.13.2" peerDependencies: "@wagmi/core": ">=2.16.7" - viem: ">=2.23.11" - wagmi: ">=2.14.15" + viem: ">=2.31.3" + wagmi: ">=2.15.6" dependenciesMeta: "@wagmi/connectors": optional: true - checksum: 10c0/186198a4999b42c62d06e200ddf34cd677a45c50e8fc925b1c8df05a996ef321791b88864766f6825fd9cab48f703c1aab63c1bc3044837fa49f38e3033a973b + checksum: 10c0/4baa19b037ec0290a50a6b8e914e39748a01bb20ae9e3b77bf4a5454a35e71efdeac8cba60f8dabf27adbc0ac93519afab7a8adbe8e185708b725296b87a8378 + languageName: node + linkType: hard + +"@reown/appkit-common@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-common@npm:1.7.11" + dependencies: + big.js: "npm:6.2.2" + dayjs: "npm:1.11.13" + viem: "npm:>=2.31.3" + checksum: 10c0/12151fa608f5f15fbb6fd8b6856e1851fa3a1f74e9b73cb47f253d3481bae1a3cc519a17bca47c2eb069bb53f5942ed133a01847359d0d16f22af794fb986a89 languageName: node linkType: hard @@ -7597,14 +7645,27 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-common@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-common@npm:1.7.4" +"@reown/appkit-common@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-common@npm:1.7.8" dependencies: big.js: "npm:6.2.2" dayjs: "npm:1.11.13" - viem: "npm:>=2.23.11" - checksum: 10c0/f33023c27b5846b4eccc480592afea7e2091d0dd19f023cdcf3c5b5428392aea6e7a1fd66c1ff32e5c47c5b3fe538eb7660e8522e766f40ebdff3f164efe2239 + viem: "npm:>=2.29.0" + checksum: 10c0/4b494f81c30596dc0de8fd7bac08111c47b8acaa0c85a5665f262f411f6055256f2ea8301c8deefa63a083288fc13e9e955f9855c5686a5d66ae536d2b5f7969 + languageName: node + linkType: hard + +"@reown/appkit-controllers@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-controllers@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-wallet": "npm:1.7.11" + "@walletconnect/universal-provider": "npm:2.21.3" + valtio: "npm:1.13.2" + viem: "npm:>=2.31.3" + checksum: 10c0/1b889b3709fbfeed27ec3d46108634f5eff146b5e295393421ef06c4eb2f00c0701ca5d1cd0ac5e11b017c8ba829acd6a3be287c7bc6d98588a3388dc7601e7b languageName: node linkType: hard @@ -7621,30 +7682,53 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-controllers@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-controllers@npm:1.7.4" +"@reown/appkit-controllers@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-controllers@npm:1.7.8" dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-wallet": "npm:1.7.4" - "@walletconnect/universal-provider": "npm:2.20.1" + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-wallet": "npm:1.7.8" + "@walletconnect/universal-provider": "npm:2.21.0" valtio: "npm:1.13.2" - viem: "npm:>=2.23.11" - checksum: 10c0/f59ea15537869b354efe9585c564540f29c2ed39a8ddb6156b17808c6ac5a1a977523a1b28d6e4e6823d30438eef1647c97693b0889a9b1aaff1151838d8e148 + viem: "npm:>=2.29.0" + checksum: 10c0/4119c2db6d99a9e306a0155a3b80d8ae7d1515ecb7d67467beae86fca3ccaa23c78a57b3eceffd82775c265e4e635933a5bdd325276b617b8990dc7aebadcc1a + languageName: node + linkType: hard + +"@reown/appkit-pay@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-pay@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-controllers": "npm:1.7.11" + "@reown/appkit-ui": "npm:1.7.11" + "@reown/appkit-utils": "npm:1.7.11" + lit: "npm:3.3.0" + valtio: "npm:1.13.2" + checksum: 10c0/2bdb48f809200c521eb5556fb38fadbfde0fb1bb576e1be86ea1f537e1231256e0323b1465e5496893fad43a424c41c7453fc28b502270ab000deb5062fa280b languageName: node linkType: hard -"@reown/appkit-pay@npm:1.6.10": - version: 1.6.10 - resolution: "@reown/appkit-pay@npm:1.6.10" +"@reown/appkit-pay@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-pay@npm:1.7.8" dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-controllers": "npm:1.7.4" - "@reown/appkit-ui": "npm:1.7.4" - "@reown/appkit-utils": "npm:1.7.4" - lit: "npm:3.2.1" + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-controllers": "npm:1.7.8" + "@reown/appkit-ui": "npm:1.7.8" + "@reown/appkit-utils": "npm:1.7.8" + lit: "npm:3.3.0" valtio: "npm:1.13.2" - checksum: 10c0/3b3e2c5a1f631e01f87ef89c28553846b86411cc15a7dd6e3d89fa20543df01d8a48212b77472c405bb54746877856a54b1920502bfd5e399842d79e2ae6263f + checksum: 10c0/bf53114d58641bead5947cb4acd39bdf202c002afe034a50b063a43ac8da2a680494d2178286942fc729392cf6e2eb94b06e113fe6dde5c5276925e807c6c7ab + languageName: node + linkType: hard + +"@reown/appkit-polyfills@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-polyfills@npm:1.7.11" + dependencies: + buffer: "npm:6.0.3" + checksum: 10c0/faaeaa162805dd9f675121365a27a9728d8d9c6adae31a77e0a748be655de5581f5e3365635e279255499874fc364481d32df08646a52118e9f96ee7daec2c45 languageName: node linkType: hard @@ -7657,12 +7741,26 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-polyfills@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-polyfills@npm:1.7.4" +"@reown/appkit-polyfills@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-polyfills@npm:1.7.8" dependencies: buffer: "npm:6.0.3" - checksum: 10c0/1a1a40bb3b532c4294bd51ad1300f6277641ee423fb0a9b11e4ae423bc700eba58d7491fc2a83626a7d69aa5d57b99df781e383b471c819e3befa6aa51f48631 + checksum: 10c0/4f1cfe738af5faf59476d1aba3bf4f6d83116bb32c8824d00fe0378453bb52220333b66603f25c5b87ed82f43319d81dfbdabda2028f6fd6f2fd4fcfb6bee203 + languageName: node + linkType: hard + +"@reown/appkit-scaffold-ui@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-scaffold-ui@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-controllers": "npm:1.7.11" + "@reown/appkit-ui": "npm:1.7.11" + "@reown/appkit-utils": "npm:1.7.11" + "@reown/appkit-wallet": "npm:1.7.11" + lit: "npm:3.3.0" + checksum: 10c0/93b25f09861e963830304ba688c0b3821d40b911ef71dda0b1d66b29b2433d42f7adda5b476260e7b65720acff881563433d4c75c6af8b77732f42f5c8ee04a9 languageName: node linkType: hard @@ -7680,17 +7778,30 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-scaffold-ui@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-scaffold-ui@npm:1.7.4" +"@reown/appkit-scaffold-ui@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-scaffold-ui@npm:1.7.8" dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-controllers": "npm:1.7.4" - "@reown/appkit-ui": "npm:1.7.4" - "@reown/appkit-utils": "npm:1.7.4" - "@reown/appkit-wallet": "npm:1.7.4" - lit: "npm:3.1.0" - checksum: 10c0/b452d5c2b4c4300b5bc83d10407533fb9889796250eb3c231cc1df18a34f09f11d378fc73f31260e10bbaab5a3d79a83919aab2aa017e75be88ea0790315f2ab + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-controllers": "npm:1.7.8" + "@reown/appkit-ui": "npm:1.7.8" + "@reown/appkit-utils": "npm:1.7.8" + "@reown/appkit-wallet": "npm:1.7.8" + lit: "npm:3.3.0" + checksum: 10c0/d07a27925da7c1e893f32d286c939f71149865a5d068ef1884b4c7cd3deb45327aca73fea9dabcde5f89aa355ceac0fb5b9ed952ccbb0e56a0c3464c07ed543e + languageName: node + linkType: hard + +"@reown/appkit-ui@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-ui@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-controllers": "npm:1.7.11" + "@reown/appkit-wallet": "npm:1.7.11" + lit: "npm:3.3.0" + qrcode: "npm:1.5.3" + checksum: 10c0/be94df62feb02387c7dc984a4f48f7af5b350adbe04ea88a392e0a14a272f3c24d76afea2070a15e568e286847f85d0be8e216cca22784614bc7e6df77855069 languageName: node linkType: hard @@ -7707,16 +7818,35 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-ui@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-ui@npm:1.7.4" +"@reown/appkit-ui@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-ui@npm:1.7.8" dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-controllers": "npm:1.7.4" - "@reown/appkit-wallet": "npm:1.7.4" - lit: "npm:3.1.0" + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-controllers": "npm:1.7.8" + "@reown/appkit-wallet": "npm:1.7.8" + lit: "npm:3.3.0" qrcode: "npm:1.5.3" - checksum: 10c0/0a0b039390b4e92c856657c44e4903429fae95bc1ee73388af3d81d372560e85736ab2d21717d14ffcd614dcae2fd53ac1891231ea1d9796c6f23cf03d4cc0fe + checksum: 10c0/f4b0df3124d419d355358f56fd54163a12802aaebfc9d75b7396ac3a2c443747216791f590c0de27bef764140a76319774d905e89e018f9fdf26dd24ba16f232 + languageName: node + linkType: hard + +"@reown/appkit-utils@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-utils@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-controllers": "npm:1.7.11" + "@reown/appkit-polyfills": "npm:1.7.11" + "@reown/appkit-wallet": "npm:1.7.11" + "@wallet-standard/wallet": "npm:1.1.0" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/universal-provider": "npm:2.21.3" + valtio: "npm:1.13.2" + viem: "npm:>=2.31.3" + peerDependencies: + valtio: 1.13.2 + checksum: 10c0/a6a9921ea36cbb9b193319af4dba273c26ef4598be1f681c7349b94067b6ba497fff018933fa42d699a14adef68f5b7c6e1ace7253aa93147bcbbc3c26195d55 languageName: node linkType: hard @@ -7738,21 +7868,33 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-utils@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-utils@npm:1.7.4" +"@reown/appkit-utils@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-utils@npm:1.7.8" dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-controllers": "npm:1.7.4" - "@reown/appkit-polyfills": "npm:1.7.4" - "@reown/appkit-wallet": "npm:1.7.4" + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-controllers": "npm:1.7.8" + "@reown/appkit-polyfills": "npm:1.7.8" + "@reown/appkit-wallet": "npm:1.7.8" "@walletconnect/logger": "npm:2.1.2" - "@walletconnect/universal-provider": "npm:2.20.1" + "@walletconnect/universal-provider": "npm:2.21.0" valtio: "npm:1.13.2" - viem: "npm:>=2.23.11" + viem: "npm:>=2.29.0" peerDependencies: valtio: 1.13.2 - checksum: 10c0/343b626436a748d59ca77ed0347aa050f007644c27cd4669a8f71fd50c40da44c3fde16909c35a9b9bf3590ba092a0fbe159a33a321ccf8149c5028fc6d1a819 + checksum: 10c0/93054dddaf90823674568639c2a1119fba07a7e5461f277e8f8ae27bd7018a7aa023ddd648b0aaa80b2cdb46e8ad5bfc2f99c8fdf1996e2d7d7c5aff1c856427 + languageName: node + linkType: hard + +"@reown/appkit-wallet@npm:1.7.11": + version: 1.7.11 + resolution: "@reown/appkit-wallet@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-polyfills": "npm:1.7.11" + "@walletconnect/logger": "npm:2.1.2" + zod: "npm:3.22.4" + checksum: 10c0/c820b6354d6e565effc7ff0115b754d3bd44dfe07f5d46ae9ece364763821213523dfcf829944bd49e76b15b548b9954a553a167af7c0a4cefa3bea68a3ba9e6 languageName: node linkType: hard @@ -7768,15 +7910,37 @@ __metadata: languageName: node linkType: hard -"@reown/appkit-wallet@npm:1.7.4": - version: 1.7.4 - resolution: "@reown/appkit-wallet@npm:1.7.4" +"@reown/appkit-wallet@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit-wallet@npm:1.7.8" dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-polyfills": "npm:1.7.4" + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-polyfills": "npm:1.7.8" "@walletconnect/logger": "npm:2.1.2" zod: "npm:3.22.4" - checksum: 10c0/ddd205b7a9c7fa127e196e8cf9afe071203ff729d39e8acee4f4459e7e6de4712a61402e95047bfc3dcd2460da061041ca031dcdd1a9f62be829befc32d4bd5a + checksum: 10c0/8021cc184dac24ad9828340924deb8b7142025c585710a634804968b6163899a4061f96ba00f614de2287a82f53562de4f053799164413acb5694aa0bcd35783 + languageName: node + linkType: hard + +"@reown/appkit@npm:1.7.11, @reown/appkit@npm:^1.7.11": + version: 1.7.11 + resolution: "@reown/appkit@npm:1.7.11" + dependencies: + "@reown/appkit-common": "npm:1.7.11" + "@reown/appkit-controllers": "npm:1.7.11" + "@reown/appkit-pay": "npm:1.7.11" + "@reown/appkit-polyfills": "npm:1.7.11" + "@reown/appkit-scaffold-ui": "npm:1.7.11" + "@reown/appkit-ui": "npm:1.7.11" + "@reown/appkit-utils": "npm:1.7.11" + "@reown/appkit-wallet": "npm:1.7.11" + "@walletconnect/types": "npm:2.21.3" + "@walletconnect/universal-provider": "npm:2.21.3" + bs58: "npm:6.0.0" + semver: "npm:7.7.2" + valtio: "npm:1.13.2" + viem: "npm:>=2.31.3" + checksum: 10c0/0b8436da1f0c95e84b2157067294656816a44559e9a5102697850bdfedc973cd1a9aa77c63d45d06408a70209b4590d1efde57dbc2f373e27172d18a10d2e40e languageName: node linkType: hard @@ -7800,24 +7964,24 @@ __metadata: languageName: node linkType: hard -"@reown/appkit@npm:1.7.4, @reown/appkit@npm:^1.7.4": - version: 1.7.4 - resolution: "@reown/appkit@npm:1.7.4" - dependencies: - "@reown/appkit-common": "npm:1.7.4" - "@reown/appkit-controllers": "npm:1.7.4" - "@reown/appkit-pay": "npm:1.6.10" - "@reown/appkit-polyfills": "npm:1.7.4" - "@reown/appkit-scaffold-ui": "npm:1.7.4" - "@reown/appkit-ui": "npm:1.7.4" - "@reown/appkit-utils": "npm:1.7.4" - "@reown/appkit-wallet": "npm:1.7.4" - "@walletconnect/types": "npm:2.20.1" - "@walletconnect/universal-provider": "npm:2.20.1" +"@reown/appkit@npm:1.7.8": + version: 1.7.8 + resolution: "@reown/appkit@npm:1.7.8" + dependencies: + "@reown/appkit-common": "npm:1.7.8" + "@reown/appkit-controllers": "npm:1.7.8" + "@reown/appkit-pay": "npm:1.7.8" + "@reown/appkit-polyfills": "npm:1.7.8" + "@reown/appkit-scaffold-ui": "npm:1.7.8" + "@reown/appkit-ui": "npm:1.7.8" + "@reown/appkit-utils": "npm:1.7.8" + "@reown/appkit-wallet": "npm:1.7.8" + "@walletconnect/types": "npm:2.21.0" + "@walletconnect/universal-provider": "npm:2.21.0" bs58: "npm:6.0.0" valtio: "npm:1.13.2" - viem: "npm:>=2.23.11" - checksum: 10c0/ee2a37d64ca2655269cc6115e02241ce05fff78af3a15e0e8007000ba8503bcffe792e8bd9f2b3c786d5ca84b87f1f471492b55e535311dd0a27dde6aa2a797c + viem: "npm:>=2.29.0" + checksum: 10c0/d5c8ba49f9eb4e2446219d9f84a603a11cb940379f5f37e46da507e94bcd2657a9fc232248b1692f3fa6c6105dd582442da0c745d13c3cbb89860db8a0bb39c6 languageName: node linkType: hard @@ -8055,6 +8219,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:1.2.6": + version: 1.2.6 + resolution: "@scure/base@npm:1.2.6" + checksum: 10c0/49bd5293371c4e062cb6ba689c8fe3ea3981b7bb9c000400dc4eafa29f56814cdcdd27c04311c2fec34de26bc373c593a1d6ca6d754398a488d587943b7c128a + languageName: node + linkType: hard + "@scure/base@npm:^1.1.3, @scure/base@npm:~1.2.2, @scure/base@npm:~1.2.4, @scure/base@npm:~1.2.5": version: 1.2.5 resolution: "@scure/base@npm:1.2.5" @@ -8102,7 +8273,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:^1.5.0": +"@scure/bip32@npm:1.7.0, @scure/bip32@npm:^1.5.0, @scure/bip32@npm:^1.7.0": version: 1.7.0 resolution: "@scure/bip32@npm:1.7.0" dependencies: @@ -8143,7 +8314,7 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:^1.4.0": +"@scure/bip39@npm:1.6.0, @scure/bip39@npm:^1.4.0, @scure/bip39@npm:^1.6.0": version: 1.6.0 resolution: "@scure/bip39@npm:1.6.0" dependencies: @@ -11270,6 +11441,27 @@ __metadata: languageName: node linkType: hard +"@wagmi/connectors@npm:5.8.5": + version: 5.8.5 + resolution: "@wagmi/connectors@npm:5.8.5" + dependencies: + "@coinbase/wallet-sdk": "npm:4.3.3" + "@metamask/sdk": "npm:0.32.0" + "@safe-global/safe-apps-provider": "npm:0.18.6" + "@safe-global/safe-apps-sdk": "npm:9.1.0" + "@walletconnect/ethereum-provider": "npm:2.21.1" + cbw-sdk: "npm:@coinbase/wallet-sdk@3.9.3" + peerDependencies: + "@wagmi/core": 2.17.3 + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/d00dbeefe090a2dd740594d560108998cee4ce02e789e4a3b591a742b3622a7a47f2504e0e1c72315935696fbda26446c496491972dcdde1e1b3f2173ceb2b8d + languageName: node + linkType: hard + "@wagmi/core@npm:2.17.1, @wagmi/core@npm:^2.17.1": version: 2.17.1 resolution: "@wagmi/core@npm:2.17.1" @@ -11290,6 +11482,42 @@ __metadata: languageName: node linkType: hard +"@wagmi/core@npm:2.17.3": + version: 2.17.3 + resolution: "@wagmi/core@npm:2.17.3" + dependencies: + eventemitter3: "npm:5.0.1" + mipd: "npm:0.0.7" + zustand: "npm:5.0.0" + peerDependencies: + "@tanstack/query-core": ">=5.0.0" + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + "@tanstack/query-core": + optional: true + typescript: + optional: true + checksum: 10c0/128875066323c87242293cfe5b22fe596dd8a55c79efeb2a7b36b6a1acd549e217cdb30215119bd203580b71118cd197274eed83c120dce5846b1894140b79be + languageName: node + linkType: hard + +"@wallet-standard/base@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/base@npm:1.1.0" + checksum: 10c0/4cae344d5a74ba4b7d063b649b191f2267bd11ea9573ebb9e78874163c03b58e3ec531bb296d0a8d7941bc09231761d97afb4c6ca8c0dc399c81d39884b4e408 + languageName: node + linkType: hard + +"@wallet-standard/wallet@npm:1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/wallet@npm:1.1.0" + dependencies: + "@wallet-standard/base": "npm:^1.1.0" + checksum: 10c0/aa53460568f209d4e38030ee5e98d4f6ea6fec159a1e7fb5a3ee81cf8d91c89f0be86b7188dbf0bb9803d10608bf88bd824f73cd6800823279738827304038e5 + languageName: node + linkType: hard + "@walletconnect/core@npm:2.19.2": version: 2.19.2 resolution: "@walletconnect/core@npm:2.19.2" @@ -11340,9 +11568,9 @@ __metadata: languageName: node linkType: hard -"@walletconnect/core@npm:2.20.1": - version: 2.20.1 - resolution: "@walletconnect/core@npm:2.20.1" +"@walletconnect/core@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/core@npm:2.21.0" dependencies: "@walletconnect/heartbeat": "npm:1.2.2" "@walletconnect/jsonrpc-provider": "npm:1.0.14" @@ -11355,13 +11583,63 @@ __metadata: "@walletconnect/relay-auth": "npm:1.1.0" "@walletconnect/safe-json": "npm:1.0.2" "@walletconnect/time": "npm:1.0.2" - "@walletconnect/types": "npm:2.20.1" - "@walletconnect/utils": "npm:2.20.1" + "@walletconnect/types": "npm:2.21.0" + "@walletconnect/utils": "npm:2.21.0" "@walletconnect/window-getters": "npm:1.0.1" es-toolkit: "npm:1.33.0" events: "npm:3.3.0" uint8arrays: "npm:3.1.0" - checksum: 10c0/1d0c6936651ae1109d017bd626e94dacfbd487b29c2133b37dda9c120c90a0a961d1b7743db9dc3c6a34ffa53b7f5f5d4d44ed9336754f7c21c029104327ad2f + checksum: 10c0/4b4915221baa2f2f4157594dccb8184e98a503a852c675d49ed59b698d19315f3a976ef01f4021ac97623f2406c55a96a3a991296fcf9cf6b3745991ac68fb41 + languageName: node + linkType: hard + +"@walletconnect/core@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/core@npm:2.21.1" + dependencies: + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/jsonrpc-ws-connection": "npm:1.0.16" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.21.1" + "@walletconnect/utils": "npm:2.21.1" + "@walletconnect/window-getters": "npm:1.0.1" + es-toolkit: "npm:1.33.0" + events: "npm:3.3.0" + uint8arrays: "npm:3.1.0" + checksum: 10c0/78664ab17591cd023dfe497e89db2e1d330354ce1b88fe4a75a700ee5a581eaa1ad0a61549b0c269587cc5d8d932155ff01ce98d74b506c41b9c172ca2ec252e + languageName: node + linkType: hard + +"@walletconnect/core@npm:2.21.3": + version: 2.21.3 + resolution: "@walletconnect/core@npm:2.21.3" + dependencies: + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/jsonrpc-ws-connection": "npm:1.0.16" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.21.3" + "@walletconnect/utils": "npm:2.21.3" + "@walletconnect/window-getters": "npm:1.0.1" + es-toolkit: "npm:1.39.3" + events: "npm:3.3.0" + uint8arrays: "npm:3.1.1" + checksum: 10c0/ac79553046409d4f01e5a83f31bcb999abe1b23c43679cd0716fb867099d0278cce2d8e6405ec9d6fa0d30716c97d8fd4c83859122930f5f0775e297e05585b2 languageName: node linkType: hard @@ -11393,6 +11671,25 @@ __metadata: languageName: node linkType: hard +"@walletconnect/ethereum-provider@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/ethereum-provider@npm:2.21.1" + dependencies: + "@reown/appkit": "npm:1.7.8" + "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/sign-client": "npm:2.21.1" + "@walletconnect/types": "npm:2.21.1" + "@walletconnect/universal-provider": "npm:2.21.1" + "@walletconnect/utils": "npm:2.21.1" + events: "npm:3.3.0" + checksum: 10c0/91247045202a7f040338f7588d7c323cc845ac47c6ca8749f38ab07ac30a219a1ef6698ee03b97f5d48ca57e3fa1e1863c9fbc1371a1471501b5843014cacd18 + languageName: node + linkType: hard + "@walletconnect/events@npm:1.0.1, @walletconnect/events@npm:^1.0.1": version: 1.0.1 resolution: "@walletconnect/events@npm:1.0.1" @@ -11561,20 +11858,54 @@ __metadata: languageName: node linkType: hard -"@walletconnect/sign-client@npm:2.20.1": - version: 2.20.1 - resolution: "@walletconnect/sign-client@npm:2.20.1" +"@walletconnect/sign-client@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/sign-client@npm:2.21.0" + dependencies: + "@walletconnect/core": "npm:2.21.0" + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.21.0" + "@walletconnect/utils": "npm:2.21.0" + events: "npm:3.3.0" + checksum: 10c0/72cca06c99a2cf49aeaefaa13783fa01505d358a578f4b18c1742b790505fb95bf4d9d80a89092531a16e257f16b2d73c3bc6846c3ff0ecafbaf5394dbe0519f + languageName: node + linkType: hard + +"@walletconnect/sign-client@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/sign-client@npm:2.21.1" + dependencies: + "@walletconnect/core": "npm:2.21.1" + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.21.1" + "@walletconnect/utils": "npm:2.21.1" + events: "npm:3.3.0" + checksum: 10c0/ed33f8150a4d9966ca80c6455557fb2aa8f396c48ca4e4f56ff0bd0f97d53dafcc3609073d7c31f54d3ea87392045ddfbca2d7a0b8544eaa5c618a3a92f90b66 + languageName: node + linkType: hard + +"@walletconnect/sign-client@npm:2.21.3": + version: 2.21.3 + resolution: "@walletconnect/sign-client@npm:2.21.3" dependencies: - "@walletconnect/core": "npm:2.20.1" + "@walletconnect/core": "npm:2.21.3" "@walletconnect/events": "npm:1.0.1" "@walletconnect/heartbeat": "npm:1.2.2" "@walletconnect/jsonrpc-utils": "npm:1.0.8" "@walletconnect/logger": "npm:2.1.2" "@walletconnect/time": "npm:1.0.2" - "@walletconnect/types": "npm:2.20.1" - "@walletconnect/utils": "npm:2.20.1" + "@walletconnect/types": "npm:2.21.3" + "@walletconnect/utils": "npm:2.21.3" events: "npm:3.3.0" - checksum: 10c0/23be476b309b6a97f117c1274b83a5b710a6d716dd5fde3fe8b1cdf3eccb41cb710276d109e42925c135395a2fc89678e28e790eb6ac6363eda5597f3f00d43a + checksum: 10c0/40d04474896ca8b3291457b8746a6e8fa85aa50a52dc121f6e8a95d82947010a859b7649317555a3503cd861667f966389599cb7edf5ab20d41acbea1c040b45 languageName: node linkType: hard @@ -11615,9 +11946,37 @@ __metadata: languageName: node linkType: hard -"@walletconnect/types@npm:2.20.1": - version: 2.20.1 - resolution: "@walletconnect/types@npm:2.20.1" +"@walletconnect/types@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/types@npm:2.21.0" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + events: "npm:3.3.0" + checksum: 10c0/1b969b045b77833315c56ae6948e551c175b6496e894be7b19db88a376d16a662a8b728ec753e01336053262ca16567ae36eed48f6dfe32cdf8d01cf66211588 + languageName: node + linkType: hard + +"@walletconnect/types@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/types@npm:2.21.1" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + events: "npm:3.3.0" + checksum: 10c0/60468f50ea7c95ac5269a9e53a0417d50302978a927c042a0376d4dcb0d336f2187a129e8c602a173ccf020a193a4dde50f3f9f74d5b8da0a9801aa9d672458e + languageName: node + linkType: hard + +"@walletconnect/types@npm:2.21.3": + version: 2.21.3 + resolution: "@walletconnect/types@npm:2.21.3" dependencies: "@walletconnect/events": "npm:1.0.1" "@walletconnect/heartbeat": "npm:1.2.2" @@ -11625,7 +11984,7 @@ __metadata: "@walletconnect/keyvaluestorage": "npm:1.1.1" "@walletconnect/logger": "npm:2.1.2" events: "npm:3.3.0" - checksum: 10c0/d42370fa751dca0b2a28182c2e1a4aa5d53548067b7046d94db6c027f2a86cc2c71cf85c09b604a44703cbf62d57859cb78ff1b1a745060aa23964d2d1f92059 + checksum: 10c0/0ae35ad796388412a836eba8e50f6a234d038ea51172546d11f282d6d6042faab0b3e3e8485410aaccf0be4c8fd42868403e3fbd2d907ba5a474304a245ca47b languageName: node linkType: hard @@ -11669,9 +12028,29 @@ __metadata: languageName: node linkType: hard -"@walletconnect/universal-provider@npm:2.20.1": - version: 2.20.1 - resolution: "@walletconnect/universal-provider@npm:2.20.1" +"@walletconnect/universal-provider@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/universal-provider@npm:2.21.0" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/sign-client": "npm:2.21.0" + "@walletconnect/types": "npm:2.21.0" + "@walletconnect/utils": "npm:2.21.0" + es-toolkit: "npm:1.33.0" + events: "npm:3.3.0" + checksum: 10c0/856fa961926b15bd91e6a35a2f7f3db832d7a81fdb04ee0553ac882ac8c307a42bdeb439b2b6bb4ca0b834953e933f4d380883d1ad73cbbc7e88568091fa8aab + languageName: node + linkType: hard + +"@walletconnect/universal-provider@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/universal-provider@npm:2.21.1" dependencies: "@walletconnect/events": "npm:1.0.1" "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" @@ -11680,12 +12059,32 @@ __metadata: "@walletconnect/jsonrpc-utils": "npm:1.0.8" "@walletconnect/keyvaluestorage": "npm:1.1.1" "@walletconnect/logger": "npm:2.1.2" - "@walletconnect/sign-client": "npm:2.20.1" - "@walletconnect/types": "npm:2.20.1" - "@walletconnect/utils": "npm:2.20.1" + "@walletconnect/sign-client": "npm:2.21.1" + "@walletconnect/types": "npm:2.21.1" + "@walletconnect/utils": "npm:2.21.1" es-toolkit: "npm:1.33.0" events: "npm:3.3.0" - checksum: 10c0/5e02c2f724935269cbe8bb230b8dee0f1ec987a85d03add91e9a986aa1fa5922e89ca6b576f0270bc1049b1ec838bacc88bf4f7694bcd6eae89c093bc1eae2e7 + checksum: 10c0/75e97c9a52025b18c05d2e029384492c8a9f82044971be6fef1856962984ff6dc48805fc732d1cd748979ab19a6eb688c9e8ed7a0944f57efd384d1ab6375252 + languageName: node + linkType: hard + +"@walletconnect/universal-provider@npm:2.21.3": + version: 2.21.3 + resolution: "@walletconnect/universal-provider@npm:2.21.3" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:2.1.2" + "@walletconnect/sign-client": "npm:2.21.3" + "@walletconnect/types": "npm:2.21.3" + "@walletconnect/utils": "npm:2.21.3" + es-toolkit: "npm:1.39.3" + events: "npm:3.3.0" + checksum: 10c0/bb62a92dbafa33149a285b434b49b1f0d73237191cb001330afd3b3b02e2182d13b44d39db6d3d5cf693ad4476e4341a30bad09d4ad7be29bb9d8cddc003ea18 languageName: node linkType: hard @@ -11739,9 +12138,34 @@ __metadata: languageName: node linkType: hard -"@walletconnect/utils@npm:2.20.1": - version: 2.20.1 - resolution: "@walletconnect/utils@npm:2.20.1" +"@walletconnect/utils@npm:2.21.0": + version: 2.21.0 + resolution: "@walletconnect/utils@npm:2.21.0" + dependencies: + "@noble/ciphers": "npm:1.2.1" + "@noble/curves": "npm:1.8.1" + "@noble/hashes": "npm:1.7.1" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.21.0" + "@walletconnect/window-getters": "npm:1.0.1" + "@walletconnect/window-metadata": "npm:1.0.1" + bs58: "npm:6.0.0" + detect-browser: "npm:5.3.0" + query-string: "npm:7.1.3" + uint8arrays: "npm:3.1.0" + viem: "npm:2.23.2" + checksum: 10c0/2a091072aba6351f1576e459056e54b6af14a900fe0bc0dcff06df7abb58fb7f4ed2637905d62ae2e85188dfecc65867ced3b28b3475bd7c1327a276745cb25e + languageName: node + linkType: hard + +"@walletconnect/utils@npm:2.21.1": + version: 2.21.1 + resolution: "@walletconnect/utils@npm:2.21.1" dependencies: "@noble/ciphers": "npm:1.2.1" "@noble/curves": "npm:1.8.1" @@ -11752,7 +12176,7 @@ __metadata: "@walletconnect/relay-auth": "npm:1.1.0" "@walletconnect/safe-json": "npm:1.0.2" "@walletconnect/time": "npm:1.0.2" - "@walletconnect/types": "npm:2.20.1" + "@walletconnect/types": "npm:2.21.1" "@walletconnect/window-getters": "npm:1.0.1" "@walletconnect/window-metadata": "npm:1.0.1" bs58: "npm:6.0.0" @@ -11760,7 +12184,35 @@ __metadata: query-string: "npm:7.1.3" uint8arrays: "npm:3.1.0" viem: "npm:2.23.2" - checksum: 10c0/8a55de531606d57ac7e39969f2bc210289fb162f27f4bdf9d2507bd4e96d2ae3f2c07329012a67db32af1792d3e2b98148ad7d17db2ed57c5e0e5510215b61c2 + checksum: 10c0/367cf46f2534805fd4555564f2b1056fcc927464b9f1b9be495e1f1c599ec43cf5cc75ea1f01bec92a0e85fba029b6298a77820b1e9e61a7bf7e1bbde3525811 + languageName: node + linkType: hard + +"@walletconnect/utils@npm:2.21.3": + version: 2.21.3 + resolution: "@walletconnect/utils@npm:2.21.3" + dependencies: + "@msgpack/msgpack": "npm:3.1.2" + "@noble/ciphers": "npm:1.3.0" + "@noble/curves": "npm:1.9.2" + "@noble/hashes": "npm:1.8.0" + "@scure/base": "npm:1.2.6" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.21.3" + "@walletconnect/window-getters": "npm:1.0.1" + "@walletconnect/window-metadata": "npm:1.0.1" + blakejs: "npm:1.2.1" + bs58: "npm:6.0.0" + detect-browser: "npm:5.3.0" + query-string: "npm:7.1.3" + uint8arrays: "npm:3.1.1" + viem: "npm:2.31.0" + checksum: 10c0/13e67c73b8923bdfd906f302a130f3534675f1174cce3cb79ccb8988772165822d5137c3fc1b6d26a5f894e385e49d1bfd3f693a304788d57a759cffa2335484 languageName: node linkType: hard @@ -12082,7 +12534,7 @@ __metadata: languageName: node linkType: hard -"abitype@npm:1.0.8, abitype@npm:^1.0.6": +"abitype@npm:1.0.8, abitype@npm:^1.0.6, abitype@npm:^1.0.8": version: 1.0.8 resolution: "abitype@npm:1.0.8" peerDependencies: @@ -13147,7 +13599,7 @@ __metadata: languageName: node linkType: hard -"blakejs@npm:^1.1.0": +"blakejs@npm:1.2.1, blakejs@npm:^1.1.0": version: 1.2.1 resolution: "blakejs@npm:1.2.1" checksum: 10c0/c284557ce55b9c70203f59d381f1b85372ef08ee616a90162174d1291a45d3e5e809fdf9edab6e998740012538515152471dc4f1f9dbfa974ba2b9c1f7b9aad7 @@ -16014,6 +16466,18 @@ __metadata: languageName: node linkType: hard +"es-toolkit@npm:1.39.3": + version: 1.39.3 + resolution: "es-toolkit@npm:1.39.3" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + checksum: 10c0/1c85e518b1d129d38fdc5796af353f45e8dcb8a20968ff25da1ae1749fc4a36f914570fcd992df33b47c7bca9f3866d53e4e6fa6411c21eb424e99a3e479c96e + languageName: node + linkType: hard + "es6-promise@npm:^4.0.3": version: 4.2.8 resolution: "es6-promise@npm:4.2.8" @@ -19902,6 +20366,15 @@ __metadata: languageName: node linkType: hard +"isows@npm:1.0.7": + version: 1.0.7 + resolution: "isows@npm:1.0.7" + peerDependencies: + ws: "*" + checksum: 10c0/43c41fe89c7c07258d0be3825f87e12da8ac9023c5b5ae6741ec00b2b8169675c04331ea73ef8c172d37a6747066f4dc93947b17cd369f92828a3b3e741afbda + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" @@ -21197,7 +21670,7 @@ __metadata: languageName: node linkType: hard -"lit-element@npm:^4.0.0, lit-element@npm:^4.1.0": +"lit-element@npm:^4.0.0, lit-element@npm:^4.2.0": version: 4.2.0 resolution: "lit-element@npm:4.2.0" dependencies: @@ -21208,7 +21681,7 @@ __metadata: languageName: node linkType: hard -"lit-html@npm:^3.1.0, lit-html@npm:^3.2.0, lit-html@npm:^3.3.0": +"lit-html@npm:^3.1.0, lit-html@npm:^3.3.0": version: 3.3.0 resolution: "lit-html@npm:3.3.0" dependencies: @@ -21228,14 +21701,14 @@ __metadata: languageName: node linkType: hard -"lit@npm:3.2.1": - version: 3.2.1 - resolution: "lit@npm:3.2.1" +"lit@npm:3.3.0": + version: 3.3.0 + resolution: "lit@npm:3.3.0" dependencies: - "@lit/reactive-element": "npm:^2.0.4" - lit-element: "npm:^4.1.0" - lit-html: "npm:^3.2.0" - checksum: 10c0/064a31459fe54ad052c0685d058dd5aef089ddc97a247888ef91a0356dfef60c8cc531e48077bbd2cb4e9f48cb86f0ff0951bb535f1d9f144d2351f253291f66 + "@lit/reactive-element": "npm:^2.1.0" + lit-element: "npm:^4.2.0" + lit-html: "npm:^3.3.0" + checksum: 10c0/27e6d109c04c8995f47c82a546407c5ed8d399705f9511d1f3ee562eb1ab4bc00fae5ec897da55fb50f202b2a659466e23cccd809d039e7d4f935fcecb2bc6a7 languageName: node linkType: hard @@ -23232,6 +23705,48 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.7.1": + version: 0.7.1 + resolution: "ox@npm:0.7.1" + dependencies: + "@adraffy/ens-normalize": "npm:^1.10.1" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + "@scure/bip32": "npm:^1.5.0" + "@scure/bip39": "npm:^1.4.0" + abitype: "npm:^1.0.6" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/15370d76f7e5fe1b06c5b9986bc709a8c433e4242660900b3d1adb2a56c8f762a2010a9166bdb95bdf531806cde7891911456c7ec8ba135fc232a5d5037ac673 + languageName: node + linkType: hard + +"ox@npm:0.8.1": + version: 0.8.1 + resolution: "ox@npm:0.8.1" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:^1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.0.8" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/3d04df384a35c94b21a29d867ee3735acf9a975d46ffb0a26cc438b92f1e4952b2b3cddb74b4213e88d2988e82687db9b85c1018c5d4b24737b1c3d7cb7c809e + languageName: node + linkType: hard + "p-defer@npm:^3.0.0": version: 3.0.0 resolution: "p-defer@npm:3.0.0" @@ -25714,6 +26229,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.7.2": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea + languageName: node + linkType: hard + "semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -28046,7 +28570,7 @@ __metadata: languageName: node linkType: hard -"uint8arrays@npm:^3.0.0": +"uint8arrays@npm:3.1.1, uint8arrays@npm:^3.0.0": version: 3.1.1 resolution: "uint8arrays@npm:3.1.1" dependencies: @@ -28655,6 +29179,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:2.31.0": + version: 2.31.0 + resolution: "viem@npm:2.31.0" + dependencies: + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:1.8.0" + "@scure/bip32": "npm:1.7.0" + "@scure/bip39": "npm:1.6.0" + abitype: "npm:1.0.8" + isows: "npm:1.0.7" + ox: "npm:0.7.1" + ws: "npm:8.18.2" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/4f327af609d41720f94664546eae1b8a892ae787630c0259a95ca145f7b07ef82387975b6ab8c223decd34ead69650119226af360d02ac7c17dbc4b60cfdf523 + languageName: node + linkType: hard + "viem@npm:2.x, viem@npm:>=2.23.11, viem@npm:^2.1.1, viem@npm:^2.27.0": version: 2.29.0 resolution: "viem@npm:2.29.0" @@ -28676,24 +29221,24 @@ __metadata: languageName: node linkType: hard -"viem@npm:^2.29.1": - version: 2.29.1 - resolution: "viem@npm:2.29.1" +"viem@npm:>=2.29.0, viem@npm:>=2.31.3, viem@npm:^2.31.4": + version: 2.31.4 + resolution: "viem@npm:2.31.4" dependencies: - "@noble/curves": "npm:1.8.2" - "@noble/hashes": "npm:1.7.2" - "@scure/bip32": "npm:1.6.2" - "@scure/bip39": "npm:1.5.4" + "@noble/curves": "npm:1.9.2" + "@noble/hashes": "npm:1.8.0" + "@scure/bip32": "npm:1.7.0" + "@scure/bip39": "npm:1.6.0" abitype: "npm:1.0.8" - isows: "npm:1.0.6" - ox: "npm:0.6.9" - ws: "npm:8.18.1" + isows: "npm:1.0.7" + ox: "npm:0.8.1" + ws: "npm:8.18.2" peerDependencies: typescript: ">=5.0.4" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/f2f0b46e9df1d6b48a19077101be9133e340664c5639585244d2c637bc717d83a9ae030476fdf27b57e84a42b33d5244f0b4eb144639ee44f8c54ace1d0c2612 + checksum: 10c0/cffc1c91b322a683758877c8a39916e984809847082343a297d3d637474b711c30e69fad2547f8e4617e6961797811a628d0fd6c3b4d105a7669d89e205edebc languageName: node linkType: hard @@ -28886,7 +29431,7 @@ __metadata: languageName: node linkType: hard -"wagmi@npm:^2.14.6, wagmi@npm:^2.15.2": +"wagmi@npm:^2.14.6": version: 2.15.2 resolution: "wagmi@npm:2.15.2" dependencies: @@ -28905,6 +29450,25 @@ __metadata: languageName: node linkType: hard +"wagmi@npm:^2.15.6": + version: 2.15.6 + resolution: "wagmi@npm:2.15.6" + dependencies: + "@wagmi/connectors": "npm:5.8.5" + "@wagmi/core": "npm:2.17.3" + use-sync-external-store: "npm:1.4.0" + peerDependencies: + "@tanstack/react-query": ">=5.0.0" + react: ">=18" + typescript: ">=5.0.4" + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/ecdbb177b8a18827e5b0bddcaf3af5147f43662eb85db1964d76f657c89211ee3c56e0565747d175143664a676c43b497d55446a1e31997d50f908f82b3ab472 + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -29666,33 +30230,33 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.4.6, ws@npm:^7.5.1, ws@npm:^7.5.10": - version: 7.5.10 - resolution: "ws@npm:7.5.10" +"ws@npm:8.18.2, ws@npm:^8, ws@npm:^8.12.0, ws@npm:^8.17.1, ws@npm:^8.18.0": + version: 8.18.2 + resolution: "ws@npm:8.18.2" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 10c0/bd7d5f4aaf04fae7960c23dcb6c6375d525e00f795dd20b9385902bd008c40a94d3db3ce97d878acc7573df852056ca546328b27b39f47609f80fb22a0a9b61d + checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 languageName: node linkType: hard -"ws@npm:^8, ws@npm:^8.12.0, ws@npm:^8.17.1, ws@npm:^8.18.0": - version: 8.18.2 - resolution: "ws@npm:8.18.2" +"ws@npm:^7.4.6, ws@npm:^7.5.1, ws@npm:^7.5.10": + version: 7.5.10 + resolution: "ws@npm:7.5.10" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" + utf-8-validate: ^5.0.2 peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 + checksum: 10c0/bd7d5f4aaf04fae7960c23dcb6c6375d525e00f795dd20b9385902bd008c40a94d3db3ce97d878acc7573df852056ca546328b27b39f47609f80fb22a0a9b61d languageName: node linkType: hard From 2fe7986ab9cc06895e969705fbfd64ccd74c720b Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Fri, 27 Jun 2025 12:50:38 +0300 Subject: [PATCH 10/16] [HUMAN App] Operator "role" can't be selected (#3416) --- .../components/mobile/my-jobs-list-mobile.tsx | 2 +- .../drawer-menu-items-operator.tsx | 2 +- .../drawer-menu-items-worker.tsx | 2 +- .../shared/components/data-entry/select.tsx | 2 +- .../src/shared/components/ui/chip.tsx | 3 +-- .../src/shared/components/ui/chips.tsx | 2 +- .../wallet-connect/wallet-connect.tsx | 24 ++++++++++++------- 7 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx index 0c2332e66a..ea51b3ce32 100644 --- a/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx +++ b/packages/apps/human-app/frontend/src/modules/worker/jobs/my-jobs/components/mobile/my-jobs-list-mobile.tsx @@ -121,7 +121,7 @@ export function MyJobsListMobile() { {allPages.map((d) => { return ( , + , ]; diff --git a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx index 41634fd1b1..502a9d0087 100644 --- a/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx +++ b/packages/apps/human-app/frontend/src/router/components/drawer-menu-items/drawer-menu-items-worker.tsx @@ -41,5 +41,5 @@ export const workerDrawerBottomMenuItems: MenuItem[] = [ } }, }, - , + , ]; diff --git a/packages/apps/human-app/frontend/src/shared/components/data-entry/select.tsx b/packages/apps/human-app/frontend/src/shared/components/data-entry/select.tsx index e1c4ec5a3b..d030114f5d 100644 --- a/packages/apps/human-app/frontend/src/shared/components/data-entry/select.tsx +++ b/packages/apps/human-app/frontend/src/shared/components/data-entry/select.tsx @@ -52,7 +52,7 @@ export function Select({ } > {options.map((elem) => ( - + {elem.name} ))} diff --git a/packages/apps/human-app/frontend/src/shared/components/ui/chip.tsx b/packages/apps/human-app/frontend/src/shared/components/ui/chip.tsx index 4b59b8c2e2..50c6226520 100644 --- a/packages/apps/human-app/frontend/src/shared/components/ui/chip.tsx +++ b/packages/apps/human-app/frontend/src/shared/components/ui/chip.tsx @@ -3,16 +3,15 @@ import { useColorMode } from '@/shared/contexts/color-mode'; interface ChipProps { label: string | React.ReactElement; - key?: string; backgroundColor?: string; } + export function Chip({ label, backgroundColor }: Readonly) { const { colorPalette } = useColorMode(); return ( {data.map((chipLabel) => ( - + ))} ); diff --git a/packages/apps/human-app/frontend/src/shared/contexts/wallet-connect/wallet-connect.tsx b/packages/apps/human-app/frontend/src/shared/contexts/wallet-connect/wallet-connect.tsx index 5ef56aa933..2f6c27d5e8 100644 --- a/packages/apps/human-app/frontend/src/shared/contexts/wallet-connect/wallet-connect.tsx +++ b/packages/apps/human-app/frontend/src/shared/contexts/wallet-connect/wallet-connect.tsx @@ -52,11 +52,7 @@ export const WalletConnectContext = createContext< | null >(null); -export function WalletConnectProvider({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +function AppKitWrapper({ children }: { children: React.ReactNode }) { const [initializing, setInitializing] = useState(true); const web3ProviderMutation = useWeb3Provider(); const { open } = useAppKit(); @@ -70,7 +66,7 @@ export function WalletConnectProvider({ ) { setInitializing(false); } - }, [web3ProviderMutation]); + }, [web3ProviderMutation.status]); const openModal = useCallback(async () => { await open(); @@ -122,11 +118,21 @@ export function WalletConnectProvider({ initializing, ]); + return ( + + {children} + + ); +} + +export function WalletConnectProvider({ + children, +}: { + children: React.ReactNode; +}) { return ( - - {children} - + {children} ); } From dc6826c3c29b5006ee307c7678abe2cc39bb2503 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Fri, 27 Jun 2025 17:15:34 +0300 Subject: [PATCH 11/16] Create pre-push hook (#3425) --- .husky/pre-commit | 1 - .husky/pre-push | 7 +++++++ packages/apps/dashboard/client/package.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100755 .husky/pre-push diff --git a/.husky/pre-commit b/.husky/pre-commit index 19c3fa6e16..b1dd4d8a8e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,4 @@ yarn dlx lint-staged -yarn workspaces foreach --all -p run typecheck # Format python file changes cd packages/sdk/python/human-protocol-sdk diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000000..c961ffea30 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,7 @@ +echo "Running typecheck before push..." + +if ! yarn workspaces foreach --all -p run typecheck; then + echo "" + echo "Typecheck failed! Please fix the errors above before pushing." + exit 1 +fi \ No newline at end of file diff --git a/packages/apps/dashboard/client/package.json b/packages/apps/dashboard/client/package.json index 254001622b..80032a771a 100644 --- a/packages/apps/dashboard/client/package.json +++ b/packages/apps/dashboard/client/package.json @@ -9,7 +9,7 @@ "clean": "tsc --build --clean && rm -rf dist", "start": "vite", "build": "tsc --build && vite build", - "lint": "yarn typecheck && eslint . --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint . --fix", "preview": "vite preview", "vercel-build": "yarn workspace human-protocol build:libs && yarn build", From bd5f178f03f24be64aad40df3bfe93b0af3d9dc8 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Mon, 30 Jun 2025 10:38:11 +0300 Subject: [PATCH 12/16] fix: include typechain to core bundle (#3426) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index e86c6d50f4..5124224307 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -8,7 +8,7 @@ "artifacts/@openzeppelin/**/[^.]*.json", "artifacts/contracts/**/[^.]*.json", "typechain-types/**/*.ts", - "dist/typechain-types" + "dist/typechain-types/**" ], "scripts": { "clean": "yarn clean:compile && yarn clean:build", From 872c3056666ce0c3fa5f63d8c2305a8c06cf2ec6 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Mon, 30 Jun 2025 13:31:16 +0300 Subject: [PATCH 13/16] [HUMAN App] feat: return all oracles for non prd (#3427) --- .../modules/oracle-discovery/oracle-discovery.controller.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts index 5b062b6b8f..9043f216d1 100644 --- a/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts +++ b/packages/apps/human-app/server/src/modules/oracle-discovery/oracle-discovery.controller.ts @@ -46,6 +46,10 @@ export class OracleDiscoveryController { const command = this.mapper.map(query, GetOraclesQuery, GetOraclesCommand); const oracles = await this.oracleDiscoveryService.getOracles(command); + if (process.env.NODE_ENV !== 'production') { + return oracles; + } + const isAudinoAvailableForUser = (req?.user?.qualifications ?? []).includes( 'audino', ); From 5083b1cfffa4612bf7b6e86c5d2d02bbcfc3e152 Mon Sep 17 00:00:00 2001 From: KirillKirill Date: Mon, 30 Jun 2025 14:09:42 +0300 Subject: [PATCH 14/16] [Dashboard] Transactions wrong data (#3423) --- .../ui/WalletTransactions/TransactionsTableBody.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/WalletTransactions/TransactionsTableBody.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/WalletTransactions/TransactionsTableBody.tsx index d2122d76cb..f1725e4252 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/WalletTransactions/TransactionsTableBody.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/WalletTransactions/TransactionsTableBody.tsx @@ -136,8 +136,8 @@ const TransactionsTableBody: FC = ({ data, isLoading, error }) => { From 0682373d862c6a2b179371ae4481852ddfd0e589 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Mon, 30 Jun 2025 14:56:37 +0300 Subject: [PATCH 15/16] [Reputation Oracle] fix: get improted provider ref (#3428) --- .../escrow-completion.service.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts index a61c4f351f..d0a4cdd285 100644 --- a/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts @@ -7,7 +7,6 @@ import { OperatorUtils, } from '@human-protocol/sdk'; import { Injectable } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; import crypto from 'crypto'; import { ethers } from 'ethers'; @@ -61,7 +60,12 @@ export class EscrowCompletionService { private readonly storageService: StorageService, private readonly outgoingWebhookService: OutgoingWebhookService, private readonly reputationService: ReputationService, - private readonly moduleRef: ModuleRef, + private readonly audinoResultsProcessor: AudinoResultsProcessor, + private readonly cvatResultsProcessor: CvatResultsProcessor, + private readonly fortuneResultsProcessor: FortuneResultsProcessor, + private readonly audinoPayoutsCalculator: AudinoPayoutsCalculator, + private readonly cvatPayoutsCalculator: CvatPayoutsCalculator, + private readonly fortunePayoutsCalculator: FortunePayoutsCalculator, ) {} async createEscrowCompletion( @@ -437,15 +441,15 @@ export class EscrowCompletionService { jobRequestType: JobRequestType, ): EscrowResultsProcessor { if (manifestUtils.isFortuneJobType(jobRequestType)) { - return this.moduleRef.get(FortuneResultsProcessor); + return this.fortuneResultsProcessor; } if (manifestUtils.isCvatJobType(jobRequestType)) { - return this.moduleRef.get(CvatResultsProcessor); + return this.cvatResultsProcessor; } if (manifestUtils.isAudinoJobType(jobRequestType)) { - return this.moduleRef.get(AudinoResultsProcessor); + return this.audinoResultsProcessor; } throw new Error( @@ -457,15 +461,15 @@ export class EscrowCompletionService { jobRequestType: JobRequestType, ): EscrowPayoutsCalculator { if (manifestUtils.isFortuneJobType(jobRequestType)) { - return this.moduleRef.get(FortunePayoutsCalculator); + return this.fortunePayoutsCalculator; } if (manifestUtils.isCvatJobType(jobRequestType)) { - return this.moduleRef.get(CvatPayoutsCalculator); + return this.cvatPayoutsCalculator; } if (manifestUtils.isAudinoJobType(jobRequestType)) { - return this.moduleRef.get(AudinoPayoutsCalculator); + return this.audinoPayoutsCalculator; } throw new Error( From e4f1ec750343bb8e22674579322d1bdb88686942 Mon Sep 17 00:00:00 2001 From: Dmitry Nechay Date: Mon, 30 Jun 2025 15:14:12 +0300 Subject: [PATCH 16/16] [HUMAN App] feat: add audino job type label and select values (#3429) --- .../frontend/src/modules/smart-contracts/EthKVStore/config.ts | 1 + packages/apps/human-app/frontend/src/shared/i18n/en.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/apps/human-app/frontend/src/modules/smart-contracts/EthKVStore/config.ts b/packages/apps/human-app/frontend/src/modules/smart-contracts/EthKVStore/config.ts index 6d7774132f..66b35ea2b2 100644 --- a/packages/apps/human-app/frontend/src/modules/smart-contracts/EthKVStore/config.ts +++ b/packages/apps/human-app/frontend/src/modules/smart-contracts/EthKVStore/config.ts @@ -13,6 +13,7 @@ export enum JobType { BOUNDING_BOXES_FROM_POINTS = 'image_boxes_from_points', SKELETONS_FROM_BOUNDING_BOXES = 'image_skeletons_from_boxes', POLYGONS = 'image_polygons', + AUDIO_TRANSCRIPTION = 'audio_transcription', } export const EthKVStoreKeys = { diff --git a/packages/apps/human-app/frontend/src/shared/i18n/en.json b/packages/apps/human-app/frontend/src/shared/i18n/en.json index dc4dde1d10..1296e089fc 100644 --- a/packages/apps/human-app/frontend/src/shared/i18n/en.json +++ b/packages/apps/human-app/frontend/src/shared/i18n/en.json @@ -421,6 +421,7 @@ "image_boxes": "Bounding Boxes", "image_boxes_from_points": "Bounding Boxes from Points", "image_skeletons_from_boxes": "Skeletons from Bounding Boxes", - "image_polygons": "Polygons" + "image_polygons": "Polygons", + "audio_transcription": "Audio Transcription" } }