@kaleidorg/mind 0.7.0 → 0.8.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/README.md +96 -8
- package/dist/context/compress.d.ts.map +1 -1
- package/dist/context/compress.js +1 -0
- package/dist/context/compress.js.map +1 -1
- package/dist/context/rgb-units.d.ts +18 -0
- package/dist/context/rgb-units.d.ts.map +1 -0
- package/dist/context/rgb-units.js +86 -0
- package/dist/context/rgb-units.js.map +1 -0
- package/dist/engine.d.ts +59 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +196 -17
- package/dist/engine.js.map +1 -1
- package/dist/fastpath/fastpath.d.ts +6 -1
- package/dist/fastpath/fastpath.d.ts.map +1 -1
- package/dist/fastpath/fastpath.js +16 -2
- package/dist/fastpath/fastpath.js.map +1 -1
- package/dist/fastpath/render.d.ts +2 -0
- package/dist/fastpath/render.d.ts.map +1 -0
- package/dist/fastpath/render.js +58 -0
- package/dist/fastpath/render.js.map +1 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +34 -18
- package/dist/funnel.js.map +1 -1
- package/dist/guards.d.ts +63 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +284 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +3 -4
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +3 -17
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/providers/openai.d.ts +64 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +233 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +33 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/qvac/config.d.ts +1 -1
- package/dist/qvac/config.js +1 -1
- package/dist/qvac/index.d.ts +1 -0
- package/dist/qvac/index.d.ts.map +1 -1
- package/dist/qvac/index.js +1 -0
- package/dist/qvac/index.js.map +1 -1
- package/dist/qvac/models.d.ts +31 -0
- package/dist/qvac/models.d.ts.map +1 -0
- package/dist/qvac/models.js +80 -0
- package/dist/qvac/models.js.map +1 -0
- package/dist/qvac/parse.d.ts +11 -0
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +29 -7
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts +14 -4
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +33 -8
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +3 -5
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/qvac/tools.d.ts +5 -0
- package/dist/qvac/tools.d.ts.map +1 -1
- package/dist/qvac/tools.js +10 -0
- package/dist/qvac/tools.js.map +1 -1
- package/dist/recipe/issue-asset.d.ts.map +1 -1
- package/dist/recipe/issue-asset.js +42 -16
- package/dist/recipe/issue-asset.js.map +1 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +1 -0
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/submarine-pay.d.ts +17 -0
- package/dist/recipe/submarine-pay.d.ts.map +1 -0
- package/dist/recipe/submarine-pay.js +79 -0
- package/dist/recipe/submarine-pay.js.map +1 -0
- package/dist/skills/registry.js +1 -1
- package/dist/skills/select.d.ts +20 -0
- package/dist/skills/select.d.ts.map +1 -0
- package/dist/skills/select.js +32 -0
- package/dist/skills/select.js.map +1 -0
- package/dist/submarine/contract.d.ts +42 -0
- package/dist/submarine/contract.d.ts.map +1 -0
- package/dist/submarine/contract.js +73 -0
- package/dist/submarine/contract.js.map +1 -0
- package/dist/testing/mock-wallet.d.ts +24 -0
- package/dist/testing/mock-wallet.d.ts.map +1 -1
- package/dist/testing/mock-wallet.js +75 -6
- package/dist/testing/mock-wallet.js.map +1 -1
- package/dist/wallet/confirm.d.ts.map +1 -1
- package/dist/wallet/confirm.js +27 -9
- package/dist/wallet/confirm.js.map +1 -1
- package/package.json +6 -1
- package/skills/README.md +1 -0
- package/skills/kaleido-trading/SKILL.md +22 -28
- package/skills/kaleido-trading/references/api.md +3 -3
- package/skills/rgb-lightning-node/SKILL.md +32 -12
- package/skills/spark-wallet/SKILL.md +1 -0
- package/skills/submarine-swaps/SKILL.md +48 -0
- package/src/context/compress.ts +1 -0
- package/src/context/rgb-units.test.ts +42 -0
- package/src/context/rgb-units.ts +89 -0
- package/src/engine.test.ts +133 -3
- package/src/engine.ts +254 -19
- package/src/fastpath/fastpath.ts +22 -2
- package/src/fastpath/render.test.ts +39 -0
- package/src/fastpath/render.ts +56 -0
- package/src/funnel.mind.test.ts +15 -17
- package/src/funnel.ts +34 -19
- package/src/guards.test.ts +399 -0
- package/src/guards.ts +299 -0
- package/src/index.ts +37 -1
- package/src/kaleidoswap/contract.test.ts +8 -16
- package/src/kaleidoswap/contract.ts +4 -32
- package/src/providers/openai.test.ts +127 -0
- package/src/providers/openai.ts +282 -0
- package/src/providers/types.ts +35 -0
- package/src/qvac/config.ts +1 -1
- package/src/qvac/index.ts +10 -0
- package/src/qvac/models.ts +98 -0
- package/src/qvac/parse.test.ts +7 -0
- package/src/qvac/parse.ts +33 -1
- package/src/qvac/provider.test.ts +54 -1
- package/src/qvac/provider.ts +49 -13
- package/src/qvac/stream.ts +3 -5
- package/src/qvac/tools.ts +10 -0
- package/src/recipe/issue-asset.test.ts +39 -0
- package/src/recipe/issue-asset.ts +41 -13
- package/src/recipe/runner.ts +1 -0
- package/src/recipe/submarine-pay.test.ts +87 -0
- package/src/recipe/submarine-pay.ts +78 -0
- package/src/skills/registry.ts +1 -1
- package/src/skills/select.ts +41 -0
- package/src/submarine/contract.ts +112 -0
- package/src/testing/mock-wallet.ts +73 -6
- package/src/wallet/confirm.ts +27 -9
package/src/qvac/provider.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { toQvacTools } from './tools.js';
|
|
|
24
24
|
|
|
25
25
|
type CompletionFn = typeof QvacSdk.completion;
|
|
26
26
|
type CancelFn = typeof QvacSdk.cancel;
|
|
27
|
+
type DeleteCacheFn = typeof QvacSdk.deleteCache;
|
|
27
28
|
|
|
28
29
|
export interface QvacProviderOptions {
|
|
29
30
|
/** The SDK's `completion` (injected — see module docs). */
|
|
@@ -41,12 +42,21 @@ export interface QvacProviderOptions {
|
|
|
41
42
|
/** Default max output tokens — caps a turn so it can't ramble. Omit for uncapped. */
|
|
42
43
|
defaultMaxTokens?: number;
|
|
43
44
|
/**
|
|
44
|
-
* Cap `<think>` reasoning at this many TOKENS (not seconds — tok/s varies
|
|
45
|
-
* the SDK
|
|
46
|
-
* the
|
|
47
|
-
* "Thinking…". Omit for
|
|
45
|
+
* Cap `<think>` reasoning at this many TOKENS (not seconds — tok/s varies).
|
|
46
|
+
* Sent as the SDK's `reasoning_budget`, so the model closes its reasoning and
|
|
47
|
+
* answers. If the stream still runs well past it, the run is cancelled and a
|
|
48
|
+
* short fallback is returned instead of hanging on "Thinking…". Omit for
|
|
49
|
+
* unlimited reasoning.
|
|
48
50
|
*/
|
|
49
51
|
maxThinkingTokens?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Keep each agentic run in a QVAC KV-cache session (`kvCache: sessionKey`),
|
|
54
|
+
* so calls after the first send only the new message instead of re-reading
|
|
55
|
+
* the whole prompt. Needs `deleteCache` to drop the session at the end.
|
|
56
|
+
*/
|
|
57
|
+
sessionCache?: boolean;
|
|
58
|
+
/** The SDK's `deleteCache` (injected); used with `sessionCache`. */
|
|
59
|
+
deleteCache?: DeleteCacheFn;
|
|
50
60
|
/** Stream the model's `<think>` reasoning, when a host wants to surface it. */
|
|
51
61
|
onThinking?: (token: string) => void;
|
|
52
62
|
/**
|
|
@@ -93,13 +103,23 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
93
103
|
// when a value is set so a host that passes neither keeps SDK defaults.
|
|
94
104
|
const temp = input.temperature ?? options.defaultTemperature;
|
|
95
105
|
const predict = input.maxTokens ?? options.defaultMaxTokens;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
:
|
|
106
|
+
// A thinking budget at or above the output cap never binds: the model can
|
|
107
|
+
// spend the whole turn reasoning and return no answer. Keep half for it.
|
|
108
|
+
const thinkingCap = input.thinking === 'off' ? 0 : (input.maxThinkingTokens ?? options.maxThinkingTokens);
|
|
109
|
+
const maxThinkingTokens =
|
|
110
|
+
thinkingCap !== undefined && predict !== undefined && thinkingCap >= predict
|
|
111
|
+
? Math.floor(predict / 2)
|
|
112
|
+
: thinkingCap;
|
|
113
|
+
// `tool_choice` is only meaningful with tools; the SDK rejects a named
|
|
114
|
+
// choice that isn't among them.
|
|
115
|
+
const toolChoice = tools && input.toolChoice ? input.toolChoice : undefined;
|
|
116
|
+
const generationParamsRaw = {
|
|
117
|
+
...(temp !== undefined ? { temp } : {}),
|
|
118
|
+
...(predict !== undefined ? { predict } : {}),
|
|
119
|
+
...(maxThinkingTokens !== undefined ? { reasoning_budget: maxThinkingTokens } : {}),
|
|
120
|
+
...(toolChoice ? { tool_choice: toolChoice } : {}),
|
|
121
|
+
};
|
|
122
|
+
const generationParams = Object.keys(generationParamsRaw).length ? generationParamsRaw : undefined;
|
|
103
123
|
|
|
104
124
|
const run = options.completion({
|
|
105
125
|
modelId,
|
|
@@ -108,6 +128,7 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
108
128
|
// Split `<think>` into separate thinkingDelta events so reasoning never
|
|
109
129
|
// pollutes the visible answer.
|
|
110
130
|
captureThinking: true,
|
|
131
|
+
...(options.sessionCache && input.sessionKey ? { kvCache: input.sessionKey } : {}),
|
|
111
132
|
...(generationParams ? { generationParams } : {}),
|
|
112
133
|
...(tools ? { tools } : {}),
|
|
113
134
|
} as unknown as Parameters<CompletionFn>[0]);
|
|
@@ -129,11 +150,13 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
129
150
|
}
|
|
130
151
|
}
|
|
131
152
|
|
|
132
|
-
const maxThinkingTokens = input.maxThinkingTokens ?? options.maxThinkingTokens;
|
|
133
153
|
const result = await consumeRun(run, {
|
|
134
154
|
onToken: input.onToken,
|
|
135
155
|
onThinking: input.onThinking ?? options.onThinking,
|
|
136
|
-
|
|
156
|
+
// Backstop only: the SDK enforces the budget itself, and our count is a
|
|
157
|
+
// char-based estimate, so leave headroom before cancelling.
|
|
158
|
+
maxThinkingTokens:
|
|
159
|
+
maxThinkingTokens === undefined ? undefined : Math.ceil(maxThinkingTokens * 1.25) + 32,
|
|
137
160
|
// Cancel the in-flight run the moment the thinking budget is blown — the
|
|
138
161
|
// SDK keeps generating otherwise. Fire-and-forget; `final` then resolves.
|
|
139
162
|
onThinkingBudgetExceeded: () => {
|
|
@@ -175,15 +198,28 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
175
198
|
...(result.stopReason ? { stopReason: result.stopReason } : {}),
|
|
176
199
|
};
|
|
177
200
|
|
|
201
|
+
const incomplete =
|
|
202
|
+
!result.text && result.toolCalls.length === 0 && (result.thinkingBudgetExceeded || !!result.truncated);
|
|
178
203
|
return {
|
|
179
204
|
text,
|
|
180
205
|
rawContent: result.rawContent,
|
|
181
206
|
toolCalls: result.toolCalls,
|
|
207
|
+
...(result.toolErrors ? { toolErrors: result.toolErrors } : {}),
|
|
182
208
|
requestId: result.requestId,
|
|
183
209
|
inference,
|
|
210
|
+
...(incomplete ? { incomplete: true } : {}),
|
|
184
211
|
};
|
|
185
212
|
},
|
|
186
213
|
|
|
214
|
+
async endSession(sessionKey: string): Promise<void> {
|
|
215
|
+
if (!options.sessionCache || !options.deleteCache) return;
|
|
216
|
+
try {
|
|
217
|
+
await options.deleteCache({ kvCacheKey: sessionKey });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.warn('[qvac] deleteCache failed:', err);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
187
223
|
async cancel(requestId: string): Promise<void> {
|
|
188
224
|
// The cancel only lands once the server has begun the request; a same-tick
|
|
189
225
|
// cancel may race the begin and is logged as a no-match by the SDK.
|
package/src/qvac/stream.ts
CHANGED
|
@@ -28,11 +28,9 @@ export interface StreamHandlers {
|
|
|
28
28
|
/** The model's `<think>` reasoning, streamed separately. */
|
|
29
29
|
onThinking?: (token: string) => void;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* only on/off), so we count thinking tokens and stop the run once they exceed
|
|
35
|
-
* this. Omit for unlimited reasoning.
|
|
31
|
+
* Stop the run once `<think>` reasoning exceeds this many tokens (estimated
|
|
32
|
+
* from characters). A backstop for the SDK's own `reasoning_budget`. Omit for
|
|
33
|
+
* unlimited reasoning.
|
|
36
34
|
*/
|
|
37
35
|
maxThinkingTokens?: number;
|
|
38
36
|
/**
|
package/src/qvac/tools.ts
CHANGED
|
@@ -104,3 +104,13 @@ export function toQvacTools(
|
|
|
104
104
|
}
|
|
105
105
|
return tools.map(toQvacTool);
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* JSON Schema for a tool's parameters: a JSON-Schema def is returned as is
|
|
110
|
+
* (nested objects intact), a Zod schema is converted to a flat object schema.
|
|
111
|
+
*/
|
|
112
|
+
export function toolParametersSchema(t: Pick<ToolDef, 'name' | 'description' | 'parameters'>): Record<string, unknown> {
|
|
113
|
+
const p = t.parameters as Obj | null | undefined;
|
|
114
|
+
if (p && typeof p === 'object' && !isZodLike(p) && ('properties' in p || p.type === 'object')) return p;
|
|
115
|
+
return toQvacTool(t).parameters;
|
|
116
|
+
}
|
|
@@ -35,6 +35,45 @@ describe('extractIssueAsset (deterministic Tier-0)', () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
describe('extractIssueAsset — review regressions', () => {
|
|
39
|
+
it('reads thousands separators in both styles, and decimals with k/m', () => {
|
|
40
|
+
expect(extractIssueAsset('crea 1.000 token CAFE chiamati Caffè Club')).toMatchObject({ amount: 1000, ticker: 'CAFE', name: 'Caffè Club' });
|
|
41
|
+
expect(extractIssueAsset('issue 1,000 TICKET tokens')).toMatchObject({ amount: 1000 });
|
|
42
|
+
expect(extractIssueAsset('mint 1,5k PIZZA tokens')).toMatchObject({ amount: 1500 });
|
|
43
|
+
expect(extractIssueAsset('mint 1.5k PIZZA tokens')).toMatchObject({ amount: 1500 });
|
|
44
|
+
});
|
|
45
|
+
it('never takes the supply from a number inside the name', () => {
|
|
46
|
+
expect(extractIssueAsset('mint tokens named Web3 Summit 2026, supply 300')).toMatchObject({ name: 'Web3 Summit 2026', amount: 300 });
|
|
47
|
+
expect(extractIssueAsset('mint tokens named Web3 Summit 2026')).toMatchObject({ name: 'Web3 Summit 2026', amount: undefined });
|
|
48
|
+
expect(issueAssetRecipe.confident!(extractIssueAsset('mint tokens named Web3 Summit 2026')!)).toBe(false);
|
|
49
|
+
expect(extractIssueAsset('launch a token called Moon with ticker MOON and 1m supply')).toMatchObject({ name: 'Moon', ticker: 'MOON', amount: 1_000_000 });
|
|
50
|
+
});
|
|
51
|
+
it('only fires when the request opens with the verb', () => {
|
|
52
|
+
expect(issueAssetRecipe.match!('I have an issue with my USDT tokens')).toBe(false);
|
|
53
|
+
expect(issueAssetRecipe.match!('how do I create a token?')).toBe(false);
|
|
54
|
+
expect(extractIssueAsset('I have an issue with my USDT tokens')).toBeNull();
|
|
55
|
+
expect(issueAssetRecipe.match!('please mint 100 PIZZA tokens')).toBe(true);
|
|
56
|
+
expect(issueAssetRecipe.match!('puoi creare 100 token CAFE?')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('apostrophes inside a name are not quotes', () => {
|
|
59
|
+
expect(extractIssueAsset("mint 100 tokens called Joe's Pizza with ticker 'JOE'")).toEqual({ name: "Joe's Pizza", ticker: 'JOE', amount: 100, schema: 'NIA' });
|
|
60
|
+
expect(extractIssueAsset("crea 50 token chiamati Bottega dell'Arte con ticker ARTE")).toMatchObject({ name: "Bottega dell'Arte", ticker: 'ARTE', amount: 50 });
|
|
61
|
+
expect(extractIssueAsset('mint 10 tokens "Pizza Points" ticker PP')).toMatchObject({ name: 'Pizza Points', ticker: 'PP', amount: 10 });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('MockWallet.reset', () => {
|
|
66
|
+
it('restores the initial state, including issued assets and UTXOs', async () => {
|
|
67
|
+
const wallet = new MockWallet({ utxos: 1 });
|
|
68
|
+
await wallet.registry().execute('rln_issue_asset', { name: 'Ticket', ticker: 'TICKET', amount: 10 });
|
|
69
|
+
expect(wallet.utxos).toBe(0);
|
|
70
|
+
wallet.reset();
|
|
71
|
+
expect(wallet.utxos).toBe(1);
|
|
72
|
+
expect(wallet.assets.TICKET).toBeUndefined();
|
|
73
|
+
await expect(wallet.registry().execute('rln_issue_asset', { name: 'Ticket', ticker: 'TICKET', amount: 10 })).resolves.toMatchObject({ issued: true });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
38
77
|
describe('issue-asset readback', () => {
|
|
39
78
|
it('fungible: supply + ticker + name', () => {
|
|
40
79
|
expect(confirmReadback({ name: 'rln_issue_asset', arguments: { name: 'Hackathon Ticket', ticker: 'TICKET', amount: 1000 } }))
|
|
@@ -13,30 +13,58 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Recipe, RecipeContext } from './types.js';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// The request must OPEN with the verb ("issue …", "please mint …", "puoi creare …"),
|
|
17
|
+
// so "I have an issue with my tokens" or "how do I create a token?" stay with the model.
|
|
18
|
+
const VERB = /^(?:(?:please|pls|per favore|can you|could you|puoi|potresti)\s+)?(issue|mint|create|launch|emetti|conia|crea|creare|emettere|coniare)\b/i;
|
|
17
19
|
const NOUN = /\b(tokens?|assets?|coins?|nfts?|tickets?|gettoni|badges?)\b/i;
|
|
18
20
|
const NOT_ISSUE = /\b(invoice|fattura|channel|canale|swap|buy|sell|send|pay|compra|vendi|invia|paga)\b/i;
|
|
19
21
|
const NFT = /\b(nfts?|unique|unico|unica)\b/i;
|
|
20
22
|
// Words that look like tickers but are part of the request, not the asset.
|
|
21
23
|
const STOPWORDS = new Set(['RGB', 'NFT', 'NFTS', 'NIA', 'CFA', 'UDA', 'BTC', 'LN', 'AN', 'A']);
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// "1000", "1,000", "1.000" (it), "1.5", "1,5" (it), optionally followed by k/m.
|
|
26
|
+
const NUM = String.raw`(\d{1,3}(?:[.,]\d{3})+|\d+(?:[.,]\d+)?)\s*([km])?`;
|
|
27
|
+
|
|
28
|
+
function toNumber(digits: string, suffix?: string): number | undefined {
|
|
29
|
+
// Groups of exactly three digits are thousands separators in both "1,000" and "1.000";
|
|
30
|
+
// a single separator followed by another digit count is a decimal point.
|
|
31
|
+
const n = /^\d{1,3}(?:[.,]\d{3})+$/.test(digits)
|
|
32
|
+
? Number(digits.replace(/[.,]/g, ''))
|
|
33
|
+
: Number(digits.replace(',', '.'));
|
|
34
|
+
const scaled = suffix ? n * (suffix.toLowerCase() === 'k' ? 1_000 : 1_000_000) : n;
|
|
35
|
+
return Number.isFinite(scaled) && scaled > 0 ? scaled : undefined;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The supply: an explicit "supply 300" / "1m supply" wins, then a number right before the token
|
|
40
|
+
* noun / ticker ("1000 TICKET tokens", "500 token"). Numbers inside the asset name
|
|
41
|
+
* ("Web3 Summit 2026") are never the supply.
|
|
42
|
+
*/
|
|
43
|
+
function parseAmount(t: string, name?: string): number | undefined {
|
|
44
|
+
const rest = name ? t.replace(name, ' ') : t;
|
|
45
|
+
const labelled = rest.match(new RegExp(String.raw`\b(?:supply|amount|quantit[àa]|totale)\s*(?:of|di|[:=])?\s*` + NUM, 'i'));
|
|
46
|
+
if (labelled) return toNumber(labelled[1]!, labelled[2]);
|
|
47
|
+
const trailing = rest.match(new RegExp(String.raw`(?:^|\s)` + NUM + String.raw`\s+(?:supply|di supply)\b`, 'i'));
|
|
48
|
+
if (trailing) return toNumber(trailing[1]!, trailing[2]);
|
|
49
|
+
const beforeNoun = rest.match(new RegExp(String.raw`(?:^|\s)` + NUM + String.raw`\s+(?:[A-Z][A-Z0-9]{1,7}\s+)?(?:tokens?|coins?|tickets?|gettoni|badges?|assets?)\b`, 'i'));
|
|
50
|
+
if (beforeNoun) return toNumber(beforeNoun[1]!, beforeNoun[2]);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const unquote = (s: string) => s.replace(/^["“'‘]+|["”'’]+$/g, '').trim();
|
|
55
|
+
|
|
31
56
|
function parseName(t: string): string | undefined {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
// "called Joe's Pizza with ticker 'JOE'" → Joe's Pizza (apostrophes inside a name are fine).
|
|
58
|
+
const m = t.match(/\b(?:called|named|chiamat[oaie])\s+(.+?)(?=\s+(?:with|ticker|con|supply)\b|[,;]|\.(?:\s|$)|$)/i);
|
|
59
|
+
if (m?.[1]) return unquote(m[1]) || undefined;
|
|
60
|
+
// Otherwise only a properly paired quote: double quotes, or single quotes that open
|
|
61
|
+
// after a space and close before a space/punctuation (so "Joe's" never opens one).
|
|
62
|
+
const quoted = t.match(/["“]([^"”]{1,40})["”]/)?.[1] ?? t.match(/(?:^|\s)['‘]([^'’]{1,40})['’](?=\s|$|[,.;!?])/)?.[1];
|
|
63
|
+
return quoted?.trim() || undefined;
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
function parseTicker(t: string, name?: string): string | undefined {
|
|
39
|
-
const explicit = t.match(/\bticker\s
|
|
67
|
+
const explicit = t.match(/\bticker\s*[:=]?\s*["'“‘]?([A-Za-z0-9]{1,8})\b/i)?.[1];
|
|
40
68
|
if (explicit) return explicit.toUpperCase();
|
|
41
69
|
// An all-caps word that isn't part of the name ("1000 TICKET tokens").
|
|
42
70
|
const nameWords = new Set((name ?? '').split(/\s+/));
|
|
@@ -57,7 +85,7 @@ export function extractIssueAsset(text: string): Record<string, unknown> | null
|
|
|
57
85
|
const schema = NFT.test(t) ? 'UDA' : 'NIA';
|
|
58
86
|
const name = parseName(t);
|
|
59
87
|
const ticker = parseTicker(t, name) ?? (name ? tickerFromName(name) : undefined);
|
|
60
|
-
const amount = schema === 'UDA' ? 1 : parseAmount(t);
|
|
88
|
+
const amount = schema === 'UDA' ? 1 : parseAmount(t, name);
|
|
61
89
|
if (!ticker && !name) return null;
|
|
62
90
|
return { name: name ?? ticker, ticker, amount, schema };
|
|
63
91
|
}
|
package/src/recipe/runner.ts
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { extractSubmarinePay, submarinePayRecipe } from './submarine-pay.js';
|
|
3
|
+
import { runRecipe } from './runner.js';
|
|
4
|
+
import { Funnel } from '../funnel.js';
|
|
5
|
+
import { paymentsRecipe } from './payments.js';
|
|
6
|
+
import { MockWallet, scriptedProvider } from '../testing/index.js';
|
|
7
|
+
import { SUBMARINE_TOOLS, isSubmarineSpendTool, formatSubmarineAmount } from '../submarine/contract.js';
|
|
8
|
+
|
|
9
|
+
// 10u = 1,000 sats on signet.
|
|
10
|
+
const INVOICE = 'lntbs10u1pnmockinvoice0qqqqqqqqqqqqqqqqqqqqqqqqq';
|
|
11
|
+
|
|
12
|
+
describe('submarine contract', () => {
|
|
13
|
+
it('only fund moves money, and it takes nothing but the swap id', () => {
|
|
14
|
+
expect(SUBMARINE_TOOLS.map((t) => t.name)).toEqual([
|
|
15
|
+
'kaleidoswap_submarine_pairs', 'kaleidoswap_submarine_create', 'kaleidoswap_submarine_fund', 'kaleidoswap_submarine_status',
|
|
16
|
+
]);
|
|
17
|
+
expect(SUBMARINE_TOOLS.filter((t) => t.requiresConfirmation).map((t) => t.name)).toEqual(['kaleidoswap_submarine_fund']);
|
|
18
|
+
expect(isSubmarineSpendTool('kaleidoswap_submarine_create')).toBe(false);
|
|
19
|
+
const fund = SUBMARINE_TOOLS.find((t) => t.name === 'kaleidoswap_submarine_fund')!;
|
|
20
|
+
expect(Object.keys((fund.parameters as any).properties)).toEqual(['swap_id']);
|
|
21
|
+
});
|
|
22
|
+
it('formats source amounts for the readback', () => {
|
|
23
|
+
expect(formatSubmarineAmount('L-USDT', '87500000')).toBe('0.875 L-USDT');
|
|
24
|
+
expect(formatSubmarineAmount('L-BTC', 1005)).toBe('1,005 sats of L-BTC');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('extractSubmarinePay', () => {
|
|
29
|
+
it('needs an invoice and an explicit Liquid asset', () => {
|
|
30
|
+
expect(extractSubmarinePay(`pay ${INVOICE} with L-USDT`)).toEqual({ invoice: INVOICE, from_asset: 'L-USDT' });
|
|
31
|
+
expect(extractSubmarinePay(`paga ${INVOICE} con USDT su Liquid`)).toEqual({ invoice: INVOICE, from_asset: 'L-USDT' });
|
|
32
|
+
expect(extractSubmarinePay(`pay ${INVOICE} using liquid bitcoin`)).toEqual({ invoice: INVOICE, from_asset: 'L-BTC' });
|
|
33
|
+
// Bare USDT is RGB USDT — not this recipe.
|
|
34
|
+
expect(extractSubmarinePay(`pay ${INVOICE} with USDT`)).toBeNull();
|
|
35
|
+
expect(extractSubmarinePay('pay bob 10 L-USDT')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('runRecipe — submarine pay against the mock /v2 maker', () => {
|
|
40
|
+
it('creates, confirms once with the maker amount, then funds', async () => {
|
|
41
|
+
const wallet = new MockWallet({ priceUsd: 80_000 });
|
|
42
|
+
const onConfirm = vi.fn(async () => ({ approved: true }));
|
|
43
|
+
const res = await runRecipe(submarinePayRecipe, `pay ${INVOICE} with L-USDT`, { provider: scriptedProvider(), tools: wallet.registry(), onConfirm });
|
|
44
|
+
expect(res.status).toBe('done');
|
|
45
|
+
expect(res.inferences).toBe(0);
|
|
46
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
47
|
+
// 1,000 sats + 0.5% at $80k = 0.804 USD → 80,400,000 units of L-USDT (8 dp).
|
|
48
|
+
expect((onConfirm.mock.calls[0] as any[])[0].summary).toBe(
|
|
49
|
+
'Pay Lightning invoice lntbs10u1p…qqqqqq with 0.804 L-USDT (fees included) through a KaleidoSwap submarine swap. Confirm?');
|
|
50
|
+
expect(wallet.liquid['L-USDT']).toBe(100 * 1e8 - 80_400_000);
|
|
51
|
+
expect(wallet.sends.map((s) => s.tool)).toEqual(['kaleidoswap_submarine_fund']);
|
|
52
|
+
expect(res.text).toMatch(/^Locked 0\.804 L-USDT in swap sub1/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('declining funds nothing, but the swap was opened (no money moved)', async () => {
|
|
56
|
+
const wallet = new MockWallet();
|
|
57
|
+
const res = await runRecipe(submarinePayRecipe, `pay ${INVOICE} with L-USDT`, {
|
|
58
|
+
provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: false }),
|
|
59
|
+
});
|
|
60
|
+
expect(res.status).toBe('cancelled');
|
|
61
|
+
expect(wallet.sends).toEqual([]);
|
|
62
|
+
expect(wallet.submarineSwaps.sub1?.funded).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('surfaces insufficient Liquid balance without locking anything', async () => {
|
|
66
|
+
const wallet = new MockWallet({ liquid: { 'L-USDT': 1000, 'L-BTC': 0 } });
|
|
67
|
+
const res = await runRecipe(submarinePayRecipe, `pay ${INVOICE} with L-USDT`, {
|
|
68
|
+
provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: true }),
|
|
69
|
+
});
|
|
70
|
+
expect(res.status).toBe('error');
|
|
71
|
+
expect(res.error ?? res.text).toMatch(/Insufficient L-USDT/);
|
|
72
|
+
expect(wallet.sends).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Funnel — submarine-pay ahead of payments', () => {
|
|
77
|
+
it('routes "pay <invoice> with L-USDT" to the submarine recipe, plain invoices elsewhere', async () => {
|
|
78
|
+
const wallet = new MockWallet();
|
|
79
|
+
const funnel = new Funnel({ provider: scriptedProvider(), tools: wallet.registry(), recipes: [submarinePayRecipe, paymentsRecipe] });
|
|
80
|
+
const out = await funnel.runTurn(`pay ${INVOICE} with L-USDT`, { onConfirm: async () => ({ approved: true }) });
|
|
81
|
+
expect(out.tier).toBe('recipe');
|
|
82
|
+
expect(out.route).toBe('submarine-pay');
|
|
83
|
+
const status = await wallet.registry().execute('kaleidoswap_submarine_status', { swap_id: 'sub1' }) as { status: string };
|
|
84
|
+
expect(status.status).toBe('transaction.claimed');
|
|
85
|
+
expect(submarinePayRecipe.match!(`pay ${INVOICE}`)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Pay this Lightning invoice with my Liquid USDT" — a submarine swap on the
|
|
3
|
+
* KaleidoSwap /v2 maker:
|
|
4
|
+
*
|
|
5
|
+
* "pay lntbs1… with L-USDT" → kaleidoswap_submarine_create → 🔒 kaleidoswap_submarine_fund
|
|
6
|
+
* "paga lntbs1… con USDT su Liquid" → same
|
|
7
|
+
*
|
|
8
|
+
* One confirmation, shown after `create` so the user sees the exact amount the
|
|
9
|
+
* maker asked for (fees included) before anything is locked. Keys never reach
|
|
10
|
+
* the model; `fund` takes only the swap id. Opt-in: register via
|
|
11
|
+
* `Funnel.recipes` BEFORE the generic payments recipe on hosts that bind the
|
|
12
|
+
* submarine tools.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Recipe, RecipeContext } from './types.js';
|
|
16
|
+
import { formatSubmarineAmount } from '../submarine/contract.js';
|
|
17
|
+
|
|
18
|
+
const INVOICE = /\b(ln(?:bcrt|tbs|bc|tb)[0-9a-z]{20,})\b/i;
|
|
19
|
+
// Only an explicit Liquid asset: a bare "USDT" is RGB USDT (atomic recipe), not L-USDT.
|
|
20
|
+
const L_USDT = /\b(l-?usdt|usdt[- ]?l|liquid[- ]usdt|usdt (?:on|su|in) liquid|usdt liquid)\b/i;
|
|
21
|
+
const L_BTC = /\b(l-?btc|liquid[- ]btc|liquid bitcoin|btc (?:on|su|in) liquid|bitcoin (?:on|su|in) liquid)\b/i;
|
|
22
|
+
|
|
23
|
+
/** Tool results may arrive as JSON text (MCP) or objects (in-process). */
|
|
24
|
+
function obj(v: unknown): Record<string, unknown> {
|
|
25
|
+
if (typeof v === 'string') {
|
|
26
|
+
try { return JSON.parse(v) as Record<string, unknown>; } catch { return {}; }
|
|
27
|
+
}
|
|
28
|
+
return (v ?? {}) as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shortInvoice(inv: string): string {
|
|
32
|
+
return `${inv.slice(0, 10)}…${inv.slice(-6)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractSubmarinePay(text: string): Record<string, unknown> | null {
|
|
36
|
+
const invoice = text.match(INVOICE)?.[1];
|
|
37
|
+
if (!invoice) return null;
|
|
38
|
+
const from_asset = L_USDT.test(text) ? 'L-USDT' : L_BTC.test(text) ? 'L-BTC' : undefined;
|
|
39
|
+
if (!from_asset) return null;
|
|
40
|
+
return { invoice, from_asset };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const submarinePayRecipe: Recipe = {
|
|
44
|
+
name: 'submarine-pay',
|
|
45
|
+
description: 'Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) via a KaleidoSwap submarine swap — one confirmation.',
|
|
46
|
+
match: (t) => INVOICE.test(t) && (L_USDT.test(t) || L_BTC.test(t)),
|
|
47
|
+
triggers: ['l-usdt', 'liquid usdt', 'l-btc', 'submarine'],
|
|
48
|
+
slots: [
|
|
49
|
+
{ name: 'invoice', type: 'string', description: 'BOLT11 Lightning invoice to pay', required: true },
|
|
50
|
+
{ name: 'from_asset', type: 'string', description: 'L-USDT or L-BTC', required: true },
|
|
51
|
+
],
|
|
52
|
+
extract: extractSubmarinePay,
|
|
53
|
+
confident: (s) => typeof s.invoice === 'string' && (s.from_asset === 'L-USDT' || s.from_asset === 'L-BTC'),
|
|
54
|
+
steps: [
|
|
55
|
+
{
|
|
56
|
+
tool: 'kaleidoswap_submarine_create',
|
|
57
|
+
as: 'swap',
|
|
58
|
+
args: (ctx) => ({ invoice: ctx.slots.invoice, from_asset: ctx.slots.from_asset }),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
confirm: (ctx: RecipeContext) => {
|
|
62
|
+
const swap = obj(ctx.results.swap);
|
|
63
|
+
const asset = String(swap.from_asset ?? ctx.slots.from_asset);
|
|
64
|
+
return `Pay Lightning invoice ${shortInvoice(String(ctx.slots.invoice))} with ${formatSubmarineAmount(asset, swap.expected_amount)} (fees included) through a KaleidoSwap submarine swap. Confirm?`;
|
|
65
|
+
},
|
|
66
|
+
final: {
|
|
67
|
+
tool: 'kaleidoswap_submarine_fund',
|
|
68
|
+
args: (ctx) => ({ swap_id: obj(ctx.results.swap).swap_id }),
|
|
69
|
+
},
|
|
70
|
+
summary: (ctx, final) => {
|
|
71
|
+
const swap = obj(ctx.results.swap);
|
|
72
|
+
const funded = obj(final);
|
|
73
|
+
const asset = String(swap.from_asset ?? ctx.slots.from_asset);
|
|
74
|
+
return `Locked ${formatSubmarineAmount(asset, swap.expected_amount)} in swap ${String(swap.swap_id)}` +
|
|
75
|
+
(funded.txid ? ` (tx ${String(funded.txid).slice(0, 12)}…)` : '') +
|
|
76
|
+
'. The maker pays the invoice once the lockup is seen — ask me for the swap status to follow it.';
|
|
77
|
+
},
|
|
78
|
+
};
|
package/src/skills/registry.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const READ_REFERENCE_TOOL = 'read_skill_reference';
|
|
|
20
20
|
* ---
|
|
21
21
|
* name: portfolio-manager
|
|
22
22
|
* description: Rebalance BTC/USDT/XAUT to target allocations.
|
|
23
|
-
* tools: get_balance, kaleidoswap_get_quote,
|
|
23
|
+
* tools: get_balance, kaleidoswap_get_quote, kaleidoswap_atomic_init
|
|
24
24
|
* triggers: rebalance, allocation, portfolio
|
|
25
25
|
* ---
|
|
26
26
|
* <instructions…>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-aware skill selection: pick a skill that can actually act with the tools
|
|
3
|
+
* a host has, before any inference runs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ToolDef } from '../types.js';
|
|
7
|
+
import type { Skill } from './types.js';
|
|
8
|
+
import { READ_REFERENCE_TOOL, SkillRegistry } from './registry.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether a skill can act given the live tool names. A `requires-tools`
|
|
12
|
+
* frontmatter list must be fully live; otherwise at least one of the skill's
|
|
13
|
+
* scoped tools must be.
|
|
14
|
+
*/
|
|
15
|
+
export function skillAvailable(skill: Skill, present: ReadonlySet<string>): boolean {
|
|
16
|
+
const required = skill.metadata?.['requires-tools'];
|
|
17
|
+
if (required) {
|
|
18
|
+
return required.split(',').map((t) => t.trim()).filter(Boolean).every((t) => present.has(t));
|
|
19
|
+
}
|
|
20
|
+
if (!skill.tools?.length) return true;
|
|
21
|
+
return skill.tools.some((t) => t !== READ_REFERENCE_TOOL && present.has(t));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Select the best skill for `query` among those that can act with `liveTools`.
|
|
26
|
+
* Skills whose `requires-tools` are missing are never picked; a skill with zero
|
|
27
|
+
* live tools is only a last resort.
|
|
28
|
+
*/
|
|
29
|
+
export function selectAvailableSkill(
|
|
30
|
+
skills: SkillRegistry | readonly Skill[],
|
|
31
|
+
query: string,
|
|
32
|
+
liveTools: Iterable<string | Pick<ToolDef, 'name'>>,
|
|
33
|
+
): Skill | null {
|
|
34
|
+
const list = skills instanceof SkillRegistry ? skills.list() : [...skills];
|
|
35
|
+
const present = new Set([...liveTools].map((t) => (typeof t === 'string' ? t : t.name)));
|
|
36
|
+
const usable = list.filter((s) => skillAvailable(s, present));
|
|
37
|
+
return (
|
|
38
|
+
new SkillRegistry(usable).select(query) ??
|
|
39
|
+
new SkillRegistry(list.filter((s) => !s.metadata?.['requires-tools'] && !usable.includes(s))).select(query)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical submarine-swap tool contract — pay a Lightning invoice from Liquid
|
|
3
|
+
* funds through the KaleidoSwap Boltz /v2 maker (kaleidoswap-maker-rs).
|
|
4
|
+
*
|
|
5
|
+
* The names match kaleido-mcp, which implements them over @kaleidorg/swap-sdk.
|
|
6
|
+
* Hosts with the SDK in-process (desktop sidecar, React Native) bind the same
|
|
7
|
+
* names with `bindSubmarineTools`.
|
|
8
|
+
*
|
|
9
|
+
* Keys and preimages never cross this contract: the host derives the refund key
|
|
10
|
+
* from its wallet mnemonic and persists the swap. `kaleidoswap_submarine_fund`
|
|
11
|
+
* — the only spend — takes nothing but the swap id, so the model cannot alter
|
|
12
|
+
* the amount, asset or lockup address it pays.
|
|
13
|
+
*
|
|
14
|
+
* Pure data — no deps, no fetch, RN-safe.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ToolDef } from '../types.js';
|
|
18
|
+
import { InProcessToolSource } from '../tools/in-process.js';
|
|
19
|
+
import type { InProcessTool } from '../tools/in-process.js';
|
|
20
|
+
|
|
21
|
+
export interface SubmarineToolDef extends ToolDef {
|
|
22
|
+
/** Moves funds → confirmation-gated. */
|
|
23
|
+
spend?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Props = Record<string, { type: string; description?: string; enum?: string[] }>;
|
|
27
|
+
|
|
28
|
+
function t(name: string, description: string, properties: Props = {}, required: string[] = [], spend = false): SubmarineToolDef {
|
|
29
|
+
return { name, description, spend, requiresConfirmation: spend, parameters: { type: 'object', properties, required } };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Assets a submarine swap can be paid from, as the maker names them. */
|
|
33
|
+
export const SUBMARINE_FROM_ASSETS = ['L-USDT', 'L-BTC'] as const;
|
|
34
|
+
export type SubmarineFromAsset = typeof SUBMARINE_FROM_ASSETS[number];
|
|
35
|
+
|
|
36
|
+
/** Smallest-unit decimals of each source asset (L-USDT: 8, L-BTC: sats). */
|
|
37
|
+
export const SUBMARINE_ASSET_DECIMALS: Record<SubmarineFromAsset, number> = { 'L-USDT': 8, 'L-BTC': 8 };
|
|
38
|
+
|
|
39
|
+
const swapId = { type: 'string', description: 'Swap id returned by kaleidoswap_submarine_create' } as const;
|
|
40
|
+
|
|
41
|
+
export const SUBMARINE_TOOLS: SubmarineToolDef[] = [
|
|
42
|
+
t('kaleidoswap_submarine_pairs',
|
|
43
|
+
'List what can pay a Lightning invoice through a submarine swap (e.g. L-USDT on Liquid → Lightning BTC): rate, min/max, fees. No args.'),
|
|
44
|
+
|
|
45
|
+
t('kaleidoswap_submarine_create',
|
|
46
|
+
'Open a submarine swap that pays a Lightning (BOLT11) invoice from Liquid funds. No funds move: returns swap_id and expected_amount (source asset smallest unit). Then confirm with the user and call kaleidoswap_submarine_fund.',
|
|
47
|
+
{
|
|
48
|
+
invoice: { type: 'string', description: 'BOLT11 Lightning invoice to pay (must carry an amount)' },
|
|
49
|
+
from_asset: { type: 'string', enum: [...SUBMARINE_FROM_ASSETS], description: 'Asset to pay with (default L-USDT)' },
|
|
50
|
+
},
|
|
51
|
+
['invoice']),
|
|
52
|
+
|
|
53
|
+
t('kaleidoswap_submarine_fund',
|
|
54
|
+
'SPEND: lock the funds for a swap from kaleidoswap_submarine_create. Takes only the swap id — amount, asset and address come from the stored swap. The maker then pays the invoice.',
|
|
55
|
+
{ swap_id: swapId },
|
|
56
|
+
['swap_id'],
|
|
57
|
+
/* spend */ true),
|
|
58
|
+
|
|
59
|
+
t('kaleidoswap_submarine_status',
|
|
60
|
+
'Status of a submarine swap. "transaction.claimed" = invoice paid, done. A failed funded swap needs a refund.',
|
|
61
|
+
{ swap_id: swapId },
|
|
62
|
+
['swap_id']),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export const SUBMARINE_SPEND_TOOLS: Set<string> = new Set(SUBMARINE_TOOLS.filter((d) => d.spend).map((d) => d.name));
|
|
66
|
+
|
|
67
|
+
export function isSubmarineSpendTool(name: string): boolean {
|
|
68
|
+
return SUBMARINE_SPEND_TOOLS.has(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getSubmarineTool(name: string): SubmarineToolDef | undefined {
|
|
72
|
+
return SUBMARINE_TOOLS.find((d) => d.name === name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Format a smallest-unit amount of a source asset for display ("52.3 L-USDT", "1,200 sats"). */
|
|
76
|
+
export function formatSubmarineAmount(asset: string, smallestUnits: unknown): string {
|
|
77
|
+
const n = Number(smallestUnits);
|
|
78
|
+
if (!Number.isFinite(n)) return `${String(smallestUnits)} ${asset}`;
|
|
79
|
+
if (asset === 'L-BTC') return `${n.toLocaleString('en-US')} sats of L-BTC`;
|
|
80
|
+
const decimals = SUBMARINE_ASSET_DECIMALS[asset as SubmarineFromAsset] ?? 0;
|
|
81
|
+
const value = n / 10 ** decimals;
|
|
82
|
+
return `${value.toLocaleString('en-US', { maximumFractionDigits: decimals })} ${asset}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type SubmarineHandler = (args: Record<string, unknown>) => Promise<unknown>;
|
|
86
|
+
|
|
87
|
+
export interface BindSubmarineOptions {
|
|
88
|
+
/** Skip tools without a handler instead of throwing (default false). */
|
|
89
|
+
allowMissing?: boolean;
|
|
90
|
+
/** ToolSource id for the registry (default 'submarine'). */
|
|
91
|
+
id?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Bind the submarine contract to in-process handlers → an InProcessToolSource. */
|
|
95
|
+
export function bindSubmarineTools(handlers: Record<string, SubmarineHandler>, opts: BindSubmarineOptions = {}): InProcessToolSource {
|
|
96
|
+
const bound: InProcessTool[] = [];
|
|
97
|
+
for (const def of SUBMARINE_TOOLS) {
|
|
98
|
+
const handler = handlers[def.name];
|
|
99
|
+
if (!handler) {
|
|
100
|
+
if (opts.allowMissing) continue;
|
|
101
|
+
throw new Error(`bindSubmarineTools: no handler for "${def.name}"`);
|
|
102
|
+
}
|
|
103
|
+
bound.push({
|
|
104
|
+
name: def.name,
|
|
105
|
+
description: def.description,
|
|
106
|
+
parameters: def.parameters,
|
|
107
|
+
requiresConfirmation: def.requiresConfirmation,
|
|
108
|
+
handler,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return new InProcessToolSource(opts.id ?? 'submarine', bound);
|
|
112
|
+
}
|