Skip to content

Commit

Permalink
feat(deployment): implements custodial deployments top up data collec…
Browse files Browse the repository at this point in the history
…tion

refs #39
  • Loading branch information
ygrishajev committed Nov 6, 2024
1 parent 8876b5c commit 9b4a461
Show file tree
Hide file tree
Showing 39 changed files with 574 additions and 169 deletions.
4 changes: 2 additions & 2 deletions apps/api/env/.env.functional.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ AKASH_SANDBOX_DATABASE_CS=postgres://postgres:password@localhost:5432/console-ak
USER_DATABASE_CS=postgres://postgres:password@localhost:5432/console-users
POSTGRES_DB_URI=postgres://postgres:password@localhost:5432/console-users
MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony"
UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony"
USDC_TOP_UP_MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony"
UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="since bread kind field rookie stairs elephant tent horror rice gain tongue collect goose rural garment cover client biology toe ability boat afford mind"
USDC_TOP_UP_MASTER_WALLET_MNEMONIC="leaf brush weapon puppy depart hockey walnut hospital orphan require unfair hunt ribbon toe cereal eagle hour door awesome dress mouse when phone return"
NETWORK=sandbox
RPC_NODE_ENDPOINT=https://rpc.sandbox-01.aksh.pw:443
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT=20000000
Expand Down
25 changes: 25 additions & 0 deletions apps/api/env/.env.unit.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
AKASH_SANDBOX_DATABASE_CS=postgres://postgres:password@localhost:5432/console-akash-sandbox
USER_DATABASE_CS=postgres://postgres:password@localhost:5432/console-users
POSTGRES_DB_URI=postgres://postgres:password@localhost:5432/console-users
MASTER_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony"
UAKT_TOP_UP_MASTER_WALLET_MNEMONIC="since bread kind field rookie stairs elephant tent horror rice gain tongue collect goose rural garment cover client biology toe ability boat afford mind"
USDC_TOP_UP_MASTER_WALLET_MNEMONIC="leaf brush weapon puppy depart hockey walnut hospital orphan require unfair hunt ribbon toe cereal eagle hour door awesome dress mouse when phone return"
NETWORK=sandbox
RPC_NODE_ENDPOINT=https://rpc.sandbox-01.aksh.pw:443
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT=20000000
DEPLOYMENT_ALLOWANCE_REFILL_AMOUNT=20000000
DEPLOYMENT_ALLOWANCE_REFILL_THRESHOLD=2000000
TRIAL_FEES_ALLOWANCE_AMOUNT=5000000
FEE_ALLOWANCE_REFILL_AMOUNT=5000000
FEE_ALLOWANCE_REFILL_THRESHOLD=500000
DEPLOYMENT_GRANT_DENOM=uakt
LOG_LEVEL=debug
BILLING_ENABLED=true
ANONYMOUS_USER_TOKEN_SECRET=ANONYMOUS_USER_TOKEN_SECRET
STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
STRIPE_PRICE_ID=STRIPE_PRICE_ID
STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET
ALLOWED_CHECKOUT_REFERRERS=["http://localhost:3000"]
STRIPE_CHECKOUT_REDIRECT_URL=http://localhost:3000
STD_OUT_LOG_FORMAT=pretty
SQL_LOG_FORMAT=pretty
3 changes: 2 additions & 1 deletion apps/api/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ module.exports = {
displayName: "unit",
...common,
testMatch: ["<rootDir>/src/**/*.spec.ts"],
setupFilesAfterEnv: ["./test/setup-unit-tests.ts"]
setupFilesAfterEnv: ["./test/setup-unit-tests.ts"],
setupFiles: ["./test/setup-unit-env.ts"]
},
{
displayName: "functional",
Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"supertest": "^6.1.5",
"ts-jest": "^29.1.4",
"ts-loader": "^9.2.5",
"type-fest": "^4.26.1",
"typescript": "5.1.3",
"webpack": "^5.91.0",
"webpack-cli": "4.10.0",
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/billing/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import "./config.provider";
import "./http-sdk.provider";
import "./wallet.provider";

export * from "./config.provider";
2 changes: 1 addition & 1 deletion apps/api/src/billing/providers/signing-client.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { container, inject } from "tsyringe";
import { config } from "@src/billing/config";
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";
import { MasterSigningClientService } from "@src/billing/services/master-signing-client/master-signing-client.service";
import { MasterWalletType } from "@src/billing/types/wallet.type";

export const MANAGED_MASTER_SIGNING_CLIENT = "MANAGED_MASTER_SIGNING_CLIENT";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AllowanceHttpService, BalanceHttpService } from "@akashnetwork/http-sdk";
import { AllowanceHttpService, BalanceHttpService, BlockHttpService } from "@akashnetwork/http-sdk";
import { container } from "tsyringe";

import { apiNodeUrl } from "@src/utils/constants";

const SERVICES = [BalanceHttpService, AllowanceHttpService];
const SERVICES = [BalanceHttpService, AllowanceHttpService, BlockHttpService];

