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

feat(network): API endpoint listing bids #859

Merged
merged 1 commit into from
Feb 21, 2025
Merged
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: 2 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { RequestContextInterceptor } from "@src/core/services/request-context-in
import { HonoInterceptor } from "@src/core/types/hono-interceptor.type";
import packageJson from "../package.json";
import { apiKeysRouter } from "./auth/routes/api-keys/api-keys.router";
import { bidsRouter } from "./bid/routes/bids/bids.router";
import { chainDb, syncUserSchema, userDb } from "./db/dbConnection";
import { deploymentSettingRouter } from "./deployment/routes/deployment-setting/deployment-setting.router";
import { clientInfoMiddleware } from "./middlewares/clientInfoMiddleware";
Expand Down Expand Up @@ -87,6 +88,7 @@ appHono.route("/", getAnonymousUserRouter);
appHono.route("/", sendVerificationEmailRouter);
appHono.route("/", deploymentSettingRouter);
appHono.route("/", apiKeysRouter);
appHono.route("/", bidsRouter);

appHono.get("/status", c => {
const version = packageJson.version;
Expand Down
25 changes: 25 additions & 0 deletions apps/api/src/bid/controllers/bid/bid.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BidHttpService } from "@akashnetwork/http-sdk";
import { singleton } from "tsyringe";

import { AuthService, Protected } from "@src/auth/services/auth.service";
import { ListBidsResponse } from "@src/bid/http-schemas/bid.schema";
import { UserWalletRepository } from "@src/billing/repositories";

@singleton()
export class BidController {
constructor(
private readonly bidHttpService: BidHttpService,
private readonly authService: AuthService,
private readonly userWalletRepository: UserWalletRepository,
) {}

@Protected([{ action: "sign", subject: "UserWallet" }])
async list(dseq: string, userId?: string): Promise<ListBidsResponse> {
const { currentUser, ability } = this.authService;

const wallets = await this.userWalletRepository.accessibleBy(ability, "sign").findByUserId(userId ?? currentUser.userId);
const bids = await this.bidHttpService.list(wallets[0].address, dseq);

return { data: bids };
}
}
100 changes: 100 additions & 0 deletions apps/api/src/bid/http-schemas/bid.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { z } from "zod";

const DeploymentResource_V3 = z.object({
cpu: z.object({
units: z.object({
val: z.string(),
}),
attributes: z.array(z.object({
key: z.string(),
value: z.string(),
})),
}),
gpu: z.object({
units: z.object({
val: z.string(),
}),
attributes: z.array(z.object({
key: z.string(),
value: z.string(),
})),
}),
memory: z.object({
quantity: z.object({
val: z.string(),
}),
attributes: z.array(z.object({
key: z.string(),
value: z.string(),
})),
}),
storage: z.array(z.object({
name: z.string(),
quantity: z.object({
val: z.string(),
}),
attributes: z.array(z.object({
key: z.string(),
value: z.string(),
})),
})),
endpoints: z.array(z.object({
kind: z.string(),
sequence_number: z.number()
}))
});

export const BidResponseSchema = z.object({
bid: z.object({
bid_id: z.object({
owner: z.string(),
dseq: z.string(),
gseq: z.number(),
oseq: z.number(),
provider: z.string(),
}),
state: z.string(),
price: z.object({
denom: z.string(),
amount: z.string(),
}),
created_at: z.string(),
resources_offer: z.array(z.object({
resources: DeploymentResource_V3,
count: z.number(),
}))
}),
escrow_account: z.object({
id: z.object({
scope: z.string(),
xid: z.string(),
}),
owner: z.string(),
state: z.string(),
balance: z.object({
denom: z.string(),
amount: z.string(),
}),
transferred: z.object({
denom: z.string(),
amount: z.string(),
}),
settled_at: z.string(),
depositor: z.string(),
funds: z.object({
denom: z.string(),
amount: z.string(),
}),
})
});

export const ListBidsQuerySchema = z.object({
dseq: z.string(),
userId: z.optional(z.string()),
});

export const ListBidsResponseSchema = z.object({
data: z.array(BidResponseSchema)
});

export type ListBidsResponse = z.infer<typeof ListBidsResponseSchema>;
34 changes: 34 additions & 0 deletions apps/api/src/bid/routes/bids/bids.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createRoute } from "@hono/zod-openapi";
import { container } from "tsyringe";

