From 61eaa1917a79377950373ebb793ebaee2e4d71aa Mon Sep 17 00:00:00 2001 From: Jai Date: Fri, 11 Sep 2026 15:56:28 +0000 Subject: [PATCH] fix: make v6 subgraph queries and dispair resolution work against RaindexV6 Three things stopped the solver from trading a v6 orderbook on a chain whose only DEX is UniswapV3. All were hit end to end on Robinhood Chain (4663). 1. The v6 subgraph queries ask for an entity that no longer exists. config.example.yaml claims the v5 and v6 subgraph schemas are identical and that the `v6=` prefix exists only to pick the order decoder. That is no longer true: the v6 schema renamed the orderbook entity to `raindex`, along with `orderbooks` -> `raindices` and the `orderbook_in` / `orderbook_not_in` filter keys -> `raindex_in` / `raindex_not_in`. Every v6 query therefore failed with "Type `Order` has no field `orderbook`", surfaced as the much less obvious "Failed to fetch orders / Received invalid response". The query builders now take the subgraph version and select the v6 entity under an `orderbook` alias, so the response shape is unchanged and no consumer of SgOrder / SgTransaction had to move. 2. Dispair resolution assumes getters the RaindexV6 deployer does not have. resolveVersionContracts derives the interpreter and store by calling I_INTERPRETER() / I_STORE() on the configured deployer. The RaindexV6-era deployer exposes only parse2, parsePragma1, describedByMetaV1, buildIntegrityFunctionPointers and supportsInterface, so both reads revert, contracts.v6 comes back undefined, and every order is skipped with "Cannot trade as dispair addresses are not configured for order V4 trade" even though dispair is configured. `contracts..interpreter` and `.store` are now optional overrides. When given they are used directly; otherwise the onchain reads happen exactly as before, so existing configs are unaffected. 3. BASES_TO_CHECK_TRADES_AGAINST was indexed unguarded. getCounterpartyOrdersAgainstBaseTokens throws a TypeError the first time it enumerates intra-orderbook counterparties on any chain sushi has no routing bases for. Guarded with `?? []`. Also bumps the sushi submodule to the Robinhood Chain (4663) support branch. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HiqQdxokJ4edjAFyAkN9G3 --- config.example.yaml | 12 +++++ lib/sushiswap | 2 +- src/config/validators.test.ts | 22 ++++++++ src/config/validators.ts | 24 +++++++++ src/config/yaml.test.ts | 4 ++ src/config/yaml.ts | 12 +++++ src/order/index.test.ts | 95 +++++++++++++++++++++++++++++++++++ src/order/index.ts | 2 +- src/state/contracts.test.ts | 49 ++++++++++++++++++ src/state/contracts.ts | 41 +++++++++------ src/subgraph/index.ts | 19 +++++-- src/subgraph/query.test.ts | 51 ++++++++++++++++++- src/subgraph/query.ts | 54 +++++++++++++++----- 13 files changed, 353 insertions(+), 34 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 22b3fd19..2cab2051 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -53,6 +53,10 @@ contracts: stabullArbAddress: "0x1234...5678" # Dispair v4 contract address dispair: "0x1234...5678" + # Optional interpreter/store overrides, only needed when the deployer above + # does not expose them onchain (RaindexV6 deployers only expose parse2) + interpreter: "0x1234...5678" + store: "0x1234...5678" v5: # Sushi RP arb v5 contract address sushiArbAddress: "0x1234...5678" @@ -64,6 +68,10 @@ contracts: stabullArbAddress: "0x1234...5678" # Dispair v5 contract address dispair: "0x1234...5678" + # Optional interpreter/store overrides, only needed when the deployer above + # does not expose them onchain (RaindexV6 deployers only expose parse2) + interpreter: "0x1234...5678" + store: "0x1234...5678" v6: # Sushi RP arb v6 contract address sushiArbAddress: "0x1234...5678" @@ -77,6 +85,10 @@ contracts: raindexArbAddress: "0x1234...5678" # Dispair v6 contract address dispair: "0x1234...5678" + # Optional interpreter/store overrides, only needed when the deployer above + # does not expose them onchain (RaindexV6 deployers only expose parse2) + interpreter: "0x1234...5678" + store: "0x1234...5678" # list of liquidity providers # if provided through an env var, they should be separated by a comma diff --git a/lib/sushiswap b/lib/sushiswap index 540aebc2..0f3da6d4 160000 --- a/lib/sushiswap +++ b/lib/sushiswap @@ -1 +1 @@ -Subproject commit 540aebc24f047fbbf0a1be166b0f0cfecc3cfbee +Subproject commit 0f3da6d41861a4497dec12199010440db74a7a97 diff --git a/src/config/validators.test.ts b/src/config/validators.test.ts index 68685aa0..49e932bc 100644 --- a/src/config/validators.test.ts +++ b/src/config/validators.test.ts @@ -778,6 +778,8 @@ describe("Test yaml Validator methods", async function () { v4: { sushiArb: sushiArbV4 as `0x${string}`, dispair: dispairV4 as `0x${string}`, + interpreter: undefined, + store: undefined, genericArb: genericArbV4 as `0x${string}`, balancerArb: balancerArbV4 as `0x${string}`, stabullArb: stabullArbV4 as `0x${string}`, @@ -802,6 +804,8 @@ describe("Test yaml Validator methods", async function () { v5: { sushiArb: sushiArbV5 as `0x${string}`, dispair: dispairV5 as `0x${string}`, + interpreter: undefined, + store: undefined, genericArb: genericArbV5 as `0x${string}`, balancerArb: balancerArbV5 as `0x${string}`, stabullArb: stabullArbV5 as `0x${string}`, @@ -827,6 +831,8 @@ describe("Test yaml Validator methods", async function () { v6: { sushiArb: sushiArbV6 as `0x${string}`, dispair: dispairV6 as `0x${string}`, + interpreter: undefined, + store: undefined, genericArb: genericArbV6 as `0x${string}`, balancerArb: balancerArbV6 as `0x${string}`, stabullArb: stabullArbV6 as `0x${string}`, @@ -856,6 +862,8 @@ describe("Test yaml Validator methods", async function () { v4: { sushiArb: sushiArbV4 as `0x${string}`, dispair: dispairV4 as `0x${string}`, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: undefined, stabullArb: undefined, @@ -863,6 +871,8 @@ describe("Test yaml Validator methods", async function () { v5: { sushiArb: undefined, dispair: undefined, + interpreter: undefined, + store: undefined, genericArb: genericArbV5 as `0x${string}`, balancerArb: undefined, stabullArb: undefined, @@ -870,6 +880,8 @@ describe("Test yaml Validator methods", async function () { v6: { sushiArb: undefined, dispair: undefined, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: balancerArbV6 as `0x${string}`, stabullArb: undefined, @@ -891,6 +903,8 @@ describe("Test yaml Validator methods", async function () { v4: { sushiArb: sushiArbV4 as `0x${string}`, dispair: undefined, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: undefined, stabullArb: undefined, @@ -912,6 +926,8 @@ describe("Test yaml Validator methods", async function () { v5: { sushiArb: undefined, dispair: dispairV5 as `0x${string}`, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: balancerArbV5 as `0x${string}`, stabullArb: undefined, @@ -932,6 +948,8 @@ describe("Test yaml Validator methods", async function () { v6: { sushiArb: undefined, dispair: undefined, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: undefined, stabullArb: undefined, @@ -968,6 +986,8 @@ describe("Test yaml Validator methods", async function () { v4: { sushiArb: sushiArbV4 as `0x${string}`, dispair: dispairV4 as `0x${string}`, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: undefined, stabullArb: undefined, @@ -991,6 +1011,8 @@ describe("Test yaml Validator methods", async function () { v6: { sushiArb: undefined, dispair: undefined, + interpreter: undefined, + store: undefined, genericArb: undefined, balancerArb: balancerArbV6 as `0x${string}`, stabullArb: undefined, diff --git a/src/config/validators.ts b/src/config/validators.ts index 1cdf45c2..4dab16e7 100644 --- a/src/config/validators.ts +++ b/src/config/validators.ts @@ -471,6 +471,12 @@ export namespace Validator { export function resolveContracts(input: any): AppOptionsContracts { const dispairV4 = resolveAddress(input?.contracts?.v4?.dispair, "dispair v4", true); + const interpreterV4 = resolveAddress( + input?.contracts?.v4?.interpreter, + "interpreter v4", + true, + ); + const storeV4 = resolveAddress(input?.contracts?.v4?.store, "store v4", true); const sushiArbAddressV4 = resolveAddress( input?.contracts?.v4?.sushiArbAddress, "sushiArbAddress v4", @@ -492,6 +498,12 @@ export namespace Validator { true, ); const dispairV5 = resolveAddress(input?.contracts?.v5?.dispair, "dispair v5", true); + const interpreterV5 = resolveAddress( + input?.contracts?.v5?.interpreter, + "interpreter v5", + true, + ); + const storeV5 = resolveAddress(input?.contracts?.v5?.store, "store v5", true); const sushiArbAddressV5 = resolveAddress( input?.contracts?.v5?.sushiArbAddress, "sushiArbAddress v5", @@ -513,6 +525,12 @@ export namespace Validator { true, ); const dispairV6 = resolveAddress(input?.contracts?.v6?.dispair, "dispair v6", true); + const interpreterV6 = resolveAddress( + input?.contracts?.v6?.interpreter, + "interpreter v6", + true, + ); + const storeV6 = resolveAddress(input?.contracts?.v6?.store, "store v6", true); const sushiArbAddressV6 = resolveAddress( input?.contracts?.v6?.sushiArbAddress, "sushiArbAddress v6", @@ -549,6 +567,8 @@ export namespace Validator { contracts.v4 = { sushiArb: sushiArbAddressV4 as `0x${string}` | undefined, dispair: dispairV4 as `0x${string}` | undefined, + interpreter: interpreterV4 as `0x${string}` | undefined, + store: storeV4 as `0x${string}` | undefined, genericArb: genericArbAddressV4 as `0x${string}` | undefined, balancerArb: balancerArbAddressV4 as `0x${string}` | undefined, stabullArb: stabullArbAddressV4 as `0x${string}` | undefined, @@ -564,6 +584,8 @@ export namespace Validator { contracts.v5 = { sushiArb: sushiArbAddressV5 as `0x${string}` | undefined, dispair: dispairV5 as `0x${string}` | undefined, + interpreter: interpreterV5 as `0x${string}` | undefined, + store: storeV5 as `0x${string}` | undefined, genericArb: genericArbAddressV5 as `0x${string}` | undefined, balancerArb: balancerArbAddressV5 as `0x${string}` | undefined, stabullArb: stabullArbAddressV5 as `0x${string}` | undefined, @@ -580,6 +602,8 @@ export namespace Validator { contracts.v6 = { sushiArb: sushiArbAddressV6 as `0x${string}` | undefined, dispair: dispairV6 as `0x${string}` | undefined, + interpreter: interpreterV6 as `0x${string}` | undefined, + store: storeV6 as `0x${string}` | undefined, genericArb: genericArbAddressV6 as `0x${string}` | undefined, balancerArb: balancerArbAddressV6 as `0x${string}` | undefined, stabullArb: stabullArbAddressV6 as `0x${string}` | undefined, diff --git a/src/config/yaml.test.ts b/src/config/yaml.test.ts index 921b81fb..45444734 100644 --- a/src/config/yaml.test.ts +++ b/src/config/yaml.test.ts @@ -97,6 +97,8 @@ orderbookTradeTypes: sushiArb: "0x1111111111111111111111111111111111111111", balancerArb: "0x3333333333333333333333333333333333333333", dispair: "0x2222222222222222222222222222222222222222", + interpreter: undefined, + store: undefined, genericArb: undefined, stabullArb: "0x4444444444444444444444444444444444444444", }, @@ -179,6 +181,8 @@ orderbookTradeTypes: v5: { sushiArbAddress: "0x1111111111111111111111111111111111111111", dispair: "0x2222222222222222222222222222222222222222", + interpreter: undefined, + store: undefined, genericArbAddress: "0x3333333333333333333333333333333333333333", }, }, diff --git a/src/config/yaml.ts b/src/config/yaml.ts index ebc37217..16c471be 100644 --- a/src/config/yaml.ts +++ b/src/config/yaml.ts @@ -33,6 +33,10 @@ export type AppOptionsContracts = { v4?: { sushiArb?: `0x${string}`; dispair?: `0x${string}`; + /** Optional interpreter override, for deployers that do not expose it onchain */ + interpreter?: `0x${string}`; + /** Optional store override, for deployers that do not expose it onchain */ + store?: `0x${string}`; genericArb?: `0x${string}`; balancerArb?: `0x${string}`; stabullArb?: `0x${string}`; @@ -40,6 +44,10 @@ export type AppOptionsContracts = { v5?: { sushiArb?: `0x${string}`; dispair?: `0x${string}`; + /** Optional interpreter override, for deployers that do not expose it onchain */ + interpreter?: `0x${string}`; + /** Optional store override, for deployers that do not expose it onchain */ + store?: `0x${string}`; genericArb?: `0x${string}`; balancerArb?: `0x${string}`; stabullArb?: `0x${string}`; @@ -47,6 +55,10 @@ export type AppOptionsContracts = { v6?: { sushiArb?: `0x${string}`; dispair?: `0x${string}`; + /** Optional interpreter override, for deployers that do not expose it onchain */ + interpreter?: `0x${string}`; + /** Optional store override, for deployers that do not expose it onchain */ + store?: `0x${string}`; genericArb?: `0x${string}`; balancerArb?: `0x${string}`; stabullArb?: `0x${string}`; diff --git a/src/order/index.test.ts b/src/order/index.test.ts index 120f5cb0..debc836f 100644 --- a/src/order/index.test.ts +++ b/src/order/index.test.ts @@ -1747,6 +1747,101 @@ describe("Test OrderManager", () => { expect(result.size).toBe(0); }); + it("should not throw when the chain has no routing base tokens configured", async () => { + // a chain that sushi has no BASES_TO_CHECK_TRADES_AGAINST entry for + delete (BASES_TO_CHECK_TRADES_AGAINST as any)[state.chainConfig.id]; + + const orderARes = Result.ok({ + type: Order.Type.V4, + owner: "0xowner", + validInputs: [ + { + token: "0xinput", + vaultId: + "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + ], + validOutputs: [ + { + token: "0xoutput", + vaultId: + "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + ], + }); + const orderBRes = Result.ok({ + type: Order.Type.V4, + owner: "0xowner", + validInputs: [ + { + token: "0xbasetoken", + vaultId: + "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + ], + validOutputs: [ + { + token: "0xinput", + vaultId: + "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + ], + }); + (Order.tryFromBytes as Mock) + .mockReturnValueOnce(orderARes) + .mockReturnValueOnce(orderBRes); + const orderA = { + __version: SubgraphVersions.V6, + orderHash: "0xhashA", + orderbook: { id: "0xorderbook" }, + orderBytes: "0xbytesA", + outputs: [ + { + token: { address: "0xoutput", symbol: "OUT", decimals: "18" }, + balance: + "0xffffffee00000000000000000000000000000000000000000000000000000001", + }, + ], + inputs: [ + { + token: { address: "0xinput", symbol: "IN", decimals: "18" }, + balance: + "0xffffffee00000000000000000000000000000000000000000000000000000001", + }, + ], + }; + const orderB = { + __version: SubgraphVersions.V6, + orderHash: "0xhashB", + orderbook: { id: "0xorderbook" }, + orderBytes: "0xbytesB", + outputs: [ + { + token: { address: "0xinput", symbol: "IN", decimals: "18" }, + balance: + "0xffffffee00000000000000000000000000000000000000000000000000000001", + }, + ], + inputs: [ + { + token: { address: "0xbasetoken", symbol: "BASE", decimals: "18" }, + balance: + "0xffffffee00000000000000000000000000000000000000000000000000000001", + }, + ], + }; + + await orderManager.addOrder(orderA as any); + await orderManager.addOrder(orderB as any); + + const pairA = getPair("0xorderbook", "0xhashA", "0xoutput", "0xinput"); + + const result = orderManager.getCounterpartyOrdersAgainstBaseTokens(pairA); + + expect(result).toBeInstanceOf(Map); + expect(result.size).toBe(0); + }); + it("should skip non-base tokens", async () => { const mockBaseToken = { address: "0xbasetoken", symbol: "BASE", decimals: 18 }; (BASES_TO_CHECK_TRADES_AGAINST as any)[state.chainConfig.id] = [mockBaseToken as any]; diff --git a/src/order/index.ts b/src/order/index.ts index 565d45fc..73b03f1a 100644 --- a/src/order/index.ts +++ b/src/order/index.ts @@ -588,7 +588,7 @@ export class OrderManager { // skip mirrored order pairs and pairs with middle token that is not in routing base tokens if ( tkn === sellToken || - BASES_TO_CHECK_TRADES_AGAINST[this.state.chainConfig.id].every( + (BASES_TO_CHECK_TRADES_AGAINST[this.state.chainConfig.id] ?? []).every( (baseToken) => baseToken.address.toLowerCase() !== tkn, ) ) { diff --git a/src/state/contracts.test.ts b/src/state/contracts.test.ts index 46794278..e77fa0eb 100644 --- a/src/state/contracts.test.ts +++ b/src/state/contracts.test.ts @@ -608,6 +608,55 @@ describe("resolveVersionContracts", () => { }); }); + it("should prefer configured interpreter/store over onchain reads", async () => { + const mockAddresses = { + dispair: "0xdispairAddress" as `0x${string}`, + interpreter: "0xconfiguredInterpreter" as `0x${string}`, + store: "0xconfiguredStore" as `0x${string}`, + sushiArb: "0xsushiArbAddress" as `0x${string}`, + }; + + const result = await resolveVersionContracts(mockClient, mockAddresses, "v6"); + + expect(result).toEqual({ + dispair: { + deployer: "0xdispairAddress", + interpreter: "0xconfiguredInterpreter", + store: "0xconfiguredStore", + }, + sushiArb: "0xsushiArbAddress", + }); + // the RaindexV6 deployer only exposes parse2, so it must not be read at all + expect(mockClient.readContract).not.toHaveBeenCalled(); + }); + + it("should still read onchain for whichever of interpreter/store is not configured", async () => { + const mockAddresses = { + dispair: "0xdispairAddress" as `0x${string}`, + interpreter: "0xconfiguredInterpreter" as `0x${string}`, + sushiArb: "0xsushiArbAddress" as `0x${string}`, + }; + + mockClient.readContract.mockResolvedValueOnce("0xstoreAddress" as `0x${string}`); + + const result = await resolveVersionContracts(mockClient, mockAddresses, "v6"); + + expect(result).toEqual({ + dispair: { + deployer: "0xdispairAddress", + interpreter: "0xconfiguredInterpreter", + store: "0xstoreAddress", + }, + sushiArb: "0xsushiArbAddress", + }); + expect(mockClient.readContract).toHaveBeenCalledTimes(1); + expect(mockClient.readContract).toHaveBeenCalledWith({ + address: "0xdispairAddress", + functionName: "I_STORE", + abi: ABI.Deployer.Primary.DeployerV6, + }); + }); + it("should resolve version contracts for v6 with all addresses when all contract calls succeed", async () => { const mockAddresses = { dispair: "0xdispairAddress" as `0x${string}`, diff --git a/src/state/contracts.ts b/src/state/contracts.ts index db152d94..b6b90c58 100644 --- a/src/state/contracts.ts +++ b/src/state/contracts.ts @@ -79,24 +79,37 @@ export async function resolveVersionContracts( return undefined; } - const interpreter = await client - .readContract({ - address: addresses.dispair, - functionName: version === "v6" ? "I_INTERPRETER" : "iInterpreter", - abi: version === "v6" ? ABI.Deployer.Primary.DeployerV6 : ABI.Deployer.Primary.Deployer, - }) - .catch(() => undefined); + // Not every expression deployer exposes its interpreter and store onchain; the + // RaindexV6-era deployers, for instance, only expose parse2. Prefer the + // configured overrides when they are given and only read onchain otherwise. + const interpreter = + addresses.interpreter ?? + (await client + .readContract({ + address: addresses.dispair, + functionName: version === "v6" ? "I_INTERPRETER" : "iInterpreter", + abi: + version === "v6" + ? ABI.Deployer.Primary.DeployerV6 + : ABI.Deployer.Primary.Deployer, + }) + .catch(() => undefined)); if (!interpreter) { return undefined; } - const store = await client - .readContract({ - address: addresses.dispair, - functionName: version === "v6" ? "I_STORE" : "iStore", - abi: version === "v6" ? ABI.Deployer.Primary.DeployerV6 : ABI.Deployer.Primary.Deployer, - }) - .catch(() => undefined); + const store = + addresses.store ?? + (await client + .readContract({ + address: addresses.dispair, + functionName: version === "v6" ? "I_STORE" : "iStore", + abi: + version === "v6" + ? ABI.Deployer.Primary.DeployerV6 + : ABI.Deployer.Primary.Deployer, + }) + .catch(() => undefined)); if (!store) { return undefined; } diff --git a/src/subgraph/index.ts b/src/subgraph/index.ts index 8b5abbf6..ee03553a 100644 --- a/src/subgraph/index.ts +++ b/src/subgraph/index.ts @@ -7,7 +7,7 @@ import { PreAssembledSpan } from "../logger"; import { SpanStatusCode } from "@opentelemetry/api"; import { ErrorSeverity, errorSnapshot } from "../error"; import { SgOrder, SgTransaction, SubgraphSyncState, SubgraphVersions } from "./types"; -import { getTxsQuery, orderbooksQuery, DEFAULT_PAGE_SIZE, getQueryPaginated } from "./query"; +import { getTxsQuery, getOrderbooksQuery, DEFAULT_PAGE_SIZE, getQueryPaginated } from "./query"; // re-export export * from "./types"; @@ -70,7 +70,11 @@ export class SubgraphManager { */ async getOrderbooks(): Promise> { const promises = this.subgraphs.map((url) => - axios.post(url, { query: orderbooksQuery }, { headers, timeout: this.requestTimeout }), + axios.post( + url, + { query: getOrderbooksQuery(this.getSubgraphVersion(url)) }, + { headers, timeout: this.requestTimeout }, + ), ); const queryResults = await Promise.allSettled(promises); const addresses = queryResults.flatMap( @@ -152,7 +156,7 @@ export class SubgraphManager { const res = await axios.post( url, { - query: getQueryPaginated(skip, this.filters), + query: getQueryPaginated(skip, this.filters, version), }, { headers, timeout: this.requestTimeout }, ); @@ -234,7 +238,14 @@ export class SubgraphManager { try { const res = await axios.post( url, - { query: getTxsQuery(startTimestamp, this.syncState[url].skip) }, + { + query: getTxsQuery( + startTimestamp, + this.syncState[url].skip, + undefined, + this.getSubgraphVersion(url), + ), + }, { headers, timeout: this.requestTimeout }, ); if (typeof res?.data?.data?.transactions !== "undefined") { diff --git a/src/subgraph/query.test.ts b/src/subgraph/query.test.ts index 7e26fa5c..362a3cec 100644 --- a/src/subgraph/query.test.ts +++ b/src/subgraph/query.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import { getQueryPaginated, getTxsQuery, DEFAULT_PAGE_SIZE } from "./query"; +import { getOrderbooksQuery, getQueryPaginated, getTxsQuery, DEFAULT_PAGE_SIZE } from "./query"; +import { SubgraphVersions } from "./types"; describe("Test getQueryPaginated", () => { it("should generate query with no filters", () => { @@ -27,6 +28,42 @@ describe("Test getQueryPaginated", () => { expect(query).toContain('orderbook_not_in: ["0xnotbook"]'); expect(query).toContain("skip: 10"); }); + + it("should select the legacy orderbook entity by default", () => { + const query = getQueryPaginated(0, undefined, SubgraphVersions.LEGACY); + expect(query).toContain("orderbook {"); + expect(query).not.toContain("raindex"); + }); + + it("should alias the v6 raindex entity as orderbook", () => { + const query = getQueryPaginated(0, undefined, SubgraphVersions.V6); + expect(query).toContain("orderbook: raindex {"); + }); + + it("should use the raindex filter key on v6", () => { + const filters = { + includeOrderbooks: new Set(["0xbook"]), + excludeOrderbooks: new Set(["0xnotbook"]), + }; + const query = getQueryPaginated(0, filters, SubgraphVersions.V6); + expect(query).toContain('raindex_in: ["0xbook"]'); + expect(query).toContain('raindex_not_in: ["0xnotbook"]'); + expect(query).not.toContain("orderbook_in"); + expect(query).not.toContain("orderbook_not_in"); + }); +}); + +describe("Test getOrderbooksQuery", () => { + it("should query orderbooks on legacy", () => { + const query = getOrderbooksQuery(SubgraphVersions.LEGACY); + expect(query).toContain("orderbooks {"); + expect(query).not.toContain("raindices"); + }); + + it("should alias raindices as orderbooks on v6", () => { + const query = getOrderbooksQuery(SubgraphVersions.V6); + expect(query).toContain("orderbooks: raindices {"); + }); }); describe("Test getTxsQuery", () => { @@ -36,4 +73,16 @@ describe("Test getTxsQuery", () => { expect(query).toContain("skip: 20"); expect(query).toContain(`first: ${DEFAULT_PAGE_SIZE}`); }); + + it("should select the legacy orderbook entity by default", () => { + const query = getTxsQuery(123456, 20); + expect(query).toContain("orderbook {"); + expect(query).not.toContain("raindex"); + }); + + it("should alias the v6 raindex entity as orderbook", () => { + const query = getTxsQuery(123456, 20, undefined, SubgraphVersions.V6); + expect(query).toContain("orderbook: raindex {"); + expect(query).not.toContain("\n orderbook {"); + }); }); diff --git a/src/subgraph/query.ts b/src/subgraph/query.ts index bf648c88..3e682147 100644 --- a/src/subgraph/query.ts +++ b/src/subgraph/query.ts @@ -1,14 +1,31 @@ import { SgFilter } from "./filter"; +import { SubgraphVersions } from "./types"; export const DEFAULT_PAGE_SIZE = 1000 as const; +/** + * The orderbook entity was renamed to `raindex` in the v6 subgraph schema, so v6 + * queries select it under an `orderbook` alias. That keeps the response shape + * identical across versions and leaves every consumer of `SgOrder`/`SgTransaction` + * untouched. + * @param version - The subgraph schema version being queried + */ +export function orderbookField(version: SubgraphVersions): string { + return version === SubgraphVersions.V6 ? "orderbook: raindex" : "orderbook"; +} + /** * Method to get the subgraph query body for order details with optional filters * @param skip - Number of results to skip * @param filters - Applies the filters for query * @returns the query string */ -export function getQueryPaginated(skip: number, filters?: SgFilter): string { +export function getQueryPaginated( + skip: number, + filters?: SgFilter, + version: SubgraphVersions = SubgraphVersions.LEGACY, +): string { + const orderbook = orderbookField(version); const getFilterVar = (header: string, f?: Set) => f ? `${header}: [${[...f].map((v) => `"${v.toLowerCase()}"`).join(", ")}], ` : ""; @@ -16,8 +33,13 @@ export function getQueryPaginated(skip: number, filters?: SgFilter): string { const exOwnerFilter = getFilterVar("owner_not_in", filters?.excludeOwners); const incOrderFilter = getFilterVar("orderHash_in", filters?.includeOrders); const exOrderFilter = getFilterVar("orderHash_not_in", filters?.excludeOrders); - const incOrderbookFilter = getFilterVar("orderbook_in", filters?.includeOrderbooks); - const exOrderbookFilter = getFilterVar("orderbook_not_in", filters?.excludeOrderbooks); + // the orderbook entity is also named `raindex` on the filter input in v6 + const orderbookFilterKey = version === SubgraphVersions.V6 ? "raindex" : "orderbook"; + const incOrderbookFilter = getFilterVar(`${orderbookFilterKey}_in`, filters?.includeOrderbooks); + const exOrderbookFilter = getFilterVar( + `${orderbookFilterKey}_not_in`, + filters?.excludeOrderbooks, + ); return `{ orders( @@ -42,7 +64,7 @@ export function getQueryPaginated(skip: number, filters?: SgFilter): string { meta active nonce - orderbook { + ${orderbook} { id } inputs { @@ -67,8 +89,8 @@ export function getQueryPaginated(skip: number, filters?: SgFilter): string { }`; } -export const orderbooksQuery = `{ - orderbooks { +export const getOrderbooksQuery = (version: SubgraphVersions = SubgraphVersions.LEGACY) => `{ + ${version === SubgraphVersions.V6 ? "orderbooks: raindices" : "orderbooks"} { id } }`; @@ -88,7 +110,13 @@ export const statusCheckQuery = `{ * @param skip - Skips the first number of results * @param endTimestamp - (optional) The timestamp to end query at */ -export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: number) => { +export const getTxsQuery = ( + startTimestamp: number, + skip: number, + endTimestamp?: number, + version: SubgraphVersions = SubgraphVersions.LEGACY, +) => { + const orderbook = orderbookField(version); const endTimestampClause = typeof endTimestamp === "number" ? `timestamp_lte: "${endTimestamp}"` : ""; return `{transactions( @@ -112,7 +140,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: meta active nonce - orderbook { + ${orderbook} { id } inputs { @@ -147,7 +175,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: meta active nonce - orderbook { + ${orderbook} { id } inputs { @@ -183,7 +211,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: symbol } } - orderbook { + ${orderbook} { id } } @@ -200,7 +228,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: symbol } } - orderbook { + ${orderbook} { id } } @@ -219,7 +247,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: symbol } } - orderbook { + ${orderbook} { id } } @@ -236,7 +264,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?: symbol } } - orderbook { + ${orderbook} { id } }