@floatonnear/mcp 0.2.0 → 0.4.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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The node a creation-time agent is bound to.
3
+ *
4
+ * ROOT_NODE in the vault. The factory binds at the root because it is the only
5
+ * node that exists at `new` — children are added later by the owner.
6
+ */
7
+ export declare const AGENT_NODE_ID = 0;
8
+ export interface GeneratedAgentKey {
9
+ publicKey: string;
10
+ /** The role this credential was filed under, when one was given. */
11
+ role?: string;
12
+ /** Where the secret was written. The secret itself is deliberately absent. */
13
+ keyringPath: string;
14
+ envPath: string;
15
+ /** A prior keyring that was moved aside, if any. */
16
+ backedUpTo?: string;
17
+ }
18
+ /**
19
+ * Write the agent's credential where the adapter will actually find it.
20
+ *
21
+ * THE SHAPE MATTERS MORE THAN THE PATH. `loadKeyring` in
22
+ * packages/float-adapter/src/keyring.ts accepts an entry only when it has a
23
+ * string `agentId` and a string `secret`:
24
+ *
25
+ * if (e && typeof e.agentId === "string" && typeof e.secret === "string")
26
+ *
27
+ * Anything else is skipped in silence — no warning, no error, the file simply
28
+ * contributes nothing. `float-agent.mjs` writes
29
+ * `{kind, vault, node, publicKey, privateKey}` into this same file, which that
30
+ * condition rejects on both counts. So the convenient-looking shape is the
31
+ * dead one, and this writes the shape that loads.
32
+ *
33
+ * `agentId` is the VAULT, not a separate account. A virtual agent has no
34
+ * account of its own: its key is a NEP-611 gas key installed ON the vault, so
35
+ * the account it signs as is the vault itself — which is also why
36
+ * `NEAR_VAULT_AGENT_ID` is set to a vault id in `.mcp.json`.
37
+ */
38
+ export declare function generateAgentKey(opts: {
39
+ vault: string;
40
+ dir?: string;
41
+ /**
42
+ * The node this agent spends from.
43
+ *
44
+ * `null` means NOT YET KNOWN, and is written as an absent field rather
45
+ * than a guess. `add_role` creates the node inside the contract, so its id
46
+ * does not exist until the transaction lands — and a client cannot read
47
+ * one action's return value from the same transaction.
48
+ *
49
+ * A WRONG NODE ID IS WORSE THAN NONE. Defaulting to 0 filed the first
50
+ * `add_role` agent against the ROOT node while the contract had assigned
51
+ * it node 1: `byNode.get(1)` missed it entirely, and `byNode.get(0)`
52
+ * returned it under the root's name. Selection by role still worked, so
53
+ * nothing failed loudly. `loadKeyring` treats a missing `nodeId` as
54
+ * "selectable by role only", which is exactly the truth here.
55
+ *
56
+ * Omitted entirely still means ROOT, which is correct for the agent bound
57
+ * at vault creation — that one genuinely is node 0.
58
+ */
59
+ nodeId?: number | null;
60
+ /**
61
+ * A human name for this agent — "marketing", "news".
62
+ *
63
+ * THE ONLY UNIQUE IDENTIFIER once a vault has several agents. `agentId` is
64
+ * the account the key signs as, which for every virtual agent on one vault
65
+ * is the SAME vault, so a keyring indexed by it collapses them and the
66
+ * last one written silently wins.
67
+ */
68
+ role?: string;
69
+ }): GeneratedAgentKey;
70
+ /** Where the files would go, without creating anything. */
71
+ export declare function agentKeyPaths(dir?: string): {
72
+ keyringPath: string;
73
+ envPath: string;
74
+ dir: string;
75
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Generating and storing the agent's key for one-signature onboarding.
3
+ *
4
+ * This is the ONE place `vault_create` stops being keyless, and the departure
5
+ * is deliberate but narrow: the key generated here is the AGENT's, never the
6
+ * owner's. An agent key can call exactly `request_payment` on one vault, is
7
+ * bounded by the budget tree, and cannot attach a deposit — so the worst a
8
+ * leaked copy does is spend inside limits the owner already set. The owner's
9
+ * full-access key still never comes near this server.
10
+ *
11
+ * THE SECRET NEVER GOES TO THE CALLER. It is written to disk and only the
12
+ * public half is returned. That is not a style preference: this session's own
13
+ * transcript acquired one private key from a screenshot and another from a
14
+ * config dump, and a tool response is a transcript entry forever. The agent
15
+ * calling `vault_create` has filesystem access to the same machine, so a path
16
+ * is strictly more useful to it than a string anyway.
17
+ */
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, chmodSync } from "node:fs";
19
+ import { dirname, join } from "node:path";
20
+ import { KeyPair } from "near-api-js";
21
+ /**
22
+ * The node a creation-time agent is bound to.
23
+ *
24
+ * ROOT_NODE in the vault. The factory binds at the root because it is the only
25
+ * node that exists at `new` — children are added later by the owner.
26
+ */
27
+ export const AGENT_NODE_ID = 0;
28
+ /**
29
+ * Write the agent's credential where the adapter will actually find it.
30
+ *
31
+ * THE SHAPE MATTERS MORE THAN THE PATH. `loadKeyring` in
32
+ * packages/float-adapter/src/keyring.ts accepts an entry only when it has a
33
+ * string `agentId` and a string `secret`:
34
+ *
35
+ * if (e && typeof e.agentId === "string" && typeof e.secret === "string")
36
+ *
37
+ * Anything else is skipped in silence — no warning, no error, the file simply
38
+ * contributes nothing. `float-agent.mjs` writes
39
+ * `{kind, vault, node, publicKey, privateKey}` into this same file, which that
40
+ * condition rejects on both counts. So the convenient-looking shape is the
41
+ * dead one, and this writes the shape that loads.
42
+ *
43
+ * `agentId` is the VAULT, not a separate account. A virtual agent has no
44
+ * account of its own: its key is a NEP-611 gas key installed ON the vault, so
45
+ * the account it signs as is the vault itself — which is also why
46
+ * `NEAR_VAULT_AGENT_ID` is set to a vault id in `.mcp.json`.
47
+ */
48
+ export function generateAgentKey(opts) {
49
+ const dir = opts.dir ?? ".float";
50
+ // `undefined` -> root (the creation-time agent). `null` -> unknown, and the
51
+ // field is omitted rather than guessed.
52
+ const nodeId = opts.nodeId === null ? undefined : (opts.nodeId ?? AGENT_NODE_ID);
53
+ const keyringPath = join(dir, "agent-keys.json");
54
+ const envPath = join(dir, "agent.env");
55
+ const kp = KeyPair.fromRandom("ed25519");
56
+ const publicKey = kp.getPublicKey().toString();
57
+ const secret = kp.toString();
58
+ mkdirSync(dir, { recursive: true });
59
+ // APPEND, NEVER REPLACE. A vault may have several agents, and more to the
60
+ // point: overwriting would destroy the credential of an agent that is
61
+ // already bound on chain and working. An on-chain binding cannot be undone
62
+ // by deleting a file, so a lost secret is a permanently stranded agent.
63
+ let existing = [];
64
+ let backedUpTo;
65
+ if (existsSync(keyringPath)) {
66
+ try {
67
+ const parsed = JSON.parse(readFileSync(keyringPath, "utf8"));
68
+ if (Array.isArray(parsed))
69
+ existing = parsed;
70
+ else
71
+ throw new Error("not an array");
72
+ }
73
+ catch {
74
+ // A malformed keyring must not be parsed into oblivion. Move it aside
75
+ // rather than overwrite it — whatever is in there may be the only copy
76
+ // of a working secret, and this is not the moment to decide it is junk.
77
+ backedUpTo = `${keyringPath}.unreadable-${publicKey.slice(8, 16)}`;
78
+ copyFileSync(keyringPath, backedUpTo);
79
+ existing = [];
80
+ }
81
+ }
82
+ existing.push({
83
+ agentId: opts.vault,
84
+ secret,
85
+ publicKey,
86
+ role: opts.role,
87
+ // Only when known. `JSON.stringify` drops an undefined value, so an
88
+ // unknown node leaves no field at all — which is what `loadKeyring`
89
+ // reads as "selectable by role, not by node".
90
+ ...(typeof nodeId === "number" ? { nodeId } : {}),
91
+ });
92
+ writeFileSync(keyringPath, JSON.stringify(existing, null, 2) + "\n");
93
+ // The env form the MCP server itself reads. Written second and separately
94
+ // because it holds ONE agent, while the keyring holds every agent — so the
95
+ // keyring is the durable record and this is a convenience.
96
+ writeFileSync(envPath, [
97
+ `# Written by vault_create. Contains a PRIVATE KEY — do not commit or paste.`,
98
+ `# The agent can only call request_payment on this vault, inside its budget.`,
99
+ `NEAR_VAULT_AGENT_ID=${opts.vault}`,
100
+ `NEAR_VAULT_AGENT_KEY=${secret}`,
101
+ ``,
102
+ ].join("\n"));
103
+ // Best effort. Windows ignores POSIX modes, so this must not be load-bearing
104
+ // — the real protection is that the key is restricted on chain.
105
+ for (const p of [keyringPath, envPath]) {
106
+ try {
107
+ chmodSync(p, 0o600);
108
+ }
109
+ catch {
110
+ /* not POSIX; the on-chain restriction is the actual boundary */
111
+ }
112
+ }
113
+ return { publicKey, role: opts.role, keyringPath, envPath, backedUpTo };
114
+ }
115
+ /** Where the files would go, without creating anything. */
116
+ export function agentKeyPaths(dir = ".float") {
117
+ return { keyringPath: join(dir, "agent-keys.json"), envPath: join(dir, "agent.env"), dir: dirname(join(dir, "x")) };
118
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Onboarding: what a user with NO vault yet needs in order to get one.
3
+ *
4
+ * This is the only part of the server that talks to someone who is not yet a
5
+ * customer, and it holds no key and signs nothing — same trust model as the
6
+ * plan tools. It answers three questions and hands back a URL:
7
+ *
8
+ * where does my vault go the FACTORY's own `vault_id_for` view
9
+ * what must I attach the factory's `required_deposit` + code storage
10
+ * how do I sign it the web page, which the user's wallet approves
11
+ *
12
+ * THE DERIVATION IS NOT REIMPLEMENTED HERE. `vault_id_for` exists on the
13
+ * factory precisely so there is one implementation of it; a local copy is a
14
+ * thing that drifts. The factory's prefix rule has already changed once (labels
15
+ * are now rooted in the caller's prefix after a squatting hole), and a client
16
+ * that had copied the old rule would have gone on confidently printing wrong
17
+ * addresses.
18
+ */
19
+ /**
20
+ * Bytes of the vault contract the factory deploys.
21
+ *
22
+ * 301,033 until `create_agent_account` was removed, which took 7,305 bytes
23
+ * with it — about 0.073 NEAR off every vault's storage deposit. The pin
24
+ * caught the drift the moment the contract shrank, which is the entire
25
+ * reason it exists.
26
+ *
27
+ * Mirrors `packages/create-float/res/vault.wasm`, and `create.pins.test.ts`
28
+ * reads that file and fails if the two disagree — so a contract rebuild cannot
29
+ * silently leave this quoting a deposit that is too small. Too small is not a
30
+ * clean failure: CreateAccount succeeds and the vault sits below its own
31
+ * storage requirement, bricking on the first write.
32
+ */
33
+ export declare const CONTRACT_BYTES = 298819;
34
+ /** NEAR charges 1e19 yoctoNEAR per byte of storage. */
35
+ export declare const STORAGE_COST_PER_BYTE: bigint;
36
+ /**
37
+ * What `web/index.html` adds on top of the code's storage stake.
38
+ *
39
+ * THE FACTORY'S FLOOR IS NOT WHAT THE USER IS ASKED FOR, and conflating the
40
+ * two quoted a deposit 0.26 NEAR too low. `begin()` in
41
+ * contracts/factory/src/lib.rs rejects below `required_deposit + bytes * 1e19`
42
+ * — a real floor, 3.037280 on testnet today. But the page attaches
43
+ * `stake + 0.36` instead, as headroom for the root node's state, and a wallet
44
+ * checks the balance against what the PAGE attaches.
45
+ *
46
+ * Measured 2026-11-03: a vault created through the page,
47
+ * tinyrice9138-testnet.fac.floatv4.testnet, holds exactly 3.297280 — the
48
+ * page's figure to the yoctoNEAR. A user funded to the old 3.237280 quote is
49
+ * rejected by their wallet needing 3.497280, which is precisely the failure
50
+ * GAS_RESERVATION_YOCTO below was written to prevent, arriving through the
51
+ * other term.
52
+ *
53
+ * This is a COPY of a value in a file that cannot import from here — the page
54
+ * is standalone HTML served to a browser with no build step. `create.pins.
55
+ * test.ts` reads that file and fails if the two disagree, the same way
56
+ * CONTRACT_BYTES and CREATE_GAS_TGAS are pinned. A copy nothing checks is a
57
+ * copy that drifts.
58
+ */
59
+ export declare const PAGE_HEADROOM_YOCTO: bigint;
60
+ /**
61
+ * Funding for an agent's gas key, when one is bound at creation.
62
+ *
63
+ * A NEP-611 gas key's BALANCE is its spending cap — `allowance` is null by
64
+ * design — so this is the only thing bounding how much gas the agent burns.
65
+ * 0.12 NEAR ≈ 50 payments at the vault's 50-TGas `MIN_GAS_FOR_DISPATCH`
66
+ * floor, matching `gasKeyFunding({tgas:60, payments:50})` in create-float.
67
+ *
68
+ * Charged ONLY when an agent is bound. The factory raises its own on-chain
69
+ * deposit floor by exactly this amount in that case, so a quote that omits it
70
+ * is refused with DEPOSIT_TOO_SMALL — and a quote that always includes it
71
+ * overcharges everyone who creates a bare vault.
72
+ *
73
+ * A COPY of `AGENT_GAS_KEY_FUNDING` in contracts/factory/src/lib.rs and
74
+ * `AGENT_GAS` in web/index.html. Three files, one number, which is exactly
75
+ * the shape that already shipped a 0.26 NEAR underquote here — so
76
+ * `create.pins.test.ts` reads both of the others and fails if any disagree.
77
+ */
78
+ export declare const AGENT_GAS_KEY_FUNDING_YOCTO: bigint;
79
+ /**
80
+ * Gas the create page attaches. MEASURED commitment is 124.88 TGas:
81
+ * 42.79 to deliver and decode the 401 KB base64 argument (measured on chain),
82
+ * plus CreateAccount 7.70, Transfer 0.23, DeployContract 34.16, and the
83
+ * factory's forwarded 30 + 10. 200 keeps ~60% headroom.
84
+ *
85
+ * Must match `web/index.html`. It is not a free choice — see
86
+ * `GAS_RESERVATION_YOCTO`.
87
+ */
88
+ export declare const CREATE_GAS_TGAS = 200n;
89
+ /**
90
+ * What the user must HOLD on top of the deposit, at signing time.
91
+ *
92
+ * A transaction reserves `prepaid_gas * 1e9` — the MAXIMUM gas price — for the
93
+ * duration of the call, and refunds the unused part a receipt later. Almost
94
+ * none of it is burned (~0.004 in practice), but the wallet checks the balance
95
+ * against the reservation and refuses below it. Quoting only the deposit is
96
+ * what produced:
97
+ *
98
+ * Sender work12.testnet does not have enough balance 3.5234
99
+ * for operation costing 3.674061500357969
100
+ *
101
+ * so the quoted total includes it.
102
+ */
103
+ export declare const GAS_RESERVATION_YOCTO: bigint;
104
+ /**
105
+ * Known factories.
106
+ *
107
+ * MAINNET IS DELIBERATELY ABSENT until one is deployed and verified. A wrong
108
+ * default here does not fail safely: it sends a real user with real NEAR to
109
+ * sign a 3.4 NEAR deposit into an account we do not control. Absent means the
110
+ * tool asks, which is the correct behaviour for a value nobody has measured.
111
+ */
112
+ export declare const FACTORY_BY_NETWORK: Record<string, string | undefined>;
113
+ export interface CreatePlanInput {
114
+ owner: string;
115
+ /**
116
+ * False when the caller does not know the user's NEAR account.
117
+ *
118
+ * An agent onboarding a stranger has no account id to pass, and demanding
119
+ * one assumes the user knows it, types it correctly, and has one at all.
120
+ * The page derives everything from the CONNECTED wallet anyway, so an
121
+ * owner-less link is not degraded — it asks the wallet instead of the human.
122
+ */
123
+ ownerKnown?: boolean;
124
+ /** From the factory's `vault_id_for`. `null` means no legal derived id. */
125
+ vaultId: string | null;
126
+ factory: string;
127
+ network: string;
128
+ /** From the factory's `get_config`. */
129
+ requiredDepositYocto: string;
130
+ paused: boolean;
131
+ appUrl: string;
132
+ /** Whether the derived account is already on chain. */
133
+ exists?: boolean;
134
+ /**
135
+ * The agent public key to bind at creation, if any.
136
+ *
137
+ * Only affects the QUOTE here — the server neither generates nor holds the
138
+ * secret half, and the page still asks the user to confirm the key before
139
+ * anything is signed.
140
+ */
141
+ agentPk?: string | null;
142
+ }
143
+ export interface CreatePlan {
144
+ owner: string | null;
145
+ /** Null when the owner is unknown — the wallet decides it at signing time. */
146
+ vault: string | null;
147
+ factory: string;
148
+ network: string;
149
+ depositYocto: string;
150
+ depositNear: string;
151
+ gasTgas: number;
152
+ gasReservationNear: string;
153
+ totalRequiredYocto: string;
154
+ totalRequiredNear: string;
155
+ url: string;
156
+ warnings: string[];
157
+ note: string;
158
+ }
159
+ /** yoctoNEAR -> a NEAR string, without floating point. */
160
+ export declare function yoctoToNear(y: bigint, dp?: number): string;
161
+ export declare function buildCreatePlan(i: CreatePlanInput): CreatePlan;
package/dist/create.js ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Onboarding: what a user with NO vault yet needs in order to get one.
3
+ *
4
+ * This is the only part of the server that talks to someone who is not yet a
5
+ * customer, and it holds no key and signs nothing — same trust model as the
6
+ * plan tools. It answers three questions and hands back a URL:
7
+ *
8
+ * where does my vault go the FACTORY's own `vault_id_for` view
9
+ * what must I attach the factory's `required_deposit` + code storage
10
+ * how do I sign it the web page, which the user's wallet approves
11
+ *
12
+ * THE DERIVATION IS NOT REIMPLEMENTED HERE. `vault_id_for` exists on the
13
+ * factory precisely so there is one implementation of it; a local copy is a
14
+ * thing that drifts. The factory's prefix rule has already changed once (labels
15
+ * are now rooted in the caller's prefix after a squatting hole), and a client
16
+ * that had copied the old rule would have gone on confidently printing wrong
17
+ * addresses.
18
+ */
19
+ /**
20
+ * Bytes of the vault contract the factory deploys.
21
+ *
22
+ * 301,033 until `create_agent_account` was removed, which took 7,305 bytes
23
+ * with it — about 0.073 NEAR off every vault's storage deposit. The pin
24
+ * caught the drift the moment the contract shrank, which is the entire
25
+ * reason it exists.
26
+ *
27
+ * Mirrors `packages/create-float/res/vault.wasm`, and `create.pins.test.ts`
28
+ * reads that file and fails if the two disagree — so a contract rebuild cannot
29
+ * silently leave this quoting a deposit that is too small. Too small is not a
30
+ * clean failure: CreateAccount succeeds and the vault sits below its own
31
+ * storage requirement, bricking on the first write.
32
+ */
33
+ export const CONTRACT_BYTES = 298819;
34
+ /** NEAR charges 1e19 yoctoNEAR per byte of storage. */
35
+ export const STORAGE_COST_PER_BYTE = 10n ** 19n;
36
+ /**
37
+ * What `web/index.html` adds on top of the code's storage stake.
38
+ *
39
+ * THE FACTORY'S FLOOR IS NOT WHAT THE USER IS ASKED FOR, and conflating the
40
+ * two quoted a deposit 0.26 NEAR too low. `begin()` in
41
+ * contracts/factory/src/lib.rs rejects below `required_deposit + bytes * 1e19`
42
+ * — a real floor, 3.037280 on testnet today. But the page attaches
43
+ * `stake + 0.36` instead, as headroom for the root node's state, and a wallet
44
+ * checks the balance against what the PAGE attaches.
45
+ *
46
+ * Measured 2026-11-03: a vault created through the page,
47
+ * tinyrice9138-testnet.fac.floatv4.testnet, holds exactly 3.297280 — the
48
+ * page's figure to the yoctoNEAR. A user funded to the old 3.237280 quote is
49
+ * rejected by their wallet needing 3.497280, which is precisely the failure
50
+ * GAS_RESERVATION_YOCTO below was written to prevent, arriving through the
51
+ * other term.
52
+ *
53
+ * This is a COPY of a value in a file that cannot import from here — the page
54
+ * is standalone HTML served to a browser with no build step. `create.pins.
55
+ * test.ts` reads that file and fails if the two disagree, the same way
56
+ * CONTRACT_BYTES and CREATE_GAS_TGAS are pinned. A copy nothing checks is a
57
+ * copy that drifts.
58
+ */
59
+ export const PAGE_HEADROOM_YOCTO = 36n * 10n ** 22n;
60
+ /**
61
+ * Funding for an agent's gas key, when one is bound at creation.
62
+ *
63
+ * A NEP-611 gas key's BALANCE is its spending cap — `allowance` is null by
64
+ * design — so this is the only thing bounding how much gas the agent burns.
65
+ * 0.12 NEAR ≈ 50 payments at the vault's 50-TGas `MIN_GAS_FOR_DISPATCH`
66
+ * floor, matching `gasKeyFunding({tgas:60, payments:50})` in create-float.
67
+ *
68
+ * Charged ONLY when an agent is bound. The factory raises its own on-chain
69
+ * deposit floor by exactly this amount in that case, so a quote that omits it
70
+ * is refused with DEPOSIT_TOO_SMALL — and a quote that always includes it
71
+ * overcharges everyone who creates a bare vault.
72
+ *
73
+ * A COPY of `AGENT_GAS_KEY_FUNDING` in contracts/factory/src/lib.rs and
74
+ * `AGENT_GAS` in web/index.html. Three files, one number, which is exactly
75
+ * the shape that already shipped a 0.26 NEAR underquote here — so
76
+ * `create.pins.test.ts` reads both of the others and fails if any disagree.
77
+ */
78
+ export const AGENT_GAS_KEY_FUNDING_YOCTO = 12n * 10n ** 22n;
79
+ /**
80
+ * Gas the create page attaches. MEASURED commitment is 124.88 TGas:
81
+ * 42.79 to deliver and decode the 401 KB base64 argument (measured on chain),
82
+ * plus CreateAccount 7.70, Transfer 0.23, DeployContract 34.16, and the
83
+ * factory's forwarded 30 + 10. 200 keeps ~60% headroom.
84
+ *
85
+ * Must match `web/index.html`. It is not a free choice — see
86
+ * `GAS_RESERVATION_YOCTO`.
87
+ */
88
+ export const CREATE_GAS_TGAS = 200n;
89
+ /**
90
+ * What the user must HOLD on top of the deposit, at signing time.
91
+ *
92
+ * A transaction reserves `prepaid_gas * 1e9` — the MAXIMUM gas price — for the
93
+ * duration of the call, and refunds the unused part a receipt later. Almost
94
+ * none of it is burned (~0.004 in practice), but the wallet checks the balance
95
+ * against the reservation and refuses below it. Quoting only the deposit is
96
+ * what produced:
97
+ *
98
+ * Sender work12.testnet does not have enough balance 3.5234
99
+ * for operation costing 3.674061500357969
100
+ *
101
+ * so the quoted total includes it.
102
+ */
103
+ export const GAS_RESERVATION_YOCTO = CREATE_GAS_TGAS * 10n ** 12n * 10n ** 9n;
104
+ /**
105
+ * Known factories.
106
+ *
107
+ * MAINNET IS DELIBERATELY ABSENT until one is deployed and verified. A wrong
108
+ * default here does not fail safely: it sends a real user with real NEAR to
109
+ * sign a 3.4 NEAR deposit into an account we do not control. Absent means the
110
+ * tool asks, which is the correct behaviour for a value nobody has measured.
111
+ */
112
+ export const FACTORY_BY_NETWORK = {
113
+ testnet: "fac.floatv4.testnet",
114
+ mainnet: undefined,
115
+ };
116
+ /** yoctoNEAR -> a NEAR string, without floating point. */
117
+ export function yoctoToNear(y, dp = 5) {
118
+ const neg = y < 0n;
119
+ const v = neg ? -y : y;
120
+ const whole = v / 10n ** 24n;
121
+ const frac = (v % 10n ** 24n).toString().padStart(24, "0").slice(0, dp);
122
+ return `${neg ? "-" : ""}${whole}.${frac}`;
123
+ }
124
+ export function buildCreatePlan(i) {
125
+ if (i.paused) {
126
+ throw new Error(`The factory ${i.factory} is paused — vault creation is disabled. ` +
127
+ `This is the operator's kill switch; try again later.`);
128
+ }
129
+ // UNKNOWN OWNER IS A VALID CASE, not a missing argument. Everything below
130
+ // that depends on the owner is skipped rather than guessed: no derived
131
+ // address, no existence check, and no `owner` on the url — the page refuses
132
+ // when that parameter disagrees with the connected wallet, so a blank or
133
+ // invented one would turn every link into a refusal.
134
+ const known = i.ownerKnown !== false && !!i.owner;
135
+ if (known && !i.vaultId) {
136
+ throw new Error(`${i.owner} has no legal vault id under ${i.factory}. ` +
137
+ `An implicit account (a 64-character hex id) cannot have one: the derived ` +
138
+ `prefix is already 64 characters, so "<prefix>.${i.factory}" exceeds NEAR's ` +
139
+ `64-character limit. Use a named account.`);
140
+ }
141
+ if (known && i.exists) {
142
+ throw new Error(`${i.vaultId} already exists. That is this owner's vault — connect to it ` +
143
+ `with NEAR_VAULT_CONTRACT=${i.vaultId} rather than creating another.`);
144
+ }
145
+ // Two different floors, and the quote must clear BOTH.
146
+ //
147
+ // pageAttaches what the wallet is actually asked to approve
148
+ // factoryFloor what `begin()` rejects below, on chain
149
+ //
150
+ // They are independent: the page's headroom is a fixed constant while the
151
+ // factory's floor moves with `set_required_deposit`. Quoting either one
152
+ // alone is what broke — the old code quoted the floor, and the page then
153
+ // asked for 0.26 more.
154
+ const stake = BigInt(CONTRACT_BYTES) * STORAGE_COST_PER_BYTE;
155
+ // Both floors move by the gas-key funding when an agent is bound: the page
156
+ // attaches it, and the factory requires it.
157
+ const agentFunding = i.agentPk ? AGENT_GAS_KEY_FUNDING_YOCTO : 0n;
158
+ const pageAttaches = stake + PAGE_HEADROOM_YOCTO + agentFunding;
159
+ const factoryFloor = BigInt(i.requiredDepositYocto) + stake + agentFunding;
160
+ const deposit = pageAttaches > factoryFloor ? pageAttaches : factoryFloor;
161
+ const total = deposit + GAS_RESERVATION_YOCTO;
162
+ // If the operator raises `required_deposit` past the page's headroom, the
163
+ // page under-attaches and the create is rejected ON CHAIN with
164
+ // DEPOSIT_TOO_SMALL. Quoting the higher figure does not rescue that — the
165
+ // user would hold enough and still fail, because the page attaches the
166
+ // lesser amount regardless of what they hold. Say so rather than quoting a
167
+ // number that cannot help.
168
+ const underAttachWarning = factoryFloor > pageAttaches
169
+ ? [
170
+ `WARNING: ${i.factory} now requires ${yoctoToNear(factoryFloor)} NEAR, but the ` +
171
+ `create page attaches only ${yoctoToNear(pageAttaches)}. The transaction will be ` +
172
+ `rejected on chain with DEPOSIT_TOO_SMALL no matter how much you hold. The page's ` +
173
+ `headroom constant needs raising — this is an operator bug, not a funding problem.`,
174
+ ]
175
+ : [];
176
+ const url = new URL(i.appUrl);
177
+ if (known)
178
+ url.searchParams.set("owner", i.owner);
179
+ if (i.agentPk)
180
+ url.searchParams.set("agent_pk", i.agentPk);
181
+ url.searchParams.set("factory", i.factory);
182
+ url.searchParams.set("network", i.network);
183
+ const unknownOwnerWarnings = known
184
+ ? []
185
+ : [
186
+ `The vault address is not known yet — it is derived from whichever wallet ` +
187
+ `you connect, and the page will show it before you approve anything.`,
188
+ `Nothing has checked whether you already have a vault. If you do, the page ` +
189
+ `will say so rather than creating a second one.`,
190
+ ];
191
+ return {
192
+ owner: known ? i.owner : null,
193
+ vault: known ? i.vaultId : null,
194
+ factory: i.factory,
195
+ network: i.network,
196
+ depositYocto: deposit.toString(),
197
+ depositNear: yoctoToNear(deposit),
198
+ gasTgas: Number(CREATE_GAS_TGAS),
199
+ gasReservationNear: yoctoToNear(GAS_RESERVATION_YOCTO),
200
+ totalRequiredYocto: total.toString(),
201
+ totalRequiredNear: yoctoToNear(total),
202
+ url: url.toString(),
203
+ warnings: [
204
+ ...underAttachWarning,
205
+ ...unknownOwnerWarnings,
206
+ `Almost all of the ${yoctoToNear(deposit)} NEAR is a refundable storage ` +
207
+ `deposit held by your vault, not a fee.`,
208
+ `You must HOLD ${yoctoToNear(total)} NEAR at signing time: the deposit plus a ` +
209
+ `${yoctoToNear(GAS_RESERVATION_YOCTO)} gas reservation. The reservation is ` +
210
+ `refunded moments later — only about 0.004 is actually spent — but a wallet ` +
211
+ `refuses the transaction if the balance is below it.`,
212
+ `Nothing can spend from this vault until you add an agent and give it a budget.`,
213
+ ],
214
+ note: `This server never sees or holds a key. Open the URL and approve the ` +
215
+ `transaction in your own wallet; the contract travels as an argument to ` +
216
+ `one ordinary function call, and the factory deploys it for you.`,
217
+ };
218
+ }
@@ -0,0 +1,55 @@
1
+ export declare class KeyFileError extends Error {
2
+ }
3
+ export interface KeyFileContents {
4
+ secret: string;
5
+ /** From a `~/.near-credentials`-shaped file. Absent for a bare key file. */
6
+ accountId?: string;
7
+ publicKey?: string;
8
+ }
9
+ /**
10
+ * Read a key out of `path`.
11
+ *
12
+ * Accepts either the `~/.near-credentials` JSON shape or a bare key on one
13
+ * line. Throws `KeyFileError` for anything else — never returns an empty or
14
+ * partial credential, because "I could not read it" collapsing into "there
15
+ * isn't one" is the failure this is built to avoid.
16
+ */
17
+ export declare function readKeyFile(path: string): KeyFileContents;
18
+ export type KeySource = "file" | "inline" | "none";
19
+ export interface ResolvedKey {
20
+ /** Absent when nothing is configured OR when the named file failed to read. */
21
+ secret?: string;
22
+ source: KeySource;
23
+ /** The path that was tried, when one was named. */
24
+ path?: string;
25
+ accountId?: string;
26
+ publicKey?: string;
27
+ /**
28
+ * Set when a file was NAMED but could not be read.
29
+ *
30
+ * Distinct from `source === "none"` on purpose, and the caller must keep it
31
+ * that way: "no key is configured" and "the key file you named is broken"
32
+ * are different problems with different fixes, and reporting the second as
33
+ * the first sends the reader to set a variable that is already set. The
34
+ * adapter's `loadKeyring` carries `skippedOtherVault` for exactly this
35
+ * reason.
36
+ */
37
+ error?: string;
38
+ /** Operator-facing notes. NAMES ONLY — these get logged. */
39
+ warnings: string[];
40
+ }
41
+ /**
42
+ * Decide which of the two variables supplies a secret.
43
+ *
44
+ * The file wins. A failed file read does NOT fall back to the inline value:
45
+ * silently using the config an operator believes they have stopped using is
46
+ * worse than not starting.
47
+ */
48
+ export declare function resolveKeySource(opts: {
49
+ inline?: string;
50
+ file?: string;
51
+ /** e.g. "NEAR_VAULT_AGENT_KEY" — named so messages can be acted on. */
52
+ inlineVar: string;
53
+ /** e.g. "NEAR_VAULT_AGENT_KEY_FILE" */
54
+ fileVar: string;
55
+ }): ResolvedKey;