@oracle-agent/oracle 0.3.2 → 0.3.3
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/README.md +12 -5
- package/SETUP.md +35 -1
- package/artifacts/specialist-packs/oracle-full-crypto.json +31 -9
- package/bin/oracle-data-mcp.mjs +247 -4
- package/bin/oracle-init.mjs +100 -27
- package/docs/profiles.md +27 -7
- package/package.json +1 -1
- package/profiles/_template/SOUL.md +8 -1
- package/profiles/oracle/SOUL.md +8 -1
- package/profiles/oracle/profile.json +5 -2
- package/profiles/protocol-builder/SOUL.md +13 -6
- package/profiles/protocol-builder/profile.json +3 -1
- package/skills/balance/SKILL.md +176 -0
- package/skills/oracle-multichain-nft-launch/SKILL.md +338 -0
- package/skills/oracle-multichain-token-launch/SKILL.md +300 -0
- package/src/data/catalog.mjs +27 -3
- package/src/data/desk-data.mjs +34 -4
- package/src/data/providers/magiceden-sol.mjs +21 -2
- package/src/data/providers/nft-gallery.mjs +163 -0
- package/src/data/providers/nft-portfolio.mjs +494 -0
- package/src/data/providers/opensea-nft.mjs +272 -0
- package/src/data/providers/portfolio-history.mjs +394 -0
- package/src/data/providers/portfolio.mjs +594 -0
- package/src/data/providers/satflow.mjs +1 -0
- package/src/exec-policy.mjs +5 -0
- package/src/gmx-attestation.mjs +1 -0
- package/src/vault-attestation.mjs +1 -0
package/src/data/desk-data.mjs
CHANGED
|
@@ -40,6 +40,9 @@ import * as balancer from "./providers/balancer.mjs";
|
|
|
40
40
|
import * as pendle from "./providers/pendle.mjs";
|
|
41
41
|
import * as odos from "./providers/odos.mjs";
|
|
42
42
|
import * as blockscout from "./providers/blockscout.mjs";
|
|
43
|
+
import * as portfolio from "./providers/portfolio.mjs";
|
|
44
|
+
import * as portfolioHistory from "./providers/portfolio-history.mjs";
|
|
45
|
+
import * as nftPortfolio from "./providers/nft-portfolio.mjs";
|
|
43
46
|
import * as paraswap from "./providers/paraswap.mjs";
|
|
44
47
|
|
|
45
48
|
const OPS = {
|
|
@@ -90,6 +93,20 @@ const OPS = {
|
|
|
90
93
|
erc20Balance: (o, a = {}) => rpc.erc20BalanceOf(a, o),
|
|
91
94
|
transactionReceipt: (o, a = {}) => rpc.transactionReceipt(a, o),
|
|
92
95
|
},
|
|
96
|
+
portfolio: {
|
|
97
|
+
health: (o) => portfolio.portfolioHealth(o),
|
|
98
|
+
balances: (o, a = {}) => portfolio.portfolioBalance(a, o),
|
|
99
|
+
snapshot: (o, a = {}) => portfolioHistory.portfolioSnapshot(a, o),
|
|
100
|
+
history: (o, a = {}) => portfolioHistory.portfolioHistory(a, o),
|
|
101
|
+
valueGraph: (o, a = {}) => portfolioHistory.portfolioValueGraph(a, o),
|
|
102
|
+
},
|
|
103
|
+
"nft-portfolio": {
|
|
104
|
+
health: (o) => nftPortfolio.nftHealth(o),
|
|
105
|
+
inventory: (o, a = {}) => nftPortfolio.nftInventory(a, o),
|
|
106
|
+
gallery: (o, a = {}) => nftPortfolio.nftPortfolioGallery(a, o),
|
|
107
|
+
pnl: (o, a = {}) => nftPortfolio.nftPnl(a, o),
|
|
108
|
+
prepareList: (o, a = {}) => nftPortfolio.nftPrepareList(a, o),
|
|
109
|
+
},
|
|
93
110
|
"solana-rpc": {
|
|
94
111
|
health: (o) => solana.solanaHealth(o),
|
|
95
112
|
latestBlockhash: (o, a = {}) => solana.solanaLatestBlockhash(a, o),
|
|
@@ -213,6 +230,9 @@ const OPS = {
|
|
|
213
230
|
health: (o) => osnft.openseaHealth(o),
|
|
214
231
|
collection: (o, a = {}) => osnft.openseaCollection(a.slug, o),
|
|
215
232
|
floor: (o, a = {}) => osnft.openseaFloor(a.slug, o),
|
|
233
|
+
accountNfts: (o, a = {}) => osnft.openseaAccountNfts(a, o),
|
|
234
|
+
accountPnl: (o, a = {}) => osnft.openseaAccountPnl(a, o),
|
|
235
|
+
prepareList: (o, a = {}) => osnft.openseaPrepareList(a, o),
|
|
216
236
|
},
|
|
217
237
|
|
|
218
238
|
"hl-outcome": {
|
|
@@ -570,6 +590,20 @@ export const data = {
|
|
|
570
590
|
call: (chainId, method, params, o) =>
|
|
571
591
|
dataCall("evm-rpc", "call", { chainId, method, params }, o),
|
|
572
592
|
},
|
|
593
|
+
portfolio: {
|
|
594
|
+
balance: (a, o) => dataCall("portfolio", "balances", a || {}, o),
|
|
595
|
+
snapshot: (a, o) => dataCall("portfolio", "snapshot", a || {}, o),
|
|
596
|
+
history: (a, o) => dataCall("portfolio", "history", a || {}, o),
|
|
597
|
+
valueGraph: (a, o) => dataCall("portfolio", "valueGraph", a || {}, o),
|
|
598
|
+
},
|
|
599
|
+
nft: {
|
|
600
|
+
inventory: (a, o) => dataCall("nft-portfolio", "inventory", a || {}, o),
|
|
601
|
+
gallery: (a, o) => dataCall("nft-portfolio", "gallery", a || {}, o),
|
|
602
|
+
pnl: (a, o) => dataCall("nft-portfolio", "pnl", a || {}, o),
|
|
603
|
+
prepareList: (a, o) => dataCall("nft-portfolio", "prepareList", a || {}, o),
|
|
604
|
+
floor: (slug, o) => dataCall("opensea-nft", "floor", { slug }, o),
|
|
605
|
+
collection: (slug, o) => dataCall("opensea-nft", "collection", { slug }, o),
|
|
606
|
+
},
|
|
573
607
|
solana: {
|
|
574
608
|
health: (o) => dataCall("solana-rpc", "health", {}, o),
|
|
575
609
|
latestBlockhash: (a, o) => dataCall("solana-rpc", "latestBlockhash", a || {}, o),
|
|
@@ -633,10 +667,6 @@ export const data = {
|
|
|
633
667
|
cow: {
|
|
634
668
|
quote: (a, o) => dataCall("cowswap", "quote", a || {}, o),
|
|
635
669
|
},
|
|
636
|
-
nft: {
|
|
637
|
-
floor: (slug, o) => dataCall("opensea-nft", "floor", { slug }, o),
|
|
638
|
-
collection: (slug, o) => dataCall("opensea-nft", "collection", { slug }, o),
|
|
639
|
-
},
|
|
640
670
|
hlWs: {
|
|
641
671
|
allMids: (o) => dataCall("hl-ws", "allMids", {}, o),
|
|
642
672
|
l2Book: (coin, o) => dataCall("hl-ws", "l2Book", { coin }, o),
|
|
@@ -89,6 +89,18 @@ function lamports(sol) {
|
|
|
89
89
|
return Math.round(positiveNumber(sol, "priceSol") * LAMPORTS_PER_SOL);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function listingExpiry(value) {
|
|
93
|
+
if (value == null || value === "") return null;
|
|
94
|
+
const expiry = Number(value);
|
|
95
|
+
if (!Number.isSafeInteger(expiry) || expiry < 0) {
|
|
96
|
+
throw new Error("magiceden: expiry must be a whole Unix timestamp in seconds or 0");
|
|
97
|
+
}
|
|
98
|
+
if (expiry !== 0 && expiry <= Math.floor(Date.now() / 1000)) {
|
|
99
|
+
throw new Error("magiceden: expiry must be in the future or 0 for no expiry");
|
|
100
|
+
}
|
|
101
|
+
return expiry;
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
export async function magicEdenSolHealth(opts = {}) {
|
|
93
105
|
try {
|
|
94
106
|
const stats = await magicEdenSolStats({ symbol: "mad_lads" }, opts);
|
|
@@ -256,20 +268,26 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
256
268
|
const tokenATA = solanaPubkey(args.tokenATA || args.tokenAddress, "tokenATA");
|
|
257
269
|
const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
|
|
258
270
|
const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
|
|
271
|
+
const expiry = listingExpiry(args.expiry);
|
|
259
272
|
const url = new URL(`${base(opts)}/instructions/sell`);
|
|
260
273
|
url.searchParams.set("seller", seller);
|
|
261
274
|
url.searchParams.set("auctionHouseAddress", auctionHouse);
|
|
262
275
|
url.searchParams.set("tokenMint", tokenMint);
|
|
263
276
|
url.searchParams.set("tokenAccount", tokenATA);
|
|
264
277
|
url.searchParams.set("price", String(priceSol));
|
|
278
|
+
if (args.sellerReferral) url.searchParams.set("sellerReferral", solanaPubkey(args.sellerReferral, "sellerReferral"));
|
|
279
|
+
if (expiry != null) url.searchParams.set("expiry", String(expiry));
|
|
265
280
|
const raw = await httpJson(url.toString(), {
|
|
266
281
|
headers: headers(opts),
|
|
267
282
|
fetchImpl: opts.fetchImpl,
|
|
268
283
|
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
269
284
|
});
|
|
270
285
|
const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
|
|
271
|
-
|
|
272
|
-
|
|
286
|
+
if (!Array.isArray(data)) {
|
|
287
|
+
if (raw?.txSigned) throw new Error("magiceden: refused a pre-signed transaction payload from the API");
|
|
288
|
+
throw new Error("magiceden: sell returned no unsigned transaction payload");
|
|
289
|
+
}
|
|
290
|
+
const transaction = Buffer.from(data).toString("base64");
|
|
273
291
|
return stampPrepared({
|
|
274
292
|
provider: "magiceden-sol",
|
|
275
293
|
chain: "solana-mainnet-beta",
|
|
@@ -282,6 +300,7 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
282
300
|
seller,
|
|
283
301
|
tokenMint,
|
|
284
302
|
priceSol,
|
|
303
|
+
expiry,
|
|
285
304
|
transaction,
|
|
286
305
|
transactionEncoding: "base64",
|
|
287
306
|
raw,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Raster-only NFT contact sheets returned as SVG image content.
|
|
2
|
+
|
|
3
|
+
import { timed } from "../http.mjs";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_IMAGE_HOSTS = Object.freeze([
|
|
6
|
+
"i.seadn.io",
|
|
7
|
+
"openseauserdata.com",
|
|
8
|
+
"arweave.net",
|
|
9
|
+
"ipfs.io",
|
|
10
|
+
"nftstorage.link",
|
|
11
|
+
"dweb.link",
|
|
12
|
+
"cloudflare-ipfs.com",
|
|
13
|
+
"ordinals.com",
|
|
14
|
+
"ordinals.g.com",
|
|
15
|
+
"cdn.helius-rpc.com",
|
|
16
|
+
"shdw-drive.genesysgo.net",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const RASTER_MIME = new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
20
|
+
|
|
21
|
+
function allowedHost(hostname) {
|
|
22
|
+
const host = String(hostname || "").toLowerCase();
|
|
23
|
+
return ALLOWED_IMAGE_HOSTS.some((allowed) => host === allowed || host.endsWith(`.${allowed}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function gatewayUrl(value) {
|
|
27
|
+
const text = String(value || "").trim();
|
|
28
|
+
if (text.startsWith("ipfs://")) return `https://ipfs.io/ipfs/${text.slice(7).replace(/^ipfs\//, "")}`;
|
|
29
|
+
if (text.startsWith("ar://")) return `https://arweave.net/${text.slice(5)}`;
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeImageUrl(value) {
|
|
34
|
+
const text = gatewayUrl(value);
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(text);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (url.protocol !== "https:" || url.username || url.password || !allowedHost(url.hostname)) return null;
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchRaster(value, opts = {}) {
|
|
46
|
+
const fetchImpl = opts.fetchImpl || globalThis.fetch;
|
|
47
|
+
if (typeof fetchImpl !== "function") throw new Error("gallery fetch unavailable");
|
|
48
|
+
let url = safeImageUrl(value);
|
|
49
|
+
if (!url) throw new Error("image host is not on the raster gallery allowlist");
|
|
50
|
+
const maxBytes = Math.min(Math.max(Number(opts.maxImageBytes ?? 1_500_000), 50_000), 4_000_000);
|
|
51
|
+
|
|
52
|
+
for (let redirect = 0; redirect <= 3; redirect += 1) {
|
|
53
|
+
const attempt = await timed(() => fetchImpl(url.toString(), {
|
|
54
|
+
method: "GET",
|
|
55
|
+
redirect: "manual",
|
|
56
|
+
headers: { accept: "image/jpeg,image/png,image/webp" },
|
|
57
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 12_000),
|
|
58
|
+
}));
|
|
59
|
+
if (!attempt.ok) throw new Error(attempt.error);
|
|
60
|
+
const response = attempt.data;
|
|
61
|
+
if (response.status >= 300 && response.status < 400) {
|
|
62
|
+
const location = response.headers?.get?.("location");
|
|
63
|
+
if (!location) throw new Error("image redirect missing location");
|
|
64
|
+
const next = safeImageUrl(new URL(location, url).toString());
|
|
65
|
+
if (!next) throw new Error("image redirect left the host allowlist");
|
|
66
|
+
url = next;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) throw new Error(`image HTTP ${response.status}`);
|
|
70
|
+
const mimeType = String(response.headers?.get?.("content-type") || "").split(";", 1)[0].toLowerCase();
|
|
71
|
+
if (!RASTER_MIME.has(mimeType)) throw new Error(`unsafe or unsupported image type ${mimeType || "unknown"}`);
|
|
72
|
+
const length = Number(response.headers?.get?.("content-length") || 0);
|
|
73
|
+
if (length > maxBytes) throw new Error("image exceeds gallery byte cap");
|
|
74
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
75
|
+
if (!bytes.length || bytes.length > maxBytes) throw new Error("image exceeds gallery byte cap");
|
|
76
|
+
return { mimeType, data: bytes.toString("base64"), sourceUrl: url.toString(), bytes: bytes.length };
|
|
77
|
+
}
|
|
78
|
+
throw new Error("too many image redirects");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function xml(value) {
|
|
82
|
+
return String(value ?? "")
|
|
83
|
+
.replaceAll("&", "&")
|
|
84
|
+
.replaceAll("<", "<")
|
|
85
|
+
.replaceAll(">", ">")
|
|
86
|
+
.replaceAll('"', """)
|
|
87
|
+
.replaceAll("'", "'");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function label(item, index) {
|
|
91
|
+
return item.name || item.collection || item.tokenId || `NFT ${index + 1}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function nftGallery(args = {}, opts = {}) {
|
|
95
|
+
const allItems = Array.isArray(args.items) ? args.items : [];
|
|
96
|
+
if (!allItems.length) throw new Error("nft gallery requires inventory items");
|
|
97
|
+
const pageSize = Math.min(Math.max(Number(args.pageSize ?? 12) || 12, 1), 12);
|
|
98
|
+
const page = Math.max(Number(args.page ?? 1) || 1, 1);
|
|
99
|
+
const totalPages = Math.max(Math.ceil(allItems.length / pageSize), 1);
|
|
100
|
+
if (page > totalPages) throw new Error(`nft gallery page ${page} exceeds ${totalPages}`);
|
|
101
|
+
const items = allItems.slice((page - 1) * pageSize, page * pageSize);
|
|
102
|
+
const loaded = [];
|
|
103
|
+
let totalBytes = 0;
|
|
104
|
+
const maxTotalBytes = Math.min(Math.max(Number(args.maxTotalBytes ?? 8_000_000), 250_000), 12_000_000);
|
|
105
|
+
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
if (totalBytes >= maxTotalBytes) {
|
|
108
|
+
loaded.push({ item, image: null, error: "gallery page byte cap reached" });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const image = await fetchRaster(item.imageUrl || item.originalImageUrl, {
|
|
113
|
+
...opts,
|
|
114
|
+
maxImageBytes: Math.min(Number(args.maxImageBytes ?? 1_500_000), maxTotalBytes - totalBytes),
|
|
115
|
+
});
|
|
116
|
+
totalBytes += image.bytes;
|
|
117
|
+
loaded.push({ item, image, error: null });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
loaded.push({ item, image: null, error: String(error?.message || error).slice(0, 180) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const columns = 3;
|
|
124
|
+
const tileWidth = 340;
|
|
125
|
+
const tileHeight = 400;
|
|
126
|
+
const headerHeight = 76;
|
|
127
|
+
const rows = Math.ceil(items.length / columns);
|
|
128
|
+
const width = columns * tileWidth;
|
|
129
|
+
const height = headerHeight + rows * tileHeight;
|
|
130
|
+
const body = loaded.map(({ item, image, error }, index) => {
|
|
131
|
+
const column = index % columns;
|
|
132
|
+
const row = Math.floor(index / columns);
|
|
133
|
+
const x = column * tileWidth;
|
|
134
|
+
const y = headerHeight + row * tileHeight;
|
|
135
|
+
const art = image
|
|
136
|
+
? `<image x="${x + 14}" y="${y + 14}" width="312" height="312" preserveAspectRatio="xMidYMid slice" href="data:${image.mimeType};base64,${image.data}"/>`
|
|
137
|
+
: `<rect x="${x + 14}" y="${y + 14}" width="312" height="312" rx="14" fill="#151820"/><text x="${x + 170}" y="${y + 165}" text-anchor="middle" fill="#76808f" font-size="16">image unavailable</text>`;
|
|
138
|
+
const title = xml(label(item, index).slice(0, 34));
|
|
139
|
+
const chain = xml(item.chain || "unknown chain");
|
|
140
|
+
return `${art}<text x="${x + 16}" y="${y + 350}" fill="#f3f5f7" font-size="17" font-weight="600">${title}</text><text x="${x + 16}" y="${y + 376}" fill="#8d98a8" font-size="13">${chain}${error ? " · placeholder" : ""}</text>`;
|
|
141
|
+
}).join("");
|
|
142
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#090b10"/><text x="24" y="34" fill="#ffffff" font-size="24" font-weight="700">NFT INVENTORY</text><text x="24" y="58" fill="#8d98a8" font-size="14">page ${page} of ${totalPages} · ${allItems.length} assets · generated ${xml(new Date().toISOString())}</text>${body}</svg>`;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
provider: "nft-portfolio",
|
|
146
|
+
operation: "gallery",
|
|
147
|
+
mimeType: "image/svg+xml",
|
|
148
|
+
dataBase64: Buffer.from(svg).toString("base64"),
|
|
149
|
+
page,
|
|
150
|
+
pageSize,
|
|
151
|
+
totalPages,
|
|
152
|
+
totalItems: allItems.length,
|
|
153
|
+
renderedItems: loaded.filter((row) => row.image).length,
|
|
154
|
+
placeholders: loaded.filter((row) => !row.image).map((row) => ({
|
|
155
|
+
assetKey: row.item.assetKey || null,
|
|
156
|
+
name: row.item.name || null,
|
|
157
|
+
sourceUrl: row.item.imageUrl || null,
|
|
158
|
+
error: row.error,
|
|
159
|
+
})),
|
|
160
|
+
rasterOnly: true,
|
|
161
|
+
externalScripts: false,
|
|
162
|
+
};
|
|
163
|
+
}
|