ethagent 1.1.2 → 2.0.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 (268) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +126 -30
  3. package/package.json +7 -2
  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,106 @@
1
+ import { readFileSync, statSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { transformSync } from 'esbuild'
5
+ import { normalizeWalletPayloadPurpose } from '../walletPurposeCompat.js'
6
+
7
+ const WALLET_PAGE_FILE = join(dirname(fileURLToPath(import.meta.url)), '..', 'page.tsx')
8
+ const WALLET_PAGE_MODULE_FILES = [
9
+ join('page', 'types.ts'),
10
+ join('page', 'html.ts'),
11
+ join('page', 'constants.ts'),
12
+ join('page', 'styles', 'base.ts'),
13
+ join('page', 'styles', 'components.ts'),
14
+ join('page', 'styles', 'responsive.ts'),
15
+ join('page', 'styles', 'index.ts'),
16
+ join('page', 'markup.ts'),
17
+ join('page', 'grainient.ts'),
18
+ join('page', 'state.ts'),
19
+ join('page', 'copy.ts'),
20
+ join('page', 'walletProvider.ts'),
21
+ join('page', 'view.ts'),
22
+ join('page', 'controller.ts'),
23
+ ] as const
24
+ const WALLET_HTML = loadWalletHtml()
25
+
26
+ export function walletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
27
+ const config = JSON.stringify({ sessionToken, ...normalizeWalletPayloadPurpose(payload) }).replaceAll('<', '\\u003c')
28
+ const injection = `<script>window.__WALLET_CONFIG__ = ${config};</script>`
29
+ return WALLET_HTML
30
+ .replace(/<title>.*?<\/title>/, `<title>${escapeHtml(title)}</title>`)
31
+ .replace('<head>', `<head>\n ${injection}`)
32
+ }
33
+
34
+ export function __testWalletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
35
+ return walletPage(title, sessionToken, payload)
36
+ }
37
+
38
+ function loadWalletHtml(): string {
39
+ const compiled = transformSync(loadWalletPageSource(), {
40
+ loader: 'ts',
41
+ target: 'es2020',
42
+ }).code
43
+ return wrapInWalletShell(compiled)
44
+ }
45
+
46
+ function loadWalletPageSource(): string {
47
+ const pageFile = locateWalletPageFile()
48
+ const pageDir = dirname(pageFile)
49
+ const files = [
50
+ ...WALLET_PAGE_MODULE_FILES.map(file => join(pageDir, file)),
51
+ pageFile,
52
+ ]
53
+ return stripWalletModuleSyntax(files.map(file => readFileSync(file, 'utf8')).join('\n'))
54
+ }
55
+
56
+ function stripWalletModuleSyntax(source: string): string {
57
+ const out: string[] = []
58
+ let skippingImport = false
59
+ for (const line of source.split(/\r?\n/)) {
60
+ const trimmed = line.trim()
61
+ if (skippingImport) {
62
+ if (/\bfrom\s+['"][^'"]+['"]/.test(trimmed) || trimmed.endsWith(';')) skippingImport = false
63
+ continue
64
+ }
65
+ if (trimmed.startsWith('import ')) {
66
+ if (!/\bfrom\s+['"][^'"]+['"]/.test(trimmed) && !trimmed.endsWith(';')) skippingImport = true
67
+ continue
68
+ }
69
+ out.push(line.replace(/^export\s+(?=(async\s+function|const|let|function|interface|type|class)\b)/, ''))
70
+ }
71
+ return out.join('\n')
72
+ }
73
+
74
+ function locateWalletPageFile(): string {
75
+ try {
76
+ statSync(WALLET_PAGE_FILE)
77
+ return WALLET_PAGE_FILE
78
+ } catch {
79
+ return join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', '..', 'src', 'identity', 'wallet', 'page.tsx')
80
+ }
81
+ }
82
+
83
+ function wrapInWalletShell(compiledJs: string): string {
84
+ const safeJs = compiledJs.replaceAll('</', '<\\/')
85
+ return `<!doctype html>
86
+ <html>
87
+ <head>
88
+ <meta charset="utf-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1">
90
+ <title>Wallet Request</title>
91
+ </head>
92
+ <body>
93
+ <script>
94
+ ${safeJs}
95
+ </script>
96
+ </body>
97
+ </html>`
98
+ }
99
+
100
+ function escapeHtml(value: string): string {
101
+ return value
102
+ .replaceAll('&', '&amp;')
103
+ .replaceAll('<', '&lt;')
104
+ .replaceAll('>', '&gt;')
105
+ .replaceAll('"', '&quot;')
106
+ }
@@ -0,0 +1,28 @@
1
+ import http from 'node:http'
2
+
3
+ export async function readJson(req: http.IncomingMessage): Promise<Record<string, unknown>> {
4
+ const chunks: Buffer[] = []
5
+ for await (const chunk of req) {
6
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
7
+ }
8
+ const raw = Buffer.concat(chunks).toString('utf8')
9
+ const parsed = raw ? JSON.parse(raw) as unknown : {}
10
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Request body must be a JSON object')
11
+ return parsed as Record<string, unknown>
12
+ }
13
+
14
+ export function respondHtml(res: http.ServerResponse, body: string): void {
15
+ res.writeHead(200, {
16
+ 'content-type': 'text/html; charset=utf-8',
17
+ 'cache-control': 'no-store',
18
+ })
19
+ res.end(body)
20
+ }
21
+
22
+ export function respondJson(res: http.ServerResponse, status: number, body: Record<string, unknown>): void {
23
+ res.writeHead(status, {
24
+ 'content-type': 'application/json; charset=utf-8',
25
+ 'cache-control': 'no-store',
26
+ })
27
+ res.end(JSON.stringify(body))
28
+ }
@@ -0,0 +1,106 @@
1
+ import http from 'node:http'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { walletPage } from './html.js'
4
+ import { readJson, respondHtml, respondJson } from './http.js'
5
+ import { BrowserWalletError, type ReadyHandler } from './types.js'
6
+ import {
7
+ assertSessionToken,
8
+ parseBrowserWalletErrorBody,
9
+ } from './validation.js'
10
+
11
+ export function startBrowserWalletServer<T>(args: {
12
+ title: string
13
+ payload: Record<string, unknown>
14
+ timeoutMs?: number
15
+ onReady?: ReadyHandler
16
+ prepare?: (body: Record<string, unknown>) => Record<string, unknown>
17
+ prepareTransaction?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>
18
+ complete: (body: Record<string, unknown>) => T
19
+ }): Promise<T> {
20
+ const sessionToken = randomUUID()
21
+ const timeoutMs = args.timeoutMs ?? 5 * 60_000
22
+
23
+ return new Promise<T>((resolve, reject) => {
24
+ let settled = false
25
+ const finish = (fn: () => void): void => {
26
+ if (settled) return
27
+ settled = true
28
+ clearTimeout(timer)
29
+ server.close()
30
+ fn()
31
+ }
32
+ const fail = (err: unknown): void => finish(() => reject(err instanceof Error ? err : new Error(String(err))))
33
+
34
+ const server = http.createServer((req, res) => {
35
+ void handleRequest(req, res).catch(err => {
36
+ respondJson(res, 500, { ok: false, error: (err as Error).message })
37
+ })
38
+ })
39
+
40
+ const timer = setTimeout(() => {
41
+ fail(new Error('Wallet Request Timed Out'))
42
+ }, timeoutMs)
43
+
44
+ const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
45
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1')
46
+ if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/ethagent')) {
47
+ respondHtml(res, walletPage(args.title, sessionToken, args.payload))
48
+ return
49
+ }
50
+ if (req.method === 'POST' && (url.pathname === '/prepare' || url.pathname === '/ethagent/prepare')) {
51
+ const body = await readJson(req)
52
+ assertSessionToken(body, sessionToken)
53
+ if (!args.prepare) {
54
+ respondJson(res, 400, { ok: false, error: 'this wallet request does not have a prepare step' })
55
+ return
56
+ }
57
+ respondJson(res, 200, { ok: true, ...args.prepare(body) })
58
+ return
59
+ }
60
+ if (req.method === 'POST' && (url.pathname === '/prepare-transaction' || url.pathname === '/ethagent/prepare-transaction')) {
61
+ const body = await readJson(req)
62
+ assertSessionToken(body, sessionToken)
63
+ if (!args.prepareTransaction) {
64
+ respondJson(res, 400, { ok: false, error: 'this wallet request does not prepare transactions' })
65
+ return
66
+ }
67
+ respondJson(res, 200, { ok: true, ...(await args.prepareTransaction(body)) })
68
+ return
69
+ }
70
+ if (req.method === 'POST' && (url.pathname === '/complete' || url.pathname === '/ethagent/complete')) {
71
+ const body = await readJson(req)
72
+ assertSessionToken(body, sessionToken)
73
+ const result = args.complete(body)
74
+ respondJson(res, 200, { ok: true })
75
+ finish(() => resolve(result))
76
+ return
77
+ }
78
+ if (req.method === 'POST' && (url.pathname === '/cancel' || url.pathname === '/ethagent/cancel')) {
79
+ const body = await readJson(req)
80
+ assertSessionToken(body, sessionToken)
81
+ respondJson(res, 200, { ok: true })
82
+ fail(new Error('wallet request was cancelled'))
83
+ return
84
+ }
85
+ if (req.method === 'POST' && (url.pathname === '/error' || url.pathname === '/ethagent/error')) {
86
+ const body = await readJson(req)
87
+ assertSessionToken(body, sessionToken)
88
+ respondJson(res, 200, { ok: true })
89
+ fail(new BrowserWalletError(parseBrowserWalletErrorBody(body)))
90
+ return
91
+ }
92
+ respondJson(res, 404, { ok: false, error: 'wallet session not found' })
93
+ }
94
+
95
+ server.once('error', fail)
96
+ server.listen(0, '127.0.0.1', () => {
97
+ const address = server.address()
98
+ if (!address || typeof address === 'string') {
99
+ fail(new Error('could not start wallet server'))
100
+ return
101
+ }
102
+ const url = `http://localhost:${address.port}/`
103
+ args.onReady?.({ url })
104
+ })
105
+ })
106
+ }
@@ -0,0 +1,191 @@
1
+ import { recoverAddressFromSignature } from '../../crypto/eth.js'
2
+ import { normalizeWalletPayloadPurpose } from '../walletPurposeCompat.js'
3
+ import { startBrowserWalletServer } from './requestServer.js'
4
+ import type {
5
+ AccountRequest,
6
+ BrowserWalletAccount,
7
+ BrowserWalletSignAndTransaction,
8
+ BrowserWalletSignature,
9
+ BrowserWalletTransaction,
10
+ SignAndTransactionRequest,
11
+ SignatureRequest,
12
+ TransactionRequest,
13
+ } from './types.js'
14
+ import {
15
+ accountMismatchError,
16
+ chainIdHex,
17
+ parseAccount,
18
+ parseHex,
19
+ } from './validation.js'
20
+
21
+ export async function requestBrowserWalletAccount(args: AccountRequest = {}): Promise<BrowserWalletAccount> {
22
+ return await startBrowserWalletServer<BrowserWalletAccount>({
23
+ title: 'ethagent wallet connection',
24
+ timeoutMs: args.timeoutMs,
25
+ onReady: args.onReady,
26
+ payload: normalizeWalletPayloadPurpose({
27
+ kind: 'account',
28
+ ...(args.purpose ? { purpose: args.purpose } : {}),
29
+ ...(args.flowId ? { flowId: args.flowId } : {}),
30
+ ...(typeof args.flowStep === 'number' ? { flowStep: args.flowStep } : {}),
31
+ ...(args.tokenChainName ? { tokenChainName: args.tokenChainName } : {}),
32
+ }),
33
+ complete: body => {
34
+ const account = parseAccount(body.account)
35
+ return { account }
36
+ },
37
+ })
38
+ }
39
+
40
+ export async function requestBrowserWalletSignature(args: SignatureRequest): Promise<BrowserWalletSignature> {
41
+ if (!args.message && !args.messageForAccount) throw new Error('Wallet signature request needs a message')
42
+ return await startBrowserWalletServer<BrowserWalletSignature>({
43
+ title: 'ethagent wallet signature',
44
+ timeoutMs: args.timeoutMs,
45
+ onReady: args.onReady,
46
+ payload: normalizeWalletPayloadPurpose({
47
+ kind: 'sign',
48
+ chainIdHex: chainIdHex(args.chainId),
49
+ message: args.message,
50
+ ...(args.expectedAccount ? { expectedAccount: args.expectedAccount } : {}),
51
+ ...(args.purpose ? { purpose: args.purpose } : {}),
52
+ ...(args.flowId ? { flowId: args.flowId } : {}),
53
+ ...(typeof args.flowStep === 'number' ? { flowStep: args.flowStep } : {}),
54
+ ...(args.tokenChainName ? { tokenChainName: args.tokenChainName } : {}),
55
+ }),
56
+ prepare: body => {
57
+ const account = parseAccount(body.account)
58
+ if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
59
+ throw accountMismatchError(account, args.expectedAccount, args.purpose)
60
+ }
61
+ const message = args.messageForAccount ? args.messageForAccount(account) : args.message!
62
+ return { message }
63
+ },
64
+ complete: body => {
65
+ const account = parseAccount(body.account)
66
+ const message = typeof body.message === 'string' ? body.message : ''
67
+ const signature = parseHex(body.signature, 'wallet signature')
68
+ if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
69
+ throw accountMismatchError(account, args.expectedAccount, args.purpose)
70
+ }
71
+ const recovered = recoverAddressFromSignature(message, signature)
72
+ if (recovered.toLowerCase() !== account.toLowerCase()) {
73
+ throw new Error('Wallet signature does not match connected account')
74
+ }
75
+ return { account, message, signature }
76
+ },
77
+ })
78
+ }
79
+
80
+ export async function sendBrowserWalletTransaction(args: TransactionRequest): Promise<BrowserWalletTransaction> {
81
+ return await startBrowserWalletServer<BrowserWalletTransaction>({
82
+ title: 'ethagent wallet transaction',
83
+ timeoutMs: args.timeoutMs,
84
+ onReady: args.onReady,
85
+ payload: normalizeWalletPayloadPurpose({
86
+ kind: 'transaction',
87
+ chainIdHex: chainIdHex(args.chainId),
88
+ expectedAccount: args.expectedAccount,
89
+ tx: {
90
+ ...(args.to ? { to: args.to } : {}),
91
+ data: args.data,
92
+ ...(args.value ? { value: args.value } : {}),
93
+ ...(args.gas ? { gas: args.gas } : {}),
94
+ ...(args.maxFeePerGas ? { maxFeePerGas: args.maxFeePerGas } : {}),
95
+ ...(args.maxPriorityFeePerGas ? { maxPriorityFeePerGas: args.maxPriorityFeePerGas } : {}),
96
+ },
97
+ ...(args.purpose ? { purpose: args.purpose } : {}),
98
+ ...(args.flowId ? { flowId: args.flowId } : {}),
99
+ ...(typeof args.flowStep === 'number' ? { flowStep: args.flowStep } : {}),
100
+ ...(args.tokenChainName ? { tokenChainName: args.tokenChainName } : {}),
101
+ }),
102
+ complete: body => {
103
+ const account = parseAccount(body.account)
104
+ if (account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
105
+ throw accountMismatchError(account, args.expectedAccount, args.purpose)
106
+ }
107
+ return { account, txHash: parseHex(body.txHash, 'transaction hash') }
108
+ },
109
+ })
110
+ }
111
+
112
+ export async function requestBrowserWalletSignatureAndTransaction<TPrepared>(
113
+ args: SignAndTransactionRequest<TPrepared>,
114
+ ): Promise<BrowserWalletSignAndTransaction<TPrepared>> {
115
+ if (!args.message && !args.messageForAccount) throw new Error('Wallet signature request needs a message')
116
+
117
+ let prepared:
118
+ | {
119
+ account: `0x${string}`
120
+ message: string
121
+ signature: `0x${string}`
122
+ tx: { to: `0x${string}`; data: `0x${string}`; value?: `0x${string}` }
123
+ prepared: TPrepared
124
+ }
125
+ | null = null
126
+
127
+ return await startBrowserWalletServer<BrowserWalletSignAndTransaction<TPrepared>>({
128
+ title: 'ethagent wallet request',
129
+ timeoutMs: args.timeoutMs,
130
+ onReady: args.onReady,
131
+ payload: normalizeWalletPayloadPurpose({
132
+ kind: 'sign-transaction',
133
+ chainIdHex: chainIdHex(args.chainId),
134
+ message: args.message,
135
+ ...(args.expectedAccount ? { expectedAccount: args.expectedAccount } : {}),
136
+ ...(args.purpose ? { purpose: args.purpose } : {}),
137
+ ...(args.flowId ? { flowId: args.flowId } : {}),
138
+ ...(typeof args.flowStep === 'number' ? { flowStep: args.flowStep } : {}),
139
+ ...(args.tokenChainName ? { tokenChainName: args.tokenChainName } : {}),
140
+ }),
141
+ prepare: body => {
142
+ const account = parseAccount(body.account)
143
+ if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
144
+ throw accountMismatchError(account, args.expectedAccount, args.purpose)
145
+ }
146
+ const message = args.messageForAccount ? args.messageForAccount(account) : args.message!
147
+ return { message }
148
+ },
149
+ prepareTransaction: async body => {
150
+ const account = parseAccount(body.account)
151
+ const message = typeof body.message === 'string' ? body.message : ''
152
+ const signature = parseHex(body.signature, 'wallet signature')
153
+ if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
154
+ throw accountMismatchError(account, args.expectedAccount, args.purpose)
155
+ }
156
+ const recovered = recoverAddressFromSignature(message, signature)
157
+ if (recovered.toLowerCase() !== account.toLowerCase()) {
158
+ throw new Error('Wallet signature does not match connected account')
159
+ }
160
+ const next = await args.prepareTransaction({ account, message, signature })
161
+ prepared = {
162
+ account,
163
+ message,
164
+ signature,
165
+ tx: {
166
+ to: next.to,
167
+ data: next.data,
168
+ ...(next.value ? { value: next.value } : {}),
169
+ },
170
+ prepared: next.prepared,
171
+ }
172
+ return {
173
+ tx: prepared.tx,
174
+ }
175
+ },
176
+ complete: body => {
177
+ if (!prepared) throw new Error('Wallet transaction was not prepared')
178
+ const account = parseAccount(body.account)
179
+ if (account.toLowerCase() !== prepared.account.toLowerCase()) {
180
+ throw accountMismatchError(account, prepared.account, args.purpose)
181
+ }
182
+ return {
183
+ account,
184
+ message: prepared.message,
185
+ signature: prepared.signature,
186
+ txHash: parseHex(body.txHash, 'transaction hash'),
187
+ prepared: prepared.prepared,
188
+ }
189
+ },
190
+ })
191
+ }