@botanary/agent 0.1.0-alpha.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +123 -0
  4. package/dist/errors.d.ts +49 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +40 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/exact-action.d.ts +48 -0
  9. package/dist/exact-action.d.ts.map +1 -0
  10. package/dist/exact-action.js +178 -0
  11. package/dist/exact-action.js.map +1 -0
  12. package/dist/fingerprint.d.ts +21 -0
  13. package/dist/fingerprint.d.ts.map +1 -0
  14. package/dist/fingerprint.js +47 -0
  15. package/dist/fingerprint.js.map +1 -0
  16. package/dist/generated/routes.d.ts +68 -0
  17. package/dist/generated/routes.d.ts.map +1 -0
  18. package/dist/generated/routes.js +92 -0
  19. package/dist/generated/routes.js.map +1 -0
  20. package/dist/generated/schema.d.ts +2903 -0
  21. package/dist/generated/schema.d.ts.map +1 -0
  22. package/dist/generated/schema.js +2 -0
  23. package/dist/generated/schema.js.map +1 -0
  24. package/dist/http-path.d.ts +92 -0
  25. package/dist/http-path.d.ts.map +1 -0
  26. package/dist/http-path.js +201 -0
  27. package/dist/http-path.js.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +7 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/pairing-code.d.ts +41 -0
  33. package/dist/pairing-code.d.ts.map +1 -0
  34. package/dist/pairing-code.js +60 -0
  35. package/dist/pairing-code.js.map +1 -0
  36. package/dist/registration.d.ts +21 -0
  37. package/dist/registration.d.ts.map +1 -0
  38. package/dist/registration.js +62 -0
  39. package/dist/registration.js.map +1 -0
  40. package/dist/runtime.d.ts +203 -0
  41. package/dist/runtime.d.ts.map +1 -0
  42. package/dist/runtime.js +559 -0
  43. package/dist/runtime.js.map +1 -0
  44. package/dist/signer.d.ts +33 -0
  45. package/dist/signer.d.ts.map +1 -0
  46. package/dist/signer.js +13 -0
  47. package/dist/signer.js.map +1 -0
  48. package/dist/transport.d.ts +13 -0
  49. package/dist/transport.d.ts.map +1 -0
  50. package/dist/transport.js +135 -0
  51. package/dist/transport.js.map +1 -0
  52. package/dist/validation.d.ts +15 -0
  53. package/dist/validation.d.ts.map +1 -0
  54. package/dist/validation.js +108 -0
  55. package/dist/validation.js.map +1 -0
  56. package/dist/views.d.ts +182 -0
  57. package/dist/views.d.ts.map +1 -0
  58. package/dist/views.js +2 -0
  59. package/dist/views.js.map +1 -0
  60. package/package.json +50 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0-alpha.1