SERVICES.forEach(Service => container.register(Service, { useValue: new Service({ baseURL: apiNodeUrl }) }));
1 change: 1 addition & 0 deletions apps/api/src/core/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./postgres.provider";
export * from "./config.provider";
import "./http-sdk.provider";
11 changes: 11 additions & 0 deletions apps/api/src/deployment/config/config.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { container, inject } from "tsyringe";

import { config } from "@src/deployment/config";

export const DEPLOYMENT_CONFIG = "DEPLOYMENT_CONFIG";

container.register(DEPLOYMENT_CONFIG, { useValue: config });

export type DeploymentConfig = typeof config;

export const InjectDeploymentConfig = () => inject(DEPLOYMENT_CONFIG);
8 changes: 8 additions & 0 deletions apps/api/src/deployment/config/env.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";

const envSchema = z.object({
AUTO_TOP_UP_JOB_INTERVAL_IN_H: z.number({ coerce: true }).optional().default(1),
AUTO_TOP_UP_DEPLOYMENT_INTERVAL_IN_DAYS: z.number({ coerce: true }).optional().default(7)
});

export const envConfig = envSchema.parse(process.env);
1 change: 1 addition & 0 deletions apps/api/src/deployment/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { envConfig as config } from "./env.config";
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { singleton } from "tsyringe";

import { TopUpDeploymentsService } from "@src/deployment/services/top-up-deployments/top-up-deployments.service";
import { TopUpCustodialDeploymentsService } from "@src/deployment/services/top-up-custodial-deployments/top-up-custodial-deployments.service";

