Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/oracle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ethers } from "ethers";
import { ABI } from "../common";

/**
* Extract oracle URL from order meta bytes.
*
* TODO: Replace with SDK's RaindexOrder.extractOracleUrl() once the wasm
* package includes it. For now, returns null (stub).
*
* @param metaHex - Hex string of meta bytes (e.g. "0x1234...")
* @returns Oracle URL if found, null otherwise
*/
export function extractOracleUrl(metaHex: string): string | null {
// TODO: Implement CBOR decoding to find RaindexSignedContextOracleV1
// magic number 0xff7a1507ba4419ca and extract URL.
// Pending SDK update — see rain.orderbook PR #2478.
return null;
}

/**
* Signed context response from oracle endpoint.
* Maps directly to SignedContextV1 in the orderbook contract.
*/
export interface SignedContextV1 {
signer: string;
context: string[];
signature: string;
}

/**
* Order details for an oracle request entry.
*/
export interface OracleOrderRequest {
order: {
owner: string;
evaluable: { interpreter: string; store: string; bytecode: string };
validInputs: { token: string; vaultId: string }[];
validOutputs: { token: string; vaultId: string }[];
nonce: string;
};
inputIOIndex: number;
outputIOIndex: number;
counterparty: string;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/** Oracle request timeout in ms */
const ORACLE_TIMEOUT_MS = 5_000;

/**
* ABI type string for the batch oracle request body:
* abi.encode((OrderV4, uint256, uint256, address)[])
*/
const OracleRequestTupleType =
`tuple(${ABI.Orderbook.V5.OrderV4} order, uint256 inputIOIndex, uint256 outputIOIndex, address counterparty)[]` as const;

/**
* Fetch signed contexts from an oracle endpoint (batch format).
*
* POSTs abi.encode((OrderV4, uint256, uint256, address)[]) and expects
* a JSON array of SignedContextV1 objects back, matching request length.
*
* @param url - Oracle endpoint URL
* @param orders - Array of order requests (usually 1 per IO pair)
* @returns Array of signed contexts in the same order as the request
*/
export async function fetchSignedContext(
url: string,
orders: OracleOrderRequest[],
): Promise<SignedContextV1[]> {
const tuples = orders.map((req) => [
req.order,
req.inputIOIndex,
req.outputIOIndex,
req.counterparty,
]);

const body = ethers.utils.defaultAbiCoder.encode([OracleRequestTupleType], [tuples]);

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ORACLE_TIMEOUT_MS);

let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: ethers.utils.arrayify(body),
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}

if (!response.ok) {
throw new Error(`Oracle request failed: ${response.status} ${response.statusText}`);
}

const json: unknown = await response.json();

if (!Array.isArray(json)) {
throw new Error("Oracle response must be an array");
}

if (json.length !== orders.length) {
throw new Error(
`Oracle response length (${json.length}) does not match request length (${orders.length})`,
);
}

// Validate shape of each entry
const contexts: SignedContextV1[] = json.map((entry: unknown, i: number) => {
if (
typeof entry !== "object" ||
entry === null ||
typeof (entry as any).signer !== "string" ||
!Array.isArray((entry as any).context) ||
typeof (entry as any).signature !== "string"
) {
throw new Error(`Oracle response[${i}] is not a valid SignedContextV1`);
}
return entry as SignedContextV1;
});
Comment thread
rouzwelt marked this conversation as resolved.
Outdated

return contexts;
}
37 changes: 37 additions & 0 deletions src/order/quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ import { AppOptions } from "../config";
import { ABI, normalizeFloat } from "../common";
import { BundledOrders, Pair, TakeOrder } from "./types";
import { decodeFunctionResult, encodeFunctionData, PublicClient } from "viem";
import { extractOracleUrl, fetchSignedContext } from "../oracle";

/**
* If the order has oracle metadata, fetch signed context and inject it
* into the takeOrder struct. Failures are swallowed so quoting proceeds
* with empty signed context.
*/
async function fetchOracleContext(orderDetails: Pair): Promise<void> {
const orderMeta = (orderDetails as any).meta;
if (!orderMeta) return;

const oracleUrl = extractOracleUrl(orderMeta);
if (!oracleUrl) return;

const signedContexts = await fetchSignedContext(oracleUrl, [
{
order: orderDetails.takeOrder.struct.order,
inputIOIndex: orderDetails.takeOrder.struct.inputIOIndex,
outputIOIndex: orderDetails.takeOrder.struct.outputIOIndex,
counterparty: "0x0000000000000000000000000000000000000000",
},
]);

orderDetails.takeOrder.struct.signedContext = signedContexts;
}

/**
* Quotes a single order
Expand Down Expand Up @@ -38,6 +63,12 @@ export async function quoteSingleOrderV3(
blockNumber?: bigint,
gas?: bigint,
) {
try {
await fetchOracleContext(orderDetails);
} catch (error) {
console.warn("Failed to fetch oracle context:", error);
}

const { data } = await viemClient
.call({
to: orderDetails.orderbook as `0x${string}`,
Expand Down Expand Up @@ -82,6 +113,12 @@ export async function quoteSingleOrderV4(
blockNumber?: bigint,
gas?: bigint,
) {
try {
await fetchOracleContext(orderDetails);
} catch (error) {
console.warn("Failed to fetch oracle context:", error);
}

const { data } = await viemClient
.call({
to: orderDetails.orderbook as `0x${string}`,
Expand Down
3 changes: 3 additions & 0 deletions src/subgraph/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function getQueryPaginated(skip: number, filters?: SgFilter): string {
owner
orderHash
orderBytes
meta
active
nonce
orderbook {
Expand Down Expand Up @@ -108,6 +109,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?:
owner
orderHash
orderBytes
meta
active
nonce
orderbook {
Expand Down Expand Up @@ -142,6 +144,7 @@ export const getTxsQuery = (startTimestamp: number, skip: number, endTimestamp?:
owner
orderHash
orderBytes
meta
active
nonce
orderbook {
Expand Down
Loading