@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,435 @@
|
|
|
1
|
+
import { D, cash, fingerprint } from "./agent/domain.js";
|
|
2
|
+
import { AgentError } from "./agent/contracts.js";
|
|
3
|
+
const address = (v) => typeof v === "string" && /^0x[a-fA-F0-9]{40}$/.test(v);
|
|
4
|
+
const number = (v) => {
|
|
5
|
+
if ((typeof v !== "string" && typeof v !== "number") ||
|
|
6
|
+
v === "" ||
|
|
7
|
+
!D(v).isFinite())
|
|
8
|
+
throw Error("Missing or invalid monetary value");
|
|
9
|
+
return D(v);
|
|
10
|
+
};
|
|
11
|
+
const unavailable = (name, reason) => ({
|
|
12
|
+
module: name,
|
|
13
|
+
status: "unavailable",
|
|
14
|
+
account_value_usd: null,
|
|
15
|
+
available_trading_usdc: null,
|
|
16
|
+
occupied_or_reserved_usdc: null,
|
|
17
|
+
positions_value_usd: null,
|
|
18
|
+
observed_at: null,
|
|
19
|
+
fetched_at: new Date().toISOString(),
|
|
20
|
+
reason,
|
|
21
|
+
});
|
|
22
|
+
const perpsQuery = `query RideOwnedPerpsBalance { hyperliquidAccountState(network: MAINNET) {
|
|
23
|
+
userId accountValue withdrawable totalMarginUsed totalNtlPos accountAbstraction isStale rawResponseJson
|
|
24
|
+
wallet { masterWalletAddress walletGeneration status } positions { coin positionValue }
|
|
25
|
+
} }`;
|
|
26
|
+
// Fixed, authenticated ownership queries only. No caller-supplied wallet or user ID.
|
|
27
|
+
export async function readAccountBalances(graphql, token, request = fetch) {
|
|
28
|
+
// Pagination and wallet reads share one deadline, below the widget tool timeout.
|
|
29
|
+
const deadline = AbortSignal.timeout(22000);
|
|
30
|
+
const query = async (document, embedded = false) => {
|
|
31
|
+
const response = await request(graphql, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
Authorization: `Bearer ${token}`,
|
|
36
|
+
...(embedded ? { "x-hyperliquid-wallet-mode": "embedded-v1" } : {}),
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({ query: document, variables: {} }),
|
|
39
|
+
redirect: "error",
|
|
40
|
+
signal: AbortSignal.any([deadline, AbortSignal.timeout(15000)]),
|
|
41
|
+
});
|
|
42
|
+
const body = (await response.json());
|
|
43
|
+
if (!response.ok || body.errors?.length || !body.data)
|
|
44
|
+
throw Error("Account source unavailable");
|
|
45
|
+
return body.data;
|
|
46
|
+
};
|
|
47
|
+
const publicJson = async (url, init) => {
|
|
48
|
+
const response = await request(url, {
|
|
49
|
+
...init,
|
|
50
|
+
redirect: "error",
|
|
51
|
+
signal: AbortSignal.any([deadline, AbortSignal.timeout(10000)]),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok)
|
|
54
|
+
throw Error("Venue balance source unavailable");
|
|
55
|
+
return response.json();
|
|
56
|
+
};
|
|
57
|
+
const info = (body) => publicJson("https://api.hyperliquid.xyz/info", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
});
|
|
62
|
+
const me = (await query("query RideBalanceIdentity { me { id username proxyWalletAddress legacyProxyWalletAddress } }")).me;
|
|
63
|
+
if (!me?.id)
|
|
64
|
+
throw new AgentError("ACCOUNT_READ_UNAVAILABLE", "Ride account ownership could not be verified.", true, 503);
|
|
65
|
+
let catalog;
|
|
66
|
+
const spotPrice = async (coin) => {
|
|
67
|
+
const payload = await (catalog ??= info({ type: "spotMetaAndAssetCtxs" }));
|
|
68
|
+
if (!Array.isArray(payload) ||
|
|
69
|
+
!payload[0]?.tokens ||
|
|
70
|
+
!payload[0]?.universe ||
|
|
71
|
+
!Array.isArray(payload[1]))
|
|
72
|
+
throw Error("Spot valuation unavailable");
|
|
73
|
+
const tokens = payload[0].tokens, markets = payload[0].universe, contexts = payload[1];
|
|
74
|
+
const price = (name, seen = new Set()) => {
|
|
75
|
+
if (name.toUpperCase() === "USDC")
|
|
76
|
+
return D(1);
|
|
77
|
+
if (seen.has(name))
|
|
78
|
+
throw Error("Unpriced spot holding");
|
|
79
|
+
seen.add(name);
|
|
80
|
+
const index = markets.findIndex((m) => tokens.find((t) => t.index === m.tokens?.[0])?.name === name);
|
|
81
|
+
if (index < 0)
|
|
82
|
+
throw Error("Unpriced spot holding");
|
|
83
|
+
const quote = tokens.find((t) => t.index === markets[index].tokens[1])?.name;
|
|
84
|
+
const p = number(contexts[index]?.midPx ?? contexts[index]?.markPx);
|
|
85
|
+
if (!quote || !p.gt(0))
|
|
86
|
+
throw Error("Unpriced spot holding");
|
|
87
|
+
return p.times(price(quote, seen));
|
|
88
|
+
};
|
|
89
|
+
return price(coin);
|
|
90
|
+
};
|
|
91
|
+
const readPerps = async () => {
|
|
92
|
+
const reads = await Promise.allSettled([
|
|
93
|
+
query(perpsQuery),
|
|
94
|
+
query(perpsQuery, true),
|
|
95
|
+
]);
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const wallets = [];
|
|
98
|
+
let verifiedAbsent = 0;
|
|
99
|
+
let missing = reads.some((r) => r.status === "rejected");
|
|
100
|
+
for (const r of reads) {
|
|
101
|
+
if (r.status !== "fulfilled")
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
const state = r.value.hyperliquidAccountState;
|
|
105
|
+
if (state?.userId !== me.id || state.isStale)
|
|
106
|
+
throw Error("Missing, stale or foreign Perps account");
|
|
107
|
+
const owner = state.wallet?.masterWalletAddress;
|
|
108
|
+
if (!address(owner)) {
|
|
109
|
+
if (String(state.wallet?.status).replaceAll("_", "").toLowerCase() ===
|
|
110
|
+
"notcreated" &&
|
|
111
|
+
number(state.accountValue).isZero() &&
|
|
112
|
+
number(state.withdrawable).isZero()) {
|
|
113
|
+
verifiedAbsent++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
throw Error("Owned wallet is not available");
|
|
117
|
+
}
|
|
118
|
+
const key = owner.toLowerCase();
|
|
119
|
+
if (seen.has(key))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(key);
|
|
122
|
+
const raw = typeof state.rawResponseJson === "string"
|
|
123
|
+
? JSON.parse(state.rawResponseJson)
|
|
124
|
+
: {};
|
|
125
|
+
const spot = raw.spotClearinghouseState ??
|
|
126
|
+
(await info({ type: "spotClearinghouseState", user: owner }));
|
|
127
|
+
if (!Array.isArray(spot?.balances))
|
|
128
|
+
throw Error("Spot balances unavailable");
|
|
129
|
+
const abstraction = String(state.accountAbstraction ?? "").toLowerCase();
|
|
130
|
+
// This is the same Classic/Unified distinction used by the App account card.
|
|
131
|
+
const unified = [
|
|
132
|
+
"unifiedaccount",
|
|
133
|
+
"unified_account",
|
|
134
|
+
"unified",
|
|
135
|
+
"u",
|
|
136
|
+
"portfoliomargin",
|
|
137
|
+
"portfolio_margin",
|
|
138
|
+
"portfolio",
|
|
139
|
+
].includes(abstraction);
|
|
140
|
+
if (!abstraction &&
|
|
141
|
+
spot.balances.some((b) => b.coin === "USDC" && number(b.total).gt(0)))
|
|
142
|
+
throw Error("Account mode is unavailable; spot USDC could overlap with Perps equity");
|
|
143
|
+
let total = number(state.accountValue), available = number(state.withdrawable);
|
|
144
|
+
const perpsValue = total;
|
|
145
|
+
const withdrawable = available;
|
|
146
|
+
let spotHold = D(0);
|
|
147
|
+
for (const holding of spot.balances) {
|
|
148
|
+
const quantity = number(holding.total);
|
|
149
|
+
if (quantity.isZero())
|
|
150
|
+
continue;
|
|
151
|
+
if (quantity.lt(0))
|
|
152
|
+
throw Error("Invalid spot holding");
|
|
153
|
+
const cashToken = String(holding.coin).toUpperCase() === "USDC";
|
|
154
|
+
if (cashToken && unified)
|
|
155
|
+
continue;
|
|
156
|
+
total = total.plus(quantity.times(cashToken ? 1 : await spotPrice(holding.coin)));
|
|
157
|
+
if (cashToken) {
|
|
158
|
+
const hold = number(holding.hold);
|
|
159
|
+
if (hold.lt(0) || hold.gt(quantity))
|
|
160
|
+
throw Error("Invalid spot reserve");
|
|
161
|
+
available = available.plus(quantity.minus(hold));
|
|
162
|
+
spotHold = spotHold.plus(hold);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const gross = number(state.totalNtlPos).abs();
|
|
166
|
+
const sourceTime = raw.time ?? raw.clearinghouseState?.time;
|
|
167
|
+
wallets.push({
|
|
168
|
+
wallet_ref: `owned_${fingerprint(key).slice(0, 16)}`,
|
|
169
|
+
generation: state.wallet.walletGeneration,
|
|
170
|
+
account_value_usd: cash(total),
|
|
171
|
+
available_trading_usdc: cash(available),
|
|
172
|
+
occupied_or_reserved_usdc: cash(D.max(0, perpsValue.minus(withdrawable)).plus(spotHold)),
|
|
173
|
+
margin_used_usdc: cash(number(state.totalMarginUsed)),
|
|
174
|
+
positions_value_usd: cash(gross),
|
|
175
|
+
positions_value_basis: "gross leveraged notional; already represented in account equity, never added to equity",
|
|
176
|
+
observed_at: Number.isFinite(sourceTime)
|
|
177
|
+
? new Date(sourceTime).toISOString()
|
|
178
|
+
: null,
|
|
179
|
+
fetched_at: new Date().toISOString(),
|
|
180
|
+
source: "Ride hyperliquidAccountState + owned Hyperliquid spot balances",
|
|
181
|
+
account_mode: abstraction || "unknown",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
missing = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!wallets.length && !missing && verifiedAbsent === 2)
|
|
189
|
+
return {
|
|
190
|
+
module: "perps",
|
|
191
|
+
status: "ok",
|
|
192
|
+
account_value_usd: "0.000000",
|
|
193
|
+
available_trading_usdc: "0.000000",
|
|
194
|
+
occupied_or_reserved_usdc: "0.000000",
|
|
195
|
+
positions_value_usd: "0.000000",
|
|
196
|
+
wallets: [],
|
|
197
|
+
observed_at: null,
|
|
198
|
+
fetched_at: new Date().toISOString(),
|
|
199
|
+
reason: "Core verified that neither owned Perps wallet generation is created.",
|
|
200
|
+
};
|
|
201
|
+
if (!wallets.length)
|
|
202
|
+
return unavailable("perps", "Perps equity, account mode or complete owned spot valuation is temporarily unavailable. No missing value is zero.");
|
|
203
|
+
const sum = (key) => cash(wallets.reduce((n, w) => n.plus(w[key]), D(0)));
|
|
204
|
+
return {
|
|
205
|
+
module: "perps",
|
|
206
|
+
status: missing ? "partial" : "ok",
|
|
207
|
+
account_value_usd: missing ? null : sum("account_value_usd"),
|
|
208
|
+
known_subtotal_usd: sum("account_value_usd"),
|
|
209
|
+
available_trading_usdc: missing ? null : sum("available_trading_usdc"),
|
|
210
|
+
occupied_or_reserved_usdc: missing
|
|
211
|
+
? null
|
|
212
|
+
: sum("occupied_or_reserved_usdc"),
|
|
213
|
+
positions_value_usd: missing ? null : sum("positions_value_usd"),
|
|
214
|
+
positions_value_basis: "gross leveraged notional; not additive to account equity",
|
|
215
|
+
wallets,
|
|
216
|
+
observed_at: wallets.every((w) => w.observed_at)
|
|
217
|
+
? wallets.map((w) => w.observed_at).sort()[0]
|
|
218
|
+
: null,
|
|
219
|
+
fetched_at: new Date().toISOString(),
|
|
220
|
+
reason: missing
|
|
221
|
+
? "One wallet generation or valuation could not be read; subtotal excludes unavailable wallets."
|
|
222
|
+
: null,
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
const inventory = async (owner) => {
|
|
226
|
+
const positions = new Map();
|
|
227
|
+
for (let offset = 0; offset <= 10000; offset += 500) {
|
|
228
|
+
deadline.throwIfAborted();
|
|
229
|
+
const url = new URL("https://data-api.polymarket.com/positions");
|
|
230
|
+
for (const [k, v] of Object.entries({
|
|
231
|
+
user: owner,
|
|
232
|
+
sizeThreshold: "0",
|
|
233
|
+
includeArchived: "true",
|
|
234
|
+
limit: "500",
|
|
235
|
+
offset: String(offset),
|
|
236
|
+
sortBy: "TOKENS",
|
|
237
|
+
sortDirection: "DESC",
|
|
238
|
+
}))
|
|
239
|
+
url.searchParams.set(k, v);
|
|
240
|
+
const rows = await publicJson(url.href);
|
|
241
|
+
if (!Array.isArray(rows) || rows.length > 500)
|
|
242
|
+
throw Error("Prediction inventory unavailable");
|
|
243
|
+
const previous = positions.size;
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
if (String(row.proxyWallet).toLowerCase() !== owner.toLowerCase() ||
|
|
246
|
+
typeof row.asset !== "string" ||
|
|
247
|
+
!row.asset)
|
|
248
|
+
throw Error("Unverified position owner");
|
|
249
|
+
const value = number(row.currentValue);
|
|
250
|
+
if (value.lt(0))
|
|
251
|
+
throw Error("Invalid position value");
|
|
252
|
+
if (positions.has(row.asset) && !D(positions.get(row.asset)).eq(value))
|
|
253
|
+
throw Error("Inconsistent inventory page");
|
|
254
|
+
positions.set(row.asset, value.toString());
|
|
255
|
+
}
|
|
256
|
+
if (rows.length < 500)
|
|
257
|
+
return positions;
|
|
258
|
+
if (positions.size === previous)
|
|
259
|
+
throw Error("Prediction inventory pagination did not advance");
|
|
260
|
+
}
|
|
261
|
+
throw Error("Prediction inventory truncated");
|
|
262
|
+
};
|
|
263
|
+
const readPrediction = async () => {
|
|
264
|
+
const owners = [
|
|
265
|
+
...new Set([me.proxyWalletAddress, me.legacyProxyWalletAddress]
|
|
266
|
+
.filter(address)
|
|
267
|
+
.map((a) => a.toLowerCase())),
|
|
268
|
+
];
|
|
269
|
+
let cashValue, holdings = null, sourceTime = null, reason = null;
|
|
270
|
+
try {
|
|
271
|
+
const appCash = (await query("query RidePredictionBalance { walletBalance { proxyWalletAddress totalBalance nativeUsdcBalance } }")).walletBalance;
|
|
272
|
+
if (!appCash ||
|
|
273
|
+
!address(appCash.proxyWalletAddress) ||
|
|
274
|
+
(appCash.proxyWalletAddress &&
|
|
275
|
+
![me.proxyWalletAddress, me.legacyProxyWalletAddress].some((a) => address(a) &&
|
|
276
|
+
a.toLowerCase() === appCash.proxyWalletAddress.toLowerCase())))
|
|
277
|
+
throw Error("Cash owner mismatch");
|
|
278
|
+
// Legacy walletBalance can replace failed RPC reads with zero. Verify
|
|
279
|
+
// both trading tokens at one Polygon block before reporting any cash.
|
|
280
|
+
const rpc = async (calls) => {
|
|
281
|
+
const rows = await publicJson("https://polygon-bor-rpc.publicnode.com", {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json" },
|
|
284
|
+
body: JSON.stringify(calls.map((c, i) => ({ jsonrpc: "2.0", id: i + 1, ...c }))),
|
|
285
|
+
});
|
|
286
|
+
if (!Array.isArray(rows) || rows.length !== calls.length)
|
|
287
|
+
throw Error("Incomplete Polygon batch");
|
|
288
|
+
return calls.map((_, i) => {
|
|
289
|
+
const matches = rows.filter((r) => r.id === i + 1);
|
|
290
|
+
if (matches.length !== 1 ||
|
|
291
|
+
matches[0].error ||
|
|
292
|
+
matches[0].result == null)
|
|
293
|
+
throw Error("Polygon read unavailable");
|
|
294
|
+
return matches[0].result;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
const [chain, block] = await rpc([
|
|
298
|
+
{ method: "eth_chainId", params: [] },
|
|
299
|
+
{ method: "eth_getBlockByNumber", params: ["latest", false] },
|
|
300
|
+
]);
|
|
301
|
+
if (chain !== "0x89" ||
|
|
302
|
+
!/^0x[0-9a-f]+$/i.test(block.number) ||
|
|
303
|
+
!/^0x[0-9a-f]+$/i.test(block.timestamp))
|
|
304
|
+
throw Error("Polygon block unavailable");
|
|
305
|
+
const blockAt = Number(BigInt(block.timestamp)) * 1000;
|
|
306
|
+
if (!Number.isFinite(blockAt) ||
|
|
307
|
+
Date.now() - blockAt > 120000 ||
|
|
308
|
+
blockAt > Date.now() + 5000)
|
|
309
|
+
throw Error("Stale Polygon block");
|
|
310
|
+
const tokens = [
|
|
311
|
+
"0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB", // pUSD, Ride Core token registry
|
|
312
|
+
"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", // USDC.e
|
|
313
|
+
"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // native USDC, funding only
|
|
314
|
+
];
|
|
315
|
+
const quantities = await rpc(owners.flatMap((owner) => tokens.map((to) => ({
|
|
316
|
+
method: "eth_call",
|
|
317
|
+
params: [
|
|
318
|
+
{
|
|
319
|
+
to,
|
|
320
|
+
data: "0x70a08231" + owner.slice(2).toLowerCase().padStart(64, "0"),
|
|
321
|
+
},
|
|
322
|
+
block.number,
|
|
323
|
+
],
|
|
324
|
+
}))));
|
|
325
|
+
const amounts = quantities.map((q) => {
|
|
326
|
+
if (typeof q !== "string" || !/^0x[0-9a-f]{64}$/i.test(q))
|
|
327
|
+
throw Error("Invalid token balance");
|
|
328
|
+
return D(BigInt(q).toString()).div(1000000);
|
|
329
|
+
});
|
|
330
|
+
const primary = owners.indexOf(appCash.proxyWalletAddress.toLowerCase()) * 3;
|
|
331
|
+
const allOwned = owners.reduce((n, _, i) => n.plus(amounts[i * 3]).plus(amounts[i * 3 + 1]), D(0));
|
|
332
|
+
const primaryCash = amounts[primary].plus(amounts[primary + 1]);
|
|
333
|
+
cashValue = {
|
|
334
|
+
totalBalance: cash(primaryCash),
|
|
335
|
+
allOwnedBalance: cash(allOwned),
|
|
336
|
+
retainedCash: cash(allOwned.minus(primaryCash)),
|
|
337
|
+
nativeUsdcBalance: cash(owners.reduce((n, _, i) => n.plus(amounts[i * 3 + 2]), D(0))),
|
|
338
|
+
};
|
|
339
|
+
sourceTime = new Date(blockAt).toISOString();
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
cashValue = null;
|
|
343
|
+
reason = "Trading collateral is temporarily unavailable.";
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
if (!owners.length)
|
|
347
|
+
throw Error("Owned Prediction wallets unavailable");
|
|
348
|
+
const inventories = await Promise.all(owners.map(inventory));
|
|
349
|
+
holdings = cash(inventories.reduce((total, rows) => total.plus([...rows.values()].reduce((n, v) => n.plus(v), D(0))), D(0)));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
reason =
|
|
353
|
+
(reason ?? "") +
|
|
354
|
+
" Complete Prediction inventory is temporarily unavailable.";
|
|
355
|
+
}
|
|
356
|
+
const value = cashValue && holdings !== null
|
|
357
|
+
? cash(number(cashValue.allOwnedBalance).plus(holdings))
|
|
358
|
+
: null;
|
|
359
|
+
return {
|
|
360
|
+
module: "prediction",
|
|
361
|
+
status: value === null ? "partial" : "ok",
|
|
362
|
+
account_value_usd: value,
|
|
363
|
+
known_subtotal_usd: cashValue || holdings !== null
|
|
364
|
+
? cash(D(cashValue?.allOwnedBalance ?? 0).plus(holdings ?? 0))
|
|
365
|
+
: null,
|
|
366
|
+
available_trading_usdc: cashValue
|
|
367
|
+
? cash(number(cashValue.totalBalance))
|
|
368
|
+
: null,
|
|
369
|
+
occupied_or_reserved_usdc: null,
|
|
370
|
+
retained_wallet_cash_usdc: cashValue?.retainedCash ?? null,
|
|
371
|
+
app_display_value_usd: cashValue && holdings !== null
|
|
372
|
+
? cash(D(cashValue.totalBalance).plus(holdings))
|
|
373
|
+
: null,
|
|
374
|
+
app_difference_reason: "App displays current trading-wallet cash plus owned positions. MCP equity also includes verified cash in retained Prediction wallets; retained cash is not current trading availability.",
|
|
375
|
+
positions_value_usd: holdings,
|
|
376
|
+
trading_collateral_basis: "Owned Ride Prediction wallet: pUSD + USDC.e verified at one Polygon block; pending-order reservations are not supplied by this API",
|
|
377
|
+
non_trading_native_usdc: cashValue?.nativeUsdcBalance == null
|
|
378
|
+
? null
|
|
379
|
+
: cash(number(cashValue.nativeUsdcBalance)),
|
|
380
|
+
non_trading_cash_included_in_total: false,
|
|
381
|
+
position_wallet_count: owners.length,
|
|
382
|
+
cash_observed_at: sourceTime,
|
|
383
|
+
positions_observed_at: null,
|
|
384
|
+
observed_at: null,
|
|
385
|
+
fetched_at: new Date().toISOString(),
|
|
386
|
+
reason: reason ??
|
|
387
|
+
"Open-order reserve is unavailable. Native USDC is funding cash and is excluded from trading equity. Retained-wallet cash is included in equity, separately from current-wallet trading availability.",
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
const reads = await Promise.allSettled([readPerps(), readPrediction()]);
|
|
391
|
+
const markets = {
|
|
392
|
+
perps: reads[0].status === "fulfilled"
|
|
393
|
+
? reads[0].value
|
|
394
|
+
: unavailable("perps", "Perps account read failed."),
|
|
395
|
+
prediction: reads[1].status === "fulfilled"
|
|
396
|
+
? reads[1].value
|
|
397
|
+
: unavailable("prediction", "Prediction account read failed."),
|
|
398
|
+
meme: {
|
|
399
|
+
...unavailable("meme", "Meme is a separate App wallet/inventory module. The authenticated MCP account API does not expose its complete owned multi-chain valuation; inspect it in the App."),
|
|
400
|
+
status: "unsupported",
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
const knownValues = Object.values(markets)
|
|
404
|
+
.map((m) => m.account_value_usd ?? m.known_subtotal_usd)
|
|
405
|
+
.filter((v) => v != null);
|
|
406
|
+
const known = knownValues.length
|
|
407
|
+
? cash(knownValues.reduce((n, v) => n.plus(v), D(0)))
|
|
408
|
+
: null;
|
|
409
|
+
const availableValues = Object.values(markets)
|
|
410
|
+
.map((m) => m.available_trading_usdc)
|
|
411
|
+
.filter((v) => v != null);
|
|
412
|
+
const knownAvailable = availableValues.length
|
|
413
|
+
? cash(availableValues.reduce((n, v) => n.plus(v), D(0)))
|
|
414
|
+
: null;
|
|
415
|
+
return {
|
|
416
|
+
account: {
|
|
417
|
+
authenticated: true,
|
|
418
|
+
user_id: me.id,
|
|
419
|
+
display_name: me.username ?? "Ride user",
|
|
420
|
+
},
|
|
421
|
+
summary: {
|
|
422
|
+
account_value_usd: null,
|
|
423
|
+
available_trading_usdc: null,
|
|
424
|
+
known_subtotal_usd: known,
|
|
425
|
+
known_available_trading_usdc: knownAvailable,
|
|
426
|
+
coverage_complete: false,
|
|
427
|
+
supported_markets_complete: markets.perps.status === "ok" && markets.prediction.status === "ok",
|
|
428
|
+
included_modules: ["perps", "prediction"],
|
|
429
|
+
excluded_modules: ["meme"],
|
|
430
|
+
basis: "Sum distinct owned trading accounts once. App copy budgets, their remaining allocation and the Agent ledger are subdivisions, never additional assets. Missing modules keep the overall total unavailable.",
|
|
431
|
+
},
|
|
432
|
+
markets,
|
|
433
|
+
fetched_at: new Date().toISOString(),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { readAccountBalances } from "./account_balances.js";
|
|
2
|
+
import { D } from "./agent/domain.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { AgentError, result } from "./agent/contracts.js";
|
|
5
|
+
export const accountCopiesInput = z
|
|
6
|
+
.object({
|
|
7
|
+
limit: z.number().int().min(1).max(100).default(100),
|
|
8
|
+
offset: z.number().int().min(0).default(0),
|
|
9
|
+
status: z
|
|
10
|
+
.enum([
|
|
11
|
+
"pending",
|
|
12
|
+
"open",
|
|
13
|
+
"closing",
|
|
14
|
+
"closed",
|
|
15
|
+
"failed",
|
|
16
|
+
"official_missing",
|
|
17
|
+
"claimed",
|
|
18
|
+
])
|
|
19
|
+
.optional(),
|
|
20
|
+
traderAddress: z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(/^0x[a-fA-F0-9]{40}$/)
|
|
23
|
+
.optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
const predictionFields = `id configId traderAddress marketTitle side status shares entryPrice
|
|
27
|
+
currentPnl currentPnlPercent displayCurrentValue displayCurrentPnl claimable openedAt closedAt`;
|
|
28
|
+
const perpsFields = `id configId traderAddress coin side size entryPx markPx positionValue marginUsed
|
|
29
|
+
unrealizedPnl realizedPnl tradingFees fundingPnl netPnl status openedAt closedAt`;
|
|
30
|
+
const perpsQuery = `query RideAppPerps {
|
|
31
|
+
myPerpsAutoRideConfigs(network: MAINNET, enabledOnly: false) {
|
|
32
|
+
id traderAddress allocatedFund effectiveAllocatedFund mode maxLeverage isEnabled lifecycleState
|
|
33
|
+
}
|
|
34
|
+
myPerpsAutoRidePositions(network: MAINNET, includeRetainedHistory: true) { ${perpsFields} }
|
|
35
|
+
}`;
|
|
36
|
+
// Account reads use the authenticated Core session and fixed queries only.
|
|
37
|
+
// They never import App copies into the Agent execution or accounting ledger.
|
|
38
|
+
export async function readAccountCopies(graphqlUrl, token, input, fetcher = fetch) {
|
|
39
|
+
const args = accountCopiesInput.parse(input);
|
|
40
|
+
const query = async (document, variables, embedded = false) => {
|
|
41
|
+
const response = await fetcher(graphqlUrl, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
Authorization: `Bearer ${token}`,
|
|
46
|
+
...(embedded ? { "x-hyperliquid-wallet-mode": "embedded-v1" } : {}),
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ query: document, variables }),
|
|
49
|
+
signal: AbortSignal.timeout(15000),
|
|
50
|
+
redirect: "error",
|
|
51
|
+
});
|
|
52
|
+
const body = (await response.json());
|
|
53
|
+
if (!response.ok || body.errors?.length || !body.data)
|
|
54
|
+
throw new AgentError("ACCOUNT_READ_UNAVAILABLE", "Ride App account data could not be read.", true, 503);
|
|
55
|
+
return body.data;
|
|
56
|
+
};
|
|
57
|
+
const connection = (alias, status) => `${alias}: copyTradePositions(
|
|
58
|
+
traderAddress: $traderAddress, status: ${status}, limit: $limit, offset: $offset
|
|
59
|
+
) { positions { ${predictionFields} } totalCount hasNextPage }`;
|
|
60
|
+
const statuses = args.status
|
|
61
|
+
? [args.status.toUpperCase()]
|
|
62
|
+
: ["PENDING", "OPEN", "CLOSING", "OFFICIAL_MISSING"];
|
|
63
|
+
const predictionQuery = `query RideAppCopies($traderAddress: String, $limit: Int!, $offset: Int!) {
|
|
64
|
+
me { id username }
|
|
65
|
+
copyTradeSummary { totalConfigs enabledConfigs totalOpenPositions totalRealizedPnl totalAllocation totalAvailableFund }
|
|
66
|
+
myCopyTradeConfigs { id traderAddress allocatedFund availableFund isEnabled currentPositions }
|
|
67
|
+
${statuses.map((s, i) => connection(`positions${i}`, s)).join("\n")}
|
|
68
|
+
}`;
|
|
69
|
+
const reads = await Promise.allSettled([
|
|
70
|
+
query(predictionQuery, {
|
|
71
|
+
limit: args.limit,
|
|
72
|
+
offset: args.offset,
|
|
73
|
+
traderAddress: args.traderAddress,
|
|
74
|
+
}),
|
|
75
|
+
query(perpsQuery, {}, false),
|
|
76
|
+
query(perpsQuery, {}, true),
|
|
77
|
+
]);
|
|
78
|
+
const prediction = reads[0];
|
|
79
|
+
if (prediction.status !== "fulfilled" || !prediction.value.me?.id)
|
|
80
|
+
throw new AgentError("ACCOUNT_READ_UNAVAILABLE", "Account identity and App copies could not be verified. Retry the account read; another login is only needed after an authentication error.", true, 503);
|
|
81
|
+
const perps = reads.slice(1);
|
|
82
|
+
const unique = (rows) => [
|
|
83
|
+
...new Map(rows.map((row) => [row.id, row])).values(),
|
|
84
|
+
];
|
|
85
|
+
const configs = unique(perps.flatMap((r) => r.status === "fulfilled" ? (r.value.myPerpsAutoRideConfigs ?? []) : []));
|
|
86
|
+
let positions = unique(perps.flatMap((r) => r.status === "fulfilled" ? (r.value.myPerpsAutoRidePositions ?? []) : []));
|
|
87
|
+
positions = positions.filter((p) => (!args.traderAddress ||
|
|
88
|
+
p.traderAddress?.toLowerCase() === args.traderAddress.toLowerCase()) &&
|
|
89
|
+
(args.status
|
|
90
|
+
? p.status?.toLowerCase() === args.status
|
|
91
|
+
: ["pending", "open", "closing", "official_missing"].includes(p.status?.toLowerCase())));
|
|
92
|
+
const pages = statuses.map((_, i) => prediction.value[`positions${i}`]);
|
|
93
|
+
const predictionPositions = unique(pages.flatMap((p) => p?.positions ?? []));
|
|
94
|
+
const complete = perps.every((r) => r.status === "fulfilled") &&
|
|
95
|
+
pages.every((p) => p && !p.hasNextPage);
|
|
96
|
+
const data = maskAddresses({
|
|
97
|
+
account: {
|
|
98
|
+
authenticated: true,
|
|
99
|
+
user_id: prediction.value.me.id,
|
|
100
|
+
display_name: prediction.value.me.username ?? "Ride user",
|
|
101
|
+
},
|
|
102
|
+
query_scope: "ride_app_copies",
|
|
103
|
+
management: "ride_app",
|
|
104
|
+
coverage_complete: complete,
|
|
105
|
+
prediction: {
|
|
106
|
+
configs: prediction.value.myCopyTradeConfigs,
|
|
107
|
+
summary: prediction.value.copyTradeSummary,
|
|
108
|
+
positions: predictionPositions,
|
|
109
|
+
has_more: pages.some((p) => p?.hasNextPage),
|
|
110
|
+
},
|
|
111
|
+
perps: {
|
|
112
|
+
configs,
|
|
113
|
+
positions,
|
|
114
|
+
coverage_complete: perps.every((r) => r.status === "fulfilled"),
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
return result(data, complete ? "ok" : "stale", complete
|
|
118
|
+
? []
|
|
119
|
+
: [
|
|
120
|
+
"Some App account reads or position pages are unavailable. Missing positions must not be treated as zero.",
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
export async function invokeAccountRead(name, args, token, graphqlUrl, invokePortfolio, fetcher = fetch) {
|
|
124
|
+
if (name === "get_my_copies")
|
|
125
|
+
return readAccountCopies(graphqlUrl, token, args, fetcher);
|
|
126
|
+
const reads = await Promise.allSettled([
|
|
127
|
+
invokePortfolio(),
|
|
128
|
+
readAccountCopies(graphqlUrl, token, {}, fetcher),
|
|
129
|
+
readAccountBalances(graphqlUrl, token, fetcher),
|
|
130
|
+
]);
|
|
131
|
+
const portfolio = reads[0].status === "fulfilled"
|
|
132
|
+
? reads[0].value
|
|
133
|
+
: result({ portfolio: null }, "stale", [
|
|
134
|
+
"Agent portfolio read is unavailable; App accounts are read separately.",
|
|
135
|
+
]);
|
|
136
|
+
const app = reads[1].status === "fulfilled" ? reads[1].value : null;
|
|
137
|
+
const balances = reads[2].status === "fulfilled" ? reads[2].value : null;
|
|
138
|
+
const hasAppCopies = app
|
|
139
|
+
? [app.data.perps, app.data.prediction].some((m) => (m?.configs?.length ?? 0) + (m?.positions?.length ?? 0) > 0)
|
|
140
|
+
: false;
|
|
141
|
+
const partial = !app ||
|
|
142
|
+
app.status === "stale" ||
|
|
143
|
+
!balances ||
|
|
144
|
+
!balances.summary.coverage_complete;
|
|
145
|
+
return result({
|
|
146
|
+
...portfolio.data,
|
|
147
|
+
account: balances?.account ??
|
|
148
|
+
app?.data.account ?? { authenticated: true },
|
|
149
|
+
query_scope: "agent_portfolio_and_ride_app",
|
|
150
|
+
agent_portfolio_status: portfolio.status,
|
|
151
|
+
account_coverage_status: partial ? "partial" : "complete",
|
|
152
|
+
app_copies: app?.data ?? {
|
|
153
|
+
coverage_complete: false,
|
|
154
|
+
status: "unavailable",
|
|
155
|
+
},
|
|
156
|
+
account_balances: balances ?? {
|
|
157
|
+
summary: {
|
|
158
|
+
coverage_complete: false,
|
|
159
|
+
account_value_usd: null,
|
|
160
|
+
known_subtotal_usd: null,
|
|
161
|
+
},
|
|
162
|
+
markets: Object.fromEntries(["perps", "prediction", "meme"].map((m) => [
|
|
163
|
+
m,
|
|
164
|
+
{
|
|
165
|
+
status: "unavailable",
|
|
166
|
+
account_value_usd: null,
|
|
167
|
+
available_trading_usdc: null,
|
|
168
|
+
reason: "Authenticated balance source unavailable; retry without treating it as zero.",
|
|
169
|
+
},
|
|
170
|
+
])),
|
|
171
|
+
},
|
|
172
|
+
}, !["ok", "empty", "stale"].includes(portfolio.status)
|
|
173
|
+
? portfolio.status
|
|
174
|
+
: partial
|
|
175
|
+
? "stale"
|
|
176
|
+
: portfolio.status === "empty" &&
|
|
177
|
+
(hasAppCopies || D(balances?.summary.known_subtotal_usd ?? 0).gt(0))
|
|
178
|
+
? "ok"
|
|
179
|
+
: portfolio.status, [
|
|
180
|
+
...portfolio.warnings,
|
|
181
|
+
...(app?.warnings ?? [
|
|
182
|
+
"Signed in. Ride App copies could not be read; retry the query without repeating login.",
|
|
183
|
+
]),
|
|
184
|
+
...(balances
|
|
185
|
+
? [
|
|
186
|
+
"Overall account total is unavailable until every module is covered. Meme valuation is unavailable through this API; copy allocation remaining is not account balance.",
|
|
187
|
+
]
|
|
188
|
+
: ["Account balances are unavailable; no missing source is zero."]),
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
function maskAddresses(value) {
|
|
192
|
+
if (typeof value === "string")
|
|
193
|
+
return value.replace(/0x[a-fA-F0-9]{40}\b/g, (address) => `${address.slice(0, 6)}...${address.slice(-4)}`);
|
|
194
|
+
if (Array.isArray(value))
|
|
195
|
+
return value.map(maskAddresses);
|
|
196
|
+
if (value && typeof value === "object")
|
|
197
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskAddresses(v)]));
|
|
198
|
+
return value;
|
|
199
|
+
}
|