@swarm-tips/client 0.0.0 → 0.1.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.
Files changed (60) hide show
  1. package/README.md +17 -1
  2. package/SBOM.spdx.json +1558 -0
  3. package/dist/cli/swarm-tx.d.ts +2 -0
  4. package/dist/cli/swarm-tx.js +46 -0
  5. package/dist/cli/vow-verify.d.ts +2 -0
  6. package/dist/cli/vow-verify.js +69 -0
  7. package/dist/contracts/generated/coordination_game.d.ts +6107 -0
  8. package/dist/contracts/generated/coordination_game.js +1 -0
  9. package/dist/contracts/generated/shillbot.d.ts +4209 -0
  10. package/dist/contracts/generated/shillbot.js +1 -0
  11. package/dist/contracts/idl/coordination_game.json +6101 -0
  12. package/dist/contracts/idl/shillbot.json +4203 -0
  13. package/dist/contracts/index.d.ts +33 -0
  14. package/dist/contracts/index.js +84 -0
  15. package/dist/coordination-game/battery.d.ts +40 -0
  16. package/dist/coordination-game/battery.js +82 -0
  17. package/dist/coordination-game/index.d.ts +4 -0
  18. package/dist/coordination-game/index.js +3 -0
  19. package/dist/coordination-game/outcome-oracle.d.ts +53 -0
  20. package/dist/coordination-game/outcome-oracle.js +136 -0
  21. package/dist/evm/index.d.ts +3 -0
  22. package/dist/evm/index.js +6 -0
  23. package/dist/evm/sign.d.ts +12 -0
  24. package/dist/evm/sign.js +23 -0
  25. package/dist/evm/testing.d.ts +21 -0
  26. package/dist/evm/testing.js +34 -0
  27. package/dist/evm/tx.d.ts +27 -0
  28. package/dist/evm/tx.js +18 -0
  29. package/dist/evm/wallet.d.ts +58 -0
  30. package/dist/evm/wallet.js +182 -0
  31. package/dist/inbox/client.d.ts +189 -0
  32. package/dist/inbox/client.js +334 -0
  33. package/dist/inbox/index.d.ts +2 -0
  34. package/dist/inbox/index.js +4 -0
  35. package/dist/inbox/solana.d.ts +11 -0
  36. package/dist/inbox/solana.js +13 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +3 -0
  39. package/dist/shillbot/index.d.ts +115 -0
  40. package/dist/shillbot/index.js +327 -0
  41. package/dist/shillbot/payout-oracle.d.ts +63 -0
  42. package/dist/shillbot/payout-oracle.js +172 -0
  43. package/dist/shillbot/switchboard.d.ts +27 -0
  44. package/dist/shillbot/switchboard.js +205 -0
  45. package/dist/vow/decoders/extension.d.ts +62 -0
  46. package/dist/vow/decoders/extension.js +48 -0
  47. package/dist/vow/decoders/shillbot.d.ts +59 -0
  48. package/dist/vow/decoders/shillbot.js +130 -0
  49. package/dist/vow/discriminator.d.ts +12 -0
  50. package/dist/vow/discriminator.js +25 -0
  51. package/dist/vow/index.d.ts +7 -0
  52. package/dist/vow/index.js +5 -0
  53. package/dist/vow/schema.d.ts +14 -0
  54. package/dist/vow/schema.js +132 -0
  55. package/dist/vow/types.d.ts +107 -0
  56. package/dist/vow/types.js +1 -0
  57. package/dist/vow/verify.d.ts +23 -0
  58. package/dist/vow/verify.js +145 -0
  59. package/package.json +92 -6
  60. package/index.js +0 -1
