From 3a59341d8f20aafc16b74f6620da67357e463b8d Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Fri, 13 Dec 2024 18:29:08 +0100 Subject: [PATCH] refactor(billing): make all transactions via same signer implementation --- apps/api/src/billing/config/env.config.ts | 2 +- .../controllers/wallet/wallet.controller.ts | 6 +- .../batch-signing-client.service.ts} | 71 +++++----- .../sync-signing-stargate-client.ts} | 8 +- .../wallet/wallet.ts} | 21 ++- .../providers/signing-client.provider.ts | 23 +++- .../src/billing/providers/wallet.provider.ts | 10 +- .../services/balances/balances.service.ts | 8 +- .../dedupe-signing-client.service.ts | 53 +++++++ apps/api/src/billing/services/index.ts | 6 +- .../managed-signer/managed-signer.service.ts | 86 ++++++++++++ .../managed-user-wallet.service.ts | 25 ++-- .../provider-cleanup.service.ts | 9 +- .../services/tx-signer/tx-signer.service.ts | 129 ------------------ ...ale-managed-deployments-cleaner.service.ts | 9 +- ...p-up-custodial-deployments.service.spec.ts | 35 ++--- .../top-up-custodial-deployments.service.ts | 6 +- ...top-up-managed-deployments.service.spec.ts | 14 +- .../top-up-managed-deployments.service.ts | 14 +- .../top-up-tools/top-up-tools.service.ts | 28 ++-- .../test/functional/create-deployment.spec.ts | 8 +- 21 files changed, 300 insertions(+), 271 deletions(-) rename apps/api/src/billing/{services/master-signing-client/master-signing-client.service.ts => lib/batch-signing-client/batch-signing-client.service.ts} (65%) rename apps/api/src/billing/{services/batch-signing-stargate-client/batch-signing-stargate-client.ts => lib/sync-signing-stargate-client/sync-signing-stargate-client.ts} (83%) rename apps/api/src/billing/{services/master-wallet/master-wallet.service.ts => lib/wallet/wallet.ts} (51%) create mode 100644 apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts create mode 100644 apps/api/src/billing/services/managed-signer/managed-signer.service.ts delete mode 100644 apps/api/src/billing/services/tx-signer/tx-signer.service.ts diff --git a/apps/api/src/billing/config/env.config.ts b/apps/api/src/billing/config/env.config.ts index e5e9efcb4..b42494039 100644 --- a/apps/api/src/billing/config/env.config.ts +++ b/apps/api/src/billing/config/env.config.ts @@ -15,7 +15,7 @@ export const envSchema = z.object({ FEE_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }), DEPLOYMENT_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }), ALLOWANCE_REFILL_BATCH_SIZE: z.number({ coerce: true }).default(10), - MASTER_WALLET_BATCHING_INTERVAL_MS: z.number().optional().default(1000), + WALLET_BATCHING_INTERVAL_MS: z.number().optional().default(1000), STRIPE_SECRET_KEY: z.string(), STRIPE_PRODUCT_ID: z.string(), STRIPE_WEBHOOK_SECRET: z.string(), diff --git a/apps/api/src/billing/controllers/wallet/wallet.controller.ts b/apps/api/src/billing/controllers/wallet/wallet.controller.ts index d4b1864b2..3d0a37188 100644 --- a/apps/api/src/billing/controllers/wallet/wallet.controller.ts +++ b/apps/api/src/billing/controllers/wallet/wallet.controller.ts @@ -6,8 +6,8 @@ import type { WalletListOutputResponse, WalletOutputResponse } from "@src/billin import type { SignTxRequestInput, SignTxResponseOutput, StartTrialRequestInput } from "@src/billing/routes"; import { GetWalletQuery } from "@src/billing/routes/get-wallet-list/get-wallet-list.router"; import { WalletInitializerService } from "@src/billing/services"; +import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service"; 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"; @@ -15,7 +15,7 @@ import { WithTransaction } from "@src/core"; export class WalletController { constructor( private readonly walletInitializer: WalletInitializerService, - private readonly signerService: TxSignerService, + private readonly signerService: ManagedSignerService, private readonly refillService: RefillService, private readonly walletReaderService: WalletReaderService ) {} @@ -38,7 +38,7 @@ export class WalletController { @Protected([{ action: "sign", subject: "UserWallet" }]) async signTx({ data: { userId, messages } }: SignTxRequestInput): Promise { return { - data: await this.signerService.signAndBroadcast(userId, messages as EncodeObject[]) + data: await this.signerService.executeEncodedTxByUserId(userId, messages as EncodeObject[]) }; } diff --git a/apps/api/src/billing/services/master-signing-client/master-signing-client.service.ts b/apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts similarity index 65% rename from apps/api/src/billing/services/master-signing-client/master-signing-client.service.ts rename to apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts index 2c1f5d66d..9bf7ee3da 100644 --- a/apps/api/src/billing/services/master-signing-client/master-signing-client.service.ts +++ b/apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts @@ -1,9 +1,7 @@ import { LoggerService } from "@akashnetwork/logging"; -import type { StdFee } from "@cosmjs/amino"; import { toHex } from "@cosmjs/encoding"; import { EncodeObject, Registry } from "@cosmjs/proto-signing"; import { calculateFee, GasPrice } from "@cosmjs/stargate"; -import type { SignerData } from "@cosmjs/stargate/build/signingstargateclient"; import { IndexedTx } from "@cosmjs/stargate/build/stargateclient"; import { BroadcastTxSyncResponse } from "@cosmjs/tendermint-rpc/build/comet38"; import { Sema } from "async-sema"; @@ -12,16 +10,16 @@ import DataLoader from "dataloader"; import { backOff } from "exponential-backoff"; import assert from "http-assert"; -import { BillingConfig } from "@src/billing/providers"; -import { BatchSigningStargateClient } from "@src/billing/services/batch-signing-stargate-client/batch-signing-stargate-client"; -import { MasterWalletService } from "@src/billing/services/master-wallet/master-wallet.service"; +import { SyncSigningStargateClient } from "@src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client"; +import { Wallet } from "@src/billing/lib/wallet/wallet"; +import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service"; interface ShortAccountInfo { accountNumber: number; sequence: number; } -interface ExecuteTxOptions { +export interface ExecuteTxOptions { fee: { granter: string; }; @@ -32,10 +30,10 @@ interface ExecuteTxInput { options?: ExecuteTxOptions; } -export class MasterSigningClientService { +export class BatchSigningClientService { private readonly FEES_DENOM = "uakt"; - private clientAsPromised: Promise; + private clientAsPromised: Promise; private readonly semaphore = new Sema(1); @@ -47,46 +45,33 @@ export class MasterSigningClientService { async (batchedInputs: ExecuteTxInput[]) => { return this.executeTxBatchBlocking(batchedInputs); }, - { cache: false, batchScheduleFn: callback => setTimeout(callback, this.config.MASTER_WALLET_BATCHING_INTERVAL_MS) } + { cache: false, batchScheduleFn: callback => setTimeout(callback, this.config.get("WALLET_BATCHING_INTERVAL_MS")) } ); private readonly logger = LoggerService.forContext(this.loggerContext); + get hasPendingTransactions() { + return this.semaphore.nrWaiting() > 0; + } + constructor( - private readonly config: BillingConfig, - private readonly masterWalletService: MasterWalletService, + private readonly config: BillingConfigService, + private readonly wallet: Wallet, private readonly registry: Registry, - private readonly loggerContext = MasterSigningClientService.name + private readonly loggerContext = BatchSigningClientService.name ) { this.clientAsPromised = this.initClient(); } private async initClient() { - return BatchSigningStargateClient.connectWithSigner(this.config.RPC_NODE_ENDPOINT, this.masterWalletService, { + return SyncSigningStargateClient.connectWithSigner(this.config.get("RPC_NODE_ENDPOINT"), this.wallet, { registry: this.registry }).then(async client => { - this.accountInfo = await client.getAccount(await this.masterWalletService.getFirstAddress()).then(account => ({ - accountNumber: account.accountNumber, - sequence: account.sequence - })); this.chainId = await client.getChainId(); - return client; }); } - async signAndBroadcast(messages: readonly EncodeObject[], fee: StdFee | "auto" | number, memo?: string) { - return (await this.clientAsPromised).signAndBroadcast(await this.masterWalletService.getFirstAddress(), messages, fee, memo); - } - - async sign(messages: readonly EncodeObject[], fee: StdFee, memo: string, explicitSignerData?: SignerData) { - return (await this.clientAsPromised).sign(await this.masterWalletService.getFirstAddress(), messages, fee, memo, explicitSignerData); - } - - async simulate(messages: readonly EncodeObject[], memo: string) { - return (await this.clientAsPromised).simulate(await this.masterWalletService.getFirstAddress(), messages, memo); - } - async executeTx(messages: readonly EncodeObject[], options?: ExecuteTxOptions) { const tx = await this.execTxLoader.load({ messages, options }); @@ -107,7 +92,7 @@ export class MasterSigningClientService { if (isSequenceMismatch) { this.clientAsPromised = this.initClient(); - this.logger.warn({ event: "ACCOUNT_SEQUENCE_MISMATCH", address: await this.masterWalletService.getFirstAddress(), attempt }); + this.logger.warn({ event: "ACCOUNT_SEQUENCE_MISMATCH", address: await this.wallet.getFirstAddress(), attempt }); return true; } @@ -125,13 +110,15 @@ export class MasterSigningClientService { let txIndex: number = 0; const client = await this.clientAsPromised; - const masterAddress = await this.masterWalletService.getFirstAddress(); + await this.updateAccountInfo(); + + const address = await this.wallet.getFirstAddress(); while (txIndex < inputs.length) { const { messages, options } = inputs[txIndex]; const fee = await this.estimateFee(messages, this.FEES_DENOM, options?.fee.granter); txes.push( - await client.sign(masterAddress, messages, fee, "", { + await client.sign(address, messages, fee, "", { accountNumber: this.accountInfo.accountNumber, sequence: this.accountInfo.sequence++, chainId: this.chainId @@ -155,6 +142,14 @@ export class MasterSigningClientService { return await Promise.all(hashes.map(hash => client.getTx(hash))); } + private async updateAccountInfo() { + const client = await this.clientAsPromised; + this.accountInfo = await client.getAccount(await this.wallet.getFirstAddress()).then(account => ({ + accountNumber: account.accountNumber, + sequence: account.sequence + })); + } + private async estimateFee(messages: readonly EncodeObject[], denom: string, granter?: string, options?: { mock?: boolean }) { if (options?.mock) { return { @@ -165,8 +160,14 @@ export class MasterSigningClientService { } const gasEstimation = await this.simulate(messages, ""); - const estimatedGas = Math.round(gasEstimation * this.config.GAS_SAFETY_MULTIPLIER); + const estimatedGas = Math.round(gasEstimation * this.config.get("GAS_SAFETY_MULTIPLIER")); + + const fee = calculateFee(estimatedGas, GasPrice.fromString(`0.025${denom}`)); + + return granter ? { ...fee, granter } : fee; + } - return calculateFee(estimatedGas, GasPrice.fromString(`0.025${denom}`)); + private async simulate(messages: readonly EncodeObject[], memo: string) { + return (await this.clientAsPromised).simulate(await this.wallet.getFirstAddress(), messages, memo); } } diff --git a/apps/api/src/billing/services/batch-signing-stargate-client/batch-signing-stargate-client.ts b/apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts similarity index 83% rename from apps/api/src/billing/services/batch-signing-stargate-client/batch-signing-stargate-client.ts rename to apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts index bc8bc89a4..d25556d56 100644 --- a/apps/api/src/billing/services/batch-signing-stargate-client/batch-signing-stargate-client.ts +++ b/apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts @@ -5,12 +5,12 @@ import type { BroadcastTxSyncResponse } from "@cosmjs/tendermint-rpc/build/comet export type { BroadcastTxSyncResponse }; -export class BatchSigningStargateClient extends SigningStargateClient { +export class SyncSigningStargateClient extends SigningStargateClient { public static async connectWithSigner( endpoint: string | HttpEndpoint, signer: OfflineSigner, options: SigningStargateClientOptions = {} - ): Promise { + ): Promise { const cometClient = await connectComet(endpoint); return this.createWithSigner(cometClient, signer, options); } @@ -19,8 +19,8 @@ export class BatchSigningStargateClient extends SigningStargateClient { cometClient: CometClient, signer: OfflineSigner, options: SigningStargateClientOptions = {} - ): Promise { - return new BatchSigningStargateClient(cometClient, signer, options); + ): Promise { + return new SyncSigningStargateClient(cometClient, signer, options); } protected constructor( diff --git a/apps/api/src/billing/services/master-wallet/master-wallet.service.ts b/apps/api/src/billing/lib/wallet/wallet.ts similarity index 51% rename from apps/api/src/billing/services/master-wallet/master-wallet.service.ts rename to apps/api/src/billing/lib/wallet/wallet.ts index 8163b226a..51b3bdd41 100644 --- a/apps/api/src/billing/services/master-wallet/master-wallet.service.ts +++ b/apps/api/src/billing/lib/wallet/wallet.ts @@ -1,13 +1,28 @@ +import { stringToPath } from "@cosmjs/crypto"; import { DirectSecp256k1HdWallet, OfflineDirectSigner } from "@cosmjs/proto-signing"; +import { DirectSecp256k1HdWalletOptions } from "@cosmjs/proto-signing/build/directsecp256k1hdwallet"; import { SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx"; -export class MasterWalletService implements OfflineDirectSigner { +export class Wallet implements OfflineDirectSigner { private readonly PREFIX = "akash"; + private readonly HD_PATH = "m/44'/118'/0'/0"; + private readonly instanceAsPromised: Promise; - constructor(mnemonic: string) { - this.instanceAsPromised = DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: this.PREFIX }); + constructor(mnemonic: string, index?: number) { + this.instanceAsPromised = DirectSecp256k1HdWallet.fromMnemonic(mnemonic, this.getInstanceOptions(index)); + } + + private getInstanceOptions(index?: number): Partial { + if (typeof index === "undefined") { + return { prefix: this.PREFIX }; + } + + return { + prefix: this.PREFIX, + hdPaths: [stringToPath(`${this.HD_PATH}/${index}`)] + }; } async getAccounts() { diff --git a/apps/api/src/billing/providers/signing-client.provider.ts b/apps/api/src/billing/providers/signing-client.provider.ts index 38e6110eb..f3f73b8c6 100644 --- a/apps/api/src/billing/providers/signing-client.provider.ts +++ b/apps/api/src/billing/providers/signing-client.provider.ts @@ -1,24 +1,37 @@ import { container, inject } from "tsyringe"; -import { config } from "@src/billing/config"; +import { BatchSigningClientService } from "@src/billing/lib/batch-signing-client/batch-signing-client.service"; import { TYPE_REGISTRY } from "@src/billing/providers/type-registry.provider"; import { MANAGED_MASTER_WALLET, UAKT_TOP_UP_MASTER_WALLET, USDC_TOP_UP_MASTER_WALLET } from "@src/billing/providers/wallet.provider"; -import { MasterSigningClientService } from "@src/billing/services/master-signing-client/master-signing-client.service"; +import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service"; import { MasterWalletType } from "@src/billing/types/wallet.type"; export const MANAGED_MASTER_SIGNING_CLIENT = "MANAGED_MASTER_SIGNING_CLIENT"; container.register(MANAGED_MASTER_SIGNING_CLIENT, { - useFactory: c => new MasterSigningClientService(config, c.resolve(MANAGED_MASTER_WALLET), c.resolve(TYPE_REGISTRY), MANAGED_MASTER_SIGNING_CLIENT) + useFactory: c => + new BatchSigningClientService(c.resolve(BillingConfigService), c.resolve(MANAGED_MASTER_WALLET), c.resolve(TYPE_REGISTRY), MANAGED_MASTER_SIGNING_CLIENT) }); export const UAKT_TOP_UP_MASTER_SIGNING_CLIENT = "UAKT_TOP_UP_MASTER_SIGNING_CLIENT"; container.register(UAKT_TOP_UP_MASTER_SIGNING_CLIENT, { - useFactory: c => new MasterSigningClientService(config, c.resolve(UAKT_TOP_UP_MASTER_WALLET), c.resolve(TYPE_REGISTRY), UAKT_TOP_UP_MASTER_SIGNING_CLIENT) + useFactory: c => + new BatchSigningClientService( + c.resolve(BillingConfigService), + c.resolve(UAKT_TOP_UP_MASTER_WALLET), + c.resolve(TYPE_REGISTRY), + UAKT_TOP_UP_MASTER_SIGNING_CLIENT + ) }); export const USDC_TOP_UP_MASTER_SIGNING_CLIENT = "USDC_TOP_UP_MASTER_SIGNING_CLIENT"; container.register(USDC_TOP_UP_MASTER_SIGNING_CLIENT, { - useFactory: c => new MasterSigningClientService(config, c.resolve(USDC_TOP_UP_MASTER_WALLET), c.resolve(TYPE_REGISTRY), USDC_TOP_UP_MASTER_SIGNING_CLIENT) + useFactory: c => + new BatchSigningClientService( + c.resolve(BillingConfigService), + c.resolve(USDC_TOP_UP_MASTER_WALLET), + c.resolve(TYPE_REGISTRY), + USDC_TOP_UP_MASTER_SIGNING_CLIENT + ) }); export const InjectSigningClient = (walletType: MasterWalletType) => inject(`${walletType}_MASTER_SIGNING_CLIENT`); diff --git a/apps/api/src/billing/providers/wallet.provider.ts b/apps/api/src/billing/providers/wallet.provider.ts index a62811805..fdd075740 100644 --- a/apps/api/src/billing/providers/wallet.provider.ts +++ b/apps/api/src/billing/providers/wallet.provider.ts @@ -1,18 +1,18 @@ import { container, inject } from "tsyringe"; import { config } from "@src/billing/config"; -import { MasterWalletService } from "@src/billing/services/master-wallet/master-wallet.service"; +import { Wallet } from "@src/billing/lib/wallet/wallet"; import { MasterWalletType } from "@src/billing/types/wallet.type"; export const MANAGED_MASTER_WALLET = "MANAGED_MASTER_WALLET"; -container.register(MANAGED_MASTER_WALLET, { useFactory: () => new MasterWalletService(config.MASTER_WALLET_MNEMONIC) }); +container.register(MANAGED_MASTER_WALLET, { useFactory: () => new Wallet(config.MASTER_WALLET_MNEMONIC) }); export const UAKT_TOP_UP_MASTER_WALLET = "UAKT_TOP_UP_MASTER_WALLET"; -container.register(UAKT_TOP_UP_MASTER_WALLET, { useFactory: () => new MasterWalletService(config.UAKT_TOP_UP_MASTER_WALLET_MNEMONIC) }); +container.register(UAKT_TOP_UP_MASTER_WALLET, { useFactory: () => new Wallet(config.UAKT_TOP_UP_MASTER_WALLET_MNEMONIC) }); export const USDC_TOP_UP_MASTER_WALLET = "USDC_TOP_UP_MASTER_WALLET"; -container.register(USDC_TOP_UP_MASTER_WALLET, { useFactory: () => new MasterWalletService(config.USDC_TOP_UP_MASTER_WALLET_MNEMONIC) }); +container.register(USDC_TOP_UP_MASTER_WALLET, { useFactory: () => new Wallet(config.USDC_TOP_UP_MASTER_WALLET_MNEMONIC) }); export const InjectWallet = (walletType: MasterWalletType) => inject(`${walletType}_MASTER_WALLET`); -export const resolveWallet = (walletType: MasterWalletType) => container.resolve(`${walletType}_MASTER_WALLET`); +export const resolveWallet = (walletType: MasterWalletType) => container.resolve(`${walletType}_MASTER_WALLET`); diff --git a/apps/api/src/billing/services/balances/balances.service.ts b/apps/api/src/billing/services/balances/balances.service.ts index 41e9bd123..d795dad49 100644 --- a/apps/api/src/billing/services/balances/balances.service.ts +++ b/apps/api/src/billing/services/balances/balances.service.ts @@ -4,14 +4,14 @@ import { singleton } from "tsyringe"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; import { InjectWallet } from "@src/billing/providers/wallet.provider"; import { UserWalletInput, UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; -import { MasterWalletService } from "@src/billing/services"; +import { Wallet } from "@src/billing/services"; @singleton() export class BalancesService { constructor( @InjectBillingConfig() private readonly config: BillingConfig, private readonly userWalletRepository: UserWalletRepository, - @InjectWallet("MANAGED") private readonly masterWalletService: MasterWalletService, + @InjectWallet("MANAGED") private readonly masterWallet: Wallet, private readonly allowanceHttpService: AllowanceHttpService ) {} @@ -51,7 +51,7 @@ export class BalancesService { private async retrieveAndCalcFeeLimit(userWallet: UserWalletOutput): Promise { const feeAllowance = await this.allowanceHttpService.getFeeAllowancesForGrantee(userWallet.address); - const masterWalletAddress = await this.masterWalletService.getFirstAddress(); + const masterWalletAddress = await this.masterWallet.getFirstAddress(); return feeAllowance.reduce((acc, allowance) => { if (allowance.granter !== masterWalletAddress) { @@ -70,7 +70,7 @@ export class BalancesService { async retrieveAndCalcDeploymentLimit(userWallet: Pick): Promise { const deploymentAllowance = await this.allowanceHttpService.getDeploymentAllowancesForGrantee(userWallet.address); - const masterWalletAddress = await this.masterWalletService.getFirstAddress(); + const masterWalletAddress = await this.masterWallet.getFirstAddress(); return deploymentAllowance.reduce((acc, allowance) => { if (allowance.granter !== masterWalletAddress || allowance.authorization.spend_limit.denom !== this.config.DEPLOYMENT_GRANT_DENOM) { diff --git a/apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts b/apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts new file mode 100644 index 000000000..d737fab4d --- /dev/null +++ b/apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts @@ -0,0 +1,53 @@ +import { LoggerService } from "@akashnetwork/logging"; +import { EncodeObject, Registry } from "@cosmjs/proto-signing"; +import * as crypto from "crypto"; +import { singleton } from "tsyringe"; + +import { BatchSigningClientService, ExecuteTxOptions } from "@src/billing/lib/batch-signing-client/batch-signing-client.service"; +import { Wallet } from "@src/billing/lib/wallet/wallet"; +import { InjectTypeRegistry } from "@src/billing/providers/type-registry.provider"; +import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service"; + +type CachedClient = { + key: string; + client: BatchSigningClientService; +}; + +@singleton() +export class DedupeSigningClientService { + private readonly clientsByAddress: Map = new Map(); + + private readonly logger = LoggerService.forContext(DedupeSigningClientService.name); + + constructor( + private readonly config: BillingConfigService, + @InjectTypeRegistry() private readonly registry: Registry + ) {} + + async executeManagedTx(mnemonic: string, walletIndex: number, messages: readonly EncodeObject[], options?: ExecuteTxOptions) { + const { client, key } = this.getClient(mnemonic, walletIndex); + + try { + return client.executeTx(messages, options); + } finally { + if (!client.hasPendingTransactions && this.clientsByAddress.has(key)) { + this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CLEAN_UP", key }); + this.clientsByAddress.delete(key); + } + } + } + + private getClient(mnemonic: string, addressIndex?: number): CachedClient { + const key = `${crypto.createHash("sha256").update(mnemonic).digest("hex")}/${addressIndex ?? 0}`; + + if (!this.clientsByAddress.has(key)) { + this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CREATE", key }); + this.clientsByAddress.set(key, { + key: key, + client: new BatchSigningClientService(this.config, new Wallet(mnemonic, addressIndex), this.registry) + }); + } + + return this.clientsByAddress.get(key); + } +} diff --git a/apps/api/src/billing/services/index.ts b/apps/api/src/billing/services/index.ts index 3d02fc475..77f63534f 100644 --- a/apps/api/src/billing/services/index.ts +++ b/apps/api/src/billing/services/index.ts @@ -1,5 +1,7 @@ export * from "@src/billing/services/managed-user-wallet/managed-user-wallet.service"; export * from "./rpc-message-service/rpc-message.service"; export * from "./wallet-initializer/wallet-initializer.service"; -export * from "./master-wallet/master-wallet.service"; -export * from "@src/billing/services/master-signing-client/master-signing-client.service"; +export * from "@src/billing/lib/wallet/wallet"; +export * from "@src/billing/lib/batch-signing-client/batch-signing-client.service"; +export * from "@src/billing/services/managed-signer/managed-signer.service"; +export * from "@src/billing/services/dedupe-signing-client/dedupe-signing-client.service"; diff --git a/apps/api/src/billing/services/managed-signer/managed-signer.service.ts b/apps/api/src/billing/services/managed-signer/managed-signer.service.ts new file mode 100644 index 000000000..4ea226231 --- /dev/null +++ b/apps/api/src/billing/services/managed-signer/managed-signer.service.ts @@ -0,0 +1,86 @@ +import { EncodeObject, Registry } from "@cosmjs/proto-signing"; +import assert from "http-assert"; +import pick from "lodash/pick"; +import { singleton } from "tsyringe"; + +import { AuthService } from "@src/auth/services/auth.service"; +import { BatchSigningClientService } from "@src/billing/lib/batch-signing-client/batch-signing-client.service"; +import { Wallet } from "@src/billing/lib/wallet/wallet"; +import { InjectSigningClient } from "@src/billing/providers/signing-client.provider"; +import { InjectTypeRegistry } from "@src/billing/providers/type-registry.provider"; +import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; +import { BalancesService } from "@src/billing/services/balances/balances.service"; +import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service"; +import { DedupeSigningClientService } from "@src/billing/services/dedupe-signing-client/dedupe-signing-client.service"; +import { ChainErrorService } from "../chain-error/chain-error.service"; +import { TrialValidationService } from "../trial-validation/trial-validation.service"; + +type StringifiedEncodeObject = Omit & { value: string }; + +@singleton() +export class ManagedSignerService { + private readonly wallet = new Wallet(this.config.get("MASTER_WALLET_MNEMONIC")); + + constructor( + private readonly config: BillingConfigService, + @InjectTypeRegistry() private readonly registry: Registry, + private readonly userWalletRepository: UserWalletRepository, + private readonly balancesService: BalancesService, + private readonly authService: AuthService, + private readonly chainErrorService: ChainErrorService, + private readonly anonymousValidateService: TrialValidationService, + @InjectSigningClient("MANAGED") private readonly masterSigningClientService: BatchSigningClientService, + private readonly dedupeSigningClientService: DedupeSigningClientService + ) {} + + async executeManagedTx(walletIndex: number, messages: readonly EncodeObject[]) { + const granter = await this.wallet.getFirstAddress(); + return await this.dedupeSigningClientService.executeManagedTx(this.config.get("MASTER_WALLET_MNEMONIC"), walletIndex, messages, { + fee: { granter } + }); + } + + async executeRootTx(messages: readonly EncodeObject[]) { + return await this.masterSigningClientService.executeTx(messages); + } + + async executeEncodedTxByUserId(userId: UserWalletOutput["userId"], messages: StringifiedEncodeObject[]) { + const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(userId); + assert(userWallet, 404, "UserWallet Not Found"); + + const decodedMessages = this.decodeMessages(messages); + + try { + await Promise.all(decodedMessages.map(message => this.anonymousValidateService.validateLeaseProviders(message, userWallet))); + } catch (error) { + throw this.chainErrorService.toAppError(error, decodedMessages); + } + + const tx = await this.executeManagedTx(userWallet.id, decodedMessages); + + await this.balancesService.refreshUserWalletLimits(userWallet); + + const result = pick(tx, ["code", "hash", "transactionHash", "rawLog"]); + + if (result.hash) { + return { + ...result, + transactionHash: result.hash + }; + } + + return result; + } + + private decodeMessages(messages: StringifiedEncodeObject[]): EncodeObject[] { + return messages.map(message => { + const value = new Uint8Array(Buffer.from(message.value, "base64")); + const decoded = this.registry.decode({ value, typeUrl: message.typeUrl }); + + return { + typeUrl: message.typeUrl, + value: decoded + }; + }); + } +} diff --git a/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts b/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts index fce79862e..7bc46f3a4 100644 --- a/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts +++ b/apps/api/src/billing/services/managed-user-wallet/managed-user-wallet.service.ts @@ -6,11 +6,10 @@ import { IndexedTx } from "@cosmjs/stargate"; import add from "date-fns/add"; import { singleton } from "tsyringe"; +import { Wallet } from "@src/billing/lib/wallet/wallet"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; -import { InjectSigningClient } from "@src/billing/providers/signing-client.provider"; import { InjectWallet } from "@src/billing/providers/wallet.provider"; -import { MasterSigningClientService } from "@src/billing/services/master-signing-client/master-signing-client.service"; -import { MasterWalletService } from "@src/billing/services/master-wallet/master-wallet.service"; +import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service"; import { RpcMessageService, SpendingAuthorizationMsgOptions } from "@src/billing/services/rpc-message-service/rpc-message.service"; import { DryRunOptions } from "@src/core/types/console"; @@ -37,8 +36,8 @@ export class ManagedUserWalletService { constructor( @InjectBillingConfig() private readonly config: BillingConfig, - @InjectWallet("MANAGED") private readonly masterWalletService: MasterWalletService, - @InjectSigningClient("MANAGED") private readonly masterSigningClientService: MasterSigningClientService, + @InjectWallet("MANAGED") private readonly masterWallet: Wallet, + private readonly managedSignerService: ManagedSignerService, private readonly rpcMessageService: RpcMessageService, private readonly allowanceHttpService: AllowanceHttpService ) {} @@ -71,7 +70,7 @@ export class ManagedUserWalletService { } async authorizeSpending(options: SpendingAuthorizationOptions) { - const masterWalletAddress = await this.masterWalletService.getFirstAddress(); + const masterWalletAddress = await this.masterWallet.getFirstAddress(); const msgOptions = { granter: masterWalletAddress, grantee: options.address, @@ -95,24 +94,24 @@ export class ManagedUserWalletService { } private async authorizeFeeSpending(options: Omit) { - const results: Promise[] = []; + const messages: EncodeObject[] = []; if (await this.allowanceHttpService.hasFeeAllowance(options.granter, options.grantee)) { - results.push(this.masterSigningClientService.executeTx([this.rpcMessageService.getRevokeAllowanceMsg(options)])); + messages.push(this.rpcMessageService.getRevokeAllowanceMsg(options)); } - results.push(this.masterSigningClientService.executeTx([this.rpcMessageService.getFeesAllowanceGrantMsg(options)])); + messages.push(this.rpcMessageService.getFeesAllowanceGrantMsg(options)); - return await Promise.all(results); + return await this.managedSignerService.executeRootTx(messages); } private async authorizeDeploymentSpending(options: SpendingAuthorizationMsgOptions) { const deploymentAllowanceMsg = this.rpcMessageService.getDepositDeploymentGrantMsg(options); - return await this.masterSigningClientService.executeTx([deploymentAllowanceMsg]); + return await this.managedSignerService.executeRootTx([deploymentAllowanceMsg]); } async revokeAll(grantee: string, reason?: string, options?: DryRunOptions) { - const masterWalletAddress = await this.masterWalletService.getFirstAddress(); + const masterWalletAddress = await this.masterWallet.getFirstAddress(); const params = { granter: masterWalletAddress, grantee }; const messages: EncodeObject[] = []; const revokeSummary = { @@ -135,7 +134,7 @@ export class ManagedUserWalletService { } if (!options?.dryRun) { - await this.masterSigningClientService.executeTx(messages); + await this.managedSignerService.executeRootTx(messages); } this.logger.info({ event: "SPENDING_REVOKED", address: params.grantee, revokeSummary, reason }); diff --git a/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts index 88794548f..b72b57346 100644 --- a/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts +++ b/apps/api/src/billing/services/provider-cleanup/provider-cleanup.service.ts @@ -4,11 +4,11 @@ import { singleton } from "tsyringe"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService, RpcMessageService } from "@src/billing/services"; +import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service"; import { ProviderCleanupParams } from "@src/billing/types/provider-cleanup"; import { ErrorService } from "@src/core/services/error/error.service"; import { ProviderCleanupSummarizer } from "@src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer"; import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository"; -import { TxSignerService } from "../tx-signer/tx-signer.service"; @singleton() export class ProviderCleanupService { @@ -18,7 +18,7 @@ export class ProviderCleanupService { @InjectBillingConfig() private readonly config: BillingConfig, private readonly userWalletRepository: UserWalletRepository, private readonly managedUserWalletService: ManagedUserWalletService, - private readonly txSignerService: TxSignerService, + private readonly managedSignerService: ManagedSignerService, private readonly deploymentRepository: DeploymentRepository, private readonly rpcMessageService: RpcMessageService, private readonly errorService: ErrorService @@ -45,7 +45,6 @@ export class ProviderCleanupService { } private async cleanUpForWallet(wallet: UserWalletOutput, options: ProviderCleanupParams, summary: ProviderCleanupSummarizer) { - const client = await this.txSignerService.getClientForAddressIndex(wallet.id); const deployments = await this.deploymentRepository.findDeploymentsForProvider({ owner: wallet.address, provider: options.provider @@ -57,7 +56,7 @@ export class ProviderCleanupService { try { if (!options.dryRun) { - await client.signAndBroadcast([message]); + await this.managedSignerService.executeManagedTx(wallet.id, [message]); this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" }); } } catch (error) { @@ -69,7 +68,7 @@ export class ProviderCleanupService { fees: this.config.FEE_ALLOWANCE_REFILL_AMOUNT } }); - await client.signAndBroadcast([message]); + await this.managedSignerService.executeManagedTx(wallet.id, [message]); this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" }); } } else { diff --git a/apps/api/src/billing/services/tx-signer/tx-signer.service.ts b/apps/api/src/billing/services/tx-signer/tx-signer.service.ts deleted file mode 100644 index 544f840d3..000000000 --- a/apps/api/src/billing/services/tx-signer/tx-signer.service.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { LoggerService } from "@akashnetwork/logging"; -import { stringToPath } from "@cosmjs/crypto"; -import { DirectSecp256k1HdWallet, EncodeObject, Registry } from "@cosmjs/proto-signing"; -import { calculateFee, GasPrice, SigningStargateClient } from "@cosmjs/stargate"; -import { DeliverTxResponse } from "@cosmjs/stargate/build/stargateclient"; -import { backOff } from "exponential-backoff"; -import assert from "http-assert"; -import pick from "lodash/pick"; -import { singleton } from "tsyringe"; - -import { AuthService } from "@src/auth/services/auth.service"; -import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; -import { InjectTypeRegistry } from "@src/billing/providers/type-registry.provider"; -import { InjectWallet } from "@src/billing/providers/wallet.provider"; -import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; -import { MasterWalletService } from "@src/billing/services"; -import { BalancesService } from "@src/billing/services/balances/balances.service"; -import { ChainErrorService } from "../chain-error/chain-error.service"; -import { TrialValidationService } from "../trial-validation/trial-validation.service"; - -type StringifiedEncodeObject = Omit & { value: string }; -export type SimpleSigningStargateClient = { - signAndBroadcast(messages: readonly EncodeObject[]): Promise; -}; - -@singleton() -export class TxSignerService { - private readonly HD_PATH = "m/44'/118'/0'/0"; - - private readonly PREFIX = "akash"; - - private readonly logger = LoggerService.forContext(TxSignerService.name); - - constructor( - @InjectBillingConfig() private readonly config: BillingConfig, - @InjectTypeRegistry() private readonly registry: Registry, - private readonly userWalletRepository: UserWalletRepository, - @InjectWallet("MANAGED") private readonly masterWalletService: MasterWalletService, - private readonly balancesService: BalancesService, - private readonly authService: AuthService, - private readonly chainErrorService: ChainErrorService, - private readonly anonymousValidateService: TrialValidationService - ) {} - - async signAndBroadcast(userId: UserWalletOutput["userId"], messages: StringifiedEncodeObject[]) { - const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(userId); - assert(userWallet, 404, "UserWallet Not Found"); - - const decodedMessages = this.decodeMessages(messages); - - try { - await Promise.all(decodedMessages.map(message => this.anonymousValidateService.validateLeaseProviders(message, userWallet))); - } catch (error) { - throw this.chainErrorService.toAppError(error, decodedMessages); - } - - const client = await this.getClientForAddressIndex(userWallet.id); - const tx = await client.signAndBroadcast(decodedMessages); - - await this.balancesService.refreshUserWalletLimits(userWallet); - - return pick(tx, ["code", "transactionHash", "rawLog"]); - } - - private decodeMessages(messages: StringifiedEncodeObject[]): EncodeObject[] { - return messages.map(message => { - const value = new Uint8Array(Buffer.from(message.value, "base64")); - const decoded = this.registry.decode({ value, typeUrl: message.typeUrl }); - - return { - typeUrl: message.typeUrl, - value: decoded - }; - }); - } - - async getClientForAddressIndex(addressIndex: number): Promise { - const wallet = await this.getWalletForAddressIndex(addressIndex); - let client = await this.createClient(wallet); - const walletAddress = (await wallet.getAccounts())[0].address; - const granter = await this.masterWalletService.getFirstAddress(); - - return { - signAndBroadcast: async (messages: readonly EncodeObject[]) => { - try { - return await backOff( - async () => { - const gasEstimation = await client.simulate(walletAddress, messages, "managed wallet gas estimation"); - const estimatedGas = Math.round(gasEstimation * this.config.GAS_SAFETY_MULTIPLIER); - const fee = calculateFee(estimatedGas, GasPrice.fromString("0.025uakt")); - - return await client.signAndBroadcast(walletAddress, messages, { ...fee, granter }, "managed wallet tx"); - }, - { - maxDelay: 5000, - numOfAttempts: 3, - jitter: "full", - retry: async (error: Error, attempt) => { - const isSequenceMismatch = error?.message?.includes("account sequence mismatch"); - - if (isSequenceMismatch) { - client = await this.createClient(wallet); - this.logger.warn({ event: "ACCOUNT_SEQUENCE_MISMATCH", address: walletAddress, attempt }); - - return true; - } - - return false; - } - } - ); - } catch (error) { - throw this.chainErrorService.toAppError(error, messages); - } - } - }; - } - - private async createClient(wallet: DirectSecp256k1HdWallet) { - return await SigningStargateClient.connectWithSigner(this.config.RPC_NODE_ENDPOINT, wallet, { - registry: this.registry - }); - } - - private async getWalletForAddressIndex(addressIndex: number) { - const hdPath = stringToPath(`${this.HD_PATH}/${addressIndex}`); - return await DirectSecp256k1HdWallet.fromMnemonic(this.config.MASTER_WALLET_MNEMONIC, { prefix: this.PREFIX, hdPaths: [hdPath] }); - } -} diff --git a/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts b/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts index ded9b8eab..b98708cd2 100644 --- a/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts +++ b/apps/api/src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service.ts @@ -5,7 +5,7 @@ import { singleton } from "tsyringe"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; import { ManagedUserWalletService, RpcMessageService } from "@src/billing/services"; -import { TxSignerService } from "@src/billing/services/tx-signer/tx-signer.service"; +import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service"; import { BlockRepository } from "@src/chain/repositories/block.repository"; import { ErrorService } from "@src/core/services/error/error.service"; import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository"; @@ -23,7 +23,7 @@ export class StaleManagedDeploymentsCleanerService { private readonly deploymentRepository: DeploymentRepository, private readonly blockRepository: BlockRepository, private readonly rpcMessageService: RpcMessageService, - private readonly txSignerService: TxSignerService, + private readonly managedSignerService: ManagedSignerService, @InjectBillingConfig() private readonly config: BillingConfig, private readonly managedUserWalletService: ManagedUserWalletService, private readonly errorService: ErrorService @@ -48,7 +48,6 @@ export class StaleManagedDeploymentsCleanerService { private async cleanUpForWallet(wallet: UserWalletOutput) { const currentHeight = await this.blockRepository.getLatestProcessedHeight(); - const client = await this.txSignerService.getClientForAddressIndex(wallet.id); const deployments = await this.deploymentRepository.findStaleDeployments({ owner: wallet.address, createdHeight: currentHeight - this.MAX_LIVE_BLOCKS @@ -59,7 +58,7 @@ export class StaleManagedDeploymentsCleanerService { this.logger.info({ event: "DEPLOYMENT_CLEAN_UP", params: { owner: wallet.address, dseq: deployment.dseq } }); try { - await client.signAndBroadcast([message]); + await this.managedSignerService.executeManagedTx(wallet.id, [message]); this.logger.info({ event: "DEPLOYMENT_CLEAN_UP_SUCCESS" }); } catch (error) { if (error.message.includes("not allowed to pay fees")) { @@ -70,7 +69,7 @@ export class StaleManagedDeploymentsCleanerService { } }); - await client.signAndBroadcast([message]); + await this.managedSignerService.executeManagedTx(wallet.id, [message]); this.logger.info({ event: "DEPLOYMENT_CLEAN_UP_SUCCESS" }); } else { throw error; diff --git a/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts index 7d67ee030..4dc858489 100644 --- a/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts +++ b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.spec.ts @@ -9,7 +9,7 @@ import { container } from "tsyringe"; import { BILLING_CONFIG } from "@src/billing/providers"; import { TYPE_REGISTRY } from "@src/billing/providers/type-registry.provider"; import { UAKT_TOP_UP_MASTER_WALLET } from "@src/billing/providers/wallet.provider"; -import { MasterSigningClientService, MasterWalletService, RpcMessageService } from "@src/billing/services"; +import { BatchSigningClientService, RpcMessageService, Wallet } from "@src/billing/services"; import { BlockHttpService } from "@src/chain/services/block-http/block-http.service"; import { ErrorService } from "@src/core/services/error/error.service"; import { config } from "@src/deployment/config"; @@ -31,13 +31,13 @@ describe(TopUpCustodialDeploymentsService.name, () => { const CURRENT_BLOCK_HEIGHT = 7481457; const UAKT_TOP_UP_MASTER_WALLET_ADDRESS = AkashAddressSeeder.create(); const USDT_TOP_UP_MASTER_WALLET_ADDRESS = AkashAddressSeeder.create(); - const mockManagedWalletService = (address: string) => { - return stub({ + const mockManagedWallet = (address: string) => { + return stub({ getFirstAddress: async () => address }); }; - const mockMasterSigningClientService = () => { - return stub({ + const mockMasterSigningClient = () => { + return stub({ executeTx: jest.fn() }); }; @@ -45,16 +45,11 @@ describe(TopUpCustodialDeploymentsService.name, () => { const allowanceHttpService = new AllowanceHttpService(); const balanceHttpService = new BalanceHttpService(); const blockHttpService = stub({ getCurrentHeight: jest.fn() }); - const uaktMasterWalletService = mockManagedWalletService(UAKT_TOP_UP_MASTER_WALLET_ADDRESS); - const usdtMasterWalletService = mockManagedWalletService(USDT_TOP_UP_MASTER_WALLET_ADDRESS); - const uaktMasterSigningClientService = mockMasterSigningClientService(); - const usdtMasterSigningClientService = mockMasterSigningClientService(); - const topUpToolsService = new TopUpToolsService( - uaktMasterWalletService, - usdtMasterWalletService, - uaktMasterSigningClientService, - usdtMasterSigningClientService - ); + const uaktMasterWallet = mockManagedWallet(UAKT_TOP_UP_MASTER_WALLET_ADDRESS); + const usdtMasterWallet = mockManagedWallet(USDT_TOP_UP_MASTER_WALLET_ADDRESS); + const uaktMasterSigningClient = mockMasterSigningClient(); + const usdtMasterSigningClient = mockMasterSigningClient(); + const topUpToolsService = new TopUpToolsService(uaktMasterWallet, usdtMasterWallet, uaktMasterSigningClient, usdtMasterSigningClient); jest.spyOn(blockHttpService, "getCurrentHeight").mockResolvedValue(CURRENT_BLOCK_HEIGHT); @@ -223,7 +218,7 @@ describe(TopUpCustodialDeploymentsService.name, () => { drainingDeployments.forEach(({ isExpectedToTopUp, deployment }) => { if (isExpectedToTopUp) { const isAkt = deployment.denom === "uakt"; - const client = isAkt ? uaktMasterSigningClientService : usdtMasterSigningClientService; + const client = isAkt ? uaktMasterSigningClient : usdtMasterSigningClient; uaktCount += isAkt ? 1 : 0; usdtCount += isAkt ? 0 : 1; @@ -248,8 +243,8 @@ describe(TopUpCustodialDeploymentsService.name, () => { }); }); - expect(uaktMasterSigningClientService.executeTx).toHaveBeenCalledTimes(uaktCount); - expect(usdtMasterSigningClientService.executeTx).toHaveBeenCalledTimes(usdtCount); + expect(uaktMasterSigningClient.executeTx).toHaveBeenCalledTimes(uaktCount); + expect(usdtMasterSigningClient.executeTx).toHaveBeenCalledTimes(usdtCount); }); xdescribe("actual top up deployment tx on demand", () => { @@ -259,8 +254,8 @@ describe(TopUpCustodialDeploymentsService.name, () => { const denom = "uakt"; const owner = ""; const dseq = "" as unknown as number; - const wallet = container.resolve(UAKT_TOP_UP_MASTER_WALLET); - const signer = new MasterSigningClientService(container.resolve(BILLING_CONFIG), wallet, container.resolve(TYPE_REGISTRY)); + const wallet = container.resolve(UAKT_TOP_UP_MASTER_WALLET); + const signer = new BatchSigningClientService(container.resolve(BILLING_CONFIG), wallet, container.resolve(TYPE_REGISTRY)); const grantee = await wallet.getFirstAddress(); try { diff --git a/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts index 8e1586b3a..9bbee3128 100644 --- a/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts +++ b/apps/api/src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service.ts @@ -2,7 +2,7 @@ import { AllowanceHttpService, BalanceHttpService, DeploymentAllowance } from "@ import { LoggerService } from "@akashnetwork/logging"; import { singleton } from "tsyringe"; -import { ExecDepositDeploymentMsgOptions, MasterSigningClientService, RpcMessageService } from "@src/billing/services"; +import { BatchSigningClientService, ExecDepositDeploymentMsgOptions, RpcMessageService } from "@src/billing/services"; import { BlockHttpService } from "@src/chain/services/block-http/block-http.service"; import { ErrorService } from "@src/core/services/error/error.service"; import { TopUpSummarizer } from "@src/deployment/lib/top-up-summarizer/top-up-summarizer"; @@ -70,7 +70,7 @@ export class TopUpCustodialDeploymentsService implements DeploymentsRefiller { private async topUpForGrant( grant: DeploymentAllowance, - client: MasterSigningClientService, + client: BatchSigningClientService, options: TopUpDeploymentsOptions, summary: TopUpSummarizer ): Promise { @@ -170,7 +170,7 @@ export class TopUpCustodialDeploymentsService implements DeploymentsRefiller { return hasSufficientDeploymentLimit && hasSufficientFeesLimit && hasSufficientFeesBalance && hasSufficientBalance; } - async topUpDeployment({ grantee, ...messageInput }: ExecDepositDeploymentMsgOptions, client: MasterSigningClientService, options: TopUpDeploymentsOptions) { + async topUpDeployment({ grantee, ...messageInput }: ExecDepositDeploymentMsgOptions, client: BatchSigningClientService, options: TopUpDeploymentsOptions) { const message = this.rpcClientService.getExecDepositDeploymentMsg({ grantee, ...messageInput }); this.logger.info({ event: "TOP_UP_DEPLOYMENT", params: { ...messageInput, masterWallet: grantee }, dryRun: options.dryRun }); diff --git a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts index 57961e98b..055d0a35b 100644 --- a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts +++ b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.spec.ts @@ -4,9 +4,8 @@ import { faker } from "@faker-js/faker"; import { BillingConfig } from "@src/billing/providers"; import { UserWalletRepository } from "@src/billing/repositories"; -import { MasterWalletService, RpcMessageService } from "@src/billing/services"; +import { ManagedSignerService, RpcMessageService, Wallet } from "@src/billing/services"; import { BalancesService } from "@src/billing/services/balances/balances.service"; -import { SimpleSigningStargateClient, TxSignerService } from "@src/billing/services/tx-signer/tx-signer.service"; import { BlockHttpService } from "@src/chain/services/block-http/block-http.service"; import { ErrorService } from "@src/core/services/error/error.service"; import { config } from "@src/deployment/config"; @@ -25,10 +24,9 @@ describe(TopUpManagedDeploymentsService.name, () => { const balancesService = stub({ retrieveAndCalcDeploymentLimit: jest.fn() }); const userWalletRepository = stub({ paginate: jest.fn() }); const blockHttpService = stub({ getCurrentHeight: () => CURRENT_BLOCK_HEIGHT }); - const client = stub({ signAndBroadcast: jest.fn() }); - const txSignerService = stub({ getClientForAddressIndex: jest.fn(() => client) }); + const managedSignerService = stub({ executeManagedTx: jest.fn() }); const billingConfig = stub({ DEPLOYMENT_GRANT_DENOM: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1" }); - const managedMasterWalletService = stub({ + const managedMasterWalletService = stub({ getFirstAddress: async () => MANAGED_MASTER_WALLET_ADDRESS }); @@ -36,7 +34,7 @@ describe(TopUpManagedDeploymentsService.name, () => { const errorService = stub({ execWithErrorHandler: (params: any, cb: () => any) => cb() }); const topUpDeploymentsService = new TopUpManagedDeploymentsService( userWalletRepository, - txSignerService, + managedSignerService, billingConfig, drainingDeploymentService, managedMasterWalletService, @@ -107,7 +105,7 @@ describe(TopUpManagedDeploymentsService.name, () => { data.forEach(({ wallet, drainingDeployments }) => { drainingDeployments.forEach(({ isExpectedToTopUp, deployment }) => { if (isExpectedToTopUp) { - expect(client.signAndBroadcast).toHaveBeenCalledWith([ + expect(managedSignerService.executeManagedTx).toHaveBeenCalledWith(wallet.id, [ { typeUrl: "/akash.deployment.v1beta3.MsgDepositDeployment", value: { @@ -127,6 +125,6 @@ describe(TopUpManagedDeploymentsService.name, () => { } }); }); - expect(client.signAndBroadcast).toHaveBeenCalledTimes(count); + expect(managedSignerService.executeManagedTx).toHaveBeenCalledTimes(count); }); }); diff --git a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts index 68c544f80..47c945895 100644 --- a/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts +++ b/apps/api/src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service.ts @@ -4,9 +4,9 @@ import { singleton } from "tsyringe"; import { BillingConfig, InjectBillingConfig } from "@src/billing/providers"; import { InjectWallet } from "@src/billing/providers/wallet.provider"; import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories"; -import { MasterWalletService, RpcMessageService } from "@src/billing/services"; +import { RpcMessageService, Wallet } from "@src/billing/services"; import { BalancesService } from "@src/billing/services/balances/balances.service"; -import { TxSignerService } from "@src/billing/services/tx-signer/tx-signer.service"; +import { ManagedSignerService } from "@src/billing/services/managed-signer/managed-signer.service"; import { BlockHttpService } from "@src/chain/services/block-http/block-http.service"; import { ErrorService } from "@src/core/services/error/error.service"; import { TopUpSummarizer } from "@src/deployment/lib/top-up-summarizer/top-up-summarizer"; @@ -19,10 +19,10 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { constructor( private readonly userWalletRepository: UserWalletRepository, - private readonly txSignerService: TxSignerService, + private readonly managedSignerService: ManagedSignerService, @InjectBillingConfig() private readonly billingConfig: BillingConfig, private readonly drainingDeploymentService: DrainingDeploymentService, - @InjectWallet("MANAGED") private readonly managedMasterWalletService: MasterWalletService, + @InjectWallet("MANAGED") private readonly managedMasterWallet: Wallet, private readonly balancesService: BalancesService, private readonly rpcClientService: RpcMessageService, private readonly blockHttpService: BlockHttpService, @@ -50,7 +50,7 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { } private async topUpForWallet(wallet: UserWalletOutput, options: TopUpDeploymentsOptions, summary: TopUpSummarizer) { - const depositor = await this.managedMasterWalletService.getFirstAddress(); + const depositor = await this.managedMasterWallet.getFirstAddress(); summary.inc("walletsCount"); const owner = wallet.address; const denom = this.billingConfig.DEPLOYMENT_GRANT_DENOM; @@ -61,8 +61,6 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { return; } - const signer = await this.txSignerService.getClientForAddressIndex(wallet.id); - let balance = await this.balancesService.retrieveAndCalcDeploymentLimit(wallet); let hasTopUp = false; @@ -81,7 +79,7 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller { this.logger.info({ event: "TOP_UP_DEPLOYMENT", params: messageInput, dryRun: options.dryRun }); if (!options.dryRun) { - await signer.signAndBroadcast([message]); + await this.managedSignerService.executeManagedTx(wallet.id, [message]); this.logger.info({ event: "TOP_UP_SUCCESS" }); } diff --git a/apps/api/src/deployment/services/top-up-tools/top-up-tools.service.ts b/apps/api/src/deployment/services/top-up-tools/top-up-tools.service.ts index 928853504..28ad01c32 100644 --- a/apps/api/src/deployment/services/top-up-tools/top-up-tools.service.ts +++ b/apps/api/src/deployment/services/top-up-tools/top-up-tools.service.ts @@ -2,33 +2,33 @@ import { singleton } from "tsyringe"; import { InjectSigningClient } from "@src/billing/providers/signing-client.provider"; import { InjectWallet } from "@src/billing/providers/wallet.provider"; -import { MasterSigningClientService, MasterWalletService } from "@src/billing/services"; +import { BatchSigningClientService, Wallet } from "@src/billing/services"; import { TopUpMasterWalletType } from "@src/billing/types/wallet.type"; @singleton() export class TopUpToolsService { readonly TYPES: TopUpMasterWalletType[] = ["UAKT_TOP_UP", "USDC_TOP_UP"]; - readonly pairs: { wallet: MasterWalletService; client: MasterSigningClientService }[]; + readonly pairs: { wallet: Wallet; client: BatchSigningClientService }[]; - private readonly wallets: Record; + private readonly wallets: Record; - private readonly clients: Record; + private readonly clients: Record; constructor( - @InjectWallet("UAKT_TOP_UP") private readonly uaktMasterWalletService: MasterWalletService, - @InjectWallet("USDC_TOP_UP") private readonly usdtMasterWalletService: MasterWalletService, - @InjectSigningClient("UAKT_TOP_UP") private readonly uaktMasterSigningClientService: MasterSigningClientService, - @InjectSigningClient("USDC_TOP_UP") private readonly usdtMasterSigningClientService: MasterSigningClientService + @InjectWallet("UAKT_TOP_UP") private readonly uaktWallet: Wallet, + @InjectWallet("USDC_TOP_UP") private readonly usdtWallet: Wallet, + @InjectSigningClient("UAKT_TOP_UP") private readonly uaktBatchSigningClientService: BatchSigningClientService, + @InjectSigningClient("USDC_TOP_UP") private readonly usdtBatchSigningClientService: BatchSigningClientService ) { this.wallets = { - UAKT_TOP_UP: this.uaktMasterWalletService, - USDC_TOP_UP: this.usdtMasterWalletService + UAKT_TOP_UP: this.uaktWallet, + USDC_TOP_UP: this.usdtWallet }; this.clients = { - UAKT_TOP_UP: this.uaktMasterSigningClientService, - USDC_TOP_UP: this.usdtMasterSigningClientService + UAKT_TOP_UP: this.uaktBatchSigningClientService, + USDC_TOP_UP: this.usdtBatchSigningClientService }; this.pairs = this.TYPES.map(walletType => ({ @@ -37,11 +37,11 @@ export class TopUpToolsService { })); } - walletFor(walletType: TopUpMasterWalletType): MasterWalletService { + walletFor(walletType: TopUpMasterWalletType): Wallet { return this.wallets[walletType]; } - clientFor(walletType: TopUpMasterWalletType): MasterSigningClientService { + clientFor(walletType: TopUpMasterWalletType): BatchSigningClientService { return this.clients[walletType]; } } diff --git a/apps/api/test/functional/create-deployment.spec.ts b/apps/api/test/functional/create-deployment.spec.ts index 0a45dcf51..c2c3d7a06 100644 --- a/apps/api/test/functional/create-deployment.spec.ts +++ b/apps/api/test/functional/create-deployment.spec.ts @@ -10,7 +10,7 @@ import { app } from "@src/app"; import { config } from "@src/billing/config"; import { TYPE_REGISTRY } from "@src/billing/providers/type-registry.provider"; import { MANAGED_MASTER_WALLET } from "@src/billing/providers/wallet.provider"; -import { MasterWalletService } from "@src/billing/services"; +import { Wallet } from "@src/billing/services"; import { DbTestingService } from "@test/services/db-testing.service"; import { WalletTestingService } from "@test/services/wallet-testing.service"; @@ -23,7 +23,7 @@ const yml = fs.readFileSync(path.resolve(__dirname, "../mocks/hello-world-sdl.ym describe("Tx Sign", () => { const registry = container.resolve(TYPE_REGISTRY); const walletService = new WalletTestingService(app); - const masterWalletService = container.resolve(MANAGED_MASTER_WALLET); + const masterWallet = container.resolve(MANAGED_MASTER_WALLET); const dbService = container.resolve(DbTestingService); afterEach(async () => { @@ -41,7 +41,7 @@ describe("Tx Sign", () => { const result = await res.json(); expect(res.status).toBe(200); - expect(result).toMatchObject({ data: { code: 0, transactionHash: expect.any(String) } }); + expect(result).toMatchObject({ data: { code: 0, transactionHash: expect.any(String), hash: expect.any(String) } }); }); }); @@ -72,7 +72,7 @@ describe("Tx Sign", () => { groups: sdl.groups(), version: await sdl.manifestVersion(), deposit: { denom: config.DEPLOYMENT_GRANT_DENOM, amount: "5000000" }, - depositor: await masterWalletService.getFirstAddress() + depositor: await masterWallet.getFirstAddress() } } ].map(message => ({ typeUrl: message.typeUrl, value: Buffer.from(registry.encode(message)).toString("base64") }))