@oracle-agent/oracle 0.4.0 → 0.4.1
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/bin/oracle-data-mcp.mjs +3 -2
- package/package.json +1 -1
- package/src/data/http.mjs +69 -16
- package/src/data/providers/magiceden-sol.mjs +23 -5
package/bin/oracle-data-mcp.mjs
CHANGED
|
@@ -263,7 +263,7 @@ const tools = [
|
|
|
263
263
|
{
|
|
264
264
|
name: "nft_prepare_list",
|
|
265
265
|
description:
|
|
266
|
-
"Prepare an NFT listing for OpenSea EVM, Magic Eden Solana, or Satflow Bitcoin after explicit user review. Requires userConfirmed=true plus asset, marketplace, price, currency where applicable, and future expiry. Returns approval/signing actions, an unsigned Solana transaction, or an unsigned Bitcoin PSBT. Never signs, submits, or broadcasts.",
|
|
266
|
+
"Prepare an NFT listing for OpenSea EVM, Magic Eden Solana, or Satflow Bitcoin after explicit user review. Requires userConfirmed=true plus asset, marketplace, price, currency where applicable, and future expiry. Magic Eden Solana also requires confirmation exactly: list <tokenMint> on magiceden-sol for <priceSol> SOL until <expiry>. Returns approval/signing actions, an unsigned Solana transaction, or an unsigned Bitcoin PSBT. Never signs, submits, or broadcasts.",
|
|
267
267
|
inputSchema: {
|
|
268
268
|
type: "object",
|
|
269
269
|
required: ["marketplace", "userConfirmed"],
|
|
@@ -284,7 +284,8 @@ const tools = [
|
|
|
284
284
|
tokenATA: { type: "string" },
|
|
285
285
|
auctionHouse: { type: "string" },
|
|
286
286
|
priceSol: { type: "string" },
|
|
287
|
-
expiry: { description: "Future ISO 8601 timestamp or Unix seconds." },
|
|
287
|
+
expiry: { description: "Future ISO 8601 timestamp or Unix seconds. Magic Eden Solana listings require finite future expiry; no-expiry is refused." },
|
|
288
|
+
confirmation: { type: "string", description: "Required for Magic Eden Solana listings. Exact phrase: list <tokenMint> on magiceden-sol for <priceSol> SOL until <expiry>." },
|
|
288
289
|
inscriptionId: { type: "string" },
|
|
289
290
|
runesOutput: { type: "string" },
|
|
290
291
|
ordAddress: { type: "string" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oracle-agent/oracle",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default — the public package never takes your key. Built for Hermes; no model key required.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/data/http.mjs
CHANGED
|
@@ -37,6 +37,35 @@ const IDENTITY_HEADERS = [
|
|
|
37
37
|
"proxy-authorization",
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
+
const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
|
|
41
|
+
const MAX_REDIRECTS = 3;
|
|
42
|
+
|
|
43
|
+
function credentialHeaderName(name) {
|
|
44
|
+
const h = String(name || "").toLowerCase();
|
|
45
|
+
return IDENTITY_HEADERS.includes(h) || /(^|-)(api[-_]?key|key|token|secret|auth|authorization|session|cookie|signature|access)(-|$)/.test(h);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripCredentialHeaders(headers) {
|
|
49
|
+
const safe = {};
|
|
50
|
+
for (const [name, value] of Object.entries(headers || {})) {
|
|
51
|
+
if (!credentialHeaderName(name)) safe[name] = value;
|
|
52
|
+
}
|
|
53
|
+
return safe;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function redirectedUrl(location, currentUrl) {
|
|
57
|
+
if (!location) throw new Error(`HTTP redirect from ${currentUrl} missing Location`);
|
|
58
|
+
const next = new URL(String(location), String(currentUrl));
|
|
59
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") {
|
|
60
|
+
throw new Error(`HTTP redirect from ${currentUrl} used unsupported protocol ${next.protocol}`);
|
|
61
|
+
}
|
|
62
|
+
return next.toString();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sameOrigin(a, b) {
|
|
66
|
+
return new URL(String(a)).origin === new URL(String(b)).origin;
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
const inflight = new Map();
|
|
41
70
|
|
|
42
71
|
/**
|
|
@@ -77,24 +106,48 @@ async function once(url, { fetchImpl, method, headers, body, timeoutMs }) {
|
|
|
77
106
|
const ac = new AbortController();
|
|
78
107
|
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
79
108
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
let currentUrl = String(url);
|
|
110
|
+
let currentHeaders = { ...(headers || {}) };
|
|
111
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
|
|
112
|
+
const res = await fetchImpl(currentUrl, {
|
|
113
|
+
method,
|
|
114
|
+
headers: currentHeaders,
|
|
115
|
+
body,
|
|
116
|
+
signal: ac.signal,
|
|
117
|
+
redirect: "manual",
|
|
118
|
+
});
|
|
119
|
+
const text = await res.text();
|
|
120
|
+
let json = null;
|
|
121
|
+
if (text) {
|
|
122
|
+
try {
|
|
123
|
+
json = JSON.parse(text);
|
|
124
|
+
} catch {
|
|
125
|
+
json = null;
|
|
126
|
+
}
|
|
88
127
|
}
|
|
128
|
+
if (REDIRECT_STATUS.has(res.status)) {
|
|
129
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
130
|
+
const err = new Error(`HTTP ${res.status} ${method} ${currentUrl} redirected non-idempotent request`);
|
|
131
|
+
err.status = res.status;
|
|
132
|
+
err.body = json ?? text.slice(0, 300);
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
if (redirects === MAX_REDIRECTS) throw new Error(`HTTP redirect loop for ${url}`);
|
|
136
|
+
const nextUrl = redirectedUrl(res.headers?.get?.("location"), currentUrl);
|
|
137
|
+
if (!sameOrigin(nextUrl, currentUrl)) currentHeaders = stripCredentialHeaders(currentHeaders);
|
|
138
|
+
currentUrl = nextUrl;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const err = new Error(`HTTP ${res.status} ${method} ${currentUrl}`);
|
|
143
|
+
err.status = res.status;
|
|
144
|
+
err.retryAfter = res.headers?.get?.("retry-after") ?? null;
|
|
145
|
+
err.body = json ?? text.slice(0, 300);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
return json ?? text;
|
|
89
149
|
}
|
|
90
|
-
|
|
91
|
-
const err = new Error(`HTTP ${res.status} ${method} ${url}`);
|
|
92
|
-
err.status = res.status;
|
|
93
|
-
err.retryAfter = res.headers?.get?.("retry-after") ?? null;
|
|
94
|
-
err.body = json ?? text.slice(0, 300);
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
return json ?? text;
|
|
150
|
+
throw new Error(`HTTP redirect loop for ${url}`);
|
|
98
151
|
} finally {
|
|
99
152
|
clearTimeout(t);
|
|
100
153
|
}
|
|
@@ -90,17 +90,33 @@ function lamports(sol) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function listingExpiry(value) {
|
|
93
|
-
if (value == null || value === "")
|
|
93
|
+
if (value == null || value === "") throw new Error("magiceden: listing expiry is required and must be in the future");
|
|
94
94
|
const expiry = Number(value);
|
|
95
|
-
if (!Number.isSafeInteger(expiry) || expiry
|
|
96
|
-
throw new Error("magiceden: expiry must be a
|
|
95
|
+
if (!Number.isSafeInteger(expiry) || expiry <= 0) {
|
|
96
|
+
throw new Error("magiceden: expiry must be a future Unix timestamp in seconds");
|
|
97
97
|
}
|
|
98
|
-
if (expiry
|
|
99
|
-
throw new Error("magiceden: expiry must be in the future
|
|
98
|
+
if (expiry <= Math.floor(Date.now() / 1000)) {
|
|
99
|
+
throw new Error("magiceden: expiry must be in the future");
|
|
100
100
|
}
|
|
101
101
|
return expiry;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function listConfirmation({ tokenMint, priceSol, expiry }) {
|
|
105
|
+
return `list ${tokenMint} on magiceden-sol for ${priceSol} SOL until ${expiry}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requireListingConfirmation(args, fields) {
|
|
109
|
+
if (args.userConfirmed !== true) {
|
|
110
|
+
throw new Error("magiceden: listing requires userConfirmed=true after reviewing tokenMint, priceSol, and expiry");
|
|
111
|
+
}
|
|
112
|
+
const expected = listConfirmation(fields);
|
|
113
|
+
const actual = String(args.confirmation || args.confirmationPhrase || "").trim();
|
|
114
|
+
if (actual !== expected) {
|
|
115
|
+
throw new Error(`magiceden: confirmation must equal ${JSON.stringify(expected)}`);
|
|
116
|
+
}
|
|
117
|
+
return expected;
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
export async function magicEdenSolHealth(opts = {}) {
|
|
105
121
|
try {
|
|
106
122
|
const stats = await magicEdenSolStats({ symbol: "mad_lads" }, opts);
|
|
@@ -269,6 +285,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
269
285
|
const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
|
|
270
286
|
const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
|
|
271
287
|
const expiry = listingExpiry(args.expiry);
|
|
288
|
+
const confirmation = requireListingConfirmation(args, { tokenMint, priceSol, expiry });
|
|
272
289
|
const url = new URL(`${base(opts)}/instructions/sell`);
|
|
273
290
|
url.searchParams.set("seller", seller);
|
|
274
291
|
url.searchParams.set("auctionHouseAddress", auctionHouse);
|
|
@@ -301,6 +318,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
301
318
|
tokenMint,
|
|
302
319
|
priceSol,
|
|
303
320
|
expiry,
|
|
321
|
+
confirmation,
|
|
304
322
|
transaction,
|
|
305
323
|
transactionEncoding: "base64",
|
|
306
324
|
raw,
|