Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(billing): make all transactions via same signer implementation #557

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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, { mock: true });
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
Loading