Skip to content

Commit 3e83dae

Browse files
committed
feat(network): add API endpoints for deployment
refs akash-network#767
1 parent 0c7a8b9 commit 3e83dae

File tree

16 files changed

+851
-18
lines changed

16 files changed

+851
-18
lines changed

apps/api/src/app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { apiKeysRouter } from "./auth/routes/api-keys/api-keys.router";
1919
import { bidsRouter } from "./bid/routes/bids/bids.router";
2020
import { chainDb, syncUserSchema, userDb } from "./db/dbConnection";
2121
import { deploymentSettingRouter } from "./deployment/routes/deployment-setting/deployment-setting.router";
22+
import { deploymentsRouter } from "./deployment/routes/deployments/deployments.router";
2223
import { clientInfoMiddleware } from "./middlewares/clientInfoMiddleware";
2324
import { apiRouter } from "./routers/apiRouter";
2425
import { dashboardRouter } from "./routers/dashboardRouter";
@@ -87,6 +88,7 @@ appHono.route("/", createAnonymousUserRouter);
8788
appHono.route("/", getAnonymousUserRouter);
8889
appHono.route("/", sendVerificationEmailRouter);
8990
appHono.route("/", deploymentSettingRouter);
91+
appHono.route("/", deploymentsRouter);
9092
appHono.route("/", apiKeysRouter);
9193
appHono.route("/", bidsRouter);
9294

apps/api/src/console.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { z } from "zod";
1111

1212
import { WalletController } from "@src/billing/controllers/wallet/wallet.controller";
1313
import { chainDb } from "@src/db/dbConnection";
14-
import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/deployment.controller";
14+
import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/top-up-deployments.controller";
1515
import { GpuBotController } from "@src/deployment/controllers/gpu-bot/gpu-bot.controller";
1616
import { UserController } from "@src/user/controllers/user/user.controller";
1717
import { UserConfigService } from "@src/user/services/user-config/user-config.service";
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { AuthzHttpService, BalanceHttpService, BidHttpService, BlockHttpService } from "@akashnetwork/http-sdk";
1+
import { AuthzHttpService, BalanceHttpService, BidHttpService, BlockHttpService, DeploymentHttpService, LeaseHttpService } from "@akashnetwork/http-sdk";
22
import { container } from "tsyringe";
33

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

6-
const SERVICES = [BalanceHttpService, AuthzHttpService, BlockHttpService, BidHttpService];
6+
const SERVICES = [BalanceHttpService, AuthzHttpService, BlockHttpService, BidHttpService, DeploymentHttpService, LeaseHttpService];
77

88
SERVICES.forEach(Service => container.register(Service, { useValue: new Service({ baseURL: apiNodeUrl }) }));
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,47 @@
1+
import { SDL } from "@akashnetwork/akashjs/build/sdl";
2+
import assert from "http-assert";
13
import { singleton } from "tsyringe";
24

