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(deployment): managed api create leases #969

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion apps/api/env/.env.functional.test
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ STRIPE_CHECKOUT_REDIRECT_URL=http://localhost:3000
AUTH0_M2M_DOMAIN=AUTH0_M2M_DOMAIN
AUTH0_M2M_SECRET=AUTH0_SECRET
AUTH0_M2M_CLIENT_ID=AUTH0_CLIENT_ID
DEPLOYMENT_ENV=test
DEPLOYMENT_ENV=test
PROVIDER_PROXY_URL=http://localhost:3040
1 change: 1 addition & 0 deletions apps/api/env/.env.local.sample
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ STRIPE_PRODUCT_ID=STRIPE_PRODUCT_ID
STRIPE_FIXED_PRICE_ID=STRIPE_FIXED_PRICE_ID
STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET
STRIPE_CHECKOUT_REDIRECT_URL=http://localhost:3000
PROVIDER_PROXY_URL=http://localhost:3040

SENTRY_ENABLED=true
SENTRY_DSN=SENTRY_DSN
Expand Down
3 changes: 2 additions & 1 deletion apps/api/env/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ SERVER_ORIGIN=https://console-api.akash.network
DRIZZLE_MIGRATIONS_FOLDER=./dist/drizzle
SENTRY_ENABLED=true
BILLING_ENABLED=true
STRIPE_CHECKOUT_REDIRECT_URL=https://console.akash.network
STRIPE_CHECKOUT_REDIRECT_URL=https://console.akash.network
PROVIDER_PROXY_URL=https://console-provider-proxy.akash.network
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"migration:gen": "drizzle-kit generate",
"prod": "doppler run -- node dist/server.js",
"release": "release-it",
"script:lease-flow": "ts-node script/lease-flow.ts",
"start": "webpack --config webpack.dev.js --watch",
"test": "jest --selectProjects unit functional --runInBand",
"test:cov": "jest --selectProjects unit functional --coverage --runInBand",
Expand Down
4 changes: 4 additions & 0 deletions apps/api/script/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copy this file to .env.local and fill in your values

API_URL=http://localhost:3080
API_KEY=your_api_key
155 changes: 155 additions & 0 deletions apps/api/script/lease-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import axios from "axios";
import * as dotenv from "dotenv";
import * as fs from "node:fs";
import * as path from "node:path";

// Load environment variables from .env.local in the script directory
const envPath = path.resolve(__dirname, ".env.local");
if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath });
} else {
console.warn(".env.local file not found in script directory, using existing environment variables");
}

// Load the SDL file
const yml = fs.readFileSync(path.resolve(__dirname, "../test/mocks/hello-world-sdl.yml"), "utf8");

// Configure axios
const API_URL = process.env.API_URL || "http://localhost:3080";
const api = axios.create({
baseURL: API_URL,
headers: {
"Content-Type": "application/json"
}
});

async function waitForBids(dseq: string, apiKey: string, maxAttempts = 10): Promise<any[]> {

Check warning on line 26 in apps/api/script/lease-flow.ts

View workflow job for this annotation

GitHub Actions / validate-n-build-api

Unexpected any. Specify a different type
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await api.get(`/v1/bids?dseq=${dseq}`, {
headers: {
"x-api-key": apiKey
}
});

if (response.data?.data?.length > 0) {
return response.data.data;
}
} catch (error) {
console.log(`Attempt ${i + 1}/${maxAttempts} failed to get bids. Retrying...`);
}

await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds between attempts
}
throw new Error("No bids received after maximum attempts");
}

/**
* This script is used to create a lease for a deployment using an api key.
* It creates a certificate, creates a deployment, waits for bids, creates a lease, and then closes the deployment.
*/
async function main() {
try {
// 1. Setup user and get authentication
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error("API_KEY environment variable is required");
}

// 2. Create certificate
console.log("Creating certificate...");
const certResponse = await api.post(
"/v1/certificates",
{},
{
headers: {
"x-api-key": apiKey
}
}
);

const { certPem, encryptedKey } = certResponse.data.data;
console.log("Certificate created successfully");

// 3. Create deployment
console.log("Creating deployment...");
const deployResponse = await api.post(
"/v1/deployments",
{
data: {
sdl: yml,
deposit: 5000000
}
},
{
headers: {
"x-api-key": apiKey
}
}
);

const { dseq, manifest } = deployResponse.data.data;
console.log(`Deployment created with dseq: ${dseq}`);

// 4. Wait for and get bids
console.log("Waiting for bids...");
const bids = await waitForBids(dseq, apiKey);
console.log(`Received ${bids.length} bids`);
const firstBid = bids[0];

const body = {
manifest,
certificate: {
certPem,
keyPem: encryptedKey
},
leases: [
{
dseq,
gseq: firstBid.bid.bid_id.gseq,
oseq: firstBid.bid.bid_id.oseq,
provider: firstBid.bid.bid_id.provider
}
]
};

// 5. Create lease and send manifest
console.log("Creating lease and sending manifest...");
const leaseResponse = await api.post("/v1/leases", body, {
headers: {
"x-api-key": apiKey
}
});

if (leaseResponse.status !== 200) {
throw new Error(`Failed to create lease: ${leaseResponse.statusText}`);
}
console.log("Lease created successfully");

// 6. Close deployment
console.log("Closing deployment...");
const closeResponse = await api.delete(`/v1/deployments/${dseq}`, {
headers: {
"x-api-key": apiKey
}
});

if (closeResponse.status !== 200) {
throw new Error(`Failed to close deployment: ${closeResponse.statusText}`);
}

console.log("Deployment closed successfully");
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Error:", {
message: error.message,
response: error.response?.data
});
} else {
console.error("Error:", error);
}
process.exit(1);
}
}

