@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,90 @@
1
+ /**
2
+ * Could we not ASK the chain, or did the chain ANSWER?
3
+ *
4
+ * These are different facts and they must never share a code path. `/v1/resolve`
5
+ * ended both of its reads with `.catch(() => null)` / `.catch(() => false)`,
6
+ * so a transient RPC failure was reported as a statement about the user's
7
+ * account:
8
+ *
9
+ * vault_id_for threw -> "No legal vault id for that account. An implicit
10
+ * account (64 hex characters) cannot have one."
11
+ * accountExists threw -> "you do not have a vault yet"
12
+ *
13
+ * Both with HTTP 200, so the dashboard proxy — which deliberately does not
14
+ * cache a failure — cached them as answers for the life of the process.
15
+ *
16
+ * MEASURED, NOT IMAGINED. Six identical calls against a running adapter gave
17
+ * four correct answers and two nulls, because the adapter's own background
18
+ * refusal-follower saturates its RPC budget and competes with request
19
+ * handling. The user-visible result is the worst available one: somebody who
20
+ * owns a populated vault is shown the create-one empty state and invited to
21
+ * create a vault that already exists.
22
+ *
23
+ * MESSAGE MATCHING, AND WHY THAT IS ACCEPTABLE HERE. `viewFunction` throws a
24
+ * plain `Error` with a constructed string, and `fetch` throws `TypeError` with
25
+ * a platform string, so there is no status code or error class to branch on
26
+ * without changing both. The strings are pinned by tests, and the failure
27
+ * direction is chosen deliberately: an unrecognised error is treated as an
28
+ * ANSWER, so a new transport failure shows up as a visible wrong answer rather
29
+ * than as a silent infinite retry.
30
+ */
31
+ /** Substrings that mean the request never got a verdict from the chain. */
32
+ const TRANSPORT = [
33
+ "fetch failed",
34
+ "enotfound",
35
+ "econnrefused",
36
+ "econnreset",
37
+ "etimedout",
38
+ "epipe",
39
+ "socket hang up",
40
+ "aborted",
41
+ "timeout",
42
+ "network",
43
+ "endpoint is deprecated",
44
+ /**
45
+ * THE ONE THAT ACTUALLY BIT US, twice, and the second time in this very
46
+ * file. fastnear answers a throttle with HTTP **200** and a JSON error
47
+ * body, so `r.ok` is true and nothing downstream looks like a transport
48
+ * failure. `accountExists` correctly refuses to guess and throws
49
+ *
50
+ * could not determine whether X exists: Rate limits exceeded. For higher
51
+ * rate limits get a subscription at https://fastnear.com
52
+ *
53
+ * which matched NONE of the patterns above — no status code, no "fetch
54
+ * failed" — so it was classified as an ANSWER and became `exists: false`.
55
+ * The live dashboard told the owner of a vault holding 42.25 USDC that they
56
+ * had none, and offered to create one.
57
+ */
58
+ "rate limit",
59
+ "rate limits exceeded",
60
+ "too many requests",
61
+ "429",
62
+ /**
63
+ * The phrase `accountExists` uses when it cannot tell. It only throws when
64
+ * it genuinely does not know — a real absence returns false without
65
+ * throwing — so this wording is always uncertainty, never a verdict.
66
+ */
67
+ "could not determine whether",
68
+ ];
69
+ /**
70
+ * HTTP statuses that mean "ask again later" rather than "your request is
71
+ * wrong". 429 is the one that actually bit us; 5xx is the same category.
72
+ *
73
+ * 400/404 are NOT here on purpose: retrying resends the same bad request, and
74
+ * calling that "unavailable" would hide a real bug behind a retry loop.
75
+ */
76
+ const RETRYABLE_HTTP = /returned http (408|425|429|5\d\d)\b/;
77
+ export function isUnavailable(e) {
78
+ const msg = e instanceof Error ? e.message : typeof e === "string" ? e : "";
79
+ if (!msg)
80
+ return false;
81
+ const m = msg.toLowerCase();
82
+ if (RETRYABLE_HTTP.test(m))
83
+ return true;
84
+ // A contract that PANICKED gave us an answer, even though it is an error.
85
+ // Checked before the transport list because a panic message can contain a
86
+ // word like "timeout" from the contract's own text.
87
+ if (m.includes("panicked") || m.includes("wasm execution failed"))
88
+ return false;
89
+ return TRANSPORT.some((t) => m.includes(t));
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floatonnear/float-adapter",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Serves the existing Float dashboard's /v1 API from NEAR vault views, so the Float UI runs unchanged against the NEAR hierarchical vault.",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "typecheck": "tsc -p tsconfig.json --noEmit"
19
19
  },
20
20
  "dependencies": {
21
- "@floatonnear/mcp": "^0.3.0"
21
+ "@floatonnear/mcp": "^0.10.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^24.0.0",