Skip to content

Commit 17e9c24

Browse files
committed
feat(network): API endpoint listing bids
refs akash-network#767
1 parent f1a0c32 commit 17e9c24

File tree

12 files changed

+606
-0
lines changed

12 files changed

+606
-0
lines changed

apps/api/src/app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { RequestContextInterceptor } from "@src/core/services/request-context-in
1616
import { HonoInterceptor } from "@src/core/types/hono-interceptor.type";
1717
import packageJson from "../package.json";
1818
import { apiKeysRouter } from "./auth/routes/api-keys/api-keys.router";
19+
import { bidsRouter } from "./bid/routes/bids/bids.router";
1920
import { chainDb, syncUserSchema, userDb } from "./db/dbConnection";
2021
import { deploymentSettingRouter } from "./deployment/routes/deployment-setting/deployment-setting.router";
2122
import { clientInfoMiddleware } from "./middlewares/clientInfoMiddleware";
@@ -87,6 +88,7 @@ appHono.route("/", getAnonymousUserRouter);
8788
appHono.route("/", sendVerificationEmailRouter);
8889
appHono.route("/", deploymentSettingRouter);
8990
appHono.route("/", apiKeysRouter);
91+
appHono.route("/", bidsRouter);
9092

9193
appHono.get("/status", c => {
9294
const version = packageJson.version;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { singleton } from "tsyringe";
2+
3+
import { AuthService, Protected } from "@src/auth/services/auth.service";
4+
import { ListBidsResponse } from "@src/bid/http-schemas/bid.schema";
5+
import { BidService } from "@src/bid/services/bid.service";
6+
import { UserWalletRepository } from "@src/billing/repositories";
7+
8+
@singleton()
9+
export class BidController {
10+
constructor(
11+
private readonly bidService: BidService,
12+
private readonly authService: AuthService,
13+
private readonly userWalletRepository: UserWalletRepository,
14+
) {}
15+
16+
@Protected([{ action: "create", subject: "DeploymentSetting" }])
17+
async list(dseq: string): Promise<ListBidsResponse> {
18+
const { currentUser } = this.authService;
19+
const wallets = await this.userWalletRepository.findByUserId(currentUser.userId);
20+
const bids = await this.bidService.list(wallets[0].address, dseq);
21+
return { data: bids };
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { z } from "zod";
2+
3+
const DeploymentResource_V3 = z.object({
4+
cpu: z.object({
5+
units: z.object({
6+
val: z.string(),
7+
}),
8+
attributes: z.array(z.object({
9+
key: z.string(),
10+
value: z.string(),
11+
})),
12+
}),
13+
gpu: z.object({
14+
units: z.object({
15+
val: z.string(),
16+
}),
17+
attributes: z.array(z.object({
18+
key: z.string(),
19+
value: z.string(),
20+
})),
21+
}),
22+
memory: z.object({
23+
quantity: z.object({
24+
val: z.string(),
25+
}),
26+
attributes: z.array(z.object({
27+
key: z.string(),
28+
value: z.string(),
29+
})),
30+
}),
31+
storage: z.array(z.object({
32+
name: z.string(),
33+
quantity: z.object({
34+
val: z.string(),
35+
}),
36+
attributes: z.array(z.object({
37+
key: z.string(),
38+
value: z.string(),
39+
})),
40+
})),
41+
endpoints: z.array(z.object({
42+
kind: z.string(),
43+
sequence_number: z.number()
44+
}))
45+
});
46+
47+
export const BidResponseSchema = z.object({
48+
bid: z.object({
49+
bid_id: z.object({
50+
owner: z.string(),
51+
dseq: z.string(),
52+
gseq: z.number(),
53+
oseq: z.number(),
54+
provider: z.string(),
55+
}),
56+
state: z.string(),
57+
price: z.object({
58+
denom: z.string(),
59+
amount: z.string(),
60+
}),
61+
created_at: z.string(),
62+
resources_offer: z.array(z.object({
63+
resources: DeploymentResource_V3,
64+
count: z.number(),
65+
}))
66+
}),
67+
escrow_account: z.object({
68+
id: z.object({
69+
scope: z.string(),
70+
xid: z.string(),
71+
}),
72+
owner: z.string(),
73+
state: z.string(),
74+
balance: z.object({
75+
denom: z.string(),
76+
amount: z.string(),
77+
}),
78+
transferred: z.object({
79+
denom: z.string(),
80+
amount: z.string(),
81+
}),
82+
settled_at: z.string(),
83+
depositor: z.string(),
84+
funds: z.object({
85+
denom: z.string(),
86+
amount: z.string(),
87+
}),
88+
})
89+
});
90+
91+
export const ListBidsParamsSchema = z.object({
92+
dseq: z.string()
93+
});
94+
95+
export const ListBidsResponseSchema = z.object({
96+
data: z.array(BidResponseSchema)
97+
});
98+
99+
export type ListBidsResponse = z.infer<typeof ListBidsResponseSchema>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createRoute } from "@hono/zod-openapi";
2+
import { container } from "tsyringe";
3+
4+
import { BidController } from "@src/bid/controllers/bid/bid.controller";
5+
import { ListBidsParamsSchema, ListBidsResponseSchema } from "@src/bid/http-schemas/bid.schema";
6+
import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
7+
8+
const listRoute = createRoute({
9+
method: "get",
10+
path: "/v1/bids/{dseq}",
11+
summary: "List bids",
12+
tags: ["Bids"],
13+
request: {
14+
params: ListBidsParamsSchema
15+
},
16+
responses: {
17+
200: {
18+
description: "List of bids",
19+
content: {
20+
"application/json": {
21+
schema: ListBidsResponseSchema
22+
}
23+
}
24+
}
25+
}
26+
});
27+
28+
export const bidsRouter = new OpenApiHonoHandler();
29+
30+
bidsRouter.openapi(listRoute, async function routeListBids(c) {
31+
const { dseq } = c.req.valid("param");
32+
const result = await container.resolve(BidController).list(dseq);
33+
return c.json(result, 200);
34+
});
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import axios from "axios";
2+
import { singleton } from "tsyringe";
3+
4+
import { RestAkashBidListResponseType } from "@src/types/rest";
5+
import { apiNodeUrl } from "@src/utils/constants";
6+
import { ListBidsResponse } from "../http-schemas/bid.schema";
7+
8+
@singleton()
9+
export class BidService {
10+
public async list(owner: string, dseq: string): Promise<ListBidsResponse['data']> {
11+
const response = await axios.get<RestAkashBidListResponseType>(`${apiNodeUrl}/akash/market/v1beta4/bids/list?filters.owner=${owner}&filters.dseq=${dseq}`);
12+
13+
return response.data.bids;
14+
}
15+
}

apps/api/test/functional/bids.spec.ts

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { faker } from "@faker-js/faker";
2+
import nock from "nock";
3+
import { container } from "tsyringe";
4+
5+
import { app } from "@src/app";
6+
import { ApiKeyAuthService } from "@src/auth/services/api-key/api-key-auth.service";
7+
import { UserWalletRepository } from "@src/billing/repositories";
8+
import { UserRepository } from "@src/user/repositories";
9+
import { apiNodeUrl } from "@src/utils/constants";
10+
11+
import { ApiKeySeeder } from "@test/seeders/api-key.seeder";
12+
import { UserSeeder } from "@test/seeders/user.seeder";
13+
import { UserWalletSeeder } from "@test/seeders/user-wallet.seeder";
14+
import { DbTestingService } from "@test/services/db-testing.service";
15+
16+
jest.setTimeout(20000);
17+
18+
xdescribe("Bids API", () => {
19+
const userRepository = container.resolve(UserRepository);
20+
const apiKeyAuthService = container.resolve(ApiKeyAuthService);
21+
const userWalletRepository = container.resolve(UserWalletRepository);
22+
const dbService = container.resolve(DbTestingService);
23+
24+
async function mockUserAndApiKey() {
25+
const userId = faker.string.uuid();
26+
const apiKey = faker.word.noun();
27+
28+
const user = UserSeeder.create({ userId });
29+
const apiKeyForUser = ApiKeySeeder.create({ userId });
30+
const walletsForUser = [UserWalletSeeder.create({ userId })];
31+
32+
jest.spyOn(userRepository, "findByUserId").mockImplementation(async (id: string) => {
33+
return Promise.resolve(id === userId ? {
34+
...user,
35+
trial: false,
36+
userWallets: { isTrialing: false }
37+
} : undefined);
38+
});
39+
40+
jest.spyOn(apiKeyAuthService, "getAndValidateApiKeyFromHeader").mockImplementation(async (key: string) => {
41+
return Promise.resolve(key === apiKey ? apiKeyForUser : undefined);
42+
});
43+
44+
jest.spyOn(userWalletRepository, "findByUserId").mockImplementation(async (id: string) => {
45+
return Promise.resolve(id === userId ? walletsForUser : undefined);
46+
});
47+
48+
return { user, apiKey, walletsForUser }
49+
}
50+
51+
afterEach(async () => {
52+
await dbService.cleanAll();
53+
});
54+
55+
afterAll(() => {
56+
jest.restoreAllMocks();
57+
});
58+
59+
describe("GET /v1/bids", () => {
60+
it("returns 401 for an unauthenticated request", async () => {
61+
const response = await app.request("/v1/bids/1234", {
62+
method: "GET",
63+
headers: new Headers({ "Content-Type": "application/json" })
64+
});
65+
66+
expect(response.status).toBe(401);
67+
});
68+
69+
it("returns 404 if no dseq set", async () => {
70+
const { apiKey } = await mockUserAndApiKey();
71+
const response = await app.request("/v1/buds", {
72+
method: "GET",
73+
headers: new Headers({ "Content-Type": "application/json", 'x-api-key': apiKey })
74+
});
75+
76+
expect(response.status).toBe(404);
77+
});
78+
79+
it("lists bids related to user and dseq", async () => {
80+
const { apiKey, walletsForUser } = await mockUserAndApiKey();
81+
const dseq = '1234';
82+
83+
nock(apiNodeUrl).get(`/akash/market/v1beta4/bids/list?filters.owner=${walletsForUser[0].address}&filters.dseq=${dseq}`).reply(200, {
84+
bids: [{
85+
bid: 'fake-bid',
86+
escrow_account: 'fake-escrow-account',
87+
}]
88+
});
89+
90+
const response = await app.request(`/v1/bids/${dseq}`, {
91+
method: "GET",
92+
headers: new Headers({ "Content-Type": "application/json", 'x-api-key': apiKey })
93+
});
94+
95+
expect(response.status).toBe(200);
96+
const result = await response.json();
97+
expect(result.data).toEqual([{
98+
bid: 'fake-bid',
99+
escrow_account: 'fake-escrow-account',
100+
}]);
101+
});
102+
});
103+
});

apps/api/test/seeders/user.seeder.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { faker } from "@faker-js/faker";
2+
3+
import { UserOutput } from "@src/user/repositories";
4+
5+
export class UserSeeder {
6+
static create({
7+
id = faker.string.uuid(),
8+
userId = faker.string.uuid(),
9+
username = faker.word.noun(),
10+
email = faker.internet.email(),
11+
emailVerified = faker.datatype.boolean(),
12+
stripeCustomerId = faker.string.uuid(),
13+
bio = faker.lorem.paragraph(),
14+
subscribedToNewsletter = faker.datatype.boolean(),
15+
youtubeUsername = faker.word.noun(),
16+
twitterUsername = faker.word.noun(),
17+
githubUsername = faker.word.noun(),
18+
lastActiveAt = faker.date.recent(),
19+
lastIp = faker.internet.ip(),
20+
lastUserAgent = faker.internet.userAgent(),
21+
lastFingerprint = faker.word.noun(),
22+
createdAt = faker.date.recent(),
23+
24+
}: Partial<UserOutput> = {}): UserOutput {
25+
return {
26+
id,
27+
userId,
28+
username,
29+
email,
30+
emailVerified,
31+
stripeCustomerId,
32+
bio,
33+
subscribedToNewsletter,
34+
youtubeUsername,
35+
twitterUsername,
36+
githubUsername,
37+
lastActiveAt,
38+
lastIp,
39+
lastUserAgent,
40+
lastFingerprint,
41+
createdAt,
42+
};
43+
}
44+
}

apps/deploy-web/env/.env.e2e.test

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TEST_WALLET_MNEMONIC="motion isolate mother convince snack twenty tumble boost elbow bundle modify balcony"

0 commit comments

Comments
 (0)