diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..182d519 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +name: CLI (Check) + +on: [push] + +jobs: + # run format, lint, and test + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun check \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index c7a5d93..292ff99 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 0aac073..5e8b58b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "scripts": { "format": "biome format --write ./src", "lint": "biome lint --write ./src", - "check": "biome check ./src", + "check-fix": "biome check ./src --apply", + "check": "biome check ./src && tsc --noEmit --project .", "dev": "IS_DEVELOPMENT_CLI_ENV=true bun run src/index.ts", "release": "bun run src/scripts/release.ts", "prod": "bun run src/index.ts", @@ -28,7 +29,7 @@ "@types/bun": "latest" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.6.2" }, "version": "0.0.29" } \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts deleted file mode 100644 index aadc946..0000000 --- a/src/api/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -export interface ApiError { - object: "error"; - code: string; - message: string; - details: Record; -} - -export const ApiErrorCode = { - Base: { - InvalidRequest: "invalid_request", - NotAuthenticated: "not_authenticated", - Unauthorized: "unauthorized", - NotFound: "not_found", - RouteNotFound: "route_not_found", - TooManyRequests: "too_many_requests", - InternalServer: "internal_server", - }, - Accounts: { - NotFound: "account.not_found", - }, - Orders: { - InvalidId: "order.invalid_id", - InvalidPrice: "order.invalid_price", - InvalidQuantity: "order.invalid_quantity", - InvalidStart: "order.invalid_start", - InvalidDuration: "order.invalid_duration", - InsufficientFunds: "order.insufficient_funds", - AlreadyCancelled: "order.already_cancelled", - NotFound: "order.not_found", - }, - Tokens: { - TokenNotFound: "token.not_found", - InvalidTokenCreateOriginClient: "token.invalid_token_create_origin_client", - InvalidTokenExpirationDuration: "token.invalid_token_expiration_duration", - MaxTokenLimitReached: "token.max_token_limit_reached", - }, - Quotes: { - NoAvailability: "quote.no_availability_satisfying_quote_parameters", - InvalidDateRange: "quote.invalid_date_range", - }, -}; - -export function objToQueryString(obj: Record): string { - return Object.keys(obj) - .map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(obj[key])) - .join("&"); -} diff --git a/src/api/orders.ts b/src/api/orders.ts deleted file mode 100644 index 88397c6..0000000 --- a/src/api/orders.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { ApiError } from "."; -import { getAuthToken } from "../helpers/config"; -import { fetchAndHandleErrors } from "../helpers/fetch"; -import type { Centicents } from "../helpers/units"; -import { getApiUrl } from "../helpers/urls"; -import type { Nullable } from "../types/empty"; - -export type OrderType = "buy" | "sell"; -export enum OrderStatus { - Pending = "pending", - Rejected = "rejected", - Open = "open", - Cancelled = "cancelled", - Filled = "filled", - Expired = "expired", -} -export interface OrderFlags { - market: boolean; - post_only: boolean; - ioc: boolean; - prorate: boolean; -} - -export interface HydratedOrder { - object: "order"; - id: string; - side: OrderType; - instance_type: string; - price: number; - start_at: string; - duration: number; - quantity: number; - flags: OrderFlags; - created_at: string; - executed: boolean; - execution_price?: number; - cancelled: boolean; - status: OrderStatus; -} -export interface PlacedOrder { - object: "order"; - id: string; - status: OrderStatus.Pending; -} - -export type Order = PlacedOrder | HydratedOrder; - -// -- place buy order - -interface PlaceBuyOrderRequestOptions { - instance_type: string; - quantity: number; - duration: number; - start_at: string; - price: Centicents; -} - -interface PlaceBuyOrderReturn { - data: Nullable; - err: Nullable; -} -export async function placeBuyOrderRequest( - body: PlaceBuyOrderRequestOptions, -): Promise { - const response = await fetchAndHandleErrors( - await getApiUrl("orders_create"), - { - method: "POST", - body: JSON.stringify({ - side: "buy", - ...body, - }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${await getAuthToken()}`, - }, - }, - ); - if (!response.ok) { - return { - data: null, - err: await response.json(), - }; - } - - return { - data: await response.json(), - err: null, - }; -} diff --git a/src/api/quoting.ts b/src/api/quoting.ts deleted file mode 100644 index 4624885..0000000 --- a/src/api/quoting.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { type ApiError, objToQueryString } from "."; -import { getAuthToken } from "../helpers/config"; -import { fetchAndHandleErrors } from "../helpers/fetch"; -import type { Centicents } from "../helpers/units"; -import { getApiUrl } from "../helpers/urls"; -import type { Nullable } from "../types/empty"; - -export interface BuyOrderQuote { - object: "quote"; - side: "buy"; - price: Centicents; - instance_type: string; - quantity: number; - duration: number; - start_at: string; -} -export interface SellOrderQuote { - object: "quote"; - side: "sell"; - price: Centicents; - contract_id: string; - quantity: number; - duration: number; - start_at: string; -} -export type OrderQuote = BuyOrderQuote | SellOrderQuote; - -// -- quote buy order - -type QuoteBuyOrderOptions = { - instance_type: string; - quantity: number; - duration: number; - min_start_date: string; - max_start_date: string; -}; - -interface QuoteBuyOrderReturn { - data: Nullable; - err: Nullable; -} -export async function quoteBuyOrderRequest( - query: QuoteBuyOrderOptions, -): Promise { - const urlBase = await getApiUrl("quote_get"); - const queryParams = { - side: "buy", - instance_type: query.instance_type, - quantity: query.quantity, - duration: query.duration, - min_start_date: query.min_start_date, - max_start_date: query.max_start_date, - }; - const queryString = objToQueryString(queryParams); - const url = `${urlBase}?${queryString}`; - - const response = await fetchAndHandleErrors(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${await getAuthToken()}`, - }, - }); - if (!response.ok) { - return { - data: null, - err: await response.json(), - }; - } - - return { - data: await response.json(), - err: null, - }; -} diff --git a/src/api/types.ts b/src/api/types.ts deleted file mode 100644 index 7dc0eb0..0000000 --- a/src/api/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ListResponseBody { - data: T[]; - object: "list"; -} diff --git a/src/apiClient.ts b/src/apiClient.ts index edfb2a4..f35269b 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -1,14 +1,22 @@ -import createClient from "openapi-fetch"; +import createClient, { type Client } from "openapi-fetch"; import { getAuthToken, loadConfig } from "./helpers/config"; import type { paths } from "./schema"; // generated by openapi-typescript +let __client: Client | undefined; + export const apiClient = async () => { - const config = await loadConfig(); + if (__client) { + return __client; + } - return createClient({ + const config = await loadConfig(); + __client = createClient({ baseUrl: config.api_url, headers: { Authorization: `Bearer ${await getAuthToken()}`, + "Content-Type": "application/json", }, }); + + return __client; }; diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 00dafbf..b8fc809 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,24 +1,24 @@ import { getCommandBase } from "./command"; import { clearAuthFromConfig } from "./config"; -export function logAndQuit(message: string) { +export function logAndQuit(message: string): never { console.error(message); process.exit(1); } -export function logLoginMessageAndQuit() { +export function logLoginMessageAndQuit(): never { const base = getCommandBase(); const loginCommand = `${base} login`; logAndQuit(`You need to login first.\n\n\t$ ${loginCommand}\n`); } -export async function logSessionTokenExpiredAndQuit() { +export async function logSessionTokenExpiredAndQuit(): Promise { await clearAuthFromConfig(); logAndQuit("\nYour session has expired. Please login again."); } -export function failedToConnect() { +export function failedToConnect(): never { logAndQuit( "Failed to connect to the server. Please check your internet connection and try again.", ); diff --git a/src/helpers/units.ts b/src/helpers/units.ts index 1080858..610209e 100644 --- a/src/helpers/units.ts +++ b/src/helpers/units.ts @@ -4,7 +4,8 @@ import type { Nullable } from "../types/empty"; export type Epoch = number; -const MILLS_PER_EPOCH = 1000 * 60; +const MILLS_PER_EPOCH = 1000 * 60; // 1 minute +const EPOCHS_PER_HOUR = (3600 * 1000) / MILLS_PER_EPOCH; export function currentEpoch(): Epoch { return Math.floor(Date.now() / MILLS_PER_EPOCH); @@ -14,6 +15,27 @@ export function epochToDate(epoch: Epoch): Date { return new Date(epoch * MILLS_PER_EPOCH); } +export function roundStartDate(startDate: Date): Date { + const now = currentEpoch(); + const startEpoch = dateToEpoch(startDate); + if (startEpoch <= now + 1) { + return epochToDate(now + 1); + } else { + return epochToDate(roundEpochUpToHour(startEpoch)); + } +} + +export function roundEndDate(endDate: Date): Date { + return epochToDate(roundEpochUpToHour(dateToEpoch(endDate))); +} + +function dateToEpoch(date: Date): number { + return Math.ceil(date.getTime() / MILLS_PER_EPOCH); +} +function roundEpochUpToHour(epoch: number): number { + return Math.ceil(epoch / EPOCHS_PER_HOUR) * EPOCHS_PER_HOUR; +} + // -- currency export type Cents = number; diff --git a/src/lib/balance.ts b/src/lib/balance.ts index 4bf360c..eee9ad1 100644 --- a/src/lib/balance.ts +++ b/src/lib/balance.ts @@ -1,15 +1,14 @@ import chalk from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; -import { isLoggedIn, loadConfig } from "../helpers/config"; +import { apiClient } from "../apiClient"; +import { isLoggedIn } from "../helpers/config"; import { logAndQuit, logLoginMessageAndQuit, logSessionTokenExpiredAndQuit, } from "../helpers/errors"; -import { fetchAndHandleErrors } from "../helpers/fetch"; import type { Centicents } from "../helpers/units"; -import { getApiUrl } from "../helpers/urls"; const usdFormatter = new Intl.NumberFormat("en-US", { style: "currency", @@ -72,10 +71,11 @@ export function registerBalance(program: Command) { }); } -async function getBalance(): Promise<{ +export type BalanceUsdCenticents = { available: { centicents: Centicents; whole: number }; reserved: { centicents: Centicents; whole: number }; -}> { +}; +async function getBalance(): Promise { const loggedIn = await isLoggedIn(); if (!loggedIn) { logLoginMessageAndQuit(); @@ -85,44 +85,53 @@ async function getBalance(): Promise<{ reserved: { centicents: 0, whole: 0 }, }; } - const config = await loadConfig(); + const client = await apiClient(); - const response = await fetchAndHandleErrors(await getApiUrl("balance_get"), { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, - }, - }); + const { data, error, response } = await client.GET("/v0/balance"); if (!response.ok) { - if (response.status === 401) { - logSessionTokenExpiredAndQuit(); - - return { - available: { centicents: 0, whole: 0 }, - reserved: { centicents: 0, whole: 0 }, - }; + switch (response.status) { + case 401: + return await logSessionTokenExpiredAndQuit(); + case 500: + return logAndQuit(`Failed to get balance: ${error?.message}`); + default: + return logAndQuit(`Failed to get balance: ${response.statusText}`); } + } - logAndQuit(`Failed to fetch balance: ${response.statusText}`); + if (!data) { + return logAndQuit( + `Failed to get balance: Unexpected response from server: ${response}`, + ); + } - return { - available: { centicents: 0, whole: 0 }, - reserved: { centicents: 0, whole: 0 }, - }; + let available: number; + switch (data.available.currency) { + case "usd": + available = data.available.amount / 10_000; + break; + default: + logAndQuit(`Unsupported currency: ${data.available.currency}`); } - const data = await response.json(); + let reserved: number; + switch (data.reserved.currency) { + case "usd": + reserved = data.reserved.amount / 10_000; + break; + default: + logAndQuit(`Unsupported currency: ${data.reserved.currency}`); + } return { available: { - centicents: data.available.amount, - whole: data.available.amount / 10_000, + centicents: available, + whole: available / 10_000, }, reserved: { - centicents: data.reserved.amount, - whole: data.reserved.amount / 10_000, + centicents: reserved, + whole: reserved / 10_000, }, }; } diff --git a/src/lib/buy.ts b/src/lib/buy.ts index bc116b4..558d177 100644 --- a/src/lib/buy.ts +++ b/src/lib/buy.ts @@ -6,16 +6,19 @@ import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; import parseDuration from "parse-duration"; -import { ApiErrorCode } from "../api"; -import { OrderStatus, placeBuyOrderRequest } from "../api/orders"; -import { quoteBuyOrderRequest } from "../api/quoting"; import { apiClient } from "../apiClient"; import { isLoggedIn } from "../helpers/config"; -import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; +import { + logAndQuit, + logLoginMessageAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors"; import { type Centicents, centicentsToDollarsFormatted, priceWholeToCenticents, + roundEndDate, + roundStartDate, } from "../helpers/units"; import type { Nullable } from "../types/empty"; import { formatDuration } from "./orders"; @@ -47,8 +50,6 @@ export function registerBuy(program: Command) { .action(buyOrderAction); } -// -- - async function buyOrderAction(options: SfBuyOptions) { const loggedIn = await isLoggedIn(); if (!loggedIn) { @@ -56,198 +57,204 @@ async function buyOrderAction(options: SfBuyOptions) { } // normalize inputs - const optionsNormalized = normalizeSfBuyOptions(options); - - if (options.quote) { - await quoteBuyOrderAction(optionsNormalized); - } else { - await placeBuyOrderAction(optionsNormalized); - } -} - -async function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -async function getOrder(orderId: string) { - const api = await apiClient(); + const isQuoteOnly = options.quote ?? false; - const { data: order } = await api.GET("/v0/orders/{id}", { - params: { path: { id: orderId } }, - }); - return order; -} + // parse duration + let durationSeconds = parseDuration(options.duration, "s"); + if (!durationSeconds) { + return logAndQuit(`Invalid duration: ${options.duration}`); + } -async function tryToGetOrder(orderId: string) { - for (let i = 0; i < 10; i++) { - const order = await getOrder(orderId); - if (order) { - return order; + // parse price + let priceCenticents: Nullable = null; + if (options.price) { + const { centicents: priceParsed, invalid: priceInputInvalid } = + priceWholeToCenticents(options.price); + if (priceInputInvalid) { + return logAndQuit(`Invalid price: ${options.price}`); } - await sleep(50); + priceCenticents = priceParsed; } - return undefined; -} - -// -- + const yesFlagOmitted = options.yes === undefined || options.yes === null; + const confirmWithUser = yesFlagOmitted || !options.yes; -async function placeBuyOrderAction(options: SfBuyParamsNormalized) { - if (!options.priceCenticents) { - const { data: quote, err } = await quoteBuyOrderRequest({ - instance_type: options.instanceType, - quantity: options.totalNodes, - duration: options.durationSeconds, - min_start_date: options.startsAt.iso, - max_start_date: options.startsAt.iso, - }); + // parse starts at + let startDate = options.start ? chrono.parseDate(options.start) : new Date(); + if (!startDate) { + return logAndQuit("Invalid start date"); + } - if (err?.code === ApiErrorCode.Quotes.NoAvailability) { - const durationInHours = options.durationSeconds / 3600; - const quantity = options.totalNodes; + // default to 1 node if not specified + const quantity = options.nodes ? Number(options.nodes) : 1; - // In the future, we should read from a price chart of yesterday's prices. - const todoEstimatedPriceInCents = 250 * 8; // 8 gpus - const estimatedPrice = - todoEstimatedPriceInCents * quantity * durationInHours; - const estimatedPriceInDollars = estimatedPrice; + if (options.quote) { + const quote = await getQuote({ + instanceType: options.type, + priceCenticents, + quantity: quantity, + startsAt: startDate, + durationSeconds, + confirmWithUser, + quoteOnly: isQuoteOnly, + }); - console.log(`No one is selling this right now. To ask someone to sell it to you, add a price you're willing to pay. For example: + if (!quote) { + return logAndQuit("Not enough data exists to quote this order."); + } - sf buy -i ${options.instanceType} -d "${durationInHours}h" -n ${quantity} -p "$${(estimatedPriceInDollars / 100).toFixed(2)}" - `); + const priceLabelUsd = c.green(centicentsToDollarsFormatted(quote.price)); - return process.exit(1); - } + console.log(`This order is projected to cost ${priceLabelUsd}`); + } else { + // quote if no price was provided + if (!priceCenticents) { + const quote = await getQuote({ + instanceType: options.type, + priceCenticents, + quantity: quantity, + startsAt: startDate, + durationSeconds, + confirmWithUser, + quoteOnly: isQuoteOnly, + }); + + if (!quote) { + const durationInHours = durationSeconds / 3600; + + // In the future, we should read from a price chart of yesterday's prices. + // For now, we'll just suggest 2.50 / gpu-hour as a default + const todoEstimatedPricePerNodeCents = 250 * 8; // $2.50 / gpu-hour * 8 gpus + const estimatedPriceInCents = + todoEstimatedPricePerNodeCents * quantity * durationInHours; // multiply by desired quantity and duration to get total estimated price in cents + + console.log(`No one is selling this right now. To ask someone to sell it to you, add a price you're willing to pay. For example: + + sf buy -i ${options.type} -d "${durationInHours}h" -n ${quantity} -p "$${(estimatedPriceInCents / 100).toFixed(2)}" + `); + + return process.exit(1); + } - if (err) { - return logAndQuit(`Failed to get quote: ${err.message}`); + priceCenticents = quote.price; + durationSeconds = quote.duration; + startDate = new Date(quote.start_at); } - if (!quote) { - return logAndQuit("Failed to get quote: No quote data received"); + // round the start and end dates. If we came from a quote, they should already be rounded, + // however, there may have been a delay between the quote and now, so we may need to move the start time up to the next minute + startDate = roundStartDate(startDate); + let endDate = dayjs(startDate).add(durationSeconds, "s").toDate(); + endDate = roundEndDate(endDate); + + if (confirmWithUser) { + const confirmationMessage = confirmPlaceOrderMessage({ + instanceType: options.type, + priceCenticents, + quantity, + startsAt: startDate, + endsAt: endDate, + confirmWithUser, + quoteOnly: isQuoteOnly, + }); + const confirmed = await confirm({ + message: confirmationMessage, + default: false, + }); + + if (!confirmed) { + logAndQuit("Order cancelled"); + } } - options.priceCenticents = quote.price; - } - - if (options.confirmWithUser) { - const confirmationMessage = confirmPlaceOrderMessage(options); - const confirmed = await confirm({ - message: confirmationMessage, - default: false, + const res = await placeBuyOrder({ + instanceType: options.type, + priceCenticents, + quantity, + // round start date again because the user might have taken a long time to confirm + // most of the time this will do nothing, but when it does it will move the start date forwrd one minute + startsAt: roundStartDate(startDate), + endsAt: endDate, + confirmWithUser, + quoteOnly: isQuoteOnly, }); - if (!confirmed) { - logAndQuit("Order cancelled"); - } - } + switch (res.status) { + case "pending": { + const orderId = res.id; + const printOrderNumber = (status: string) => + console.log(`\n${c.dim(`${orderId}\n\n`)}`); - // If the user is requesting the order to be placed immediately, - // we need to set the start time to the current time - // to avoid causing any delay from quoting - if (!options.startsAt.wasSetByUser) { - options.startsAt = { - iso: new Date().toISOString(), - date: new Date(), - wasSetByUser: false - }; - } - const { data: pendingOrder, err } = await placeBuyOrderRequest({ - instance_type: options.instanceType, - quantity: options.totalNodes, - duration: options.durationSeconds, - start_at: options.startsAt.iso, - price: options.priceCenticents, - }); - if (err) { - return logAndQuit(`Failed to place order: ${err.message}`); - } - - if (pendingOrder && pendingOrder.status === OrderStatus.Pending) { - const orderId = pendingOrder.id; - const printOrderNumber = (status: string) => - console.log(`\n${c.dim(`${orderId}\n\n`)}`); - - const order = await tryToGetOrder(orderId); + const order = await tryToGetOrder(orderId); - if (!order) { - console.log(`\n${c.dim(`Order ${orderId} is pending`)}`); - return; - } - printOrderNumber(order.status); + if (!order) { + console.log(`\n${c.dim(`Order ${orderId} is pending`)}`); + return; + } + printOrderNumber(order.status); - if (order.status === "filled") { - const now = new Date(); - const startAt = new Date(order.start_at); - const timeDiff = startAt.getTime() - now.getTime(); - const oneMinuteInMs = 60 * 1000; + if (order.status === "filled") { + const now = new Date(); + const startAt = new Date(order.start_at); + const timeDiff = startAt.getTime() - now.getTime(); + const oneMinuteInMs = 60 * 1000; - if (now >= startAt || timeDiff <= oneMinuteInMs) { - console.log(`Your nodes are currently spinning up. Once they're online, you can view them using: + if (now >= startAt || timeDiff <= oneMinuteInMs) { + console.log(`Your nodes are currently spinning up. Once they're online, you can view them using: - sf instances ls + sf instances ls -`); - } else { - const contractStartTime = dayjs(startAt); - const timeFromNow = contractStartTime.fromNow(); - console.log(`Your contract begins ${c.green(timeFromNow)}. You can view more details using: + `); + } else { + const contractStartTime = dayjs(startAt); + const timeFromNow = contractStartTime.fromNow(); + console.log(`Your contract begins ${c.green(timeFromNow)}. You can view more details using: - sf contracts ls + sf contracts ls -`); - } + `); + } - return; - } else { - console.log(`Your order wasn't accepted yet. You can check it's status with: + return; + } else { + console.log(`Your order wasn't accepted yet. You can check it's status with: - sf orders ls + sf orders ls -If you want to cancel the order, you can do so with: + If you want to cancel the order, you can do so with: - sf orders cancel ${orderId} + sf orders cancel ${orderId} - `); + `); - return; + return; + } + } + default: + return logAndQuit( + `Failed to place order: Unexpected order status: ${res.status}`, + ); } } } -function actualDuration(options: SfBuyParamsNormalized): number { - const now = new Date(); - const startAt = new Date(options.startsAt.iso); - const requestedDuration = options.durationSeconds; - - // If start time is in the future, return the requested duration - if (startAt > now) { - return requestedDuration; - } - - // Calculate the time to the next hour - const nextHour = new Date(now); - nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0); - const timeToNextHour = Math.ceil((nextHour.getTime() - now.getTime()) / 1000); - - // Return the sum of time to next hour and requested duration - return timeToNextHour + requestedDuration; -} - -function confirmPlaceOrderMessage(options: SfBuyParamsNormalized) { +function confirmPlaceOrderMessage(options: BuyOptions) { if (!options.priceCenticents) { return ""; } - const totalNodesLabel = c.green(options.totalNodes); + const totalNodesLabel = c.green(options.quantity); const instanceTypeLabel = c.green(options.instanceType); - const nodesLabel = options.totalNodes > 1 ? "nodes" : "node"; - const durationHumanReadable = formatDuration(actualDuration(options) * 1000); + const nodesLabel = options.quantity > 1 ? "nodes" : "node"; + + const durationHumanReadable = formatDuration( + options.endsAt.getTime() - options.startsAt.getTime(), + ); const endsAtLabel = c.green( - dayjs(options.endsAt.iso).format("MM/DD/YYYY hh:mm A"), + dayjs(options.endsAt).format("MM/DD/YYYY hh:mm A"), ); - const fromNowTime = dayjs(options.startsAt.iso).fromNow(); + const fromNowTime = dayjs(options.startsAt).fromNow(); let timeDescription: string; if ( @@ -257,7 +264,7 @@ function confirmPlaceOrderMessage(options: SfBuyParamsNormalized) { timeDescription = `from ${c.green("now")} until ${endsAtLabel}`; } else { const startAtLabel = c.green( - dayjs(options.startsAt.iso).format("MM/DD/YYYY hh:mm A"), + dayjs(options.startsAt).format("MM/DD/YYYY hh:mm A"), ); timeDescription = `from ${startAtLabel} (${c.green(fromNowTime)}) until ${endsAtLabel}`; } @@ -273,99 +280,119 @@ function confirmPlaceOrderMessage(options: SfBuyParamsNormalized) { return `${topLine}\n${priceLine} `; } -// -- - -async function quoteBuyOrderAction(options: SfBuyParamsNormalized) { - const { data: quote, err } = await quoteBuyOrderRequest({ - instance_type: options.instanceType, - quantity: options.totalNodes, - duration: options.durationSeconds, - min_start_date: options.startsAt.iso, - max_start_date: options.startsAt.iso, +type BuyOptions = { + instanceType: string; + priceCenticents: number; + quantity: number; + startsAt: Date; + endsAt: Date; + confirmWithUser: boolean; + quoteOnly: boolean; +}; +export async function placeBuyOrder(options: BuyOptions) { + const api = await apiClient(); + const { data, error, response } = await api.POST("/v0/orders", { + body: { + side: "buy", + instance_type: options.instanceType, + quantity: options.quantity, + // round start date again because the user might take a long time to confirm + start_at: roundStartDate(options.startsAt).toISOString(), + end_at: options.endsAt.toISOString(), + price: options.priceCenticents, + }, }); - if (err) { - if (err.code === ApiErrorCode.Quotes.NoAvailability) { - return logAndQuit("Not enough data exists to quote this order."); - } - return logAndQuit(`Failed to quote order: ${err.message}`); + if (!response.ok) { + switch (response.status) { + case 400: + return logAndQuit(`Bad Request: ${error?.message}`); + case 401: + return await logSessionTokenExpiredAndQuit(); + case 500: + return logAndQuit(`Failed to place order: ${error?.message}`); + default: + return logAndQuit(`Failed to place order: ${response.statusText}`); + } } - if (quote) { - const priceLabelUsd = c.green(centicentsToDollarsFormatted(quote.price)); - - console.log(`This order is projected to cost ${priceLabelUsd}`); + if (!data) { + return logAndQuit( + `Failed to place order: Unexpected response from server: ${response}`, + ); } -} -// -- + return data; +} -interface SfBuyParamsNormalized { +type QuoteOptions = { instanceType: string; - totalNodes: number; - durationSeconds: number; priceCenticents: Nullable; - startsAt: { - iso: string; - date: Date; - wasSetByUser: boolean; - }; - endsAt: { - iso: string; - date: Date; - }; + quantity: number; + startsAt: Date; + durationSeconds: number; confirmWithUser: boolean; quoteOnly: boolean; -} -function normalizeSfBuyOptions(options: SfBuyOptions): SfBuyParamsNormalized { - const isQuoteOnly = options.quote ?? false; +}; +async function getQuote(options: QuoteOptions) { + const api = await apiClient(); - // parse duration - const durationSeconds = parseDuration(options.duration, "s"); - if (!durationSeconds) { - logAndQuit(`Invalid duration: ${options.duration}`); - process.exit(1); // make typescript happy - } + const { data, error, response } = await api.GET("/v0/quote", { + params: { + query: { + side: "buy", + instance_type: options.instanceType, + quantity: options.quantity, + duration: options.durationSeconds, + min_start_date: options.startsAt.toISOString(), + max_start_date: options.startsAt.toISOString(), + }, + }, + }); - // parse price - let priceCenticents: Nullable = null; - if (options.price) { - const { centicents: priceParsed, invalid: priceInputInvalid } = - priceWholeToCenticents(options.price); - if (priceInputInvalid) { - logAndQuit(`Invalid price: ${options.price}`); - process.exit(1); + if (!response.ok) { + switch (response.status) { + case 400: + return logAndQuit(`Bad Request: ${error?.message}`); + case 401: + return await logSessionTokenExpiredAndQuit(); + case 500: + return logAndQuit(`Failed to get quote: ${error?.code}`); + default: + return logAndQuit(`Failed to get quote: ${response.statusText}`); } - priceCenticents = priceParsed; } - // parse starts at - const startDate = options.start - ? chrono.parseDate(options.start) - : new Date(); - if (!startDate) { - logAndQuit("Invalid start date"); - process.exit(1); + if (!data) { + return logAndQuit( + `Failed to get quote: Unexpected response from server: ${response}`, + ); } - const yesFlagOmitted = options.yes === undefined || options.yes === null; - const confirmWithUser = yesFlagOmitted || !options.yes; + return data.quote; +} - return { - instanceType: options.type, - totalNodes: options.nodes ? Number(options.nodes) : 1, - durationSeconds, - priceCenticents, - startsAt: { - iso: startDate.toISOString(), - date: startDate, - wasSetByUser: options.start !== undefined, - }, - endsAt: { - iso: dayjs(startDate).add(durationSeconds, "s").toISOString(), - date: dayjs(startDate).add(durationSeconds, "s").toDate(), - }, - confirmWithUser: confirmWithUser, - quoteOnly: isQuoteOnly, - }; +async function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function getOrder(orderId: string) { + const api = await apiClient(); + + const { data: order } = await api.GET("/v0/orders/{id}", { + params: { path: { id: orderId } }, + }); + return order; +} + +async function tryToGetOrder(orderId: string) { + for (let i = 0; i < 10; i++) { + const order = await getOrder(orderId); + if (order) { + return order; + } + await sleep(50); + } + + return undefined; } diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts index e9504dc..7bf4ceb 100644 --- a/src/lib/contracts.ts +++ b/src/lib/contracts.ts @@ -1,9 +1,12 @@ import Table from "cli-table3"; import { Command } from "commander"; -import { getAuthToken, isLoggedIn } from "../helpers/config"; -import { logLoginMessageAndQuit } from "../helpers/errors"; -import { fetchAndHandleErrors } from "../helpers/fetch"; -import { getApiUrl } from "../helpers/urls"; +import { apiClient } from "../apiClient"; +import { isLoggedIn } from "../helpers/config"; +import { + logAndQuit, + logLoginMessageAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors"; interface Contract { object: string; @@ -17,10 +20,19 @@ interface Contract { quantities: number[]; }; colocate_with: string[]; - cluster_id: string; + cluster_id?: string; } function printTable(data: Contract[]) { + if (data.length === 0) { + const table = new Table(); + table.push([ + { colSpan: 6, content: "No contracts found", hAlign: "center" }, + ]); + + console.log(table.toString()); + } + for (const contract of data) { // print the contract shape in a table // if the contract is empty, will print empty shape table @@ -68,30 +80,51 @@ export function registerContracts(program: Command) { console.log(await listContracts()); } else { const data = await listContracts(); - printTable(data.data); + printTable(data); } process.exit(0); }), ); } -async function listContracts() { +async function listContracts(): Promise { const loggedIn = await isLoggedIn(); if (!loggedIn) { return logLoginMessageAndQuit(); } - const response = await fetchAndHandleErrors( - await getApiUrl("contracts_list"), - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${await getAuthToken()}`, - }, - }, - ); - - const data = await response.json(); - return data; + const api = await apiClient(); + + const { data, error, response } = await api.GET("/v0/contracts"); + + if (!response.ok) { + switch (response.status) { + case 400: + return logAndQuit(`Bad Request: ${error?.message}`); + case 401: + return await logSessionTokenExpiredAndQuit(); + default: + return logAndQuit(`Failed to get contracts: ${response.statusText}`); + } + } + + if (!data) { + return logAndQuit( + `Failed to get contracts: Unexpected response from server: ${response}`, + ); + } + + // filter out pending contracts + // we use loop instead of filter bc type + const contracts: Contract[] = []; + for (const contract of data.data) { + if (contract.status === "active") { + contracts.push({ + ...contract, + colocate_with: contract.colocate_with ?? [], + }); + } + } + + return contracts; } diff --git a/src/lib/dev.ts b/src/lib/dev.ts index 69b094e..4a0c8aa 100644 --- a/src/lib/dev.ts +++ b/src/lib/dev.ts @@ -1,5 +1,8 @@ import { confirm } from "@inquirer/prompts"; +import chalk from "chalk"; import type { Command } from "commander"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; import { deleteConfig, getConfigPath, @@ -11,11 +14,8 @@ import { logLoginMessageAndQuit, logSessionTokenExpiredAndQuit, } from "../helpers/errors"; -import { getApiUrl } from "../helpers/urls"; import { currentEpoch, epochToDate } from "../helpers/units"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import chalk from "chalk"; +import { getApiUrl } from "../helpers/urls"; dayjs.extend(utc); diff --git a/src/lib/instances.ts b/src/lib/instances.ts index cde0b7b..6b24fde 100644 --- a/src/lib/instances.ts +++ b/src/lib/instances.ts @@ -1,9 +1,13 @@ import chalk, { type ChalkInstance } from "chalk"; import Table from "cli-table3"; import type { Command } from "commander"; -import { getAuthToken, isLoggedIn } from "../helpers/config"; -import { logLoginMessageAndQuit } from "../helpers/errors"; -import { getApiUrl } from "../helpers/urls"; +import { apiClient } from "../apiClient"; +import { isLoggedIn } from "../helpers/config"; +import { + logAndQuit, + logLoginMessageAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors"; export function registerInstances(program: Command) { const instances = program @@ -150,28 +154,38 @@ const colorInstanceType = (instanceType: InstanceType) => async function getInstances({ clusterId, -}: { clusterId?: string }): Promise> { +}: { clusterId?: string }): Promise { const loggedIn = await isLoggedIn(); if (!loggedIn) { logLoginMessageAndQuit(); } - let url = await getApiUrl("instances_list"); - if (clusterId) { - url += `?cluster_id=${clusterId}`; - } - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${await getAuthToken()}`, - }, - }); + const api = await apiClient(); + + const { data, error, response } = await api.GET("/v0/instances"); if (!response.ok) { - throw new Error(`Failed to fetch instances: ${response.statusText}`); + switch (response.status) { + case 400: + return logAndQuit(`Bad Request: ${error?.message}`); + case 401: + return await logSessionTokenExpiredAndQuit(); + default: + return logAndQuit(`Failed to get instances: ${response.statusText}`); + } + } + + if (!data) { + return logAndQuit( + `Failed to get instances: Unexpected response from server: ${response}`, + ); } - const responseData: ListResponseBody = await response.json(); - return responseData.data; + return data.data.map((instance) => ({ + object: instance.object, + id: instance.id, + type: instance.type as InstanceType, + ip: instance.ip, + status: instance.status, + })); } diff --git a/src/lib/orders.ts b/src/lib/orders.ts index 70a8ca6..b379765 100644 --- a/src/lib/orders.ts +++ b/src/lib/orders.ts @@ -3,9 +3,6 @@ import type { Command } from "commander"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; -import { type ApiError, ApiErrorCode } from "../api"; -import type { HydratedOrder } from "../api/orders"; -import type { ListResponseBody } from "../api/types"; import { getAuthToken, isLoggedIn } from "../helpers/config"; import { logAndQuit, @@ -47,12 +44,45 @@ export function formatDuration(ms: number) { return result || "0ms"; } +export type OrderType = "buy" | "sell"; +export enum OrderStatus { + Pending = "pending", + Rejected = "rejected", + Open = "open", + Cancelled = "cancelled", + Filled = "filled", + Expired = "expired", +} +export interface OrderFlags { + market: boolean; + post_only: boolean; + ioc: boolean; + prorate: boolean; +} + +export interface HydratedOrder { + object: "order"; + id: string; + side: OrderType; + instance_type: string; + price: number; + start_at: string; + end_at: string; + quantity: number; + flags: OrderFlags; + created_at: string; + executed: boolean; + execution_price?: number; + cancelled: boolean; + status: OrderStatus; +} + export type PlaceSellOrderParameters = { side: "sell"; quantity: number; price: number; - duration: number; start_at: string; + end_at: string; contract_id: string; }; @@ -65,7 +95,13 @@ export type PlaceOrderParameters = { start_at: string; }; +interface ListResponseBody { + data: T[]; + object: "list"; +} + function printAsTable(orders: Array) { + orders.sort((a, b) => a.start_at.localeCompare(b.start_at)); const table = new Table({ head: [ "ID", @@ -96,13 +132,16 @@ function printAsTable(orders: Array) { } const startDate = new Date(order.start_at); + const duration = formatDuration( + dayjs(order.end_at).diff(dayjs(startDate), "ms"), + ); table.push([ order.id, order.side, order.instance_type, usdFormatter.format(order.price / 10000), order.quantity.toString(), - formatDuration(order.duration * 1000), + duration, startDate.toLocaleString(), status, executionPrice ? usdFormatter.format(executionPrice / 10000) : "-", @@ -191,7 +230,7 @@ export async function getOrders(props: { export async function submitOrderCancellationByIdAction( orderId: string, -): Promise { +): Promise { const loggedIn = await isLoggedIn(); if (!loggedIn) { logLoginMessageAndQuit(); @@ -202,31 +241,31 @@ export async function submitOrderCancellationByIdAction( method: "DELETE", body: JSON.stringify({}), headers: { - "Content-Type": "application/json", + "Content-ype": "application/json", Authorization: `Bearer ${await getAuthToken()}`, }, }); if (!response.ok) { if (response.status === 401) { - await logSessionTokenExpiredAndQuit(); + return await logSessionTokenExpiredAndQuit(); } - const error = (await response.json()) as ApiError; - if (error.code === ApiErrorCode.Orders.NotFound) { - logAndQuit(`Order ${orderId} not found`); - } else if (error.code === ApiErrorCode.Orders.AlreadyCancelled) { - logAndQuit(`Order ${orderId} is already cancelled`); + const error = await response.json(); + switch (error.code) { + case "order.not_found": + return logAndQuit(`Order ${orderId} not found`); + case "order.already_cancelled": + return logAndQuit(`Order ${orderId} is already cancelled`); + default: + // TODO: handle more specific errors + return logAndQuit(`Failed to cancel order ${orderId}`); } - - // TODO: handle more specific errors - - logAndQuit(`Failed to cancel order ${orderId}`); } const resp = await response.json(); const cancellationSubmitted = resp.object === "pending"; if (!cancellationSubmitted) { - logAndQuit(`Failed to cancel order ${orderId}`); + return logAndQuit(`Failed to cancel order ${orderId}`); } // cancellation submitted successfully diff --git a/src/lib/sell.ts b/src/lib/sell.ts index 5cf5fd2..f7ad3d4 100644 --- a/src/lib/sell.ts +++ b/src/lib/sell.ts @@ -1,11 +1,19 @@ import * as chrono from "chrono-node"; import type { Command } from "commander"; +import dayjs from "dayjs"; import parseDuration from "parse-duration"; -import { getAuthToken, isLoggedIn } from "../helpers/config"; -import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; -import { fetchAndHandleErrors } from "../helpers/fetch"; -import { priceWholeToCenticents } from "../helpers/units"; -import { getApiUrl } from "../helpers/urls"; +import { apiClient } from "../apiClient"; +import { isLoggedIn } from "../helpers/config"; +import { + logAndQuit, + logLoginMessageAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors"; +import { + priceWholeToCenticents, + roundEndDate, + roundStartDate, +} from "../helpers/units"; import type { PlaceSellOrderParameters } from "./orders"; export function registerSell(program: Command) { @@ -58,13 +66,17 @@ async function placeSellOrder(options: { if (!durationSecs) { return logAndQuit("Invalid duration"); } - const startDate = options.start - ? chrono.parseDate(options.start) - : new Date(); + + let startDate = options.start ? chrono.parseDate(options.start) : new Date(); if (!startDate) { return logAndQuit("Invalid start date"); } + startDate = roundStartDate(startDate); + + let endDate = dayjs(startDate).add(durationSecs, "s").toDate(); + endDate = roundEndDate(endDate); + const { centicents: priceCenticents, invalid } = priceWholeToCenticents( options.price, ); @@ -77,27 +89,30 @@ async function placeSellOrder(options: { quantity: forceAsNumber(options.nodes), price: priceCenticents, contract_id: options.contractId, - duration: durationSecs, start_at: startDate.toISOString(), + end_at: endDate.toISOString(), ...flags, }; - const res = await postSellOrder(params); - if (!res.ok) { - return logAndQuit("Failed to place sell order"); + const api = await apiClient(); + const { data, error, response } = await api.POST("/v0/orders", { + body: params, + }); + + if (!response.ok) { + switch (response.status) { + case 400: + return logAndQuit( + `Bad Request: ${error?.message}: ${JSON.stringify(error?.details, null, 2)}`, + ); + // return logAndQuit(`Bad Request: ${error?.message}`); + case 401: + return await logSessionTokenExpiredAndQuit(); + default: + return logAndQuit(`Failed to place sell order: ${response.statusText}`); + } } - const data = await res.json(); + console.log(data); process.exit(0); } - -async function postSellOrder(params: PlaceSellOrderParameters) { - return await fetchAndHandleErrors(await getApiUrl("orders_create"), { - method: "POST", - body: JSON.stringify(params), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${await getAuthToken()}`, - }, - }); -} diff --git a/src/lib/ssh.ts b/src/lib/ssh.ts index 0a5cf46..def461e 100644 --- a/src/lib/ssh.ts +++ b/src/lib/ssh.ts @@ -1,7 +1,7 @@ import type { Command } from "commander"; -import { getAuthorizationHeader } from "../helpers/config"; -import { logAndQuit } from "../helpers/errors"; -import { getApiUrl } from "../helpers/urls"; +import { apiClient } from "../apiClient"; +import { isLoggedIn } from "../helpers/config"; +import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors"; function isPubkey(key: string): boolean { const pubKeyPattern = /^ssh-/; @@ -44,6 +44,11 @@ export function registerSSH(program: Command) { .argument("[name]", "The name of the node to SSH into"); cmd.action(async (name, options) => { + const loggedIn = await isLoggedIn(); + if (!loggedIn) { + logLoginMessageAndQuit(); + } + if (Object.keys(options).length === 0 && !name) { cmd.help(); return; @@ -57,7 +62,15 @@ export function registerSSH(program: Command) { } const key = await readFileOrKey(options.add); - await postSSHKeys(key, options.user); + + const api = await apiClient(); + await api.POST("/v0/credentials", { + body: { + pubkey: key, + username: options.user, + }, + }); + console.log("Added ssh key"); process.exit(0); @@ -66,46 +79,3 @@ export function registerSSH(program: Command) { cmd.help(); }); } - -export type SSHCredential = { - object: "ssh_credential"; - id: string; - pubkey: string; - username: string; -}; - -export type CredentialObject = SSHCredential; - -export type PostSSHCredentialBody = { - pubkey: string; - username: string; -}; - -export async function getSSHKeys() { - const res = await fetch(await getApiUrl("credentials_list"), { - headers: await getAuthorizationHeader(), - }); - - const data = await res.json(); - return data as SSHCredential[]; -} - -export async function postSSHKeys(key: string, username: string) { - const res = await fetch(await getApiUrl("credentials_create"), { - method: "POST", - headers: { - ...(await getAuthorizationHeader()), - "Content-Type": "application/json", - }, - body: JSON.stringify({ - pubkey: key, - username, - }), - }); - if (!res.ok) { - console.error(await res.text()); - throw new Error("Failed to add SSH key"); - } - const data = await res.json(); - return data as SSHCredential; -} diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts index bf76306..b412f3f 100644 --- a/src/lib/tokens.ts +++ b/src/lib/tokens.ts @@ -5,7 +5,6 @@ import Table from "cli-table3"; import type { Command } from "commander"; import dayjs from "dayjs"; import ora from "ora"; -import { type ApiError, ApiErrorCode } from "../api"; import { getCommandBase } from "../helpers/command"; import { getAuthToken, isLoggedIn } from "../helpers/config"; import { @@ -311,8 +310,8 @@ async function deleteTokenById(id: string) { await logSessionTokenExpiredAndQuit(); } - const error = (await response.json()) as ApiError; - if (error.code === ApiErrorCode.Tokens.TokenNotFound) { + const error = await response.json(); + if (error.code === "token.not_found") { loadingSpinner.fail("Token not found"); process.exit(1); } diff --git a/src/schema.ts b/src/schema.ts index 6c09b3b..aeaf5ff 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -4,94 +4,94 @@ */ export interface paths { - "/v0/orders": { + "/v0/prices": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Orders"]; + get: operations["getV0Prices"]; put?: never; - post: operations["postV0Orders"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/orders/{id}": { + "/v0/quote": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0OrdersById"]; + get: operations["getV0Quote"]; put?: never; post?: never; - delete: operations["deleteV0OrdersById"]; + delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/instances": { + "/v0/orders": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Instances"]; + get: operations["getV0Orders"]; put?: never; - post?: never; + post: operations["postV0Orders"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/instances/{id}": { + "/v0/orders/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0InstancesById"]; + get: operations["getV0OrdersById"]; put?: never; post?: never; - delete?: never; + delete: operations["deleteV0OrdersById"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/credentials": { + "/v0/instances": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Credentials"]; + get: operations["getV0Instances"]; put?: never; - post: operations["postV0Credentials"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/contracts": { + "/v0/instances/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Contracts"]; + get: operations["getV0InstancesById"]; put?: never; post?: never; delete?: never; @@ -100,30 +100,30 @@ export interface paths { patch?: never; trace?: never; }; - "/v0/contracts/{id}": { + "/v0/credentials": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0ContractsById"]; + get: operations["getV0Credentials"]; put?: never; - post?: never; + post: operations["postV0Credentials"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/v0/prices": { + "/v0/contracts": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Prices"]; + get: operations["getV0Contracts"]; put?: never; post?: never; delete?: never; @@ -132,14 +132,14 @@ export interface paths { patch?: never; trace?: never; }; - "/v0/balance": { + "/v0/contracts/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Balance"]; + get: operations["getV0ContractsById"]; put?: never; post?: never; delete?: never; @@ -148,14 +148,14 @@ export interface paths { patch?: never; trace?: never; }; - "/v0/quote": { + "/v0/balance": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["getV0Quote"]; + get: operations["getV0Balance"]; put?: never; post?: never; delete?: never; @@ -176,6 +176,338 @@ export interface components { } export type $defs = Record; export interface operations { + getV0Prices: { + parameters: { + query: { + /** @description The instance type. */ + instance_type: string; + /** @description The minimum quantity of nodes filled blocks included in the price calculation contain. */ + min_quantity?: number; + /** @description The maximum quantity of nodes filled blocks included in the price calculation contain. */ + max_quantity?: number; + /** @description The minimum duration, in seconds, of filled blocks. */ + min_duration?: number; + /** @description The maximum duration, in seconds, of filled blocks. */ + max_duration?: number; + /** @description The number of days to go back, starting from today. If you provide 0, you will only see prices for today. If you provide 1, you will see prices over all of yesterday, and today. */ + since_n_days_ago?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data: { + /** @constant */ + object: "price-history-item"; + gpu_hour?: { + /** @description The minimum price per GPU hour for the period (in centicents, 1/100th of a cent). */ + min: number; + /** @description The maximum price per GPU hour for the period (in centicents, 1/100th of a cent). */ + max: number; + /** @description The average price per GPU hour for the period (in centicents, 1/100th of a cent). */ + avg: number; + }; + /** @description ISO 8601 datetime marking the start of the period. */ + period_start: string; + /** @description ISO 8601 datetime marking the end of the period. */ + period_end: string; + /** @description Whether there was no price data for this period. */ + no_data: boolean; + }[]; + /** @constant */ + object: "list"; + }; + "multipart/form-data": { + data: { + /** @constant */ + object: "price-history-item"; + gpu_hour?: { + /** @description The minimum price per GPU hour for the period (in centicents, 1/100th of a cent). */ + min: number; + /** @description The maximum price per GPU hour for the period (in centicents, 1/100th of a cent). */ + max: number; + /** @description The average price per GPU hour for the period (in centicents, 1/100th of a cent). */ + avg: number; + }; + /** @description ISO 8601 datetime marking the start of the period. */ + period_start: string; + /** @description ISO 8601 datetime marking the end of the period. */ + period_end: string; + /** @description Whether there was no price data for this period. */ + no_data: boolean; + }[]; + /** @constant */ + object: "list"; + }; + "text/plain": { + data: { + /** @constant */ + object: "price-history-item"; + gpu_hour?: { + /** @description The minimum price per GPU hour for the period (in centicents, 1/100th of a cent). */ + min: number; + /** @description The maximum price per GPU hour for the period (in centicents, 1/100th of a cent). */ + max: number; + /** @description The average price per GPU hour for the period (in centicents, 1/100th of a cent). */ + avg: number; + }; + /** @description ISO 8601 datetime marking the start of the period. */ + period_start: string; + /** @description ISO 8601 datetime marking the end of the period. */ + period_end: string; + /** @description Whether there was no price data for this period. */ + no_data: boolean; + }[]; + /** @constant */ + object: "list"; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + }; + }; + }; + }; + getV0Quote: { + parameters: { + query: { + side: "buy" | "sell"; + /** @description Inclusive lower bound for the start time, as an ISO 8601 string. The query will consider all valid start times at or after this time. The difference between this and `max_start_time` can be at most 24 hours. */ + min_start_date: string; + /** @description Inclusive upper bound for the start time, as an ISO 8601 string. The query will consider all valid start times on or before this time. The difference between this and `min_start_time` can be at most 24 hours. */ + max_start_date: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The instance type. */ + instance_type?: string; + contract_id?: string; + }; + header?: { + /** @description Generate a bearer token with `$ sf tokens create`. */ + authorization?: string; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "buy"; + quote: { + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + /** @description The instance type. */ + instance_type: string; + } | null; + } + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "sell"; + quote: { + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + contract_id: string; + } | null; + }; + "multipart/form-data": + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "buy"; + quote: { + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + /** @description The instance type. */ + instance_type: string; + } | null; + } + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "sell"; + quote: { + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + contract_id: string; + } | null; + }; + "text/plain": + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "buy"; + quote: { + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + /** @description The instance type. */ + instance_type: string; + } | null; + } + | { + /** @constant */ + object: "quote"; + /** @constant */ + side: "sell"; + quote: { + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + price: number; + /** @description The number of nodes. */ + quantity: number; + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ + start_at: string; + /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ + duration: number; + contract_id: string; + } | null; + }; + }; + }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "internal_server"; + message: string; + details?: Record; + }; + }; + }; + }; + }; getV0Orders: { parameters: { query?: { @@ -189,7 +521,7 @@ export interface operations { min_quantity?: string; max_quantity?: string; side?: string; - include_public?: string | boolean; + include_public?: boolean; }; header?: { /** @description Generate a bearer token with `$ sf tokens create`. */ @@ -224,15 +556,15 @@ export interface operations { | { /** @constant */ side: "buy"; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags?: { /** @description If true, this will be a market order. */ @@ -248,13 +580,13 @@ export interface operations { /** @constant */ side: "sell"; contract_id: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags?: { /** @description If true, this will be a market order. */ @@ -269,15 +601,15 @@ export interface operations { | { /** @constant */ side: "buy"; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags?: { /** @description If true, this will be a market order. */ @@ -293,13 +625,13 @@ export interface operations { /** @constant */ side: "sell"; contract_id: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags?: { /** @description If true, this will be a market order. */ @@ -314,15 +646,15 @@ export interface operations { | { /** @constant */ side: "buy"; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags?: { /** @description If true, this will be a market order. */ @@ -338,13 +670,13 @@ export interface operations { /** @constant */ side: "sell"; contract_id: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags?: { /** @description If true, this will be a market order. */ @@ -386,6 +718,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -450,15 +813,15 @@ export interface operations { | "cancelled" | "filled" | "expired"; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags: { /** @description If true, this will be a market order. */ @@ -488,15 +851,15 @@ export interface operations { | "cancelled" | "filled" | "expired"; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags: { /** @description If true, this will be a market order. */ @@ -526,15 +889,15 @@ export interface operations { | "cancelled" | "filled" | "expired"; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ + /** @description The number of nodes. */ quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ + /** @description The start time, as an ISO 8601 string. Start times must be either "right now" or on the hour. Order start times must be in the future, and can be either the next minute from now or on the hour. For example, if it's 16:00, valid start times include 16:01, 17:00, and 18:00, but not 16:30. Dates are always rounded up to the nearest minute. */ start_at: string; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ + /** @description The end time, as an ISO 8601 string. End times must be on the hour, i.e. 16:00, 17:00, 18:00, etc. 17:30, 17:01, etc are not valid end times. Dates are always rounded up to the nearest minute. */ + end_at: string; + /** @description Price in Centicents (1/100th of a cent, One Centicent = $0.0001) */ price: number; flags: { /** @description If true, this will be a market order. */ @@ -554,6 +917,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -620,6 +1014,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -711,6 +1136,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -792,6 +1248,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -877,6 +1364,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -965,6 +1483,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -1029,7 +1578,7 @@ export interface operations { id: string; /** Format: date-time */ created_at: string; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0. */ shape: { @@ -1060,7 +1609,7 @@ export interface operations { id: string; /** Format: date-time */ created_at: string; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0. */ shape: { @@ -1091,7 +1640,7 @@ export interface operations { id: string; /** Format: date-time */ created_at: string; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0. */ shape: { @@ -1114,6 +1663,37 @@ export interface operations { }; }; }; + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "multipart/form-data": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + "text/plain": { + /** @constant */ + object: "error"; + /** @constant */ + code: "not_authenticated"; + message: string; + details?: Record; + }; + }; + }; 500: { headers: { [name: string]: unknown; @@ -1175,7 +1755,7 @@ export interface operations { id: string; /** Format: date-time */ created_at: string; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0. */ shape: { @@ -1201,7 +1781,7 @@ export interface operations { id: string; /** Format: date-time */ created_at: string; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0. */ shape: { @@ -1227,7 +1807,7 @@ export interface operations { id: string; /** Format: date-time */ created_at: string; - /** @description The instance type of the order */ + /** @description The instance type. */ instance_type: string; /** @description A shape that describes the distribution of the contract's size over time. Must end with a quantity of 0. */ shape: { @@ -1246,7 +1826,7 @@ export interface operations { }; }; }; - 500: { + 401: { headers: { [name: string]: unknown; }; @@ -1255,7 +1835,7 @@ export interface operations { /** @constant */ object: "error"; /** @constant */ - code: "internal_server"; + code: "not_authenticated"; message: string; details?: Record; }; @@ -1263,7 +1843,7 @@ export interface operations { /** @constant */ object: "error"; /** @constant */ - code: "internal_server"; + code: "not_authenticated"; message: string; details?: Record; }; @@ -1271,65 +1851,12 @@ export interface operations { /** @constant */ object: "error"; /** @constant */ - code: "internal_server"; + code: "not_authenticated"; message: string; details?: Record; }; }; }; - }; - }; - getV0Prices: { - parameters: { - query: { - instance_type: string; - quantity?: string | number; - duration?: string | number; - since?: string | number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - data: { - expected: number; - min: number; - max: number; - timestamp: string; - }[]; - /** @constant */ - object: "list"; - }; - "multipart/form-data": { - data: { - expected: number; - min: number; - max: number; - timestamp: string; - }[]; - /** @constant */ - object: "list"; - }; - "text/plain": { - data: { - expected: number; - min: number; - max: number; - timestamp: string; - }[]; - /** @constant */ - object: "list"; - }; - }; - }; 500: { headers: { [name: string]: unknown; @@ -1430,7 +1957,7 @@ export interface operations { }; }; }; - 500: { + 401: { headers: { [name: string]: unknown; }; @@ -1439,7 +1966,7 @@ export interface operations { /** @constant */ object: "error"; /** @constant */ - code: "internal_server"; + code: "not_authenticated"; message: string; details?: Record; }; @@ -1447,7 +1974,7 @@ export interface operations { /** @constant */ object: "error"; /** @constant */ - code: "internal_server"; + code: "not_authenticated"; message: string; details?: Record; }; @@ -1455,142 +1982,12 @@ export interface operations { /** @constant */ object: "error"; /** @constant */ - code: "internal_server"; + code: "not_authenticated"; message: string; details?: Record; }; }; }; - }; - }; - getV0Quote: { - parameters: { - query: { - side: "buy" | "sell"; - /** @description Inclusive lower bound for the start time, as an ISO 8601 string. The query will consider all valid start times at or after this time. The difference between this and `max_start_time` can be at most 24 hours. */ - min_start_date: string; - /** @description Inclusive upper bound for the start time, as an ISO 8601 string. The query will consider all valid start times on or before this time. The difference between this and `min_start_time` can be at most 24 hours. */ - max_start_date: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The number of nodes */ - quantity: number; - /** @description The instance type of the order */ - instance_type?: string; - contract_id?: string; - }; - header?: { - /** @description Generate a bearer token with `$ sf tokens create`. */ - authorization?: string; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": - | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "buy"; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ - price: number; - /** @description The number of nodes */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ - start_at: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The instance type of the order */ - instance_type: string; - } - | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "sell"; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ - price: number; - /** @description The number of nodes */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ - start_at: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - contract_id: string; - }; - "multipart/form-data": - | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "buy"; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ - price: number; - /** @description The number of nodes */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ - start_at: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The instance type of the order */ - instance_type: string; - } - | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "sell"; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ - price: number; - /** @description The number of nodes */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ - start_at: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - contract_id: string; - }; - "text/plain": - | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "buy"; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ - price: number; - /** @description The number of nodes */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ - start_at: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - /** @description The instance type of the order */ - instance_type: string; - } - | { - /** @constant */ - object: "quote"; - /** @constant */ - side: "sell"; - /** @description Amount in Centicents (1/100th of a cent, One Centicent = $0.0001) */ - price: number; - /** @description The number of nodes */ - quantity: number; - /** @description The start time, as an ISO 8601 string. Start that aren't "right now" will be rounded up to the nearest the hour. For example, if it's 16:00, you put in 17:10, the start time will be rounded up to 18:00. However, if it's 17:10, and you put in 17:10, the start time will be 17:10. */ - start_at: string; - /** @description The duration, in seconds. Duration will be rounded such that the contract ends on the hour. For example if `start_time` is 17:10 and you put in 30m, the duration will be rounded up to 50m. Similarly, if `start_time` is 18:00 and you put 50m, the duration will be rounded up to 1h. */ - duration: number; - contract_id: string; - }; - }; - }; 500: { headers: { [name: string]: unknown;