Skip to content

Commit 5a58c29

Browse files
authored
feat(network): add API endpoints for deployment (#860)
refs #767
1 parent 42fa955 commit 5a58c29

File tree

18 files changed

+895
-24
lines changed

18 files changed

+895
-24
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/billing/services/managed-signer/managed-signer.service.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,17 @@ export class ManagedSignerService {
4545
}
4646

4747
async executeEncodedTxByUserId(userId: UserWalletOutput["userId"], messages: StringifiedEncodeObject[]) {
48+
return this.executeDecodedTxByUserId(userId, this.decodeMessages(messages));
49+
}
50+
51+
async executeDecodedTxByUserId(userId: UserWalletOutput["userId"], messages: EncodeObject[]) {
4852
const userWallet = await this.userWalletRepository.accessibleBy(this.authService.ability, "sign").findOneByUserId(userId);
4953
assert(userWallet, 404, "UserWallet Not Found");
5054

51-
const decodedMessages = this.decodeMessages(messages);
52-
53-
await Promise.all(decodedMessages.map(message => this.anonymousValidateService.validateLeaseProviders(message, userWallet)));
55+
await Promise.all(messages.map(message => this.anonymousValidateService.validateLeaseProviders(message, userWallet)));
5456

5557
try {
56-
const tx = await this.executeManagedTx(userWallet.id, decodedMessages);
58+
const tx = await this.executeManagedTx(userWallet.id, messages);
5759

5860
await this.balancesService.refreshUserWalletLimits(userWallet);
5961

@@ -68,7 +70,7 @@ export class ManagedSignerService {
6870

6971
return result;
7072
} catch (error) {
71-
throw this.chainErrorService.toAppError(error, decodedMessages);
73+
throw this.chainErrorService.toAppError(error, messages);
7274
}
7375
}
7476

apps/api/src/billing/services/rpc-message-service/rpc-message.service.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { DepositDeploymentAuthorization, MsgCloseDeployment, MsgDepositDeployment } from "@akashnetwork/akash-api/v1beta3";
1+
import { GroupSpec } from "@akashnetwork/akash-api/akash/deployment/v1beta3";
2+
import { DepositDeploymentAuthorization, MsgCloseDeployment, MsgCreateDeployment, MsgDepositDeployment } from "@akashnetwork/akash-api/v1beta3";
23
import { MsgExec, MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx";
34
import { BasicAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant";
45
import { MsgGrantAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx";
@@ -29,6 +30,12 @@ export interface ExecDepositDeploymentMsgOptions extends DepositDeploymentMsgOpt
2930
grantee: string;
3031
}
3132

33+
export interface CreateDeploymentMsgOptions extends DepositDeploymentMsgOptionsBase {
34+
groups: GroupSpec[];
35+
manifestVersion: Uint8Array;
36+
depositor: string;
37+
}
38+
3239
export interface DepositDeploymentMsg {
3340
typeUrl: "/akash.deployment.v1beta3.MsgDepositDeployment";
3441
value: {
@@ -135,6 +142,25 @@ export class RpcMessageService {
135142
};
136143
}
137144

145+
getCreateDeploymentMsg({ owner, dseq, groups, manifestVersion, denom, amount, depositor }: CreateDeploymentMsgOptions) {
146+
return {
147+
typeUrl: `/akash.deployment.v1beta3.MsgCreateDeployment`,
148+
value: MsgCreateDeployment.fromPartial({
149+
id: {
150+
owner,
151+
dseq,
152+
},
153+
groups,
154+
version: manifestVersion,
155+
deposit: {
156+
denom,
157+
amount: amount.toString(),
158+
},
159+
depositor,
160+
})
161+
};
162+
}
163+
138164
getDepositDeploymentMsg({ owner, dseq, amount, denom, depositor }: DepositDeploymentMsgOptions): DepositDeploymentMsg {
139165
return {
140166
typeUrl: "/akash.deployment.v1beta3.MsgDepositDeployment",

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,39 @@
11
import { singleton } from "tsyringe";
22

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";
3+
import { AuthService, Protected } from "@src/auth/services/auth.service";
4+
import { UserWalletRepository } from "@src/billing/repositories";
5+
import { CreateDeploymentRequest, CreateDeploymentResponse, GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
6+
import { DeploymentService } from "@src/deployment/services/deployment/deployment.service";
77

88
@singleton()
9-
export class TopUpDeploymentsController {
9+
export class DeploymentController {
1010
constructor(
11-
private readonly topUpManagedDeploymentsService: TopUpManagedDeploymentsService,
12-
private readonly staleDeploymentsCleanerService: StaleManagedDeploymentsCleanerService
11+
private readonly deploymentService: DeploymentService,
12+
private readonly authService: AuthService,
13+
private readonly userWalletRepository: UserWalletRepository
1314
) {}
1415

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

19-
async cleanUpStaleDeployment(options: CleanUpStaleDeploymentsParams) {
20-
await this.staleDeploymentsCleanerService.cleanup(options);
28+
@Protected([{ action: "sign", subject: "UserWallet" }])
29+
async create(input: CreateDeploymentRequest['data']): Promise<CreateDeploymentResponse> {
30+
const { currentUser, ability } = this.authService;
31+
32+
const wallets = await this.userWalletRepository.accessibleBy(ability, "sign").findByUserId(currentUser.userId);
33+
const result = await this.deploymentService.create(wallets[0], input);
34+
35+
return {
36+
data: result
37+
};
2138
}
2239
}
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,75 @@
1+
import { z } from "zod";
2+
3+
import { SignTxResponseOutputSchema } from "@src/billing/routes/sign-and-broadcast-tx/sign-and-broadcast-tx.router";
4+
5+
export const DeploymentResponseSchema = z.object({
6+
deployment: z.object({
7+
deployment_id: z.object({
8+
owner: z.string(),
9+
dseq: z.string(),
10+
}),
11+
state: z.string(),
12+
version: z.string(),
13+
created_at: z.string(),
14+
}),
15+
leases: z.array(z.object({
16+
lease_id: z.object({
17+
owner: z.string(),
18+
dseq: z.string(),
19+
gseq: z.number(),
20+
oseq: z.number(),
21+
provider: z.string(),
22+
}),
23+
state: z.string(),
24+
price: z.object({
25+
denom: z.string(),
26+
amount: z.string(),
27+
}),
28+
created_at: z.string(),
29+
closed_on: z.string(),
30+
})),
31+
escrow_account: z.object({
32+
id: z.object({
33+
scope: z.string(),
34+
xid: z.string(),
35+
}),
36+
owner: z.string(),
37+
state: z.string(),
38+
balance: z.object({
39+
denom: z.string(),
40+
amount: z.string(),
41+
}),
42+
transferred: z.object({
43+
denom: z.string(),
44+
amount: z.string(),
45+
}),
46+
settled_at: z.string(),
47+
depositor: z.string(),
48+
funds: z.object({
49+
denom: z.string(),
50+
amount: z.string(),
51+
})
52+
})
53+
});
54+
55+
export const GetDeploymentQuerySchema = z.object({
56+
dseq: z.string(),
57+
userId: z.optional(z.string()),
58+
});
59+
60+
export const GetDeploymentResponseSchema = z.object({
61+
data: DeploymentResponseSchema
62+
});
63+
64+
export const CreateDeploymentRequestSchema = z.object({
65+
data: z.object({
66+
sdl: z.string(),
67+
deposit: z.number(),
68+
})
69+
});
70+
71+
export const CreateDeploymentResponseSchema = SignTxResponseOutputSchema;
72+
73+
export type GetDeploymentResponse = z.infer<typeof GetDeploymentResponseSchema>;
74+
export type CreateDeploymentRequest = z.infer<typeof CreateDeploymentRequestSchema>;
75+
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,67 @@
1+
import { SDL } from "@akashnetwork/akashjs/build/sdl";
2+
import { BlockHttpService, DeploymentHttpService, LeaseHttpService } from "@akashnetwork/http-sdk";
3+
import { BadRequest, InternalServerError, NotFound } from "http-errors";
4+
import { singleton } from "tsyringe";
5+
6+
import { InjectWallet } from "@src/billing/providers/wallet.provider";
7+
import { UserWalletOutput } from "@src/billing/repositories";
8+
import { ManagedSignerService, RpcMessageService, Wallet } from "@src/billing/services";
9+
import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
10+
import { CreateDeploymentRequest, CreateDeploymentResponse, GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
11+
12+
@singleton()
13+
export class DeploymentService {
14+
constructor(
15+
private readonly blockHttpService: BlockHttpService,
16+
private readonly deploymentHttpService: DeploymentHttpService,
17+
private readonly leaseHttpService: LeaseHttpService,
18+
private readonly signerService: ManagedSignerService,
19+
@InjectWallet("MANAGED") private readonly masterWallet: Wallet,
20+
private readonly billingConfigService: BillingConfigService,
21+
private readonly rpcMessageService: RpcMessageService,
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(wallet: UserWalletOutput, input: CreateDeploymentRequest['data']): Promise<CreateDeploymentResponse['data']> {
44+
let sdl: SDL;
45+
try {
46+
sdl = SDL.fromString(input.sdl, 'beta3');
47+
} catch (error) {
48+
if (error.name === 'SdlValidationError') {
49+
throw new BadRequest(error.message);
50+
}
51+
52+
throw new BadRequest("Invalid SDL");
53+
}
54+
55+
const message = this.rpcMessageService.getCreateDeploymentMsg({
56+
owner: wallet.address,
57+
dseq: await this.blockHttpService.getCurrentHeight(),
58+
groups: sdl.groups(),
59+
denom: this.billingConfigService.get("DEPLOYMENT_GRANT_DENOM"),
60+
amount: input.deposit,
61+
manifestVersion: await sdl.manifestVersion(),
62+
depositor: await this.masterWallet.getFirstAddress()
63+
});
64+
65+
return await this.signerService.executeDecodedTxByUserId(wallet.userId, [message]);
66+
}
67+
}

0 commit comments

Comments
 (0)