// Trading API quickstart: mint a token, buy 2 USDC of SOL, and
// find the fill in your transactions.
//
//   npm install tsx @turnkey/api-key-stamper
//   TM_KEY_FILE=path/to/your-api-key.json npx tsx quickstart.ts
//
// This trades real funds. Each run buys another 2 USDC of SOL
// while your wallet has funds.
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { createPrivateKey, sign } from "node:crypto";
import { readFileSync } from "node:fs";

const API = "https://api.truemarkets.co";
const KEY_FILE = process.env.TM_KEY_FILE!;
const ORDER_USDC = "2";

let token = "";
const sleep = (ms: number) =>
  new Promise((r) => setTimeout(r, ms));

async function post(path: string, body: unknown) {
  return call("POST", path, body);
}

async function get(path: string) {
  return call("GET", path, undefined);
}

async function call(method: string, path: string, body: unknown) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: {
      "Content-Type": "application/json",
      ...(token && { Authorization: `Bearer ${token}` }),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) {
    const reason = json.code ?? json.type ?? "";
    const detail = `${res.status} ${reason} ${json.message ?? ""}`;
    throw new Error(`${method} ${path}: ${detail}`);
  }
  return json;
}

// #region key
// One key file does both jobs: it signs the token challenge, and,
// because the app registered it on your wallet, your payloads.
const { key_id, private_key: jwk } = JSON.parse(
  readFileSync(KEY_FILE, "utf8"),
);
const y = Buffer.from(jwk.y, "base64url");
const stamper = new ApiKeyStamper({
  // The compressed public key and the private key, as hex.
  apiPublicKey: Buffer.concat([
    Buffer.from([y[31] % 2 === 0 ? 2 : 3]),
    Buffer.from(jwk.x, "base64url"),
  ]).toString("hex"),
  apiPrivateKey: Buffer.from(jwk.d, "base64url").toString("hex"),
});
const stamp = async (payload: string) =>
  (await stamper.stamp(payload)).stampHeaderValue;
// #endregion key

async function main() {
  // #region assets
  const { data: assets } = await get("/v1/gateway/assets");
  const sol = assets.find(
    (a: any) => a.symbol === "SOL" && a.chain === "solana",
  );
  // #endregion assets

  // #region token
  const timestamp = Math.floor(Date.now() / 1000);
  const signature = sign(
    "sha256",
    Buffer.from(`${key_id}.${timestamp}`),
    {
      key: createPrivateKey({ key: jwk, format: "jwk" }),
      // r and s concatenated, which the API expects
      dsaEncoding: "ieee-p1363",
    },
  ).toString("base64url");

  const minted = await post("/v1/auth/api-key/token", {
    key_id,
    timestamp,
    signature,
  });
  token = minted.access_token;
  // minted.refresh_token gets a new pair later, without signing.
  // #endregion token
  console.log(`✓ token     expires ${minted.expires_in}`);

  // #region fund
  const usdcAvailable = async () => {
    const { data } = await get("/v1/gateway/balances");
    const usdc = data.find(
      (b: any) => b.symbol === "USDC" && b.chain === "solana",
    );
    return Number(usdc?.available ?? 0);
  };
  if ((await usdcAvailable()) < Number(ORDER_USDC)) {
    console.log("… send about 3 USDC on Solana to your wallet");
    while ((await usdcAvailable()) < Number(ORDER_USDC)) {
      await sleep(10_000);
    }
  }
  // #endregion fund
  console.log("✓ funded");

  // #region order
  const order = await post("/v1/gateway/orders", {
    asset_id: sol.id,
    qty: ORDER_USDC,
    qty_unit: "quote",
    side: "buy",
    type: "market",
  });
  // An empty order_id means no order was created, and
  // quote.issues says why.
  if (!order.order_id) {
    throw new Error(
      `no order: ${JSON.stringify(order.quote?.issues)}`,
    );
  }

  // Sign each payload and execute straight away, before the
  // quote expires.
  const signatures = await Promise.all(
    order.payloads.map((p: any) => stamp(p.payload)),
  );
  const executed = await post(
    `/v1/gateway/orders/${order.order_id}/execute`,
    { signatures, auth_type: "api_key" },
  );
  // #endregion order

  // #region fill
  let filled = executed;
  while (
    !["complete", "canceled", "failed"].includes(filled.status)
  ) {
    await sleep(2000);
    filled = await get(`/v1/gateway/orders/${order.order_id}`);
  }
  console.log(
    `✓ order     ${filled.status}  ` +
      `${filled.executed_qty ?? ""} SOL for ${ORDER_USDC} USDC`,
  );

  const feed = await get("/v1/gateway/transactions?type=order");
  const row = feed.data.find((t: any) => t.id === order.order_id);
  // #endregion fill
  console.log(
    row
      ? `✓ in your transactions (${row.status})`
      : "… not in your transactions yet",
  );
}

main().catch((err) => {
  console.error(`✗ ${err.message}`);
  process.exit(1);
});