@singleton()
export class TopUpDeploymentsController {
constructor(private readonly topUpDeploymentsService: TopUpDeploymentsService) {}
constructor(private readonly topUpDeploymentsService: TopUpCustodialDeploymentsService) {}

async topUpDeployments() {
await this.topUpDeploymentsService.topUpDeployments();
Expand Down
13 changes: 9 additions & 4 deletions apps/api/src/deployment/repositories/lease/lease.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,36 @@ import { Lease } from "@akashnetwork/database/dbSchemas/akash";
import { col, fn, Op } from "sequelize";
import { singleton } from "tsyringe";

interface DrainingLeasesOptions {
export interface DrainingLeasesOptions {
closureHeight: number;
owner: string;
denom: string;
}

export interface DrainingDeploymentOutput {
dseq: number;
denom: string;
blockRate: number;
predictedClosedHeight: number;
}

@singleton()
export class LeaseRepository {
async findDrainingLeases(options: DrainingLeasesOptions): Promise<DrainingDeploymentOutput[]> {
return (await Lease.findAll({
const leases = await Lease.findAll({
where: {
closedHeight: null,
owner: options.owner,
denom: options.denom,
predictedClosedHeight: {
[Op.lte]: options.closureHeight
}
},
attributes: ["dseq", "denom", [fn("sum", col("price")), "blockRate"]],
attributes: ["dseq", "denom", [fn("min", col("predictedClosedHeight")), "predictedClosedHeight"], [fn("sum", col("price")), "blockRate"]],
group: ["dseq", "denom"],
plain: true
})) as unknown as DrainingDeploymentOutput[];
});

return leases ? (leases as unknown as DrainingDeploymentOutput[]) : [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { AllowanceHttpService, BalanceHttpService, BlockHttpService, Denom } from "@akashnetwork/http-sdk";
import { container } from "tsyringe";

import { MasterSigningClientService, MasterWalletService } from "@src/billing/services";
import type { Sentry } from "@src/core/providers/sentry.provider";
import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service";
import { config } from "@src/deployment/config";
import { DrainingLeasesOptions, LeaseRepository } from "@src/deployment/repositories/lease/lease.repository";
import { TopUpCustodialDeploymentsService } from "./top-up-custodial-deployments.service";

import { AkashAddressSeeder } from "@test/seeders/akash-address.seeder";
import { BalanceSeeder } from "@test/seeders/balance.seeder";
import { DeploymentGrantSeeder } from "@test/seeders/deployment-grant.seeder";
import { DrainingDeploymentSeeder } from "@test/seeders/draining-deployment.seeder";
import { FeesAuthorizationSeeder } from "@test/seeders/fees-authorization.seeder";

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 {
getFirstAddress: async () => address
} as unknown as MasterWalletService;
};
const mockMasterSigningClientService = () => {
return {
execTx: jest.fn()
} as unknown as MasterSigningClientService;
};

const allowanceHttpService = new AllowanceHttpService();
const balanceHttpService = new BalanceHttpService();
const blockHttpService = new BlockHttpService();
const uaktMasterWalletService = mockManagedWalletService(UAKT_TOP_UP_MASTER_WALLET_ADDRESS);
const usdtMasterWalletService = mockManagedWalletService(USDT_TOP_UP_MASTER_WALLET_ADDRESS);
const uaktMasterSigningClientService = mockMasterSigningClientService();
const usdtMasterSigningClientService = mockMasterSigningClientService();

jest.spyOn(blockHttpService, "getCurrentHeight").mockResolvedValue(CURRENT_BLOCK_HEIGHT);

const leaseRepository = container.resolve(LeaseRepository);
jest.spyOn(leaseRepository, "findDrainingLeases").mockResolvedValue([]);
const sentryEventService = container.resolve(SentryEventService);
const sentry = {
captureEvent: jest.fn()
} as unknown as Sentry;
const topUpDeploymentsService = new TopUpCustodialDeploymentsService(
allowanceHttpService,
balanceHttpService,
blockHttpService,
uaktMasterWalletService,
usdtMasterWalletService,
uaktMasterSigningClientService,
usdtMasterSigningClientService,
leaseRepository,
config,
sentry,
sentryEventService
);

type SeedParams = {
denom: Denom;
balance?: string;
grantee: string;
expectedDeploymentsTopUpCount?: 0 | 1 | 2;
hasDeployments?: boolean;
client: MasterSigningClientService;
};

const seedFor = ({ denom, balance = "100000000", grantee, expectedDeploymentsTopUpCount = 2, hasDeployments = true, client }: SeedParams) => {
const owner = AkashAddressSeeder.create();

return {
balance: BalanceSeeder.create({ denom, amount: balance }),
grant: DeploymentGrantSeeder.create({
granter: owner,
grantee: grantee,
authorization: { spend_limit: { denom, amount: "100000000" } }
}),
feeAllowance: FeesAuthorizationSeeder.create({
granter: owner,
grantee: grantee,
allowance: { spend_limit: { denom } }
}),
drainingDeployments: hasDeployments
? [
{
deployment: DrainingDeploymentSeeder.create({ denom, blockRate: 50, predictedClosedHeight: CURRENT_BLOCK_HEIGHT + 1500 }),
expectedTopUpAmount: expectedDeploymentsTopUpCount ? 4897959 : undefined
},
{
deployment: DrainingDeploymentSeeder.create({ denom, blockRate: 45, predictedClosedHeight: CURRENT_BLOCK_HEIGHT + 1700 }),
expectedTopUpAmount: expectedDeploymentsTopUpCount > 1 ? 4408163 : undefined
}
]
: [],
client: client
};
};

const data = [
seedFor({
denom: "uakt",
grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS,
client: uaktMasterSigningClientService
}),
seedFor({
denom: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1",
grantee: USDT_TOP_UP_MASTER_WALLET_ADDRESS,
client: usdtMasterSigningClientService
}),
seedFor({
denom: "uakt",
balance: "5500000",
grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS,
expectedDeploymentsTopUpCount: 1,
client: uaktMasterSigningClientService
}),
seedFor({
denom: "uakt",
balance: "5500000",
grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS,
hasDeployments: false,
client: uaktMasterSigningClientService
}),
seedFor({
denom: "uakt",
balance: "0",
grantee: UAKT_TOP_UP_MASTER_WALLET_ADDRESS,
expectedDeploymentsTopUpCount: 0,
client: uaktMasterSigningClientService
})
];

jest.spyOn(allowanceHttpService, "paginateDeploymentGrants").mockImplementation(async (params, cb) => {
return await cb(data.filter(({ grant }) => "grantee" in params && grant.grantee === params.grantee).map(({ grant }) => grant));
});
jest.spyOn(allowanceHttpService, "getFeeAllowanceForGranterAndGrantee").mockImplementation(async (granter: string, grantee: string) => {
return data.find(({ grant }) => grant.granter === granter && grant.grantee === grantee)?.feeAllowance;
});
jest.spyOn(balanceHttpService, "getBalance").mockImplementation(async (address: string, denom: Denom) => {
return (
data.find(({ grant }) => grant.granter === address)?.balance || {
amount: "0",
denom
}
);
});
jest.spyOn(leaseRepository, "findDrainingLeases").mockImplementation(async ({ owner, denom }: DrainingLeasesOptions) => {
return (
data
.find(({ grant }) => grant.granter === owner && grant.authorization.spend_limit.denom === denom)
?.drainingDeployments?.map(({ deployment }) => deployment) || []
);
});
jest.spyOn(topUpDeploymentsService, "topUpDeployment");

console.log("DEBUG data", JSON.stringify(data, null, 2));

it("should top up draining deployment given owners have sufficient grants and balances", async () => {
await topUpDeploymentsService.topUpDeployments();

expect(topUpDeploymentsService.topUpDeployment).toHaveBeenCalledTimes(5);

data.forEach(({ drainingDeployments, client }) => {
drainingDeployments.forEach(({ expectedTopUpAmount, deployment }) => {
if (expectedTopUpAmount) {
expect(topUpDeploymentsService.topUpDeployment).toHaveBeenCalledWith(expectedTopUpAmount, deployment, client);
}
});
});
});
});
Loading

0 comments on commit 9b4a461

Please sign in to comment.