@blockrun/franklin 3.8.7 → 3.8.9
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/dist/agent/bash-guard.js +29 -0
- package/dist/agent/loop.js +61 -2
- package/dist/agent/permissions.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +251 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +72 -2
- package/dist/index.js +17 -1
- package/dist/panel/html.js +111 -21
- package/dist/panel/server.js +15 -4
- package/dist/tools/activate.d.ts +29 -0
- package/dist/tools/activate.js +96 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/read.js +20 -1
- package/dist/tools/tool-categories.d.ts +22 -0
- package/dist/tools/tool-categories.js +44 -0
- package/dist/tools/trading-execute.d.ts +11 -21
- package/dist/tools/trading-execute.js +43 -130
- package/dist/tools/trading-views.d.ts +64 -0
- package/dist/tools/trading-views.js +115 -0
- package/dist/tools/trading.js +86 -7
- package/dist/tools/webhook.d.ts +18 -0
- package/dist/tools/webhook.js +185 -0
- package/dist/tools/write.js +20 -0
- package/dist/trading/data.d.ts +24 -1
- package/dist/trading/data.js +67 -102
- package/dist/trading/providers/blockrun/client.d.ts +48 -0
- package/dist/trading/providers/blockrun/client.js +253 -0
- package/dist/trading/providers/blockrun/price.d.ts +24 -0
- package/dist/trading/providers/blockrun/price.js +110 -0
- package/dist/trading/providers/coingecko/client.d.ts +20 -0
- package/dist/trading/providers/coingecko/client.js +87 -0
- package/dist/trading/providers/coingecko/markets.d.ts +3 -0
- package/dist/trading/providers/coingecko/markets.js +25 -0
- package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
- package/dist/trading/providers/coingecko/ohlcv.js +29 -0
- package/dist/trading/providers/coingecko/price.d.ts +11 -0
- package/dist/trading/providers/coingecko/price.js +41 -0
- package/dist/trading/providers/coingecko/trending.d.ts +3 -0
- package/dist/trading/providers/coingecko/trending.js +22 -0
- package/dist/trading/providers/fetcher.d.ts +43 -0
- package/dist/trading/providers/fetcher.js +45 -0
- package/dist/trading/providers/registry.d.ts +45 -0
- package/dist/trading/providers/registry.js +82 -0
- package/dist/trading/providers/standard-models.d.ts +94 -0
- package/dist/trading/providers/standard-models.js +21 -0
- package/dist/trading/providers/telemetry.d.ts +51 -0
- package/dist/trading/providers/telemetry.js +115 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebhookPost — generic HTTP POST tool for pushing agent output to any
|
|
3
|
+
* webhook endpoint. Covers WeChat Work, Feishu, Discord, Slack, Telegram
|
|
4
|
+
* Bot API, PushPlus, ServerChan, custom HTTP receivers — anything that
|
|
5
|
+
* accepts a JSON body over POST.
|
|
6
|
+
*
|
|
7
|
+
* Intentionally not per-vendor: those integrations differ only in body
|
|
8
|
+
* shape, which the agent already knows how to construct. One tool, eight
|
|
9
|
+
* channels. If a channel needs a signature header (e.g., Feishu sign
|
|
10
|
+
* mode), the agent passes it in via `headers`.
|
|
11
|
+
*
|
|
12
|
+
* Safety: outbound URLs are a publish surface. We refuse localhost,
|
|
13
|
+
* private ranges, and file schemes so an agent can't be tricked into
|
|
14
|
+
* hitting internal services. A permission prompt fires on first use per
|
|
15
|
+
* session.
|
|
16
|
+
*/
|
|
17
|
+
import { isIP } from 'node:net';
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
19
|
+
const MAX_BODY_BYTES = 512 * 1024; // 512 KB is generous for a chat push.
|
|
20
|
+
function isPrivateHost(hostname) {
|
|
21
|
+
const h = hostname
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^\[/, '')
|
|
24
|
+
.replace(/\]$/, '')
|
|
25
|
+
.split('%', 1)[0]
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
if (h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '::' || h === '::1')
|
|
28
|
+
return true;
|
|
29
|
+
// IPv4 private ranges.
|
|
30
|
+
if (isIP(h) === 4) {
|
|
31
|
+
const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(h);
|
|
32
|
+
if (m) {
|
|
33
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
34
|
+
if (a === 10)
|
|
35
|
+
return true;
|
|
36
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
37
|
+
return true;
|
|
38
|
+
if (a === 192 && b === 168)
|
|
39
|
+
return true;
|
|
40
|
+
if (a === 169 && b === 254)
|
|
41
|
+
return true; // link-local
|
|
42
|
+
if (a === 127)
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (isIP(h) === 6) {
|
|
47
|
+
if (h.startsWith('fc') || h.startsWith('fd') || h.startsWith('fe80:'))
|
|
48
|
+
return true;
|
|
49
|
+
if (h.startsWith('::ffff:')) {
|
|
50
|
+
return isPrivateHost(h.slice('::ffff:'.length));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
async function execute(input, ctx) {
|
|
56
|
+
const { url, body, headers, method = 'POST' } = input;
|
|
57
|
+
if (!url || typeof url !== 'string') {
|
|
58
|
+
return { output: 'Error: url is required (string).', isError: true };
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = new URL(url);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { output: `Error: invalid URL: ${url}`, isError: true };
|
|
66
|
+
}
|
|
67
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
68
|
+
return { output: `Error: only http(s) URLs allowed, got ${parsed.protocol}`, isError: true };
|
|
69
|
+
}
|
|
70
|
+
if (isPrivateHost(parsed.hostname)) {
|
|
71
|
+
return {
|
|
72
|
+
output: `Error: refusing to post to private/loopback host ${parsed.hostname}. ` +
|
|
73
|
+
`WebhookPost is for public webhook endpoints only.`,
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Serialize body. Accept object/array (JSON.stringify) or string (used as-is).
|
|
78
|
+
let bodyText;
|
|
79
|
+
let contentType = 'application/json';
|
|
80
|
+
if (typeof body === 'string') {
|
|
81
|
+
bodyText = body;
|
|
82
|
+
contentType = 'text/plain';
|
|
83
|
+
}
|
|
84
|
+
else if (body === undefined || body === null) {
|
|
85
|
+
bodyText = '';
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
try {
|
|
89
|
+
bodyText = JSON.stringify(body);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return { output: `Error: body is not JSON-serializable: ${err.message}`, isError: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const bodyBytes = Buffer.byteLength(bodyText, 'utf-8');
|
|
96
|
+
if (bodyBytes > MAX_BODY_BYTES) {
|
|
97
|
+
return {
|
|
98
|
+
output: `Error: body is ${(bodyBytes / 1024).toFixed(1)} KB, exceeds ${MAX_BODY_BYTES / 1024} KB cap.`,
|
|
99
|
+
isError: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const finalHeaders = {
|
|
103
|
+
'Content-Type': contentType,
|
|
104
|
+
'User-Agent': 'franklin/3.8.9 (webhook)',
|
|
105
|
+
...(headers ?? {}),
|
|
106
|
+
};
|
|
107
|
+
const ctrl = new AbortController();
|
|
108
|
+
// Chain abort from the execution scope so Ctrl+C cancels the webhook call.
|
|
109
|
+
const onParentAbort = () => ctrl.abort();
|
|
110
|
+
ctx.abortSignal.addEventListener('abort', onParentAbort);
|
|
111
|
+
const timer = setTimeout(() => ctrl.abort(), DEFAULT_TIMEOUT_MS);
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(url, {
|
|
114
|
+
method,
|
|
115
|
+
headers: finalHeaders,
|
|
116
|
+
body: bodyText || undefined,
|
|
117
|
+
signal: ctrl.signal,
|
|
118
|
+
});
|
|
119
|
+
// Capture a small slice of the response body for debugging — most webhook
|
|
120
|
+
// endpoints return a short ack ({"ok":true}, "ok", "1", etc.).
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
const preview = text.length > 500 ? text.slice(0, 500) + '...' : text;
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
return {
|
|
125
|
+
output: `Webhook POST failed: HTTP ${res.status} ${res.statusText}\nResponse: ${preview}`,
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
output: `Posted ${bodyBytes}B to ${parsed.host}${parsed.pathname}\n` +
|
|
131
|
+
`Response ${res.status}: ${preview || '(empty)'}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err.name === 'AbortError') {
|
|
136
|
+
if (ctx.abortSignal.aborted)
|
|
137
|
+
return { output: 'Webhook POST canceled by user.', isError: true };
|
|
138
|
+
return { output: `Webhook POST timed out after ${DEFAULT_TIMEOUT_MS}ms`, isError: true };
|
|
139
|
+
}
|
|
140
|
+
return { output: `Webhook POST error: ${err.message}`, isError: true };
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
ctx.abortSignal.removeEventListener('abort', onParentAbort);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export const webhookPostCapability = {
|
|
148
|
+
spec: {
|
|
149
|
+
name: 'WebhookPost',
|
|
150
|
+
description: 'POST a JSON or plain-text payload to a webhook URL. Works with any service that ' +
|
|
151
|
+
'accepts an HTTP POST: WeChat Work bots, Feishu/Lark bots, Discord/Slack webhooks, ' +
|
|
152
|
+
'Telegram Bot API (sendMessage), PushPlus, ServerChan, or a custom receiver. ' +
|
|
153
|
+
'The agent is responsible for the body shape — each channel has its own schema ' +
|
|
154
|
+
'(e.g., Discord: { "content": "hello" }; WeChat Work: { "msgtype": "markdown", ' +
|
|
155
|
+
'"markdown": { "content": "..." } }).\n\n' +
|
|
156
|
+
'Safety: private/loopback hosts are refused. Bodies over 512KB are refused. ' +
|
|
157
|
+
'Do NOT use for GET requests — use WebFetch for reads.',
|
|
158
|
+
input_schema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
required: ['url', 'body'],
|
|
161
|
+
properties: {
|
|
162
|
+
url: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'Full https (or http) webhook URL. Must be a public host.',
|
|
165
|
+
},
|
|
166
|
+
body: {
|
|
167
|
+
description: 'Request body. Pass an object/array → JSON.stringify. Pass a string → sent ' +
|
|
168
|
+
'as text/plain. Construct the shape each channel expects.',
|
|
169
|
+
},
|
|
170
|
+
headers: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
description: 'Optional extra request headers (auth tokens, signing headers). Content-Type ' +
|
|
173
|
+
'is set automatically based on body type.',
|
|
174
|
+
},
|
|
175
|
+
method: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
enum: ['POST', 'PUT', 'PATCH'],
|
|
178
|
+
description: 'HTTP method. Defaults to POST.',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
execute,
|
|
184
|
+
concurrent: false,
|
|
185
|
+
};
|
package/dist/tools/write.js
CHANGED
|
@@ -87,6 +87,26 @@ async function execute(input, ctx) {
|
|
|
87
87
|
isError: true,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
// Write-size cap. A user-intended file write should never exceed a few
|
|
91
|
+
// MB; larger payloads are almost always accidental (log dumps, serialized
|
|
92
|
+
// objects) and refusing them explicitly beats a silent disk-full.
|
|
93
|
+
const MAX_WRITE_BYTES = 10 * 1024 * 1024;
|
|
94
|
+
const contentBytes = Buffer.byteLength(content, 'utf-8');
|
|
95
|
+
if (contentBytes > MAX_WRITE_BYTES) {
|
|
96
|
+
return {
|
|
97
|
+
output: `Error: refusing to write ${(contentBytes / 1024 / 1024).toFixed(1)}MB to ${resolved} — max allowed is ${MAX_WRITE_BYTES / 1024 / 1024}MB. Split into smaller writes, or use Bash if this is intentional bulk output.`,
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Content sniff — warn (not block) if NUL bytes detected. Text tools
|
|
102
|
+
// writing binary is almost always a mistake; explicit Buffer writes
|
|
103
|
+
// should go through Bash.
|
|
104
|
+
if (content.indexOf('\0') !== -1) {
|
|
105
|
+
return {
|
|
106
|
+
output: `Error: refusing to write NUL-byte content to ${resolved}. This tool writes text files only. For binary output use Bash with a base64 decode or an external script.`,
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
90
110
|
try {
|
|
91
111
|
// Ensure parent directory exists
|
|
92
112
|
const parentDir = path.dirname(resolved);
|
package/dist/trading/data.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy data.ts — thin shim over the provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the historical `string | Data` error-by-string return convention so
|
|
5
|
+
* the existing callers (`trading.ts`, `tools/index.ts` wiring into
|
|
6
|
+
* `LiveExchange`) keep working without a call-site rewrite. New code should
|
|
7
|
+
* import from `./providers/registry.js` and consume `PriceData | ProviderError`
|
|
8
|
+
* directly — the structured error shape enables UI color-coding and retry
|
|
9
|
+
* classification the string convention can't express.
|
|
10
|
+
*/
|
|
11
|
+
import type { AssetClass, MarketCode } from './providers/standard-models.js';
|
|
1
12
|
export interface PriceData {
|
|
2
13
|
price: number;
|
|
3
14
|
change24h: number;
|
|
@@ -24,7 +35,19 @@ export interface MarketCoin {
|
|
|
24
35
|
volume24h: number;
|
|
25
36
|
}
|
|
26
37
|
export declare function resolveId(ticker: string): string;
|
|
27
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Look up a spot price. `assetClass` defaults to 'crypto' so all existing
|
|
40
|
+
* crypto-only callers (`TradingSignal`, `LiveExchange.getPrice`) behave
|
|
41
|
+
* exactly as before. Pass 'fx' / 'commodity' / 'stock' (plus a `market`
|
|
42
|
+
* code for stocks) to hit the multi-asset Gateway endpoints.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getPrice(ticker: string, assetClass?: AssetClass, market?: MarketCode): Promise<PriceData | string>;
|
|
45
|
+
/** Convenience: FX pair lookup (e.g. "EUR-USD"). */
|
|
46
|
+
export declare function getFxPrice(ticker: string): Promise<PriceData | string>;
|
|
47
|
+
/** Convenience: commodity lookup (e.g. "XAU-USD" for gold). */
|
|
48
|
+
export declare function getCommodityPrice(ticker: string): Promise<PriceData | string>;
|
|
49
|
+
/** Convenience: stock lookup (e.g. "AAPL" on market "us"). */
|
|
50
|
+
export declare function getStockPrice(ticker: string, market: MarketCode): Promise<PriceData | string>;
|
|
28
51
|
export declare function getOHLCV(ticker: string, days?: number): Promise<OHLCVData | string>;
|
|
29
52
|
export declare function getTrending(): Promise<TrendingCoin[] | string>;
|
|
30
53
|
export declare function getMarketOverview(): Promise<MarketCoin[] | string>;
|
package/dist/trading/data.js
CHANGED
|
@@ -1,112 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return Promise.resolve(hit.data);
|
|
18
|
-
return fn().then(data => {
|
|
19
|
-
cache.set(key, { data, expiry: Date.now() + ttlMs });
|
|
20
|
-
return data;
|
|
21
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Legacy data.ts — thin shim over the provider registry.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the historical `string | Data` error-by-string return convention so
|
|
5
|
+
* the existing callers (`trading.ts`, `tools/index.ts` wiring into
|
|
6
|
+
* `LiveExchange`) keep working without a call-site rewrite. New code should
|
|
7
|
+
* import from `./providers/registry.js` and consume `PriceData | ProviderError`
|
|
8
|
+
* directly — the structured error shape enables UI color-coding and retry
|
|
9
|
+
* classification the string convention can't express.
|
|
10
|
+
*/
|
|
11
|
+
import { getProvider, getPriceProvider } from './providers/registry.js';
|
|
12
|
+
import { runFetcher } from './providers/fetcher.js';
|
|
13
|
+
import { isProviderError } from './providers/standard-models.js';
|
|
14
|
+
export function resolveId(ticker) {
|
|
15
|
+
// Delegated — kept exported for backwards compat with any external caller.
|
|
16
|
+
return ticker.toLowerCase();
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return await res.json();
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
if (e instanceof DOMException && e.name === "AbortError")
|
|
43
|
-
return "request timed out";
|
|
44
|
-
return String(e);
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
clearTimeout(timer);
|
|
48
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* Look up a spot price. `assetClass` defaults to 'crypto' so all existing
|
|
20
|
+
* crypto-only callers (`TradingSignal`, `LiveExchange.getPrice`) behave
|
|
21
|
+
* exactly as before. Pass 'fx' / 'commodity' / 'stock' (plus a `market`
|
|
22
|
+
* code for stocks) to hit the multi-asset Gateway endpoints.
|
|
23
|
+
*/
|
|
24
|
+
export async function getPrice(ticker, assetClass = 'crypto', market) {
|
|
25
|
+
const result = await runFetcher(getPriceProvider(assetClass), { ticker, assetClass, market });
|
|
26
|
+
if (isProviderError(result))
|
|
27
|
+
return result.message;
|
|
28
|
+
return {
|
|
29
|
+
price: result.priceUsd,
|
|
30
|
+
change24h: result.change24hPct,
|
|
31
|
+
volume24h: result.volume24hUsd,
|
|
32
|
+
marketCap: result.marketCapUsd,
|
|
33
|
+
};
|
|
49
34
|
}
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
/** Convenience: FX pair lookup (e.g. "EUR-USD"). */
|
|
36
|
+
export async function getFxPrice(ticker) {
|
|
37
|
+
return getPrice(ticker, 'fx');
|
|
38
|
+
}
|
|
39
|
+
/** Convenience: commodity lookup (e.g. "XAU-USD" for gold). */
|
|
40
|
+
export async function getCommodityPrice(ticker) {
|
|
41
|
+
return getPrice(ticker, 'commodity');
|
|
52
42
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
56
|
-
const raw = await geckofetch(`/simple/price?ids=${id}&vs_currencies=usd&include_24hr_change=true&include_market_cap=true&include_24hr_vol=true`);
|
|
57
|
-
if (typeof raw === "string")
|
|
58
|
-
return raw;
|
|
59
|
-
const d = raw[id];
|
|
60
|
-
if (!d)
|
|
61
|
-
return `no data for ${ticker}`;
|
|
62
|
-
return {
|
|
63
|
-
price: d.usd,
|
|
64
|
-
change24h: d.usd_24h_change,
|
|
65
|
-
volume24h: d.usd_24h_vol,
|
|
66
|
-
marketCap: d.usd_market_cap,
|
|
67
|
-
};
|
|
68
|
-
});
|
|
43
|
+
/** Convenience: stock lookup (e.g. "AAPL" on market "us"). */
|
|
44
|
+
export async function getStockPrice(ticker, market) {
|
|
45
|
+
return getPrice(ticker, 'stock', market);
|
|
69
46
|
}
|
|
70
47
|
export async function getOHLCV(ticker, days = 30) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return raw;
|
|
76
|
-
const prices = raw.prices;
|
|
77
|
-
return {
|
|
78
|
-
timestamps: prices.map(p => p[0]),
|
|
79
|
-
closes: prices.map(p => p[1]),
|
|
80
|
-
};
|
|
81
|
-
});
|
|
48
|
+
const result = await runFetcher(getProvider('ohlcv'), { ticker, days });
|
|
49
|
+
if (isProviderError(result))
|
|
50
|
+
return result.message;
|
|
51
|
+
return { closes: result.closes, timestamps: result.timestamps };
|
|
82
52
|
}
|
|
83
53
|
export async function getTrending() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
marketCapRank: c.item.market_cap_rank,
|
|
94
|
-
}));
|
|
95
|
-
});
|
|
54
|
+
const result = await runFetcher(getProvider('trending'), {});
|
|
55
|
+
if (isProviderError(result))
|
|
56
|
+
return result.message;
|
|
57
|
+
return result.map(c => ({
|
|
58
|
+
id: c.providerId,
|
|
59
|
+
name: c.name,
|
|
60
|
+
symbol: c.symbol,
|
|
61
|
+
marketCapRank: c.marketCapRank,
|
|
62
|
+
}));
|
|
96
63
|
}
|
|
97
64
|
export async function getMarketOverview() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}));
|
|
111
|
-
});
|
|
65
|
+
const result = await runFetcher(getProvider('markets'), { limit: 20 });
|
|
66
|
+
if (isProviderError(result))
|
|
67
|
+
return result.message;
|
|
68
|
+
return result.map(c => ({
|
|
69
|
+
id: c.providerId,
|
|
70
|
+
symbol: c.ticker.toLowerCase(),
|
|
71
|
+
name: c.name,
|
|
72
|
+
price: c.priceUsd,
|
|
73
|
+
change24h: c.change24hPct,
|
|
74
|
+
marketCap: c.marketCapUsd,
|
|
75
|
+
volume24h: c.volume24hUsd,
|
|
76
|
+
}));
|
|
112
77
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared BlockRun Gateway HTTP client + short-TTL cache.
|
|
3
|
+
*
|
|
4
|
+
* Used by every BlockRun-backed fetcher (price, future OHLCV, etc). Mirrors
|
|
5
|
+
* the shape of `coingecko/client.ts` so the two providers feel the same to
|
|
6
|
+
* callers and tests.
|
|
7
|
+
*
|
|
8
|
+
* Chain-aware: base URL follows `loadChain()` — Base mainnet users hit
|
|
9
|
+
* `blockrun.ai`, Solana users hit `sol.blockrun.ai`. Trading data endpoints
|
|
10
|
+
* live under `/v1/*` (not `/api/v1`, which is the LLM proxy surface).
|
|
11
|
+
*
|
|
12
|
+
* PR 1 scope: free endpoints only (crypto / fx / commodity price). Paid
|
|
13
|
+
* stocks endpoints (`/v1/stocks/{market}/price/{symbol}`) arrive in PR 2
|
|
14
|
+
* together with the x402 signing wrapper.
|
|
15
|
+
*/
|
|
16
|
+
import type { ProviderError } from '../standard-models.js';
|
|
17
|
+
export declare function cached<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T>;
|
|
18
|
+
/** For tests: wipe every cached entry. */
|
|
19
|
+
export declare function clearCache(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Fire-and-parse: GET a BlockRun Gateway REST endpoint. Returns parsed JSON
|
|
22
|
+
* or a structured ProviderError — never throws. Records latency + outcome
|
|
23
|
+
* to the telemetry singleton so the Panel Markets page can show live health.
|
|
24
|
+
*/
|
|
25
|
+
export declare function blockrunGet(path: string, opts?: {
|
|
26
|
+
endpoint: string;
|
|
27
|
+
paid?: boolean;
|
|
28
|
+
costUsd?: number;
|
|
29
|
+
}): Promise<unknown | ProviderError>;
|
|
30
|
+
/**
|
|
31
|
+
* GET a paid BlockRun Gateway endpoint with automatic x402 signing.
|
|
32
|
+
* Returns parsed JSON or a structured ProviderError. Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function blockrunGetPaid(path: string, opts: {
|
|
35
|
+
endpoint: string;
|
|
36
|
+
costUsd: number;
|
|
37
|
+
}): Promise<unknown | ProviderError>;
|
|
38
|
+
/**
|
|
39
|
+
* Pyth-style symbols always end in `-USD`. Agents may pass `BTC` meaning
|
|
40
|
+
* `BTC-USD`; normalize so both shapes work.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizePythSymbol(ticker: string): string;
|
|
43
|
+
/** TTLs chosen to match CoinGecko's; Pyth pushes more often but we don't
|
|
44
|
+
* need sub-minute freshness for Franklin's agent cadence. */
|
|
45
|
+
export declare const TTL: {
|
|
46
|
+
readonly price: number;
|
|
47
|
+
readonly ohlcv: number;
|
|
48
|
+
};
|