import { BidController } from "@src/bid/controllers/bid/bid.controller";
import { ListBidsQuerySchema, ListBidsResponseSchema } from "@src/bid/http-schemas/bid.schema";
import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";

const listRoute = createRoute({
method: "get",
path: "/v1/bids",
summary: "List bids",
tags: ["Bids"],
request: {
query: ListBidsQuerySchema
},
responses: {
200: {
description: "List of bids",
content: {
"application/json": {
schema: ListBidsResponseSchema
}
}
}
}
});

export const bidsRouter = new OpenApiHonoHandler();

bidsRouter.openapi(listRoute, async function routeListBids(c) {
const { dseq, userId } = c.req.valid("query");
const result = await container.resolve(BidController).list(dseq, userId);
return c.json(result, 200);
});
4 changes: 2 additions & 2 deletions apps/api/src/core/providers/http-sdk.provider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AuthzHttpService, BalanceHttpService, BlockHttpService } from "@akashnetwork/http-sdk";
import { AuthzHttpService, BalanceHttpService, BidHttpService, BlockHttpService } from "@akashnetwork/http-sdk";
import { container } from "tsyringe";

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

const SERVICES = [BalanceHttpService, AuthzHttpService, BlockHttpService];
const SERVICES = [BalanceHttpService, AuthzHttpService, BlockHttpService, BidHttpService];

SERVICES.forEach(Service => container.register(Service, { useValue: new Service({ baseURL: apiNodeUrl }) }));
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { MsgCloseDeployment, MsgCreateDeployment } from "@akashnetwork/akash-api/v1beta3";
import { SDL } from "@akashnetwork/akashjs/build/sdl";
import { getAkashTypeRegistry } from "@akashnetwork/akashjs/build/stargate";
import { BidHttpService } from "@akashnetwork/http-sdk";
import { LoggerService } from "@akashnetwork/logging";
import { DirectSecp256k1HdWallet, EncodeObject, Registry } from "@cosmjs/proto-signing";
import { calculateFee, SigningStargateClient } from "@cosmjs/stargate";
import axios from "axios";
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import pick from "lodash/pick";
import { singleton } from "tsyringe";