3-
import { StaleManagedDeploymentsCleanerService } from "@src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service";
4-
import { TopUpManagedDeploymentsService } from "@src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service";
5-
import { TopUpDeploymentsOptions } from "@src/deployment/types/deployments-refiller";
6-
import { CleanUpStaleDeploymentsParams } from "@src/deployment/types/state-deployments";
5+
import { AuthService, Protected } from "@src/auth/services/auth.service";
6+
import { UserWalletRepository } from "@src/billing/repositories";
7+
import { CreateDeploymentRequest, CreateDeploymentResponse, GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
8+
import { DeploymentService } from "@src/deployment/services/deployment/deployment.service";
79

810
@singleton()
9-
export class TopUpDeploymentsController {
11+
export class DeploymentController {
1012
constructor(
11-
private readonly topUpManagedDeploymentsService: TopUpManagedDeploymentsService,
12-
private readonly staleDeploymentsCleanerService: StaleManagedDeploymentsCleanerService
13+
private readonly deploymentService: DeploymentService,
14+
private readonly authService: AuthService,
15+
private readonly userWalletRepository: UserWalletRepository
1316
) {}
1417

15-
async topUpDeployments(options: TopUpDeploymentsOptions) {
16-
await this.topUpManagedDeploymentsService.topUpDeployments(options);
18+
@Protected([{ action: "sign", subject: "UserWallet" }])
19+
async findByDseqAndUserId(dseq: string, userId?: string): Promise<GetDeploymentResponse> {
20+
const { currentUser, ability } = this.authService;
21+
22+
const wallets = await this.userWalletRepository.accessibleBy(ability, "sign").findByUserId(userId ?? currentUser.userId);
23+
const deployment = await this.deploymentService.findByOwnerAndDseq(wallets[0].address, dseq);
24+
25+
return {
26+
data: deployment
27+
}
1728
}
1829

19-
async cleanUpStaleDeployment(options: CleanUpStaleDeploymentsParams) {
20-
await this.staleDeploymentsCleanerService.cleanup(options);
30+
@Protected([{ action: "sign", subject: "UserWallet" }])
31+
async create(input: CreateDeploymentRequest['data']): Promise<CreateDeploymentResponse> {
32+
let sdl: SDL;
33+
try {
34+
sdl = SDL.fromString(input.sdl, 'beta3');
35+
} catch (_error) {
36+
assert(false, 400, "Invalid SDL");
37+
}
38+
39+
const result = await this.deploymentService.create(sdl);
40+
41+
return {
42+
data: {
43+
transactionHash: result.transactionHash.toString()
44+
}
45+
};
2146
}
2247
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { singleton } from "tsyringe";
2+
3+
import { StaleManagedDeploymentsCleanerService } from "@src/deployment/services/stale-managed-deployments-cleaner/stale-managed-deployments-cleaner.service";
4+
import { TopUpManagedDeploymentsService } from "@src/deployment/services/top-up-managed-deployments/top-up-managed-deployments.service";
5+
import { TopUpDeploymentsOptions } from "@src/deployment/types/deployments-refiller";
6+
import { CleanUpStaleDeploymentsParams } from "@src/deployment/types/state-deployments";
7+
8+
@singleton()
9+
export class TopUpDeploymentsController {
10+
constructor(
11+
private readonly topUpManagedDeploymentsService: TopUpManagedDeploymentsService,
12+
private readonly staleDeploymentsCleanerService: StaleManagedDeploymentsCleanerService
13+
) {}
14+
15+
async topUpDeployments(options: TopUpDeploymentsOptions) {
16+
await this.topUpManagedDeploymentsService.topUpDeployments(options);
17+
}
18+
19+
async cleanUpStaleDeployment(options: CleanUpStaleDeploymentsParams) {
20+
await this.staleDeploymentsCleanerService.cleanup(options);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { z } from "zod";
2+
3+
export const DeploymentResponseSchema = z.object({
4+
deployment: z.object({
5+
deployment_id: z.object({
6+
owner: z.string(),
7+
dseq: z.string(),
8+
}),
9+
state: z.string(),
10+
version: z.string(),
11+
created_at: z.string(),
12+
}),
13+
leases: z.array(z.object({
14+
lease_id: z.object({
15+
owner: z.string(),
16+
dseq: z.string(),
17+
gseq: z.number(),
18+
oseq: z.number(),
19+
provider: z.string(),
20+
}),
21+
state: z.string(),
22+
price: z.object({
23+
denom: z.string(),
24+
amount: z.string(),
25+
}),
26+
created_at: z.string(),
27+
closed_on: z.string(),
28+
})),
29+
escrow_account: z.object({
30+
id: z.object({
31+
scope: z.string(),
32+
xid: z.string(),
33+
}),
34+
owner: z.string(),
35+
state: z.string(),
36+
balance: z.object({
37+
denom: z.string(),
38+
amount: z.string(),
39+
}),
40+
transferred: z.object({
41+
denom: z.string(),
42+
amount: z.string(),
43+
}),
44+
settled_at: z.string(),
45+
depositor: z.string(),
46+
funds: z.object({
47+
denom: z.string(),
48+
amount: z.string(),
49+
})
50+
})
51+
});
52+
53+
export const GetDeploymentQuerySchema = z.object({
54+
dseq: z.string(),
55+
userId: z.optional(z.string()),
56+
});
57+
58+
export const GetDeploymentResponseSchema = z.object({
59+
data: DeploymentResponseSchema
60+
});
61+
62+
export const CreateDeploymentRequestSchema = z.object({
63+
data: z.object({
64+
sdl: z.string(),
65+
})
66+
});
67+
68+
export const CreateDeploymentResponseSchema = z.object({
69+
data: z.object({
70+
transactionHash: z.string(),
71+
})
72+
});
73+
74+
export type GetDeploymentResponse = z.infer<typeof GetDeploymentResponseSchema>;
75+
export type CreateDeploymentRequest = z.infer<typeof CreateDeploymentRequestSchema>;
76+
export type CreateDeploymentResponse = z.infer<typeof CreateDeploymentResponseSchema>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { createRoute } from "@hono/zod-openapi";
2+
import { container } from "tsyringe";
3+
4+
import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
5+
import { DeploymentController } from "@src/deployment/controllers/deployment/deployment.controller";
6+
import { CreateDeploymentRequestSchema, CreateDeploymentResponseSchema, GetDeploymentQuerySchema, GetDeploymentResponseSchema } from "@src/deployment/http-schemas/deployment.schema";
7+
8+
const getRoute = createRoute({
9+
method: "get",
10+
path: "/v1/deployments",
11+
summary: "Get a deployment",
12+
tags: ["Deployments"],
13+
request: {
14+
query: GetDeploymentQuerySchema
15+
},
16+
responses: {
17+
200: {
18+
description: "Returns deployment info",
19+
content: {
20+
"application/json": {
21+
schema: GetDeploymentResponseSchema
22+
}
23+
}
24+
}
25+
}
26+
});
27+
28+
const postRoute = createRoute({
29+
method: "post",
30+
path: "/v1/deployments",
31+
summary: "Create new deployment",
32+
tags: ["Deployments"],
33+
request: {
34+
body: {
35+
content: {
36+
"application/json": {
37+
schema: CreateDeploymentRequestSchema
38+
}
39+
}
40+
}
41+
},
42+
responses: {
43+
201: {
44+
description: "API key created successfully",
45+
content: {
46+
"application/json": {
47+
schema: CreateDeploymentResponseSchema
48+
}
49+
}
50+
}
51+
}
52+
});
53+
54+
export const deploymentsRouter = new OpenApiHonoHandler();
55+
56+
deploymentsRouter.openapi(getRoute, async function routeGetDeployment(c) {
57+
const { dseq, userId } = c.req.valid("query");
58+
const result = await container.resolve(DeploymentController).findByDseqAndUserId(dseq, userId);
59+
return c.json(result, 200);
60+
});
61+
62+
deploymentsRouter.openapi(postRoute, async function routeCreateDeployment(c) {
63+
const { data } = c.req.valid("json");
64+
const result = await container.resolve(DeploymentController).create(data);
65+
return c.json(result, 201);
66+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { MsgCreateDeployment } from "@akashnetwork/akash-api/v1beta3";
2+
import { SDL } from "@akashnetwork/akashjs/build/sdl";
3+
import { getAkashTypeRegistry } from "@akashnetwork/akashjs/build/stargate";
4+
import { DeploymentHttpService, LeaseHttpService } from "@akashnetwork/http-sdk";
5+
import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing";
6+
import { SigningStargateClient } from "@cosmjs/stargate";
7+
import { InternalServerError, NotFound } from "http-errors";
8+
import { singleton } from "tsyringe";
9+
10+
import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
11+
import { CreateDeploymentResponse, GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
12+
import { MessageTransmitterService } from "@src/deployment/services/message-transmitter/message-transmitter.service";
13+
import { env } from "@src/utils/env";
14+
15+
@singleton()
16+
export class DeploymentService {
17+
constructor(
18+
private readonly config: BillingConfigService,
19+
private readonly messageTransmitterService: MessageTransmitterService,
20+
private readonly deploymentHttpService: DeploymentHttpService,
21+
private readonly leaseHttpService: LeaseHttpService,
22+
) { }
23+
24+
public async findByOwnerAndDseq(owner: string, dseq: string): Promise<GetDeploymentResponse['data']> {
25+
const deploymentResponse = await this.deploymentHttpService.findByOwnerAndDseq(owner, dseq);
26+
if ("code" in deploymentResponse) {
27+
if (deploymentResponse.message?.toLowerCase().includes("deployment not found")) {
28+
throw new NotFound("Deployment not found");
29+
}
30+
31+
throw new InternalServerError(deploymentResponse.message);
32+
}
33+
34+
const { leases } = await this.leaseHttpService.listByOwnerAndDseq(owner, dseq);
35+
36+
return {
37+
deployment: deploymentResponse.deployment,
38+
leases: leases.map(({ lease }) => lease),
39+
escrow_account: deploymentResponse.escrow_account
40+
};
41+
}
42+
43+
public async create(sdl: SDL): Promise<CreateDeploymentResponse['data']> {
44+
const dseq = (await this.messageTransmitterService.getCurrentHeight()).toString();
45+
const manifestVersion = await sdl.manifestVersion();
46+
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(env.GPU_BOT_WALLET_MNEMONIC, { prefix: "akash" });
47+
const [account] = await wallet.getAccounts();
48+
const myRegistry = new Registry([...getAkashTypeRegistry()]);
49+
const client = await SigningStargateClient.connectWithSigner(this.config.get("RPC_NODE_ENDPOINT"), wallet, {
50+
registry: myRegistry,
51+
broadcastTimeoutMs: 30_000
52+
});
53+
54+
const message = {
55+
typeUrl: `/akash.deployment.v1beta3.MsgCreateDeployment`,
56+
value: MsgCreateDeployment.fromPartial({
57+
id: {
58+
owner: account.address,
59+
dseq: dseq
60+
},
61+
groups: sdl.groups(),
62+
version: manifestVersion,
63+
deposit: {
64+
denom: "uakt",
65+
amount: "500000"
66+
},
67+
depositor: account.address
68+
})
69+
};
70+
71+
return await this.messageTransmitterService.signAndBroadcast(account.address, client, [message]);
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { LoggerService } from "@akashnetwork/logging";
2+
import { EncodeObject } from "@cosmjs/proto-signing";
3+
import { calculateFee, SigningStargateClient } from "@cosmjs/stargate";
4+
import axios from "axios";
5+
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
6+
import { singleton } from "tsyringe";
7+
8+
import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
9+
import { apiNodeUrl } from "@src/utils/constants";
10+
11+
@singleton()
12+
export class MessageTransmitterService {
13+
private readonly logger = LoggerService.forContext(MessageTransmitterService.name);
14+
15+
constructor(private readonly config: BillingConfigService) { }
16+
17+
async getCurrentHeight() {
18+
const response = await axios.get(`${apiNodeUrl}/blocks/latest`);
19+
20+
const height = parseInt(response.data.block.header.height);
21+
22+
if (isNaN(height)) throw new Error("Failed to get current height");
23+
24+
return height;
25+
}
26+
27+
async signAndBroadcast(address: string, client: SigningStargateClient, messages: readonly EncodeObject[]) {
28+
const simulation = await client.simulate(address, messages, "");
29+
30+
const fee = calculateFee(Math.round(simulation * 1.35), `${this.config.get("AVERAGE_GAS_PRICE")}uakt`);
31+
32+
const txRaw = await client.sign(address, messages, fee, "");
33+
34+
const txRawBytes = Uint8Array.from(TxRaw.encode(txRaw).finish());
35+
const txResult = await client.broadcastTx(txRawBytes);
36+
37+
if (txResult.code !== 0) {
38+
this.logger.error(txResult);
39+
throw new Error(`Error broadcasting transaction: ${txResult.rawLog}`);
40+
}
41+
42+
return txResult;
43+
}
44+
}

apps/api/src/services/external/apiNodeService.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { getTransactionByAddress } from "@src/services/db/transactionsService";
1111
import {
1212
CosmosGovProposalResponse,
1313
CosmosGovProposalsResponse,
14+
RestAkashDeploymentInfoResponse,
1415
RestAkashDeploymentListResponse,
15-
RestAkasheploymentInfoResponse,
1616
RestAkashLeaseListResponse,
1717
RestCosmosBankBalancesResponse,
1818
RestCosmosDistributionDelegatorsRewardsResponse,
@@ -392,7 +392,7 @@ export async function getDeployment(owner: string, dseq: string) {
392392
return null;
393393
}
394394

395-
const deploymentData = (await deploymentResponse.json()) as RestAkasheploymentInfoResponse;
395+
const deploymentData = (await deploymentResponse.json()) as RestAkashDeploymentInfoResponse;
396396

397397
if ("code" in deploymentData) {
398398
if (deploymentData.message?.toLowerCase().includes("deployment not found")) {

0 commit comments

Comments
 (0)