@onride/agent 0.0.0-stage → 0.5.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +31 -0
- package/LICENSE +21 -0
- package/README.md +174 -2
- package/dist/account_balances.js +435 -0
- package/dist/account_reads.js +199 -0
- package/dist/agent/adapter.js +115 -0
- package/dist/agent/analytics.js +174 -0
- package/dist/agent/backend.js +237 -0
- package/dist/agent/card_hosts.js +9 -0
- package/dist/agent/contracts.js +276 -0
- package/dist/agent/conversational.js +86 -0
- package/dist/agent/core_bridge.js +1155 -0
- package/dist/agent/deposits.js +309 -0
- package/dist/agent/discovery.js +205 -0
- package/dist/agent/domain.js +507 -0
- package/dist/agent/firebase.js +34 -0
- package/dist/agent/funding.js +230 -0
- package/dist/agent/http.js +40 -0
- package/dist/agent/human.js +298 -0
- package/dist/agent/human_handoffs.js +39 -0
- package/dist/agent/ledger.js +137 -0
- package/dist/agent/mcp.js +245 -0
- package/dist/agent/order_rules.js +61 -0
- package/dist/agent/perps_account.js +41 -0
- package/dist/agent/prediction.js +607 -0
- package/dist/agent/prediction_equity_history.js +910 -0
- package/dist/agent/prediction_history.js +336 -0
- package/dist/agent/preferences.js +114 -0
- package/dist/agent/repository.js +106 -0
- package/dist/agent/runner.js +683 -0
- package/dist/agent/service.js +1347 -0
- package/dist/backend.js +6 -0
- package/dist/bridge.js +6 -0
- package/dist/cli/clients.js +99 -0
- package/dist/cli/commands.js +332 -0
- package/dist/cli/onboarding.js +53 -0
- package/dist/cli/session.js +224 -0
- package/dist/cli/setup.js +256 -0
- package/dist/cli.js +16 -0
- package/dist/guide.js +92 -0
- package/dist/http_security.js +79 -0
- package/dist/mcp-cli.js +7 -0
- package/dist/oauth.js +1032 -0
- package/dist/server.js +171 -0
- package/dist/start.js +18 -0
- package/dist/version.js +25 -0
- package/docs/confirmation-cards.md +118 -0
- package/docs/conversation-v050.md +30 -0
- package/docs/conversational-v17.md +48 -0
- package/docs/deployment.md +58 -0
- package/docs/perps-leverage.md +43 -0
- package/docs/prediction-history.md +28 -0
- package/docs/preferences-funding-v042.md +17 -0
- package/docs/ride-agent-migration.md +127 -0
- package/docs/v17-completion-checklist.md +31 -0
- package/docs/v17-rollout.md +44 -0
- package/docs/widget-account-v043.md +17 -0
- package/package.json +75 -4
- package/public/agent.html +2313 -0
- package/public/confirm.html +2373 -0
- package/public/fund.html +563 -0
- package/skills/ride/SKILL.md +18 -0
- package/skills/ride/references/commands.md +47 -0
- package/skills/ride/references/workflow.md +47 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { Interface, ZeroHash, ZeroAddress, solidityPackedKeccak256, } from "ethers";
|
|
2
|
+
import { D, now } from "./domain.js";
|
|
3
|
+
import { accountingEquity } from "./prediction_history.js";
|
|
4
|
+
const DAY = 86400;
|
|
5
|
+
const abi20 = new Interface([
|
|
6
|
+
"function balanceOf(address) view returns(uint256)",
|
|
7
|
+
"function transfer(address,uint256) returns(bool)",
|
|
8
|
+
"function transferFrom(address,address,uint256) returns(bool)",
|
|
9
|
+
"function VAULT() view returns(address)",
|
|
10
|
+
]);
|
|
11
|
+
const safe = new Interface([
|
|
12
|
+
"function execTransaction(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,bytes signatures) returns(bool)",
|
|
13
|
+
]);
|
|
14
|
+
const abi1155 = new Interface([
|
|
15
|
+
"function balanceOfBatch(address[],uint256[]) view returns(uint256[])",
|
|
16
|
+
"function payoutDenominator(bytes32) view returns(uint256)",
|
|
17
|
+
"function payoutNumerators(bytes32,uint256) view returns(uint256)",
|
|
18
|
+
"function getCollectionId(bytes32,bytes32,uint256) view returns(bytes32)",
|
|
19
|
+
]);
|
|
20
|
+
const negRisk = new Interface(["function wcol() view returns(address)"]);
|
|
21
|
+
// Polygon deployment in Polymarket's contract-security registry. Resolve its
|
|
22
|
+
// immutable wrapped collateral on-chain rather than assuming a token mapping.
|
|
23
|
+
const NEG_RISK_ADAPTER = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296";
|
|
24
|
+
const hex = (n) => `0x${n.toString(16)}`;
|
|
25
|
+
const lower = (s) => s.toLowerCase();
|
|
26
|
+
const valid = (n) => typeof n === "number" && Number.isFinite(n);
|
|
27
|
+
function check(ok, reason, context) {
|
|
28
|
+
if (!ok)
|
|
29
|
+
throw new Error(reason, { cause: context });
|
|
30
|
+
}
|
|
31
|
+
export function canonicalOutcomeSlot(token, collections, collaterals) {
|
|
32
|
+
const matches = collections.flatMap((collection, index) => collaterals.some((collateral) => BigInt(solidityPackedKeccak256(["address", "bytes32"], [collateral, collection])).toString() === token)
|
|
33
|
+
? [index]
|
|
34
|
+
: []);
|
|
35
|
+
check(matches.length === 1, "unverified_ctf_token_identity");
|
|
36
|
+
return matches[0];
|
|
37
|
+
}
|
|
38
|
+
// Only an exact ERC20 transfer, directly or in a Safe CALL, can fill a missing
|
|
39
|
+
// external funding row. Never infer funding from arbitrary contract calls.
|
|
40
|
+
export function explicitCollateralTransfer(tx, m) {
|
|
41
|
+
try {
|
|
42
|
+
if (m.kind !== "cash" || !m.from || !m.to || BigInt(tx.value) !== 0n)
|
|
43
|
+
return false;
|
|
44
|
+
const token = m.asset.slice(5);
|
|
45
|
+
let target = lower(tx.to), caller = lower(tx.from), input = tx.input;
|
|
46
|
+
if (target !== token) {
|
|
47
|
+
const call = safe.parseTransaction({ data: input });
|
|
48
|
+
if (!call ||
|
|
49
|
+
call.name !== "execTransaction" ||
|
|
50
|
+
call.args.operation !== 0n ||
|
|
51
|
+
call.args.value !== 0n)
|
|
52
|
+
return false;
|
|
53
|
+
caller = target;
|
|
54
|
+
target = lower(call.args.to);
|
|
55
|
+
input = call.args.data;
|
|
56
|
+
}
|
|
57
|
+
if (target !== token)
|
|
58
|
+
return false;
|
|
59
|
+
const call = abi20.parseTransaction({ data: input });
|
|
60
|
+
if (!call || !["transfer", "transferFrom"].includes(call.name))
|
|
61
|
+
return false;
|
|
62
|
+
const from = call.name === "transfer" ? caller : lower(call.args[0]);
|
|
63
|
+
const to = lower(call.args[call.name === "transfer" ? 0 : 1]);
|
|
64
|
+
const amount = BigInt(call.args[call.name === "transfer" ? 1 : 2]);
|
|
65
|
+
return (from === lower(m.from) &&
|
|
66
|
+
to === lower(m.to) &&
|
|
67
|
+
amount === (m.amount < 0n ? -m.amount : m.amount));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function collateralConversion(movements, wallet, pusd, usdce, vault = pusd) {
|
|
74
|
+
const legs = movements.filter((m) => m.amount !== 0n);
|
|
75
|
+
if (legs.length !== 2 ||
|
|
76
|
+
legs.some((m) => m.kind !== "cash") ||
|
|
77
|
+
legs[0].hash !== legs[1].hash ||
|
|
78
|
+
legs[0].block !== legs[1].block)
|
|
79
|
+
return false;
|
|
80
|
+
const p = legs.find((m) => m.asset === `cash:${lower(pusd)}`), u = legs.find((m) => m.asset === `cash:${lower(usdce)}`);
|
|
81
|
+
if (!p || !u || p.amount + u.amount !== 0n)
|
|
82
|
+
return false;
|
|
83
|
+
if (p.amount > 0n)
|
|
84
|
+
return (p.from === ZeroAddress &&
|
|
85
|
+
p.to === lower(wallet) &&
|
|
86
|
+
u.from === lower(wallet) &&
|
|
87
|
+
u.to === lower(pusd));
|
|
88
|
+
return (p.from === lower(wallet) &&
|
|
89
|
+
(p.to === ZeroAddress || p.to === lower(pusd)) &&
|
|
90
|
+
u.from === lower(vault) &&
|
|
91
|
+
u.to === lower(wallet));
|
|
92
|
+
}
|
|
93
|
+
// A bucket's timestamp is its START, not its closing mark. Never use prices
|
|
94
|
+
// observed after the valuation instant, nor extrapolate a sparse quote forever.
|
|
95
|
+
export function historicalMark(points, at, settlement) {
|
|
96
|
+
if (settlement && settlement.at <= at)
|
|
97
|
+
return settlement.price;
|
|
98
|
+
const eligible = points.filter((p) => p.timestamp + p.resolution_seconds <= at &&
|
|
99
|
+
p.resolution_seconds <= 10800 &&
|
|
100
|
+
(!settlement || p.timestamp + p.resolution_seconds < settlement.at));
|
|
101
|
+
const p = eligible.at(-1);
|
|
102
|
+
check(p &&
|
|
103
|
+
at - (p.timestamp + p.resolution_seconds) <= 6 * 3600 &&
|
|
104
|
+
valid(p.price) &&
|
|
105
|
+
p.price >= 0 &&
|
|
106
|
+
p.price <= 1, "missing_historical_price");
|
|
107
|
+
return p.price;
|
|
108
|
+
}
|
|
109
|
+
export function inventoryAt(current, movements, at) {
|
|
110
|
+
const inventory = new Map(current);
|
|
111
|
+
for (const m of movements)
|
|
112
|
+
if (m.at > at)
|
|
113
|
+
inventory.set(m.asset, (inventory.get(m.asset) ?? 0n) - m.amount);
|
|
114
|
+
check([...inventory.values()].every((n) => n >= 0n), "incomplete_transfer_inventory");
|
|
115
|
+
return inventory;
|
|
116
|
+
}
|
|
117
|
+
export function equityMetrics(points, funding, provenance) {
|
|
118
|
+
check(points.length >= 29 && points.at(-1).at - points[0].at >= 28 * DAY, "insufficient_equity_history");
|
|
119
|
+
let nav = D(1), peak = D(1), dd = D(0);
|
|
120
|
+
for (let i = 0; i < points.length; i++) {
|
|
121
|
+
const p = points[i];
|
|
122
|
+
check(Number.isSafeInteger(p.at) && D(p.equity).gt(0), "nonpositive_historical_equity");
|
|
123
|
+
if (!i)
|
|
124
|
+
continue;
|
|
125
|
+
const previous = points[i - 1];
|
|
126
|
+
check(p.at > previous.at && p.at - previous.at <= 26 * 3600, "equity_history_gap");
|
|
127
|
+
const flows = funding.filter((f) => f.at > previous.at && f.at <= p.at);
|
|
128
|
+
check(flows.every((f) => Number.isSafeInteger(f.at) && D(f.amount).isFinite()), "invalid_funding_ledger");
|
|
129
|
+
const deposits = flows.reduce((n, f) => n.plus(D.max(0, f.amount)), D(0));
|
|
130
|
+
const withdrawals = flows.reduce((n, f) => n.plus(D.max(0, D(f.amount).negated())), D(0));
|
|
131
|
+
nav = nav
|
|
132
|
+
.times(D(p.equity).plus(withdrawals))
|
|
133
|
+
.div(D(previous.equity).plus(deposits));
|
|
134
|
+
peak = D.max(peak, nav);
|
|
135
|
+
dd = D.max(dd, D(1).minus(nav.div(peak)));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
source_roi_pct: nav.minus(1).times(100).toNumber(),
|
|
139
|
+
source_drawdown_pct: dd.times(100).toNumber(),
|
|
140
|
+
metric_window: {
|
|
141
|
+
start_at: points[0].at * 1000,
|
|
142
|
+
end_at: points.at(-1).at * 1000,
|
|
143
|
+
basis: "polygon_inventory_nav_1m_daily_v2",
|
|
144
|
+
},
|
|
145
|
+
metric_provenance: provenance,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Bound reconstruction work independently from recommendation request latency.
|
|
149
|
+
// A single instance is shared by the source roster, with two wallet jobs and
|
|
150
|
+
// six public-data requests in flight at most; completed metrics are persisted.
|
|
151
|
+
export class Gate {
|
|
152
|
+
maximum;
|
|
153
|
+
active = 0;
|
|
154
|
+
queue = [];
|
|
155
|
+
constructor(maximum) {
|
|
156
|
+
this.maximum = maximum;
|
|
157
|
+
}
|
|
158
|
+
async run(f) {
|
|
159
|
+
if (this.active >= this.maximum)
|
|
160
|
+
await new Promise((r) => this.queue.push(r));
|
|
161
|
+
this.active++;
|
|
162
|
+
try {
|
|
163
|
+
return await f();
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
this.active--;
|
|
167
|
+
this.queue.shift()?.();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export class PredictionEquityHistory {
|
|
172
|
+
rpcUrl;
|
|
173
|
+
contracts;
|
|
174
|
+
httpGate = new Gate(6);
|
|
175
|
+
blockStarts = new Map();
|
|
176
|
+
prices = new Map();
|
|
177
|
+
constructor(rpcUrl, contracts) {
|
|
178
|
+
this.rpcUrl = rpcUrl;
|
|
179
|
+
this.contracts = contracts;
|
|
180
|
+
}
|
|
181
|
+
async load(wallet) {
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timer = setTimeout(() => controller.abort(new Error("equity_history_timeout")), 300_000);
|
|
184
|
+
try {
|
|
185
|
+
return await this.reconstruct(wallet, controller.signal);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
controller.abort();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async reconstruct(wallet, signal) {
|
|
193
|
+
check(/^0x[0-9a-f]{40}$/i.test(wallet), "invalid_history_identity");
|
|
194
|
+
const read = async (url, body) => this.httpGate.run(async () => {
|
|
195
|
+
signal.throwIfAborted();
|
|
196
|
+
const r = await fetch(url, {
|
|
197
|
+
signal,
|
|
198
|
+
redirect: "error",
|
|
199
|
+
...(body
|
|
200
|
+
? {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "content-type": "application/json" },
|
|
203
|
+
body: JSON.stringify(body),
|
|
204
|
+
}
|
|
205
|
+
: {}),
|
|
206
|
+
});
|
|
207
|
+
check(r.ok && r.body, "equity_source_unavailable");
|
|
208
|
+
let size = 0;
|
|
209
|
+
const chunks = [];
|
|
210
|
+
for await (const chunk of r.body) {
|
|
211
|
+
size += chunk.length;
|
|
212
|
+
check(size <= 8 * 1024 * 1024, "history_response_too_large");
|
|
213
|
+
chunks.push(chunk);
|
|
214
|
+
}
|
|
215
|
+
return Buffer.concat(chunks);
|
|
216
|
+
});
|
|
217
|
+
let rpcId = 0;
|
|
218
|
+
const callQueue = [];
|
|
219
|
+
let batchScheduled = false;
|
|
220
|
+
const flushCalls = async () => {
|
|
221
|
+
const batch = callQueue.splice(0, 20);
|
|
222
|
+
try {
|
|
223
|
+
const response = JSON.parse((await read(this.rpcUrl, batch.map((x) => x.payload))).toString());
|
|
224
|
+
check(Array.isArray(response) && response.length === batch.length, "incomplete_archive_rpc_batch");
|
|
225
|
+
for (const entry of batch) {
|
|
226
|
+
const result = response.find((x) => x.id === entry.id);
|
|
227
|
+
if (!result || result.error || result.result == null)
|
|
228
|
+
entry.reject(new Error("archive_rpc_unavailable"));
|
|
229
|
+
else
|
|
230
|
+
entry.resolve(result.result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
batch.forEach((entry) => entry.reject(e));
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
if (callQueue.length)
|
|
238
|
+
setTimeout(() => void flushCalls(), 10);
|
|
239
|
+
else
|
|
240
|
+
batchScheduled = false;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const rpc = async (method, params) => {
|
|
244
|
+
const id = ++rpcId;
|
|
245
|
+
if (["eth_call", "eth_getTransactionByHash"].includes(method))
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
callQueue.push({
|
|
248
|
+
payload: { jsonrpc: "2.0", id, method, params },
|
|
249
|
+
id,
|
|
250
|
+
resolve,
|
|
251
|
+
reject,
|
|
252
|
+
});
|
|
253
|
+
if (!batchScheduled) {
|
|
254
|
+
batchScheduled = true;
|
|
255
|
+
setTimeout(() => void flushCalls(), 10);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const result = JSON.parse((await read(this.rpcUrl, {
|
|
259
|
+
jsonrpc: "2.0",
|
|
260
|
+
id,
|
|
261
|
+
method,
|
|
262
|
+
params,
|
|
263
|
+
})).toString());
|
|
264
|
+
check(!result.error && result.result != null, "archive_rpc_unavailable");
|
|
265
|
+
return result.result;
|
|
266
|
+
};
|
|
267
|
+
const data = async (path, params) => {
|
|
268
|
+
const url = new URL(path, "https://data-api.polymarket.com");
|
|
269
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
270
|
+
return JSON.parse((await read(url)).toString());
|
|
271
|
+
};
|
|
272
|
+
const paged = async (path, params, maximum = 50) => {
|
|
273
|
+
const out = [], seen = new Set();
|
|
274
|
+
let cursor;
|
|
275
|
+
for (let i = 0; i < maximum; i++) {
|
|
276
|
+
const r = await data(path, {
|
|
277
|
+
...params,
|
|
278
|
+
limit: "1000",
|
|
279
|
+
...(cursor ? { cursor } : {}),
|
|
280
|
+
});
|
|
281
|
+
check(Array.isArray(r.data) &&
|
|
282
|
+
r.data.length <= 1000 &&
|
|
283
|
+
typeof r.pagination?.has_more === "boolean", "incomplete_equity_source_pages");
|
|
284
|
+
out.push(...r.data);
|
|
285
|
+
if (!r.pagination.has_more) {
|
|
286
|
+
check(r.pagination.next_cursor === null, "incomplete_equity_source_pages");
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
cursor = r.pagination.next_cursor;
|
|
290
|
+
check(cursor && !seen.has(cursor) && r.data.length, "incomplete_equity_source_pages");
|
|
291
|
+
seen.add(cursor);
|
|
292
|
+
}
|
|
293
|
+
throw new Error("equity_source_page_limit");
|
|
294
|
+
};
|
|
295
|
+
const c = await this.contracts();
|
|
296
|
+
check(Number(BigInt(await rpc("eth_chainId", []))) === 137, "wrong_history_network");
|
|
297
|
+
const head = Number(BigInt(await rpc("eth_blockNumber", []))) - 6;
|
|
298
|
+
const block = await rpc("eth_getBlockByNumber", [hex(head), false]);
|
|
299
|
+
const end = Number(BigInt(block.timestamp)), start = Math.floor(end / DAY) * DAY - 29 * DAY;
|
|
300
|
+
check(now() / 1000 - end < 300, "stale_inventory_anchor");
|
|
301
|
+
let startPromise = this.blockStarts.get(start);
|
|
302
|
+
if (!startPromise) {
|
|
303
|
+
startPromise = (async () => {
|
|
304
|
+
let lo = Math.max(1, head - 40 * DAY), hi = head;
|
|
305
|
+
let lt = Number(BigInt((await rpc("eth_getBlockByNumber", [hex(lo), false])).timestamp)), ht = end;
|
|
306
|
+
check(lt < start, "history_block_range_unavailable");
|
|
307
|
+
for (let n = 0; hi - lo > 1 && n < 40; n++) {
|
|
308
|
+
const mid = Math.max(lo + 1, Math.min(hi - 1, lo + Math.floor(((start - lt) * (hi - lo)) / (ht - lt))));
|
|
309
|
+
const mt = Number(BigInt((await rpc("eth_getBlockByNumber", [hex(mid), false])).timestamp));
|
|
310
|
+
if (mt <= start) {
|
|
311
|
+
lo = mid;
|
|
312
|
+
lt = mt;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
hi = mid;
|
|
316
|
+
ht = mt;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
check(hi - lo === 1 && lt <= start && ht > start, "history_block_range_unavailable");
|
|
320
|
+
return lo;
|
|
321
|
+
})();
|
|
322
|
+
this.blockStarts.set(start, startPromise);
|
|
323
|
+
startPromise.catch(() => this.blockStarts.delete(start));
|
|
324
|
+
if (this.blockStarts.size > 3)
|
|
325
|
+
this.blockStarts.delete(this.blockStarts.keys().next().value);
|
|
326
|
+
}
|
|
327
|
+
const startBlock = await startPromise;
|
|
328
|
+
const snapshotUrl = new URL("/v1/accounting/snapshot", "https://data-api.polymarket.com");
|
|
329
|
+
snapshotUrl.searchParams.set("user", wallet);
|
|
330
|
+
const [open, lost, activity, snapshotBytes] = await Promise.all([
|
|
331
|
+
paged("/v2/positions", {
|
|
332
|
+
user: wallet,
|
|
333
|
+
status: "OPEN",
|
|
334
|
+
include_archived: "true",
|
|
335
|
+
}),
|
|
336
|
+
paged("/v2/positions", { user: wallet, status: "REDEEMABLE_LOST" }),
|
|
337
|
+
paged("/v2/activity", {
|
|
338
|
+
user: wallet,
|
|
339
|
+
start: String(start),
|
|
340
|
+
end: String(end),
|
|
341
|
+
type: "TRADE,SPLIT,MERGE,REDEEM,REWARD,CONVERSION,DEPOSIT,WITHDRAWAL,MAKER_REBATE,TAKER_REBATE,REFERRAL_REWARD,YIELD,MIGRATION,TIP",
|
|
342
|
+
exclude_deposits_withdrawals: "false",
|
|
343
|
+
sort_direction: "ASC",
|
|
344
|
+
}),
|
|
345
|
+
read(snapshotUrl),
|
|
346
|
+
]);
|
|
347
|
+
const snapshot = accountingEquity(snapshotBytes);
|
|
348
|
+
check(Math.abs(snapshot.at / 1000 - end) <= 180, "stale_inventory_anchor");
|
|
349
|
+
const addresses = [
|
|
350
|
+
c.collateral,
|
|
351
|
+
c.wrapped_collateral,
|
|
352
|
+
c.conditional_tokens,
|
|
353
|
+
].map(lower);
|
|
354
|
+
const tokens = new Set();
|
|
355
|
+
for (const p of [...open, ...lost]) {
|
|
356
|
+
check(lower(p.proxy_wallet) === lower(wallet) &&
|
|
357
|
+
/^\d+$/.test(p.token_id) &&
|
|
358
|
+
valid(p.current_size) &&
|
|
359
|
+
p.current_size >= 0, "invalid_position_inventory");
|
|
360
|
+
if (p.current_size > 0)
|
|
361
|
+
tokens.add(p.token_id);
|
|
362
|
+
}
|
|
363
|
+
const movements = [], ids = new Map();
|
|
364
|
+
const transferPage = async (direction) => {
|
|
365
|
+
let pageKey;
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
for (let page = 0; page < 50; page++) {
|
|
368
|
+
const r = await rpc("alchemy_getAssetTransfers", [
|
|
369
|
+
{
|
|
370
|
+
fromBlock: hex(startBlock),
|
|
371
|
+
toBlock: hex(head),
|
|
372
|
+
[direction]: wallet,
|
|
373
|
+
contractAddresses: addresses,
|
|
374
|
+
category: ["erc20", "erc1155"],
|
|
375
|
+
maxCount: "0x3e8",
|
|
376
|
+
excludeZeroValue: false,
|
|
377
|
+
withMetadata: true,
|
|
378
|
+
order: "asc",
|
|
379
|
+
...(pageKey ? { pageKey } : {}),
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
check(Array.isArray(r.transfers) && r.transfers.length <= 1000, "incomplete_chain_transfer_pages");
|
|
383
|
+
for (const t of r.transfers) {
|
|
384
|
+
check(typeof t.uniqueId === "string" && typeof t.hash === "string", "invalid_chain_transfer");
|
|
385
|
+
const contract = lower(t.rawContract?.address ?? "");
|
|
386
|
+
check(addresses.includes(contract), "unexpected_inventory_contract");
|
|
387
|
+
const digest = JSON.stringify(t), previous = ids.get(t.uniqueId);
|
|
388
|
+
if (previous) {
|
|
389
|
+
check(previous === digest, "conflicting_chain_transfer");
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
ids.set(t.uniqueId, digest);
|
|
393
|
+
const at = Date.parse(t.metadata?.blockTimestamp) / 1000, number = Number(BigInt(t.blockNum));
|
|
394
|
+
check(Number.isSafeInteger(at) &&
|
|
395
|
+
number >= startBlock &&
|
|
396
|
+
number <= head &&
|
|
397
|
+
at <= end &&
|
|
398
|
+
(lower(t.from) === lower(wallet) ||
|
|
399
|
+
lower(t.to) === lower(wallet)), "invalid_chain_transfer");
|
|
400
|
+
const sign = BigInt(Number(lower(t.to) === lower(wallet)) -
|
|
401
|
+
Number(lower(t.from) === lower(wallet)));
|
|
402
|
+
if (t.category === "erc20") {
|
|
403
|
+
check(contract !== lower(c.conditional_tokens) &&
|
|
404
|
+
/^0x[0-9a-f]+$/i.test(t.rawContract.value), "invalid_cash_transfer");
|
|
405
|
+
movements.push({
|
|
406
|
+
at,
|
|
407
|
+
block: number,
|
|
408
|
+
asset: `cash:${contract}`,
|
|
409
|
+
amount: BigInt(t.rawContract.value) * sign,
|
|
410
|
+
hash: lower(t.hash),
|
|
411
|
+
kind: "cash",
|
|
412
|
+
from: lower(t.from),
|
|
413
|
+
to: lower(t.to),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
check(contract === lower(c.conditional_tokens) &&
|
|
418
|
+
Array.isArray(t.erc1155Metadata) &&
|
|
419
|
+
t.erc1155Metadata.length > 0, "unsupported_outcome_contract");
|
|
420
|
+
for (const entry of t.erc1155Metadata) {
|
|
421
|
+
const token = BigInt(entry.tokenId).toString(), amount = BigInt(entry.value) * sign;
|
|
422
|
+
if (!amount)
|
|
423
|
+
continue;
|
|
424
|
+
tokens.add(token);
|
|
425
|
+
movements.push({
|
|
426
|
+
at,
|
|
427
|
+
block: number,
|
|
428
|
+
asset: token,
|
|
429
|
+
amount,
|
|
430
|
+
hash: lower(t.hash),
|
|
431
|
+
kind: "outcome",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!r.pageKey)
|
|
437
|
+
return;
|
|
438
|
+
check(typeof r.pageKey === "string" &&
|
|
439
|
+
!seen.has(r.pageKey) &&
|
|
440
|
+
r.transfers.length > 0, "incomplete_chain_transfer_pages");
|
|
441
|
+
seen.add(r.pageKey);
|
|
442
|
+
pageKey = r.pageKey;
|
|
443
|
+
}
|
|
444
|
+
throw new Error("chain_transfer_page_limit");
|
|
445
|
+
};
|
|
446
|
+
await Promise.all([transferPage("fromAddress"), transferPage("toAddress")]);
|
|
447
|
+
check(tokens.size <= 2000, "historical_token_limit");
|
|
448
|
+
const tokenIds = [...tokens], transactions = new Set(activity.map((a) => lower(a.transaction_hash)));
|
|
449
|
+
const financialTypes = new Set([
|
|
450
|
+
"TRADE",
|
|
451
|
+
"SPLIT",
|
|
452
|
+
"MERGE",
|
|
453
|
+
"REDEEM",
|
|
454
|
+
"REWARD",
|
|
455
|
+
"CONVERSION",
|
|
456
|
+
"DEPOSIT",
|
|
457
|
+
"WITHDRAWAL",
|
|
458
|
+
"MAKER_REBATE",
|
|
459
|
+
"TAKER_REBATE",
|
|
460
|
+
"REFERRAL_REWARD",
|
|
461
|
+
"YIELD",
|
|
462
|
+
"MIGRATION",
|
|
463
|
+
"TIP",
|
|
464
|
+
]);
|
|
465
|
+
for (const a of activity)
|
|
466
|
+
check(lower(a.proxy_wallet) === lower(wallet) &&
|
|
467
|
+
financialTypes.has(a.type) &&
|
|
468
|
+
Number.isSafeInteger(a.timestamp) &&
|
|
469
|
+
a.timestamp >= start &&
|
|
470
|
+
a.timestamp <= end, "invalid_wallet_activity");
|
|
471
|
+
const additionalFunding = [], inspected = new Map(), conversions = new Set();
|
|
472
|
+
let collateralVault;
|
|
473
|
+
for (const m of movements.filter((m) => m.at > start && m.amount !== 0n && !transactions.has(m.hash))) {
|
|
474
|
+
if (conversions.has(m.hash))
|
|
475
|
+
continue;
|
|
476
|
+
const related = movements.filter((other) => other.hash === m.hash);
|
|
477
|
+
if (!collateralVault &&
|
|
478
|
+
related.length === 2 &&
|
|
479
|
+
related.every((leg) => leg.kind === "cash") &&
|
|
480
|
+
related[0].amount + related[1].amount === 0n)
|
|
481
|
+
collateralVault = abi20.decodeFunctionResult("VAULT", await rpc("eth_call", [
|
|
482
|
+
{ to: c.collateral, data: abi20.encodeFunctionData("VAULT") },
|
|
483
|
+
hex(head),
|
|
484
|
+
]))[0];
|
|
485
|
+
if (collateralConversion(related, wallet, c.collateral, c.wrapped_collateral, collateralVault)) {
|
|
486
|
+
conversions.add(m.hash);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
check(m.kind === "cash" &&
|
|
490
|
+
!movements.some((other) => other.hash === m.hash &&
|
|
491
|
+
other.kind === "outcome" &&
|
|
492
|
+
other.amount !== 0n), "unclassified_outcome_capital_transfer", m);
|
|
493
|
+
let tx = inspected.get(m.hash);
|
|
494
|
+
if (!tx) {
|
|
495
|
+
tx = await rpc("eth_getTransactionByHash", [m.hash]);
|
|
496
|
+
inspected.set(m.hash, tx);
|
|
497
|
+
}
|
|
498
|
+
check(explicitCollateralTransfer(tx, m), "unclassified_cash_transfer", m);
|
|
499
|
+
additionalFunding.push({
|
|
500
|
+
at: m.at,
|
|
501
|
+
amount: D(m.amount.toString()).div(1e6).toString(),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
const chainTransactions = new Set(movements.map((m) => m.hash));
|
|
505
|
+
for (const a of activity.filter((a) => ["TRADE", "DEPOSIT", "WITHDRAWAL"].includes(a.type) && a.usdc_size > 0))
|
|
506
|
+
check(chainTransactions.has(lower(a.transaction_hash)), "activity_missing_from_chain_transfers");
|
|
507
|
+
const funding = activity
|
|
508
|
+
.filter((a) => ["DEPOSIT", "WITHDRAWAL", "TIP"].includes(a.type))
|
|
509
|
+
.map((a) => {
|
|
510
|
+
check(valid(a.usdc_size) &&
|
|
511
|
+
a.usdc_size >= 0 &&
|
|
512
|
+
(a.type !== "TIP" || ["IN", "OUT"].includes(a.side)), "invalid_funding_ledger");
|
|
513
|
+
return {
|
|
514
|
+
at: a.timestamp,
|
|
515
|
+
amount: a.usdc_size *
|
|
516
|
+
(a.type === "WITHDRAWAL" || a.side === "OUT" ? -1 : 1),
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
funding.push(...additionalFunding);
|
|
520
|
+
const balances = async (atBlock) => {
|
|
521
|
+
const out = new Map();
|
|
522
|
+
await Promise.all(addresses.slice(0, 2).map(async (token) => {
|
|
523
|
+
const result = await rpc("eth_call", [
|
|
524
|
+
{
|
|
525
|
+
to: token,
|
|
526
|
+
data: abi20.encodeFunctionData("balanceOf", [wallet]),
|
|
527
|
+
},
|
|
528
|
+
hex(atBlock),
|
|
529
|
+
]);
|
|
530
|
+
out.set(`cash:${token}`, BigInt(abi20.decodeFunctionResult("balanceOf", result)[0]));
|
|
531
|
+
}));
|
|
532
|
+
for (let i = 0; i < tokenIds.length; i += 200) {
|
|
533
|
+
const ids = tokenIds.slice(i, i + 200);
|
|
534
|
+
const result = await rpc("eth_call", [
|
|
535
|
+
{
|
|
536
|
+
to: c.conditional_tokens,
|
|
537
|
+
data: abi1155.encodeFunctionData("balanceOfBatch", [
|
|
538
|
+
ids.map(() => wallet),
|
|
539
|
+
ids,
|
|
540
|
+
]),
|
|
541
|
+
},
|
|
542
|
+
hex(atBlock),
|
|
543
|
+
]);
|
|
544
|
+
const values = abi1155.decodeFunctionResult("balanceOfBatch", result)[0];
|
|
545
|
+
check(values.length === ids.length, "incomplete_inventory_balances");
|
|
546
|
+
ids.forEach((id, j) => out.set(id, BigInt(values[j])));
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
};
|
|
550
|
+
const [current, initial] = await Promise.all([
|
|
551
|
+
balances(head),
|
|
552
|
+
balances(startBlock),
|
|
553
|
+
]);
|
|
554
|
+
const reconstructedInitial = inventoryAt(current, movements.filter((m) => m.block > startBlock), start - 1);
|
|
555
|
+
check([...initial].every(([key, amount]) => reconstructedInitial.get(key) === amount), "chain_transfer_boundary_mismatch");
|
|
556
|
+
const times = Array.from({ length: 30 }, (_, i) => start + i * DAY);
|
|
557
|
+
if (end > times.at(-1))
|
|
558
|
+
times.push(end);
|
|
559
|
+
const inventories = times.map((at) => inventoryAt(current, movements, at));
|
|
560
|
+
const heldTokens = tokenIds.filter((id) => inventories.some((inventory) => inventory.get(id) > 0n));
|
|
561
|
+
const refs = new Map();
|
|
562
|
+
for (let i = 0; i < heldTokens.length; i += 50) {
|
|
563
|
+
const r = await data("/v2/tokens", {
|
|
564
|
+
token_id: heldTokens.slice(i, i + 50).join(","),
|
|
565
|
+
});
|
|
566
|
+
check(Array.isArray(r.data), "missing_token_metadata");
|
|
567
|
+
r.data.forEach((t) => refs.set(t.token_id, t));
|
|
568
|
+
}
|
|
569
|
+
check(heldTokens.every((id) => refs.get(id)?.module === "v1_ctf"), "unsupported_historical_token_module");
|
|
570
|
+
const resolutionHints = new Map(), hintTransactions = new Map();
|
|
571
|
+
const resolvedConditions = [
|
|
572
|
+
...new Set([...refs.values()]
|
|
573
|
+
.filter((ref) => ref.resolved)
|
|
574
|
+
.map((ref) => ref.condition_id)),
|
|
575
|
+
];
|
|
576
|
+
await Promise.all(Array.from({ length: Math.ceil(resolvedConditions.length / 20) }, async (_, index) => {
|
|
577
|
+
// Hints only accelerate locating a block. Both sides of the actual CTF
|
|
578
|
+
// payout transition are independently verified before a hint is accepted.
|
|
579
|
+
try {
|
|
580
|
+
const r = await data("/v2/resolutions", {
|
|
581
|
+
condition: resolvedConditions
|
|
582
|
+
.slice(index * 20, index * 20 + 20)
|
|
583
|
+
.join(","),
|
|
584
|
+
});
|
|
585
|
+
if (Array.isArray(r.data))
|
|
586
|
+
for (const row of r.data)
|
|
587
|
+
if (typeof row.condition_id === "string" &&
|
|
588
|
+
row.status === "resolved")
|
|
589
|
+
resolutionHints.set(lower(row.condition_id), row);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
/* The verified archive binary search remains available. */
|
|
593
|
+
}
|
|
594
|
+
}));
|
|
595
|
+
const settlementBlocks = new Map();
|
|
596
|
+
const collaterals = [c.collateral, c.wrapped_collateral];
|
|
597
|
+
if ([...refs.values()].some((ref) => ref.neg_risk === true)) {
|
|
598
|
+
const wrapped = negRisk.decodeFunctionResult("wcol", await rpc("eth_call", [
|
|
599
|
+
{ to: NEG_RISK_ADAPTER, data: negRisk.encodeFunctionData("wcol") },
|
|
600
|
+
hex(head),
|
|
601
|
+
]))[0];
|
|
602
|
+
collaterals.push(wrapped);
|
|
603
|
+
}
|
|
604
|
+
const collections = new Map();
|
|
605
|
+
const outcomeSlot = async (id, condition) => {
|
|
606
|
+
let result = collections.get(condition);
|
|
607
|
+
if (!result) {
|
|
608
|
+
result = Promise.all([0, 1].map(async (index) => abi1155.decodeFunctionResult("getCollectionId", await rpc("eth_call", [
|
|
609
|
+
{
|
|
610
|
+
to: c.conditional_tokens,
|
|
611
|
+
data: abi1155.encodeFunctionData("getCollectionId", [
|
|
612
|
+
ZeroHash,
|
|
613
|
+
condition,
|
|
614
|
+
1 << index,
|
|
615
|
+
]),
|
|
616
|
+
},
|
|
617
|
+
hex(head),
|
|
618
|
+
]))[0]));
|
|
619
|
+
collections.set(condition, result);
|
|
620
|
+
}
|
|
621
|
+
return canonicalOutcomeSlot(id, await result, collaterals);
|
|
622
|
+
};
|
|
623
|
+
const denominator = async (condition, number) => BigInt(abi1155.decodeFunctionResult("payoutDenominator", await rpc("eth_call", [
|
|
624
|
+
{
|
|
625
|
+
to: c.conditional_tokens,
|
|
626
|
+
data: abi1155.encodeFunctionData("payoutDenominator", [
|
|
627
|
+
condition,
|
|
628
|
+
]),
|
|
629
|
+
},
|
|
630
|
+
hex(number),
|
|
631
|
+
]))[0]);
|
|
632
|
+
const settlement = (condition) => {
|
|
633
|
+
let result = settlementBlocks.get(condition);
|
|
634
|
+
if (!result) {
|
|
635
|
+
result = (async () => {
|
|
636
|
+
const current = await denominator(condition, head);
|
|
637
|
+
if (current === 0n)
|
|
638
|
+
return null;
|
|
639
|
+
if ((await denominator(condition, startBlock)) > 0n)
|
|
640
|
+
return { at: start, denominator: current };
|
|
641
|
+
const hint = resolutionHints.get(lower(condition));
|
|
642
|
+
let hintBlock = hint?.resolved_block;
|
|
643
|
+
if (!Number.isSafeInteger(hintBlock) &&
|
|
644
|
+
/^0x[0-9a-f]{64}$/i.test(hint?.transaction_hash ?? "")) {
|
|
645
|
+
let transaction = hintTransactions.get(hint.transaction_hash);
|
|
646
|
+
if (!transaction) {
|
|
647
|
+
transaction = rpc("eth_getTransactionByHash", [
|
|
648
|
+
hint.transaction_hash,
|
|
649
|
+
])
|
|
650
|
+
.then((tx) => Number(BigInt(tx.blockNumber)))
|
|
651
|
+
.catch(() => undefined);
|
|
652
|
+
hintTransactions.set(hint.transaction_hash, transaction);
|
|
653
|
+
}
|
|
654
|
+
hintBlock = await transaction;
|
|
655
|
+
}
|
|
656
|
+
if (Number.isSafeInteger(hintBlock) &&
|
|
657
|
+
hintBlock > startBlock &&
|
|
658
|
+
hintBlock <= head) {
|
|
659
|
+
const [before, after] = await Promise.all([
|
|
660
|
+
denominator(condition, hintBlock - 1),
|
|
661
|
+
denominator(condition, hintBlock),
|
|
662
|
+
]);
|
|
663
|
+
if (before === 0n && after === current) {
|
|
664
|
+
const settledBlock = await rpc("eth_getBlockByNumber", [
|
|
665
|
+
hex(hintBlock),
|
|
666
|
+
false,
|
|
667
|
+
]);
|
|
668
|
+
return {
|
|
669
|
+
at: Number(BigInt(settledBlock.timestamp)),
|
|
670
|
+
denominator: current,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
let lo = startBlock, hi = head;
|
|
675
|
+
while (hi - lo > 1) {
|
|
676
|
+
const mid = Math.floor((hi + lo) / 2);
|
|
677
|
+
if ((await denominator(condition, mid)) > 0n)
|
|
678
|
+
hi = mid;
|
|
679
|
+
else
|
|
680
|
+
lo = mid;
|
|
681
|
+
}
|
|
682
|
+
const settledBlock = await rpc("eth_getBlockByNumber", [
|
|
683
|
+
hex(hi),
|
|
684
|
+
false,
|
|
685
|
+
]);
|
|
686
|
+
return {
|
|
687
|
+
at: Number(BigInt(settledBlock.timestamp)),
|
|
688
|
+
denominator: current,
|
|
689
|
+
};
|
|
690
|
+
})();
|
|
691
|
+
settlementBlocks.set(condition, result);
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
};
|
|
695
|
+
const priceCurves = new Map(), settlements = new Map();
|
|
696
|
+
await Promise.all(heldTokens.map(async (id) => {
|
|
697
|
+
const ref = refs.get(id), slot = await outcomeSlot(id, ref.condition_id), resolved = await settlement(ref.condition_id);
|
|
698
|
+
if (resolved) {
|
|
699
|
+
const numerator = BigInt(abi1155.decodeFunctionResult("payoutNumerators", await rpc("eth_call", [
|
|
700
|
+
{
|
|
701
|
+
to: c.conditional_tokens,
|
|
702
|
+
data: abi1155.encodeFunctionData("payoutNumerators", [
|
|
703
|
+
ref.condition_id,
|
|
704
|
+
slot,
|
|
705
|
+
]),
|
|
706
|
+
},
|
|
707
|
+
hex(head),
|
|
708
|
+
]))[0]);
|
|
709
|
+
const price = D(numerator.toString())
|
|
710
|
+
.div(resolved.denominator.toString())
|
|
711
|
+
.toNumber();
|
|
712
|
+
check(price >= 0 && price <= 1, "invalid_settlement_payout");
|
|
713
|
+
settlements.set(id, { at: resolved.at, price });
|
|
714
|
+
if (resolved.at <= start) {
|
|
715
|
+
priceCurves.set(id, []);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
let cachedPrices = this.prices.get(id);
|
|
720
|
+
if (!cachedPrices || cachedPrices.until <= now()) {
|
|
721
|
+
const value = paged("/v2/prices-history", { token_id: id, interval: "1m", bucket_seconds: "10800" }, 5).then((rows) => {
|
|
722
|
+
check(rows.length <= 320, "unexpected_price_history_window");
|
|
723
|
+
return rows;
|
|
724
|
+
});
|
|
725
|
+
cachedPrices = { until: now() + 3600_000, value };
|
|
726
|
+
this.prices.set(id, cachedPrices);
|
|
727
|
+
value.catch(() => {
|
|
728
|
+
if (this.prices.get(id)?.value === value)
|
|
729
|
+
this.prices.delete(id);
|
|
730
|
+
});
|
|
731
|
+
if (this.prices.size > 512)
|
|
732
|
+
this.prices.delete(this.prices.keys().next().value);
|
|
733
|
+
}
|
|
734
|
+
const p = [...(await cachedPrices.value)];
|
|
735
|
+
// A current three-hour average can differ materially from the current
|
|
736
|
+
// snapshot even with a complete inventory. Read the anchor's own latest
|
|
737
|
+
// observation instead of increasing the reconciliation tolerance.
|
|
738
|
+
const anchor = await data("/v2/prices-history", {
|
|
739
|
+
token_id: id,
|
|
740
|
+
as_of: String(end),
|
|
741
|
+
});
|
|
742
|
+
check(Array.isArray(anchor.data) &&
|
|
743
|
+
anchor.data.length <= 1 &&
|
|
744
|
+
anchor.pagination?.has_more === false &&
|
|
745
|
+
anchor.pagination.next_cursor === null, "invalid_anchor_price");
|
|
746
|
+
for (const point of anchor.data) {
|
|
747
|
+
const existing = p.findIndex((x) => x.timestamp === point.timestamp);
|
|
748
|
+
if (existing >= 0)
|
|
749
|
+
p.splice(existing, 1);
|
|
750
|
+
p.push(point);
|
|
751
|
+
}
|
|
752
|
+
p.sort((a, b) => a.timestamp - b.timestamp);
|
|
753
|
+
check(p.every((x, i) => Number.isSafeInteger(x.timestamp) &&
|
|
754
|
+
valid(x.price) &&
|
|
755
|
+
x.price >= 0 &&
|
|
756
|
+
x.price <= 1 &&
|
|
757
|
+
Number.isSafeInteger(x.resolution_seconds) &&
|
|
758
|
+
x.resolution_seconds >= 0 &&
|
|
759
|
+
(i === 0 || x.timestamp > p[i - 1].timestamp)), "invalid_historical_price_curve");
|
|
760
|
+
priceCurves.set(id, p);
|
|
761
|
+
}));
|
|
762
|
+
const fallbackPriceSources = new Set();
|
|
763
|
+
const priceAt = async (id, at) => {
|
|
764
|
+
const curve = priceCurves.get(id), settlement = settlements.get(id);
|
|
765
|
+
try {
|
|
766
|
+
return historicalMark(curve, at, settlement);
|
|
767
|
+
}
|
|
768
|
+
catch (e) {
|
|
769
|
+
if (!(e instanceof Error) || e.message !== "missing_historical_price")
|
|
770
|
+
throw e;
|
|
771
|
+
// A coarse series can have a hole while a finer point-in-time tier
|
|
772
|
+
// retains an actual observation. Keep the same age/no-future checks.
|
|
773
|
+
let cutoff = at;
|
|
774
|
+
try {
|
|
775
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
776
|
+
const r = await data("/v2/prices-history", {
|
|
777
|
+
token_id: id,
|
|
778
|
+
as_of: String(cutoff),
|
|
779
|
+
});
|
|
780
|
+
check(Array.isArray(r.data) &&
|
|
781
|
+
r.data.length <= 1 &&
|
|
782
|
+
r.pagination?.has_more === false &&
|
|
783
|
+
r.pagination.next_cursor === null, "invalid_historical_price_curve");
|
|
784
|
+
const point = r.data[0];
|
|
785
|
+
check(point &&
|
|
786
|
+
Number.isSafeInteger(point.timestamp) &&
|
|
787
|
+
Number.isSafeInteger(point.resolution_seconds) &&
|
|
788
|
+
point.resolution_seconds >= 0 &&
|
|
789
|
+
point.resolution_seconds <= 10800 &&
|
|
790
|
+
valid(point.price) &&
|
|
791
|
+
point.price >= 0 &&
|
|
792
|
+
point.price <= 1, "missing_historical_price", { token_id: id, at });
|
|
793
|
+
if (point.timestamp + point.resolution_seconds <= at) {
|
|
794
|
+
try {
|
|
795
|
+
const price = historicalMark([point], at, settlement);
|
|
796
|
+
fallbackPriceSources.add("polymarket_data_v2_point_in_time");
|
|
797
|
+
return price;
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
throw new Error("missing_historical_price", {
|
|
801
|
+
cause: { token_id: id, at, observation: point },
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
cutoff = at - point.resolution_seconds;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
if (!(error instanceof Error) ||
|
|
810
|
+
error.message !== "missing_historical_price")
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
// The official CLOB archive retains periodic marks absent from the
|
|
814
|
+
// native v2 tier. Request a bounded window and discard its occasionally
|
|
815
|
+
// appended terminal point outside that window. Never use it as history.
|
|
816
|
+
try {
|
|
817
|
+
const url = new URL("/prices-history", "https://clob.polymarket.com");
|
|
818
|
+
for (const [key, value] of Object.entries({
|
|
819
|
+
market: id,
|
|
820
|
+
startTs: String(at - 12 * 3600),
|
|
821
|
+
endTs: String(at),
|
|
822
|
+
fidelity: "60",
|
|
823
|
+
}))
|
|
824
|
+
url.searchParams.set(key, value);
|
|
825
|
+
const response = JSON.parse((await read(url)).toString());
|
|
826
|
+
check(Array.isArray(response.history) && response.history.length <= 1000, "missing_historical_price");
|
|
827
|
+
const points = response.history
|
|
828
|
+
.filter((p) => Number.isSafeInteger(p.t) && p.t >= at - 12 * 3600 && p.t <= at)
|
|
829
|
+
.map((p) => ({
|
|
830
|
+
timestamp: p.t,
|
|
831
|
+
price: p.p,
|
|
832
|
+
resolution_seconds: 3600,
|
|
833
|
+
}))
|
|
834
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
835
|
+
check(points.every((p, i) => valid(p.price) &&
|
|
836
|
+
p.price >= 0 &&
|
|
837
|
+
p.price <= 1 &&
|
|
838
|
+
(!i || p.timestamp > points[i - 1].timestamp)), "missing_historical_price");
|
|
839
|
+
const price = historicalMark(points, at, settlement);
|
|
840
|
+
fallbackPriceSources.add("polymarket_clob_prices_history");
|
|
841
|
+
return price;
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
throw new Error("missing_historical_price", {
|
|
845
|
+
cause: { token_id: id, at },
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
const curve = await Promise.all(times.map(async (at, index) => {
|
|
851
|
+
const inventory = inventories[index];
|
|
852
|
+
let equity = addresses
|
|
853
|
+
.slice(0, 2)
|
|
854
|
+
.reduce((n, token) => n.plus(D(inventory.get(`cash:${token}`).toString()).div(1e6)), D(0));
|
|
855
|
+
const values = await Promise.all(heldTokens
|
|
856
|
+
.filter((id) => inventory.get(id) > 0n)
|
|
857
|
+
.map(async (id) => D(inventory.get(id).toString())
|
|
858
|
+
.div(1e6)
|
|
859
|
+
.times(await priceAt(id, at))));
|
|
860
|
+
equity = values.reduce((n, value) => n.plus(value), equity);
|
|
861
|
+
return { at, equity: equity.toString() };
|
|
862
|
+
}));
|
|
863
|
+
const difference = D(curve.at(-1).equity)
|
|
864
|
+
.minus(snapshot.equity)
|
|
865
|
+
.abs()
|
|
866
|
+
.div(snapshot.equity)
|
|
867
|
+
.times(100);
|
|
868
|
+
check(difference.lte(1), "equity_reconciliation_failed", {
|
|
869
|
+
difference_pct: difference.toNumber(),
|
|
870
|
+
reconstructed: curve.at(-1),
|
|
871
|
+
snapshot: snapshot.equity.toString(),
|
|
872
|
+
tokens: heldTokens.map((id) => ({
|
|
873
|
+
id,
|
|
874
|
+
quantity: current.get(id).toString(),
|
|
875
|
+
})),
|
|
876
|
+
});
|
|
877
|
+
check((await rpc("eth_getBlockByNumber", [hex(head), false])).hash ===
|
|
878
|
+
block.hash, "inventory_anchor_reorg");
|
|
879
|
+
const metrics = equityMetrics(curve, funding, {
|
|
880
|
+
source: "polygon_transfers_and_polymarket_historical_marks",
|
|
881
|
+
sampling: "daily",
|
|
882
|
+
estimated: true,
|
|
883
|
+
cashflow_adjusted: true,
|
|
884
|
+
flow_timing: "deposits_at_start_withdrawals_at_end",
|
|
885
|
+
reconciliation_difference_pct: difference.toNumber(),
|
|
886
|
+
inventory_start_block: startBlock,
|
|
887
|
+
inventory_end_block: head,
|
|
888
|
+
inventory_end_block_hash: block.hash,
|
|
889
|
+
token_count: tokenIds.length,
|
|
890
|
+
transfer_count: ids.size,
|
|
891
|
+
max_price_age_seconds: 6 * 3600,
|
|
892
|
+
price_bucket_seconds: 10800,
|
|
893
|
+
fallback_price_sources: [...fallbackPriceSources].sort(),
|
|
894
|
+
});
|
|
895
|
+
return {
|
|
896
|
+
metrics,
|
|
897
|
+
evidence: {
|
|
898
|
+
curve,
|
|
899
|
+
funding,
|
|
900
|
+
start_block: startBlock,
|
|
901
|
+
end_block: head,
|
|
902
|
+
end_block_hash: block.hash,
|
|
903
|
+
token_count: tokenIds.length,
|
|
904
|
+
transfer_count: ids.size,
|
|
905
|
+
activity_count: activity.length,
|
|
906
|
+
reconciliation_difference_pct: difference.toNumber(),
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
}
|