4
+
5
+ - Recover durable API payment status without signing or forwarding an existing submission again.
6
+ - Distinguish finalized payment evidence from provider HTTP success and retain uncertain outcomes.
7
+
8
+ - Extract agent signing, pairing, sessions, delegated execution, API payments and waits from MCP.
9
+ - Inject signer storage and preserve desktop profile and owner-session behavior in MCP.
10
+ - Generate agent wire types and transport routes from the reviewed canonical API contract.
11
+ - Validate signing inputs, registration context and session responses at runtime.
12
+ - Coalesce session refreshes and retain existing reconciliation handles after failures.
13
+ - Verify standalone and tool consumers using packed artifacts outside the workspace.
14
+
15
+ - Add exact transfer/swap preparation, transaction and hash verification, explicit native gas ceilings,
16
+ one-attempt submission handles and agent-scoped receipt lookup by original hash.
17
+ - Support owner-approved registration bindings and proof-based app-credential rotation without
18
+ creating or widening a spending grant.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Botanary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @botanary/agent
2
+
3
+ Alpha version 0.1.0-alpha.1, canonical API contract 2026-09-07. MIT licensed. Node 22 or later.
4
+ This package has not been verified against a deployed developer platform or a settled testnet transaction.
5
+
6
+ The runtime is extracted from `botanary-mcp`. MCP keeps desktop keychain adapters, profile selection,
7
+ owner sessions and approval handles. The CLI consumes that desktop adapter through MCP. The shared
8
+ package has no dependency on desktop storage, the Botanary backend or frontend source.
9
+
10
+ ```ts
11
+ // Node server only. The caller supplies an agent signer whose private key stays in its own storage.
12
+ import { AgentRuntime, registerAgent, type AgentSigner } from '@botanary/agent';
13
+ import { createBotanary } from '@botanary/sdk';
14
+
15
+ export async function registerAndRead(signer: AgentSigner) {
16
+ const app = createBotanary({ apiKey: process.env.BOTANARY_SECRET_KEY! });
17
+ const registration = await registerAgent(signer, app, 'Research agent');
18
+ return { id: registration.id, access: registration.apiAccess, grant: registration.grantStatus };
19
+ }
20
+
21
+ export async function readConnectedAgent(signer: AgentSigner) {
22
+ const agent = new AgentRuntime(signer);
23
+ return agent.me();
24
+ }
25
+ ```
26
+
27
+ Registration requires `app:read` and `agents:write` on the app key. It verifies an EIP-191 challenge
28
+ bound to the application, environment, key ID, agent public key, one-use nonce and expiry. It creates
29
+ neither an account connection nor an on-chain grant. Pass the returned registration ID to
30
+ `app.connections.begin({ agentRegistrationId: registration.id, redirectUri, scopes })`, including
31
+ `accounts:read` and `agents:read` in the requested scopes. That app key also needs `connections:write`
32
+ and every requested scope. The owner reviews the public key and explicitly checks the agent API
33
+ option for one account. `me()` then describes that exact binding; registration alone cannot make it
34
+ succeed. The owner still signs any spending grant separately.
35
+
36
+ For app-key rotation, repeat `registerAgent` with an active same-app/environment key carrying every
37
+ existing consent scope. This proves possession again, preserves the customer connection and
38
+ invalidates old agent sessions. Revoked owner consent requires a new owner approval. Disabled
39
+ registrations cannot be revived through registration proof.
40
+
41
+ `AgentSigner` supplies `ensure()` with public metadata, `sign(message)` for EIP-191 registration,
42
+ and `signHash(hash)` for raw session and execution digests. `createViemAgentSigner(account)` adapts
43
+ an existing viem-compatible account; it does not load, generate, export or back up a key. Each customer
44
+ connection needs a distinct identity. Independent runtime instances have independent cached sessions.
45
+
46
+ Session creation coalesces concurrent calls, validates nonce and expiry, and refreshes rejected reads
47
+ once. Mutations are not automatically retried. The default transport uses generated agent routes and
48
+ openapi-fetch, refuses owner routes and app credentials, forbids redirects and cookies, and bounds
49
+ response bodies and request time. The injected transport seam is for trusted host adapters such as MCP.
50
+
51
+ `callApi` preserves the provider/endpoint requirements, signature and relay protocol. A pending
52
+ approval includes its `requirementId`; `resumeApi(requirementId)` reads durable payment status before
53
+ continuing approval, without making another requirements request. A failed relay carries its original requirement ID. Do not treat an
54
+ ambiguous relay failure as permission to create a new purchase. The owner must complete paid-API setup
55
+ first; per-agent off-chain mandate checks and the on-chain payment authorization remain distinct.
56
+
57
+ `status: 'submitted'` with `paymentStatus: 'pending'` retains the same requirement while finalized
58
+ chain evidence is unavailable. Resume it to read progress; the SDK never signs or forwards an already
59
+ submitted payment again. `status: 'settled'` proves payment, while `providerStatus` separately records
60
+ whether the provider served the request. A provider can fail after payment. Check both fields and the
61
+ explicit `simulated` marker before presenting an outcome.
62
+
63
+ `spendUnderGrant` validates the v0.7 UserOp shape, signs once and preserves its hash on relay errors.
64
+ `awaitTerminal` returns the actual last reported state, including a pending state when its budget ends.
65
+ Submission is not confirmation. Existing low-level `buildDelegatedAction` and `buildDelegatedSwap`
66
+ methods retain the legacy display-number wire inputs for CLI/MCP compatibility. New integrations use
67
+ `prepareExactAction` below. Customer binding and real testnet settlement proof remain release work.
68
+ No mainnet grant capability is added here.
69
+
70
+ ## Exact execution
71
+
72
+ An already connected agent with an owner-signed grant can prepare an action. Obtain the delegation ID,
73
+ permission ID and pinned executor address from that approved grant and its network deployment. The
74
+ app key is never an execution credential. Amounts and the maximum native gas cost are integer strings.
75
+
76
+ ```ts
77
+ import { AgentRuntime, type AgentSigner, type AgentActionBinding } from '@botanary/agent';
78
+
79
+ export async function sendUnderApprovedGrant(signer: AgentSigner, binding: AgentActionBinding) {
80
+ const runtime = new AgentRuntime(signer);
81
+ const prepared = await runtime.prepareExactAction({
82
+ accountId: 'your-connected-account-id',
83
+ accountAddress: '0x1111111111111111111111111111111111111111',
84
+ chainId: 84532,
85
+ action: 'transfer',
86
+ tokenAddress: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
87
+ recipient: '0x2222222222222222222222222222222222222222',
88
+ amount: '1000000',
89
+ }, binding);
90
+ // Persist this hash with the application's logical operation before submitting.
91
+ const hash = prepared.userOpHash;
92
+ const receipt = await prepared.submit();
93
+ return { hash, receipt };
94
+ }
95
+ ```
96
+
97
+ Replace the example account and recipient with the owner-approved values. Preparation verifies the
98
+ account, chain, permission, executor, exact calldata, Smart Sessions nonce lane, native gas ceiling and
99
+ v0.7 EntryPoint hash. It refuses failed simulations, unexpected factories and paymasters. The on-chain
100
+ grant remains the authority boundary; advisory simulation is not evidence of settlement.
101
+
102
+ `submit()` signs and sends at most once for a prepared handle, including concurrent calls and failures.
103
+ It never builds a replacement automatically. Persist the hash before submission. After a timeout use
104
+ `prepared.reconcile()` or, after restarting the client, `runtime.getUserOpByHash(chainId, hash)`.
105
+ A 404 means no submission is recorded; it does not prove the network rejected it. Keep the outcome
106
+ unknown and reconcile before offering another spend. The prepared handle is process-local; durable
107
+ application idempotency across process restarts is a separate platform delivery requirement.
108
+
109
+ For a swap, use `action: 'swap'`, `tokenOutAddress`, integer `amount`, and optional `maxSlippageBps`.
110
+ Pass an `AgentSwapBinding` that also pins `routerAddress` and integer `minAmountOut`. The default
111
+ verifier decodes `swapExactIn(tokenIn, tokenOut, amountIn, minOut, to)` and checks both assets, input,
112
+ minimum receive and receiving account. For another reviewed router ABI, supply `verifyRoute(facts)`
113
+ and return true only after decoding and checking every supplied fact. Unknown router calldata is
114
+ refused without that verifier. The outer grant call still pins the router, exact allowance and zero
115
+ native value. A token address or slippage label in response metadata alone cannot authorize a swap.
116
+
117
+ API access disablement and on-chain permission revocation are separate. Disabling a registration does
118
+ not revoke a previously signed grant. Never interpret `grantStatus: unknown` as revoked authority.
119
+
120
+ From this package directory: `pnpm install --frozen-lockfile`, `pnpm check:generated`, `pnpm typecheck`,
121
+ `pnpm test`, and `pnpm test:packed`. Install the independent SDK package before the packed check.
122
+ The optional `BOTANARY_VERIFY_TOOLS=1 pnpm test:packed` also packs MCP and CLI and drives a CLI agent
123
+ read through the extracted runtime against a loopback HTTP fixture. Install those packages first.
@@ -0,0 +1,49 @@
1
+ /** Thrown for any non-2xx response, carrying the HTTP status alongside the SERVER's own message (never a
2
+ * generic one - whoever built this agent needs the reason, §6-40 one layer up from the chain). The
3
+ * status is what lets `AgentRuntime` tell "this session token is stale, re-mint and retry" (401) apart
4
+ * from every other failure, which it should surface as-is. */
5
+ export declare class BotanaryApiError extends Error {
6
+ readonly status: number;
7
+ /**
8
+ * The backend's own `error.details` blob, carried through rather than flattened into the message.
9
+ * Some refusals are ACTIONABLE and say so in structured fields - `missingModule` names a setup module
10
+ * the account lacks - and a tool that can only read prose has to pattern-match on wording to notice.
11
+ * Keeping the object means a handler keys off a field the backend guarantees instead of a sentence it
12
+ * is free to reword. Null whenever the response carried none.
13
+ */
14
+ readonly details: Record<string, unknown> | null;
15
+ /**
16
+ * The `Retry-After` header's value in SECONDS, when the server sent one (429/503 carry it). Null
17
+ * otherwise. Kept as a first-class field because a caller that wants to survive a rate limit -
18
+ * `wallet/login.ts`'s poll loop is the one that must - cannot see response headers otherwise, and
19
+ * guessing a backoff when the server told you the exact number is how a retry storm starts.
20
+ * Only the numeric (delta-seconds) form is parsed; the HTTP-date form yields null, and the caller
21
+ * falls back to its own backoff rather than mis-parsing a date as a duration.
22
+ */
23
+ readonly retryAfterSeconds: number | null;
24
+ requestId?: string;
25
+ code?: string;
26
+ operationId?: string;
27
+ userOpHash?: string;
28
+ requirementId?: string;
29
+ ambiguous: boolean;
30
+ constructor(message: string, status: number,
31
+ /**
32
+ * The backend's own `error.details` blob, carried through rather than flattened into the message.
33
+ * Some refusals are ACTIONABLE and say so in structured fields - `missingModule` names a setup module
34
+ * the account lacks - and a tool that can only read prose has to pattern-match on wording to notice.
35
+ * Keeping the object means a handler keys off a field the backend guarantees instead of a sentence it
36
+ * is free to reword. Null whenever the response carried none.
37
+ */
38
+ details?: Record<string, unknown> | null,
39
+ /**
40
+ * The `Retry-After` header's value in SECONDS, when the server sent one (429/503 carry it). Null
41
+ * otherwise. Kept as a first-class field because a caller that wants to survive a rate limit -
42
+ * `wallet/login.ts`'s poll loop is the one that must - cannot see response headers otherwise, and
43
+ * guessing a backoff when the server told you the exact number is how a retry storm starts.
44
+ * Only the numeric (delta-seconds) form is parsed; the HTTP-date form yields null, and the caller
45
+ * falls back to its own backoff rather than mis-parsing a date as a duration.
46
+ */
47
+ retryAfterSeconds?: number | null);
48
+ }
49
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;+DAG+D;AAC/D,qBAAa,gBAAiB,SAAQ,KAAK;IASvC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAChD;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI;IAzB3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,UAAS;gBAEhB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;IACvB;;;;;;OAMG;IACM,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAW;IACvD;;;;;;;OAOG;IACM,iBAAiB,GAAE,MAAM,GAAG,IAAW;CAKnD"}
package/dist/errors.js ADDED
@@ -0,0 +1,40 @@
1
+ /** Thrown for any non-2xx response, carrying the HTTP status alongside the SERVER's own message (never a
2
+ * generic one - whoever built this agent needs the reason, §6-40 one layer up from the chain). The
3
+ * status is what lets `AgentRuntime` tell "this session token is stale, re-mint and retry" (401) apart
4
+ * from every other failure, which it should surface as-is. */
5
+ export class BotanaryApiError extends Error {
6
+ status;
7
+ details;
8
+ retryAfterSeconds;
9
+ requestId;
10
+ code;
11
+ operationId;
12
+ userOpHash;
13
+ requirementId;
14
+ ambiguous = false;
15
+ constructor(message, status,
16
+ /**
17
+ * The backend's own `error.details` blob, carried through rather than flattened into the message.
18
+ * Some refusals are ACTIONABLE and say so in structured fields - `missingModule` names a setup module
19
+ * the account lacks - and a tool that can only read prose has to pattern-match on wording to notice.
20
+ * Keeping the object means a handler keys off a field the backend guarantees instead of a sentence it
21
+ * is free to reword. Null whenever the response carried none.
22
+ */
23
+ details = null,
24
+ /**
25
+ * The `Retry-After` header's value in SECONDS, when the server sent one (429/503 carry it). Null
26
+ * otherwise. Kept as a first-class field because a caller that wants to survive a rate limit -
27
+ * `wallet/login.ts`'s poll loop is the one that must - cannot see response headers otherwise, and
28
+ * guessing a backoff when the server told you the exact number is how a retry storm starts.
29
+ * Only the numeric (delta-seconds) form is parsed; the HTTP-date form yields null, and the caller
30
+ * falls back to its own backoff rather than mis-parsing a date as a duration.
31
+ */
32
+ retryAfterSeconds = null) {
33
+ super(message);
34
+ this.status = status;
35
+ this.details = details;
36
+ this.retryAfterSeconds = retryAfterSeconds;
37
+ this.name = 'BotanaryApiError';
38
+ }
39
+ }
40
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;+DAG+D;AAC/D,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAS9B;IAQA;IASA;IAzBX,SAAS,CAAU;IACnB,IAAI,CAAU;IACd,WAAW,CAAU;IACrB,UAAU,CAAU;IACpB,aAAa,CAAU;IACvB,SAAS,GAAG,KAAK,CAAC;IAClB,YACE,OAAe,EACN,MAAc;IACvB;;;;;;OAMG;IACM,UAA0C,IAAI;IACvD;;;;;;;OAOG;IACM,oBAAmC,IAAI;QAEhD,KAAK,CAAC,OAAO,CAAC,CAAC;QAnBN,WAAM,GAAN,MAAM,CAAQ;QAQd,YAAO,GAAP,OAAO,CAAuC;QAS9C,sBAAiB,GAAjB,iBAAiB,CAAsB;QAGhD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,48 @@
1
+ import { type Hex } from 'viem';
2
+ import type { components } from './generated/schema.js';
3
+ import type { UnsignedUserOp } from './views.js';
4
+ export type ExactAgentAction = components['schemas']['ExactDelegatedActionInput'];
5
+ export type ExactAgentExecution = components['schemas']['ExactDelegatedExecution'];
6
+ export interface AgentActionBinding {
7
+ delegationId: string;
8
+ permissionId: Hex;
9
+ executorAddress: Hex;
10
+ /** Maximum native gas cost for this operation, in wei. */
11
+ maxGasCostWei: string;
12
+ }
13
+ export interface SwapRouteFacts {
14
+ readonly chainId: number;
15
+ readonly accountAddress: Hex;
16
+ readonly routerAddress: Hex;
17
+ readonly tokenAddress: Hex;
18
+ readonly tokenOutAddress: Hex;
19
+ readonly amount: string;
20
+ readonly minAmountOut: string;
21
+ readonly callData: Hex;
22
+ }
23
+ export interface AgentSwapBinding extends AgentActionBinding {
24
+ routerAddress: Hex;
25
+ minAmountOut: string;
26
+ /** For a reviewed router ABI other than swapExactIn. Must verify all supplied facts. */
27
+ verifyRoute?: (facts: Readonly<SwapRouteFacts>) => boolean | Promise<boolean>;
28
+ }
29
+ export interface PreparedAgentAction {
30
+ readonly userOpHash: Hex;
31
+ readonly execution: Readonly<ExactAgentExecution>;
32
+ /** One attempt per prepared action, including concurrent calls and transport failures. */
33
+ submit(): Promise<Record<string, unknown>>;
34
+ /** Read by the original hash. A 404 is still an unknown network outcome. */
35
+ reconcile(): Promise<Record<string, unknown>>;
36
+ }
37
+ /** Copy and validate before awaiting transport, so caller mutations cannot change the signed intent. */
38
+ export declare function exactActionInput(value: ExactAgentAction): ExactAgentAction;
39
+ export declare function actionBinding(value: AgentActionBinding | AgentSwapBinding, action: ExactAgentAction): AgentActionBinding | AgentSwapBinding;
40
+ /** Validate the actual call and recompute its chain-bound v0.7 hash before handing it to a signer. */
41
+ export declare function validateExactAction(value: unknown, input: ExactAgentAction, binding: AgentActionBinding | AgentSwapBinding): Promise<{
42
+ userOp: UnsignedUserOp;
43
+ userOpHash: Hex;
44
+ execution: Readonly<ExactAgentExecution>;
45
+ }>;
46
+ /** Receipt validation happens after submission, so failure must retain the reconciliation handle. */
47
+ export declare function exactActionReceipt(value: unknown, hash: Hex): Record<string, unknown>;
48
+ //# sourceMappingURL=exact-action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact-action.d.ts","sourceRoot":"","sources":["../src/exact-action.ts"],"names":[],"mappings":"AACA,OAAO,EAAqG,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAGnI,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,2BAA2B,CAAC,CAAC;AAClF,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,yBAAyB,CAAC,CAAC;AACnF,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,GAAG,CAAC;IAClB,eAAe,EAAE,GAAG,CAAC;IACrB,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;CACvB;AACD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,GAAG,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,GAAG,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC;CACxB;AACD,MAAM,WAAW,gBAAiB,SAAQ,kBAAkB;IAC1D,aAAa,EAAE,GAAG,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,wFAAwF;IACxF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/E;AACD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IAClD,0FAA0F;IAC1F,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C,4EAA4E;IAC5E,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC/C;AAkBD,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CAkB1E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,GAAG,gBAAgB,CAU3I;AAED,sGAAsG;AACtG,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,OAAO,CAAC;IAC1I,MAAM,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,GAAG,CAAC;IAAC,SAAS,EAAE,QAAQ,CAAC,mBAAmB,CAAC,CAAC;CACnF,CAAC,CA0ED;AAED,qGAAqG;AACrG,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYrF"}
@@ -0,0 +1,178 @@
1
+ import { BotanaryApiError } from './errors.js';
2
+ import { encodeFunctionData, encodePacked, decodeFunctionData, parseAbi, getAddress, zeroAddress, zeroHash } from 'viem';
3
+ import { entryPoint07Address, getUserOperationHash } from 'viem/account-abstraction';
4
+ import { hex, invalidResponse, record, resourceId, unsignedUserOp, userOpQuantity } from './validation.js';
5
+ const kernelAbi = parseAbi(['function execute(bytes32 mode, bytes executionCalldata) payable']);
6
+ const grantAbi = parseAbi([
7
+ 'function executeUnderGrant(bytes32 permissionId, address target, uint256 value, bytes callData)',
8
+ 'function executeUnderGrantWithAllowance(bytes32 permissionId, address target, uint256 value, address token, uint256 allowance, bytes callData)',
9
+ ]);
10
+ const transferAbi = parseAbi(['function transfer(address to, uint256 amount) returns (bool)']);
11
+ const swapAbi = parseAbi(['function swapExactIn(address tokenIn, address tokenOut, uint256 amountIn, uint256 minOut, address to) returns (uint256 amountOut)']);
12
+ const sessionKey = BigInt('0x000100000000008bdaba73cd9815d79069c247eb4bda0000');
13
+ function same(left, right) { return typeof left === 'string' && left.toLowerCase() === right.toLowerCase(); }
14
+ function integer(value, positive = true) {
15
+ if (typeof value !== 'string' || !/^(0|[1-9][0-9]{0,77})$/.test(value))
16
+ return invalidResponse();
17
+ const result = BigInt(value);
18
+ if (result >= 2n ** 256n || (positive && result === 0n))
19
+ return invalidResponse();
20
+ return result;
21
+ }
22
+ /** Copy and validate before awaiting transport, so caller mutations cannot change the signed intent. */
23
+ export function exactActionInput(value) {
24
+ const input = record(value);
25
+ const allowed = ['accountId', 'accountAddress', 'chainId', 'action', 'tokenAddress', 'amount', 'recipient', 'tokenOutAddress', 'maxSlippageBps'];
26
+ if (Object.keys(input).some(key => !allowed.includes(key)))
27
+ invalidResponse();
28
+ resourceId(input.accountId);
29
+ hex(input.accountAddress, 20);
30
+ hex(input.tokenAddress, 20);
31
+ integer(input.amount);
32
+ if (!Number.isSafeInteger(input.chainId) || Number(input.chainId) <= 0)
33
+ invalidResponse();
34
+ if (input.action === 'transfer') {
35
+ hex(input.recipient, 20);
36
+ if (input.tokenOutAddress !== undefined || input.maxSlippageBps !== undefined)
37
+ invalidResponse();
38
+ }
39
+ else if (input.action === 'swap') {
40
+ hex(input.tokenOutAddress, 20);
41
+ if (input.recipient !== undefined || (input.maxSlippageBps !== undefined &&
42
+ (!Number.isInteger(input.maxSlippageBps) || Number(input.maxSlippageBps) < 1 || Number(input.maxSlippageBps) > 1000)))
43
+ invalidResponse();
44
+ }
45
+ else
46
+ invalidResponse();
47
+ return Object.freeze({ ...value, accountAddress: getAddress(value.accountAddress), tokenAddress: getAddress(value.tokenAddress),
48
+ ...(value.recipient !== undefined ? { recipient: getAddress(value.recipient) } : {}),
49
+ ...(value.tokenOutAddress !== undefined ? { tokenOutAddress: getAddress(value.tokenOutAddress) } : {}),
50
+ });
51
+ }
52
+ export function actionBinding(value, action) {
53
+ resourceId(value.delegationId);
54
+ hex(value.permissionId, 32);
55
+ hex(value.executorAddress, 20);
56
+ integer(value.maxGasCostWei);
57
+ if (action.action === 'swap') {
58
+ const swap = value;
59
+ hex(swap.routerAddress, 20);
60
+ integer(swap.minAmountOut);
61
+ if (swap.verifyRoute !== undefined && typeof swap.verifyRoute !== 'function')
62
+ invalidResponse();
63
+ }
64
+ return Object.freeze({ ...value, executorAddress: getAddress(value.executorAddress),
65
+ ...(action.action === 'swap' ? { routerAddress: getAddress(value.routerAddress) } : {}),
66
+ });
67
+ }
68
+ /** Validate the actual call and recompute its chain-bound v0.7 hash before handing it to a signer. */
69
+ export async function validateExactAction(value, input, binding) {
70
+ try {
71
+ const build = record(value);
72
+ const context = record(build.execution);
73
+ const wire = unsignedUserOp(build.userOp);
74
+ const op = Object.freeze({ ...wire, sender: getAddress(wire.sender), entryPoint: getAddress(wire.entryPoint) });
75
+ hex(build.userOpHash, 32);
76
+ if (build.intentType !== 'delegated_action' || record(build.simulation).willSucceed !== true ||
77
+ context.accountId !== input.accountId || !same(context.accountAddress, input.accountAddress) ||
78
+ context.chainId !== input.chainId || context.action !== input.action || context.amount !== input.amount ||
79
+ !same(context.tokenAddress, input.tokenAddress) || context.delegationId !== binding.delegationId ||
80
+ !same(context.permissionId, binding.permissionId) || context.gasMethod !== 'native' ||
81
+ !same(op.sender, input.accountAddress) || op.chainId !== input.chainId || !same(op.entryPoint, entryPoint07Address) ||
82
+ op.factory != null || op.factoryData != null || op.paymaster != null || op.paymasterData != null ||
83
+ op.paymasterVerificationGasLimit != null || op.paymasterPostOpGasLimit != null)
84
+ invalidResponse();
85
+ const nonce = userOpQuantity(op.nonce);
86
+ if (nonce >> 64n !== sessionKey)
87
+ invalidResponse();
88
+ const callGasLimit = userOpQuantity(op.callGasLimit, 128);
89
+ const verificationGasLimit = userOpQuantity(op.verificationGasLimit, 128);
90
+ const preVerificationGas = userOpQuantity(op.preVerificationGas);
91
+ const maxFeePerGas = userOpQuantity(op.maxFeePerGas, 128);
92
+ const maxPriorityFeePerGas = userOpQuantity(op.maxPriorityFeePerGas, 128);
93
+ if (maxPriorityFeePerGas > maxFeePerGas ||
94
+ (callGasLimit + verificationGasLimit + preVerificationGas) * maxFeePerGas > integer(binding.maxGasCostWei))
95
+ invalidResponse();
96
+ // Smart Sessions requires raw Kernel execute, without an owner executeUserOp wrapper.
97
+ const rawCallData = op.callData;
98
+ let inner;
99
+ if (input.action === 'transfer') {
100
+ if (!same(context.recipient, input.recipient) || context.tokenOutAddress !== undefined || context.maxSlippageBps !== undefined)
101
+ invalidResponse();
102
+ const native = same(input.tokenAddress, zeroAddress);
103
+ inner = encodeFunctionData({ abi: grantAbi, functionName: 'executeUnderGrant', args: [binding.permissionId,
104
+ (native ? input.recipient : input.tokenAddress), native ? integer(input.amount) : 0n,
105
+ native ? '0x' : encodeFunctionData({ abi: transferAbi, functionName: 'transfer', args: [input.recipient, integer(input.amount)] }),
106
+ ] });
107
+ }
108
+ else {
109
+ const swap = binding;
110
+ if (!same(context.tokenOutAddress, input.tokenOutAddress) || context.recipient !== undefined ||
111
+ context.maxSlippageBps !== (input.maxSlippageBps ?? 50))
112
+ invalidResponse();
113
+ const kernel = decodeFunctionData({ abi: kernelAbi, data: rawCallData });
114
+ const packed = kernel.args[1];
115
+ const decoded = decodeFunctionData({ abi: grantAbi, data: `0x${packed.slice(106)}` });
116
+ if (decoded.functionName !== 'executeUnderGrantWithAllowance')
117
+ invalidResponse();
118
+ const routeData = decoded.args[5];
119
+ const facts = Object.freeze({ chainId: input.chainId, accountAddress: input.accountAddress,
120
+ routerAddress: swap.routerAddress, tokenAddress: input.tokenAddress, tokenOutAddress: input.tokenOutAddress,
121
+ amount: input.amount, minAmountOut: swap.minAmountOut, callData: routeData });
122
+ if (swap.verifyRoute) {
123
+ if (await swap.verifyRoute(facts) !== true)
124
+ invalidResponse();
125
+ }
126
+ else {
127
+ const route = decodeFunctionData({ abi: swapAbi, data: routeData });
128
+ const [from, to, amount, minOut, recipient] = route.args;
129
+ if (!same(from, input.tokenAddress) || !same(to, input.tokenOutAddress) || amount !== integer(input.amount) ||
130
+ minOut < integer(swap.minAmountOut) || !same(recipient, input.accountAddress) ||
131
+ !same(encodeFunctionData({ abi: swapAbi, functionName: 'swapExactIn', args: route.args }), routeData))
132
+ invalidResponse();
133
+ }
134
+ inner = encodeFunctionData({ abi: grantAbi, functionName: 'executeUnderGrantWithAllowance', args: [binding.permissionId,
135
+ swap.routerAddress, 0n, input.tokenAddress, integer(input.amount), routeData] });
136
+ }
137
+ const expected = encodeFunctionData({ abi: kernelAbi, functionName: 'execute', args: [zeroHash,
138
+ encodePacked(['address', 'uint256', 'bytes'], [binding.executorAddress, 0n, inner])] });
139
+ if (!same(rawCallData, expected))
140
+ invalidResponse();
141
+ const hash = getUserOperationHash({ entryPointAddress: entryPoint07Address, entryPointVersion: '0.7', chainId: input.chainId,
142
+ userOperation: { sender: op.sender, nonce, callData: op.callData, callGasLimit, verificationGasLimit, preVerificationGas,
143
+ maxFeePerGas, maxPriorityFeePerGas, signature: '0x' } });
144
+ if (!same(hash, build.userOpHash))
145
+ invalidResponse();
146
+ const execution = Object.freeze({ ...input, delegationId: binding.delegationId, permissionId: binding.permissionId,
147
+ gasMethod: 'native', ...(input.action === 'swap' ? { maxSlippageBps: input.maxSlippageBps ?? 50 } : {}) });
148
+ // Whitelist fields instead of forwarding extra server-controlled authorization extensions.
149
+ const userOp = Object.freeze({ sender: op.sender, chainId: op.chainId, entryPoint: op.entryPoint,
150
+ nonce: op.nonce, callData: op.callData, callGasLimit: op.callGasLimit, verificationGasLimit: op.verificationGasLimit,
151
+ preVerificationGas: op.preVerificationGas, maxFeePerGas: op.maxFeePerGas, maxPriorityFeePerGas: op.maxPriorityFeePerGas,
152
+ signature: '0x' });
153
+ return { userOp, userOpHash: hash, execution };
154
+ }
155
+ catch {
156
+ return invalidResponse();
157
+ }
158
+ }
159
+ /** Receipt validation happens after submission, so failure must retain the reconciliation handle. */
160
+ export function exactActionReceipt(value, hash) {
161
+ try {
162
+ const receipt = record(value);
163
+ resourceId(receipt.id);
164
+ if (!same(receipt.userOpHash, hash) || !['pending', 'included', 'failed'].includes(String(receipt.status)))
165
+ throw new Error();
166
+ if (receipt.txHash != null)
167
+ hex(receipt.txHash, 32);
168
+ return receipt;
169
+ }
170
+ catch {
171
+ const error = new BotanaryApiError('Botanary returned an invalid receipt. Reconcile using the original operation hash.', 0);
172
+ error.code = 'invalid_response';
173
+ error.userOpHash = hash;
174
+ error.ambiguous = true;
175
+ throw error;
176
+ }
177
+ }
178
+ //# sourceMappingURL=exact-action.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact-action.js","sourceRoot":"","sources":["../src/exact-action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAY,MAAM,MAAM,CAAC;AACnI,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAqC3G,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,iEAAiE,CAAC,CAAC,CAAC;AAChG,MAAM,QAAQ,GAAG,QAAQ,CAAC;IACxB,iGAAiG;IACjG,gJAAgJ;CACjJ,CAAC,CAAC;AACH,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,8DAA8D,CAAC,CAAC,CAAC;AAC/F,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,mIAAmI,CAAC,CAAC,CAAC;AAChK,MAAM,UAAU,GAAG,MAAM,CAAC,oDAAoD,CAAC,CAAC;AAChF,SAAS,IAAI,CAAC,IAAa,EAAE,KAAa,IAAa,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AACvI,SAAS,OAAO,CAAC,KAAc,EAAE,QAAQ,GAAG,IAAI;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,eAAe,EAAE,CAAC;IACjG,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAI,MAAM,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,KAAK,EAAE,CAAC;QAAE,OAAO,eAAe,EAAE,CAAC;IAClF,OAAO,MAAM,CAAC;AAChB,CAAC;AAGD,wGAAwG;AACxG,MAAM,UAAU,gBAAgB,CAAC,KAAuB;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,WAAW,EAAE,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;IACjJ,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,eAAe,EAAE,CAAC;IAC9E,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAAC,GAAG,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAAC,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/G,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,eAAe,EAAE,CAAC;IAC1F,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAChC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACzB,IAAI,KAAK,CAAC,eAAe,KAAK,SAAS,IAAI,KAAK,CAAC,cAAc,KAAK,SAAS;YAAE,eAAe,EAAE,CAAC;IACnG,CAAC;SAAM,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QACnC,GAAG,CAAC,KAAK,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS;YACtE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC,CAAC;YAAE,eAAe,EAAE,CAAC;IAC7I,CAAC;;QAAM,eAAe,EAAE,CAAC;IACzB,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,UAAU,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,YAAY,EAAE,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC;QAC7H,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpF,GAAG,CAAC,KAAK,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvG,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAA4C,EAAE,MAAwB;IAClG,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAAC,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAAC,GAAG,CAAC,KAAK,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IAC1H,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,KAAyB,CAAC;QACvC,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACxD,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,WAAW,KAAK,UAAU;YAAE,eAAe,EAAE,CAAC;IAClG,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,EAAE,eAAe,EAAE,UAAU,CAAC,KAAK,CAAC,eAAe,CAAC;QACjF,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,CAAE,KAA0B,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9G,CAAC,CAAC;AACL,CAAC;AAED,sGAAsG;AACtG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,KAAc,EAAE,KAAuB,EAAE,OAA8C;IAG/H,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAChH,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC1B,IAAI,KAAK,CAAC,UAAU,KAAK,kBAAkB,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,WAAW,KAAK,IAAI;YAC1F,OAAO,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC,cAAc,CAAC;YAC5F,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;YACvG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,YAAY;YAChG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,SAAS,KAAK,QAAQ;YACnF,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,CAAC;YACnH,EAAE,CAAC,OAAO,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,IAAI,IAAI,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,aAAa,IAAI,IAAI;YAChG,EAAE,CAAC,6BAA6B,IAAI,IAAI,IAAI,EAAE,CAAC,uBAAuB,IAAI,IAAI;YAAE,eAAe,EAAE,CAAC;QACpG,MAAM,KAAK,GAAG,cAAc,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,KAAK,IAAI,GAAG,KAAK,UAAU;YAAE,eAAe,EAAE,CAAC;QACnD,MAAM,YAAY,GAAG,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QAC1D,MAAM,oBAAoB,GAAG,cAAc,CAAC,EAAE,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC;QAC1E,MAAM,kBAAkB,GAAG,cAAc,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC;QACjE,MAAM,YAAY,GAAG,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QAC1D,MAAM,oBAAoB,GAAG,cAAc,CAAC,EAAE,CAAC,oBAAoB,EAAE,GAAG,CAAC,CAAC;QAC1E,IAAI,oBAAoB,GAAG,YAAY;YACrC,CAAC,YAAY,GAAG,oBAAoB,GAAG,kBAAkB,CAAC,GAAG,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;YAAE,eAAe,EAAE,CAAC;QAChI,sFAAsF;QACtF,MAAM,WAAW,GAAG,EAAE,CAAC,QAAQ,CAAC;QAChC,IAAI,KAAU,CAAC;QACf,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,SAAU,CAAC,IAAI,OAAO,CAAC,eAAe,KAAK,SAAS,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS;gBAAE,eAAe,EAAE,CAAC;YACnJ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;YACrD,KAAK,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY;oBACxG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE;oBAC3F,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,SAAgB,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;iBAC1I,EAAE,CAAC,CAAC;QACP,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,OAA2B,CAAC;YACzC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,eAAgB,CAAC,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS;gBAC3F,OAAO,CAAC,cAAc,KAAK,CAAC,KAAK,CAAC,cAAc,IAAI,EAAE,CAAC;gBAAE,eAAe,EAAE,CAAC;YAC7E,MAAM,MAAM,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;YACzE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC9B,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YACtF,IAAI,OAAO,CAAC,YAAY,KAAK,gCAAgC;gBAAE,eAAe,EAAE,CAAC;YACjF,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,KAAK,GAAmB,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,cAAqB;gBAC/G,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,EAAE,KAAK,CAAC,YAAmB,EAAE,eAAe,EAAE,KAAK,CAAC,eAAsB;gBACzH,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;YAChF,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACrB,IAAI,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,IAAI;oBAAE,eAAe,EAAE,CAAC;YAChE,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;gBACpE,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;gBACzD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,eAAgB,CAAC,IAAI,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;oBAC1G,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,cAAc,CAAC;oBAC7E,CAAC,IAAI,CAAC,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,SAAS,CAAC;oBAAE,eAAe,EAAE,CAAC;YAC7H,CAAC;YACD,KAAK,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,gCAAgC,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY;oBACrH,IAAI,CAAC,aAAa,EAAE,EAAE,EAAE,KAAK,CAAC,YAAmB,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,QAAQ;gBAC5F,YAAY,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1F,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC;YAAE,eAAe,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,oBAAoB,CAAC,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO;YAC1H,aAAa,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB;gBACtH,YAAY,EAAE,oBAAoB,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC;YAAE,eAAe,EAAE,CAAC;QACrD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY;YAChH,SAAS,EAAE,QAAiB,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACtH,2FAA2F;QAC3F,MAAM,MAAM,GAAmB,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,CAAC,UAAU;YAC9G,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,CAAC,YAAY,EAAE,oBAAoB,EAAE,EAAE,CAAC,oBAAoB;YACpH,kBAAkB,EAAE,EAAE,CAAC,kBAAkB,EAAE,YAAY,EAAE,EAAE,CAAC,YAAY,EAAE,oBAAoB,EAAE,EAAE,CAAC,oBAAoB;YACvH,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,eAAe,EAAE,CAAC;IAAC,CAAC;AACvC,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,kBAAkB,CAAC,KAAc,EAAE,IAAS;IAC1D,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9B,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,EAAE,CAAC;QAC9H,IAAI,OAAO,CAAC,MAAM,IAAI,IAAI;YAAE,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACpD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,KAAK,GAAG,IAAI,gBAAgB,CAAC,oFAAoF,EAAE,CAAC,CAAC,CAAC;QAC5H,KAAK,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC;QACjF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Hex } from 'viem';
2
+ /** Encode `bytes` as Crockford Base32. Not meant to round-trip (no decoder here, and slicing the
3
+ * result to a shorter display length - see below - deliberately drops the tail) - this is a display
4
+ * transform for short human-facing codes, not a data-interchange encoding. */
5
+ export declare function crockfordBase32(bytes: Uint8Array): string;
6
+ /** Bytes of address fingerprint / pairing nonce - chosen for "short to type" over "cryptographically
7
+ * weighty", since neither is a secret (see README's "why intercepting the code alone grants nothing").
8
+ * FINGERPRINT_BYTES: ~24 bits, purely a human recognition aid - the full address is always available
9
+ * via get_identity for exact verification, so collisions here cost nothing but a moment's "hm, two
10
+ * agents look similar", not a security property.
11
+ * NONCE_BYTES: ~40 bits of fresh randomness per pairing code, the thing that actually makes each code
12
+ * single-use in practice (combined with the short TTL - see pairing-code.ts). */
13
+ export declare const FINGERPRINT_BYTES = 3;
14
+ export declare const NONCE_BYTES = 5;
15
+ /** A short, deterministic, human-typable id for an address - always the same for the same address.
16
+ * Purely a display/recognition aid (see module doc); the address itself is the real identifier. */
17
+ export declare function deriveFingerprint(address: Hex): string;
18
+ /** A fresh random nonce for one pairing code. Never derived from the key - pure randomness, so a new
19
+ * call can never be predicted from a previous one. */
20
+ export declare function generatePairingNonce(): string;
21
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAOhC;;+EAE+E;AAC/E,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAgBzD;AAED;;;;;;kFAMkF;AAClF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B;oGACoG;AACpG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAGtD;AAED;uDACuD;AACvD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C"}
@@ -0,0 +1,47 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { hexToBytes } from 'viem';
3
+ // Crockford Base32: 32 symbols, excludes I/L/O/U so nothing is ambiguous when read aloud or typed by
4
+ // hand (no risk of confusing 0/O, 1/I/L), case-insensitive by convention. This is the same alphabet
5
+ // family used by ULIDs and plenty of other human-facing codes for exactly this reason.
6
+ const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
7
+ /** Encode `bytes` as Crockford Base32. Not meant to round-trip (no decoder here, and slicing the
8
+ * result to a shorter display length - see below - deliberately drops the tail) - this is a display
9
+ * transform for short human-facing codes, not a data-interchange encoding. */
10
+ export function crockfordBase32(bytes) {
11
+ let bitBuffer = 0;
12
+ let bitCount = 0;
13
+ let output = '';
14
+ for (const byte of bytes) {
15
+ bitBuffer = (bitBuffer << 8) | byte;
16
+ bitCount += 8;
17
+ while (bitCount >= 5) {
18
+ output += ALPHABET[(bitBuffer >>> (bitCount - 5)) & 0x1f];
19
+ bitCount -= 5;
20
+ }
21
+ }
22
+ if (bitCount > 0) {
23
+ output += ALPHABET[(bitBuffer << (5 - bitCount)) & 0x1f];
24
+ }
25
+ return output;
26
+ }
27
+ /** Bytes of address fingerprint / pairing nonce - chosen for "short to type" over "cryptographically
28
+ * weighty", since neither is a secret (see README's "why intercepting the code alone grants nothing").
29
+ * FINGERPRINT_BYTES: ~24 bits, purely a human recognition aid - the full address is always available
30
+ * via get_identity for exact verification, so collisions here cost nothing but a moment's "hm, two
31
+ * agents look similar", not a security property.
32
+ * NONCE_BYTES: ~40 bits of fresh randomness per pairing code, the thing that actually makes each code
33
+ * single-use in practice (combined with the short TTL - see pairing-code.ts). */
34
+ export const FINGERPRINT_BYTES = 3;
35
+ export const NONCE_BYTES = 5;
36
+ /** A short, deterministic, human-typable id for an address - always the same for the same address.
37
+ * Purely a display/recognition aid (see module doc); the address itself is the real identifier. */
38
+ export function deriveFingerprint(address) {
39
+ const bytes = hexToBytes(address);
40
+ return crockfordBase32(bytes.slice(0, FINGERPRINT_BYTES));
41
+ }
42
+ /** A fresh random nonce for one pairing code. Never derived from the key - pure randomness, so a new
43
+ * call can never be predicted from a previous one. */
44
+ export function generatePairingNonce() {
45
+ return crockfordBase32(randomBytes(NONCE_BYTES));
46
+ }
47
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAGlC,qGAAqG;AACrG,oGAAoG;AACpG,uFAAuF;AACvF,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD;;+EAE+E;AAC/E,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,SAAS,GAAG,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QACpC,QAAQ,IAAI,CAAC,CAAC;QACd,OAAO,QAAQ,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,QAAQ,CAAC,CAAC,SAAS,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YAC1D,QAAQ,IAAI,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IACD,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,QAAQ,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;kFAMkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B;oGACoG;AACpG,MAAM,UAAU,iBAAiB,CAAC,OAAY;IAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;uDACuD;AACvD,MAAM,UAAU,oBAAoB;IAClC,OAAO,eAAe,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC;AACnD,CAAC"}