@oracle-agent/oracle 0.3.3 → 0.3.4
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 +5 -12
- package/SETUP.md +1 -35
- package/artifacts/specialist-packs/oracle-full-crypto.json +9 -31
- package/bin/oracle-data-mcp.mjs +4 -247
- package/bin/oracle-init.mjs +27 -100
- package/docs/profiles.md +7 -27
- package/package.json +1 -1
- package/profiles/_template/SOUL.md +1 -8
- package/profiles/oracle/SOUL.md +1 -8
- package/profiles/oracle/profile.json +2 -5
- package/profiles/protocol-builder/SOUL.md +6 -13
- package/profiles/protocol-builder/profile.json +1 -3
- package/src/data/catalog.mjs +3 -27
- package/src/data/desk-data.mjs +4 -34
- package/src/data/providers/magiceden-sol.mjs +2 -21
- package/src/data/providers/opensea-nft.mjs +0 -272
- package/src/data/providers/satflow.mjs +0 -1
- package/src/data/providers/uniswap-v3.mjs +17 -1
- package/src/exec-policy.mjs +0 -5
- package/src/gmx-attestation.mjs +0 -1
- package/src/router/prepare-route.mjs +23 -0
- package/src/router/route-sources.mjs +37 -0
- package/src/vault-attestation.mjs +0 -1
- package/skills/balance/SKILL.md +0 -176
- package/skills/oracle-multichain-nft-launch/SKILL.md +0 -338
- package/skills/oracle-multichain-token-launch/SKILL.md +0 -300
- package/src/data/providers/nft-gallery.mjs +0 -163
- package/src/data/providers/nft-portfolio.mjs +0 -494
- package/src/data/providers/portfolio-history.mjs +0 -394
- package/src/data/providers/portfolio.mjs +0 -594
package/docs/profiles.md
CHANGED
|
@@ -34,28 +34,19 @@ Three practical reasons, learned the hard way:
|
|
|
34
34
|
|
|
35
35
|
| Profile | Owns | Typical grant |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `oracle` | routing, synthesis, multi-chain comparison
|
|
37
|
+
| `oracle` | routing, synthesis, multi-chain comparison | read + simulate only |
|
|
38
38
|
| `polymarket-agent` | prediction markets, event odds, CLOB cards/API-key order intents | read, quote, prepare |
|
|
39
39
|
| `hyperliquid-agent` | perps, spot, HIP-3 builder dexs, HIP-4 outcomes | read, quote, prepare |
|
|
40
40
|
| `robinhood-agent` | Robinhood Chain (4663) tokens, NFTs, tokenized Robinhood-style assets, capped NFT mints | read, quote, prepare |
|
|
41
41
|
| `solana-agent` | Solana swaps, research, Jupiter routes | read, quote, prepare |
|
|
42
42
|
| `bitcoin-agent` | Bitcoin L1, Ordinals/runes, inscriptions | read, prepare:inscription |
|
|
43
43
|
| `stable-agent` | Stable (988), USDT-native gas quirks | read, quote, prepare |
|
|
44
|
-
| `protocol-builder` | scaffold, review, prepare
|
|
44
|
+
| `protocol-builder` | scaffold, review, prepare protocol/NFT/gacha/DEX deploys and mint bots | prepare:deploy, prepare:mint, simulate |
|
|
45
45
|
| `_template` | your new lane | you decide |
|
|
46
46
|
|
|
47
|
-
`protocol-builder`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
metadata, liquidity, mint, and authority actions as separate user approvals.
|
|
51
|
-
|
|
52
|
-
The root `oracle` lane owns `/balance`, natural-language balance, and portfolio
|
|
53
|
-
history requests. Its `balance` skill calls the read-only
|
|
54
|
-
`portfolio_snapshot` MCP tool once, records a compact profile-local observation,
|
|
55
|
-
reports partial coverage and unavailable providers, and labels `knownUsd` as
|
|
56
|
-
incomplete instead of inventing a full portfolio total. `portfolio_history`
|
|
57
|
-
reads those observations and `portfolio_value_graph` renders the known-value
|
|
58
|
-
series while omitting unavailable values rather than plotting fake zeroes.
|
|
47
|
+
`protocol-builder` can design a protocol and prepare an unsigned deploy or mint
|
|
48
|
+
bot transaction. It never house-signs, and its destinations and gas-war caps are
|
|
49
|
+
allowlisted like any other lane.
|
|
59
50
|
|
|
60
51
|
## Model choice is yours
|
|
61
52
|
|
|
@@ -110,21 +101,10 @@ hermes profile create polymarket-agent
|
|
|
110
101
|
```
|
|
111
102
|
|
|
112
103
|
Then give it a `SOUL.md` (who it is, what it owns, what it must refuse) and a
|
|
113
|
-
`config.yaml` (model + provider). Point it at Oracle's MCP read plane
|
|
114
|
-
|
|
115
|
-
`oracle-init --apply` writes this for you. Manual form (Hermes wants command and
|
|
116
|
-
args as separate tokens):
|
|
104
|
+
`config.yaml` (model + provider). Point it at Oracle's MCP read plane:
|
|
117
105
|
|
|
118
106
|
```bash
|
|
119
|
-
|
|
120
|
-
npx oracle-data
|
|
121
|
-
|
|
122
|
-
# terminal 2 — wire MCP into a lane
|
|
123
|
-
hermes -p polymarket-agent mcp add oracle-data \
|
|
124
|
-
--command node \
|
|
125
|
-
--args "$(node -p "require.resolve('@oracle-agent/oracle/package.json').replace(/package\\.json$/, 'bin/oracle-data-mcp.mjs')")"
|
|
126
|
-
# or after npm link / PATH has the bin:
|
|
127
|
-
# hermes -p polymarket-agent mcp add oracle-data --command oracle-data-mcp
|
|
107
|
+
hermes mcp add oracle-data --command "oracle-data-mcp"
|
|
128
108
|
```
|
|
129
109
|
|
|
130
110
|
Now that lane can read 30+ providers across 11 chains, quote real routes, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oracle-agent/oracle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|
|
@@ -38,15 +38,8 @@ Terse. Answer first, evidence second. State confidence: `high` / `moderate` /
|
|
|
38
38
|
## Wiring it up
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
# preferred: installer writes SOUL, skills, and MCP config
|
|
42
|
-
npx oracle-init --apply
|
|
43
|
-
|
|
44
|
-
# or by hand
|
|
45
41
|
hermes profile create my-lane
|
|
46
|
-
|
|
47
|
-
hermes -p my-lane mcp add oracle-data --command oracle-data-mcp
|
|
48
|
-
# if the bin is not on PATH:
|
|
49
|
-
# hermes -p my-lane mcp add oracle-data --command node --args /abs/path/to/oracle-data-mcp.mjs
|
|
42
|
+
hermes mcp add oracle-data --command "oracle-data-mcp"
|
|
50
43
|
```
|
|
51
44
|
|
|
52
45
|
Then copy this `SOUL.md` into `~/.hermes/profiles/my-lane/SOUL.md` and set the
|
package/profiles/oracle/SOUL.md
CHANGED
|
@@ -20,11 +20,9 @@ value yourself.
|
|
|
20
20
|
| Stable (988), USDT-native gas | `stable-agent` |
|
|
21
21
|
| tokenized Robinhood-style assets / stock tokens | exact home-chain lane + `oracle-rfq-tokenized-assets` |
|
|
22
22
|
| meme-token launches, sniping, liquidity/pool watches | token's home-chain lane + `oracle-meme-token-sniper` |
|
|
23
|
-
|
|
|
24
|
-
| deploy/review custom contracts, gacha, DEX, launchpad, or capped NFT mint bot | `protocol-builder` + `oracle-nft-mint-gas-war` |
|
|
23
|
+
| deploy/review a contract, NFT/gacha launch, DEX, or capped NFT mint bot | `protocol-builder` + `oracle-nft-mint-gas-war` |
|
|
25
24
|
| RFQ / solver-intent route comparison across chains | `oracle` + `oracle-rfq-tokenized-assets` |
|
|
26
25
|
| graph/card alert rendering | token's home-chain lane + `oracle-chain-graphs-telegram-cards` |
|
|
27
|
-
| `/balance`, balance, holdings, wallet portfolio | `oracle` + `balance`; one deterministic `portfolio_snapshot` read plus profile-local observation |
|
|
28
26
|
| compare chains, "which is cheaper" | you, using the data plane |
|
|
29
27
|
|
|
30
28
|
If the chain is ambiguous, resolve the token's home chain first (DexScreener via
|
|
@@ -55,11 +53,6 @@ the data plane). If it stays ambiguous, ask. Do not guess a chain.
|
|
|
55
53
|
delta → the action did not succeed. Say so plainly.
|
|
56
54
|
7. **Never invent chain facts.** If it did not come from a live read, label it
|
|
57
55
|
`unknown`.
|
|
58
|
-
8. **Balance uses one source of truth.** `/balance` and plain-language balance
|
|
59
|
-
requests call `portfolio_snapshot`; use `portfolio_history` and
|
|
60
|
-
`portfolio_value_graph` for historical requests. Report `knownUsd` as
|
|
61
|
-
incomplete whenever a provider, address, price, token/NFT indexer, or chain
|
|
62
|
-
adapter is missing. Never turn an unavailable historical value into zero.
|
|
63
56
|
|
|
64
57
|
## Confidence
|
|
65
58
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"label": "oracle",
|
|
5
5
|
"role": "router",
|
|
6
6
|
"color": "#7CC4FF",
|
|
7
|
-
"description": "Router for multichain trading, building, analysis,
|
|
7
|
+
"description": "Router for multichain trading, building, analysis, scanners, RFQ, tokenized-asset buys, NFT mint gas limits, meme-token sniping, Solana, Bitcoin, and protocol launches.",
|
|
8
8
|
"model": {
|
|
9
9
|
"note": "Wants the strongest reasoner available: routing and synthesis are judgment calls.",
|
|
10
10
|
"suggested": "strong-reasoner"
|
|
@@ -18,10 +18,7 @@
|
|
|
18
18
|
"oracle-meme-token-sniper",
|
|
19
19
|
"oracle-chain-graphs-telegram-cards",
|
|
20
20
|
"oracle-rfq-tokenized-assets",
|
|
21
|
-
"oracle-nft-mint-gas-war"
|
|
22
|
-
"oracle-multichain-token-launch",
|
|
23
|
-
"oracle-multichain-nft-launch",
|
|
24
|
-
"balance"
|
|
21
|
+
"oracle-nft-mint-gas-war"
|
|
25
22
|
],
|
|
26
23
|
"mcp": [
|
|
27
24
|
"oracle-data"
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# protocol builder
|
|
2
2
|
|
|
3
|
-
You
|
|
4
|
-
|
|
5
|
-
pages, and scanner-backed on-chain apps. You never sign one. Unsupported chain
|
|
6
|
-
adapters fail closed.
|
|
3
|
+
You design, review, and prepare deploys for protocols, NFT/gacha products, DEX
|
|
4
|
+
surfaces, launchpads, mint pages, and scanner-backed on-chain apps. You never sign one.
|
|
7
5
|
|
|
8
6
|
## What you own
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
protocols before cloning them.
|
|
8
|
+
Contract scaffolding, NFT/gacha mint mechanics, DEX/pool/launchpad design,
|
|
9
|
+
security review, deploy and verify scripts, and **unsigned** deploy transactions.
|
|
10
|
+
Research of existing protocols before cloning them.
|
|
14
11
|
|
|
15
12
|
## Deployment is permanent
|
|
16
13
|
|
|
@@ -45,11 +42,7 @@ what an attacker gains from each privileged function.
|
|
|
45
42
|
3. **Simulate before preparing.** An unsimulated deploy is a guess.
|
|
46
43
|
4. **State the authority model before the code.** A user who doesn't know who owns
|
|
47
44
|
the contract cannot consent to deploying it.
|
|
48
|
-
5. **Receipts or it didn't happen
|
|
49
|
-
6. **Chain-family support is explicit.** Use `TEMPLATE_READY`, `ADAPTER_READY`,
|
|
50
|
-
`GUIDED_BUILD`, `RESEARCH_ONLY`, or `UNSUPPORTED`. RPC reachability is not deploy support.
|
|
51
|
-
7. **One approval per side effect.** Deploy, metadata upload, mint, liquidity,
|
|
52
|
-
authority transfer/revoke, reveal, and verification remain separate.
|
|
45
|
+
5. **Receipts or it didn't happen** — deployed address, receipt, verified source.
|
|
53
46
|
|
|
54
47
|
## Voice
|
|
55
48
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"label": "protocol builder",
|
|
5
5
|
"role": "builder",
|
|
6
6
|
"color": "#ff8c5a",
|
|
7
|
-
"description": "Builder lane for protocol,
|
|
7
|
+
"description": "Builder lane for protocol, NFT/gacha, DEX, scanner, and mint-bot surfaces with unsigned deploy/mint preparation.",
|
|
8
8
|
"model": {
|
|
9
9
|
"note": "Contract review is unforgiving and mistakes are permanent; wants the strongest reasoner available.",
|
|
10
10
|
"suggested": "strong-reasoner"
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
"oracle-protocol-builder",
|
|
14
14
|
"oracle-contract-research",
|
|
15
15
|
"oracle-protocol-security",
|
|
16
|
-
"oracle-multichain-token-launch",
|
|
17
|
-
"oracle-multichain-nft-launch",
|
|
18
16
|
"oracle-nft-gacha-launch",
|
|
19
17
|
"oracle-dex-launch",
|
|
20
18
|
"oracle-receipts",
|
package/src/data/catalog.mjs
CHANGED
|
@@ -98,29 +98,6 @@ registerProvider({
|
|
|
98
98
|
description: "RH agent local HTTP (unauthenticated read routes)",
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
registerProvider({
|
|
102
|
-
id: "portfolio",
|
|
103
|
-
venue: "multichain-wallet",
|
|
104
|
-
chainIds: [1, 10, 56, 137, 988, 999, 2741, 4663, 8453, 42161, 43114],
|
|
105
|
-
auth: "none",
|
|
106
|
-
ops: ["health", "balances", "snapshot", "history", "valueGraph"],
|
|
107
|
-
execution: "read-only",
|
|
108
|
-
description:
|
|
109
|
-
"Read-only balance aggregation across configured EVM chains, Solana, Bitcoin, and Hyperliquid, plus profile-local observation snapshots, history, and value graphs with explicit partial coverage.",
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
registerProvider({
|
|
113
|
-
id: "nft-portfolio",
|
|
114
|
-
venue: "multichain-nft-wallet",
|
|
115
|
-
chainIds: [1, 10, 137, 988, 999, 2741, 4663, 8453, 42161, 43114],
|
|
116
|
-
auth: "optionalApiKey",
|
|
117
|
-
ops: ["health", "inventory", "gallery", "pnl", "prepareList"],
|
|
118
|
-
execution: "prepare",
|
|
119
|
-
baseEnv: ["OPENSEA_API_KEY", "MAGICEDEN_API_KEY", "SATFLOW_API_KEY"],
|
|
120
|
-
description:
|
|
121
|
-
"Normalized EVM, Solana, and Bitcoin NFT inventory, static contact-sheet galleries, explicit PnL coverage, and user-confirmed unsigned listing preparation.",
|
|
122
|
-
});
|
|
123
|
-
|
|
124
101
|
registerProvider({
|
|
125
102
|
id: "evm-rpc",
|
|
126
103
|
venue: "evm",
|
|
@@ -404,12 +381,11 @@ registerProvider({
|
|
|
404
381
|
registerProvider({
|
|
405
382
|
id: "opensea-nft",
|
|
406
383
|
venue: "nft",
|
|
407
|
-
chainIds: [1
|
|
384
|
+
chainIds: [1],
|
|
408
385
|
auth: "apiKey",
|
|
409
|
-
ops: ["health", "collection", "floor"
|
|
410
|
-
execution: "prepare",
|
|
386
|
+
ops: ["health", "collection", "floor"],
|
|
411
387
|
baseEnv: ["OPENSEA_API_KEY", "OPENSEA_ENV_FILE"],
|
|
412
|
-
description: "OpenSea
|
|
388
|
+
description: "OpenSea collection + floor (OPENSEA_API_KEY)",
|
|
413
389
|
});
|
|
414
390
|
|
|
415
391
|
registerProvider({
|
package/src/data/desk-data.mjs
CHANGED
|
@@ -40,9 +40,6 @@ 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";
|
|
46
43
|
import * as paraswap from "./providers/paraswap.mjs";
|
|
47
44
|
|
|
48
45
|
const OPS = {
|
|
@@ -93,20 +90,6 @@ const OPS = {
|
|
|
93
90
|
erc20Balance: (o, a = {}) => rpc.erc20BalanceOf(a, o),
|
|
94
91
|
transactionReceipt: (o, a = {}) => rpc.transactionReceipt(a, o),
|
|
95
92
|
},
|
|
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
|
-
},
|
|
110
93
|
"solana-rpc": {
|
|
111
94
|
health: (o) => solana.solanaHealth(o),
|
|
112
95
|
latestBlockhash: (o, a = {}) => solana.solanaLatestBlockhash(a, o),
|
|
@@ -230,9 +213,6 @@ const OPS = {
|
|
|
230
213
|
health: (o) => osnft.openseaHealth(o),
|
|
231
214
|
collection: (o, a = {}) => osnft.openseaCollection(a.slug, o),
|
|
232
215
|
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),
|
|
236
216
|
},
|
|
237
217
|
|
|
238
218
|
"hl-outcome": {
|
|
@@ -590,20 +570,6 @@ export const data = {
|
|
|
590
570
|
call: (chainId, method, params, o) =>
|
|
591
571
|
dataCall("evm-rpc", "call", { chainId, method, params }, o),
|
|
592
572
|
},
|
|
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
|
-
},
|
|
607
573
|
solana: {
|
|
608
574
|
health: (o) => dataCall("solana-rpc", "health", {}, o),
|
|
609
575
|
latestBlockhash: (a, o) => dataCall("solana-rpc", "latestBlockhash", a || {}, o),
|
|
@@ -667,6 +633,10 @@ export const data = {
|
|
|
667
633
|
cow: {
|
|
668
634
|
quote: (a, o) => dataCall("cowswap", "quote", a || {}, o),
|
|
669
635
|
},
|
|
636
|
+
nft: {
|
|
637
|
+
floor: (slug, o) => dataCall("opensea-nft", "floor", { slug }, o),
|
|
638
|
+
collection: (slug, o) => dataCall("opensea-nft", "collection", { slug }, o),
|
|
639
|
+
},
|
|
670
640
|
hlWs: {
|
|
671
641
|
allMids: (o) => dataCall("hl-ws", "allMids", {}, o),
|
|
672
642
|
l2Book: (coin, o) => dataCall("hl-ws", "l2Book", { coin }, o),
|
|
@@ -89,18 +89,6 @@ 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
|
-
|
|
104
92
|
export async function magicEdenSolHealth(opts = {}) {
|
|
105
93
|
try {
|
|
106
94
|
const stats = await magicEdenSolStats({ symbol: "mad_lads" }, opts);
|
|
@@ -268,26 +256,20 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
268
256
|
const tokenATA = solanaPubkey(args.tokenATA || args.tokenAddress, "tokenATA");
|
|
269
257
|
const auctionHouse = solanaPubkey(args.auctionHouse, "auctionHouse");
|
|
270
258
|
const priceSol = positiveNumber(args.priceSol ?? args.price, "priceSol");
|
|
271
|
-
const expiry = listingExpiry(args.expiry);
|
|
272
259
|
const url = new URL(`${base(opts)}/instructions/sell`);
|
|
273
260
|
url.searchParams.set("seller", seller);
|
|
274
261
|
url.searchParams.set("auctionHouseAddress", auctionHouse);
|
|
275
262
|
url.searchParams.set("tokenMint", tokenMint);
|
|
276
263
|
url.searchParams.set("tokenAccount", tokenATA);
|
|
277
264
|
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));
|
|
280
265
|
const raw = await httpJson(url.toString(), {
|
|
281
266
|
headers: headers(opts),
|
|
282
267
|
fetchImpl: opts.fetchImpl,
|
|
283
268
|
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
284
269
|
});
|
|
285
270
|
const data = raw?.v0?.tx?.data ?? raw?.tx?.data ?? null;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
throw new Error("magiceden: sell returned no unsigned transaction payload");
|
|
289
|
-
}
|
|
290
|
-
const transaction = Buffer.from(data).toString("base64");
|
|
271
|
+
const transaction = Array.isArray(data) ? Buffer.from(data).toString("base64") : null;
|
|
272
|
+
if (!transaction) throw new Error("magiceden: sell returned no transaction payload");
|
|
291
273
|
return stampPrepared({
|
|
292
274
|
provider: "magiceden-sol",
|
|
293
275
|
chain: "solana-mainnet-beta",
|
|
@@ -300,7 +282,6 @@ export async function magicEdenSolPrepareList(args = {}, opts = {}) {
|
|
|
300
282
|
seller,
|
|
301
283
|
tokenMint,
|
|
302
284
|
priceSol,
|
|
303
|
-
expiry,
|
|
304
285
|
transaction,
|
|
305
286
|
transactionEncoding: "base64",
|
|
306
287
|
raw,
|
|
@@ -1,49 +1,11 @@
|
|
|
1
1
|
// OpenSea NFT floors — uses OPENSEA_API_KEY from env or ~/.config/locals-only/opensea.env
|
|
2
2
|
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
-
import { getAddress } from "ethers";
|
|
5
4
|
import { httpJson } from "../http.mjs";
|
|
6
5
|
import { resolveProviderEndpoint, credentialedHeaders } from "../provider-endpoint.mjs";
|
|
7
|
-
import { stampPrepared } from "../../prepare-envelope.mjs";
|
|
8
6
|
|
|
9
7
|
export const OPENSEA_API = "https://api.opensea.io/api/v2";
|
|
10
8
|
|
|
11
|
-
export const OPENSEA_ACCOUNT_CHAINS = Object.freeze([
|
|
12
|
-
"blast",
|
|
13
|
-
"base",
|
|
14
|
-
"ethereum",
|
|
15
|
-
"zora",
|
|
16
|
-
"arbitrum",
|
|
17
|
-
"sei",
|
|
18
|
-
"avalanche",
|
|
19
|
-
"polygon",
|
|
20
|
-
"optimism",
|
|
21
|
-
"ape_chain",
|
|
22
|
-
"flow",
|
|
23
|
-
"b3",
|
|
24
|
-
"soneium",
|
|
25
|
-
"ronin",
|
|
26
|
-
"bera_chain",
|
|
27
|
-
"solana",
|
|
28
|
-
"shape",
|
|
29
|
-
"unichain",
|
|
30
|
-
"gunzilla",
|
|
31
|
-
"abstract",
|
|
32
|
-
"animechain",
|
|
33
|
-
"hyperevm",
|
|
34
|
-
"somnia",
|
|
35
|
-
"monad",
|
|
36
|
-
"hyperliquid",
|
|
37
|
-
"megaeth",
|
|
38
|
-
"ink",
|
|
39
|
-
"robinhood",
|
|
40
|
-
"stablechain",
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
const OPENSEA_CHAIN_SET = new Set(OPENSEA_ACCOUNT_CHAINS);
|
|
44
|
-
const OPENSEA_NON_EVM_CHAINS = new Set(["solana", "flow"]);
|
|
45
|
-
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
46
|
-
|
|
47
9
|
function loadKeyFromDisk() {
|
|
48
10
|
const p = process.env.OPENSEA_ENV_FILE || `${process.env.HOME || ""}/.config/locals-only/opensea.env`;
|
|
49
11
|
if (!existsSync(p)) return "";
|
|
@@ -80,81 +42,6 @@ function headers(opts = {}) {
|
|
|
80
42
|
return credentialedHeaders({ accept: "application/json" }, { "x-api-key": apiKey(opts) }, endpoint(opts).trusted);
|
|
81
43
|
}
|
|
82
44
|
|
|
83
|
-
function accountChain(value) {
|
|
84
|
-
const chain = String(value || "ethereum").trim().toLowerCase();
|
|
85
|
-
if (!OPENSEA_CHAIN_SET.has(chain)) throw new Error(`opensea: unsupported account chain ${chain}`);
|
|
86
|
-
return chain;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function evmAddress(value, label = "address") {
|
|
90
|
-
try {
|
|
91
|
-
return getAddress(String(value || "").trim().toLowerCase());
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error(`opensea: invalid ${label}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function accountAddress(value, chain = null) {
|
|
98
|
-
const address = String(value || "").trim();
|
|
99
|
-
if (!address) throw new Error("opensea: account address required");
|
|
100
|
-
if (chain && !OPENSEA_NON_EVM_CHAINS.has(chain)) return evmAddress(address, "account address");
|
|
101
|
-
if (!chain && /^0x[0-9a-fA-F]{40}$/.test(address)) return evmAddress(address, "account address");
|
|
102
|
-
if (!/^[1-9A-HJ-NP-Za-km-z]{32,64}$/.test(address)) throw new Error("opensea: invalid account address");
|
|
103
|
-
return address;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function positiveDecimal(value, label) {
|
|
107
|
-
const text = String(value ?? "").trim();
|
|
108
|
-
if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(text) || Number(text) <= 0) {
|
|
109
|
-
throw new Error(`opensea: ${label} must be a positive plain decimal`);
|
|
110
|
-
}
|
|
111
|
-
return text;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function positiveInteger(value, label) {
|
|
115
|
-
const n = Number(value);
|
|
116
|
-
if (!Number.isSafeInteger(n) || n <= 0) throw new Error(`opensea: ${label} must be a positive integer`);
|
|
117
|
-
return n;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isoTime(value, label) {
|
|
121
|
-
const text = String(value || "").trim();
|
|
122
|
-
const ms = Date.parse(text);
|
|
123
|
-
if (!text || !Number.isFinite(ms)) throw new Error(`opensea: ${label} must be an ISO 8601 timestamp`);
|
|
124
|
-
return { text: new Date(ms).toISOString(), ms };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function normalizeAccountNft(raw = {}, chain, owner) {
|
|
128
|
-
return {
|
|
129
|
-
chain,
|
|
130
|
-
owner,
|
|
131
|
-
contract: raw.contract || null,
|
|
132
|
-
tokenId: raw.identifier == null ? null : String(raw.identifier),
|
|
133
|
-
tokenStandard: raw.token_standard || null,
|
|
134
|
-
collection: raw.collection || null,
|
|
135
|
-
name: raw.name || null,
|
|
136
|
-
description: raw.description || null,
|
|
137
|
-
imageUrl: raw.display_image_url || raw.image_url || raw.original_image_url || null,
|
|
138
|
-
originalImageUrl: raw.original_image_url || raw.image_url || null,
|
|
139
|
-
animationUrl: raw.display_animation_url || raw.original_animation_url || null,
|
|
140
|
-
metadataUrl: raw.metadata_url || null,
|
|
141
|
-
marketplaceUrl: raw.opensea_url || null,
|
|
142
|
-
estimatedValueUsd: raw.estimated_value_usd != null && Number.isFinite(Number(raw.estimated_value_usd))
|
|
143
|
-
? Number(raw.estimated_value_usd)
|
|
144
|
-
: null,
|
|
145
|
-
spam: raw.is_disabled === true || raw.is_nsfw === true,
|
|
146
|
-
disabled: raw.is_disabled === true,
|
|
147
|
-
nsfw: raw.is_nsfw === true,
|
|
148
|
-
traits: Array.isArray(raw.traits) ? raw.traits : [],
|
|
149
|
-
updatedAt: raw.updated_at || null,
|
|
150
|
-
acquisitionCost: null,
|
|
151
|
-
itemPnl: {
|
|
152
|
-
status: "unavailable",
|
|
153
|
-
reason: "OpenSea account inventory does not expose trustworthy per-item acquisition cost",
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
45
|
/** Shared request context for the multichain scanner. */
|
|
159
46
|
export function openseaContext(opts = {}) {
|
|
160
47
|
return {
|
|
@@ -210,162 +97,3 @@ export async function openseaFloor(slug, opts = {}) {
|
|
|
210
97
|
},
|
|
211
98
|
};
|
|
212
99
|
}
|
|
213
|
-
|
|
214
|
-
/** Paginated account NFT inventory for one OpenSea-supported chain. */
|
|
215
|
-
export async function openseaAccountNfts(args = {}, opts = {}) {
|
|
216
|
-
if (!apiKey(opts)) throw new Error("OPENSEA_API_KEY required");
|
|
217
|
-
const chain = accountChain(args.chain);
|
|
218
|
-
const owner = accountAddress(args.address || args.owner, chain);
|
|
219
|
-
const pageSize = Math.min(Math.max(Number(args.pageSize ?? args.limit ?? 200) || 200, 1), 200);
|
|
220
|
-
const maxPages = Math.min(Math.max(Number(args.maxPages ?? 5) || 5, 1), 25);
|
|
221
|
-
const nfts = [];
|
|
222
|
-
let next = args.next ? String(args.next) : null;
|
|
223
|
-
let pages = 0;
|
|
224
|
-
|
|
225
|
-
do {
|
|
226
|
-
const url = new URL(`${base(opts)}/chain/${encodeURIComponent(chain)}/account/${encodeURIComponent(owner)}/nfts`);
|
|
227
|
-
url.searchParams.set("limit", String(pageSize));
|
|
228
|
-
if (args.collection) url.searchParams.set("collection", String(args.collection));
|
|
229
|
-
if (next) url.searchParams.set("next", next);
|
|
230
|
-
const raw = await httpJson(url.toString(), {
|
|
231
|
-
headers: headers(opts),
|
|
232
|
-
fetchImpl: opts.fetchImpl,
|
|
233
|
-
timeoutMs: opts.timeoutMs ?? 20_000,
|
|
234
|
-
});
|
|
235
|
-
for (const item of Array.isArray(raw?.nfts) ? raw.nfts : []) {
|
|
236
|
-
nfts.push(normalizeAccountNft(item, chain, owner));
|
|
237
|
-
}
|
|
238
|
-
next = raw?.next ? String(raw.next) : null;
|
|
239
|
-
pages += 1;
|
|
240
|
-
} while (next && pages < maxPages);
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
provider: "opensea-nft",
|
|
244
|
-
source: "opensea-account-inventory",
|
|
245
|
-
chain,
|
|
246
|
-
owner,
|
|
247
|
-
count: nfts.length,
|
|
248
|
-
pages,
|
|
249
|
-
complete: !next,
|
|
250
|
-
next,
|
|
251
|
-
nfts,
|
|
252
|
-
exec: false,
|
|
253
|
-
readOnly: true,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** OpenSea-indexed account PnL. This is account-level, not per-NFT cost basis. */
|
|
258
|
-
export async function openseaAccountPnl(args = {}, opts = {}) {
|
|
259
|
-
if (!apiKey(opts)) throw new Error("OPENSEA_API_KEY required");
|
|
260
|
-
const address = accountAddress(args.address || args.owner);
|
|
261
|
-
const raw = await httpJson(`${base(opts)}/account/${encodeURIComponent(address)}/pnl`, {
|
|
262
|
-
headers: headers(opts),
|
|
263
|
-
fetchImpl: opts.fetchImpl,
|
|
264
|
-
timeoutMs: opts.timeoutMs ?? 20_000,
|
|
265
|
-
});
|
|
266
|
-
return {
|
|
267
|
-
provider: "opensea-nft",
|
|
268
|
-
source: "opensea-indexed-account-pnl",
|
|
269
|
-
address,
|
|
270
|
-
scope: "OpenSea-indexed account trading PnL across supported currencies, not per-item NFT cost basis",
|
|
271
|
-
methodology: "provider-indexed",
|
|
272
|
-
realizedPnlUsd: raw?.realized_pnl_usd ?? null,
|
|
273
|
-
unrealizedPnlUsd: raw?.unrealized_pnl_usd ?? null,
|
|
274
|
-
totalPnlUsd: raw?.total_pnl_usd ?? null,
|
|
275
|
-
netInvestedUsd: raw?.net_invested_usd ?? null,
|
|
276
|
-
currentValueUsd: raw?.current_value_usd ?? null,
|
|
277
|
-
returnPercentage: raw?.return_percentage ?? null,
|
|
278
|
-
itemLevelPnlAvailable: false,
|
|
279
|
-
raw,
|
|
280
|
-
exec: false,
|
|
281
|
-
readOnly: true,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function normalizeListingItem(raw = {}) {
|
|
286
|
-
const chain = accountChain(raw.chain);
|
|
287
|
-
if (OPENSEA_NON_EVM_CHAINS.has(chain)) {
|
|
288
|
-
throw new Error("opensea: listing actions currently require an EVM NFT chain");
|
|
289
|
-
}
|
|
290
|
-
const start = raw.startTime || raw.start_time
|
|
291
|
-
? isoTime(raw.startTime || raw.start_time, "startTime")
|
|
292
|
-
: { text: new Date().toISOString(), ms: Date.now() };
|
|
293
|
-
const end = isoTime(raw.endTime || raw.end_time, "endTime");
|
|
294
|
-
if (end.ms <= start.ms) throw new Error("opensea: endTime must be after startTime");
|
|
295
|
-
const tokenId = String(raw.tokenId ?? raw.token_id ?? "").trim();
|
|
296
|
-
if (!/^\d+$/.test(tokenId)) throw new Error("opensea: tokenId must be an unsigned integer string");
|
|
297
|
-
return {
|
|
298
|
-
chain,
|
|
299
|
-
contract: evmAddress(raw.contract, "NFT contract"),
|
|
300
|
-
token_id: tokenId,
|
|
301
|
-
quantity: positiveInteger(raw.quantity ?? 1, "quantity"),
|
|
302
|
-
price: {
|
|
303
|
-
amount: positiveDecimal(raw.price?.amount ?? raw.amount ?? raw.priceAmount, "price amount"),
|
|
304
|
-
currency: evmAddress(raw.price?.currency ?? raw.currency ?? ZERO_ADDRESS, "price currency"),
|
|
305
|
-
},
|
|
306
|
-
start_time: start.text,
|
|
307
|
-
end_time: end.text,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function containsApprovalStep(steps = []) {
|
|
312
|
-
return steps.some((step) => /approv|setapprovalforall/i.test(JSON.stringify(step)));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Ask OpenSea for approval and Seaport signing actions. Nothing is signed,
|
|
317
|
-
* submitted, or broadcast. The user's wallet handles every returned action.
|
|
318
|
-
*/
|
|
319
|
-
export async function openseaPrepareList(args = {}, opts = {}) {
|
|
320
|
-
if (!apiKey(opts)) throw new Error("OPENSEA_API_KEY required");
|
|
321
|
-
if (args.userConfirmed !== true) {
|
|
322
|
-
throw new Error("opensea: userConfirmed=true required after NFT, marketplace, price, currency, and expiry review");
|
|
323
|
-
}
|
|
324
|
-
const seller = evmAddress(args.seller || args.address || args.owner, "seller");
|
|
325
|
-
const sourceItems = Array.isArray(args.items) && args.items.length ? args.items : [args];
|
|
326
|
-
if (sourceItems.length > 50) throw new Error("opensea: at most 50 listing items per preparation");
|
|
327
|
-
const items = sourceItems.map(normalizeListingItem);
|
|
328
|
-
const chains = new Set(items.map((item) => item.chain));
|
|
329
|
-
if (chains.size !== 1) throw new Error("opensea: all prepared listing items must be on one chain");
|
|
330
|
-
const useCreatorFee = args.useCreatorFee !== false;
|
|
331
|
-
const request = {
|
|
332
|
-
address: seller,
|
|
333
|
-
items,
|
|
334
|
-
use_creator_fee: useCreatorFee,
|
|
335
|
-
};
|
|
336
|
-
if (args.taker) request.taker = evmAddress(args.taker, "taker");
|
|
337
|
-
|
|
338
|
-
const raw = await httpJson(`${base(opts)}/listings/actions`, {
|
|
339
|
-
method: "POST",
|
|
340
|
-
headers: headers(opts),
|
|
341
|
-
body: request,
|
|
342
|
-
fetchImpl: opts.fetchImpl,
|
|
343
|
-
timeoutMs: opts.timeoutMs ?? 25_000,
|
|
344
|
-
retries: 0,
|
|
345
|
-
dedupe: false,
|
|
346
|
-
});
|
|
347
|
-
const steps = Array.isArray(raw?.steps) ? raw.steps : [];
|
|
348
|
-
if (!steps.length) throw new Error("opensea: listing action response contained no wallet steps");
|
|
349
|
-
|
|
350
|
-
return stampPrepared({
|
|
351
|
-
provider: "opensea-nft",
|
|
352
|
-
marketplace: "opensea",
|
|
353
|
-
chain: items[0].chain,
|
|
354
|
-
kind: "nft-list-actions",
|
|
355
|
-
prepareReady: true,
|
|
356
|
-
executionReady: false,
|
|
357
|
-
signingReady: false,
|
|
358
|
-
broadcastReady: false,
|
|
359
|
-
requiresUserSignature: true,
|
|
360
|
-
requiresSeparateApproval: containsApprovalStep(steps),
|
|
361
|
-
seller,
|
|
362
|
-
items,
|
|
363
|
-
useCreatorFee,
|
|
364
|
-
taker: request.taker || null,
|
|
365
|
-
feeDisclosure: useCreatorFee
|
|
366
|
-
? "Creator fees requested. Inspect the returned Seaport consideration recipients and wallet simulation before signing."
|
|
367
|
-
: "Creator fees disabled by explicit request. Marketplace protocol fees may still apply and must be inspected before signing.",
|
|
368
|
-
steps,
|
|
369
|
-
note: "Execute approvals separately, then sign the Seaport order in the user's wallet. Oracle does not submit or broadcast the listing.",
|
|
370
|
-
});
|
|
371
|
-
}
|
|
@@ -330,7 +330,6 @@ export async function satflowPrepareList(args = {}, opts = {}) {
|
|
|
330
330
|
kind: "list-intent",
|
|
331
331
|
requiresUserSignature: true,
|
|
332
332
|
marketplace: "satflow",
|
|
333
|
-
listingRequest: body,
|
|
334
333
|
intent: data,
|
|
335
334
|
unsignedPsbt: data?.unsignedListingPSBTBase64 || data?.unsigned_psbt || null,
|
|
336
335
|
note: "Sign listing PSBTs in a Bitcoin wallet; listing broadcast is outside the data plane.",
|