ethagent 1.1.2 → 2.0.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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +124 -32
  3. package/package.json +8 -3
  4. package/src/app/FirstRun.tsx +190 -146
  5. package/src/app/FirstRunTimeline.tsx +47 -0
  6. package/src/app/input/AppInputProvider.tsx +1 -1
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -1
  8. package/src/chat/ChatBottomPane.tsx +0 -1
  9. package/src/chat/ChatInput.tsx +6 -6
  10. package/src/chat/ChatScreen.tsx +35 -15
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +10 -22
  13. package/src/chat/CopyPicker.tsx +0 -1
  14. package/src/chat/MessageList.tsx +62 -45
  15. package/src/chat/PermissionPrompt.tsx +13 -9
  16. package/src/chat/PlanApprovalView.tsx +3 -3
  17. package/src/chat/ResumeView.tsx +1 -4
  18. package/src/chat/RewindView.tsx +2 -2
  19. package/src/chat/chatInputState.ts +1 -1
  20. package/src/chat/chatScreenUtils.ts +22 -11
  21. package/src/chat/chatSessionState.ts +2 -2
  22. package/src/chat/chatTurnOrchestrator.ts +16 -81
  23. package/src/chat/commands.ts +1 -1
  24. package/src/chat/textCursor.ts +1 -1
  25. package/src/chat/transcriptViewport.ts +2 -7
  26. package/src/cli/ResetConfirmView.tsx +1 -1
  27. package/src/cli/main.tsx +9 -3
  28. package/src/cli/preview.tsx +0 -5
  29. package/src/cli/updateNotice.ts +4 -2
  30. package/src/identity/continuity/editor.ts +7 -107
  31. package/src/identity/continuity/envelope.ts +1048 -40
  32. package/src/identity/continuity/history.ts +4 -4
  33. package/src/identity/continuity/localBackup.ts +249 -0
  34. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  35. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  36. package/src/identity/continuity/privateEdit/files.ts +23 -0
  37. package/src/identity/continuity/privateEdit/types.ts +28 -0
  38. package/src/identity/continuity/privateEdit.ts +10 -298
  39. package/src/identity/continuity/publicSkills.ts +8 -9
  40. package/src/identity/continuity/snapshots.ts +17 -6
  41. package/src/identity/continuity/storage/defaults.ts +111 -0
  42. package/src/identity/continuity/storage/files.ts +72 -0
  43. package/src/identity/continuity/storage/markdown.ts +81 -0
  44. package/src/identity/continuity/storage/paths.ts +24 -0
  45. package/src/identity/continuity/storage/scaffold.ts +124 -0
  46. package/src/identity/continuity/storage/status.ts +86 -0
  47. package/src/identity/continuity/storage/types.ts +27 -0
  48. package/src/identity/continuity/storage.ts +32 -507
  49. package/src/identity/continuity/zipWriter.ts +95 -0
  50. package/src/identity/crypto/backupEnvelope.ts +14 -247
  51. package/src/identity/crypto/eth.ts +7 -7
  52. package/src/identity/ens/agentRecords.ts +96 -0
  53. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  54. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  55. package/src/identity/ens/ensAutomation/names.ts +14 -0
  56. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  57. package/src/identity/ens/ensAutomation/read.ts +114 -0
  58. package/src/identity/ens/ensAutomation/root.ts +63 -0
  59. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  60. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  61. package/src/identity/ens/ensAutomation/types.ts +126 -0
  62. package/src/identity/ens/ensAutomation.ts +29 -0
  63. package/src/identity/ens/ensLookup/client.ts +43 -0
  64. package/src/identity/ens/ensLookup/constants.ts +26 -0
  65. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  66. package/src/identity/ens/ensLookup/names.ts +34 -0
  67. package/src/identity/ens/ensLookup/records.ts +45 -0
  68. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  69. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  70. package/src/identity/ens/ensLookup/types.ts +38 -0
  71. package/src/identity/ens/ensLookup/validation.ts +72 -0
  72. package/src/identity/ens/ensLookup.ts +19 -0
  73. package/src/identity/ens/ensRegistration.ts +199 -0
  74. package/src/identity/ens/resolverDelegation.ts +48 -0
  75. package/src/identity/hub/IdentityHub.tsx +13 -817
  76. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  77. package/src/identity/hub/Routes.tsx +361 -0
  78. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  79. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  80. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  81. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  82. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  83. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  84. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  85. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  86. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  87. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  88. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  89. package/src/identity/hub/effects/create.ts +310 -0
  90. package/src/identity/hub/effects/ens/flows.ts +218 -0
  91. package/src/identity/hub/effects/ens/index.ts +11 -0
  92. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  93. package/src/identity/hub/effects/index.ts +74 -0
  94. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  95. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  96. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  97. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  98. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  99. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  100. package/src/identity/hub/effects/receipts.ts +46 -0
  101. package/src/identity/hub/effects/restore/apply.ts +112 -0
  102. package/src/identity/hub/effects/restore/auth.ts +159 -0
  103. package/src/identity/hub/effects/restore/discover.ts +86 -0
  104. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  105. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  106. package/src/identity/hub/effects/restore/index.ts +22 -0
  107. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  108. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  109. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  110. package/src/identity/hub/effects/restore/shared.ts +91 -0
  111. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  112. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  113. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  114. package/src/identity/hub/effects/shared/sync.ts +190 -0
  115. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  116. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  117. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  118. package/src/identity/hub/effects/types.ts +53 -0
  119. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  120. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  121. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  122. package/src/identity/hub/{screens → flows/continuity}/RecoveryConfirmScreen.tsx +28 -19
  123. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  124. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  125. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  126. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  127. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  128. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  129. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  130. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  131. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  132. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  133. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  134. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  135. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  136. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  137. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  138. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  139. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  140. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  141. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  142. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  143. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  144. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  145. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  146. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  147. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +23 -44
  148. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  149. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  150. package/src/identity/hub/identityHubReducer.ts +164 -99
  151. package/src/identity/hub/model/continuity.ts +94 -0
  152. package/src/identity/hub/model/copy.ts +35 -0
  153. package/src/identity/hub/model/custody.ts +54 -0
  154. package/src/identity/hub/model/ens.ts +49 -0
  155. package/src/identity/hub/model/errors.ts +140 -0
  156. package/src/identity/hub/model/format.ts +15 -0
  157. package/src/identity/hub/model/identity.ts +94 -0
  158. package/src/identity/hub/model/network.ts +32 -0
  159. package/src/identity/hub/model/transfer.ts +57 -0
  160. package/src/identity/hub/operatorWallets.ts +131 -0
  161. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  162. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  165. package/src/identity/hub/reconciliation/index.ts +21 -0
  166. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  167. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  168. package/src/identity/hub/txGuard.ts +51 -0
  169. package/src/identity/hub/types.ts +17 -0
  170. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  171. package/src/identity/hub/useIdentityHubController.ts +396 -0
  172. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  173. package/src/identity/hub/utils.ts +79 -0
  174. package/src/identity/identityCompat.ts +34 -0
  175. package/src/identity/profile/agentIcon.ts +61 -0
  176. package/src/identity/profile/imagePicker.ts +12 -12
  177. package/src/identity/registry/erc8004/abi.ts +14 -0
  178. package/src/identity/registry/erc8004/chains.ts +150 -0
  179. package/src/identity/registry/erc8004/client.ts +11 -0
  180. package/src/identity/registry/erc8004/discovery.ts +511 -0
  181. package/src/identity/registry/erc8004/metadata.ts +335 -0
  182. package/src/identity/registry/erc8004/ownership.ts +121 -0
  183. package/src/identity/registry/erc8004/preflight.ts +123 -0
  184. package/src/identity/registry/erc8004/transactions.ts +77 -0
  185. package/src/identity/registry/erc8004/types.ts +88 -0
  186. package/src/identity/registry/erc8004/uri.ts +59 -0
  187. package/src/identity/registry/erc8004/utils.ts +58 -0
  188. package/src/identity/registry/erc8004.ts +53 -1106
  189. package/src/identity/registry/fieldParsers.ts +28 -0
  190. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  191. package/src/identity/registry/operatorVault/constants.ts +38 -0
  192. package/src/identity/registry/operatorVault/read.ts +246 -0
  193. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  194. package/src/identity/registry/operatorVault.ts +44 -0
  195. package/src/identity/storage/ipfs.ts +26 -24
  196. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  197. package/src/identity/wallet/browserWallet/html.ts +106 -0
  198. package/src/identity/wallet/browserWallet/http.ts +28 -0
  199. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  200. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  201. package/src/identity/wallet/browserWallet/session.ts +325 -0
  202. package/src/identity/wallet/browserWallet/types.ts +192 -0
  203. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  204. package/src/identity/wallet/browserWallet.ts +30 -393
  205. package/src/identity/wallet/page/constants.ts +5 -0
  206. package/src/identity/wallet/page/controller.ts +251 -0
  207. package/src/identity/wallet/page/copy.ts +340 -0
  208. package/src/identity/wallet/page/grainient.ts +278 -0
  209. package/src/identity/wallet/page/html.ts +28 -0
  210. package/src/identity/wallet/page/markup.ts +50 -0
  211. package/src/identity/wallet/page/state.ts +9 -0
  212. package/src/identity/wallet/page/styles/base.ts +259 -0
  213. package/src/identity/wallet/page/styles/components.ts +262 -0
  214. package/src/identity/wallet/page/styles/index.ts +5 -0
  215. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  216. package/src/identity/wallet/page/types.ts +47 -0
  217. package/src/identity/wallet/page/view.ts +535 -0
  218. package/src/identity/wallet/page/walletProvider.ts +70 -0
  219. package/src/identity/wallet/page.tsx +38 -0
  220. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  221. package/src/mcp/manager.ts +0 -1
  222. package/src/models/ModelPicker.tsx +36 -30
  223. package/src/models/catalog.ts +5 -2
  224. package/src/models/huggingface.ts +9 -9
  225. package/src/models/llamacpp.ts +13 -13
  226. package/src/models/modelDisplay.ts +75 -0
  227. package/src/models/modelPickerOptions.ts +16 -3
  228. package/src/models/modelRecommendation.ts +0 -1
  229. package/src/providers/errors.ts +16 -0
  230. package/src/providers/gemini.ts +252 -39
  231. package/src/providers/registry.ts +2 -2
  232. package/src/providers/retry.ts +1 -1
  233. package/src/runtime/sessionMode.ts +1 -1
  234. package/src/runtime/systemPrompt.ts +2 -0
  235. package/src/runtime/toolExecution.ts +18 -22
  236. package/src/runtime/toolIntent.ts +0 -20
  237. package/src/runtime/turn.ts +0 -92
  238. package/src/storage/atomicWrite.ts +4 -1
  239. package/src/storage/config.ts +181 -5
  240. package/src/storage/identity.ts +9 -3
  241. package/src/storage/secrets.ts +2 -2
  242. package/src/tools/bashSafety.ts +8 -0
  243. package/src/tools/changeDirectoryTool.ts +1 -1
  244. package/src/tools/deleteFileTool.ts +4 -4
  245. package/src/tools/editTool.ts +4 -4
  246. package/src/tools/editUtils.ts +5 -5
  247. package/src/tools/privateContinuityEditTool.ts +4 -5
  248. package/src/tools/privateContinuityReadTool.ts +1 -2
  249. package/src/tools/registry.ts +30 -0
  250. package/src/tools/writeFileTool.ts +5 -5
  251. package/src/ui/BrandSplash.tsx +20 -85
  252. package/src/ui/ProgressBar.tsx +3 -5
  253. package/src/ui/Select.tsx +20 -8
  254. package/src/ui/Spinner.tsx +38 -3
  255. package/src/ui/Surface.tsx +2 -2
  256. package/src/ui/TextInput.tsx +63 -20
  257. package/src/ui/theme.ts +7 -34
  258. package/src/utils/openExternal.ts +21 -0
  259. package/src/utils/withRetry.ts +47 -3
  260. package/src/identity/hub/identityHubEffects.ts +0 -937
  261. package/src/identity/hub/identityHubModel.ts +0 -371
  262. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -156
  263. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -146
  264. package/src/identity/hub/screens/IdentitySummary.tsx +0 -106
  265. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  266. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  267. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  268. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -0,0 +1,325 @@
