import { abi, createKuruClient, decodeKuruEventLog, decodeBookUpdatesPacked, NativeExecInstruction, NativeSide, NativeTif } from "@toxicflow-labs/ts-sdk";
import { createPublicClient, createWalletClient, http, parseUnits, type Hash } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { monadTestnet } from "viem/chains";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Set ${name}`);
return value;
}
const account = privateKeyToAccount(required("PRIVATE_KEY") as `0x${string}`);
const transport = http(required("RPC_URL"));
const publicClient = createPublicClient({ chain: monadTestnet, transport });
const walletClient = createWalletClient({ account, chain: monadTestnet, transport });
if (await publicClient.getChainId() !== 10143) throw new Error("Expected Monad testnet");
// Dedicated tutorial owner; do not run another trading process for this account.
const accountCore = "0xe08573a1Ecc4aa507c9626071fA0c4513caf7906";
const market = "0xD2F64EaF7E85688802d24C18ea268C365533b344"; // MON/USDC
const token = "0xb01DC575a7ef75e58A05D2bde73ab79f8e7dd0Be"; // Test USDC
const kuru = createKuruClient({ publicClient, walletClient, account, addresses: { accountCore } });
async function confirmed(hash: Hash) {
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error(`Transaction reverted: ${hash}`);
return receipt;
}
function scaleDecimals(scale: bigint): number {
if (!/^10*$/.test(scale.toString())) throw new Error("Expected power-of-ten precision");
return scale.toString().length - 1;
}
const [priceScale, sizeScale, tick, minQuote, maxQuote] = await publicClient.readContract({ address: market, abi: abi.orderBookAbi, functionName: "getMarketParams" });
const price = parseUnits(required("LIMIT_PRICE"), scaleDecimals(BigInt(priceScale)));
const quantity = parseUnits(required("BASE_QUANTITY"), scaleDecimals(sizeScale));
const decimals = await publicClient.readContract({ address: token, abi: abi.erc20MetadataAbi, functionName: "decimals" });
const amount = parseUnits(required("DEPOSIT_USDC"), decimals);
if (price <= 0n || price > 0xffff_ffffn || price % BigInt(tick) !== 0n || quantity <= 0n || quantity >= (1n << 96n) || amount <= 0n) throw new Error("Invalid price, tick, quantity or deposit");
const quote = (price * quantity / sizeScale) * 10n ** BigInt(decimals) / BigInt(priceScale);
if (quote < minQuote || quote > maxQuote) throw new Error("Order outside market notional bounds");
let userId = await kuru.account.getRootAccountId({ rootOwner: account.address });
if (userId !== 0n) {
const ids = await Promise.all(Array.from({ length: 62 }, (_, slotIdx) => kuru.spot.getOrderId({ market, userId, slotIdx })));
if (ids.some(id => id !== 0n)) throw new Error("Use an account without existing orders in this market");
}
await confirmed(await kuru.account.approveErc20({ token, spender: accountCore, amount }));
await confirmed(await kuru.account.depositToOwner({ rootOwner: account.address, token, amount }));
userId = await kuru.account.getRootAccountId({ rootOwner: account.address });
const receipt = await confirmed(await kuru.spot.batch({
market, userId,
orders: [{ side: NativeSide.BUY, quantity, price, tif: NativeTif.GTC, executionInstruction: NativeExecInstruction.POST_ONLY, minSizeAfterBlock: 0n }],
cancelSlotIdxs: [],
}));
// Decode only market logs; a successful post-only action can still place no order.
for (const log of receipt.logs) {
if (log.address.toLowerCase() !== market.toLowerCase()) continue;
const event = decodeKuruEventLog(log);
if (event.eventName === "BookUpdatesPacked") {
const args = event.args as unknown as { packedUpdates: `0x${string}` };
console.log(decodeBookUpdatesPacked(args.packedUpdates));
}
}
// This dedicated account began with no live orders; inspect all slots instead of assuming slot zero.
for (let slotIdx = 0; slotIdx < 62; slotIdx++) {
const orderId = await kuru.spot.getOrderId({ market, userId, slotIdx });
if (orderId === 0n) continue;
await confirmed(await kuru.spot.cancelBySlots({ market, userId, cancelSlotIdxs: [slotIdx] }));
}
console.log({ accountId: userId, free: await kuru.account.getBalance({ accountId: userId, token }), reserved: await kuru.account.getSpotReservedBalance({ accountId: userId, token }) });