import WebSocket from "ws";
import { privateKeyToAccount } from "viem/accounts";
const ENDPOINT = "wss://propamm-staging.biconomy.io/v1/ws";
const CHAIN_ID = 84532;
const EXECUTOR = "0x0000006f9bBA066e4efda2e0CAbE4FF8cf9030aC"; // 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" },
],
AnchorEntry: [
{ name: "tokenIn", type: "address" }, { name: "tokenOut", type: "address" },
{ name: "price", type: "uint256" }, { name: "timestampMs", type: "uint256" },
{ name: "ttl", type: "uint256" }, { name: "skewPpm", type: "uint256" },
{ name: "reverseSkewPpm", type: "uint256" },
],
AnchorBatch: [{ name: "mm", type: "address" }, { name: "entries", type: "AnchorEntry[]" }],
} 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 } }));
}
let lastTimestampMs = 0n;
async function sendAnchors(ws: WebSocket) {
const t = BigInt(Date.now());
lastTimestampMs = t > lastTimestampMs ? t : lastTimestampMs + 1n; // strictly increasing per pair
const entries = [
{
tokenIn: TOKEN_IN, tokenOut: TOKEN_OUT,
price: yourPrice(), // TOKEN_IN to TOKEN_OUT; the reverse prices at 1e36 / price
timestampMs: lastTimestampMs, // drift counts from here
ttl: 15n, // the pair stops quoting 15 s after this if no newer anchor lands
skewPpm: 0n, // extra discount on TOKEN_IN to TOKEN_OUT
reverseSkewPpm: 0n, // extra discount on TOKEN_OUT to TOKEN_IN
},
// one entry per further pair you quote, up to 32 per message
];
const message = { mm: account.address, entries };
const signature = await account.signTypedData({ domain, types, primaryType: "AnchorBatch", message });
ws.send(frame({ type: "anchor", payload: { ...message, signature, chainId: CHAIN_ID } }));
}
const ws = new WebSocket(ENDPOINT, { headers: { "x-api-key": process.env.PROPAMM_API_KEY as string } });
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 sendAnchors(ws), 1000); // every pair's price, every second
});
ws.on("close", () => process.exit(1)); // let a supervisor restart for a clean reconnect