From 4addc52640b2cb681eb1d81950030c8294be817b Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:35:43 -0500 Subject: [PATCH 1/7] fix(wallet): return user wallet when starting trial if exists --- .../wallet-initializer/wallet-initializer.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index a0741aadc..6a87eb242 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -13,6 +13,12 @@ export class WalletInitializerService { ) {} async initializeAndGrantTrialLimits(userId: UserWalletInput["userId"]) { + const currentUserWallet = await this.userWalletRepository.findOneByUserId(userId); + + if (currentUserWallet) { + return this.userWalletRepository.toPublic(currentUserWallet); + } + const { id } = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: id }); const userWallet = await this.userWalletRepository.updateById( From 80994ae82facebdbc3cafd89c7de4c445c64a22b Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:19:14 -0500 Subject: [PATCH 2/7] fix(wallet): authz even if user wallet exists --- .../wallet-initializer.service.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index 6a87eb242..dc674ab01 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -1,7 +1,7 @@ import { singleton } from "tsyringe"; import { AuthService } from "@src/auth/services/auth.service"; -import { UserWalletInput, UserWalletRepository } from "@src/billing/repositories"; +import { UserWalletInput, UserWalletPublicOutput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; @singleton() @@ -13,16 +13,15 @@ export class WalletInitializerService { ) {} async initializeAndGrantTrialLimits(userId: UserWalletInput["userId"]) { - const currentUserWallet = await this.userWalletRepository.findOneByUserId(userId); + let userWallet = await this.userWalletRepository.findOneByUserId(userId); - if (currentUserWallet) { - return this.userWalletRepository.toPublic(currentUserWallet); + if (!userWallet) { + userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); } - const { id } = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); - const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: id }); - const userWallet = await this.userWalletRepository.updateById( - id, + const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); + userWallet = await this.userWalletRepository.updateById( + userWallet.id, { address: wallet.address, deploymentAllowance: wallet.limits.deployment, From bf4b4da99b0ceba4e9136817d8b68dd0b8cef24f Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:35:11 -0500 Subject: [PATCH 3/7] fix(wallet): remove import --- .../services/wallet-initializer/wallet-initializer.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index dc674ab01..9cdfeb977 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -1,7 +1,7 @@ import { singleton } from "tsyringe"; import { AuthService } from "@src/auth/services/auth.service"; -import { UserWalletInput, UserWalletPublicOutput, UserWalletRepository } from "@src/billing/repositories"; +import { UserWalletInput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; @singleton() From 63a04dca9a40fdbc46b9b86aefeecd12b43cc9f4 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:51:42 -0500 Subject: [PATCH 4/7] feat(wallet): use smaphore to control flow for init trial --- .../wallet-initializer.service.ts | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index 9cdfeb977..d77b5241c 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -3,34 +3,66 @@ import { singleton } from "tsyringe"; import { AuthService } from "@src/auth/services/auth.service"; import { UserWalletInput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; +import { Sema } from "async-sema"; @singleton() export class WalletInitializerService { + private readonly semaphores = new Map(); + constructor( private readonly walletManager: ManagedUserWalletService, private readonly userWalletRepository: UserWalletRepository, private readonly authService: AuthService ) {} - async initializeAndGrantTrialLimits(userId: UserWalletInput["userId"]) { - let userWallet = await this.userWalletRepository.findOneByUserId(userId); + private getSemaphore(userId: string): Sema { + let semaphore = this.semaphores.get(userId); + if (!semaphore) { + semaphore = new Sema(1); + this.semaphores.set(userId, semaphore); + } + return semaphore; + } + + private async waitForCompletion(userId: string): Promise { + const semaphore = this.getSemaphore(userId); - if (!userWallet) { - userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); + while (!(await semaphore.tryAcquire())) { + await new Promise(resolve => setTimeout(resolve, 100)); } - const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); - userWallet = await this.userWalletRepository.updateById( - userWallet.id, - { - address: wallet.address, - deploymentAllowance: wallet.limits.deployment, - feeAllowance: wallet.limits.fees - }, - { returning: true } - ); + semaphore.release(); + } + + async initializeAndGrantTrialLimits(userId: UserWalletInput["userId"]) { + // Wait for any existing operation to complete + await this.waitForCompletion(userId); + + const semaphore = this.getSemaphore(userId); + await semaphore.acquire(); - return this.userWalletRepository.toPublic(userWallet); + try { + let userWallet = await this.userWalletRepository.findOneByUserId(userId); + + if (!userWallet) { + userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); + + const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); + userWallet = await this.userWalletRepository.updateById( + userWallet.id, + { + address: wallet.address, + deploymentAllowance: wallet.limits.deployment, + feeAllowance: wallet.limits.fees + }, + { returning: true } + ); + } + + return this.userWalletRepository.toPublic(userWallet); + } finally { + semaphore.release(); + } } async initialize(userId: UserWalletInput["userId"]) { From bfcc3f6b4af54bb8c66bfe328abd4aab1d768eb1 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:05:51 -0500 Subject: [PATCH 5/7] fix(wallet): imports eslint --- .../services/wallet-initializer/wallet-initializer.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index d77b5241c..e0716d353 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -1,9 +1,9 @@ +import { Sema } from "async-sema"; import { singleton } from "tsyringe"; import { AuthService } from "@src/auth/services/auth.service"; import { UserWalletInput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; -import { Sema } from "async-sema"; @singleton() export class WalletInitializerService { From 978cc6d5b78fbb80f62ebcf7f7afa86d7dc83a94 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:20:15 -0500 Subject: [PATCH 6/7] fix(wallet): clear semaphore from map once done --- .../services/wallet-initializer/wallet-initializer.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index e0716d353..5a34a8196 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -62,6 +62,7 @@ export class WalletInitializerService { return this.userWalletRepository.toPublic(userWallet); } finally { semaphore.release(); + this.semaphores.delete(userId); } } From 0812e5d83beba391bf938a03a5f1bef547aee4bb Mon Sep 17 00:00:00 2001 From: Iaroslav Gryshaiev Date: Mon, 16 Dec 2024 15:20:09 +0000 Subject: [PATCH 7/7] feat: implement a semaphore decorator (#563) --- .../controllers/wallet/wallet.controller.ts | 2 + .../wallet-initializer.service.ts | 69 +++++-------------- apps/api/src/core/lib/semaphore.decorator.ts | 28 ++++++++ 3 files changed, 48 insertions(+), 51 deletions(-) create mode 100644 apps/api/src/core/lib/semaphore.decorator.ts diff --git a/apps/api/src/billing/controllers/wallet/wallet.controller.ts b/apps/api/src/billing/controllers/wallet/wallet.controller.ts index d4b1864b2..32ef8234c 100644 --- a/apps/api/src/billing/controllers/wallet/wallet.controller.ts +++ b/apps/api/src/billing/controllers/wallet/wallet.controller.ts @@ -10,6 +10,7 @@ import { RefillService } from "@src/billing/services/refill/refill.service"; import { TxSignerService } from "@src/billing/services/tx-signer/tx-signer.service"; import { GetWalletOptions, WalletReaderService } from "@src/billing/services/wallet-reader/wallet-reader.service"; import { WithTransaction } from "@src/core"; +import { Semaphore } from "@src/core/lib/semaphore.decorator"; @scoped(Lifecycle.ResolutionScoped) export class WalletController { @@ -20,6 +21,7 @@ export class WalletController { private readonly walletReaderService: WalletReaderService ) {} + @Semaphore() @WithTransaction() @Protected([{ action: "create", subject: "UserWallet" }]) async create({ data: { userId } }: StartTrialRequestInput): Promise { diff --git a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts index 5a34a8196..dbf9107bb 100644 --- a/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts +++ b/apps/api/src/billing/services/wallet-initializer/wallet-initializer.service.ts @@ -1,69 +1,36 @@ -import { Sema } from "async-sema"; import { singleton } from "tsyringe"; import { AuthService } from "@src/auth/services/auth.service"; -import { UserWalletInput, UserWalletRepository } from "@src/billing/repositories"; +import { UserWalletInput, UserWalletPublicOutput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService } from "@src/billing/services"; @singleton() export class WalletInitializerService { - private readonly semaphores = new Map(); - constructor( private readonly walletManager: ManagedUserWalletService, private readonly userWalletRepository: UserWalletRepository, private readonly authService: AuthService ) {} - private getSemaphore(userId: string): Sema { - let semaphore = this.semaphores.get(userId); - if (!semaphore) { - semaphore = new Sema(1); - this.semaphores.set(userId, semaphore); + async initializeAndGrantTrialLimits(userId: UserWalletInput["userId"]): Promise { + let userWallet = await this.userWalletRepository.findOneByUserId(userId); + + if (!userWallet) { + userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); + + const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); + userWallet = await this.userWalletRepository.updateById( + userWallet.id, + { + address: wallet.address, + deploymentAllowance: wallet.limits.deployment, + feeAllowance: wallet.limits.fees + }, + { returning: true } + ); } - return semaphore; - } - - private async waitForCompletion(userId: string): Promise { - const semaphore = this.getSemaphore(userId); - - while (!(await semaphore.tryAcquire())) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - - semaphore.release(); - } - async initializeAndGrantTrialLimits(userId: UserWalletInput["userId"]) { - // Wait for any existing operation to complete - await this.waitForCompletion(userId); - - const semaphore = this.getSemaphore(userId); - await semaphore.acquire(); - - try { - let userWallet = await this.userWalletRepository.findOneByUserId(userId); - - if (!userWallet) { - userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "create").create({ userId }); - - const wallet = await this.walletManager.createAndAuthorizeTrialSpending({ addressIndex: userWallet.id }); - userWallet = await this.userWalletRepository.updateById( - userWallet.id, - { - address: wallet.address, - deploymentAllowance: wallet.limits.deployment, - feeAllowance: wallet.limits.fees - }, - { returning: true } - ); - } - - return this.userWalletRepository.toPublic(userWallet); - } finally { - semaphore.release(); - this.semaphores.delete(userId); - } + return this.userWalletRepository.toPublic(userWallet); } async initialize(userId: UserWalletInput["userId"]) { diff --git a/apps/api/src/core/lib/semaphore.decorator.ts b/apps/api/src/core/lib/semaphore.decorator.ts new file mode 100644 index 000000000..e46107b18 --- /dev/null +++ b/apps/api/src/core/lib/semaphore.decorator.ts @@ -0,0 +1,28 @@ +import { Sema } from "async-sema"; + +export const Semaphore = () => (target: object, propertyName: string, descriptor: PropertyDescriptor) => { + const semaphores = new Map(); + + const originalMethod = descriptor.value; + + const getSemaphore = (key: string): Sema => { + let semaphore = semaphores.get(key); + if (!semaphore) { + semaphore = new Sema(1); + semaphores.set(key, semaphore); + } + return semaphore; + }; + + descriptor.value = async function semaphoredFunction(...args: unknown[]) { + const key = JSON.stringify(args); + const semaphore = getSemaphore(key); + await semaphore.acquire(); + try { + return await originalMethod.apply(this, args); + } finally { + semaphore.release(); + semaphores.delete(key); + } + }; +};