Skip to content

Commit

Permalink
refactor(billing): make all transactions via same signer implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ygrishajev committed Dec 13, 2024
1 parent 29d8b30 commit 3a59341
Show file tree
Hide file tree
Showing 21 changed files with 300 additions and 271 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/billing/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/billing/controllers/wallet/wallet.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ 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";

@scoped(Lifecycle.ResolutionScoped)
export class WalletController {
constructor(
private readonly walletInitializer: WalletInitializerService,
private readonly signerService: TxSignerService,
private readonly signerService: ManagedSignerService,
private readonly refillService: RefillService,
private readonly walletReaderService: WalletReaderService
) {}
Expand All @@ -38,7 +38,7 @@ export class WalletController {
@Protected([{ action: "sign", subject: "UserWallet" }])
async signTx({ data: { userId, messages } }: SignTxRequestInput): Promise<SignTxResponseOutput> {
return {
data: await this.signerService.signAndBroadcast(userId, messages as EncodeObject[])
data: await this.signerService.executeEncodedTxByUserId(userId, messages as EncodeObject[])
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
};
Expand All @@ -32,10 +30,10 @@ interface ExecuteTxInput {
options?: ExecuteTxOptions;
}

export class MasterSigningClientService {
export class BatchSigningClientService {
private readonly FEES_DENOM = "uakt";

private clientAsPromised: Promise<BatchSigningStargateClient>;
private clientAsPromised: Promise<SyncSigningStargateClient>;

private readonly semaphore = new Sema(1);

Expand All @@ -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 });

Expand All @@ -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;
}
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BatchSigningStargateClient> {
): Promise<SyncSigningStargateClient> {
const cometClient = await connectComet(endpoint);
return this.createWithSigner(cometClient, signer, options);
}
Expand All @@ -19,8 +19,8 @@ export class BatchSigningStargateClient extends SigningStargateClient {
cometClient: CometClient,
signer: OfflineSigner,
options: SigningStargateClientOptions = {}
): Promise<BatchSigningStargateClient> {
return new BatchSigningStargateClient(cometClient, signer, options);
): Promise<SyncSigningStargateClient> {
return new SyncSigningStargateClient(cometClient, signer, options);
}

protected constructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DirectSecp256k1HdWallet>;

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<DirectSecp256k1HdWalletOptions> {
if (typeof index === "undefined") {
return { prefix: this.PREFIX };
}

return {
prefix: this.PREFIX,
hdPaths: [stringToPath(`${this.HD_PATH}/${index}`)]
};
}

async getAccounts() {
Expand Down
23 changes: 18 additions & 5 deletions apps/api/src/billing/providers/signing-client.provider.ts
Original file line number Diff line number Diff line change
@@ -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`);
10 changes: 5 additions & 5 deletions apps/api/src/billing/providers/wallet.provider.ts
Original file line number Diff line number Diff line change
@@ -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<MasterWalletService>(`${walletType}_MASTER_WALLET`);
export const resolveWallet = (walletType: MasterWalletType) => container.resolve<Wallet>(`${walletType}_MASTER_WALLET`);
8 changes: 4 additions & 4 deletions apps/api/src/billing/services/balances/balances.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}

Expand Down Expand Up @@ -51,7 +51,7 @@ export class BalancesService {

private async retrieveAndCalcFeeLimit(userWallet: UserWalletOutput): Promise<number> {
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) {
Expand All @@ -70,7 +70,7 @@ export class BalancesService {

async retrieveAndCalcDeploymentLimit(userWallet: Pick<UserWalletOutput, "address">): Promise<number> {
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) {
Expand Down
Loading

0 comments on commit 3a59341

Please sign in to comment.