@@ -0,0 +1,182 @@
1
+ import { createConfig } from "wagmi";
2
+ import { injected, walletConnect } from "wagmi/connectors";
3
+ import { createWalletClient, fallback, http as viemHttp, } from "viem";
4
+ import { privateKeyToAccount } from "viem/accounts";
5
+ import { e2eTestModeEnabled } from "./testing.js";
6
+ /**
7
+ * Resilient transport: a viem `fallback` across the given RPC URLs (fails over
8
+ * to the next on a transient RPC error), or the chain default when none are
9
+ * given. Public testnet RPCs (e.g. sepolia.base.org) intermittently reject
10
+ * `eth_sendRawTransaction` with -32602; failover keeps commit/reveal from
11
+ * dying on one bad endpoint.
12
+ */
13
+ function buildTransport(rpcUrls) {
14
+ if (rpcUrls && rpcUrls.length > 0) {
15
+ return fallback(rpcUrls.map((url) => viemHttp(url)));
16
+ }
17
+ return viemHttp();
18
+ }
19
+ /**
20
+ * Read an E2E test private key from a `?<param>=<0x-hex>` query param, if
21
+ * present (default param `evmTestWallet`). Mirrors the Solana `?testWallet=`
22
+ * pattern so Playwright can drive the EVM path headlessly. Returns null off the
23
+ * happy path (no window, missing/malformed param) AND whenever the page isn't
24
+ * in E2E test mode — so a crafted prod link can't activate the connector.
25
+ */
26
+ export function getEvmTestWalletKey(param = "evmTestWallet") {
27
+ if (typeof window === "undefined")
28
+ return null;
29
+ if (!e2eTestModeEnabled())
30
+ return null;
31
+ const raw = new URLSearchParams(window.location.search).get(param);
32
+ if (!raw)
33
+ return null;
34
+ const hex = raw.startsWith("0x") ? raw : `0x${raw}`;
35
+ // A secp256k1 private key is 32 bytes → 64 hex chars after the 0x prefix.
36
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hex)) {
37
+ console.error("[evm-kit] ignoring malformed test wallet param");
38
+ return null;
39
+ }
40
+ return hex;
41
+ }
42
+ /**
43
+ * Read an E2E RPC override from `?evmRpc=<https-url>`. Lets Playwright point the
44
+ * page at a reliable provider (e.g. QuickNode) WITHOUT baking the key into the
45
+ * public bundle. Caller gates this to test mode (test wallet present) so a
46
+ * crafted link can't redirect a real user's wallet to a hostile RPC. Only https
47
+ * URLs are accepted. Also gated on E2E test mode (defense in depth — its only
48
+ * caller already requires a test wallet, which is itself test-mode-gated).
49
+ */
50
+ export function getEvmRpcOverride(param = "evmRpc") {
51
+ if (typeof window === "undefined")
52
+ return null;
53
+ if (!e2eTestModeEnabled())
54
+ return null;
55
+ const raw = new URLSearchParams(window.location.search).get(param);
56
+ if (!raw)
57
+ return null;
58
+ try {
59
+ const url = new URL(raw);
60
+ if (url.protocol !== "https:") {
61
+ // Log for the same reason the malformed branch below does: a rejected
62
+ // override is either a hostile downgrade attempt or an e2e run pointed
63
+ // at http://, and silently returning null leaves no trace of either.
64
+ console.error(`[evm-kit] ignoring non-https evmRpc param (protocol ${url.protocol})`);
65
+ return null;
66
+ }
67
+ return url.toString();
68
+ }
69
+ catch {
70
+ console.error("[evm-kit] ignoring malformed evmRpc param");
71
+ return null;
72
+ }
73
+ }
74
+ /**
75
+ * A wagmi connector backed by a raw private key via a viem wallet client.
76
+ * Auto-connects and signs txs + messages without a browser extension, so
77
+ * Playwright can drive an EVM flow headlessly. Test-only — gated behind the
78
+ * test-wallet query param at config-build time.
79
+ */
80
+ function evmTestConnector(key, chain, transport) {
81
+ const account = privateKeyToAccount(key);
82
+ const client = createWalletClient({ account, chain, transport });
83
+ // An EIP-1193 shim routing the wallet methods wagmi calls to the viem client;
84
+ // everything else falls through to the chain transport.
85
+ const request = async (args) => {
86
+ const params = args.params ?? [];
87
+ switch (args.method) {
88
+ case "eth_accounts":
89
+ case "eth_requestAccounts":
90
+ return [account.address];
91
+ case "eth_chainId":
92
+ return chain.id;
93
+ case "personal_sign": {
94
+ const [message] = params;
95
+ return client.signMessage({ message: { raw: message } });
96
+ }
97
+ case "eth_sendTransaction": {
98
+ // wagmi hands EIP-1193 params in RPC-hex form (gas/value/nonce as 0x
99
+ // strings). Passing those straight into viem's TYPED sendTransaction
100
+ // produces a signed tx that strict RPCs reject ("failed to decode signed
101
+ // transaction"). Take only the essential fields and let viem re-prepare
102
+ // gas/fees/nonce itself — the path a plain viem walletClient uses.
103
+ const [tx] = params;
104
+ return client.sendTransaction({
105
+ to: tx.to,
106
+ data: tx.data ?? tx.input,
107
+ value: tx.value ? BigInt(tx.value) : undefined,
108
+ });
109
+ }
110
+ default:
111
+ return client.request({
112
+ method: args.method,
113
+ params,
114
+ });
115
+ }
116
+ };
117
+ // wagmi's injected connector subscribes to EIP-1193 events; provide no-op
118
+ // on/removeListener so connect() doesn't crash on the private-key provider.
119
+ return injected({
120
+ target: {
121
+ id: "evmTestWallet",
122
+ name: "EVM Test Wallet",
123
+ provider: { request, on: () => { }, removeListener: () => { } },
124
+ },
125
+ });
126
+ }
127
+ /**
128
+ * Choose the connector set for a config. Pure, so the precedence rule is
129
+ * unit-testable: the E2E test connector always wins (a live sweep must never be
130
+ * hijacked by a real wallet), then app-supplied connectors, then the built-in
131
+ * injected/WalletConnect pair.
132
+ */
133
+ export function selectConnectors(testConnector, appConnectors, builtIn) {
134
+ if (testConnector)
135
+ return [testConnector];
136
+ if (appConnectors && appConnectors.length > 0)
137
+ return appConnectors;
138
+ return builtIn;
139
+ }
140
+ /**
141
+ * Build a wagmi config for one or several EVM chains. Multi-chain lets the
142
+ * wallet switch chains (e.g. Base Sepolia ↔ Ethereum Sepolia) so the same site
143
+ * plays each — each chain gets its own resilient transport. The `injected`
144
+ * connector serves humans; the private-key test connector serves E2E when the
145
+ * test-wallet query param is present (bound to the `?evmChain=<caip2>` chain,
146
+ * or the first listed chain).
147
+ */
148
+ export function buildWagmiConfig(chains, opts = {}) {
149
+ const { walletConnectProjectId } = opts;
150
+ const list = chains;
151
+ const key = getEvmTestWalletKey();
152
+ // A `?evmRpc=` override is honored ONLY in test mode (test wallet present), so
153
+ // a crafted link can't point a real user's wallet at a hostile RPC. When set,
154
+ // use that single reliable endpoint (no fallback) — fail-fast-or-succeed.
155
+ const override = key ? getEvmRpcOverride() : null;
156
+ const transports = Object.fromEntries(list.map(({ chain, rpcUrls: chainRpcs }) => [
157
+ chain.id,
158
+ override ? viemHttp(override) : buildTransport(chainRpcs),
159
+ ]));
160
+ // Test connector binds to one chain: the `?evmChain=<eip155:id>` selection if
161
+ // it matches a listed chain, else the first. `useSwitchChain` in the game hook
162
+ // moves the wallet to the chain the player picked before any staking tx.
163
+ const testChain = pickTestChain(list) ?? list[0].chain;
164
+ const connectors = selectConnectors(key ? evmTestConnector(key, testChain, transports[testChain.id]) : null, opts.connectors, walletConnectProjectId
165
+ ? [injected(), walletConnect({ projectId: walletConnectProjectId })]
166
+ : [injected()]);
167
+ const chainTuple = list.map((c) => c.chain);
168
+ return createConfig({ chains: chainTuple, connectors, transports });
169
+ }
170
+ /** Which listed chain the E2E test wallet should bind to: the `?evmChain=`
171
+ * eip155 selection when it matches, else null (caller defaults to first). */
172
+ function pickTestChain(list) {
173
+ if (typeof window === "undefined")
174
+ return null;
175
+ const raw = new URLSearchParams(window.location.search).get("evmChain");
176
+ if (!raw)
177
+ return null;
178
+ const id = raw.startsWith("eip155:")
179
+ ? Number(raw.slice("eip155:".length))
180
+ : Number(raw);
181
+ return list.find((c) => c.chain.id === id)?.chain ?? null;
182
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * REST client for the agent-inbox twins on mcp-server
3
+ * (`/internal/inbox/*` on mcp.swarm.tips). The twins share the MCP inbox's
4
+ * storage layer, so shapes mirror the `agent_*` MCP tools exactly
5
+ * (`from_wallet` is CAIP-10, `msg_id` doubles as the paging/ack cursor).
6
+ *
7
+ * Session + request flow (mirrors scripts/seed-inbox.ts):
8
+ *
9
+ * createSession(wallet, signNonce)
10
+ * │
11
+ * ├─ localStorage hit? ──────────────► use cached {session_id, tier}
12
+ * │ │
13
+ * └─ POST /session {wallet} → {nonce} │
14
+ * signNonce(nonce) (ONE wallet popup) │
15
+ * POST /session {wallet,nonce,sig} │
16
+ * → {session_id, tier} → cache ────┤
17
+ * ▼
18
+ * getMessages / send / ack ── X-Inbox-Session header
19
+ * │
20
+ * └─ 401? → drop cache → re-mint (popup) → retry ONCE → else throw
21
+ *
22
+ * The `signNonce` callback is chain-agnostic: it receives the raw nonce
23
+ * string and must return the wire-format signature string. Solana =
24
+ * bs58(ed25519 detached over the nonce UTF-8 bytes) — use `solanaNonceSigner`
25
+ * from this package; EVM = the `personal_sign` hex string as-is.
26
+ */
27
+ export type NonceSigner = (nonce: string) => Promise<string>;
28
+ export interface InboxSession {
29
+ session_id: string;
30
+ tier: string;
31
+ }
32
+ export interface InboxMessage {
33
+ /** Message id; also the paging + ack cursor (lexically ordered). */
34
+ msg_id: string;
35
+ /** CAIP-10 sender, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:BASE58". */
36
+ from_wallet: string;
37
+ thread_id: string;
38
+ intent: string | null;
39
+ body: string;
40
+ /** RFC 3339 send timestamp. */
41
+ sent_at: string;
42
+ /** True for org seed-wallet traffic (server-tagged). */
43
+ seed?: boolean;
44
+ /** Present only under `include_sent` reads: "sent" = a mirror copy the
45
+ * connected wallet wrote, "received" = an inbound message. Absent on
46
+ * inbound-only reads (treat as "received"). */
47
+ direction?: "received" | "sent";
48
+ }
49
+ export interface MessagePage {
50
+ messages: InboxMessage[];
51
+ next_cursor: string | null;
52
+ }
53
+ /** One post on a public topic board (open-challenge / subcontract). Bodies
54
+ * are third-party text — render as text, never HTML. */
55
+ export interface TopicPost {
56
+ post_id: string;
57
+ topic_id: string;
58
+ /** CAIP-10 author id. */
59
+ author_wallet: string;
60
+ body: string;
61
+ /** post_id this replies to, or null for a root post. */
62
+ reply_to: string | null;
63
+ intent: string | null;
64
+ /** Unsigned-tx-flow / game / task id this post points at, or null. Drives
65
+ * the join/claim CTA. */
66
+ ref_id: string | null;
67
+ reported_count: number;
68
+ /** RFC 3339 creation timestamp. */
69
+ created_at: string;
70
+ seed?: boolean;
71
+ }
72
+ export interface TopicPage {
73
+ topic_id: string;
74
+ posts: TopicPost[];
75
+ next_cursor: string | null;
76
+ /** Server-side count of posts dropped by the auto-hide moderation flag. */
77
+ filtered_hidden?: number;
78
+ /** Posts dropped for author trust below the requested `min_trust`. */
79
+ filtered_below_min_trust?: number;
80
+ }
81
+ export interface PublishReceipt {
82
+ published: boolean;
83
+ post_id: string;
84
+ posts_remaining_today?: number;
85
+ }
86
+ export interface ReportReceipt {
87
+ reported: boolean;
88
+ reported_count: number;
89
+ hidden: boolean;
90
+ already_reported: boolean;
91
+ }
92
+ /** Known public topic boards. `open-challenge` = game matchmaking,
93
+ * `subcontract` = Shillbot task handoff. */
94
+ export type TopicId = "open-challenge" | "subcontract";
95
+ export interface SendReceipt {
96
+ msg_id: string;
97
+ thread_id: string;
98
+ sends_remaining_today?: number;
99
+ }
100
+ /** Non-2xx responses and client-side boundary rejections. `status` is the
101
+ * HTTP status, or 0 for errors raised before any request was made. */
102
+ export declare class InboxApiError extends Error {
103
+ readonly status: number;
104
+ constructor(status: number, message: string);
105
+ }
106
+ interface StorageLike {
107
+ getItem(key: string): string | null;
108
+ setItem(key: string, value: string): void;
109
+ removeItem(key: string): void;
110
+ }
111
+ export interface InboxClientOpts {
112
+ /** REST base URL; default https://mcp.swarm.tips. Frontends thread their
113
+ * VITE_ env override through this (same pattern as API_BASE_URL). */
114
+ baseUrl?: string;
115
+ /** Session cache. Defaults to window.localStorage; pass null to disable. */
116
+ storage?: StorageLike | null;
117
+ /** fetch implementation — tests inject a mock. */
118
+ fetchFn?: typeof fetch;
119
+ }
120
+ export declare class InboxClient {
121
+ private readonly baseUrl;
122
+ private readonly storage;
123
+ private readonly fetchFn;
124
+ private session;
125
+ private wallet;
126
+ private signNonce;
127
+ constructor(opts?: InboxClientOpts);
128
+ /** True when a cached session exists for `wallet` — opening the inbox will
129
+ * not prompt a wallet signature. */
130
+ hasCachedSession(wallet: string): boolean;
131
+ /** Mint (or restore from cache) the inbox session for `wallet`. Signing
132
+ * happens at most once per call — zero times on a cache hit. The wallet +
133
+ * signer are retained for transparent re-mint when a request 401s. */
134
+ createSession(wallet: string, signNonce: NonceSigner): Promise<InboxSession>;
135
+ /** Drop the in-memory and cached session (e.g. on wallet disconnect). */
136
+ clearSession(wallet?: string): void;
137
+ /** Read a page of messages, newest first. Omit `threadId` for the whole
138
+ * mailbox; pass the previous page's `next_cursor` to page older. */
139
+ getMessages(opts?: {
140
+ threadId?: string;
141
+ cursor?: string;
142
+ limit?: number;
143
+ /** Merge the sender's own mirror copies into the page so a thread read
144
+ * shows both sides. Maps to `include_sent=true`. Sent messages carry
145
+ * `direction:"sent"`. */
146
+ includeSent?: boolean;
147
+ }): Promise<MessagePage>;
148
+ /** Read one thread (e.g. `task:{task_id}` / `game:{id}`), newest first. Pass
149
+ * `includeSent` to fold in the connected wallet's own replies (full
150
+ * two-way conversation). */
151
+ getThread(threadId: string, opts?: {
152
+ cursor?: string;
153
+ limit?: number;
154
+ includeSent?: boolean;
155
+ }): Promise<MessagePage>;
156
+ /** Send a message. Rejects empty / oversize bodies client-side; quota and
157
+ * mute rejections surface as InboxApiError with the server's message. */
158
+ send(toWallet: string, body: string, threadId?: string, intent?: string): Promise<SendReceipt>;
159
+ /** Advance the read watermark up to a msg_id cursor. NOTE: the watermark is
160
+ * mailbox-global — acking a cursor also acks every older message. */
161
+ ack(upToCursor: string): Promise<void>;
162
+ /** Read a page of a public topic board, newest first. OPEN — no session
163
+ * needed. `minTrust` drops posts whose author trust is below it
164
+ * server-side. Hidden (auto-moderated) posts are always dropped. */
165
+ readTopic(topicId: TopicId, opts?: {
166
+ cursor?: string;
167
+ limit?: number;
168
+ minTrust?: number;
169
+ }): Promise<TopicPage>;
170
+ /** Publish a post to a topic board. Session-gated (same X-Inbox-Session as
171
+ * inbox send). Rejects empty / oversize bodies client-side; quota
172
+ * rejections surface as InboxApiError with the server's message. */
173
+ publishPost(topicId: TopicId, body: string, opts?: {
174
+ replyTo?: string;
175
+ intent?: string;
176
+ refId?: string;
177
+ }): Promise<PublishReceipt>;
178
+ /** Report a post for moderation. Session-gated. Idempotent per reporter —
179
+ * a repeat report returns `already_reported: true`. */
180
+ reportPost(topicId: TopicId, postId: string): Promise<ReportReceipt>;
181
+ private getUnauthed;
182
+ private mintSession;
183
+ private postUnauthed;
184
+ private authed;
185
+ private doFetch;
186
+ private readCachedSession;
187
+ private writeCachedSession;
188
+ }
189
+ export {};