Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 26 additions & 4 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1243,14 +1243,36 @@ const result = await orderbook.createPriceAboveThresholdMarket({

#### `orderbook.getMarketInfo(queryId: number): Promise<MarketInfo>`

Gets detailed information about a market.
Gets detailed information about a market. Returns `MarketInfo` object containing `queryComponents` bytes.

```typescript
const market = await orderbook.getMarketInfo(queryId);
console.log(`Settle Time: ${new Date(market.settleTime * 1000)}`);
console.log(`Settled: ${market.settled}`);
if (market.settled) {
console.log(`Winner: ${market.winningOutcome ? "YES" : "NO"}`);
```

#### `decodeMarketData(encoded: string | Uint8Array): MarketData`

Decodes the `queryComponents` field from a `MarketInfo` object into high-level structured data.

#### Example
```typescript
import { decodeMarketData } from "@trufnetwork/sdk-js";

const market = await orderbook.getMarketInfo(123);
const details = decodeMarketData(market.queryComponents);

console.log(`Type: ${details.type}`); // e.g. "above"
console.log(`Thresholds: ${details.thresholds}`); // e.g. ["100000.0"]
```

#### `MarketData` Interface
```typescript
interface MarketData {
dataProvider: string;
streamId: string;
actionId: string;
type: "above" | "below" | "between" | "equals" | "unknown";
thresholds: string[]; // Formatted numeric values as strings
}
```

Expand Down
56 changes: 56 additions & 0 deletions examples/decode_market_example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { decodeMarketData } from "../../src/util/orderbookHelpers";
import { NodeTNClient } from "../../src/index.node";
import { Wallet } from "ethers";

async function main() {
console.log("--- Prediction Market Decoding Example (Real Data JS) ---");

const endpoint = "https://gateway.testnet.truf.network";
const privateKey = "0x0000000000000000000000000000000000000000000000000000000000000001";
const wallet = new Wallet(privateKey);

console.log(`Endpoint: ${endpoint}\n`);

// 1. Initialize Client
const client = new NodeTNClient({
endpoint,
signerInfo: {
address: wallet.address,
signer: wallet,
}
});

// 2. Load Orderbook
const orderbook = client.loadOrderbookAction();

// 3. List Latest Markets
console.log("Fetching latest markets...");
const markets = await orderbook.listMarkets({
limit: 3,
offset: 0
});

console.log(`Found ${markets.length} latest markets. Decoding details...\n`);

// 4. Fetch and Decode each market
for (const m of markets) {
console.log(`Processing Market ID: ${m.id}`);

try {
// Fetch full info (including queryComponents)
const marketInfo = await orderbook.getMarketInfo(m.id);

// Decode components
const details = decodeMarketData(marketInfo.queryComponents);

console.log(` Market Type: ${details.type}`);
console.log(` Thresholds: ${details.thresholds.join(", ")}`);
console.log(` Action: ${details.actionId}`);
console.log(` Stream: ${details.streamId}\n`);
} catch (e) {
console.error(` Error processing market ${m.id}:`, e);
}
}
}

main().catch(console.error);
120 changes: 120 additions & 0 deletions src/util/AttestationEncoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,94 @@ export function encodeActionArgs(args: any[], types?: Record<number, TypeHint>):
return buffer;
}

/**
* Decodes canonical bytes back into action arguments.
* This is the inverse of encodeActionArgs.
*
* @param data - Encoded argument bytes
* @returns Array of decoded values
*/
export function decodeActionArgs(data: Uint8Array): any[] {
if (data.length < 4) {
throw new Error('Data too short for arg count');
}

let offset = 0;
const argCount = readUint32LE(data, offset);
offset += 4;

const args: any[] = [];
for (let i = 0; i < argCount; i++) {
if (offset + 4 > data.length) {
throw new Error(`Data too short for arg ${i} length`);
}

const argLen = readUint32LE(data, offset);
offset += 4;

if (offset + argLen > data.length) {
throw new Error(`Data too short for arg ${i} bytes`);
}

const argBytes = data.slice(offset, offset + argLen);
const { value: decodedArg } = decodeEncodedValue(argBytes, 0);
args.push(decodedValueToJS(decodedArg));
offset += argLen;
}

return args;
}

/**
* Decodes ABI-encoded query_components tuple.
* Format: (address data_provider, bytes32 stream_id, string action_id, bytes args)
*
* @param encoded - ABI-encoded bytes
* @returns Object with decoded components
*/
export function decodeQueryComponents(encoded: Uint8Array): {
dataProvider: string;
streamId: string;
actionId: string;
args: Uint8Array;
} {
const abiCoder = AbiCoder.defaultAbiCoder();
const decoded = abiCoder.decode(
['address', 'bytes32', 'string', 'bytes'],
encoded
);

// Trim trailing null bytes from streamId (bytes32)
const streamIdBytes = hexToBytes(decoded[1]);
let lastNonZero = -1;
for (let i = 31; i >= 0; i--) {
if (streamIdBytes[i] !== 0) {
lastNonZero = i;
break;
}
}
const streamId = new TextDecoder().decode(streamIdBytes.slice(0, lastNonZero + 1));

return {
dataProvider: decoded[0].toLowerCase(),
streamId,
actionId: decoded[2],
args: hexToBytes(decoded[3]),
};
}

/**
* Helper to convert hex string to Uint8Array
*/
function hexToBytes(hex: string): Uint8Array {
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
const bytes = new Uint8Array(cleanHex.length / 2);
for (let i = 0; i < cleanHex.length; i += 2) {
bytes[i / 2] = parseInt(cleanHex.slice(i, i + 2), 16);
}
return bytes;
}

/**
* Writes a uint32 value in little-endian format
* Used for writing arg count and length prefixes
Expand Down Expand Up @@ -759,6 +847,38 @@ if (import.meta.vitest) {
});
});

describe('decodeActionArgs', () => {
it('should decode empty args', () => {
const original: any[] = [];
const encoded = encodeActionArgs(original);
const decoded = decodeActionArgs(encoded);
expect(decoded).toEqual(original);
});

it('should decode single string arg', () => {
const original = ['hello'];
const encoded = encodeActionArgs(original);
const decoded = decodeActionArgs(encoded);
expect(decoded).toEqual(original);
});

it('should decode single number arg', () => {
const original = [42];
const encoded = encodeActionArgs(original);
const decoded = decodeActionArgs(encoded);
expect(Number(decoded[0])).toBe(42);
});

it('should decode multiple args of different types', () => {
const original = ['hello', 42, true];
const encoded = encodeActionArgs(original);
const decoded = decodeActionArgs(encoded);
expect(decoded[0]).toBe('hello');
expect(Number(decoded[1])).toBe(42);
expect(decoded[2]).toBe(true);
});
});

describe('writeUint32LE and readUint32LE', () => {
it('should round-trip uint32 values', () => {
const buffer = new Uint8Array(4);
Expand Down
105 changes: 105 additions & 0 deletions src/util/orderbookHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,109 @@ describe("orderbookHelpers", () => {
expect(result.length).toBeGreaterThan(0);
});
});

describe("decodeMarketData", () => {
const importHelper = async () => {
const { decodeMarketData } = await import("./orderbookHelpers");
return { decodeMarketData };
};

it("should round-trip price_above_threshold", async () => {
const { decodeMarketData } = await importHelper();
const threshold = "100000.0";
const args = encodeActionArgs(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
1700000000,
threshold,
0
);

const encoded = encodeQueryComponents(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
"price_above_threshold",
args
);

const decoded = decodeMarketData(encoded);
expect(decoded.type).toBe("above");
expect(decoded.thresholds[0]).toBe(threshold);
expect(decoded.dataProvider).toBe(TEST_DATA_PROVIDER.toLowerCase());
expect(decoded.streamId).toBe(TEST_STREAM_ID);
});

it("should round-trip price_below_threshold", async () => {
const { decodeMarketData } = await importHelper();
const threshold = "4.5";
const args = encodeActionArgs(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
1700000000,
threshold,
0
);

const encoded = encodeQueryComponents(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
"price_below_threshold",
args
);

const decoded = decodeMarketData(encoded);
expect(decoded.type).toBe("below");
expect(decoded.thresholds[0]).toBe(threshold);
});

it("should round-trip value_in_range", async () => {
const { decodeMarketData } = await importHelper();
const min = "90000.0";
const max = "110000.0";
const args = encodeRangeActionArgs(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
1700000000,
min,
max,
0
);

const encoded = encodeQueryComponents(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
"value_in_range",
args
);

const decoded = decodeMarketData(encoded);
expect(decoded.type).toBe("between");
expect(decoded.thresholds).toEqual([min, max]);
});

it("should round-trip value_equals", async () => {
const { decodeMarketData } = await importHelper();
const target = "5.25";
const tolerance = "0.01";
const args = encodeEqualsActionArgs(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
1700000000,
target,
tolerance,
0
);

const encoded = encodeQueryComponents(
TEST_DATA_PROVIDER,
TEST_STREAM_ID,
"value_equals",
args
);

const decoded = decodeMarketData(encoded);
expect(decoded.type).toBe("equals");
expect(decoded.thresholds).toEqual([target, tolerance]);
});
});
});
Loading
Loading