import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
import { getGpuModelsAvailability } from "@src/routes/v1/gpu";
import { RestAkashBidListResponseType } from "@src/types/rest";
import { apiNodeUrl } from "@src/utils/constants";
import { sleep } from "@src/utils/delay";
import { env } from "@src/utils/env";
Expand All @@ -20,7 +21,10 @@ import { sdlTemplateWithRam, sdlTemplateWithRamAndInterface } from "./sdl-templa
export class GpuBidsCreatorService {
private readonly logger = LoggerService.forContext(GpuBidsCreatorService.name);

constructor(private readonly config: BillingConfigService) {}
constructor(
private readonly config: BillingConfigService,
private readonly bidHttpService: BidHttpService,
) { }

async createGpuBids() {
if (!env.GPU_BOT_WALLET_MNEMONIC) throw new Error("The env variable GPU_BOT_WALLET_MNEMONIC is not set.");
Expand All @@ -29,7 +33,7 @@ export class GpuBidsCreatorService {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(env.GPU_BOT_WALLET_MNEMONIC, { prefix: "akash" });
const [account] = await wallet.getAccounts();

this.logger.info("Wallet Address: " + account.address);
this.logger.info({ event: "CREATING_GPU_BIDS", address: account.address });

const myRegistry = new Registry([...getAkashTypeRegistry()]);

Expand All @@ -40,7 +44,7 @@ export class GpuBidsCreatorService {
const balanceBefore = await client.getBalance(account.address, "uakt");
const balanceBeforeUAkt = parseFloat(balanceBefore.amount);
const akt = Math.round((balanceBeforeUAkt / 1_000_000) * 100) / 100;
this.logger.info("Balance: " + akt + "akt");
this.logger.info({ event: "CLIENT_CONNECTED", balance: akt });

const gpuModels = await getGpuModelsAvailability();

Expand All @@ -51,7 +55,7 @@ export class GpuBidsCreatorService {
const balanceAfterUAkt = parseFloat(balanceAfter.amount);
const diff = balanceBeforeUAkt - balanceAfterUAkt;

this.logger.info(`The operation cost ${diff / 1_000_000} akt`);
this.logger.info({ event: "GPU_BIDS_CREATED", cost: diff / 1_000_000 });
}

private async signAndBroadcast(address: string, client: SigningStargateClient, messages: readonly EncodeObject[]) {
Expand Down Expand Up @@ -86,34 +90,30 @@ export class GpuBidsCreatorService {
(a, b) => a.vendor.localeCompare(b.vendor) || a.model.localeCompare(b.model) || a.ram.localeCompare(b.ram) || a.interface.localeCompare(b.interface)
);

this.logger.info(`Creating bids for every models (includeInterface: ${includeInterface})...`);
this.logger.info({ event: "CREATING_BIDS", includeInterface });

const doneModels: string[] = [];
for (const model of models) {
const dseq = (await this.getCurrentHeight()).toString();

this.logger.info(`Creating deployment for ${model.vendor} ${model.model} ${model.ram} ${model.interface}...`);
this.logger.info({ event: "CREATING_DEPLOYMENT", ...pick(model, ["vendor", "model", "ram", "interface"]) });

if (doneModels.includes(model.model + "-" + model.ram)) {
this.logger.info(" Skipping.");
this.logger.info({ event: "SKIPPING_DEPLOYMENT", ...pick(model, ["model", "ram"]) });
continue;
}

const gpuSdl = this.getModelSdl(model.vendor, model.model, model.ram, includeInterface ? model.interface : undefined);

await this.createDeployment(client, gpuSdl, walletAddress, dseq);

this.logger.info("Done. Waiting for bids... ");
this.logger.info({ event: "DEPLOYMENT_CREATED" });

await sleep(30_000);

const bids = await this.getBids(walletAddress, dseq);

this.logger.info(`Got ${bids.bids.length} bids. Closing deployment...`);
const bids = await this.bidHttpService.list(walletAddress, dseq);

this.logger.info({ event: "DEPLOYMENT_CLOSING", bidsCount: bids.length });
await this.closeDeployment(client, walletAddress, dseq);

this.logger.info(" Done.");
this.logger.info({ event: "DEPLOYMENT_CLOSED" });

if (!includeInterface) {
doneModels.push(model.model + "-" + model.ram);
Expand All @@ -122,7 +122,7 @@ export class GpuBidsCreatorService {
await sleep(10_000);
}

this.logger.info("Finished!");
this.logger.info({ event: "BIDS_CREATED" });
}

private async createDeployment(client: SigningStargateClient, sdlStr: string, owner: string, dseq: string) {
Expand Down Expand Up @@ -163,12 +163,6 @@ export class GpuBidsCreatorService {
await this.signAndBroadcast(owner, client, [message]);
}

private async getBids(owner: string, dseq: string) {
const response = await axios.get<RestAkashBidListResponseType>(`${apiNodeUrl}/akash/market/v1beta4/bids/list?filters.owner=${owner}&filters.dseq=${dseq}`);

return response.data;
}

private getModelSdl(vendor: string, model: string, ram: string, gpuInterface?: string) {
let gpuSdl = gpuInterface ? sdlTemplateWithRamAndInterface : sdlTemplateWithRam;
gpuSdl = gpuSdl.replace("<VENDOR>", vendor);
Expand Down
47 changes: 0 additions & 47 deletions apps/api/src/types/rest/akashBidListResponse.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/api/src/types/rest/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./akashBidListResponse";
export * from "./akashDeploymentListResponse";
export * from "./akashLeaseListResponse";
export * from "./akashDeploymentInfoResponse";
Expand Down
Loading
Loading