@kaleidorg/mind 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/funnel.d.ts +19 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +48 -10
- package/dist/funnel.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +3 -3
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +16 -4
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
- package/dist/knowledge/bitcoin-copilot.js +102 -0
- package/dist/knowledge/bitcoin-copilot.js.map +1 -1
- package/dist/knowledge/btc-map.d.ts +14 -17
- package/dist/knowledge/btc-map.d.ts.map +1 -1
- package/dist/knowledge/btc-map.js +66 -266
- package/dist/knowledge/btc-map.js.map +1 -1
- package/dist/lsps1/contract.d.ts.map +1 -1
- package/dist/lsps1/contract.js +28 -10
- package/dist/lsps1/contract.js.map +1 -1
- package/dist/qvac/parse.d.ts +15 -0
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +68 -5
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/text.d.ts.map +1 -1
- package/dist/qvac/text.js +4 -0
- package/dist/qvac/text.js.map +1 -1
- package/dist/recipe/buy-asset-channel.d.ts +26 -0
- package/dist/recipe/buy-asset-channel.d.ts.map +1 -0
- package/dist/recipe/buy-asset-channel.js +112 -0
- package/dist/recipe/buy-asset-channel.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts +26 -18
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-atomic.js +101 -63
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.d.ts +35 -0
- package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -0
- package/dist/recipe/kaleidoswap-channel-order.js +493 -0
- package/dist/recipe/kaleidoswap-channel-order.js.map +1 -0
- package/dist/recipe/kaleidoswap-price.d.ts +21 -0
- package/dist/recipe/kaleidoswap-price.d.ts.map +1 -0
- package/dist/recipe/kaleidoswap-price.js +57 -0
- package/dist/recipe/kaleidoswap-price.js.map +1 -0
- package/dist/recipe/runner.d.ts +7 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +115 -29
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/swap.d.ts +26 -1
- package/dist/recipe/swap.d.ts.map +1 -1
- package/dist/recipe/swap.js +108 -13
- package/dist/recipe/swap.js.map +1 -1
- package/dist/recipe/types.d.ts +25 -1
- package/dist/recipe/types.d.ts.map +1 -1
- package/dist/skills/registry.d.ts +33 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +45 -1
- package/dist/skills/registry.js.map +1 -1
- package/package.json +1 -1
- package/skills/README.md +3 -0
- package/skills/kaleido-lsps/SKILL.md +101 -43
- package/skills/kaleido-trading/SKILL.md +81 -31
- package/skills/merchant-finder/SKILL.md +96 -66
- package/skills/rgb-lightning-node/SKILL.md +108 -0
- package/skills/wallet-assistant/SKILL.md +32 -21
- package/src/funnel.ts +66 -11
- package/src/index.ts +14 -2
- package/src/kaleidoswap/contract.test.ts +7 -2
- package/src/kaleidoswap/contract.ts +27 -5
- package/src/knowledge/bitcoin-copilot.ts +111 -0
- package/src/knowledge/btc-map.test.ts +53 -96
- package/src/knowledge/btc-map.ts +72 -287
- package/src/lsps1/contract.ts +32 -14
- package/src/qvac/parse.test.ts +70 -1
- package/src/qvac/parse.ts +71 -5
- package/src/qvac/text.ts +4 -0
- package/src/recipe/buy-asset-channel.test.ts +148 -0
- package/src/recipe/buy-asset-channel.ts +118 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +134 -61
- package/src/recipe/kaleidoswap-atomic.ts +112 -66
- package/src/recipe/kaleidoswap-channel-order.test.ts +333 -0
- package/src/recipe/kaleidoswap-channel-order.ts +548 -0
- package/src/recipe/kaleidoswap-price.ts +68 -0
- package/src/recipe/recipe.test.ts +61 -5
- package/src/recipe/runner.ts +128 -31
- package/src/recipe/swap.ts +109 -13
- package/src/recipe/types.ts +25 -1
- package/src/skills/registry.ts +52 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in "buy inbound channel capacity from the LSP" recipe (LSPS1).
|
|
3
|
+
*
|
|
4
|
+
* "buy a 500k inbound channel"
|
|
5
|
+
* "I need 200000 sats of inbound liquidity"
|
|
6
|
+
* "open a channel from the LSP, 1M inbound for 30 days"
|
|
7
|
+
* ↓ heuristic pre-filter (0 inf) decides to enter the recipe branch
|
|
8
|
+
* ↓ 1 model inference (forced LLM slot extraction)
|
|
9
|
+
* lsp_get_info ← LSP options + assets (read-only)
|
|
10
|
+
* lsp_estimate_fees ← LSP fee preview (read-only)
|
|
11
|
+
* rln_get_node_info ← NODE client_pubkey (read-only)
|
|
12
|
+
* ↓ [ONE confirmation gate — shows estimated total fee + channel terms]
|
|
13
|
+
* lsp_create_order ← LSP creates the order → bolt11 invoice
|
|
14
|
+
* rln_pay_invoice ← NODE pays the LSP invoice → channel opens
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the single-confirm pattern from kaleidoswapAtomicRecipe: the user
|
|
17
|
+
* decides ONCE on the fee, then create_order + pay_invoice run as one
|
|
18
|
+
* approved unit. The channel's actual opening is asynchronous — the recipe
|
|
19
|
+
* reports "order placed and paid, channel opening" and leaves polling to a
|
|
20
|
+
* follow-up turn (lsp_get_order) so the chat isn't blocked.
|
|
21
|
+
*
|
|
22
|
+
* forceModelExtract: a small model can't reliably regex out a fuzzy phrasing
|
|
23
|
+
* like "I want a channel from the LSP, 500k inbound for a month, no push" —
|
|
24
|
+
* so the recipe still owns the chain + the single confirm, but lets the LLM
|
|
25
|
+
* do the natural-language understanding for slot extraction.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { Recipe, RecipeContext } from './types.js';
|
|
29
|
+
|
|
30
|
+
/** Default expiry: ~30 days at 10-min blocks. The recipe surfaces this in confirm. */
|
|
31
|
+
const DEFAULT_EXPIRY_BLOCKS = 4320;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fire on inbound-liquidity / channel-order intent. Excludes:
|
|
35
|
+
* - explanatory / educational questions ("why do I need a channel?", "what
|
|
36
|
+
* is a channel?") — those go to RAG-backed agentic answering.
|
|
37
|
+
* - the trading skill's territory.
|
|
38
|
+
*/
|
|
39
|
+
function CHANNEL_INTENT(t: string): boolean {
|
|
40
|
+
// Explanatory/question phrasing → not an order, let the agentic path handle it.
|
|
41
|
+
if (/\b(why|how|what|when|explain|tell\s+me|do\s+I\s+need|should\s+I|can\s+I)\b/i.test(t)) return false;
|
|
42
|
+
// Explicit LSPS1 keywords always match.
|
|
43
|
+
if (/\b(lsps1|lsp\s+order|channel\s+order)\b/i.test(t)) return true;
|
|
44
|
+
// Inbound liquidity asks.
|
|
45
|
+
if (/\binbound(\s+(liquidity|capacity|channel))?\b/i.test(t)) return true;
|
|
46
|
+
if (/\bcan('?t| not)\s+receive\b/i.test(t)) return true;
|
|
47
|
+
// "Buy / open / get a channel from the LSP" (or just "from KaleidoSwap"
|
|
48
|
+
// when the keyword "channel" is present).
|
|
49
|
+
if (/\b(buy|open|get|order)\b.*\bchannel\b/i.test(t)) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SATS_RE =
|
|
54
|
+
/\b([0-9][\d,.]*)\s*(k|m|million|million sats?)?\s*(sats?|satoshis?|inbound|channel)?\b/i;
|
|
55
|
+
|
|
56
|
+
/** "500k" → 500000; "1m" → 1_000_000; "200,000" → 200000. */
|
|
57
|
+
function parseAmountWord(num: string, suffix?: string): number | undefined {
|
|
58
|
+
const n = Number(num.replace(/,/g, ''));
|
|
59
|
+
if (!Number.isFinite(n)) return undefined;
|
|
60
|
+
if (!suffix) return Math.round(n);
|
|
61
|
+
const s = suffix.toLowerCase();
|
|
62
|
+
if (/^k/.test(s)) return Math.round(n * 1_000);
|
|
63
|
+
if (/^m/.test(s)) return Math.round(n * 1_000_000);
|
|
64
|
+
return Math.round(n);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Deterministic extractor — fast pre-filter for the Funnel to decide whether
|
|
69
|
+
* to enter the recipe branch. The model still runs (forceModelExtract) for
|
|
70
|
+
* the slots actually used in execution.
|
|
71
|
+
*/
|
|
72
|
+
export function extractChannelOrder(text: string): Record<string, unknown> | null {
|
|
73
|
+
const t = text.trim();
|
|
74
|
+
if (!CHANNEL_INTENT(t)) return null;
|
|
75
|
+
|
|
76
|
+
// Count standalone numeric tokens. Two-or-more numbers (e.g.
|
|
77
|
+
// "20000 my side 80000 lsp") are ambiguous to the regex — bail out and
|
|
78
|
+
// let the LLM disambiguate; the Funnel still fires the recipe via
|
|
79
|
+
// forceModelExtract + match().
|
|
80
|
+
const numberTokens = t.match(/\b\d[\d,.]*\s*(?:k|m|million)?\b/gi) ?? [];
|
|
81
|
+
const multipleNumbers = numberTokens.length >= 2;
|
|
82
|
+
|
|
83
|
+
// Expiry in days/months/blocks → blocks (10 min ≈ 1 block). Safe to parse
|
|
84
|
+
// independently of the balance numbers because the unit token disambiguates.
|
|
85
|
+
let channel_expiry_blocks: number | undefined;
|
|
86
|
+
const exp = t.match(/(\d+)\s*(day|days|week|weeks|month|months|block|blocks)\b/i);
|
|
87
|
+
if (exp) {
|
|
88
|
+
const n = Number(exp[1]);
|
|
89
|
+
const unit = exp[2]!.toLowerCase();
|
|
90
|
+
if (/block/.test(unit)) channel_expiry_blocks = n;
|
|
91
|
+
else if (/day/.test(unit)) channel_expiry_blocks = n * 144;
|
|
92
|
+
else if (/week/.test(unit)) channel_expiry_blocks = n * 144 * 7;
|
|
93
|
+
else if (/month/.test(unit)) channel_expiry_blocks = n * 144 * 30;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Side-tagged amounts. We try KEYWORD-FIRST patterns ("on my side 5000",
|
|
97
|
+
// "lsp_balance 100000") before NUMBER-FIRST ("5000 on my side", "100k on lsp"),
|
|
98
|
+
// because "5000 lsp_balance 100000" is ambiguous to number-first regexes.
|
|
99
|
+
let client_balance_sat: number | undefined;
|
|
100
|
+
let lsp_balance_sat: number | undefined;
|
|
101
|
+
|
|
102
|
+
// NUMBER then KEYWORD — directional phrases ("X on my side", "X on lsp",
|
|
103
|
+
// "X sats my side"). The (?:sats?\s+)? lets the user say the unit between
|
|
104
|
+
// the number and the side keyword: "100k sats my side" → 100000.
|
|
105
|
+
const clientNum = t.match(/\b(\d[\d,.]*)\s*(k|m)?\s+(?:sats?\s+)?(?:on\s+(?:my|client|user)\s+side|on\s+my\b|on\s+mine\b|on\s+client|my\s+side|mine\b|as\s+push|push|outbound)\b/i);
|
|
106
|
+
if (clientNum && clientNum[1]) client_balance_sat = parseAmountWord(clientNum[1], clientNum[2]);
|
|
107
|
+
|
|
108
|
+
const lspNum = t.match(/\b(\d[\d,.]*)\s*(k|m)?\s+(?:sats?\s+)?(?:on\s+(?:the\s+)?lsps?|for\s+(?:the\s+)?lsps?|as\s+inbound|inbound|lsps?[_\s]+side)\b/i);
|
|
109
|
+
if (lspNum && lspNum[1]) lsp_balance_sat = parseAmountWord(lspNum[1], lspNum[2]);
|
|
110
|
+
|
|
111
|
+
// "on the other" / "the other side" (when "my side" was mentioned) -> lsp side
|
|
112
|
+
if (client_balance_sat != null && lsp_balance_sat == null) {
|
|
113
|
+
const otherNum = t.match(/\b(\d[\d,.]*)\s*(k|m)?\s+(?:sats?\s+)?(?:on\s+the\s+other|the\s+other\s+side)\b/i);
|
|
114
|
+
if (otherNum && otherNum[1]) lsp_balance_sat = parseAmountWord(otherNum[1], otherNum[2]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Specific anchored pattern for "on my side X and Y on the other" or similar structures
|
|
118
|
+
if (client_balance_sat != null && lsp_balance_sat == null) {
|
|
119
|
+
const otherAfterMy = t.match(/on\s+my\s+side.*?\b(\d[\d,.]*)\s*(k|m)?\s+(?:sats?\s+)?on\s+the\s+other\b/i);
|
|
120
|
+
if (otherAfterMy && otherAfterMy[1]) {
|
|
121
|
+
lsp_balance_sat = parseAmountWord(otherAfterMy[1]!, otherAfterMy[2]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If we have a "my side" client and a second untagged number, treat the second as lsp (the other side)
|
|
126
|
+
if (client_balance_sat != null && lsp_balance_sat == null) {
|
|
127
|
+
const allNumMatches = [...t.matchAll(/\b(\d[\d,.]*)\s*(k|m)?\b/gi)];
|
|
128
|
+
const clientStr = client_balance_sat.toString();
|
|
129
|
+
const otherMatch = allNumMatches.find(m => {
|
|
130
|
+
const n = m[1] ? parseAmountWord(m[1], m[2]) : null;
|
|
131
|
+
return n != null && n.toString() !== clientStr && n > 0;
|
|
132
|
+
});
|
|
133
|
+
if (otherMatch) {
|
|
134
|
+
lsp_balance_sat = parseAmountWord(otherMatch[1]!, otherMatch[2]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// KEYWORD then NUMBER — programmatic phrasings ("client_balance 5000",
|
|
139
|
+
// "lsp_balance 100000", "with 10k push"). Skip "my side" / "on lsp" here
|
|
140
|
+
// — those are number-first in real English (handled above).
|
|
141
|
+
if (client_balance_sat == null) {
|
|
142
|
+
const clientKw = t.match(/(?:client[_\s]+balance|push\s+of|outbound\s+of|with\s+(\d[\d,.]*)\s*(k|m)?\s+push)\s*(?:of\s+)?(\d[\d,.]*)?\s*(k|m)?\b/i);
|
|
143
|
+
if (clientKw) {
|
|
144
|
+
// Either "with N push" (groups 1+2) or "client_balance N" (groups 3+4).
|
|
145
|
+
const num = clientKw[1] ?? clientKw[3];
|
|
146
|
+
const suf = clientKw[1] ? clientKw[2] : clientKw[4];
|
|
147
|
+
if (num) client_balance_sat = parseAmountWord(num, suf);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (lsp_balance_sat == null) {
|
|
151
|
+
const lspKw = t.match(/(?:lsp[_\s]+balance|lsp[_\s]+side|inbound\s+capacity|inbound\s+of|lsps?[_\s]+balance)\s*(?:of\s+)?(\d[\d,.]*)\s*(k|m)?\b/i);
|
|
152
|
+
if (lspKw && lspKw[1]) lsp_balance_sat = parseAmountWord(lspKw[1], lspKw[2]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Flexible "lsp 100k" / "100k lsp" / "100k on lsp" / "on lsps" patterns (handles "lsps", "lsp's" etc.)
|
|
156
|
+
if (lsp_balance_sat == null) {
|
|
157
|
+
const lspNumFirst = t.match(/\b(\d[\d,.]*)\s*(k|m)?\s+(?:sats?\s+)?lsps?\b/i);
|
|
158
|
+
if (lspNumFirst && lspNumFirst[1]) lsp_balance_sat = parseAmountWord(lspNumFirst[1], lspNumFirst[2]);
|
|
159
|
+
const lspWordFirst = t.match(/\blsps?\s+(?:balance\s+)?(\d[\d,.]*)\s*(k|m)?\b/i);
|
|
160
|
+
if (lsp_balance_sat == null && lspWordFirst && lspWordFirst[1]) lsp_balance_sat = parseAmountWord(lspWordFirst[1], lspWordFirst[2]);
|
|
161
|
+
const lspOn = t.match(/\b(\d[\d,.]*)\s*(k|m)?\s+(?:sats?\s+)?on\s+lsps?\b/i);
|
|
162
|
+
if (lsp_balance_sat == null && lspOn && lspOn[1]) lsp_balance_sat = parseAmountWord(lspOn[1], lspOn[2]);
|
|
163
|
+
const lspAfter = t.match(/\b(\d[\d,.]*)\s*(k|m)?\s+lsps?\s*(?:side|balance)?\b/i);
|
|
164
|
+
if (lsp_balance_sat == null && lspAfter && lspAfter[1]) lsp_balance_sat = parseAmountWord(lspAfter[1], lspAfter[2]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// SINGLE-amount default: if there's only one number and we couldn't tag it
|
|
168
|
+
// as client/lsp by phrasing, treat it as lsp_balance_sat (the user is
|
|
169
|
+
// asking for inbound liquidity). With multiple numbers and no disambiguating
|
|
170
|
+
// phrasing, return null and let the LLM sort it out.
|
|
171
|
+
if (!lsp_balance_sat && !multipleNumbers) {
|
|
172
|
+
const m = t.match(/\b(\d[\d,.]*)\s*(k|m|million)?\b/i);
|
|
173
|
+
if (m && m[1]) lsp_balance_sat = parseAmountWord(m[1], m[2]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// RGB asset channel: a ticker (USDT/XAUT) plus an asset amount.
|
|
177
|
+
// We don't resolve the ticker → asset_id here — that happens deterministically
|
|
178
|
+
// from lsp_get_info during the recipe. We just record what the user said.
|
|
179
|
+
let asset_ticker: string | undefined;
|
|
180
|
+
const tickerMatch = t.match(/\b(usdt|tether|xaut|gold)\b/i);
|
|
181
|
+
if (tickerMatch) {
|
|
182
|
+
const x = tickerMatch[1]!.toLowerCase();
|
|
183
|
+
asset_ticker = /usdt|tether/.test(x) ? 'USDT' : 'XAUT';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let lsp_asset_amount: number | undefined;
|
|
187
|
+
let client_asset_amount: number | undefined;
|
|
188
|
+
// Asset amount keywords. "N USDT" alone is ambiguous; with side keywords
|
|
189
|
+
// we can disambiguate: "N USDT inbound" / "N USDT lsp side" → lsp side;
|
|
190
|
+
// "N USDT my side" / "N USDT pushed" / "pushed N USDT" → client side.
|
|
191
|
+
if (asset_ticker) {
|
|
192
|
+
// CLIENT-side asset (push)
|
|
193
|
+
const pushAssetNum = t.match(/\b(\d[\d,.]*)\s+(?:usdt|tether|xaut|gold)\s+(?:on\s+my\s+side|on\s+(?:my|client|user)\s+side|my\s+side|pushed?(?:\s+to\s+(?:my|client)\s+side)?)\b/i);
|
|
194
|
+
if (pushAssetNum && pushAssetNum[1]) client_asset_amount = parseAmountWord(pushAssetNum[1]);
|
|
195
|
+
const pushAssetKw = t.match(/\bpush(?:ed)?\s+(\d[\d,.]*)\s*(?:usdt|tether|xaut|gold)\b/i);
|
|
196
|
+
if (client_asset_amount == null && pushAssetKw && pushAssetKw[1]) client_asset_amount = parseAmountWord(pushAssetKw[1]);
|
|
197
|
+
|
|
198
|
+
// LSP-side asset (inbound)
|
|
199
|
+
const lspAssetNum = t.match(/\b(\d[\d,.]*)\s+(?:usdt|tether|xaut|gold)\s+(?:inbound|on\s+(?:the\s+)?lsp(?:\s+side)?|for\s+(?:the\s+)?lsp|lsp[_\s]+side)\b/i);
|
|
200
|
+
if (lspAssetNum && lspAssetNum[1]) lsp_asset_amount = parseAmountWord(lspAssetNum[1]);
|
|
201
|
+
|
|
202
|
+
// Default: a single "N USDT" without a side keyword → lsp side (the
|
|
203
|
+
// inbound ask). Skip if we already captured client_asset_amount and the
|
|
204
|
+
// SAME number could be the user-side amount.
|
|
205
|
+
if (lsp_asset_amount == null) {
|
|
206
|
+
const allAssetMatches = t.match(/\b\d[\d,.]*\s*(?:usdt|tether|xaut|gold)\b/gi) ?? [];
|
|
207
|
+
const ambiguous = allAssetMatches.length > 1;
|
|
208
|
+
if (!ambiguous) {
|
|
209
|
+
const am = t.match(/\b(\d[\d,.]*)\s*(?:usdt|tether|xaut|gold)\b/i);
|
|
210
|
+
if (am && am[1]) lsp_asset_amount = parseAmountWord(am[1]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const out: Record<string, unknown> = {};
|
|
216
|
+
if (lsp_balance_sat != null) out.lsp_balance_sat = lsp_balance_sat;
|
|
217
|
+
if (client_balance_sat != null) out.client_balance_sat = client_balance_sat;
|
|
218
|
+
if (channel_expiry_blocks != null) out.channel_expiry_blocks = channel_expiry_blocks;
|
|
219
|
+
if (asset_ticker != null) out.asset_ticker = asset_ticker;
|
|
220
|
+
if (lsp_asset_amount != null) out.lsp_asset_amount = lsp_asset_amount;
|
|
221
|
+
if (client_asset_amount != null) out.client_asset_amount = client_asset_amount;
|
|
222
|
+
// Return null when no concrete fields were extracted — the Funnel still
|
|
223
|
+
// fires the recipe because forceModelExtract + match() carry the intent.
|
|
224
|
+
// The runner's LLM extraction populates slots; if even the LLM can't
|
|
225
|
+
// produce lsp_balance_sat, runRecipe returns status:'needs-info'.
|
|
226
|
+
//
|
|
227
|
+
// Note: the deterministic extractor is intentionally "best effort" and a bit
|
|
228
|
+
// brittle for Funnel pre-filtering. The LLM (via forceModelExtract) is the
|
|
229
|
+
// primary slot parser for varied natural language. If you change sentence
|
|
230
|
+
// structure a lot, the LLM descriptions (with examples) are what save us.
|
|
231
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface LspAsset {
|
|
235
|
+
asset_id?: string;
|
|
236
|
+
ticker?: string;
|
|
237
|
+
name?: string;
|
|
238
|
+
precision?: number;
|
|
239
|
+
min_initial_lsp_amount?: number;
|
|
240
|
+
max_initial_lsp_amount?: number;
|
|
241
|
+
}
|
|
242
|
+
interface LspInfo {
|
|
243
|
+
lsp_connection_url?: string;
|
|
244
|
+
options?: {
|
|
245
|
+
min_initial_lsp_balance_sat?: number;
|
|
246
|
+
max_initial_lsp_balance_sat?: number;
|
|
247
|
+
max_channel_expiry_blocks?: number;
|
|
248
|
+
};
|
|
249
|
+
assets?: LspAsset[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Find the LSP's record for a ticker (USDT, XAUT). Case-insensitive. */
|
|
253
|
+
function findAsset(info: LspInfo | undefined, ticker: string | undefined): LspAsset | undefined {
|
|
254
|
+
if (!info?.assets || !ticker) return undefined;
|
|
255
|
+
const t = ticker.toUpperCase();
|
|
256
|
+
return info.assets.find((a) => (a.ticker ?? '').toUpperCase() === t);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** "100" USDT (precision 6) → 100_000_000 micro-USDT. */
|
|
260
|
+
function scaleAsset(amount: number | undefined, precision: number | undefined): number | undefined {
|
|
261
|
+
if (amount == null) return undefined;
|
|
262
|
+
const p = Number(precision ?? 0);
|
|
263
|
+
return Math.round(amount * Math.pow(10, p));
|
|
264
|
+
}
|
|
265
|
+
interface FeesResult {
|
|
266
|
+
setup_fee?: number;
|
|
267
|
+
capacity_fee?: number;
|
|
268
|
+
duration_fee?: number;
|
|
269
|
+
total_fee?: number;
|
|
270
|
+
}
|
|
271
|
+
interface NodeInfo { pubkey?: string }
|
|
272
|
+
interface OrderResult {
|
|
273
|
+
order_id?: string;
|
|
274
|
+
access_token?: string;
|
|
275
|
+
order_state?: string;
|
|
276
|
+
// The maker echoes the capacities it ACCEPTED — may differ from what was
|
|
277
|
+
// requested (e.g. it can zero client_balance_sat). Used for verification.
|
|
278
|
+
lsp_balance_sat?: number;
|
|
279
|
+
client_balance_sat?: number;
|
|
280
|
+
asset_id?: string;
|
|
281
|
+
lsp_asset_amount?: number;
|
|
282
|
+
client_asset_amount?: number;
|
|
283
|
+
payment?: {
|
|
284
|
+
bolt11?: {
|
|
285
|
+
invoice?: string;
|
|
286
|
+
order_total_sat?: number;
|
|
287
|
+
fee_total_sat?: number;
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
interface ChannelRow {
|
|
292
|
+
channel_id?: string;
|
|
293
|
+
capacity_sat?: number;
|
|
294
|
+
inbound_sat?: number;
|
|
295
|
+
outbound_sat?: number;
|
|
296
|
+
asset_id?: string;
|
|
297
|
+
asset_local_amount?: number;
|
|
298
|
+
asset_remote_amount?: number;
|
|
299
|
+
ready?: boolean;
|
|
300
|
+
status?: string;
|
|
301
|
+
}
|
|
302
|
+
interface ChannelsResult { channels?: ChannelRow[]; count?: number }
|
|
303
|
+
|
|
304
|
+
export const kaleidoswapChannelOrderRecipe: Recipe = {
|
|
305
|
+
name: 'kaleidoswap-channel-order',
|
|
306
|
+
description:
|
|
307
|
+
"Buy inbound Lightning channel capacity from the LSP via LSPS1: check options, estimate fees, fetch the user's pubkey, confirm once, create the order and pay the LSP invoice.",
|
|
308
|
+
match: (t) => CHANNEL_INTENT(t),
|
|
309
|
+
triggers: ['inbound', 'liquidity', 'channel order', 'lsps1', 'lsp', 'open channel'],
|
|
310
|
+
slots: [
|
|
311
|
+
{
|
|
312
|
+
name: 'lsp_balance_sat',
|
|
313
|
+
type: 'number',
|
|
314
|
+
description:
|
|
315
|
+
"Sats the LSP commits on THEIR side — the inbound capacity for the user. " +
|
|
316
|
+
"Phrasings: 'inbound', 'lsp side', 'their side', 'on lsp', 'on lsps', 'X for lsp', 'lsp balance', 'lsp 100k', '100k lsp', '100k on lsp', 'on the other', 'the other side' (when 'my side' mentioned). " +
|
|
317
|
+
"Example: in 'buy a channel, 20000 my side, 80000 on lsp', lsp_balance_sat = 80000. " +
|
|
318
|
+
"Another: 'buy a channel for me with 100000 on lsps and 20000 on my side' → lsp_balance_sat = 100000. " +
|
|
319
|
+
"Example: 'get a channel with 30000 on my side and 80000 on the other' → lsp_balance_sat = 80000.",
|
|
320
|
+
required: true,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'client_balance_sat',
|
|
324
|
+
type: 'number',
|
|
325
|
+
description:
|
|
326
|
+
"Sats the user PRE-FUNDS into the channel (push amount). 0 by default. " +
|
|
327
|
+
"Phrasings: 'my side', 'client side', 'outbound', 'push', 'I put in', 'X on my side', 'on my side' (the other is lsp). " +
|
|
328
|
+
"Example: in 'buy a channel, 20000 my side, 80000 on lsp', client_balance_sat = 20000. " +
|
|
329
|
+
"Another: 'with 100000 on lsps and 20000 on my side' → client_balance_sat = 20000. " +
|
|
330
|
+
"Example: 'get a channel with 30000 on my side and 80000 on the other' → client_balance_sat = 30000.",
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: 'channel_expiry_blocks',
|
|
334
|
+
type: 'number',
|
|
335
|
+
description:
|
|
336
|
+
"Lease duration in blocks (10 min per block). Default 4320 (~30 days). " +
|
|
337
|
+
"Map natural language: '1 month' → 4320, '1 week' → 1008, 'N days' → N*144.",
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: 'asset_ticker',
|
|
341
|
+
type: 'string',
|
|
342
|
+
description:
|
|
343
|
+
"RGB asset ticker for an asset channel (USDT or XAUT). Omit for a plain BTC channel. " +
|
|
344
|
+
"Recognise: 'USDT channel', 'a USDT channel', 'channel with USDT', 'Tether' → USDT; " +
|
|
345
|
+
"'gold', 'XAUT' → XAUT.",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'lsp_asset_amount',
|
|
349
|
+
type: 'number',
|
|
350
|
+
description:
|
|
351
|
+
"Asset units the LSP commits on their side. UNITS, not micro-units (the host scales " +
|
|
352
|
+
"by the asset's precision). Example: '100 USDT' → lsp_asset_amount = 100. " +
|
|
353
|
+
"Only set when the user is buying an asset channel.",
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: 'client_asset_amount',
|
|
357
|
+
type: 'number',
|
|
358
|
+
description:
|
|
359
|
+
"Asset units the LSP pushes to the USER's side at channel open (costs sats at the " +
|
|
360
|
+
"current swap rate). UNITS, not micro-units. Default 0. Only set if the user wants " +
|
|
361
|
+
"spendable asset balance immediately, not just inbound capacity. Requires rfq_id.",
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'rfq_id',
|
|
365
|
+
type: 'string',
|
|
366
|
+
description:
|
|
367
|
+
"Quote id from a prior kaleidoswap_get_quote — required only when client_asset_amount > 0 " +
|
|
368
|
+
"so the LSP can price the asset push at a fixed rate. Omit otherwise.",
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
extract: extractChannelOrder,
|
|
372
|
+
forceModelExtract: true,
|
|
373
|
+
confident: (s) => Number(s.lsp_balance_sat) > 0,
|
|
374
|
+
steps: [
|
|
375
|
+
// 1. LSP options (limits + node URI). Read-only.
|
|
376
|
+
{
|
|
377
|
+
tool: 'lsp_get_info',
|
|
378
|
+
as: 'info',
|
|
379
|
+
args: () => ({}),
|
|
380
|
+
},
|
|
381
|
+
// 2. Fee estimate for the requested size. For asset channels, the maker's
|
|
382
|
+
// estimate_fees doesn't yet take asset_id (per the integration test
|
|
383
|
+
// body) — the asset spec is on the create_order body. Estimate the
|
|
384
|
+
// sats portion; the asset side is provisioned LSP-server-side.
|
|
385
|
+
{
|
|
386
|
+
tool: 'lsp_estimate_fees',
|
|
387
|
+
as: 'fees',
|
|
388
|
+
args: (ctx) => ({
|
|
389
|
+
lsp_balance_sat: Number(ctx.slots.lsp_balance_sat),
|
|
390
|
+
client_balance_sat: Number(ctx.slots.client_balance_sat ?? 0),
|
|
391
|
+
channel_expiry_blocks: Number(ctx.slots.channel_expiry_blocks ?? DEFAULT_EXPIRY_BLOCKS),
|
|
392
|
+
}),
|
|
393
|
+
},
|
|
394
|
+
// 3. User's node pubkey — needed for create_order. Deterministic.
|
|
395
|
+
{
|
|
396
|
+
tool: 'rln_get_node_info',
|
|
397
|
+
as: 'node',
|
|
398
|
+
args: () => ({}),
|
|
399
|
+
},
|
|
400
|
+
// 3a. Snapshot existing channels so we can identify the NEW one after the
|
|
401
|
+
// order opens (diff by channel_id). Without this, verification can't
|
|
402
|
+
// tell the freshly-opened channel from pre-existing ones.
|
|
403
|
+
{
|
|
404
|
+
tool: 'rln_list_channels',
|
|
405
|
+
as: 'channels_before',
|
|
406
|
+
args: () => ({}),
|
|
407
|
+
},
|
|
408
|
+
// 3b. Asset push leg: when client_asset_amount > 0, the maker requires
|
|
409
|
+
// a fresh rfq_id from kaleidoswap_get_quote(BTC → asset) so the LSP
|
|
410
|
+
// can lock the BTC price for the asset push. The maker's RFQ ↔
|
|
411
|
+
// order asset-id check is strict, so pass the FULL rgb: URI (not
|
|
412
|
+
// the ticker) as to_asset, matching what create_order will send.
|
|
413
|
+
// Skip when there's no push asset.
|
|
414
|
+
{
|
|
415
|
+
tool: 'kaleidoswap_get_quote',
|
|
416
|
+
as: 'asset_quote',
|
|
417
|
+
args: (ctx) => {
|
|
418
|
+
const info = ctx.results.info as LspInfo | undefined;
|
|
419
|
+
const ticker = ctx.slots.asset_ticker ? String(ctx.slots.asset_ticker) : undefined;
|
|
420
|
+
const asset = findAsset(info, ticker);
|
|
421
|
+
return {
|
|
422
|
+
from_asset: 'BTC',
|
|
423
|
+
to_asset: asset?.asset_id ?? ticker ?? 'USDT',
|
|
424
|
+
amount: scaleAsset(Number(ctx.slots.client_asset_amount ?? 0), asset?.precision),
|
|
425
|
+
amount_side: 'to',
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
skipIf: (ctx) => Number(ctx.slots.client_asset_amount ?? 0) <= 0,
|
|
429
|
+
},
|
|
430
|
+
// 4. Create the order. Spend → this is where the single confirm gate
|
|
431
|
+
// fires. For asset channels we resolve the ticker → asset_id from
|
|
432
|
+
// lsp_get_info.assets, and scale the agent-facing unit amount by the
|
|
433
|
+
// asset's precision (USDT precision=6 → ×1e6).
|
|
434
|
+
{
|
|
435
|
+
tool: 'lsp_create_order',
|
|
436
|
+
as: 'order',
|
|
437
|
+
args: (ctx) => {
|
|
438
|
+
const node = ctx.results.node as NodeInfo | undefined;
|
|
439
|
+
const info = ctx.results.info as LspInfo | undefined;
|
|
440
|
+
const tickerSlot = ctx.slots.asset_ticker ? String(ctx.slots.asset_ticker) : undefined;
|
|
441
|
+
const asset = findAsset(info, tickerSlot);
|
|
442
|
+
const body: Record<string, unknown> = {
|
|
443
|
+
client_pubkey: node?.pubkey,
|
|
444
|
+
lsp_balance_sat: Number(ctx.slots.lsp_balance_sat),
|
|
445
|
+
client_balance_sat: Number(ctx.slots.client_balance_sat ?? 0),
|
|
446
|
+
channel_expiry_blocks: Number(ctx.slots.channel_expiry_blocks ?? DEFAULT_EXPIRY_BLOCKS),
|
|
447
|
+
};
|
|
448
|
+
if (asset?.asset_id && (ctx.slots.lsp_asset_amount != null || ctx.slots.client_asset_amount != null)) {
|
|
449
|
+
body.asset_id = asset.asset_id;
|
|
450
|
+
const lspAmt = scaleAsset(Number(ctx.slots.lsp_asset_amount ?? 0), asset.precision);
|
|
451
|
+
const cliAmt = scaleAsset(Number(ctx.slots.client_asset_amount ?? 0), asset.precision);
|
|
452
|
+
if (lspAmt != null) body.lsp_asset_amount = lspAmt;
|
|
453
|
+
if (cliAmt != null) body.client_asset_amount = cliAmt;
|
|
454
|
+
// client_asset_amount > 0 requires an rfq_id from a BTC→asset quote.
|
|
455
|
+
// Auto-sourced from step 3b above; falls back to a user-supplied
|
|
456
|
+
// slot if step 3b was skipped or didn't return one.
|
|
457
|
+
const autoQuote = ctx.results.asset_quote as { rfq_id?: string } | undefined;
|
|
458
|
+
const rfq = autoQuote?.rfq_id ?? (ctx.slots.rfq_id != null ? String(ctx.slots.rfq_id) : undefined);
|
|
459
|
+
if (rfq) body.rfq_id = rfq;
|
|
460
|
+
}
|
|
461
|
+
return body;
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
// 5. Pay the LSP's Lightning invoice. Spend, but no second prompt — the
|
|
465
|
+
// single recipe-level confirm covered the decision to commit funds.
|
|
466
|
+
{
|
|
467
|
+
tool: 'rln_pay_invoice',
|
|
468
|
+
as: 'paid',
|
|
469
|
+
args: (ctx) => {
|
|
470
|
+
const order = ctx.results.order as OrderResult | undefined;
|
|
471
|
+
return { invoice: order?.payment?.bolt11?.invoice };
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
// 6. VERIFY: list the node's channels so we can compare the requested
|
|
476
|
+
// capacity against what actually opened. Read-only, so no gate. On
|
|
477
|
+
// regtest the channel funds within seconds; on slower nets it may not
|
|
478
|
+
// be visible yet — the summary reports either way.
|
|
479
|
+
final: {
|
|
480
|
+
tool: 'rln_list_channels',
|
|
481
|
+
as: 'channels',
|
|
482
|
+
args: () => ({}),
|
|
483
|
+
},
|
|
484
|
+
// ONE confirmation, fired after estimate_fees + get_node_info, before
|
|
485
|
+
// lsp_create_order. Shows the real total fee + BOTH sides of the channel.
|
|
486
|
+
confirm: (ctx: RecipeContext) => {
|
|
487
|
+
const fees = ctx.results.fees as FeesResult | undefined;
|
|
488
|
+
const inbound = Number(ctx.slots.lsp_balance_sat);
|
|
489
|
+
const mine = Number(ctx.slots.client_balance_sat ?? 0);
|
|
490
|
+
const expiry = Number(ctx.slots.channel_expiry_blocks ?? DEFAULT_EXPIRY_BLOCKS);
|
|
491
|
+
const days = Math.round(expiry / 144);
|
|
492
|
+
const feeStr = fees?.total_fee != null ? ` for ${fees.total_fee.toLocaleString()} sats` : '';
|
|
493
|
+
const minePart = mine > 0 ? ` + ${mine.toLocaleString()} sats on your side` : '';
|
|
494
|
+
const ticker = ctx.slots.asset_ticker ? String(ctx.slots.asset_ticker) : undefined;
|
|
495
|
+
const lspAsset = Number(ctx.slots.lsp_asset_amount ?? 0);
|
|
496
|
+
const cliAsset = Number(ctx.slots.client_asset_amount ?? 0);
|
|
497
|
+
const assetPart = ticker
|
|
498
|
+
? ` + ${lspAsset.toLocaleString()} ${ticker} inbound${cliAsset > 0 ? ` and ${cliAsset.toLocaleString()} ${ticker} on your side` : ''}`
|
|
499
|
+
: '';
|
|
500
|
+
return `Buy a channel: ${inbound.toLocaleString()} sats inbound${minePart}${assetPart} from the LSP (~${days} days)${feeStr}. Proceed?`;
|
|
501
|
+
},
|
|
502
|
+
summary: (ctx) => {
|
|
503
|
+
const order = ctx.results.order as OrderResult | undefined;
|
|
504
|
+
const channels = ctx.results.channels as ChannelsResult | undefined;
|
|
505
|
+
const id = order?.order_id ?? '?';
|
|
506
|
+
const token = order?.access_token;
|
|
507
|
+
const tokenNote = token ? ` order_id=${id} access_token=${token}` : '';
|
|
508
|
+
const total = order?.payment?.bolt11?.order_total_sat;
|
|
509
|
+
const paid = total != null ? `, paid ${total.toLocaleString()} sats` : '';
|
|
510
|
+
|
|
511
|
+
// VERIFY requested vs accepted (the maker echoes what it actually took).
|
|
512
|
+
const reqInbound = Number(ctx.slots.lsp_balance_sat);
|
|
513
|
+
const reqMine = Number(ctx.slots.client_balance_sat ?? 0);
|
|
514
|
+
const gotInbound = order?.lsp_balance_sat;
|
|
515
|
+
const gotMine = order?.client_balance_sat;
|
|
516
|
+
const mismatches: string[] = [];
|
|
517
|
+
if (gotInbound != null && gotInbound !== reqInbound) {
|
|
518
|
+
mismatches.push(`inbound ${reqInbound.toLocaleString()}→${gotInbound.toLocaleString()} sats`);
|
|
519
|
+
}
|
|
520
|
+
if (gotMine != null && gotMine !== reqMine) {
|
|
521
|
+
mismatches.push(`your side ${reqMine.toLocaleString()}→${gotMine.toLocaleString()} sats`);
|
|
522
|
+
}
|
|
523
|
+
const adjusted = mismatches.length
|
|
524
|
+
? ` ⚠ the LSP adjusted: ${mismatches.join(', ')}.`
|
|
525
|
+
: '';
|
|
526
|
+
|
|
527
|
+
// VERIFY against the freshly-opened channel — identified by DIFF against
|
|
528
|
+
// the pre-order snapshot, so we never mistake a pre-existing channel for
|
|
529
|
+
// the new one.
|
|
530
|
+
const before = ctx.results.channels_before as ChannelsResult | undefined;
|
|
531
|
+
const beforeIds = new Set((before?.channels ?? []).map((c) => c.channel_id));
|
|
532
|
+
const fresh = (channels?.channels ?? []).filter((c) => c.channel_id && !beforeIds.has(c.channel_id));
|
|
533
|
+
const match = fresh[0];
|
|
534
|
+
let opened = ' The channel will open once the LSP confirms the payment — ask me to check its status (call lsp_get_order with the exact order_id and access_token above).';
|
|
535
|
+
if (match) {
|
|
536
|
+
const cap = match.capacity_sat != null ? `${match.capacity_sat.toLocaleString()}-sat` : 'new';
|
|
537
|
+
const ready = match.ready ? 'ready' : (match.status ?? 'opening');
|
|
538
|
+
const inb = match.inbound_sat != null ? `, ${match.inbound_sat.toLocaleString()} sats inbound` : '';
|
|
539
|
+
opened = ` New channel ${cap} is open (${ready})${inb}.`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const ticker = ctx.slots.asset_ticker ? String(ctx.slots.asset_ticker) : undefined;
|
|
543
|
+
const lspAsset = Number(ctx.slots.lsp_asset_amount ?? 0);
|
|
544
|
+
const assetPart = ticker ? ` (${lspAsset.toLocaleString()} ${ticker} inbound)` : '';
|
|
545
|
+
|
|
546
|
+
return `Channel order created. To check status use: lsp_get_order with${tokenNote} .${paid}${assetPart}.${adjusted}${opened}`;
|
|
547
|
+
},
|
|
548
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price / rate / "how much" recipe — quote-only.
|
|
3
|
+
*
|
|
4
|
+
* A price question is NOT a swap. The user wants the rate, not to move funds.
|
|
5
|
+
* This recipe fires the maker quote (with amount=1 on the asked-about asset)
|
|
6
|
+
* and stops — no init, no execute, no confirmation gate. The user can then
|
|
7
|
+
* say "ok, do it" if they want to actually swap, which routes to the atomic
|
|
8
|
+
* recipe.
|
|
9
|
+
*
|
|
10
|
+
* "what is the price of usdt in sats" → quote {from:BTC, to:USDT, amt 1 on TO}
|
|
11
|
+
* "btc price" → quote {from:USDT, to:BTC, amt 1 on TO}
|
|
12
|
+
* "how much sats for 1 usdt" → quote {from:BTC, to:USDT, amt 1 on TO}
|
|
13
|
+
*
|
|
14
|
+
* Disjoint from `kaleidoswapAtomicRecipe`: this matches PRICE phrasings only;
|
|
15
|
+
* the atomic recipe matches swap/buy/sell phrasings only. Order matters in
|
|
16
|
+
* the Funnel's recipe list — register the price recipe FIRST so a phrase
|
|
17
|
+
* like "what's the BTC price" never reaches the swap recipe.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Recipe, RecipeContext } from './types.js';
|
|
21
|
+
import { extractPriceQuery } from './swap.js';
|
|
22
|
+
|
|
23
|
+
const ASSET = /\b(btc|bitcoin|sats?|usdt|tether|xaut|gold)\b/i;
|
|
24
|
+
const PRICE_INTENT = (t: string) =>
|
|
25
|
+
/\b(price|rate|cost|worth|how\s+(?:much|many))\b/i.test(t) && ASSET.test(t);
|
|
26
|
+
|
|
27
|
+
interface QuoteResult {
|
|
28
|
+
rfq_id?: string;
|
|
29
|
+
from_amount_display?: string;
|
|
30
|
+
to_amount_display?: string;
|
|
31
|
+
fee_display?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const kaleidoswapPriceRecipe: Recipe = {
|
|
35
|
+
name: 'kaleidoswap-price',
|
|
36
|
+
description:
|
|
37
|
+
'Quote the rate of one asset in another (read-only, no swap). Triggered by "price of X", "X price", "rate of X", "how much is X", "cost of X".',
|
|
38
|
+
match: (t) => PRICE_INTENT(t),
|
|
39
|
+
triggers: ['price', 'rate', 'cost', 'worth'],
|
|
40
|
+
slots: [
|
|
41
|
+
{ name: 'from_asset', type: 'string', description: 'Denomination (the unit you want the price IN)', required: true },
|
|
42
|
+
{ name: 'to_asset', type: 'string', description: 'The asset whose price you want', required: true },
|
|
43
|
+
{ name: 'amount', type: 'number', description: 'Always 1 — pricing a unit of to_asset', required: true },
|
|
44
|
+
{ name: 'amount_side', type: 'string', description: "Always 'to' — the amount sits on the priced leg" },
|
|
45
|
+
],
|
|
46
|
+
extract: extractPriceQuery,
|
|
47
|
+
confident: (s) => !!s.from_asset && !!s.to_asset,
|
|
48
|
+
// No intermediate steps; the quote is the final action. It is read-only,
|
|
49
|
+
// so no confirmation gate fires — and there's no spend after it.
|
|
50
|
+
steps: [],
|
|
51
|
+
final: {
|
|
52
|
+
tool: 'kaleidoswap_get_quote',
|
|
53
|
+
as: 'quote',
|
|
54
|
+
args: (ctx) => ({
|
|
55
|
+
from_asset: ctx.slots.from_asset,
|
|
56
|
+
to_asset: ctx.slots.to_asset,
|
|
57
|
+
amount: ctx.slots.amount ?? 1,
|
|
58
|
+
amount_side: ctx.slots.amount_side ?? 'to',
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
summary: (ctx: RecipeContext) => {
|
|
62
|
+
const q = ctx.results.quote as QuoteResult | undefined;
|
|
63
|
+
const from = q?.from_amount_display;
|
|
64
|
+
const to = q?.to_amount_display;
|
|
65
|
+
if (from && to) return `${to} = ${from}.`;
|
|
66
|
+
return `Quoted ${ctx.slots.to_asset} at 1 unit.`;
|
|
67
|
+
},
|
|
68
|
+
};
|