@sundaeswap/sprinkles 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -181
- package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +4 -4
- package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +25 -3
- package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +7 -9
- package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
- package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
- package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
- package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
- package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/index.js +60 -3
- package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -1
- package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
- package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
- package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
- package/dist/cjs/Sprinkle/actions/cli-adapter.js +20 -2
- package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -1
- package/dist/cjs/Sprinkle/actions/index.js +12 -0
- package/dist/cjs/Sprinkle/actions/index.js.map +1 -1
- package/dist/cjs/Sprinkle/actions/mcp-adapter.js +146 -4
- package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +267 -5
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/schemas.js +17 -1
- package/dist/cjs/Sprinkle/schemas.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +4 -4
- package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +25 -3
- package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +7 -9
- package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
- package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
- package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
- package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
- package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/index.js +8 -3
- package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -1
- package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
- package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
- package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
- package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
- package/dist/esm/Sprinkle/actions/cli-adapter.js +20 -2
- package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -1
- package/dist/esm/Sprinkle/actions/index.js +1 -1
- package/dist/esm/Sprinkle/actions/index.js.map +1 -1
- package/dist/esm/Sprinkle/actions/mcp-adapter.js +145 -5
- package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +259 -8
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/schemas.js +16 -0
- package/dist/esm/Sprinkle/schemas.js.map +1 -1
- package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
- package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/index.d.ts +6 -2
- package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
- package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
- package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
- package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -1
- package/dist/types/Sprinkle/actions/index.d.ts +2 -1
- package/dist/types/Sprinkle/actions/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +24 -0
- package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -1
- package/dist/types/Sprinkle/index.d.ts +3 -1
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/schemas.d.ts +72 -0
- package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/builtin-actions.test.ts +4 -4
- package/src/Sprinkle/__tests__/cli-adapter.test.ts +24 -3
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
- package/src/Sprinkle/__tests__/mcp-adapter.test.ts +7 -5
- package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
- package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
- package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
- package/src/Sprinkle/actions/builtin/index.ts +41 -2
- package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
- package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
- package/src/Sprinkle/actions/cli-adapter.ts +18 -2
- package/src/Sprinkle/actions/index.ts +2 -1
- package/src/Sprinkle/actions/mcp-adapter.ts +179 -4
- package/src/Sprinkle/index.ts +261 -3
- package/src/Sprinkle/schemas.ts +20 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native script utilities for Sprinkle actions.
|
|
3
|
+
*
|
|
4
|
+
* Provides conversion from user-friendly input formats (CBOR hex or structured JSON)
|
|
5
|
+
* to Blaze NativeScript objects, and a helper to attach scripts during tx completion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Core } from "@blaze-cardano/sdk";
|
|
9
|
+
import type { TxBuilder } from "@blaze-cardano/tx";
|
|
10
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
11
|
+
import type { TMultisigScript } from "../../schemas.js";
|
|
12
|
+
import { ActionError } from "../types.js";
|
|
13
|
+
import type { IActionContext } from "../types.js";
|
|
14
|
+
|
|
15
|
+
/** Input type: either a CBOR hex string or a MultisigScript JSON structure */
|
|
16
|
+
export type NativeScriptInput = string | TMultisigScript;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Converts a MultisigScript JSON structure to a Blaze NativeScript.
|
|
20
|
+
* Handles the recursive structure (AllOf, AnyOf, AtLeast contain nested scripts).
|
|
21
|
+
*/
|
|
22
|
+
function multisigToNativeScript(ms: TMultisigScript): Core.NativeScript {
|
|
23
|
+
if ("Signature" in ms) {
|
|
24
|
+
const hash = Core.Ed25519KeyHashHex(ms.Signature.key_hash);
|
|
25
|
+
return Core.NativeScript.newScriptPubkey(new Core.ScriptPubkey(hash));
|
|
26
|
+
}
|
|
27
|
+
if ("AllOf" in ms) {
|
|
28
|
+
const scripts = ms.AllOf.scripts.map(multisigToNativeScript);
|
|
29
|
+
return Core.NativeScript.newScriptAll(new Core.ScriptAll(scripts));
|
|
30
|
+
}
|
|
31
|
+
if ("AnyOf" in ms) {
|
|
32
|
+
const scripts = ms.AnyOf.scripts.map(multisigToNativeScript);
|
|
33
|
+
return Core.NativeScript.newScriptAny(new Core.ScriptAny(scripts));
|
|
34
|
+
}
|
|
35
|
+
if ("AtLeast" in ms) {
|
|
36
|
+
const scripts = ms.AtLeast.scripts.map(multisigToNativeScript);
|
|
37
|
+
return Core.NativeScript.newScriptNOfK(
|
|
38
|
+
new Core.ScriptNOfK(scripts, Number(ms.AtLeast.required)),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if ("Before" in ms) {
|
|
42
|
+
return Core.NativeScript.newTimelockExpiry(
|
|
43
|
+
new Core.TimelockExpiry(Core.Slot(Number(ms.Before.time))),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if ("After" in ms) {
|
|
47
|
+
return Core.NativeScript.newTimelockStart(
|
|
48
|
+
new Core.TimelockStart(Core.Slot(Number(ms.After.time))),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if ("Script" in ms) {
|
|
52
|
+
// Script variant references a script by hash — this is a ScriptPubkey
|
|
53
|
+
// but using the script hash directly as a key hash reference
|
|
54
|
+
const hash = Core.Ed25519KeyHashHex(ms.Script.script_hash);
|
|
55
|
+
return Core.NativeScript.newScriptPubkey(new Core.ScriptPubkey(hash));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new ActionError(
|
|
59
|
+
"Unrecognized MultisigScript variant",
|
|
60
|
+
"INVALID_NATIVE_SCRIPT",
|
|
61
|
+
{ input: ms },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Converts a native script input (CBOR hex string or MultisigScript JSON)
|
|
67
|
+
* to a Blaze Script object suitable for `provideScript()`.
|
|
68
|
+
*/
|
|
69
|
+
export function toNativeScript(input: NativeScriptInput): Core.Script {
|
|
70
|
+
if (typeof input === "string") {
|
|
71
|
+
try {
|
|
72
|
+
const nativeScript = Core.NativeScript.fromCbor(
|
|
73
|
+
Core.HexBlob(input),
|
|
74
|
+
);
|
|
75
|
+
return Core.Script.newNativeScript(nativeScript);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new ActionError(
|
|
78
|
+
`Invalid native script CBOR: ${err instanceof Error ? err.message : String(err)}`,
|
|
79
|
+
"INVALID_NATIVE_SCRIPT",
|
|
80
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const nativeScript = multisigToNativeScript(input);
|
|
87
|
+
return Core.Script.newNativeScript(nativeScript);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof ActionError) throw err;
|
|
90
|
+
throw new ActionError(
|
|
91
|
+
`Failed to convert MultisigScript to native script: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
"INVALID_NATIVE_SCRIPT",
|
|
93
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Regex to extract a script hash from Blaze's "Could not resolve script hash" error */
|
|
99
|
+
const MISSING_SCRIPT_HASH_RE =
|
|
100
|
+
/complete: Could not resolve script hash[: ]*([0-9a-fA-F]+)/;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds a hash → Script lookup from the addressbook in the sprinkle context.
|
|
104
|
+
* Returns an empty map if there's no addressbook.
|
|
105
|
+
*/
|
|
106
|
+
function buildAddressbookIndex(
|
|
107
|
+
addressbook: Record<string, TMultisigScript> | undefined,
|
|
108
|
+
): Map<string, Core.Script> {
|
|
109
|
+
const index = new Map<string, Core.Script>();
|
|
110
|
+
if (!addressbook) return index;
|
|
111
|
+
for (const [, ms] of Object.entries(addressbook)) {
|
|
112
|
+
try {
|
|
113
|
+
const script = toNativeScript(ms);
|
|
114
|
+
index.set(script.hash(), script);
|
|
115
|
+
} catch {
|
|
116
|
+
// Skip invalid addressbook entries
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return index;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Attaches optional native scripts to a transaction builder and completes it.
|
|
124
|
+
*
|
|
125
|
+
* If completion fails because Blaze cannot resolve a required native script hash,
|
|
126
|
+
* and a context with an addressbook is provided, it will look up the missing script
|
|
127
|
+
* by hash in the addressbook and retry once.
|
|
128
|
+
*/
|
|
129
|
+
export async function completeWithScripts<S extends TSchema>(
|
|
130
|
+
txBuilder: TxBuilder,
|
|
131
|
+
nativeScripts?: NativeScriptInput[],
|
|
132
|
+
context?: IActionContext<S>,
|
|
133
|
+
): Promise<Core.Transaction> {
|
|
134
|
+
if (nativeScripts?.length) {
|
|
135
|
+
for (const input of nativeScripts) {
|
|
136
|
+
txBuilder.provideScript(toNativeScript(input));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return await txBuilder.complete();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
144
|
+
const match = message.match(MISSING_SCRIPT_HASH_RE);
|
|
145
|
+
|
|
146
|
+
if (!match || !context?.sprinkle?.addressbook) {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Build hash index from addressbook and try to resolve the missing script
|
|
151
|
+
const addressbookIndex = buildAddressbookIndex(
|
|
152
|
+
context.sprinkle.addressbook,
|
|
153
|
+
);
|
|
154
|
+
const missingHash = match[1];
|
|
155
|
+
const script = addressbookIndex.get(missingHash);
|
|
156
|
+
|
|
157
|
+
if (!script) {
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Attach the found script and retry
|
|
162
|
+
txBuilder.provideScript(script);
|
|
163
|
+
return txBuilder.complete();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in utility actions for the Sprinkle action system.
|
|
3
|
+
* These actions provide common Cardano operations: minting tokens,
|
|
4
|
+
* sending ADA/tokens, and registering stake scripts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
9
|
+
import { Core, makeValue } from "@blaze-cardano/sdk";
|
|
10
|
+
import { ActionError } from "../types.js";
|
|
11
|
+
import type { IAction } from "../types.js";
|
|
12
|
+
import { getBlazeFromContext, isHotWallet } from "./blaze-helper.js";
|
|
13
|
+
import { NativeScriptsParam } from "../../schemas.js";
|
|
14
|
+
import type { NativeScriptInput } from "./native-script.js";
|
|
15
|
+
import { completeWithScripts } from "./native-script.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `mint-token` -- Mint native tokens using a native script derived from the wallet's payment key.
|
|
19
|
+
* The policy is a simple ScriptPubkey requiring the wallet's payment credential to sign.
|
|
20
|
+
* Requires a hot wallet.
|
|
21
|
+
*/
|
|
22
|
+
export const mintToken: IAction<
|
|
23
|
+
{ tokenName: string; amount: string; nativeScripts?: NativeScriptInput[] },
|
|
24
|
+
{ policyId: string; tokenName: string; amount: string; txCbor: string },
|
|
25
|
+
TSchema
|
|
26
|
+
> = {
|
|
27
|
+
name: "mint-token",
|
|
28
|
+
description:
|
|
29
|
+
"Mint native tokens using a native script policy derived from the wallet's payment key.",
|
|
30
|
+
category: "utility",
|
|
31
|
+
inputSchema: Type.Object({
|
|
32
|
+
tokenName: Type.String({ description: "Name of the token to mint" }),
|
|
33
|
+
amount: Type.String({
|
|
34
|
+
description: "Amount of tokens to mint (as string for BigInt safety)",
|
|
35
|
+
}),
|
|
36
|
+
nativeScripts: NativeScriptsParam,
|
|
37
|
+
}),
|
|
38
|
+
outputSchema: Type.Object({
|
|
39
|
+
policyId: Type.String({ description: "Policy ID of the minted token" }),
|
|
40
|
+
tokenName: Type.String({ description: "Token name" }),
|
|
41
|
+
amount: Type.String({ description: "Amount minted" }),
|
|
42
|
+
txCbor: Type.String({
|
|
43
|
+
description: "Unsigned transaction CBOR hex ready for signing",
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
execute: async (input, context) => {
|
|
47
|
+
const blaze = await getBlazeFromContext(context);
|
|
48
|
+
|
|
49
|
+
if (!isHotWallet(blaze)) {
|
|
50
|
+
throw new ActionError(
|
|
51
|
+
"Mint token requires a hot wallet to derive the native script policy.",
|
|
52
|
+
"COLD_WALLET",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get the wallet's payment credential hash for the native script
|
|
57
|
+
let address: Core.Address;
|
|
58
|
+
try {
|
|
59
|
+
address = await blaze.wallet.getChangeAddress();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new ActionError(
|
|
62
|
+
`Failed to get wallet address: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
"NO_ADDRESS",
|
|
64
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const baseAddress = address.asBase();
|
|
69
|
+
if (!baseAddress) {
|
|
70
|
+
throw new ActionError(
|
|
71
|
+
"Could not derive base address from wallet. Ensure the wallet has a payment credential.",
|
|
72
|
+
"INVALID_ADDRESS",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const paymentCredential = baseAddress.getPaymentCredential();
|
|
77
|
+
if (!paymentCredential?.hash) {
|
|
78
|
+
throw new ActionError(
|
|
79
|
+
"Could not extract payment credential hash from wallet address.",
|
|
80
|
+
"NO_PAYMENT_CREDENTIAL",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const hash = Core.Ed25519KeyHashHex(paymentCredential.hash.toString());
|
|
85
|
+
const tokenPolicy = new Core.ScriptPubkey(hash);
|
|
86
|
+
const policy = Core.Script.newNativeScript(
|
|
87
|
+
Core.NativeScript.newScriptPubkey(tokenPolicy),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const policyId = policy.hash();
|
|
91
|
+
const assetName = Core.AssetName(
|
|
92
|
+
Core.toHex(Buffer.from(input.tokenName)),
|
|
93
|
+
);
|
|
94
|
+
const mintAmount = BigInt(input.amount);
|
|
95
|
+
|
|
96
|
+
const mints = new Map<Core.AssetName, bigint>();
|
|
97
|
+
mints.set(assetName, mintAmount);
|
|
98
|
+
|
|
99
|
+
let tx: Core.Transaction;
|
|
100
|
+
try {
|
|
101
|
+
const txBuilder = blaze
|
|
102
|
+
.newTransaction()
|
|
103
|
+
.addMint(Core.PolicyId(policyId), mints)
|
|
104
|
+
.provideScript(policy);
|
|
105
|
+
tx = await completeWithScripts(txBuilder, input.nativeScripts, context);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err instanceof ActionError) throw err;
|
|
108
|
+
throw new ActionError(
|
|
109
|
+
`Failed to build mint transaction: ${err instanceof Error ? err.message : String(err)}`,
|
|
110
|
+
"BUILD_ERROR",
|
|
111
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
policyId: policyId.toString(),
|
|
117
|
+
tokenName: input.tokenName,
|
|
118
|
+
amount: input.amount,
|
|
119
|
+
txCbor: tx.toCbor(),
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `simple-send` -- Send ADA or a native token to a recipient address.
|
|
126
|
+
* Builds an unsigned transaction that can be signed via TxDialog or sign-transaction.
|
|
127
|
+
*/
|
|
128
|
+
export const simpleSend: IAction<
|
|
129
|
+
{
|
|
130
|
+
recipientAddress: string;
|
|
131
|
+
lovelace?: string;
|
|
132
|
+
policyId?: string;
|
|
133
|
+
assetName?: string;
|
|
134
|
+
tokenAmount?: string;
|
|
135
|
+
nativeScripts?: NativeScriptInput[];
|
|
136
|
+
},
|
|
137
|
+
{ txCbor: string },
|
|
138
|
+
TSchema
|
|
139
|
+
> = {
|
|
140
|
+
name: "simple-send",
|
|
141
|
+
description: "Send ADA or a native token to a recipient address.",
|
|
142
|
+
category: "wallet",
|
|
143
|
+
inputSchema: Type.Object({
|
|
144
|
+
recipientAddress: Type.String({
|
|
145
|
+
description: "Bech32 recipient address",
|
|
146
|
+
}),
|
|
147
|
+
lovelace: Type.Optional(
|
|
148
|
+
Type.String({
|
|
149
|
+
description:
|
|
150
|
+
"Amount of lovelace to send (as string for BigInt safety). Required if not sending a token.",
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
policyId: Type.Optional(
|
|
154
|
+
Type.String({ description: "Policy ID of the token to send" }),
|
|
155
|
+
),
|
|
156
|
+
assetName: Type.Optional(
|
|
157
|
+
Type.String({
|
|
158
|
+
description: "Asset name of the token to send (hex-encoded)",
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
161
|
+
tokenAmount: Type.Optional(
|
|
162
|
+
Type.String({
|
|
163
|
+
description: "Amount of tokens to send (as string for BigInt safety)",
|
|
164
|
+
}),
|
|
165
|
+
),
|
|
166
|
+
nativeScripts: NativeScriptsParam,
|
|
167
|
+
}),
|
|
168
|
+
outputSchema: Type.Object({
|
|
169
|
+
txCbor: Type.String({
|
|
170
|
+
description: "Unsigned transaction CBOR hex ready for signing",
|
|
171
|
+
}),
|
|
172
|
+
}),
|
|
173
|
+
execute: async (input, context) => {
|
|
174
|
+
const blaze = await getBlazeFromContext(context);
|
|
175
|
+
|
|
176
|
+
// Validate: must provide lovelace or token details
|
|
177
|
+
const hasToken = input.policyId && input.assetName && input.tokenAmount;
|
|
178
|
+
if (!input.lovelace && !hasToken) {
|
|
179
|
+
throw new ActionError(
|
|
180
|
+
"Must provide either lovelace amount or token details (policyId, assetName, tokenAmount).",
|
|
181
|
+
"INVALID_INPUT",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let recipient: Core.Address;
|
|
186
|
+
try {
|
|
187
|
+
recipient = Core.Address.fromBech32(input.recipientAddress);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
throw new ActionError(
|
|
190
|
+
`Invalid recipient address: ${err instanceof Error ? err.message : String(err)}`,
|
|
191
|
+
"INVALID_ADDRESS",
|
|
192
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let value: Core.Value;
|
|
197
|
+
if (hasToken) {
|
|
198
|
+
const assetId =
|
|
199
|
+
`${input.policyId}${input.assetName}` as Core.AssetId;
|
|
200
|
+
value = makeValue(
|
|
201
|
+
BigInt(input.lovelace ?? "0"),
|
|
202
|
+
[assetId, BigInt(input.tokenAmount!)],
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
value = makeValue(BigInt(input.lovelace!));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let tx: Core.Transaction;
|
|
209
|
+
try {
|
|
210
|
+
const txBuilder = blaze.newTransaction().payAssets(recipient, value);
|
|
211
|
+
tx = await completeWithScripts(txBuilder, input.nativeScripts, context);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err instanceof ActionError) throw err;
|
|
214
|
+
throw new ActionError(
|
|
215
|
+
`Failed to build send transaction: ${err instanceof Error ? err.message : String(err)}`,
|
|
216
|
+
"BUILD_ERROR",
|
|
217
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
txCbor: tx.toCbor(),
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `register-stake-script` -- Register a stake script credential on-chain.
|
|
229
|
+
* Takes a script hash and builds a stake registration transaction.
|
|
230
|
+
*/
|
|
231
|
+
export const registerStakeScript: IAction<
|
|
232
|
+
{ scriptHash: string; nativeScripts?: NativeScriptInput[] },
|
|
233
|
+
{ txCbor: string },
|
|
234
|
+
TSchema
|
|
235
|
+
> = {
|
|
236
|
+
name: "register-stake-script",
|
|
237
|
+
description:
|
|
238
|
+
"Register a stake script credential on-chain. Takes a script hash (hex) and builds the registration transaction.",
|
|
239
|
+
category: "utility",
|
|
240
|
+
inputSchema: Type.Object({
|
|
241
|
+
scriptHash: Type.String({
|
|
242
|
+
description: "Stake script hash in hex (28 bytes / 56 hex characters)",
|
|
243
|
+
}),
|
|
244
|
+
nativeScripts: NativeScriptsParam,
|
|
245
|
+
}),
|
|
246
|
+
outputSchema: Type.Object({
|
|
247
|
+
txCbor: Type.String({
|
|
248
|
+
description: "Unsigned transaction CBOR hex ready for signing",
|
|
249
|
+
}),
|
|
250
|
+
}),
|
|
251
|
+
execute: async (input, context) => {
|
|
252
|
+
const blaze = await getBlazeFromContext(context);
|
|
253
|
+
|
|
254
|
+
// Validate hex format
|
|
255
|
+
if (!/^[0-9a-fA-F]{56}$/.test(input.scriptHash)) {
|
|
256
|
+
throw new ActionError(
|
|
257
|
+
"Script hash must be exactly 56 hex characters (28 bytes).",
|
|
258
|
+
"INVALID_SCRIPT_HASH",
|
|
259
|
+
{ provided: input.scriptHash },
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const credential = Core.Credential.fromCore({
|
|
264
|
+
hash: Core.Hash28ByteBase16(input.scriptHash),
|
|
265
|
+
type: Core.CredentialType.ScriptHash,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
let tx: Core.Transaction;
|
|
269
|
+
try {
|
|
270
|
+
const txBuilder = blaze.newTransaction().addRegisterStake(credential);
|
|
271
|
+
tx = await completeWithScripts(txBuilder, input.nativeScripts, context);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (err instanceof ActionError) throw err;
|
|
274
|
+
throw new ActionError(
|
|
275
|
+
`Failed to build stake registration transaction: ${err instanceof Error ? err.message : String(err)}`,
|
|
276
|
+
"BUILD_ERROR",
|
|
277
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
txCbor: tx.toCbor(),
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
};
|
|
@@ -132,18 +132,34 @@ export function coerceValue(raw: unknown, schema: TSchema): unknown {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
if (isObject(schema)) {
|
|
135
|
+
let obj = raw;
|
|
135
136
|
// If it's a JSON string, parse it
|
|
136
137
|
if (typeof raw === "string") {
|
|
137
138
|
const trimmed = raw.trim();
|
|
138
139
|
if (trimmed.startsWith("{")) {
|
|
139
140
|
try {
|
|
140
|
-
|
|
141
|
+
obj = JSON.parse(trimmed);
|
|
141
142
|
} catch {
|
|
142
143
|
return raw;
|
|
143
144
|
}
|
|
145
|
+
} else {
|
|
146
|
+
return raw;
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
|
-
|
|
149
|
+
// Recurse into properties to coerce nested values
|
|
150
|
+
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
|
|
151
|
+
const props = schema.properties as Record<string, TSchema> | undefined;
|
|
152
|
+
if (props) {
|
|
153
|
+
const result: Record<string, unknown> = { ...(obj as Record<string, unknown>) };
|
|
154
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
155
|
+
if (key in result) {
|
|
156
|
+
result[key] = coerceValue(result[key], propSchema);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return obj;
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
if (isString(schema)) {
|
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
runMcp,
|
|
28
28
|
} from "./mcp-adapter.js";
|
|
29
29
|
|
|
30
|
-
export { getBuiltinActions } from "./builtin/index.js";
|
|
30
|
+
export { getBuiltinActions, toNativeScript, completeWithScripts } from "./builtin/index.js";
|
|
31
|
+
export type { NativeScriptInput } from "./builtin/index.js";
|
|
31
32
|
|
|
32
33
|
export { promptAndExecute } from "./tui-helpers.js";
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides:
|
|
5
5
|
* - TypeBox to JSON Schema conversion (typeboxToJsonSchema)
|
|
6
|
+
* - TypeBox to Zod conversion for MCP SDK compatibility (typeboxToZod)
|
|
6
7
|
* - BigInt string coercion for MCP input (coerceMcpInput)
|
|
7
8
|
* - Lazy MCP SDK import with graceful error (getMcpSdk)
|
|
8
9
|
* - MCP server creation from registered actions (createMcpServer)
|
|
@@ -279,6 +280,180 @@ export function coerceMcpInput(input: unknown, schema: TSchema): unknown {
|
|
|
279
280
|
return input;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// TypeBox to Zod conversion
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Zod module shape (the parts we use from zod).
|
|
289
|
+
* Typed loosely since zod is a transitive dependency of @modelcontextprotocol/sdk,
|
|
290
|
+
* not a direct dependency of Sprinkles.
|
|
291
|
+
*/
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
293
|
+
type ZodType = any;
|
|
294
|
+
interface IZodModule {
|
|
295
|
+
string: () => ZodType;
|
|
296
|
+
number: () => ZodType;
|
|
297
|
+
boolean: () => ZodType;
|
|
298
|
+
literal: (value: unknown) => ZodType;
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
300
|
+
object: (shape: Record<string, any>) => ZodType;
|
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
302
|
+
array: (item: any) => ZodType;
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
304
|
+
union: (members: any[]) => ZodType;
|
|
305
|
+
nullType: () => ZodType;
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
307
|
+
any: () => any;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let _zod: IZodModule | undefined;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Lazily import the Zod module.
|
|
314
|
+
* Zod is a transitive dependency of @modelcontextprotocol/sdk, so it's
|
|
315
|
+
* guaranteed to be available when MCP mode is used.
|
|
316
|
+
*/
|
|
317
|
+
async function getZod(): Promise<IZodModule> {
|
|
318
|
+
if (_zod) return _zod;
|
|
319
|
+
try {
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
+
const mod = (await import("zod")) as any;
|
|
322
|
+
_zod = {
|
|
323
|
+
string: () => mod.string(),
|
|
324
|
+
number: () => mod.number(),
|
|
325
|
+
boolean: () => mod.boolean(),
|
|
326
|
+
literal: (v: unknown) => mod.literal(v),
|
|
327
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
328
|
+
object: (shape: Record<string, any>) => mod.object(shape),
|
|
329
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
330
|
+
array: (item: any) => mod.array(item),
|
|
331
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
332
|
+
union: (members: any[]) => mod.union(members),
|
|
333
|
+
nullType: () => mod.nullType(),
|
|
334
|
+
any: () => mod.any(),
|
|
335
|
+
};
|
|
336
|
+
return _zod;
|
|
337
|
+
} catch {
|
|
338
|
+
throw new Error(
|
|
339
|
+
"MCP mode requires zod (via @modelcontextprotocol/sdk). Install @modelcontextprotocol/sdk.",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Convert a TypeBox schema to a Zod schema for MCP SDK compatibility.
|
|
346
|
+
*
|
|
347
|
+
* The MCP SDK (v1.27+) expects Zod schemas for tool input validation,
|
|
348
|
+
* not plain JSON Schema objects. This function converts TypeBox schemas
|
|
349
|
+
* to their Zod equivalents so the SDK correctly registers tool parameters.
|
|
350
|
+
*
|
|
351
|
+
* BigInt fields are represented as z.string() with a regex pattern since
|
|
352
|
+
* JSON (and thus MCP) has no BigInt type. The coerceMcpInput function
|
|
353
|
+
* handles converting these string values back to BigInt at call time.
|
|
354
|
+
*/
|
|
355
|
+
export async function typeboxToZod(schema: TSchema): Promise<ZodType> {
|
|
356
|
+
const z = await getZod();
|
|
357
|
+
return _typeboxToZod(schema, z);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _typeboxToZod(schema: TSchema, z: IZodModule): ZodType {
|
|
361
|
+
if (isBigInt(schema)) {
|
|
362
|
+
// BigInt -> string with regex pattern (JSON has no BigInt)
|
|
363
|
+
return z.string().regex(/^-?[0-9]+$/);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (isString(schema)) {
|
|
367
|
+
let s = z.string();
|
|
368
|
+
if (schema.description) s = s.describe(schema.description);
|
|
369
|
+
return s;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (isNumber(schema) || isInteger(schema)) {
|
|
373
|
+
let n = z.number();
|
|
374
|
+
if (schema.description) n = n.describe(schema.description);
|
|
375
|
+
return n;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (isBoolean(schema)) {
|
|
379
|
+
let b = z.boolean();
|
|
380
|
+
if (schema.description) b = b.describe(schema.description);
|
|
381
|
+
return b;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (isNull(schema)) {
|
|
385
|
+
return z.nullType();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (isLiteral(schema)) {
|
|
389
|
+
return z.literal(schema.const);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (isArray(schema)) {
|
|
393
|
+
const itemSchema = (schema as { items?: TSchema }).items;
|
|
394
|
+
if (itemSchema) {
|
|
395
|
+
return z.array(_typeboxToZod(itemSchema, z));
|
|
396
|
+
}
|
|
397
|
+
return z.array(z.any());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (isObject(schema)) {
|
|
401
|
+
const properties = schema.properties as Record<string, TSchema>;
|
|
402
|
+
const shape: Record<string, ZodType> = {};
|
|
403
|
+
|
|
404
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
405
|
+
let zodProp = _typeboxToZod(propSchema, z);
|
|
406
|
+
if (isOptional(propSchema)) {
|
|
407
|
+
zodProp = zodProp.optional();
|
|
408
|
+
}
|
|
409
|
+
shape[propName] = zodProp;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return z.object(shape);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (isUnion(schema)) {
|
|
416
|
+
const members = schema.anyOf.map((member: TSchema) =>
|
|
417
|
+
_typeboxToZod(member, z),
|
|
418
|
+
);
|
|
419
|
+
if (members.length >= 2) {
|
|
420
|
+
return z.union(members);
|
|
421
|
+
}
|
|
422
|
+
return members[0] ?? z.any();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Fallback: accept anything
|
|
426
|
+
return z.any();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Convert a TypeBox object schema to a Zod "raw shape" (Record<string, ZodType>).
|
|
431
|
+
* This is the format expected by the MCP SDK's tool() API.
|
|
432
|
+
*/
|
|
433
|
+
export async function typeboxToZodShape(
|
|
434
|
+
schema: TSchema,
|
|
435
|
+
): Promise<Record<string, ZodType>> {
|
|
436
|
+
const z = await getZod();
|
|
437
|
+
|
|
438
|
+
if (!isObject(schema)) {
|
|
439
|
+
// Non-object input schemas get wrapped as { input: zodSchema }
|
|
440
|
+
return { input: _typeboxToZod(schema, z) };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const properties = schema.properties as Record<string, TSchema>;
|
|
444
|
+
const shape: Record<string, ZodType> = {};
|
|
445
|
+
|
|
446
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
447
|
+
let zodProp = _typeboxToZod(propSchema, z);
|
|
448
|
+
if (isOptional(propSchema)) {
|
|
449
|
+
zodProp = zodProp.optional();
|
|
450
|
+
}
|
|
451
|
+
shape[propName] = zodProp;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return shape;
|
|
455
|
+
}
|
|
456
|
+
|
|
282
457
|
// ---------------------------------------------------------------------------
|
|
283
458
|
// Lazy MCP SDK import
|
|
284
459
|
// ---------------------------------------------------------------------------
|
|
@@ -364,15 +539,15 @@ export async function createMcpServer<S extends TSchema>(
|
|
|
364
539
|
const actions = sprinkle.listActions() as AnyAction<S>[];
|
|
365
540
|
|
|
366
541
|
for (const action of actions) {
|
|
367
|
-
const
|
|
542
|
+
const zodShape = await typeboxToZodShape(action.inputSchema);
|
|
368
543
|
|
|
369
544
|
// Register the action as an MCP tool.
|
|
370
|
-
// The high-level McpServer.tool() API accepts
|
|
371
|
-
//
|
|
545
|
+
// The high-level McpServer.tool() API accepts a Zod raw shape
|
|
546
|
+
// (Record<string, ZodType>) for input validation.
|
|
372
547
|
server.tool(
|
|
373
548
|
action.name,
|
|
374
549
|
action.description,
|
|
375
|
-
|
|
550
|
+
zodShape,
|
|
376
551
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
377
552
|
async (input: unknown): Promise<any> => {
|
|
378
553
|
const coercedInput = coerceMcpInput(input, action.inputSchema);
|