1
+ import http from 'node:http'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { recoverAddressFromSignature } from '../../crypto/eth.js'
4
+ import { normalizeWalletPayloadPurpose } from '../walletPurposeCompat.js'
5
+ import { walletPage } from './html.js'
6
+ import { readJson, respondHtml, respondJson } from './http.js'
7
+ import {
8
+ BrowserWalletError,
9
+ type BrowserWalletSession,
10
+ type BrowserWalletSignAndTransaction,
11
+ type BrowserWalletSignature,
12
+ type BrowserWalletTransaction,
13
+ type PendingPrompt,
14
+ type ReadyHandler,
15
+ type SignAndTransactionRequest,
16
+ } from './types.js'
17
+ import {
18
+ accountMismatchError,
19
+ chainIdHex,
20
+ parseAccount,
21
+ parseBrowserWalletErrorBody,
22
+ parseHex,
23
+ } from './validation.js'
24
+
25
+ export async function openBrowserWalletSession(args: {
26
+ title?: string
27
+ onReady?: ReadyHandler
28
+ } = {}): Promise<BrowserWalletSession> {
29
+ const title = args.title ?? 'ethagent wallet session'
30
+ const sseClients: Set<http.ServerResponse> = new Set()
31
+ let pending: PendingPrompt | null = null
32
+ let closed = false
33
+
34
+ const pushEvent = (kind: string, data: Record<string, unknown>): void => {
35
+ const payload = `event: ${kind}\ndata: ${JSON.stringify(data)}\n\n`
36
+ for (const res of sseClients) {
37
+ try { res.write(payload) } catch { }
38
+ }
39
+ }
40
+
41
+ const failPending = (err: unknown): void => {
42
+ if (!pending) return
43
+ clearTimeout(pending.timeout)
44
+ pending.reject(err instanceof Error ? err : new Error(String(err)))
45
+ pending = null
46
+ }
47
+
48
+ const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
49
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1')
50
+ if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/ethagent')) {
51
+ respondHtml(res, walletPage(title, '', { kind: 'session-wait' }))
52
+ return
53
+ }
54
+ if (req.method === 'GET' && url.pathname === '/events') {
55
+ res.writeHead(200, {
56
+ 'content-type': 'text/event-stream',
57
+ 'cache-control': 'no-store',
58
+ 'connection': 'keep-alive',
59
+ })
60
+ res.write(`: connected\n\n`)
61
+ sseClients.add(res)
62
+ req.on('close', () => sseClients.delete(res))
63
+ if (pending) {
64
+ res.write(`event: prompt\ndata: ${JSON.stringify({ sessionToken: pending.sessionToken, ...pending.payload })}\n\n`)
65
+ }
66
+ return
67
+ }
68
+ if (req.method === 'POST' && url.pathname === '/prepare') {
69
+ const body = await readJson(req)
70
+ if (!pending || body.sessionToken !== pending.sessionToken) {
71
+ respondJson(res, 409, { ok: false, error: 'no active prompt' })
72
+ return
73
+ }
74
+ if (!pending.prepare) {
75
+ respondJson(res, 400, { ok: false, error: 'this prompt does not have a prepare step' })
76
+ return
77
+ }
78
+ respondJson(res, 200, { ok: true, ...pending.prepare(body) })
79
+ return
80
+ }
81
+ if (req.method === 'POST' && url.pathname === '/prepare-transaction') {
82
+ const body = await readJson(req)
83
+ if (!pending || body.sessionToken !== pending.sessionToken) {
84
+ respondJson(res, 409, { ok: false, error: 'no active prompt' })
85
+ return
86
+ }
87
+ if (!pending.prepareTransaction) {
88
+ respondJson(res, 400, { ok: false, error: 'this prompt does not prepare transactions' })
89
+ return
90
+ }
91
+ respondJson(res, 200, { ok: true, ...(await pending.prepareTransaction(body)) })
92
+ return
93
+ }
94
+ if (req.method === 'POST' && url.pathname === '/complete') {
95
+ const body = await readJson(req)
96
+ if (!pending || body.sessionToken !== pending.sessionToken) {
97
+ respondJson(res, 409, { ok: false, error: 'no active prompt' })
98
+ return
99
+ }
100
+ const finished = pending
101
+ pending = null
102
+ respondJson(res, 200, { ok: true })
103
+ clearTimeout(finished.timeout)
104
+ finished.resolve(body)
105
+ return
106
+ }
107
+ if (req.method === 'POST' && url.pathname === '/cancel') {
108
+ const body = await readJson(req)
109
+ if (pending && body.sessionToken === pending.sessionToken) {
110
+ respondJson(res, 200, { ok: true })
111
+ failPending(new Error('wallet request was cancelled'))
112
+ return
113
+ }
114
+ respondJson(res, 409, { ok: false, error: 'no active prompt' })
115
+ return
116
+ }
117
+ if (req.method === 'POST' && url.pathname === '/error') {
118
+ const body = await readJson(req)
119
+ if (pending && body.sessionToken === pending.sessionToken) {
120
+ respondJson(res, 200, { ok: true })
121
+ failPending(new BrowserWalletError(parseBrowserWalletErrorBody(body)))
122
+ return
123
+ }
124
+ respondJson(res, 409, { ok: false, error: 'no active prompt' })
125
+ return
126
+ }
127
+ respondJson(res, 404, { ok: false, error: 'wallet session not found' })
128
+ }
129
+
130
+ const server = http.createServer((req, res) => {
131
+ void handleRequest(req, res).catch(err => {
132
+ respondJson(res, 500, { ok: false, error: (err as Error).message })
133
+ })
134
+ })
135
+
136
+ const url = await new Promise<string>((resolve, reject) => {
137
+ server.once('error', reject)
138
+ server.listen(0, '127.0.0.1', () => {
139
+ const addr = server.address()
140
+ if (!addr || typeof addr === 'string') {
141
+ reject(new Error('could not start wallet server'))
142
+ return
143
+ }
144
+ resolve(`http://localhost:${addr.port}/`)
145
+ })
146
+ })
147
+ args.onReady?.({ url })
148
+
149
+ const dispatch = <T>(opts: {
150
+ payload: Record<string, unknown>
151
+ prepare?: (body: Record<string, unknown>) => Record<string, unknown>
152
+ prepareTransaction?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>
153
+ complete: (body: Record<string, unknown>) => T
154
+ timeoutMs?: number
155
+ }): Promise<T> => {
156
+ if (closed) return Promise.reject(new Error('wallet session is closed'))
157
+ if (pending) return Promise.reject(new Error('wallet session has another prompt in flight'))
158
+ const sessionToken = randomUUID()
159
+ const payload = normalizeWalletPayloadPurpose(opts.payload)
160
+ return new Promise<T>((resolve, reject) => {
161
+ const timeout = setTimeout(() => {
162
+ failPending(new Error('Wallet Request Timed Out'))
163
+ }, opts.timeoutMs ?? 5 * 60_000)
164
+ pending = {
165
+ sessionToken,
166
+ payload,
167
+ ...(opts.prepare ? { prepare: opts.prepare } : {}),
168
+ ...(opts.prepareTransaction ? { prepareTransaction: opts.prepareTransaction } : {}),
169
+ resolve: body => {
170
+ try { resolve(opts.complete(body)) } catch (err) { reject(err) }
171
+ },
172
+ reject,
173
+ timeout,
174
+ }
175
+ pushEvent('prompt', { sessionToken, ...payload })
176
+ })
177
+ }
178
+
179
+ return {
180
+ url,
181
+ requestSignature: async (req): Promise<BrowserWalletSignature> => {
182
+ if (!req.message && !req.messageForAccount) throw new Error('Wallet signature request needs a message')
183
+ return dispatch<BrowserWalletSignature>({
184
+ payload: {
185
+ kind: 'sign',
186
+ chainIdHex: chainIdHex(req.chainId),
187
+ message: req.message,
188
+ ...(req.expectedAccount ? { expectedAccount: req.expectedAccount } : {}),
189
+ ...(req.purpose ? { purpose: req.purpose } : {}),
190
+ ...(req.flowId ? { flowId: req.flowId } : {}),
191
+ ...(typeof req.flowStep === 'number' ? { flowStep: req.flowStep } : {}),
192
+ ...(req.tokenChainName ? { tokenChainName: req.tokenChainName } : {}),
193
+ },
194
+ ...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
195
+ prepare: body => {
196
+ const account = parseAccount(body.account)
197
+ if (req.expectedAccount && account.toLowerCase() !== req.expectedAccount.toLowerCase()) {
198
+ throw accountMismatchError(account, req.expectedAccount, req.purpose)
199
+ }
200
+ const message = req.messageForAccount ? req.messageForAccount(account) : req.message!
201
+ return { message }
202
+ },
203
+ complete: body => {
204
+ const account = parseAccount(body.account)
205
+ const message = typeof body.message === 'string' ? body.message : ''
206
+ const signature = parseHex(body.signature, 'wallet signature')
207
+ if (req.expectedAccount && account.toLowerCase() !== req.expectedAccount.toLowerCase()) {
208
+ throw accountMismatchError(account, req.expectedAccount, req.purpose)
209
+ }
210
+ const recovered = recoverAddressFromSignature(message, signature)
211
+ if (recovered.toLowerCase() !== account.toLowerCase()) {
212
+ throw new Error('Wallet signature does not match connected account')
213
+ }
214
+ return { account, message, signature }
215
+ },
216
+ })
217
+ },
218
+ sendTransaction: async (req): Promise<BrowserWalletTransaction> => dispatch<BrowserWalletTransaction>({
219
+ payload: {
220
+ kind: 'transaction',
221
+ chainIdHex: chainIdHex(req.chainId),
222
+ expectedAccount: req.expectedAccount,
223
+ tx: { to: req.to, data: req.data, ...(req.value ? { value: req.value } : {}) },
224
+ ...(req.purpose ? { purpose: req.purpose } : {}),
225
+ ...(req.flowId ? { flowId: req.flowId } : {}),
226
+ ...(typeof req.flowStep === 'number' ? { flowStep: req.flowStep } : {}),
227
+ ...(req.tokenChainName ? { tokenChainName: req.tokenChainName } : {}),
228
+ },
229
+ ...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
230
+ complete: body => {
231
+ const account = parseAccount(body.account)
232
+ if (account.toLowerCase() !== req.expectedAccount.toLowerCase()) {
233
+ throw accountMismatchError(account, req.expectedAccount, req.purpose)
234
+ }
235
+ return { account, txHash: parseHex(body.txHash, 'transaction hash') }
236
+ },
237
+ }),
238
+ requestSignatureAndTransaction: async <TPrepared>(
239
+ req: SignAndTransactionRequest<TPrepared>,
240
+ ): Promise<BrowserWalletSignAndTransaction<TPrepared>> => {
241
+ if (!req.message && !req.messageForAccount) throw new Error('Wallet signature request needs a message')
242
+ let prepared:
243
+ | {
244
+ account: `0x${string}`
245
+ message: string
246
+ signature: `0x${string}`
247
+ tx: { to: `0x${string}`; data: `0x${string}`; value?: `0x${string}` }
248
+ prepared: TPrepared
249
+ }
250
+ | null = null
251
+ return dispatch<BrowserWalletSignAndTransaction<TPrepared>>({
252
+ payload: {
253
+ kind: 'sign-transaction',
254
+ chainIdHex: chainIdHex(req.chainId),
255
+ message: req.message,
256
+ ...(req.expectedAccount ? { expectedAccount: req.expectedAccount } : {}),
257
+ ...(req.purpose ? { purpose: req.purpose } : {}),
258
+ ...(req.flowId ? { flowId: req.flowId } : {}),
259
+ ...(typeof req.flowStep === 'number' ? { flowStep: req.flowStep } : {}),
260
+ ...(req.tokenChainName ? { tokenChainName: req.tokenChainName } : {}),
261
+ },
262
+ ...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
263
+ prepare: body => {
264
+ const account = parseAccount(body.account)
265
+ if (req.expectedAccount && account.toLowerCase() !== req.expectedAccount.toLowerCase()) {
266
+ throw accountMismatchError(account, req.expectedAccount, req.purpose)
267
+ }
268
+ const message = req.messageForAccount ? req.messageForAccount(account) : req.message!
269
+ return { message }
270
+ },
271
+ prepareTransaction: async body => {
272
+ const account = parseAccount(body.account)
273
+ const message = typeof body.message === 'string' ? body.message : ''
274
+ const signature = parseHex(body.signature, 'wallet signature')
275
+ if (req.expectedAccount && account.toLowerCase() !== req.expectedAccount.toLowerCase()) {
276
+ throw accountMismatchError(account, req.expectedAccount, req.purpose)
277
+ }
278
+ const recovered = recoverAddressFromSignature(message, signature)
279
+ if (recovered.toLowerCase() !== account.toLowerCase()) {
280
+ throw new Error('Wallet signature does not match connected account')
281
+ }
282
+ const next = await req.prepareTransaction({ account, message, signature })
283
+ prepared = {
284
+ account,
285
+ message,
286
+ signature,
287
+ tx: {
288
+ to: next.to,
289
+ data: next.data,
290
+ ...(next.value ? { value: next.value } : {}),
291
+ },
292
+ prepared: next.prepared,
293
+ }
294
+ return { tx: prepared.tx }
295
+ },
296
+ complete: body => {
297
+ if (!prepared) throw new Error('Wallet transaction was not prepared')
298
+ const account = parseAccount(body.account)
299
+ if (account.toLowerCase() !== prepared.account.toLowerCase()) {
300
+ throw accountMismatchError(account, prepared.account, req.purpose)
301
+ }
302
+ return {
303
+ account,
304
+ message: prepared.message,
305
+ signature: prepared.signature,
306
+ txHash: parseHex(body.txHash, 'transaction hash'),
307
+ prepared: prepared.prepared,
308
+ }
309
+ },
310
+ })
311
+ },
312
+ close: async (): Promise<void> => {
313
+ if (closed) return
314
+ closed = true
315
+ if (pending) failPending(new Error('wallet session closed before request completed'))
316
+ pushEvent('done', {})
317
+ await new Promise(resolve => setTimeout(resolve, 250))
318
+ for (const res of sseClients) {
319
+ try { res.end() } catch { }
320
+ }
321
+ sseClients.clear()
322
+ await new Promise<void>(resolve => server.close(() => resolve()))
323
+ },
324
+ }
325
+ }
@@ -0,0 +1,192 @@
1
+ import type { Address, Hex, PublicClient } from 'viem'
2
+
3
+ export type ReadyHandler = (session: BrowserWalletReady) => void
4
+
5
+ export type BrowserWalletReady = {
6
+ url: string
7
+ }
8
+
9
+ export type WalletPurpose =
10
+ | 'connect-operator-wallet'
11
+ | 'create-agent'
12
+ | 'restore-owner-wallet'
13
+ | 'restore-operator-wallet'
14
+ | 'update-snapshot-owner'
15
+ | 'update-snapshot-operator'
16
+ | 'update-snapshot-connected'
17
+ | 'update-ens'
18
+ | 'clear-ens'
19
+ | 'update-profile-owner'
20
+ | 'update-profile-operator'
21
+ | 'update-profile-connected'
22
+ | 'update-ens-records'
23
+ | 'clear-ens-records'
24
+ | 'create-simple-ens-subdomain'
25
+ | 'set-simple-ens-records'
26
+ | 'create-agent-ens-subdomain'
27
+ | 'set-agent-ens-records'
28
+ | 'update-operators'
29
+ | 'operator-proof'
30
+ | 'authorize-operator-wallet-resolver'
31
+ | 'revoke-operator-wallet-resolver'
32
+ | 'reconcile-resolver-approvals'
33
+ | 'sync-operator-vault'
34
+ | 'refetch-snapshot'
35
+ | 'prepare-transfer-sender'
36
+ | 'prepare-transfer-target'
37
+ | 'publish-transfer-snapshot'
38
+ | 'deploy-agent-vault'
39
+ | 'deposit-agent-vault'
40
+ | 'unwrap-agent-vault'
41
+ | 'rotate-agent-uri-vault-owner'
42
+ | 'rotate-agent-uri-vault-operator'
43
+ | 'withdraw-vault'
44
+ | 'register-root-commit'
45
+ | 'register-root-tx'
46
+ | 'delete-ens-subdomain'
47
+
48
+ export type SignatureRequest = {
49
+ chainId: number
50
+ expectedAccount?: Address
51
+ message?: string
52
+ messageForAccount?: (account: Address) => string
53
+ timeoutMs?: number
54
+ onReady?: ReadyHandler
55
+ purpose?: WalletPurpose
56
+ flowId?: string
57
+ flowStep?: number
58
+ tokenChainName?: string
59
+ }
60
+
61
+ export type TransactionRequest = {
62
+ chainId: number
63
+ expectedAccount: Address
64
+ to?: Address
65
+ data: Hex
66
+ value?: Hex
67
+ gas?: Hex
68
+ maxFeePerGas?: Hex
69
+ maxPriorityFeePerGas?: Hex
70
+ timeoutMs?: number
71
+ onReady?: ReadyHandler
72
+ purpose?: WalletPurpose
73
+ flowId?: string
74
+ flowStep?: number
75
+ tokenChainName?: string
76
+ }
77
+
78
+ export type SignAndTransactionRequest<TPrepared> = {
79
+ chainId: number
80
+ expectedAccount?: Address
81
+ message?: string
82
+ messageForAccount?: (account: Address) => string
83
+ timeoutMs?: number
84
+ onReady?: ReadyHandler
85
+ purpose?: WalletPurpose
86
+ flowId?: string
87
+ flowStep?: number
88
+ tokenChainName?: string
89
+ prepareTransaction: (wallet: BrowserWalletSignature) => Promise<{
90
+ to: Address
91
+ data: Hex
92
+ value?: Hex
93
+ prepared: TPrepared
94
+ }>
95
+ }
96
+
97
+ export type AccountRequest = {
98
+ timeoutMs?: number
99
+ onReady?: ReadyHandler
100
+ purpose?: WalletPurpose
101
+ flowId?: string
102
+ flowStep?: number
103
+ tokenChainName?: string
104
+ }
105
+
106
+ export type BrowserWalletErrorPayload = {
107
+ message: string
108
+ code?: string
109
+ data?: string
110
+ causes?: readonly string[]
111
+ method?: string
112
+ purpose?: string
113
+ chainIdHex?: string
114
+ title?: string
115
+ }
116
+
117
+ export class BrowserWalletError extends Error {
118
+ readonly code?: string
119
+ readonly data?: string
120
+ readonly causes: readonly string[]
121
+ readonly method?: string
122
+ readonly purpose?: string
123
+ readonly chainIdHex?: string
124
+ readonly title?: string
125
+ constructor(payload: BrowserWalletErrorPayload) {
126
+ super(payload.message || 'Wallet request failed')
127
+ this.name = 'BrowserWalletError'
128
+ if (payload.code !== undefined) this.code = payload.code
129
+ if (payload.data !== undefined) this.data = payload.data
130
+ this.causes = payload.causes ?? []
131
+ if (payload.method !== undefined) this.method = payload.method
132
+ if (payload.purpose !== undefined) this.purpose = payload.purpose
133
+ if (payload.chainIdHex !== undefined) this.chainIdHex = payload.chainIdHex
134
+ if (payload.title !== undefined) this.title = payload.title
135
+ }
136
+ }
137
+
138
+ export type PrepareTransactionGasFeeClient = Pick<PublicClient, 'estimateGas' | 'estimateFeesPerGas'>
139
+
140
+ export type PrepareTransactionGasFeeArgs = {
141
+ client: PrepareTransactionGasFeeClient
142
+ account: Address
143
+ to?: Address
144
+ data: Hex
145
+ value?: bigint
146
+ }
147
+
148
+ export type PreparedGasFee = {
149
+ gas: Hex
150
+ maxFeePerGas: Hex
151
+ maxPriorityFeePerGas: Hex
152
+ }
153
+
154
+ export type BrowserWalletSignature = {
155
+ account: Address
156
+ message: string
157
+ signature: Hex
158
+ }
159
+
160
+ export type BrowserWalletTransaction = {
161
+ account: Address
162
+ txHash: Hex
163
+ }
164
+
165
+ export type BrowserWalletSignAndTransaction<TPrepared> = BrowserWalletSignature & {
166
+ txHash: Hex
167
+ prepared: TPrepared
168
+ }
169
+
170
+ export type BrowserWalletAccount = {
171
+ account: Address
172
+ }
173
+
174
+ export type BrowserWalletSession = {
175
+ url: string
176
+ requestSignature: (req: SignatureRequest) => Promise<BrowserWalletSignature>
177
+ sendTransaction: (req: TransactionRequest) => Promise<BrowserWalletTransaction>
178
+ requestSignatureAndTransaction: <TPrepared>(
179
+ req: SignAndTransactionRequest<TPrepared>,
180
+ ) => Promise<BrowserWalletSignAndTransaction<TPrepared>>
181
+ close: () => Promise<void>
182
+ }
183
+
184
+ export type PendingPrompt = {
185
+ sessionToken: string
186
+ payload: Record<string, unknown>
187
+ prepare?: (body: Record<string, unknown>) => Record<string, unknown>
188
+ prepareTransaction?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>
189
+ resolve: (body: Record<string, unknown>) => void
190
+ reject: (err: unknown) => void
191
+ timeout: NodeJS.Timeout
192
+ }
@@ -0,0 +1,74 @@
1
+ import { getAddress, type Address, type Hex } from 'viem'
2
+ import { normalizeWalletPurposeValue } from '../walletPurposeCompat.js'
3
+ import type { BrowserWalletErrorPayload, WalletPurpose } from './types.js'
4
+
5
+ export function parseBrowserWalletErrorBody(body: Record<string, unknown>): BrowserWalletErrorPayload {
6
+ const message = typeof body.message === 'string' && body.message.trim() ? body.message.trim() : 'Wallet request failed'
7
+ const code = typeof body.code === 'string' && body.code.trim() ? body.code.trim() : undefined
8
+ const data = typeof body.data === 'string' && body.data.trim() ? body.data.trim() : undefined
9
+ const rawCauses = Array.isArray(body.causes) ? body.causes : []
10
+ const causes = rawCauses.filter((c): c is string => typeof c === 'string' && c.trim().length > 0).slice(0, 5)
11
+ const method = typeof body.method === 'string' && body.method.trim() ? body.method.trim() : undefined
12
+ const purpose = normalizeWalletPurposeValue(body.purpose)
13
+ const chainIdHex = typeof body.chainIdHex === 'string' && body.chainIdHex.trim() ? body.chainIdHex.trim() : undefined
14
+ const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : undefined
15
+ return { message, code, data, causes, method, purpose, chainIdHex, title }
16
+ }
17
+
18
+ export function parseAccount(value: unknown): Address {
19
+ if (typeof value !== 'string') throw new Error('Wallet account is missing')
20
+ return getAddress(value)
21
+ }
22
+
23
+ export function parseHex(value: unknown, label: string): Hex {
24
+ if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is invalid`)
25
+ return value as Hex
26
+ }
27
+
28
+ export function accountMismatchError(account: Address, expectedAccount: Address, purpose?: WalletPurpose): Error {
29
+ if (isOwnerWalletPurpose(purpose)) {
30
+ return new Error(`Owner Wallet Required: connected wallet ${account} does not match owner wallet ${expectedAccount}`)
31
+ }
32
+ if (isOperatorWalletPurpose(purpose)) {
33
+ return new Error(`Operator Wallet Required: connected wallet ${account} does not match operator wallet ${expectedAccount}`)
34
+ }
35
+ if (purpose === 'prepare-transfer-sender' || purpose === 'publish-transfer-snapshot') {
36
+ return new Error(`Sender Wallet Required: connected wallet ${account} does not match sender wallet ${expectedAccount}`)
37
+ }
38
+ if (purpose === 'prepare-transfer-target') {
39
+ return new Error(`Receiver Wallet Required: connected wallet ${account} does not match receiver wallet ${expectedAccount}. Switch accounts in your wallet to the receiver address and retry.`)
40
+ }
41
+ return new Error(`Connected wallet ${account} does not match expected wallet ${expectedAccount}`)
42
+ }
43
+
44
+ export function assertSessionToken(body: Record<string, unknown>, sessionToken: string): void {
45
+ if (body.sessionToken !== sessionToken) throw new Error('Wallet session token is invalid')
46
+ }
47
+
48
+ export function chainIdHex(chainId: number): Hex {
49
+ return `0x${chainId.toString(16)}` as Hex
50
+ }
51
+
52
+ function isOwnerWalletPurpose(purpose: WalletPurpose | undefined): boolean {
53
+ return purpose === 'restore-owner-wallet'
54
+ || purpose === 'create-agent'
55
+ || purpose === 'create-agent-ens-subdomain'
56
+ || purpose === 'set-agent-ens-records'
57
+ || purpose === 'update-operators'
58
+ || purpose === 'update-snapshot-owner'
59
+ || purpose === 'update-profile-owner'
60
+ || purpose === 'update-ens'
61
+ || purpose === 'clear-ens'
62
+ || purpose === 'deploy-agent-vault'
63
+ || purpose === 'sync-operator-vault'
64
+ || purpose === 'rotate-agent-uri-vault-owner'
65
+ }
66
+
67
+ function isOperatorWalletPurpose(purpose: WalletPurpose | undefined): boolean {
68
+ return purpose === 'restore-operator-wallet'
69
+ || purpose === 'operator-proof'
70
+ || purpose === 'connect-operator-wallet'
71
+ || purpose === 'update-snapshot-operator'
72
+ || purpose === 'update-profile-operator'
73
+ || purpose === 'rotate-agent-uri-vault-operator'
74
+ }