@floatonnear/mcp 0.0.0-stage → 0.1.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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Human decimal strings <-> integer base units.
3
+ *
4
+ * Everything here goes through BigInt and string manipulation, never `Number`.
5
+ * A 6-decimal token amount above ~9 billion already exceeds
6
+ * Number.MAX_SAFE_INTEGER in base units, and the failure mode of a float path
7
+ * is silent: the low digits become zeroes, the contract receives a different
8
+ * amount than the human typed, and every on-chain cap check passes because the
9
+ * contract never saw the original string.
10
+ */
11
+ /**
12
+ * Parse a human amount into base units.
13
+ *
14
+ * Throws rather than truncating when the input carries more fractional digits
15
+ * than the token supports. Truncating would send an amount the human did not
16
+ * ask for, and nothing downstream could detect it.
17
+ */
18
+ export declare function toBaseUnits(amount: string, decimals: number): bigint;
19
+ /** Render base units as a canonical human string: no trailing point, no trailing zeroes. */
20
+ export declare function fromBaseUnits(units: bigint, decimals: number): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Human decimal strings <-> integer base units.
3
+ *
4
+ * Everything here goes through BigInt and string manipulation, never `Number`.
5
+ * A 6-decimal token amount above ~9 billion already exceeds
6
+ * Number.MAX_SAFE_INTEGER in base units, and the failure mode of a float path
7
+ * is silent: the low digits become zeroes, the contract receives a different
8
+ * amount than the human typed, and every on-chain cap check passes because the
9
+ * contract never saw the original string.
10
+ */
11
+ /** `123` or `123.456`. No sign, no exponent, no separators, no bare point. */
12
+ const DECIMAL = /^\d+(\.\d+)?$/;
13
+ const MAX_DECIMALS = 38;
14
+ function assertDecimals(decimals) {
15
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > MAX_DECIMALS) {
16
+ throw new Error(`decimals must be an integer in 0..=${MAX_DECIMALS}, got ${decimals}`);
17
+ }
18
+ }
19
+ /**
20
+ * Parse a human amount into base units.
21
+ *
22
+ * Throws rather than truncating when the input carries more fractional digits
23
+ * than the token supports. Truncating would send an amount the human did not
24
+ * ask for, and nothing downstream could detect it.
25
+ */
26
+ export function toBaseUnits(amount, decimals) {
27
+ assertDecimals(decimals);
28
+ if (typeof amount !== "string") {
29
+ throw new Error("amount must be a string, so no float precision is lost before parsing");
30
+ }
31
+ if (!DECIMAL.test(amount)) {
32
+ throw new Error(`invalid amount ${JSON.stringify(amount)}: expected digits with an optional ` +
33
+ `single decimal point, e.g. "12" or "12.50" (no sign, exponent, or separators)`);
34
+ }
35
+ const [whole = "", frac = ""] = amount.split(".");
36
+ if (frac.length > decimals) {
37
+ throw new Error(`too many decimal places in ${JSON.stringify(amount)}: the token has ${decimals}, ` +
38
+ `the amount has ${frac.length}. Refusing to truncate — round it yourself and resend.`);
39
+ }
40
+ return BigInt(whole + frac.padEnd(decimals, "0"));
41
+ }
42
+ /** Render base units as a canonical human string: no trailing point, no trailing zeroes. */
43
+ export function fromBaseUnits(units, decimals) {
44
+ assertDecimals(decimals);
45
+ if (units < 0n)
46
+ throw new Error("base units must not be negative");
47
+ if (decimals === 0)
48
+ return units.toString();
49
+ const scale = 10n ** BigInt(decimals);
50
+ const whole = units / scale;
51
+ // padStart matters: 1n at 6 decimals is 0.000001, and an unpadded remainder
52
+ // would render it as 0.1.
53
+ const frac = (units % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
54
+ return frac ? `${whole}.${frac}` : whole.toString();
55
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./amounts.js";
2
+ export * from "./payloads.js";
3
+ export * from "./rpc.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./amounts.js";
2
+ export * from "./payloads.js";
3
+ export * from "./rpc.js";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Owner-method payload builders.
3
+ *
4
+ * Owner methods are NEVER signed by this server. It builds an unsigned payload
5
+ * and hands it back for the human to approve in their own wallet, so the MCP
6
+ * server is not in the trust path for anything that changes policy. The only
7
+ * thing it signs is `request_payment`, with an agent key that is
8
+ * cryptographically restricted to exactly that method on exactly this vault.
9
+ *
10
+ * Validation is duplicated here on purpose. The contract rejects all of these
11
+ * too, but failing locally costs no gas and no wallet round-trip, and the
12
+ * message can explain *why* a bound exists.
13
+ */
14
+ export declare const ONE_YOCTO = "1";
15
+ /** Mirrors `AGENT_ACCOUNT_STORAGE` in the contract: 0.05 NEAR. */
16
+ export declare const AGENT_ACCOUNT_STORAGE_YOCTO: string;
17
+ /** Mirrors `MIN_AGENT_KEY_ALLOWANCE` in the contract: 0.5 NEAR. */
18
+ export declare const MIN_AGENT_KEY_ALLOWANCE_YOCTO: string;
19
+ /**
20
+ * Gas attached to a payment, for every route.
21
+ *
22
+ * Clears the contract's only floor — MIN_GAS_FOR_DISPATCH, 50 Tgas — with ~10x
23
+ * headroom over the ~9.3 Tgas a MAX_DEPTH payment actually burns. That floor is
24
+ * mirrored in shared/contract-constants.json and asserted on both sides, so a
25
+ * change to the contract's requirement cannot silently leave this too low.
26
+ */
27
+ export declare const DEFAULT_GAS_TGAS = 100;
28
+ export declare const MAX_TOKENS = 4;
29
+ export declare const MAX_RECIPIENTS = 32;
30
+ export declare const MAX_LABEL_LEN = 48;
31
+ export declare const MAX_SUB_LABEL_LEN = 32;
32
+ export declare const MAX_COOLDOWN_SEC: number;
33
+ export declare const MAX_PURGE_LIMIT = 100;
34
+ export interface HumanTokenLimit {
35
+ token: string;
36
+ dailyCap: string;
37
+ perTxCap: string;
38
+ decimals: number;
39
+ }
40
+ export interface OwnerPayload {
41
+ receiverId: string;
42
+ methodName: string;
43
+ args: Record<string, unknown>;
44
+ gasTgas: number;
45
+ depositYocto: string;
46
+ /** What the human is being asked to approve, in human units. */
47
+ summary: string;
48
+ /** Non-empty only when the action is destructive or hard to undo. */
49
+ warnings: string[];
50
+ }
51
+ export declare function planAddNode(vault: string, parent: number | null, label: string, limits: HumanTokenLimit[], cooldownSec: number, recipients: string[] | null): OwnerPayload;
52
+ export declare function planSetNodeCaps(vault: string, nodeId: number, limits: HumanTokenLimit[]): OwnerPayload;
53
+ export declare function planBindAgent(vault: string, nodeId: number, accountId: string): OwnerPayload;
54
+ export declare function planCreateAgentAccount(vault: string, nodeId: number, subLabel: string, publicKey: string, keyAllowanceNear: string): OwnerPayload;
55
+ export declare function planSetRecipients(vault: string, nodeId: number, recipients: string[] | null): OwnerPayload;
56
+ export declare function planSetCooldown(vault: string, nodeId: number, cooldownSec: number): OwnerPayload;
57
+ export declare function planFreezeNode(vault: string, nodeId: number, frozen: boolean): OwnerPayload;
58
+ export declare function planSetPaused(vault: string, paused: boolean): OwnerPayload;
59
+ export declare function planSetStrictAllocation(vault: string, strict: boolean): OwnerPayload;
60
+ export declare function planPurgeSettled(vault: string, beforeDay: number, limit: number): OwnerPayload;
61
+ /** Registry used by the MCP server, and asserted against the contract's method list. */
62
+ export declare const OWNER_PLANNERS: {
63
+ readonly add_node: {
64
+ readonly methodName: "add_node";
65
+ readonly fn: typeof planAddNode;
66
+ };
67
+ readonly set_node_caps: {
68
+ readonly methodName: "set_node_caps";
69
+ readonly fn: typeof planSetNodeCaps;
70
+ };
71
+ readonly bind_agent: {
72
+ readonly methodName: "bind_agent";
73
+ readonly fn: typeof planBindAgent;
74
+ };
75
+ readonly create_agent_account: {
76
+ readonly methodName: "create_agent_account";
77
+ readonly fn: typeof planCreateAgentAccount;
78
+ };
79
+ readonly set_recipients: {
80
+ readonly methodName: "set_recipients";
81
+ readonly fn: typeof planSetRecipients;
82
+ };
83
+ readonly set_cooldown: {
84
+ readonly methodName: "set_cooldown";
85
+ readonly fn: typeof planSetCooldown;
86
+ };
87
+ readonly freeze_node: {
88
+ readonly methodName: "freeze_node";
89
+ readonly fn: typeof planFreezeNode;
90
+ };
91
+ readonly set_paused: {
92
+ readonly methodName: "set_paused";
93
+ readonly fn: typeof planSetPaused;
94
+ };
95
+ readonly set_strict_allocation: {
96
+ readonly methodName: "set_strict_allocation";
97
+ readonly fn: typeof planSetStrictAllocation;
98
+ };
99
+ readonly purge_settled: {
100
+ readonly methodName: "purge_settled";
101
+ readonly fn: typeof planPurgeSettled;
102
+ };
103
+ };
104
+ /** A ready-to-run command, so approving does not require trusting this server. */
105
+ export declare function toCliCommand(p: OwnerPayload, network?: string): string;
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Owner-method payload builders.
3
+ *
4
+ * Owner methods are NEVER signed by this server. It builds an unsigned payload
5
+ * and hands it back for the human to approve in their own wallet, so the MCP
6
+ * server is not in the trust path for anything that changes policy. The only
7
+ * thing it signs is `request_payment`, with an agent key that is
8
+ * cryptographically restricted to exactly that method on exactly this vault.
9
+ *
10
+ * Validation is duplicated here on purpose. The contract rejects all of these
11
+ * too, but failing locally costs no gas and no wallet round-trip, and the
12
+ * message can explain *why* a bound exists.
13
+ */
14
+ import { toBaseUnits, fromBaseUnits } from "./amounts.js";
15
+ export const ONE_YOCTO = "1";
16
+ /** Mirrors `AGENT_ACCOUNT_STORAGE` in the contract: 0.05 NEAR. */
17
+ export const AGENT_ACCOUNT_STORAGE_YOCTO = (5n * 10n ** 22n).toString();
18
+ /** Mirrors `MIN_AGENT_KEY_ALLOWANCE` in the contract: 0.5 NEAR. */
19
+ export const MIN_AGENT_KEY_ALLOWANCE_YOCTO = (5n * 10n ** 23n).toString();
20
+ /**
21
+ * Gas attached to a payment, for every route.
22
+ *
23
+ * Clears the contract's only floor — MIN_GAS_FOR_DISPATCH, 50 Tgas — with ~10x
24
+ * headroom over the ~9.3 Tgas a MAX_DEPTH payment actually burns. That floor is
25
+ * mirrored in shared/contract-constants.json and asserted on both sides, so a
26
+ * change to the contract's requirement cannot silently leave this too low.
27
+ */
28
+ export const DEFAULT_GAS_TGAS = 100;
29
+ // Mirrors of the contract's bounds. Duplicated so a bad call fails here rather
30
+ // than costing gas — and pinned against shared/contract-constants.json by
31
+ // constants.test.ts, because a silent drift would turn this validation into a
32
+ // lie that the chain then rejects.
33
+ export const MAX_TOKENS = 4;
34
+ export const MAX_RECIPIENTS = 32;
35
+ export const MAX_LABEL_LEN = 48;
36
+ export const MAX_SUB_LABEL_LEN = 32;
37
+ export const MAX_COOLDOWN_SEC = 7 * 24 * 60 * 60;
38
+ export const MAX_PURGE_LIMIT = 100;
39
+ const NEAR_DECIMALS = 24;
40
+ function limitsToArgs(limits) {
41
+ if (limits.length === 0)
42
+ throw new Error("at least one token limit is required");
43
+ if (limits.length > MAX_TOKENS) {
44
+ throw new Error(`at most ${MAX_TOKENS} token limits per node (MAX_TOKENS)`);
45
+ }
46
+ const seen = new Set();
47
+ for (const l of limits) {
48
+ if (seen.has(l.token))
49
+ throw new Error(`duplicate token ${l.token}`);
50
+ seen.add(l.token);
51
+ }
52
+ return limits.map((l) => {
53
+ const daily = toBaseUnits(l.dailyCap, l.decimals);
54
+ const perTx = toBaseUnits(l.perTxCap, l.decimals);
55
+ if (daily === 0n || perTx === 0n) {
56
+ throw new Error(`caps must be positive (token ${l.token})`);
57
+ }
58
+ if (perTx > daily) {
59
+ throw new Error(`per-tx cap ${l.perTxCap} exceeds the daily cap ${l.dailyCap} for ${l.token}; ` +
60
+ `the contract requires per_tx_cap <= daily_cap`);
61
+ }
62
+ // snake_case and U128-as-string: the contract's ABI, and a JSON number
63
+ // would lose precision above 2^53.
64
+ return { token: l.token, daily_cap: daily.toString(), per_tx_cap: perTx.toString() };
65
+ });
66
+ }
67
+ /** Human units in the summary, always. Base units here are how a 1000x slip gets approved. */
68
+ function describeLimits(limits) {
69
+ return limits
70
+ .map((l) => `${l.token}: ${l.dailyCap}/day, max ${l.perTxCap}/payment`)
71
+ .join("; ");
72
+ }
73
+ const SUB_LABEL = new RegExp(`^[a-z0-9_-]{1,${MAX_SUB_LABEL_LEN}}$`);
74
+ function assertRecipients(recipients) {
75
+ if (recipients === null)
76
+ return;
77
+ if (recipients.length === 0) {
78
+ throw new Error("recipients must be a non-empty list, or null to inherit the parent's");
79
+ }
80
+ if (recipients.length > MAX_RECIPIENTS) {
81
+ throw new Error(`at most ${MAX_RECIPIENTS} recipients per node (MAX_RECIPIENTS)`);
82
+ }
83
+ if (new Set(recipients).size !== recipients.length) {
84
+ throw new Error("duplicate recipient");
85
+ }
86
+ }
87
+ function assertCooldown(cooldownSec) {
88
+ if (!Number.isInteger(cooldownSec) || cooldownSec < 0) {
89
+ throw new Error("cooldownSec must be a non-negative integer");
90
+ }
91
+ if (cooldownSec > MAX_COOLDOWN_SEC) {
92
+ throw new Error(`cooldown must be at most ${MAX_COOLDOWN_SEC}s (MAX_COOLDOWN_SEC)`);
93
+ }
94
+ }
95
+ export function planAddNode(vault, parent, label, limits, cooldownSec, recipients) {
96
+ if (!label || label.length > MAX_LABEL_LEN) {
97
+ throw new Error(`label must be 1..=${MAX_LABEL_LEN} characters (MAX_LABEL_LEN)`);
98
+ }
99
+ assertCooldown(cooldownSec);
100
+ assertRecipients(recipients);
101
+ return {
102
+ receiverId: vault,
103
+ methodName: "add_node",
104
+ args: {
105
+ parent,
106
+ label,
107
+ limits: limitsToArgs(limits),
108
+ cooldown_sec: cooldownSec,
109
+ recipients,
110
+ },
111
+ gasTgas: DEFAULT_GAS_TGAS,
112
+ depositYocto: ONE_YOCTO,
113
+ summary: `Create budget node "${label}" ${parent === null ? "as the ROOT" : `under node ${parent}`}` +
114
+ ` with ${describeLimits(limits)}` +
115
+ (cooldownSec ? `, ${cooldownSec}s cooldown between payments` : "") +
116
+ (recipients ? `, paying only: ${recipients.join(", ")}` : `, inheriting the payee allowlist`),
117
+ warnings: [],
118
+ };
119
+ }
120
+ export function planSetNodeCaps(vault, nodeId, limits) {
121
+ return {
122
+ receiverId: vault,
123
+ methodName: "set_node_caps",
124
+ args: { node_id: nodeId, limits: limitsToArgs(limits) },
125
+ gasTgas: DEFAULT_GAS_TGAS,
126
+ depositYocto: ONE_YOCTO,
127
+ summary: `Set node ${nodeId} caps to ${describeLimits(limits)}`,
128
+ warnings: [],
129
+ };
130
+ }
131
+ export function planBindAgent(vault, nodeId, accountId) {
132
+ return {
133
+ receiverId: vault,
134
+ methodName: "bind_agent",
135
+ args: { node_id: nodeId, account_id: accountId },
136
+ gasTgas: DEFAULT_GAS_TGAS,
137
+ depositYocto: ONE_YOCTO,
138
+ summary: `Let ${accountId} spend from node ${nodeId}`,
139
+ warnings: [
140
+ `${accountId} is an EXISTING account whose keys you do not control. ` +
141
+ `Prefer create_agent_account, which issues a key restricted to request_payment.`,
142
+ ],
143
+ };
144
+ }
145
+ export function planCreateAgentAccount(vault, nodeId, subLabel, publicKey, keyAllowanceNear) {
146
+ if (!SUB_LABEL.test(subLabel)) {
147
+ throw new Error(`invalid sub_label ${JSON.stringify(subLabel)}: 1..=32 chars of [a-z0-9_-]. ` +
148
+ `A dot in particular is refused — the account id is DERIVED as ` +
149
+ `<label>.${vault} so the new account cannot be aimed outside this vault's subtree.`);
150
+ }
151
+ if (!publicKey.startsWith("ed25519:") && !publicKey.startsWith("secp256k1:")) {
152
+ throw new Error("publicKey must be in NEAR form, e.g. ed25519:...");
153
+ }
154
+ const allowance = toBaseUnits(keyAllowanceNear, NEAR_DECIMALS);
155
+ if (allowance < BigInt(MIN_AGENT_KEY_ALLOWANCE_YOCTO)) {
156
+ throw new Error(`key allowance ${keyAllowanceNear} NEAR is below the usable floor of ` +
157
+ `${fromBaseUnits(BigInt(MIN_AGENT_KEY_ALLOWANCE_YOCTO), NEAR_DECIMALS)} NEAR. ` +
158
+ `NEAR reserves the FULL prepaid gas of each call against the key up front and ` +
159
+ `refunds the remainder, so an allowance sized to expected burn produces a key ` +
160
+ `that cannot transact at all.`);
161
+ }
162
+ const deposit = allowance + BigInt(AGENT_ACCOUNT_STORAGE_YOCTO);
163
+ const derived = `${subLabel}.${vault}`;
164
+ return {
165
+ receiverId: vault,
166
+ methodName: "create_agent_account",
167
+ args: {
168
+ node_id: nodeId,
169
+ sub_label: subLabel,
170
+ public_key: publicKey,
171
+ key_allowance: allowance.toString(),
172
+ },
173
+ gasTgas: DEFAULT_GAS_TGAS,
174
+ depositYocto: deposit.toString(),
175
+ summary: `Create agent account ${derived}, bind it to node ${nodeId}, and give it ONE key ` +
176
+ `that can call only request_payment on ${vault} — it cannot call other methods, ` +
177
+ `cannot call other contracts, and cannot attach a deposit. ` +
178
+ `Funding ${fromBaseUnits(deposit, NEAR_DECIMALS)} NEAR ` +
179
+ `(${keyAllowanceNear} gas allowance + ` +
180
+ `${fromBaseUnits(BigInt(AGENT_ACCOUNT_STORAGE_YOCTO), NEAR_DECIMALS)} account storage).`,
181
+ warnings: [],
182
+ };
183
+ }
184
+ export function planSetRecipients(vault, nodeId, recipients) {
185
+ assertRecipients(recipients);
186
+ return {
187
+ receiverId: vault,
188
+ methodName: "set_recipients",
189
+ args: { node_id: nodeId, recipients },
190
+ gasTgas: DEFAULT_GAS_TGAS,
191
+ depositYocto: ONE_YOCTO,
192
+ summary: recipients
193
+ ? `Node ${nodeId} may pay only: ${recipients.join(", ")}`
194
+ : `Node ${nodeId} inherits its payee allowlist from the nearest ancestor that sets one`,
195
+ warnings: recipients
196
+ ? [
197
+ `This REPLACES the inherited allowlist for node ${nodeId} and its descendants; ` +
198
+ `it does not add to it. Payees it currently inherits will stop being payable.`,
199
+ ]
200
+ : [],
201
+ };
202
+ }
203
+ export function planSetCooldown(vault, nodeId, cooldownSec) {
204
+ assertCooldown(cooldownSec);
205
+ return {
206
+ receiverId: vault,
207
+ methodName: "set_cooldown",
208
+ args: { node_id: nodeId, cooldown_sec: cooldownSec },
209
+ gasTgas: DEFAULT_GAS_TGAS,
210
+ depositYocto: ONE_YOCTO,
211
+ summary: cooldownSec
212
+ ? `Node ${nodeId} must wait ${cooldownSec}s between payments`
213
+ : `Remove the cooldown on node ${nodeId}`,
214
+ warnings: [],
215
+ };
216
+ }
217
+ export function planFreezeNode(vault, nodeId, frozen) {
218
+ return {
219
+ receiverId: vault,
220
+ methodName: "freeze_node",
221
+ args: { node_id: nodeId, frozen },
222
+ gasTgas: DEFAULT_GAS_TGAS,
223
+ depositYocto: ONE_YOCTO,
224
+ summary: frozen ? `Freeze node ${nodeId}` : `Unfreeze node ${nodeId}`,
225
+ warnings: frozen
226
+ ? [`This blocks payments from node ${nodeId} AND every node in its subtree.`]
227
+ : [],
228
+ };
229
+ }
230
+ export function planSetPaused(vault, paused) {
231
+ return {
232
+ receiverId: vault,
233
+ methodName: "set_paused",
234
+ args: { paused },
235
+ gasTgas: DEFAULT_GAS_TGAS,
236
+ depositYocto: ONE_YOCTO,
237
+ summary: paused ? `PAUSE the whole vault` : `Resume the vault`,
238
+ warnings: paused ? [`This will halt every agent payment until you resume it.`] : [],
239
+ };
240
+ }
241
+ export function planSetStrictAllocation(vault, strict) {
242
+ return {
243
+ receiverId: vault,
244
+ methodName: "set_strict_allocation",
245
+ args: { strict },
246
+ gasTgas: DEFAULT_GAS_TGAS,
247
+ depositYocto: ONE_YOCTO,
248
+ summary: strict
249
+ ? `Require the sum of child caps to fit inside each parent cap (no overcommit)`
250
+ : `Allow overcommit: child caps may sum above the parent, and children then ` +
251
+ `contend first-come-first-served for the shared parent pool`,
252
+ warnings: [],
253
+ };
254
+ }
255
+ export function planPurgeSettled(vault, beforeDay, limit) {
256
+ if (!Number.isInteger(limit) || limit < 0 || limit > MAX_PURGE_LIMIT) {
257
+ throw new Error(`limit must be an integer in 0..=${MAX_PURGE_LIMIT} (MAX_PURGE_LIMIT)`);
258
+ }
259
+ return {
260
+ receiverId: vault,
261
+ methodName: "purge_settled",
262
+ args: { before_day: beforeDay, limit },
263
+ gasTgas: DEFAULT_GAS_TGAS,
264
+ depositYocto: ONE_YOCTO,
265
+ summary: `Delete up to ${limit} settled or failed payment records reserved before UTC day ${beforeDay}`,
266
+ warnings: [
267
+ `Irreversible: this deletes payment HISTORY. Spend totals are NOT refunded, ` +
268
+ `but a deleted request id becomes reusable, so a client retrying an id older ` +
269
+ `than day ${beforeDay} would be paid a second time.`,
270
+ ],
271
+ };
272
+ }
273
+ /** Registry used by the MCP server, and asserted against the contract's method list. */
274
+ export const OWNER_PLANNERS = {
275
+ add_node: { methodName: "add_node", fn: planAddNode },
276
+ set_node_caps: { methodName: "set_node_caps", fn: planSetNodeCaps },
277
+ bind_agent: { methodName: "bind_agent", fn: planBindAgent },
278
+ create_agent_account: { methodName: "create_agent_account", fn: planCreateAgentAccount },
279
+ set_recipients: { methodName: "set_recipients", fn: planSetRecipients },
280
+ set_cooldown: { methodName: "set_cooldown", fn: planSetCooldown },
281
+ freeze_node: { methodName: "freeze_node", fn: planFreezeNode },
282
+ set_paused: { methodName: "set_paused", fn: planSetPaused },
283
+ set_strict_allocation: { methodName: "set_strict_allocation", fn: planSetStrictAllocation },
284
+ purge_settled: { methodName: "purge_settled", fn: planPurgeSettled },
285
+ };
286
+ /** A ready-to-run command, so approving does not require trusting this server. */
287
+ export function toCliCommand(p, network = "testnet") {
288
+ return (`near contract call-function as-transaction ${p.receiverId} ${p.methodName} ` +
289
+ `json-args '${JSON.stringify(p.args)}' prepaid-gas '${p.gasTgas}.0 Tgas' ` +
290
+ `attached-deposit '${p.depositYocto} yoctoNEAR' ` +
291
+ `sign-as <YOUR-OWNER-ACCOUNT> network-config ${network} sign-with-keychain send`);
292
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Getting a cross-chain quote, and signing the binding the vault verifies.
3
+ *
4
+ * A cross-chain payment is an ordinary NEP-141 transfer on NEAR to a ONE-TIME
5
+ * deposit address; a NEAR Intents solver delivers the destination asset and
6
+ * prices the far chain's gas into the quote. No MPC signature, no derived
7
+ * address, no ETH to keep topped up.
8
+ *
9
+ * The catch is that the deposit address is unpredictable and single-use, so it
10
+ * can never be on the vault's payee allowlist. The vault checks the allowlist
11
+ * against the final payee and then sends the money somewhere it cannot connect
12
+ * back to that payee. If an agent could choose that address, the allowlist
13
+ * would be decorative — quote a swap paying yourself and the vault funds it
14
+ * with every other check passing.
15
+ *
16
+ * So a key the vault owner registered signs the binding between payee and
17
+ * deposit address, and the contract refuses any Intents payment without it.
18
+ * This module is the holder of that key. It is a TRUSTED PRODUCER: whoever
19
+ * runs it can redirect any Intents payment the budget allows, which is why it
20
+ * belongs on the operator's machine and not on the ones agents run on.
21
+ */
22
+ export declare const ONE_CLICK_URL: string;
23
+ /** Mainnet asset ids, from GET /v0/tokens. Intents has no testnet. */
24
+ export declare const ASSETS: {
25
+ readonly usdcNear: "nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1";
26
+ readonly usdcBase: "nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near";
27
+ };
28
+ export interface QuoteBinding {
29
+ vault: string;
30
+ requestId: string;
31
+ token: string;
32
+ amount: bigint;
33
+ payee: string;
34
+ depositAddress: string;
35
+ deadlineNs: bigint;
36
+ }
37
+ /** Field order is the struct's declaration order; borsh has no field names. */
38
+ export declare function encodeBinding(b: QuoteBinding): Uint8Array;
39
+ export declare function signBinding(b: QuoteBinding, secretKey: Uint8Array): Uint8Array;
40
+ export declare function publicKeyOf(secretKey: Uint8Array): Uint8Array;
41
+ export declare const toBase64: (b: Uint8Array) => string;
42
+ export interface QuoteResult {
43
+ /** Where the vault must send the NEAR-side tokens. */
44
+ depositAddress: string;
45
+ /** What leaves the vault, in base units. */
46
+ amountIn: bigint;
47
+ /** What the payee receives on the destination chain, in base units. */
48
+ amountOut: bigint;
49
+ deadlineNs: bigint;
50
+ timeEstimateSec: number;
51
+ }
52
+ /**
53
+ * Ask 1Click what it costs to deliver `amountOut` to `payee` on the
54
+ * destination chain.
55
+ *
56
+ * EXACT_OUTPUT, deliberately. A payment is a promise about what the PAYEE
57
+ * receives; EXACT_INPUT would deliver "about a dollar" and leave the agent
58
+ * unable to settle an invoice. The vault is then charged `amountIn`, which is
59
+ * what actually leaves it — so the budget governs real outflow, not the
60
+ * nominal figure.
61
+ */
62
+ export declare function getQuote(opts: {
63
+ originAsset?: string;
64
+ destinationAsset?: string;
65
+ amountOut: bigint;
66
+ payee: string;
67
+ refundTo: string;
68
+ deadline: Date;
69
+ slippageBps?: number;
70
+ jwt?: string;
71
+ dry?: boolean;
72
+ }): Promise<QuoteResult>;
73
+ /** The `route` argument for a cross-chain payment. */
74
+ export declare function intentsRoute(depositAddress: string, deadlineNs: bigint, signature: Uint8Array): {
75
+ intents: {
76
+ deposit_address: string;
77
+ quote_deadline_ns: string;
78
+ signature: string;
79
+ };
80
+ };