Skip to content

Commit

Permalink
feat(analytics): add financial endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Redm4x committed Dec 20, 2024
1 parent 36a084f commit 71a77d0
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { eq, inArray, lte } from "drizzle-orm";
import { count, eq, inArray, lte } from "drizzle-orm";
import first from "lodash/first";
import omit from "lodash/omit";
import pick from "lodash/pick";
Expand Down Expand Up @@ -73,6 +73,11 @@ export class UserWalletRepository extends BaseRepository<ApiPgTables["UserWallet
return this.toOutputList(await this.cursor.query.UserWallets.findMany({ where: this.whereAccessibleBy(where) }));
}

async payingUserCount() {
const [{ count: payingUserCount }] = await this.cursor.select({ count: count() }).from(this.table).where(eq(this.table.isTrialing, false));
return payingUserCount;
}

protected toOutput(dbOutput: DbUserWalletOutput): UserWalletOutput {
const deploymentAllowance = dbOutput?.deploymentAllowance && parseFloat(dbOutput.deploymentAllowance);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Provider } from "@akashnetwork/database/dbSchemas/akash";

Check failure on line 1 in apps/api/src/billing/services/financial-stats/financial-stats.service.ts

View workflow job for this annotation

GitHub Actions / validate-n-build

Run autofix to sort these imports!
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { USDC_IBC_DENOMS } from "@src/billing/config/network.config";
import { BillingConfig, InjectBillingConfig } from "@src/billing/providers";
import { UserWalletRepository } from "@src/billing/repositories";
import { chainDb } from "@src/db/dbConnection";
import { RestCosmosBankBalancesResponse } from "@src/types/rest";
import { CosmosDistributionCommunityPoolResponse } from "@src/types/rest/cosmosDistributionCommunityPoolResponse";
import { apiNodeUrl } from "@src/utils/constants";
import axios from "axios";
import { Op, QueryTypes } from "sequelize";
import { singleton } from "tsyringe";

@singleton()
export class FinancialStatsService {
constructor(
@InjectBillingConfig() private readonly config: BillingConfig,
private readonly userWalletRepository: UserWalletRepository
) {}

async getPayingUserCount() {
return this.userWalletRepository.payingUserCount();
}

async getMasterWalletBalanceUsdc() {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(this.config.MASTER_WALLET_MNEMONIC, { prefix: "akash" });
const [account] = await wallet.getAccounts();

return this.getWalletBalances(account.address, USDC_IBC_DENOMS.mainnetId);
}

private async getWalletBalances(address: string, denom: string) {
const response = await axios.get<RestCosmosBankBalancesResponse>(`${apiNodeUrl}/cosmos/bank/v1beta1/balances/${address}?pagination.limit=1000`);
return parseFloat(response.data.balances.find(b => b.denom === denom)?.amount || "0");
}

async getAkashPubProviderBalances() {
const ovrclkProviders = await Provider.findAll({
where: {
hostUri: { [Op.like]: "%akash.pub:8443" }
}
});

const balances = await Promise.all(
ovrclkProviders.map(async p => {
const balance = await this.getWalletBalances(p.owner, USDC_IBC_DENOMS.mainnetId);
return { provider: p.hostUri, balanceUsdc: balance / 1_000_000 };
})
);

return balances;
}

async getCommunityPoolUsdc() {
const communityPoolData = await axios.get<CosmosDistributionCommunityPoolResponse>(`${apiNodeUrl}/cosmos/distribution/v1beta1/community_pool`);
return parseFloat(communityPoolData.data.pool.find(x => x.denom === USDC_IBC_DENOMS.mainnetId)?.amount || "0");
}

async getProviderRevenues() {
const results = await chainDb.query<{ hostUri: string; usdEarned: string }>(
`
WITH trial_deployments_ids AS (
SELECT DISTINCT m."relatedDeploymentId" AS "deployment_id"
FROM "transaction" t
INNER JOIN message m ON m."txId"=t.id
WHERE t."memo"='managed wallet tx' AND m.type='/akash.market.v1beta4.MsgCreateLease' AND t.height > 18515430 AND m.height > 18515430 -- 18515430 is height on trial launch (2024-10-17)
),
trial_leases AS (
SELECT
l.owner AS "owner",
l."createdHeight",
l."closedHeight",
l."providerAddress",
l.denom AS denom,
l.price AS price,
LEAST((SELECT MAX(height) FROM block), COALESCE(l."closedHeight",l."predictedClosedHeight")) - l."createdHeight" AS duration
FROM trial_deployments_ids
INNER JOIN deployment d ON d.id="deployment_id"
INNER JOIN lease l ON l."deploymentId"=d."id"
WHERE l.denom='uusdc'
),
billed_leases AS (
SELECT
l.owner,
p."hostUri",
ROUND(l.duration * l.price::numeric / 1000000, 2) AS "Spent USD"
FROM trial_leases l
INNER JOIN provider p ON p.owner=l."providerAddress"
)
SELECT
"hostUri",
SUM("Spent USD") AS "usdEarned"
FROM billed_leases
GROUP BY "hostUri"
ORDER BY SUM("Spent USD") DESC
`,
{ type: QueryTypes.SELECT }
);

return results.map(p => ({
provider: p.hostUri,
usdEarned: parseFloat(p.usdEarned)
}));
}
}
3 changes: 3 additions & 0 deletions apps/api/src/routers/internalRouter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi";

