import { keccak256, getBytes, concat, toUtf8Bytes, zeroPadValue, toBeArray, hexlify, SigningKey, Wallet } from "ethers";
const BASE_URL = "https://dev.upsidemax.xyz";
const INVITE_CODE = "<your-invite-code>"; // from the UpsideMAX team (Devnet only)
const SYMBOL = "BTC-USDC"; // resolved from configs below — never hardcode an ID
const MARKET_DEPLOYER = 1;
const wallet = Wallet.createRandom();
const PRIVATE_KEY = wallet.privateKey, ADDRESS = wallet.address.toLowerCase();
// --- EIP-712 signer (domain: Exchange / v1 / chainId 9767 / verifyingContract 0x0) ---
const kb = (d: Uint8Array) => getBytes(keccak256(d));
const u = (v: bigint | number) => getBytes(zeroPadValue(toBeArray(BigInt(v)), 32));
const hs = (s: string) => kb(toUtf8Bytes(s));
const cat = (a: Uint8Array[]) => getBytes(concat(a));
const DOMAIN = kb(cat([hs("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), hs("Exchange"), hs("1"), u(9767), u(0)]));
const TYPED: Record<string, (a: any, n: bigint) => Uint8Array[]> = { // full set → Authentication
registerAccount: (a, n) => [hs("RegisterAccount(address address,uint64 nonce)"), getBytes(zeroPadValue(a.address, 32)), u(n)],
lockCollateral: (a, n) => [hs("LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)"), u(a.marketDeployerId), u(a.coinId), hs(String(a.amount)), u(n)],
};
const canon = (o: any): string =>
Array.isArray(o) ? "[" + o.map(canon).join(",") + "]"
: o && typeof o === "object" ? "{" + Object.keys(o).sort().map(k => JSON.stringify(k) + ":" + canon(o[k])).join(",") + "}"
: JSON.stringify(o);
function digest(action: any, n: bigint): Uint8Array {
let struct: Uint8Array;
if (TYPED[action.type]) struct = kb(cat(TYPED[action.type](action, n)));
else { // Agent path
const h = kb(cat([toUtf8Bytes(canon(action)), getBytes(zeroPadValue(toBeArray(n), 8))]));
struct = kb(cat([hs("Agent(string source,bytes32 actionHash)"), hs("b"), h]));
}
return kb(cat([Uint8Array.from([0x19, 0x01]), DOMAIN, struct]));
}
let lastNonce = 0;
const nextNonce = () => (lastNonce = Math.max(lastNonce + 1, Date.now())); // strictly increasing
async function signAndSend(action: any, extra: Record<string, unknown> = {}) {
const n = BigInt(nextNonce());
const sig = new SigningKey(PRIVATE_KEY).sign(hexlify(digest(action, n)));
return (await fetch(`${BASE_URL}/exchange`, { method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, nonce: Number(n), signature: { r: sig.r, s: sig.s, v: sig.v }, ...extra }) })).json();
}
const info = (q: any) => fetch(`${BASE_URL}/info`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(q) }).then(r => r.json());
(async () => {
// 1. Resolve the contract. Filter to status "Active": delisted contracts are returned
// too, with a full parameter set and nothing else to mark them.
const live = ((await info({ type: "configs" })).contracts as any[])
.filter(c => c.status === "Active" && c.name === SYMBOL);
if (live.length !== 1) throw new Error(`expected exactly one Active ${SYMBOL}, found ${live.length} — stop, do not substitute`);
const C = live[0], USDC = C.quoteCoinId;
// 2. Register — inviteCode is an unsigned, top-level field
const acct = (await signAndSend({ type: "registerAccount", address: ADDRESS }, { inviteCode: INVITE_CODE })).response.accountId;
// 3. Wait for the 10,000 USDC airdrop to become usable margin. Devnet credits it straight to
// the market-deployer margin pool; if it instead arrives as a chain-level balance, move
// it in with lockCollateral. Polling marginAvailableForOrder works for either path.
const marginAvail = async () =>
BigInt((await info({ type: "userAccount", accountId: String(acct), marketDeployerId: MARKET_DEPLOYER })).marginAvailableForOrder ?? "0");
const chainUsdc = async () => {
const bals = (await info({ type: "userAccount", accountId: String(acct), marketDeployerId: 0 })).chainBalances ?? [];
const c = bals.find((x: any) => x.coinId === USDC);
return c ? BigInt(c.amount) : 0n;
};
while ((await marginAvail()) === 0n) {
const chain = await chainUsdc();
if (chain > 0n) await signAndSend({ type: "lockCollateral", marketDeployerId: MARKET_DEPLOYER, coinId: USDC, amount: String(chain) });
await new Promise(r => setTimeout(r, 1000));
}
// 4. Price off the live mark. A limit price further from mark than the contract's
// priceBandBps is rejected, so never invent one. Integer maths only — no floats.
const toRaw = (units: bigint, increment: string) => {
const raw = units - (units % BigInt(increment)); // snap down to tick / step
if (raw <= 0n) throw new Error("value rounds to zero");
return raw.toString();
};
const markRaw = BigInt((await info({ type: "marketState", asset: String(C.contractId) })).markPx);
const px = toRaw(markRaw * 80n / 100n, C.tickSize); // 20% below mark: rests, won't fill
const sz = toRaw(BigInt(Math.round(0.01 * 10 ** C.qtyScale)), C.stepSize);
// 5. Place the order. A 202 means "accepted for processing" — NOT that the order rested.
// Confirm by polling; a rejection never appears here, only on the orderUpdates channel.
const cloid = String(nextNonce());
console.log(await signAndSend({ type: "order", grouping: "na",
orders: [{ a: C.contractId, b: true, p: px, s: sz, r: false, c: cloid, t: { limit: { tif: "Gtc" } } }] }));
let oid: number | undefined;
for (let i = 0; i < 15 && oid === undefined; i++) {
const orders = (await info({ type: "userOrders", accountId: String(acct), marketDeployerId: MARKET_DEPLOYER, contractId: C.contractId })).orders as any[];
oid = orders.find(o => o.clientOrderId === cloid)?.id;
if (oid === undefined) await new Promise(r => setTimeout(r, 1000));
}
if (oid === undefined) throw new Error("the order never rested — it was rejected. Subscribe to orderUpdates for the reason.");
// 6. Verify, then cancel
console.log(await info({ type: "userOrders", accountId: String(acct), marketDeployerId: MARKET_DEPLOYER }));
console.log(await signAndSend({ type: "cancel", cancels: [{ a: C.contractId, o: Number(oid) }] }));
})();