import WebSocket from "ws";
import { privateKeyToAccount } from "viem/accounts";
const ENDPOINT = "wss://propamm-staging.biconomy.io";
const CHAIN_ID = 84532;
const EXECUTOR = "0x000000fFA5f8Ae192Ab65204f9B7E062CbF4e05D"; // same address on every chain
const PROVIDER = "0xYourProviderContract";
const TOKEN_IN = "0x8b414aD7005EeFd315aF2A16538885Eae229bab7"; // MockWETH, 18 decimals
const TOKEN_OUT = "0xAbbdbbbd6d56593A9c5656c06cB30D61E4a544Df"; // MockUSDC, 18 decimals
const account = privateKeyToAccount(process.env.MM_SIGNER_KEY as `0x${string}`);
const domain = { name: "PropAMMExecutor", version: "2", chainId: CHAIN_ID, verifyingContract: EXECUTOR } as const;
const types = {
Rung: [{ name: "size", type: "uint256" }, { name: "offsetPpm", type: "uint256" }],
OffsetLadder: [
{ name: "mm", type: "address" }, { name: "provider", type: "address" },
{ name: "tokenIn", type: "address" }, { name: "tokenOut", type: "address" },
{ name: "rungs", type: "Rung[]" }, { name: "nonce", type: "uint256" },
{ name: "expiresAt", type: "uint256" }, { name: "driftPpmPerSecond", type: "uint256" },
],
Anchor: [
{ name: "mm", type: "address" }, { name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" }, { name: "price", type: "uint256" },
{ name: "nonce", type: "uint256" }, { name: "timestamp", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
],
} as const;
const now = () => BigInt(Math.floor(Date.now() / 1000));
// bigint fields go on the wire as decimal strings; chainId stays a number.
const frame = (o: unknown) => JSON.stringify(o, (_, v) => (typeof v === "bigint" ? v.toString() : v));
function yourPrice(): bigint {
return 2000n * 10n ** 18n; // 2000 tokenOut per tokenIn, both 18 decimals
}
async function sendOffsets(ws: WebSocket) {
const message = {
mm: account.address, provider: PROVIDER, tokenIn: TOKEN_IN, tokenOut: TOKEN_OUT,
rungs: [
{ size: 1n * 10n ** 18n, offsetPpm: 1000n }, // first 1 tokenIn at 10 bps below the anchor
{ size: 3n * 10n ** 18n, offsetPpm: 2500n }, // next 2 tokenIn at 25 bps below
],
nonce: BigInt(Date.now()), // depth nonce, shared with price-ladder
expiresAt: now() + 1800n, // the shape lives 30 minutes
driftPpmPerSecond: 10n, // widen 1 bp per second of anchor age
};
const signature = await account.signTypedData({ domain, types, primaryType: "OffsetLadder", message });
ws.send(frame({ type: "offsets", payload: { ...message, signature, chainId: CHAIN_ID } }));
}
async function sendAnchor(ws: WebSocket) {
const t = now();
const message = {
mm: account.address, tokenIn: TOKEN_IN, tokenOut: TOKEN_OUT,
price: yourPrice(),
nonce: BigInt(Date.now()), // anchor nonce, its own sequence
timestamp: t, // drift counts from here
expiresAt: t + 15n, // the ladder stops quoting 15 s after this if no newer anchor lands
};
const signature = await account.signTypedData({ domain, types, primaryType: "Anchor", message });
ws.send(frame({ type: "anchor", payload: { ...message, signature, chainId: CHAIN_ID } }));
}
const ws = new WebSocket(ENDPOINT);
ws.on("message", (m) => console.log(m.toString()));
ws.on("open", async () => {
ws.send(JSON.stringify({ type: "subscribe", data: { type: "price-ledger", mm: account.address } }));
await sendOffsets(ws); // once, then when sizes, offsets or drift change
setInterval(() => void sendAnchor(ws), 1000); // the price, every second
});
ws.on("close", () => process.exit(1)); // let a supervisor restart for a clean reconnect