main();
2 changes: 2 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { certificateRouter } from "./certificate/routes/certificate.router";
import { chainDb, syncUserSchema, userDb } from "./db/dbConnection";
import { deploymentSettingRouter } from "./deployment/routes/deployment-setting/deployment-setting.router";
import { deploymentsRouter } from "./deployment/routes/deployments/deployments.router";
import { leasesRouter } from "./deployment/routes/leases/leases.router";
import { clientInfoMiddleware } from "./middlewares/clientInfoMiddleware";
import { apiRouter } from "./routers/apiRouter";
import { dashboardRouter } from "./routers/dashboardRouter";
Expand Down Expand Up @@ -90,6 +91,7 @@ appHono.route("/", getAnonymousUserRouter);
appHono.route("/", sendVerificationEmailRouter);
appHono.route("/", deploymentSettingRouter);
appHono.route("/", deploymentsRouter);
appHono.route("/", leasesRouter);
appHono.route("/", apiKeysRouter);
appHono.route("/", bidsRouter);
appHono.route("/", certificateRouter);
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/bid/controllers/bid/bid.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ export class BidController {
constructor(
private readonly bidHttpService: BidHttpService,
private readonly authService: AuthService,
private readonly userWalletRepository: UserWalletRepository,
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 wallets = await this.userWalletRepository.accessibleBy(ability, "sign").findByUserId(userId ?? currentUser.id);
const bids = await this.bidHttpService.list(wallets[0].address, dseq);

return { data: bids };
Expand Down
97 changes: 56 additions & 41 deletions apps/api/src/bid/http-schemas/bid.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,57 @@ import { z } from "zod";
const DeploymentResource_V3 = z.object({
cpu: z.object({
units: z.object({
val: z.string(),
val: z.string()
}),
attributes: z.array(z.object({
key: z.string(),
value: z.string(),
})),
attributes: z.array(
z.object({
key: z.string(),
value: z.string()
})
)
}),
gpu: z.object({
units: z.object({
val: z.string(),
val: z.string()
}),
attributes: z.array(z.object({
key: z.string(),
value: z.string(),
})),
attributes: z.array(
z.object({
key: z.string(),
value: z.string()
})
)
}),
memory: z.object({
quantity: z.object({
val: z.string(),
val: z.string()
}),
attributes: z.array(z.object({
key: z.string(),
value: 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()
}))
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({
Expand All @@ -51,50 +63,53 @@ export const BidResponseSchema = z.object({
dseq: z.string(),
gseq: z.number(),
oseq: z.number(),
provider: z.string(),
provider: z.string()
}),
state: z.string(),
price: z.object({
denom: z.string(),
amount: z.string(),
amount: z.string()
}),
created_at: z.string(),
resources_offer: z.array(z.object({
resources: DeploymentResource_V3,
count: z.number(),
}))
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(),
xid: z.string()
}),
owner: z.string(),
state: z.string(),
balance: z.object({
denom: z.string(),
amount: z.string(),
amount: z.string()
}),
transferred: z.object({
denom: z.string(),
amount: z.string(),
amount: z.string()
}),
settled_at: z.string(),
depositor: z.string(),
funds: z.object({
denom: z.string(),
amount: z.string(),
}),
amount: z.string()
})
})
});

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

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

export type BidResponse = z.infer<typeof BidResponseSchema>;
export type ListBidsResponse = z.infer<typeof ListBidsResponseSchema>;
2 changes: 1 addition & 1 deletion apps/api/src/billing/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const envSchema = z.object({
TRIAL_DEPLOYMENT_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
TRIAL_FEES_ALLOWANCE_AMOUNT: z.number({ coerce: true }),
DEPLOYMENT_GRANT_DENOM: z.string(),
GAS_SAFETY_MULTIPLIER: z.number({ coerce: true }).default(1.5),
GAS_SAFETY_MULTIPLIER: z.number({ coerce: true }).default(1.6),
AVERAGE_GAS_PRICE: z.number({ coerce: true }).default(0.0025),
FEE_ALLOWANCE_REFILL_THRESHOLD: z.number({ coerce: true }),
FEE_ALLOWANCE_REFILL_AMOUNT: z.number({ coerce: true }),
Expand Down
Loading
Loading