Skip to content

Commit 1a14ee8

Browse files
authored
sf orders cancel <id> (#11)
* ping & me * nodes label & start date floor * epoch * cancel order * cancel
1 parent 45da86e commit 1a14ee8

5 files changed

Lines changed: 154 additions & 11 deletions

File tree

src/helpers/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const ApiErrorCode = {
2828
InvalidStart: "order.invalid_start",
2929
InvalidDuration: "order.invalid_duration",
3030
InsufficientFunds: "order.insufficient_funds",
31+
AlreadyCancelled: "order.already_cancelled",
3132
NotFound: "order.not_found",
3233
},
3334
Tokens: {

src/helpers/urls.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ const webPaths = {
88

99
const apiPaths = {
1010
index: "/",
11+
me: "/v0/me",
1112
ping: "/v0/ping",
1213

1314
orders_create: "/v0/orders",
1415
orders_list: "/v0/orders",
1516
orders_get: ({ id }: { id: string }) => `/v0/orders/${id}`,
17+
orders_cancel: ({ id }: { id: string }) => `/v0/orders/${id}`,
1618

1719
instances_list: "/v0/instances",
1820
instances_get: ({ id }: { id: string }) => `/v0/instances/${id}`,

src/lib/buy.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function registerBuy(program: Command) {
3535

3636
function confirmPlaceOrderParametersMessage(params: PlaceOrderParameters) {
3737
const { quantity, price, instance_type, duration, start_at } = params;
38+
const nodesLabel = quantity > 1 ? "nodes" : "node";
3839

3940
const startDate = new Date(start_at);
4041

@@ -43,11 +44,11 @@ function confirmPlaceOrderParametersMessage(params: PlaceOrderParameters) {
4344
const centicentsAsDollars = (price / 10_000).toFixed(2);
4445
const durationHumanReadable = formatDuration(duration * 1000);
4546

46-
const topLine = `${c.green(quantity)} ${c.green(instance_type)} nodes for ${c.green(durationHumanReadable)} starting ${c.green(humanReadableStartAt)} (${c.green(fromNowTime)})`;
47+
const topLine = `${c.green(quantity)} ${c.green(instance_type)} ${nodesLabel} for ${c.green(durationHumanReadable)} starting ${c.green(humanReadableStartAt)} (${c.green(fromNowTime)})`;
4748

4849
const priceLine = `\nBuy for ${c.green(`$${centicentsAsDollars}`)}? ${c.dim("(y/n)")}`;
4950

50-
return `\n${topLine}\n${priceLine} `;
51+
return `${topLine}\n${priceLine} `;
5152
}
5253

5354
interface PostOrderResponse {
@@ -91,7 +92,9 @@ async function placeBuyOrder(props: PlaceBuyOrderArguments) {
9192
return logAndQuit("Invalid duration");
9293
}
9394

94-
const startDate = start ? chrono.parseDate(start) : new Date();
95+
const startDate = start
96+
? chrono.parseDate(start)
97+
: dayjs().add(1, "hour").toDate();
9598
if (!startDate) {
9699
return logAndQuit("Invalid start date");
97100
}

src/lib/dev.ts

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
import { confirm } from "@inquirer/prompts";
22
import type { Command } from "commander";
3-
import { deleteConfig, getConfigPath, loadConfig } from "../helpers/config";
3+
import {
4+
deleteConfig,
5+
getConfigPath,
6+
isLoggedIn,
7+
loadConfig,
8+
} from "../helpers/config";
9+
import {
10+
logAndQuit,
11+
logLoginMessageAndQuit,
12+
logSessionTokenExpiredAndQuit,
13+
} from "../helpers/errors";
14+
import { getApiUrl } from "../helpers/urls";
415

516
// development only commands
617
export function registerDev(program: Command) {
718
if (process.env.IS_DEVELOPMENT_CLI_ENV) {
8-
program.command("ping").action(async () => {
9-
console.log("pong");
19+
registerConfig(program);
20+
21+
program.command("me").action(async () => {
22+
const accountId = await getLoggedInAccountId();
23+
console.log(accountId);
24+
1025
process.exit(0);
1126
});
27+
program.command("epoch").action(async () => {
28+
const MILLS_PER_EPOCH = 1000 * 60 * 60;
29+
console.log(Math.floor(Date.now() / MILLS_PER_EPOCH));
1230

13-
registerConfig(program);
31+
process.exit(0);
32+
});
33+
program.command("ping").action(async () => {
34+
const data = await pingServer();
35+
console.log(data);
36+
37+
process.exit(0);
38+
});
1439
}
1540
}
1641

42+
// --
43+
1744
function registerConfig(program: Command) {
1845
const configCmd = program
1946
.command("config")
@@ -44,8 +71,6 @@ function registerConfig(program: Command) {
4471
.action(removeConfigAction);
4572
}
4673

47-
// --
48-
4974
async function showConfigAction() {
5075
const config = await loadConfig();
5176
console.log(config);
@@ -62,3 +87,59 @@ async function removeConfigAction() {
6287
}
6388
process.exit(0);
6489
}
90+
91+
// --
92+
93+
async function getLoggedInAccountId() {
94+
const loggedIn = await isLoggedIn();
95+
if (!loggedIn) {
96+
logLoginMessageAndQuit();
97+
}
98+
const config = await loadConfig();
99+
100+
const response = await fetch(await getApiUrl("me"), {
101+
method: "GET",
102+
headers: {
103+
"Content-Type": "application/json",
104+
Authorization: `Bearer ${config.auth_token}`,
105+
},
106+
});
107+
if (!response.ok) {
108+
if (response.status === 401) {
109+
logSessionTokenExpiredAndQuit();
110+
}
111+
112+
logAndQuit("Failed to fetch account info");
113+
}
114+
115+
const data = await response.json();
116+
117+
return data.id;
118+
}
119+
120+
async function pingServer() {
121+
const loggedIn = await isLoggedIn();
122+
if (!loggedIn) {
123+
logLoginMessageAndQuit();
124+
}
125+
const config = await loadConfig();
126+
127+
const response = await fetch(await getApiUrl("ping"), {
128+
method: "GET",
129+
headers: {
130+
"Content-Type": "application/json",
131+
Authorization: `Bearer ${config.auth_token}`,
132+
},
133+
});
134+
if (!response.ok) {
135+
if (response.status === 401) {
136+
logSessionTokenExpiredAndQuit();
137+
}
138+
139+
logAndQuit("Failed to ping server");
140+
}
141+
142+
const data = await response.json();
143+
144+
return data;
145+
}

src/lib/orders.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import dayjs from "dayjs";
44
import duration from "dayjs/plugin/duration";
55
import relativeTime from "dayjs/plugin/relativeTime";
66
import { getAuthToken, isLoggedIn } from "../helpers/config";
7-
import { logAndQuit, logLoginMessageAndQuit } from "../helpers/errors";
7+
import {
8+
ApiErrorCode,
9+
logAndQuit,
10+
logLoginMessageAndQuit,
11+
logSessionTokenExpiredAndQuit,
12+
type ApiError,
13+
} from "../helpers/errors";
814
import { getApiUrl } from "../helpers/urls";
915
import type { ListResponseBody, Order } from "./types";
1016

@@ -65,7 +71,7 @@ export function priceToCenticents(price: string | number): number {
6571
const numericPrice = Number.parseFloat(price.replace(/^\$/, ""));
6672

6773
// Convert dollars to centicents
68-
return Math.round(numericPrice * 10000);
74+
return Math.round(numericPrice * 10_000);
6975
} catch (error) {
7076
logAndQuit("Invalid price");
7177
}
@@ -142,6 +148,11 @@ export function registerOrders(program: Command) {
142148

143149
process.exit(0);
144150
});
151+
152+
ordersCommand
153+
.command("cancel <id>")
154+
.description("Cancel an order")
155+
.action(submitOrderCancellationByIdAction);
145156
}
146157

147158
export async function getOrders(props: {
@@ -186,3 +197,48 @@ export async function getOrders(props: {
186197
const resp = (await response.json()) as ListResponseBody<Order>;
187198
return resp.data;
188199
}
200+
201+
export async function submitOrderCancellationByIdAction(
202+
orderId: string,
203+
): Promise<void> {
204+
const loggedIn = await isLoggedIn();
205+
if (!loggedIn) {
206+
logLoginMessageAndQuit();
207+
}
208+
209+
const url = await getApiUrl("orders_cancel", { id: orderId });
210+
const response = await fetch(url, {
211+
method: "DELETE",
212+
body: JSON.stringify({}),
213+
headers: {
214+
"Content-Type": "application/json",
215+
Authorization: `Bearer ${await getAuthToken()}`,
216+
},
217+
});
218+
if (!response.ok) {
219+
if (response.status === 401) {
220+
await logSessionTokenExpiredAndQuit();
221+
}
222+
223+
const error = (await response.json()) as ApiError;
224+
if (error.code === ApiErrorCode.Orders.NotFound) {
225+
logAndQuit(`Order ${orderId} not found`);
226+
} else if (error.code === ApiErrorCode.Orders.AlreadyCancelled) {
227+
logAndQuit(`Order ${orderId} is already cancelled`);
228+
}
229+
230+
// TODO: handle more specific errors
231+
232+
logAndQuit(`Failed to cancel order ${orderId}`);
233+
}
234+
235+
const resp = await response.json();
236+
const cancellationSubmitted = resp.object === "pending";
237+
if (!cancellationSubmitted) {
238+
logAndQuit(`Failed to cancel order ${orderId}`);
239+
}
240+
241+
// cancellation submitted successfully
242+
console.log(`Cancellation for Order ${orderId} submitted.`);
243+
process.exit(0);
244+
}

0 commit comments

Comments
 (0)