@floatonnear/float-adapter 0.2.1 → 0.3.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,112 @@
1
+ import { planAddRole, planFreezeNode, planPurgeSettled, planSetCooldown, planSetNodeCaps, planSetPaused, planSetRecipients, planSetStrictAllocation, planWithdraw, } from "@floatonnear/mcp/payloads";
2
+ /**
3
+ * The action name is not one we serve.
4
+ *
5
+ * A SEPARATE type from a bad argument, because the HTTP layer has to tell
6
+ * them apart: it used to catch every throw from here and report
7
+ * `unknown action: <name>`, so a negative cooldown or a duplicate recipient
8
+ * was reported as a method that does not exist - the one explanation that
9
+ * cannot lead the caller to a fix.
10
+ */
11
+ export class UnknownActionError extends Error {
12
+ constructor(action) {
13
+ super(`unknown action: ${action}`);
14
+ this.name = "UnknownActionError";
15
+ }
16
+ }
17
+ /**
18
+ * A node-scoped action must name its node.
19
+ *
20
+ * THE TYPE SAYS `nodeId: number` AND THAT PROVES NOTHING AT RUNTIME. The body
21
+ * arrives as JSON and is cast, so a request that simply omits the field type-
22
+ * checks all the way through. Measured live on 2026-11-09:
23
+ * `{"action":"freeze"}` returned HTTP 200 with `args: {"frozen": false}` and
24
+ * the summary "Unfreeze node undefined" — `JSON.stringify` drops an undefined
25
+ * value, so `node_id` vanished from the args and from the CLI string, and the
26
+ * user was handed a confident command for a node that does not exist.
27
+ *
28
+ * It reverts rather than doing damage: `freeze_node(node_id: NodeId, ..)` uses
29
+ * `NodeId = u32` with no `#[serde(default)]`, so near-sdk fails to deserialize
30
+ * instead of falling back to `ROOT_NODE = 0`. Wasted gas and a confusing
31
+ * failure, not a silently frozen root.
32
+ *
33
+ * `!== "number"`, NOT `!b.nodeId`. Root is node 0 and 0 is falsy, so the
34
+ * obvious guard would refuse the node most likely to be frozen or re-capped.
35
+ * `NaN` is excluded too, because `JSON.parse` cannot produce it but a hand-
36
+ * built caller can, and `node_id: NaN` serialises to `null`.
37
+ */
38
+ function requireNode(b) {
39
+ if (typeof b.nodeId !== "number" || !Number.isInteger(b.nodeId) || b.nodeId < 0) {
40
+ throw new Error(`action '${b.action}' needs nodeId: a non-negative integer naming the node to change. ` +
41
+ `Root is nodeId 0. Omitting it would build a command with no node_id at all, ` +
42
+ `which the contract refuses after the owner has already signed it.`);
43
+ }
44
+ return b.nodeId;
45
+ }
46
+ export function planOwnerAction(b, env) {
47
+ switch (b.action) {
48
+ case "caps":
49
+ // HUMAN units, with decimals — `planSetNodeCaps` scales them itself via
50
+ // limitsToArgs. Calling toBaseUnits here too would scale twice and turn
51
+ // a 100 USDC cap into 100 million USDC, which is not a cap at all.
52
+ return planSetNodeCaps(env.vault, requireNode(b), [
53
+ {
54
+ token: env.token,
55
+ dailyCap: b.dailyCap ?? "0",
56
+ perTxCap: b.perTxCap ?? "0",
57
+ decimals: env.decimals,
58
+ },
59
+ ]);
60
+ case "cooldown":
61
+ return planSetCooldown(env.vault, requireNode(b), b.cooldownSec ?? 0);
62
+ case "recipients":
63
+ // `?? null` is load-bearing: an absent list means INHERIT. Defaulting to
64
+ // `[]` would panic `validate_recipients`, which demands "1..=32
65
+ // recipients, or None to inherit".
66
+ return planSetRecipients(env.vault, requireNode(b), b.recipients ?? null);
67
+ case "freeze":
68
+ return planFreezeNode(env.vault, requireNode(b), b.frozen ?? false);
69
+ case "pause":
70
+ // Vault-wide. Takes no nodeId, because it stops every payment at every
71
+ // level rather than muting one agent.
72
+ return planSetPaused(env.vault, b.paused ?? false);
73
+ case "allocation":
74
+ return planSetStrictAllocation(env.vault, b.strict ?? false);
75
+ case "withdraw":
76
+ // BASE UNITS, and no default. An amount is the one argument that must
77
+ // never be guessed: `?? "0"` would be refused by the contract, and any
78
+ // other fallback would move a quantity nobody asked for.
79
+ if (!b.amountBase)
80
+ throw new Error("withdraw needs amountBase, in base units");
81
+ if (!b.receiverId)
82
+ throw new Error("withdraw needs receiverId");
83
+ return planWithdraw(env.vault, b.token ?? env.token, b.amountBase, b.receiverId, env.decimals);
84
+ case "role":
85
+ // NOTHING IS DEFAULTED that the user has to confirm. A label, a key
86
+ // and a funding amount are all things an owner approves in a wallet,
87
+ // and a guessed one would be approved by someone who never chose it.
88
+ if (!b.label)
89
+ throw new Error("role needs a label");
90
+ if (!b.publicKey)
91
+ throw new Error("role needs the agent's publicKey");
92
+ if (!b.fundingYocto)
93
+ throw new Error("role needs fundingYocto for the gas key");
94
+ return planAddRole(env.vault, b.label, [
95
+ {
96
+ token: env.token,
97
+ dailyCap: b.dailyCap ?? "0",
98
+ perTxCap: b.perTxCap ?? "0",
99
+ decimals: env.decimals,
100
+ },
101
+ ], b.cooldownSec ?? 0, b.recipients ?? null, b.publicKey, b.fundingYocto);
102
+ case "purge":
103
+ // `before_day` is read from the contract's own `current_day` by the
104
+ // caller and passed as current_day + 1 — records are removed strictly
105
+ // BEFORE it. Defaulting it here would invent a cutoff.
106
+ if (typeof b.beforeDay !== "number")
107
+ throw new Error("purge needs beforeDay");
108
+ return planPurgeSettled(env.vault, b.beforeDay, b.limit ?? 100);
109
+ default:
110
+ throw new UnknownActionError(String(b.action));
111
+ }
112
+ }
package/dist/server.d.ts CHANGED
@@ -12,8 +12,17 @@
12
12
  * NEAR_VAULT_RPC_URL default https://test.rpc.fastnear.com
13
13
  * NEAR_VAULT_TOKEN required (the NEP-141 the vault pays in)
14
14
  * NEAR_VAULT_TOKEN_DECIMALS default 6
15
- * NEAR_VAULT_AGENT_ID only needed for POST /v1/pay
15
+ * NEAR_VAULT_AGENT_ID only needed for POST /v1/pay. Taken from the key
16
+ * file's `account_id` when one is given.
16
17
  * NEAR_VAULT_AGENT_KEY only needed for POST /v1/pay (the restricted key)
18
+ * NEAR_VAULT_AGENT_KEY_FILE PREFERRED over the inline value above: a PATH to
19
+ * a ~/.near-credentials-shaped JSON file or a bare
20
+ * key file. See key-file.ts — a path is not a
21
+ * secret, so reading this config to check which
22
+ * vault is wired up stops printing a live private
23
+ * key. If both are set the FILE wins.
24
+ * FLOAT_QUOTE_SIGNER_KEY 32-byte hex, for rail=intents
25
+ * FLOAT_QUOTE_SIGNER_KEY_FILE the same, as a path
17
26
  * PORT default 8787 (Float's dashboard default)
18
27
  */
19
28
  export {};