@oracle-agent/oracle 0.1.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/CONTRIBUTING.md +98 -0
- package/LICENSE +202 -0
- package/README.md +384 -0
- package/SECURITY.md +89 -0
- package/SETUP.md +235 -0
- package/artifacts/inscription/oracle-was-here-preview.png +0 -0
- package/artifacts/inscription/oracle-was-here.svg +1 -0
- package/artifacts/specialist-packs/oracle-full-crypto.json +273 -0
- package/bin/desk-server.mjs +438 -0
- package/bin/oracle-data-mcp.mjs +603 -0
- package/bin/oracle-init.mjs +423 -0
- package/bin/oracle-public-server.mjs +36 -0
- package/bin/oracle-route.mjs +254 -0
- package/bin/oracle-scan.mjs +192 -0
- package/docs/adding-a-chain.md +229 -0
- package/docs/architecture.md +135 -0
- package/docs/profiles.md +132 -0
- package/examples/add-a-chain.mjs +65 -0
- package/examples/research-a-token.mjs +70 -0
- package/package.json +90 -0
- package/profiles/_template/SOUL.md +47 -0
- package/profiles/_template/profile.json +22 -0
- package/profiles/bitcoin-agent/SOUL.md +31 -0
- package/profiles/bitcoin-agent/profile.json +32 -0
- package/profiles/hyperliquid-agent/SOUL.md +34 -0
- package/profiles/hyperliquid-agent/profile.json +37 -0
- package/profiles/oracle/SOUL.md +65 -0
- package/profiles/oracle/profile.json +35 -0
- package/profiles/polymarket-agent/SOUL.md +35 -0
- package/profiles/polymarket-agent/profile.json +34 -0
- package/profiles/profile.schema.json +90 -0
- package/profiles/protocol-builder/SOUL.md +50 -0
- package/profiles/protocol-builder/profile.json +37 -0
- package/profiles/robinhood-agent/SOUL.md +45 -0
- package/profiles/robinhood-agent/profile.json +39 -0
- package/profiles/solana-agent/SOUL.md +37 -0
- package/profiles/solana-agent/profile.json +37 -0
- package/profiles/stable-agent/SOUL.md +43 -0
- package/profiles/stable-agent/profile.json +37 -0
- package/public/oracle-console/app.js +272 -0
- package/public/oracle-console/bitcoin-wallets.js +206 -0
- package/public/oracle-console/index.html +91 -0
- package/public/oracle-console/styles.css +239 -0
- package/public/oracle-splash/index.html +931 -0
- package/scripts/build-inscription.py +230 -0
- package/scripts/check-test-count.mjs +105 -0
- package/scripts/e2e-hl-markets.mjs +21 -0
- package/scripts/e2e-hl-perps.mjs +48 -0
- package/scripts/e2e-hypercore-staking.mjs +128 -0
- package/scripts/e2e-solana-bitcoin.mjs +183 -0
- package/scripts/public-api-scan.mjs +23 -0
- package/scripts/secret-scan.mjs +181 -0
- package/scripts/verify-v3-venues.mjs +192 -0
- package/skills/oracle-best-execution/SKILL.md +127 -0
- package/skills/oracle-bitcoin/SKILL.md +53 -0
- package/skills/oracle-chain-graphs-telegram-cards/SKILL.md +59 -0
- package/skills/oracle-circuit-breaker/SKILL.md +51 -0
- package/skills/oracle-contract-research/SKILL.md +55 -0
- package/skills/oracle-desk/SKILL.md +58 -0
- package/skills/oracle-dex-launch/SKILL.md +38 -0
- package/skills/oracle-grants/SKILL.md +69 -0
- package/skills/oracle-hypercore-staking/SKILL.md +57 -0
- package/skills/oracle-hyperliquid/SKILL.md +56 -0
- package/skills/oracle-meme-token-sniper/SKILL.md +73 -0
- package/skills/oracle-nft-gacha-launch/SKILL.md +48 -0
- package/skills/oracle-nft-mint-gas-war/SKILL.md +63 -0
- package/skills/oracle-polymarket/SKILL.md +60 -0
- package/skills/oracle-protocol-builder/SKILL.md +38 -0
- package/skills/oracle-protocol-security/SKILL.md +60 -0
- package/skills/oracle-public-product/SKILL.md +44 -0
- package/skills/oracle-receipts/SKILL.md +52 -0
- package/skills/oracle-rfq-tokenized-assets/SKILL.md +69 -0
- package/skills/oracle-smart-wallet-scanner/SKILL.md +49 -0
- package/skills/oracle-solana/SKILL.md +65 -0
- package/skills/oracle-solana-nft/SKILL.md +54 -0
- package/skills/oracle-token-research/SKILL.md +67 -0
- package/src/agent-auth.mjs +191 -0
- package/src/approval-guard.mjs +282 -0
- package/src/attestation-secret.mjs +88 -0
- package/src/audit-log.mjs +196 -0
- package/src/auto-slippage.mjs +378 -0
- package/src/capability-posture.mjs +125 -0
- package/src/chains.mjs +62 -0
- package/src/data/catalog.mjs +495 -0
- package/src/data/desk-data.mjs +623 -0
- package/src/data/http.mjs +200 -0
- package/src/data/provider-endpoint.mjs +94 -0
- package/src/data/providers/aerodrome.mjs +244 -0
- package/src/data/providers/balancer.mjs +208 -0
- package/src/data/providers/bitcoin-esplora.mjs +230 -0
- package/src/data/providers/bitcoin-meta.mjs +378 -0
- package/src/data/providers/blockscout.mjs +14 -0
- package/src/data/providers/bridges.mjs +241 -0
- package/src/data/providers/cowswap.mjs +501 -0
- package/src/data/providers/curve.mjs +200 -0
- package/src/data/providers/defillama.mjs +88 -0
- package/src/data/providers/dexscreener.mjs +43 -0
- package/src/data/providers/evm-rpc.mjs +203 -0
- package/src/data/providers/geckoterminal.mjs +34 -0
- package/src/data/providers/gmx.mjs +495 -0
- package/src/data/providers/hl-info.mjs +102 -0
- package/src/data/providers/hl-markets.mjs +210 -0
- package/src/data/providers/hl-perps.mjs +382 -0
- package/src/data/providers/hl-staking.mjs +352 -0
- package/src/data/providers/hl-ws.mjs +119 -0
- package/src/data/providers/hyperevm-dex.mjs +49 -0
- package/src/data/providers/jupiter.mjs +182 -0
- package/src/data/providers/lifi.mjs +150 -0
- package/src/data/providers/magiceden-sol.mjs +355 -0
- package/src/data/providers/morpho.mjs +173 -0
- package/src/data/providers/odos.mjs +155 -0
- package/src/data/providers/oneinch.mjs +173 -0
- package/src/data/providers/opensea-multichain.mjs +136 -0
- package/src/data/providers/opensea-nft.mjs +99 -0
- package/src/data/providers/paraswap.mjs +117 -0
- package/src/data/providers/pendle.mjs +187 -0
- package/src/data/providers/poly-public.mjs +96 -0
- package/src/data/providers/poly-ws.mjs +103 -0
- package/src/data/providers/rh-agent.mjs +59 -0
- package/src/data/providers/satflow.mjs +336 -0
- package/src/data/providers/solana-rpc.mjs +186 -0
- package/src/data/providers/uniswap-v3.mjs +303 -0
- package/src/data/providers/zerox.mjs +166 -0
- package/src/data/public-api-scan.mjs +61 -0
- package/src/data/quote-placeholder.mjs +31 -0
- package/src/exact-integer.mjs +72 -0
- package/src/exec-policy.mjs +444 -0
- package/src/flags.mjs +15 -0
- package/src/fresh-window.mjs +76 -0
- package/src/gmx-attestation.mjs +175 -0
- package/src/index.mjs +50 -0
- package/src/nft-gas-war-guard.mjs +139 -0
- package/src/onboarding/agent-keys.mjs +157 -0
- package/src/onboarding/index.mjs +18 -0
- package/src/onboarding/tiers.mjs +139 -0
- package/src/oracle-env.mjs +38 -0
- package/src/protocol-execution.mjs +84 -0
- package/src/public-api/buzz-integration.mjs +256 -0
- package/src/public-api/connect-agent.mjs +397 -0
- package/src/public-api/grants.mjs +142 -0
- package/src/public-api/http.mjs +374 -0
- package/src/public-control/aa-adapter.mjs +402 -0
- package/src/public-control/build-registry.mjs +227 -0
- package/src/public-control/bundler-client.mjs +372 -0
- package/src/public-control/grant-indexer.mjs +296 -0
- package/src/public-control/policy-render.mjs +69 -0
- package/src/public-control/policy-schema.mjs +318 -0
- package/src/public-control/runtime-config.mjs +265 -0
- package/src/public-control/session-key-model.mjs +374 -0
- package/src/public-control/session-orchestrator.mjs +412 -0
- package/src/route-attestation.mjs +132 -0
- package/src/router/best-execution.mjs +221 -0
- package/src/router/index.mjs +185 -0
- package/src/router/prepare-bridge.mjs +288 -0
- package/src/router/prepare-route.mjs +341 -0
- package/src/router/proposal.mjs +311 -0
- package/src/router/risk-classifier.mjs +119 -0
- package/src/router/route-sources.mjs +292 -0
- package/src/scanner/chains.config.mjs +381 -0
- package/src/scanner/contract.mjs +270 -0
- package/src/scanner/evm-scanner.mjs +394 -0
- package/src/scanner/index.mjs +9 -0
- package/src/scanner/v2-venue.mjs +335 -0
- package/src/scanner/v3-venue.mjs +290 -0
- package/src/scopes.mjs +44 -0
- package/src/sell-simulation.mjs +167 -0
- package/src/token-transfer-guard.mjs +188 -0
- package/src/vault-attestation.mjs +145 -0
- package/src/venues.mjs +206 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// OpenSea NFT floors — uses OPENSEA_API_KEY from env or ~/.config/locals-only/opensea.env
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { httpJson } from "../http.mjs";
|
|
5
|
+
import { resolveProviderEndpoint, credentialedHeaders } from "../provider-endpoint.mjs";
|
|
6
|
+
|
|
7
|
+
export const OPENSEA_API = "https://api.opensea.io/api/v2";
|
|
8
|
+
|
|
9
|
+
function loadKeyFromDisk() {
|
|
10
|
+
const p = process.env.OPENSEA_ENV_FILE || `${process.env.HOME || ""}/.config/locals-only/opensea.env`;
|
|
11
|
+
if (!existsSync(p)) return "";
|
|
12
|
+
try {
|
|
13
|
+
for (const ln of readFileSync(p, "utf8").split("\n")) {
|
|
14
|
+
const m = ln.match(/^\s*OPENSEA_API_KEY\s*=\s*(.+)\s*$/);
|
|
15
|
+
if (m) return m[1].replace(/^["']|["']$/g, "").trim();
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
/* ignore */
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function apiKey(opts = {}) {
|
|
24
|
+
return opts.apiKey || process.env.OPENSEA_API_KEY || process.env.OPEN_SEA_API_KEY || loadKeyFromDisk();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function endpoint(opts = {}) {
|
|
28
|
+
return resolveProviderEndpoint({
|
|
29
|
+
provider: "opensea",
|
|
30
|
+
defaultUrl: OPENSEA_API,
|
|
31
|
+
hosts: ["api.opensea.io", "testnets-api.opensea.io"],
|
|
32
|
+
url: opts.baseUrl || process.env.OPENSEA_API_URL,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function base(opts = {}) {
|
|
37
|
+
return endpoint(opts).baseUrl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function headers(opts = {}) {
|
|
41
|
+
// x-api-key is attached only for a pinned OpenSea host.
|
|
42
|
+
return credentialedHeaders({ accept: "application/json" }, { "x-api-key": apiKey(opts) }, endpoint(opts).trusted);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Shared request context for the multichain scanner. */
|
|
46
|
+
export function openseaContext(opts = {}) {
|
|
47
|
+
return {
|
|
48
|
+
base: base(opts),
|
|
49
|
+
headers: headers(opts),
|
|
50
|
+
fetchImpl: opts.fetchImpl,
|
|
51
|
+
timeoutMs: opts.timeoutMs,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function openseaHealth(opts = {}) {
|
|
56
|
+
if (!apiKey(opts)) {
|
|
57
|
+
return { ok: false, configured: false, error: "OPENSEA_API_KEY not set" };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const c = await openseaCollection("boredapeyachtclub", opts);
|
|
61
|
+
return { ok: true, configured: true, name: c.name || c.collection };
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return { ok: false, configured: true, error: String(e.message || e).slice(0, 160) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function openseaCollection(slug, opts = {}) {
|
|
68
|
+
if (!slug) throw new Error("collection slug required");
|
|
69
|
+
if (!apiKey(opts)) throw new Error("OPENSEA_API_KEY required");
|
|
70
|
+
return httpJson(`${base(opts)}/collections/${encodeURIComponent(slug)}`, {
|
|
71
|
+
headers: headers(opts),
|
|
72
|
+
fetchImpl: opts.fetchImpl,
|
|
73
|
+
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Best-effort floor from collection stats / collection payload. */
|
|
78
|
+
export async function openseaFloor(slug, opts = {}) {
|
|
79
|
+
const c = await openseaCollection(slug, opts);
|
|
80
|
+
const stats = c.stats || c.total || {};
|
|
81
|
+
const floor =
|
|
82
|
+
c.floor_price ??
|
|
83
|
+
c.floorPrice ??
|
|
84
|
+
stats.floor_price ??
|
|
85
|
+
stats.floorPrice ??
|
|
86
|
+
null;
|
|
87
|
+
return {
|
|
88
|
+
slug: c.collection || slug,
|
|
89
|
+
name: c.name || null,
|
|
90
|
+
floor,
|
|
91
|
+
totalSupply: c.total_supply ?? stats.total_supply ?? null,
|
|
92
|
+
numOwners: c.num_owners ?? stats.num_owners ?? null,
|
|
93
|
+
raw: {
|
|
94
|
+
contracts: c.contracts,
|
|
95
|
+
fees: c.fees,
|
|
96
|
+
category: c.category,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// ParaSwap/Velora public market API — keyless price routes, quote-only here.
|
|
2
|
+
import { httpJson } from "../http.mjs";
|
|
3
|
+
import { attachAutoSlippage, bindAutoSlippageGuardToCall } from "../../auto-slippage.mjs";
|
|
4
|
+
|
|
5
|
+
export const PARASWAP_API = "https://api.paraswap.io";
|
|
6
|
+
const base = (o = {}) => (o.baseUrl || process.env.PARASWAP_API_URL || PARASWAP_API).replace(/\/$/, "");
|
|
7
|
+
function token(value, name) {
|
|
8
|
+
const s = String(value || "");
|
|
9
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(s)) throw new Error(`${name} must be a 20-byte token address`);
|
|
10
|
+
return s.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function paraswapTokens(args = {}, opts = {}) {
|
|
14
|
+
const chainId = Number(args.chainId || 1);
|
|
15
|
+
return httpJson(`${base(opts)}/tokens/${chainId}`, { fetchImpl: opts.fetchImpl, timeoutMs: opts.timeoutMs });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function paraswapPrice(args = {}, opts = {}) {
|
|
19
|
+
const chainId = Number(args.chainId || args.network || 1);
|
|
20
|
+
if (args.amount == null) throw new Error("amount required");
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
srcToken: token(args.srcToken, "srcToken"),
|
|
23
|
+
destToken: token(args.destToken, "destToken"),
|
|
24
|
+
amount: String(args.amount),
|
|
25
|
+
srcDecimals: String(Number(args.srcDecimals)),
|
|
26
|
+
destDecimals: String(Number(args.destDecimals)),
|
|
27
|
+
side: String(args.side || "SELL").toUpperCase(),
|
|
28
|
+
network: String(chainId),
|
|
29
|
+
version: String(args.version || "6.2"),
|
|
30
|
+
});
|
|
31
|
+
if (args.includeDEXS) params.set("includeDEXS", String(args.includeDEXS));
|
|
32
|
+
if (args.excludeDEXS) params.set("excludeDEXS", String(args.excludeDEXS));
|
|
33
|
+
const result = await httpJson(`${base(opts)}/prices?${params}`, {
|
|
34
|
+
fetchImpl: opts.fetchImpl,
|
|
35
|
+
timeoutMs: opts.timeoutMs ?? 20_000,
|
|
36
|
+
});
|
|
37
|
+
const amountOut = result?.priceRoute?.destAmount;
|
|
38
|
+
if (!amountOut) return { ...result, executionReady: false };
|
|
39
|
+
return {
|
|
40
|
+
...attachAutoSlippage(result, {
|
|
41
|
+
chainId,
|
|
42
|
+
venue: result?.priceRoute?.contractAddress || "paraswap-quote",
|
|
43
|
+
amountOut,
|
|
44
|
+
liquidityUsd: args.liquidityUsd,
|
|
45
|
+
priceChange5m: args.priceChange5m,
|
|
46
|
+
quoteAgeMs: 0,
|
|
47
|
+
requestedCapBps: args.maxSlippageBps ?? args.slippageBps,
|
|
48
|
+
}),
|
|
49
|
+
executionReady: false,
|
|
50
|
+
executionBlocker: "call prepare to bind the price route to a validated transaction",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function paraswapPrepare(args = {}, opts = {}) {
|
|
55
|
+
if (String(args.side || "SELL").toUpperCase() !== "SELL") {
|
|
56
|
+
throw new Error("paraswap: agentic prepare currently supports exact-input SELL only");
|
|
57
|
+
}
|
|
58
|
+
const chainId = Number(args.chainId || args.network || 1);
|
|
59
|
+
const userAddress = token(args.userAddress, "userAddress");
|
|
60
|
+
const quote = await paraswapPrice(args, opts);
|
|
61
|
+
const priceRoute = quote?.priceRoute;
|
|
62
|
+
const expectedTarget = String(priceRoute?.contractAddress || "").toLowerCase();
|
|
63
|
+
if (!expectedTarget) throw new Error("paraswap: price route missing contractAddress");
|
|
64
|
+
const body = {
|
|
65
|
+
srcToken: token(args.srcToken, "srcToken"),
|
|
66
|
+
destToken: token(args.destToken, "destToken"),
|
|
67
|
+
srcAmount: String(priceRoute.srcAmount || args.amount),
|
|
68
|
+
// ParaSwap treats destAmount as the on-chain minimum for SELL routes.
|
|
69
|
+
destAmount: String(quote.amountOutMinimum),
|
|
70
|
+
priceRoute,
|
|
71
|
+
userAddress,
|
|
72
|
+
ignoreChecks: false,
|
|
73
|
+
ignoreGasEstimate: false,
|
|
74
|
+
};
|
|
75
|
+
if (args.receiver) body.receiver = token(args.receiver, "receiver");
|
|
76
|
+
const tx = await httpJson(`${base(opts)}/transactions/${chainId}`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body,
|
|
79
|
+
fetchImpl: opts.fetchImpl,
|
|
80
|
+
timeoutMs: opts.timeoutMs ?? 25_000,
|
|
81
|
+
});
|
|
82
|
+
if (Number(tx.chainId ?? chainId) !== chainId) throw new Error("paraswap: transaction chain mismatch");
|
|
83
|
+
if (String(tx.to || "").toLowerCase() !== expectedTarget) throw new Error("paraswap: transaction target mismatch");
|
|
84
|
+
if (tx.from && String(tx.from).toLowerCase() !== userAddress) throw new Error("paraswap: transaction sender mismatch");
|
|
85
|
+
return {
|
|
86
|
+
provider: "paraswap",
|
|
87
|
+
calldataReady: true,
|
|
88
|
+
// NOT executable authority. The bytes are assembled but nothing is signed:
|
|
89
|
+
// the user's wallet is still the only thing that can authorize this. The
|
|
90
|
+
// previous `executionReady: true` read as "cleared to execute" and trained
|
|
91
|
+
// agents to treat a quote as permission.
|
|
92
|
+
requiresUserSignature: true,
|
|
93
|
+
signingReady: false,
|
|
94
|
+
broadcastReady: false,
|
|
95
|
+
chainId,
|
|
96
|
+
quote,
|
|
97
|
+
autoSlippage: quote.autoSlippage,
|
|
98
|
+
requiresApproval: body.srcToken !== "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
|
|
99
|
+
? { token: body.srcToken, spender: priceRoute.tokenTransferProxy || expectedTarget, amount: body.srcAmount }
|
|
100
|
+
: null,
|
|
101
|
+
transaction: {
|
|
102
|
+
chainId,
|
|
103
|
+
from: tx.from || userAddress,
|
|
104
|
+
to: tx.to,
|
|
105
|
+
data: tx.data,
|
|
106
|
+
value: tx.value ?? "0x0",
|
|
107
|
+
...(tx.gas != null ? { gasLimit: tx.gas } : {}),
|
|
108
|
+
slippageGuard: bindAutoSlippageGuardToCall(quote.autoSlippage, { chainId, venue: tx.to, data: tx.data }),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function paraswapHealth(opts = {}) {
|
|
114
|
+
const data = await paraswapTokens({ chainId: 1 }, opts);
|
|
115
|
+
const count = Array.isArray(data?.tokens) ? data.tokens.length : Object.keys(data?.tokens || {}).length;
|
|
116
|
+
return { ok: count > 0, tokenSample: count };
|
|
117
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Pendle public core API — markets and Hosted SDK Convert tx builder, no key.
|
|
2
|
+
import { getAddress } from "ethers";
|
|
3
|
+
import { httpJson } from "../http.mjs";
|
|
4
|
+
import { attachAutoSlippage, bindAutoSlippageGuardToCall, resolveAutoSlippage } from "../../auto-slippage.mjs";
|
|
5
|
+
|
|
6
|
+
export const PENDLE_API = "https://api-v2.pendle.finance/core";
|
|
7
|
+
|
|
8
|
+
// Official Pendle Router from deployment docs. Bytecode checked via eth_getCode
|
|
9
|
+
// on Arbitrum/Base 2026-07-23.
|
|
10
|
+
export const PENDLE_ROUTER_BY_CHAIN = Object.freeze({
|
|
11
|
+
42161: "0x888888888889758F76e7103c6CbF23ABbF58F946",
|
|
12
|
+
8453: "0x888888888889758F76e7103c6CbF23ABbF58F946",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
16
|
+
const NATIVE = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
17
|
+
|
|
18
|
+
const base = (o = {}) => (o.baseUrl || process.env.PENDLE_API_URL || PENDLE_API).replace(/\/$/, "");
|
|
19
|
+
|
|
20
|
+
function checksum(value, label) {
|
|
21
|
+
try {
|
|
22
|
+
return getAddress(value);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(`${label} must be a valid EVM address`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normal(value, label) {
|
|
29
|
+
return checksum(value, label).toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isNative(value) {
|
|
33
|
+
const v = String(value || "").toLowerCase();
|
|
34
|
+
return v === "native" || v === "eth" || v === NATIVE || v === ZERO;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function amountString(value, label) {
|
|
38
|
+
let amount;
|
|
39
|
+
try {
|
|
40
|
+
amount = BigInt(String(value));
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${label} must be an integer string`);
|
|
43
|
+
}
|
|
44
|
+
if (amount <= 0n) throw new Error(`${label} must be positive`);
|
|
45
|
+
return amount.toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeInputs(inputs) {
|
|
49
|
+
if (!Array.isArray(inputs) || inputs.length === 0) throw new Error("pendle convert requires inputs[]");
|
|
50
|
+
return inputs.map((input, i) => ({
|
|
51
|
+
token: checksum(input.token, `pendle input ${i} token`),
|
|
52
|
+
amount: amountString(input.amount, `pendle input ${i} amount`),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeOutputs(outputs) {
|
|
57
|
+
if (!Array.isArray(outputs) || outputs.length === 0) throw new Error("pendle convert requires outputs[]");
|
|
58
|
+
return outputs.map((token, i) => checksum(token, `pendle output ${i} token`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function routerFor(chainId) {
|
|
62
|
+
const router = PENDLE_ROUTER_BY_CHAIN[Number(chainId)];
|
|
63
|
+
if (!router) throw new Error(`Pendle prepare not enabled for chainId ${chainId}`);
|
|
64
|
+
return router;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function firstOutputAmount(result) {
|
|
68
|
+
const amount = result?.routes?.[0]?.outputs?.[0]?.amount || result?.outputs?.[0]?.amount;
|
|
69
|
+
if (!amount) throw new Error("Pendle convert response missing output amount");
|
|
70
|
+
return amountString(amount, "pendle output amount");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeRequiredApprovals(requiredApprovals, spender) {
|
|
74
|
+
const approvals = (Array.isArray(requiredApprovals) ? requiredApprovals : [])
|
|
75
|
+
.filter((approval) => approval?.token && approval?.amount && !isNative(approval.token))
|
|
76
|
+
.map((approval) => ({
|
|
77
|
+
token: normal(approval.token, "pendle approval token"),
|
|
78
|
+
spender: normal(spender, "pendle approval spender"),
|
|
79
|
+
amount: amountString(approval.amount, "pendle approval amount"),
|
|
80
|
+
}));
|
|
81
|
+
if (!approvals.length) return null;
|
|
82
|
+
return approvals.length === 1 ? approvals[0] : approvals;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function pendleMarkets(args = {}, opts = {}) {
|
|
86
|
+
const chainId = Number(args.chainId || 42161);
|
|
87
|
+
const p = new URLSearchParams({
|
|
88
|
+
limit: String(Math.min(100, Math.max(1, Number(args.limit || 20)))),
|
|
89
|
+
skip: String(Math.max(0, Number(args.skip || 0))),
|
|
90
|
+
});
|
|
91
|
+
if (args.isExpired != null) p.set("isExpired", String(Boolean(args.isExpired)));
|
|
92
|
+
return httpJson(`${base(opts)}/v1/${chainId}/markets?${p}`, {
|
|
93
|
+
fetchImpl: opts.fetchImpl,
|
|
94
|
+
timeoutMs: opts.timeoutMs,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function pendleHealth(opts = {}) {
|
|
99
|
+
const data = await pendleMarkets({ chainId: 42161, limit: 1 }, opts);
|
|
100
|
+
return { ok: Array.isArray(data?.results), marketSample: data?.results?.length || 0 };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function pendlePrepare(q = {}, opts = {}) {
|
|
104
|
+
const chainId = Number(q.chainId ?? 42161);
|
|
105
|
+
const router = routerFor(chainId);
|
|
106
|
+
const from = q.from || q.fromAddress || q.userAddress || q.taker || q.receiver;
|
|
107
|
+
if (!from) throw new Error("pendlePrepare requires from/receiver");
|
|
108
|
+
const receiver = checksum(q.receiver || from, "pendle receiver");
|
|
109
|
+
const inputs = normalizeInputs(q.inputs);
|
|
110
|
+
const outputs = normalizeOutputs(q.outputs);
|
|
111
|
+
if (q.enableAggregator === true || (Array.isArray(q.aggregators) && q.aggregators.length)) {
|
|
112
|
+
throw new Error("Pendle aggregator paths are disabled by policy; use direct convert only");
|
|
113
|
+
}
|
|
114
|
+
const selected = resolveAutoSlippage({
|
|
115
|
+
liquidityUsd: q.liquidityUsd,
|
|
116
|
+
priceChange5m: q.priceChange5m,
|
|
117
|
+
requestedCapBps: q.maxSlippageBps ?? q.slippageBps,
|
|
118
|
+
});
|
|
119
|
+
if (!selected.executable) {
|
|
120
|
+
throw new Error(`Pendle: route requires ${selected.requiredBps} bps above cap ${selected.capBps}`);
|
|
121
|
+
}
|
|
122
|
+
const body = {
|
|
123
|
+
receiver,
|
|
124
|
+
slippage: selected.selectedBps / 10_000,
|
|
125
|
+
enableAggregator: false,
|
|
126
|
+
useLimitOrder: false,
|
|
127
|
+
inputs,
|
|
128
|
+
outputs,
|
|
129
|
+
...(q.redeemRewards != null ? { redeemRewards: Boolean(q.redeemRewards) } : {}),
|
|
130
|
+
...(q.needScale != null ? { needScale: Boolean(q.needScale) } : {}),
|
|
131
|
+
...(q.additionalData ? { additionalData: String(q.additionalData) } : {}),
|
|
132
|
+
};
|
|
133
|
+
const raw = await httpJson(`${base(opts)}/v3/sdk/${chainId}/convert`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body,
|
|
136
|
+
fetchImpl: opts.fetchImpl,
|
|
137
|
+
timeoutMs: opts.timeoutMs ?? 25_000,
|
|
138
|
+
});
|
|
139
|
+
const route = raw?.routes?.[0];
|
|
140
|
+
const tx = route?.tx;
|
|
141
|
+
if (!tx?.to || !tx?.data) throw new Error("Pendle convert returned no executable tx");
|
|
142
|
+
if (normal(tx.to, "pendle tx target") !== normal(router, "pendle router")) {
|
|
143
|
+
throw new Error("Pendle convert returned unexpected router target");
|
|
144
|
+
}
|
|
145
|
+
if (tx.from && normal(tx.from, "pendle tx sender") !== normal(from, "pendle sender")) {
|
|
146
|
+
throw new Error("Pendle convert transaction sender mismatch");
|
|
147
|
+
}
|
|
148
|
+
const amountOut = firstOutputAmount(raw);
|
|
149
|
+
const quote = attachAutoSlippage(raw, {
|
|
150
|
+
chainId,
|
|
151
|
+
venue: tx.to,
|
|
152
|
+
amountOut,
|
|
153
|
+
liquidityUsd: q.liquidityUsd,
|
|
154
|
+
priceChange5m: q.priceChange5m,
|
|
155
|
+
requestedCapBps: q.maxSlippageBps ?? q.slippageBps,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
provider: "pendle",
|
|
159
|
+
calldataReady: true,
|
|
160
|
+
// NOT executable authority. The bytes are assembled but nothing is signed:
|
|
161
|
+
// the user's wallet is still the only thing that can authorize this. The
|
|
162
|
+
// previous `executionReady: true` read as "cleared to execute" and trained
|
|
163
|
+
// agents to treat a quote as permission.
|
|
164
|
+
requiresUserSignature: true,
|
|
165
|
+
signingReady: false,
|
|
166
|
+
broadcastReady: false,
|
|
167
|
+
chainId,
|
|
168
|
+
action: raw.action || null,
|
|
169
|
+
quote,
|
|
170
|
+
autoSlippage: quote.autoSlippage,
|
|
171
|
+
requiresApproval: normalizeRequiredApprovals(raw.requiredApprovals, tx.to),
|
|
172
|
+
transaction: {
|
|
173
|
+
chainId,
|
|
174
|
+
from: tx.from || from,
|
|
175
|
+
to: tx.to,
|
|
176
|
+
data: tx.data,
|
|
177
|
+
value: tx.value ?? "0x0",
|
|
178
|
+
slippageGuard: bindAutoSlippageGuardToCall(quote.autoSlippage, { chainId, venue: tx.to, data: tx.data }),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function pendleQuote(q = {}, opts = {}) {
|
|
184
|
+
const prepared = await pendlePrepare(q, opts);
|
|
185
|
+
const { transaction: _transaction, ...quote } = prepared;
|
|
186
|
+
return quote;
|
|
187
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Polymarket public REST — CLOB + Gamma. No L2 API key, no wallet.
|
|
2
|
+
|
|
3
|
+
import { httpJson } from "../http.mjs";
|
|
4
|
+
|
|
5
|
+
export const POLY_CLOB_DEFAULT = "https://clob.polymarket.com";
|
|
6
|
+
export const POLY_GAMMA_DEFAULT = "https://gamma-api.polymarket.com";
|
|
7
|
+
|
|
8
|
+
function clobBase(opts = {}) {
|
|
9
|
+
return (opts.clobUrl || process.env.POLY_CLOB_URL || POLY_CLOB_DEFAULT).replace(/\/$/, "");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function gammaBase(opts = {}) {
|
|
13
|
+
return (opts.gammaUrl || process.env.POLY_GAMMA_URL || POLY_GAMMA_DEFAULT).replace(/\/$/, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function polyTime(opts = {}) {
|
|
17
|
+
return httpJson(`${clobBase(opts)}/time`, {
|
|
18
|
+
fetchImpl: opts.fetchImpl,
|
|
19
|
+
timeoutMs: opts.timeoutMs,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function polyHealth(opts = {}) {
|
|
24
|
+
const t = await polyTime(opts);
|
|
25
|
+
const ok = t != null && t !== "";
|
|
26
|
+
return { ok, time: t };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function polyMarkets({ limit = 5, offset = 0, ...rest } = {}, opts = {}) {
|
|
30
|
+
const q = new URLSearchParams({
|
|
31
|
+
limit: String(limit),
|
|
32
|
+
offset: String(offset),
|
|
33
|
+
...Object.fromEntries(
|
|
34
|
+
Object.entries(rest)
|
|
35
|
+
.filter(([, v]) => v != null)
|
|
36
|
+
.map(([k, v]) => [k, String(v)])
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
return httpJson(`${gammaBase(opts)}/markets?${q}`, {
|
|
40
|
+
fetchImpl: opts.fetchImpl,
|
|
41
|
+
timeoutMs: opts.timeoutMs,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function polyBook(tokenId, opts = {}) {
|
|
46
|
+
if (!tokenId) throw new Error("polyBook requires tokenId");
|
|
47
|
+
const q = new URLSearchParams({ token_id: String(tokenId) });
|
|
48
|
+
return httpJson(`${clobBase(opts)}/book?${q}`, {
|
|
49
|
+
fetchImpl: opts.fetchImpl,
|
|
50
|
+
timeoutMs: opts.timeoutMs,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function polyMidpoint(tokenId, opts = {}) {
|
|
55
|
+
if (!tokenId) throw new Error("polyMidpoint requires tokenId");
|
|
56
|
+
const q = new URLSearchParams({ token_id: String(tokenId) });
|
|
57
|
+
return httpJson(`${clobBase(opts)}/midpoint?${q}`, {
|
|
58
|
+
fetchImpl: opts.fetchImpl,
|
|
59
|
+
timeoutMs: opts.timeoutMs,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// D1 expansions
|
|
64
|
+
export async function polySpread(tokenId, opts = {}) {
|
|
65
|
+
if (!tokenId) throw new Error("polySpread requires tokenId");
|
|
66
|
+
const q = new URLSearchParams({ token_id: String(tokenId) });
|
|
67
|
+
return httpJson(`${clobBase(opts)}/spread?${q}`, {
|
|
68
|
+
fetchImpl: opts.fetchImpl,
|
|
69
|
+
timeoutMs: opts.timeoutMs,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function polyPrice(tokenId, { side = "buy" } = {}, opts = {}) {
|
|
74
|
+
if (!tokenId) throw new Error("polyPrice requires tokenId");
|
|
75
|
+
const q = new URLSearchParams({ token_id: String(tokenId), side: String(side) });
|
|
76
|
+
return httpJson(`${clobBase(opts)}/price?${q}`, {
|
|
77
|
+
fetchImpl: opts.fetchImpl,
|
|
78
|
+
timeoutMs: opts.timeoutMs,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function polyEvents({ limit = 5, offset = 0, ...rest } = {}, opts = {}) {
|
|
83
|
+
const q = new URLSearchParams({
|
|
84
|
+
limit: String(limit),
|
|
85
|
+
offset: String(offset),
|
|
86
|
+
...Object.fromEntries(
|
|
87
|
+
Object.entries(rest)
|
|
88
|
+
.filter(([, v]) => v != null)
|
|
89
|
+
.map(([k, v]) => [k, String(v)])
|
|
90
|
+
),
|
|
91
|
+
});
|
|
92
|
+
return httpJson(`${gammaBase(opts)}/events?${q}`, {
|
|
93
|
+
fetchImpl: opts.fetchImpl,
|
|
94
|
+
timeoutMs: opts.timeoutMs,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Short-lived Polymarket CLOB WebSocket market snapshots.
|
|
2
|
+
|
|
3
|
+
const POLY_WS = process.env.POLY_WS_URL || "wss://ws-subscriptions-clob.polymarket.com/ws/market";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to asset_ids (token ids) and return first book-like payload.
|
|
7
|
+
*/
|
|
8
|
+
export async function polyWsSnapshot(assetIds = [], opts = {}) {
|
|
9
|
+
const ids = Array.isArray(assetIds) ? assetIds.map(String) : [String(assetIds)];
|
|
10
|
+
if (!ids.length || !ids[0]) throw new Error("polyWsSnapshot requires assetIds");
|
|
11
|
+
const timeoutMs = opts.timeoutMs ?? 8_000;
|
|
12
|
+
const maxMessages = opts.maxMessages ?? 1;
|
|
13
|
+
const WebSocketImpl = opts.WebSocket || globalThis.WebSocket;
|
|
14
|
+
if (!WebSocketImpl) throw new Error("WebSocket not available");
|
|
15
|
+
|
|
16
|
+
const url = opts.url || POLY_WS;
|
|
17
|
+
const started = Date.now();
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let settled = false;
|
|
21
|
+
const messages = [];
|
|
22
|
+
let dataCount = 0;
|
|
23
|
+
const ws = new WebSocketImpl(url);
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
if (!settled) {
|
|
27
|
+
settled = true;
|
|
28
|
+
if (messages.length) resolve({ ok: true, partial: true, ms: Date.now() - started, messages });
|
|
29
|
+
else reject(new Error(`poly ws timeout after ${timeoutMs}ms`));
|
|
30
|
+
}
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
|
|
33
|
+
function cleanup() {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
try {
|
|
36
|
+
ws.close();
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ws.addEventListener("open", () => {
|
|
43
|
+
// CLOB market channel: assets_ids
|
|
44
|
+
ws.send(
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
assets_ids: ids,
|
|
47
|
+
type: "market",
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
ws.addEventListener("message", (ev) => {
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = JSON.parse(typeof ev.data === "string" ? ev.data : String(ev.data));
|
|
56
|
+
} catch {
|
|
57
|
+
data = { raw: String(ev.data).slice(0, 300) };
|
|
58
|
+
}
|
|
59
|
+
messages.push(data);
|
|
60
|
+
dataCount += 1;
|
|
61
|
+
if (dataCount >= maxMessages) {
|
|
62
|
+
cleanup();
|
|
63
|
+
if (!settled) {
|
|
64
|
+
settled = true;
|
|
65
|
+
resolve({
|
|
66
|
+
ok: true,
|
|
67
|
+
ms: Date.now() - started,
|
|
68
|
+
assetIds: ids,
|
|
69
|
+
messages,
|
|
70
|
+
last: messages[messages.length - 1],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
ws.addEventListener("error", () => {
|
|
77
|
+
cleanup();
|
|
78
|
+
if (!settled) {
|
|
79
|
+
settled = true;
|
|
80
|
+
reject(new Error("poly ws error"));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function polyWsHealth(opts = {}) {
|
|
87
|
+
// Use a well-known active token if none provided — caller can pass assetId
|
|
88
|
+
const assetId =
|
|
89
|
+
opts.assetId ||
|
|
90
|
+
"98022490269692409998126496127597032490334070080325855126491859374983463996227";
|
|
91
|
+
try {
|
|
92
|
+
const snap = await polyWsSnapshot([assetId], { ...opts, maxMessages: 1 });
|
|
93
|
+
return { ok: true, ms: snap.ms, got: Boolean(snap.last), partial: snap.partial || false };
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return { ok: false, error: String(e.message || e).slice(0, 160) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function polyWsBook(assetId, opts = {}) {
|
|
100
|
+
if (!assetId) throw new Error("assetId required");
|
|
101
|
+
const snap = await polyWsSnapshot([assetId], opts);
|
|
102
|
+
return { ms: snap.ms, assetId: String(assetId), data: snap.last, messages: snap.messages };
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// RH agent HTTP read client — talks to the live RH product over HTTP.
|
|
2
|
+
// Unauthenticated D0/D1 ops only. Does NOT import rhbot.
|
|
3
|
+
|
|
4
|
+
import { httpJson } from "../http.mjs";
|
|
5
|
+
|
|
6
|
+
export const RH_AGENT_DEFAULT_BASE = "http://127.0.0.1:8792";
|
|
7
|
+
|
|
8
|
+
function baseUrl(opts = {}) {
|
|
9
|
+
return (
|
|
10
|
+
opts.baseUrl ||
|
|
11
|
+
process.env.RH_AGENT_BASE_URL ||
|
|
12
|
+
process.env.RH_AGENT_URL ||
|
|
13
|
+
RH_AGENT_DEFAULT_BASE
|
|
14
|
+
).replace(/\/$/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function get(path, opts = {}) {
|
|
18
|
+
return httpJson(`${baseUrl(opts)}${path}`, {
|
|
19
|
+
fetchImpl: opts.fetchImpl,
|
|
20
|
+
timeoutMs: opts.timeoutMs,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function rhHealth(opts = {}) {
|
|
25
|
+
return get("/api/health", opts);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function rhPolicy(opts = {}) {
|
|
29
|
+
return get("/api/agent/policy", opts);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// D1 — public RH routes (no requireUser)
|
|
33
|
+
export async function rhChainConfig(opts = {}) {
|
|
34
|
+
return get("/api/chain/config", opts);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function rhChainGas(opts = {}) {
|
|
38
|
+
return get("/api/chain/gas", opts);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function rhSwapsConfig(opts = {}) {
|
|
42
|
+
return get("/api/swaps/config", opts);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function rhSwapsPresets(opts = {}) {
|
|
46
|
+
return get("/api/swaps/presets", opts);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function rhTradingStatus(opts = {}) {
|
|
50
|
+
return get("/api/trading/status", opts);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function rhNftDrops(opts = {}) {
|
|
54
|
+
return get("/api/nft-drops", opts);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function rhFeeEstimate(opts = {}) {
|
|
58
|
+
return get("/api/fee/estimate", opts);
|
|
59
|
+
}
|