From f9eac8e21d853a47365985028c1773c5849c714f Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 4 Feb 2026 18:25:57 +0000 Subject: [PATCH 1/2] init --- src/core/process/round.ts | 15 +++++-- src/order/index.test.ts | 91 ++++++++++++++++++++++++++++++++++----- src/order/index.ts | 17 ++++++-- src/order/sync.test.ts | 6 +++ src/order/sync.ts | 7 ++- src/order/types/index.ts | 8 ++-- src/order/types/v4.ts | 44 ++++++++++++++----- 7 files changed, 155 insertions(+), 33 deletions(-) diff --git a/src/core/process/round.ts b/src/core/process/round.ts index d6fb54c6..7d954783 100644 --- a/src/core/process/round.ts +++ b/src/core/process/round.ts @@ -1,8 +1,8 @@ import { RainSolver } from ".."; -import { Pair } from "../../order"; import { Token } from "sushi/currency"; import { iterRandom, Result } from "../../common"; import { SpanStatusCode } from "@opentelemetry/api"; +import { OrderbookVersions, Pair } from "../../order"; import { PreAssembledSpan, SpanWithContext } from "../../logger"; import { ErrorSeverity, errorSnapshot, isTimeout, KnownErrors } from "../../error"; import { @@ -181,8 +181,17 @@ export async function processOrderInit( ), )?.balance ?? orderDetails.buyTokenVaultBalance; - // skip if the output vault is empty - if (orderDetails.sellTokenVaultBalance <= 0n) { + // skip if the output vault is empty for + // non v6 orderbook or non vaultless v6 orderbook + if ( + (orderDetails.orderbookVersion !== OrderbookVersions.V6 && + orderDetails.sellTokenVaultBalance <= 0n) || + (orderDetails.orderbookVersion === OrderbookVersions.V6 && + orderDetails.takeOrder.struct.order.validOutputs[ + orderDetails.takeOrder.struct.outputIOIndex + ].vaultId !== 0n && + orderDetails.sellTokenVaultBalance <= 0n) + ) { const endTime = performance.now(); const settlement: Settlement = { pair, diff --git a/src/order/index.test.ts b/src/order/index.test.ts index ba53530e..421db332 100644 --- a/src/order/index.test.ts +++ b/src/order/index.test.ts @@ -1188,7 +1188,14 @@ describe("Test OrderManager", () => { const balance = "0x1234"; const spy = vi.spyOn(common, "normalizeFloat"); - const result = orderManager.updateVault(orderbook, owner, token, vaultId, balance); + const result = orderManager.updateVault( + orderbook, + owner, + token, + vaultId, + balance, + OrderbookVersions.V5, + ); expect(result).toBeUndefined(); expect(spy).toHaveBeenCalledWith(balance, token.decimals); @@ -1206,7 +1213,7 @@ describe("Test OrderManager", () => { const vaultId = 123n; const balance = 1000000000000000000n; - orderManager.updateVault(orderbook, owner, token, vaultId, balance); + orderManager.updateVault(orderbook, owner, token, vaultId, balance, OrderbookVersions.V4); const orderbookMap = orderManager.ownerTokenVaultMap.get(orderbook); expect(orderbookMap).toBeDefined(); @@ -1236,7 +1243,7 @@ describe("Test OrderManager", () => { const balance = "0xffffffee00000000000000000000000000000000000000000000000000000001"; const spy = vi.spyOn(common, "normalizeFloat"); - orderManager.updateVault(orderbook, owner, token, vaultId, balance); + orderManager.updateVault(orderbook, owner, token, vaultId, balance, OrderbookVersions.V5); expect(spy).toHaveBeenCalledWith(balance, token.decimals); @@ -1258,6 +1265,40 @@ describe("Test OrderManager", () => { spy.mockRestore(); }); + it("should set 0 vault balance for vaultless orderbook v6", () => { + const orderbook = "0xorderbook1"; + const owner = "0xowner1"; + const token = { + address: "0xtoken1", + symbol: "TOKEN1", + decimals: 18, + }; + const vaultId = 0n; + const balance = "0xffffffee00000000000000000000000000000000000000000000000000000001"; // ignored + const spy = vi.spyOn(common, "normalizeFloat"); + + orderManager.updateVault(orderbook, owner, token, vaultId, balance, OrderbookVersions.V6); + + expect(spy).not.toHaveBeenCalledWith(); + + const orderbookMap = orderManager.ownerTokenVaultMap.get(orderbook); + expect(orderbookMap).toBeDefined(); + + const ownerMap = orderbookMap?.get(owner); + expect(ownerMap).toBeDefined(); + + const tokenMap = ownerMap?.get(token.address); + expect(tokenMap).toBeDefined(); + + const vault = tokenMap?.get(vaultId); + expect(vault).toBeDefined(); + expect(vault?.id).toBe(vaultId); + expect(vault?.balance).toBe(0n); // skip tracking, always 0 + expect(vault?.token).toEqual(token); + + spy.mockRestore(); + }); + it("should update vault correctly when balance is decimal string", () => { const orderbook = "0xorderbook1"; const owner = "0xowner1"; @@ -1270,7 +1311,7 @@ describe("Test OrderManager", () => { const balance = "1000000000000000000"; const spy = vi.spyOn(common, "normalizeFloat"); - orderManager.updateVault(orderbook, owner, token, vaultId, balance); + orderManager.updateVault(orderbook, owner, token, vaultId, balance, OrderbookVersions.V4); expect(spy).not.toHaveBeenCalled(); @@ -1305,7 +1346,14 @@ describe("Test OrderManager", () => { const newBalance = 2000000000000000000n; // First update - create vault - orderManager.updateVault(orderbook, owner, token, vaultId, initialBalance); + orderManager.updateVault( + orderbook, + owner, + token, + vaultId, + initialBalance, + OrderbookVersions.V4, + ); // verify initial state const vault = orderManager.ownerTokenVaultMap @@ -1316,7 +1364,14 @@ describe("Test OrderManager", () => { expect(vault?.balance).toBe(initialBalance); // second update - update balance - orderManager.updateVault(orderbook, owner, token, vaultId, newBalance); + orderManager.updateVault( + orderbook, + owner, + token, + vaultId, + newBalance, + OrderbookVersions.V4, + ); // verify updated balance const updatedVault = orderManager.ownerTokenVaultMap @@ -1342,8 +1397,8 @@ describe("Test OrderManager", () => { const balance1 = 1000000000000000000n; const balance2 = 2000000000000000000n; - orderManager.updateVault(orderbook, owner, token, vaultId1, balance1); - orderManager.updateVault(orderbook, owner, token, vaultId2, balance2); + orderManager.updateVault(orderbook, owner, token, vaultId1, balance1, OrderbookVersions.V4); + orderManager.updateVault(orderbook, owner, token, vaultId2, balance2, OrderbookVersions.V4); const tokenMap = orderManager.ownerTokenVaultMap .get(orderbook) @@ -1374,10 +1429,24 @@ describe("Test OrderManager", () => { const balance2 = 500000000n; // Add first vault - orderManager.updateVault(orderbook, owner, token1, vaultId1, balance1); + orderManager.updateVault( + orderbook, + owner, + token1, + vaultId1, + balance1, + OrderbookVersions.V4, + ); // Add second vault with different token - orderManager.updateVault(orderbook, owner, token2, vaultId2, balance2); + orderManager.updateVault( + orderbook, + owner, + token2, + vaultId2, + balance2, + OrderbookVersions.V4, + ); // verify both vaults exist const ownerMap = orderManager.ownerTokenVaultMap.get(orderbook)?.get(owner); @@ -1442,6 +1511,7 @@ describe("Test OrderManager", () => { }, 20n, // From validOutputs[1].vaultId 2000n, // sellTokenVaultBalance + OrderbookVersions.V5, ); // should use inputIOIndex: 0 (first input) @@ -1456,6 +1526,7 @@ describe("Test OrderManager", () => { }, 30n, // From validInputs[0].vaultId 1000n, // buyTokenVaultBalance + OrderbookVersions.V5, ); updateVaultSpy.mockRestore(); diff --git a/src/order/index.ts b/src/order/index.ts index ac765c0e..90aad60e 100644 --- a/src/order/index.ts +++ b/src/order/index.ts @@ -18,6 +18,7 @@ import { CounterpartySource, OrderbooksOwnersProfileMap, OrderbookOwnerTokenVaultsMap, + OrderbookVersions, } from "./types"; export * from "./types"; @@ -229,6 +230,7 @@ export class OrderManager { }, BigInt(outputVault.vaultId), pair.sellTokenVaultBalance, + pair.orderbookVersion, ); this.updateVault( orderbook, @@ -240,6 +242,7 @@ export class OrderManager { }, BigInt(inputVault.vaultId), pair.buyTokenVaultBalance, + pair.orderbookVersion, ); } @@ -250,6 +253,7 @@ export class OrderManager { * @param token - The token details * @param vaultId - The vault id * @param balance - The new vault balance + * @param orderbookVersion - Specifies the orderbook version */ updateVault( orderbook: string, @@ -257,14 +261,19 @@ export class OrderManager { token: TokenDetails, vaultId: bigint, balance: string | bigint, + orderbookVersion: OrderbookVersions, ) { // normalize balance based on vault type let normalizedBalance: bigint; if (typeof balance === "string") { if (balance.startsWith("0x")) { - const normalized = normalizeFloat(balance, token.decimals); - if (normalized.isErr()) return; - normalizedBalance = normalized.value; + if (orderbookVersion === OrderbookVersions.V6 && vaultId === 0n) { + normalizedBalance = 0n; + } else { + const normalized = normalizeFloat(balance, token.decimals); + if (normalized.isErr()) return; + normalizedBalance = normalized.value; + } } else { normalizedBalance = BigInt(balance); } @@ -413,7 +422,7 @@ export class OrderManager { new OrderManagerError( "Failed to create order pair from args", OrderManagerErrorType.WasmEncodedError, - pairResult.error, + pairResult.error.readableMsg, ), ); } diff --git a/src/order/sync.test.ts b/src/order/sync.test.ts index dae5bd43..90a973c7 100644 --- a/src/order/sync.test.ts +++ b/src/order/sync.test.ts @@ -85,6 +85,7 @@ describe("Test syncOrders", () => { }, 123n, "1000000000000000000", + "", ); expect(mockUpdateVault).toHaveBeenCalledTimes(1); }); @@ -129,6 +130,7 @@ describe("Test syncOrders", () => { }, 456n, "500000000", + "", ); expect(mockUpdateVault).toHaveBeenCalledTimes(1); }); @@ -194,6 +196,7 @@ describe("Test syncOrders", () => { }, 100n, "2000000000000000000", + "", ); expect(mockUpdateVault).toHaveBeenNthCalledWith( 2, @@ -206,6 +209,7 @@ describe("Test syncOrders", () => { }, 200n, "1000000000", + "", ); }); @@ -270,6 +274,7 @@ describe("Test syncOrders", () => { }, 789n, "50000000", + "", ); expect(mockUpdateVault).toHaveBeenNthCalledWith( 2, @@ -282,6 +287,7 @@ describe("Test syncOrders", () => { }, 101112n, "3000000000000000000", + "", ); }); diff --git a/src/order/sync.ts b/src/order/sync.ts index 37b5168c..3e306494 100644 --- a/src/order/sync.ts +++ b/src/order/sync.ts @@ -1,7 +1,7 @@ -import { OrderManager } from "."; +import { OrderbookVersions, OrderManager } from "."; import { errorSnapshot } from "../error"; import { PreAssembledSpan } from "../logger"; -import { SgTransaction } from "../subgraph/types"; +import { SgTransaction, SubgraphVersions } from "../subgraph/types"; import { applyFilters } from "../subgraph/filter"; /** Syncs orders and vaults to upstream changes since the last fetch */ @@ -35,6 +35,7 @@ export async function syncOrders(this: OrderManager) { }, BigInt(event.vault.vaultId), event.vault.balance, + version === SubgraphVersions.V6 ? OrderbookVersions.V6 : ("" as any), ); } if (event.__typename === "Clear" || event.__typename === "TakeOrder") { @@ -50,6 +51,7 @@ export async function syncOrders(this: OrderManager) { }, BigInt(trade.inputVaultBalanceChange.vault.vaultId), trade.inputVaultBalanceChange.vault.balance, + version === SubgraphVersions.V6 ? OrderbookVersions.V6 : ("" as any), ); this.updateVault( trade.outputVaultBalanceChange.orderbook.id, @@ -61,6 +63,7 @@ export async function syncOrders(this: OrderManager) { }, BigInt(trade.outputVaultBalanceChange.vault.vaultId), trade.outputVaultBalanceChange.vault.balance, + version === SubgraphVersions.V6 ? OrderbookVersions.V6 : ("" as any), ); }); } diff --git a/src/order/types/index.ts b/src/order/types/index.ts index 6fad1e39..2fc9992b 100644 --- a/src/order/types/index.ts +++ b/src/order/types/index.ts @@ -20,6 +20,8 @@ import { TakeOrdersConfigTypeV5, } from "./v4"; +export const ZERO_BYTES_32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; + // Re-export types from v3 and v4 export { OrderV3, OrderProfileV3, PairV3, TakeOrderDetailsV3, TakeOrdersConfigTypeV3, TakeOrderV3 }; export { @@ -128,9 +130,9 @@ export type BundledOrders = { }; export enum OrderbookVersions { - V4, - V5, - V6, + V4 = "v4", + V5 = "v5", + V6 = "v6", } export type PairBase = { diff --git a/src/order/types/v4.ts b/src/order/types/v4.ts index d56f16bd..14d8c75e 100644 --- a/src/order/types/v4.ts +++ b/src/order/types/v4.ts @@ -1,11 +1,11 @@ -import { Evaluable, OrderbookVersions } from "../types"; +import { Evaluable, OrderbookVersions, ZERO_BYTES_32 } from "../types"; import { SgOrder, SubgraphVersions } from "../../subgraph"; import { WasmEncodedError } from "@rainlanguage/float"; import { Order, PairBase, TakeOrderDetailsBase } from "."; import { ABI, normalizeFloat, Result } from "../../common"; import { decodeAbiParameters, DecodeAbiParametersErrorType } from "viem"; -// these types are used in orderbook v5 +// these types are used in orderbook v5 and v6 /** Represents an v4 order */ export type V4 = { @@ -118,13 +118,35 @@ export namespace PairV4 { balance: outputBalanceHex, } = outputVaultDetails; - const inputBalanceRes = normalizeFloat(inputBalanceHex, inputDecimals); - if (inputBalanceRes.isErr()) { - return Result.err(inputBalanceRes.error); - } - const outputBalanceRes = normalizeFloat(outputBalanceHex, outputDecimals); - if (outputBalanceRes.isErr()) { - return Result.err(outputBalanceRes.error); + let inputBalance = 0n; + let outputBalance = 0n; + if (orderDetails.__version === SubgraphVersions.V6) { + if (orderStruct.validInputs[inputIOIndex].vaultId !== ZERO_BYTES_32) { + const inputBalanceRes = normalizeFloat(inputBalanceHex, inputDecimals); + if (inputBalanceRes.isErr()) { + return Result.err(inputBalanceRes.error); + } + inputBalance = inputBalanceRes.value; + } + if (orderStruct.validOutputs[outputIOIndex].vaultId !== ZERO_BYTES_32) { + const outputBalanceRes = normalizeFloat(outputBalanceHex, outputDecimals); + if (outputBalanceRes.isErr()) { + return Result.err(outputBalanceRes.error); + } + outputBalance = outputBalanceRes.value; + } + } else { + const inputBalanceRes = normalizeFloat(inputBalanceHex, inputDecimals); + if (inputBalanceRes.isErr()) { + return Result.err(inputBalanceRes.error); + } + inputBalance = inputBalanceRes.value; + + const outputBalanceRes = normalizeFloat(outputBalanceHex, outputDecimals); + if (outputBalanceRes.isErr()) { + return Result.err(outputBalanceRes.error); + } + outputBalance = outputBalanceRes.value; } return Result.ok({ orderbookVersion: @@ -135,11 +157,11 @@ export namespace PairV4 { buyToken: inputToken.toLowerCase(), buyTokenSymbol: inputSymbol, buyTokenDecimals: inputDecimals, - buyTokenVaultBalance: inputBalanceRes.value, + buyTokenVaultBalance: inputBalance, sellToken: outputToken.toLowerCase(), sellTokenSymbol: outputSymbol, sellTokenDecimals: outputDecimals, - sellTokenVaultBalance: outputBalanceRes.value, + sellTokenVaultBalance: outputBalance, takeOrder: { id: orderHash, struct: { From c922e21d428df9a4b64d6328e6caeee55afec612 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 4 Feb 2026 19:44:24 +0000 Subject: [PATCH 2/2] Update round.ts --- src/core/process/round.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/process/round.ts b/src/core/process/round.ts index 7d954783..4bc8c997 100644 --- a/src/core/process/round.ts +++ b/src/core/process/round.ts @@ -2,8 +2,8 @@ import { RainSolver } from ".."; import { Token } from "sushi/currency"; import { iterRandom, Result } from "../../common"; import { SpanStatusCode } from "@opentelemetry/api"; -import { OrderbookVersions, Pair } from "../../order"; import { PreAssembledSpan, SpanWithContext } from "../../logger"; +import { OrderbookVersions, Pair, ZERO_BYTES_32 } from "../../order"; import { ErrorSeverity, errorSnapshot, isTimeout, KnownErrors } from "../../error"; import { ProcessOrderStatus, @@ -189,7 +189,7 @@ export async function processOrderInit( (orderDetails.orderbookVersion === OrderbookVersions.V6 && orderDetails.takeOrder.struct.order.validOutputs[ orderDetails.takeOrder.struct.outputIOIndex - ].vaultId !== 0n && + ].vaultId !== ZERO_BYTES_32 && orderDetails.sellTokenVaultBalance <= 0n) ) { const endTime = performance.now();