import { privateMiddleware } from "@src/middlewares/privateMiddleware";
import { env } from "@src/utils/env";
import routes from "../routes/internal";

Expand All @@ -20,4 +21,6 @@ const swaggerInstance = swaggerUI({ url: `/internal/doc` });

internalRouter.get(`/swagger`, swaggerInstance);

internalRouter.use("/financial", privateMiddleware);

routes.forEach(route => internalRouter.route(`/`, route));
44 changes: 44 additions & 0 deletions apps/api/src/routes/internal/financial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { container } from "tsyringe";

import { FinancialStatsService } from "@src/billing/services/financial-stats/financial-stats.service";

const route = createRoute({
method: "get",
path: "/financial",
summary: "",
responses: {
200: {
description: "List of gpu models and their availability.",
content: {
"application/json": {
schema: z.object({})
}
}
}
}
});

export default new OpenAPIHono().openapi(route, async c => {
const financialStatsService = container.resolve(FinancialStatsService);

const [masterBalanceUsdc, providerRevenues, communityPoolUsdc, ovrclkProviderBalances, payingUserCount] = await Promise.all([
financialStatsService.getMasterWalletBalanceUsdc(),
financialStatsService.getProviderRevenues(),
financialStatsService.getCommunityPoolUsdc(),
financialStatsService.getAkashPubProviderBalances(),
financialStatsService.getPayingUserCount()
]);

const readyToRecycle = ovrclkProviderBalances.map(x => x.balanceUsdc).reduce((a, b) => a + b, 0);

return c.json({
date: new Date(),
trialBalanceUsdc: masterBalanceUsdc / 1_000_000,
communityPoolUsdc: communityPoolUsdc / 1_000_000,
readyToRecycle,
payingUserCount,
ovrclkProviderBalances,
providerRevenues: providerRevenues
});
});
3 changes: 2 additions & 1 deletion apps/api/src/routes/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import gpuPrices from "../v1/gpuPrices";
import leasesDuration from "../v1/leasesDuration";
import providerDashboard from "../v1/providerDashboard";
import providerVersions from "../v1/providerVersions";
import financial from "./financial";

export default [providerVersions, gpu, leasesDuration, gpuModels, gpuPrices, providerDashboard];
export default [providerVersions, gpu, leasesDuration, gpuModels, gpuPrices, providerDashboard, financial];

0 comments on commit 71a77d0

Please sign in to comment.