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
@@ -1,393 +1,30 @@
1
- import http from 'node:http'
2
- import { randomUUID } from 'node:crypto'
3
- import { readFileSync } from 'node:fs'
4
- import { dirname, join } from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
- import { getAddress, type Address, type Hex } from 'viem'
7
- import { recoverAddressFromSignature } from '../crypto/eth.js'
8
-
9
- const WALLET_PAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'wallet-page')
10
- const WALLET_HTML = loadWalletHtml()
11
-
12
- type ReadyHandler = (session: BrowserWalletReady) => void
13
-
14
- export type BrowserWalletReady = {
15
- url: string
16
- }
17
-
18
- type SignatureRequest = {
19
- chainId: number
20
- expectedAccount?: Address
21
- message?: string
22
- messageForAccount?: (account: Address) => string
23
- timeoutMs?: number
24
- onReady?: ReadyHandler
25
- }
26
-
27
- type TransactionRequest = {
28
- chainId: number
29
- expectedAccount: Address
30
- to: Address
31
- data: Hex
32
- value?: Hex
33
- timeoutMs?: number
34
- onReady?: ReadyHandler
35
- }
36
-
37
- type SignAndTransactionRequest<TPrepared> = {
38
- chainId: number
39
- expectedAccount?: Address
40
- message?: string
41
- messageForAccount?: (account: Address) => string
42
- timeoutMs?: number
43
- onReady?: ReadyHandler
44
- prepareTransaction: (wallet: BrowserWalletSignature) => Promise<{
45
- to: Address
46
- data: Hex
47
- value?: Hex
48
- prepared: TPrepared
49
- }>
50
- }
51
-
52
- type AccountRequest = {
53
- timeoutMs?: number
54
- onReady?: ReadyHandler
55
- }
56
-
57
- export type BrowserWalletSignature = {
58
- account: Address
59
- message: string
60
- signature: Hex
61
- }
62
-
63
- export type BrowserWalletTransaction = {
64
- account: Address
65
- txHash: Hex
66
- }
67
-
68
- export type BrowserWalletSignAndTransaction<TPrepared> = BrowserWalletSignature & {
69
- txHash: Hex
70
- prepared: TPrepared
71
- }
72
-
73
- export type BrowserWalletAccount = {
74
- account: Address
75
- }
76
-
77
- export async function requestBrowserWalletAccount(args: AccountRequest = {}): Promise<BrowserWalletAccount> {
78
- return await startBrowserWalletServer<BrowserWalletAccount>({
79
- title: 'ethagent wallet connection',
80
- timeoutMs: args.timeoutMs,
81
- onReady: args.onReady,
82
- payload: {
83
- kind: 'account',
84
- },
85
- complete: body => {
86
- const account = parseAccount(body.account)
87
- return { account }
88
- },
89
- })
90
- }
91
-
92
- export async function requestBrowserWalletSignature(args: SignatureRequest): Promise<BrowserWalletSignature> {
93
- if (!args.message && !args.messageForAccount) throw new Error('wallet signature request needs a message')
94
- return await startBrowserWalletServer<BrowserWalletSignature>({
95
- title: 'ethagent wallet signature',
96
- timeoutMs: args.timeoutMs,
97
- onReady: args.onReady,
98
- payload: {
99
- kind: 'sign',
100
- chainIdHex: chainIdHex(args.chainId),
101
- message: args.message,
102
- },
103
- prepare: body => {
104
- const account = parseAccount(body.account)
105
- if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
106
- throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
107
- }
108
- const message = args.messageForAccount ? args.messageForAccount(account) : args.message!
109
- return { message }
110
- },
111
- complete: body => {
112
- const account = parseAccount(body.account)
113
- const message = typeof body.message === 'string' ? body.message : ''
114
- const signature = parseHex(body.signature, 'wallet signature')
115
- if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
116
- throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
117
- }
118
- const recovered = recoverAddressFromSignature(message, signature)
119
- if (recovered.toLowerCase() !== account.toLowerCase()) {
120
- throw new Error('wallet signature does not match connected account')
121
- }
122
- return { account, message, signature }
123
- },
124
- })
125
- }
126
-
127
- export async function sendBrowserWalletTransaction(args: TransactionRequest): Promise<BrowserWalletTransaction> {
128
- return await startBrowserWalletServer<BrowserWalletTransaction>({
129
- title: 'ethagent wallet transaction',
130
- timeoutMs: args.timeoutMs,
131
- onReady: args.onReady,
132
- payload: {
133
- kind: 'transaction',
134
- chainIdHex: chainIdHex(args.chainId),
135
- expectedAccount: args.expectedAccount,
136
- tx: {
137
- to: args.to,
138
- data: args.data,
139
- ...(args.value ? { value: args.value } : {}),
140
- },
141
- },
142
- complete: body => {
143
- const account = parseAccount(body.account)
144
- if (account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
145
- throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
146
- }
147
- return { account, txHash: parseHex(body.txHash, 'transaction hash') }
148
- },
149
- })
150
- }
151
-
152
- export async function requestBrowserWalletSignatureAndTransaction<TPrepared>(
153
- args: SignAndTransactionRequest<TPrepared>,
154
- ): Promise<BrowserWalletSignAndTransaction<TPrepared>> {
155
- if (!args.message && !args.messageForAccount) throw new Error('wallet signature request needs a message')
156
-
157
- let prepared:
158
- | {
159
- account: Address
160
- message: string
161
- signature: Hex
162
- tx: { to: Address; data: Hex; value?: Hex }
163
- prepared: TPrepared
164
- }
165
- | null = null
166
-
167
- return await startBrowserWalletServer<BrowserWalletSignAndTransaction<TPrepared>>({
168
- title: 'ethagent wallet approval',
169
- timeoutMs: args.timeoutMs,
170
- onReady: args.onReady,
171
- payload: {
172
- kind: 'sign-transaction',
173
- chainIdHex: chainIdHex(args.chainId),
174
- message: args.message,
175
- },
176
- prepare: body => {
177
- const account = parseAccount(body.account)
178
- if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
179
- throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
180
- }
181
- const message = args.messageForAccount ? args.messageForAccount(account) : args.message!
182
- return { message }
183
- },
184
- prepareTransaction: async body => {
185
- const account = parseAccount(body.account)
186
- const message = typeof body.message === 'string' ? body.message : ''
187
- const signature = parseHex(body.signature, 'wallet signature')
188
- if (args.expectedAccount && account.toLowerCase() !== args.expectedAccount.toLowerCase()) {
189
- throw new Error(`connected wallet ${account} does not match owner ${args.expectedAccount}`)
190
- }
191
- const recovered = recoverAddressFromSignature(message, signature)
192
- if (recovered.toLowerCase() !== account.toLowerCase()) {
193
- throw new Error('wallet signature does not match connected account')
194
- }
195
- const next = await args.prepareTransaction({ account, message, signature })
196
- prepared = {
197
- account,
198
- message,
199
- signature,
200
- tx: {
201
- to: next.to,
202
- data: next.data,
203
- ...(next.value ? { value: next.value } : {}),
204
- },
205
- prepared: next.prepared,
206
- }
207
- return {
208
- tx: prepared.tx,
209
- }
210
- },
211
- complete: body => {
212
- if (!prepared) throw new Error('wallet transaction was not prepared')
213
- const account = parseAccount(body.account)
214
- if (account.toLowerCase() !== prepared.account.toLowerCase()) {
215
- throw new Error(`connected wallet ${account} does not match owner ${prepared.account}`)
216
- }
217
- return {
218
- account,
219
- message: prepared.message,
220
- signature: prepared.signature,
221
- txHash: parseHex(body.txHash, 'transaction hash'),
222
- prepared: prepared.prepared,
223
- }
224
- },
225
- })
226
- }
227
-
228
- function startBrowserWalletServer<T>(args: {
229
- title: string
230
- payload: Record<string, unknown>
231
- timeoutMs?: number
232
- onReady?: ReadyHandler
233
- prepare?: (body: Record<string, unknown>) => Record<string, unknown>
234
- prepareTransaction?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>
235
- complete: (body: Record<string, unknown>) => T
236
- }): Promise<T> {
237
- const sessionToken = randomUUID()
238
- const timeoutMs = args.timeoutMs ?? 5 * 60_000
239
-
240
- return new Promise<T>((resolve, reject) => {
241
- let settled = false
242
- const finish = (fn: () => void): void => {
243
- if (settled) return
244
- settled = true
245
- clearTimeout(timer)
246
- server.close()
247
- fn()
248
- }
249
- const fail = (err: unknown): void => finish(() => reject(err instanceof Error ? err : new Error(String(err))))
250
-
251
- const server = http.createServer((req, res) => {
252
- void handleRequest(req, res).catch(err => {
253
- respondJson(res, 500, { ok: false, error: (err as Error).message })
254
- })
255
- })
256
-
257
- const timer = setTimeout(() => {
258
- fail(new Error('browser wallet request timed out'))
259
- }, timeoutMs)
260
-
261
- const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
262
- const url = new URL(req.url ?? '/', 'http://127.0.0.1')
263
- if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/ethagent')) {
264
- respondHtml(res, walletPage(args.title, sessionToken, args.payload))
265
- return
266
- }
267
- if (req.method === 'POST' && (url.pathname === '/prepare' || url.pathname === '/ethagent/prepare')) {
268
- const body = await readJson(req)
269
- assertSessionToken(body, sessionToken)
270
- if (!args.prepare) {
271
- respondJson(res, 400, { ok: false, error: 'this wallet request does not have a prepare step' })
272
- return
273
- }
274
- respondJson(res, 200, { ok: true, ...args.prepare(body) })
275
- return
276
- }
277
- if (req.method === 'POST' && (url.pathname === '/prepare-transaction' || url.pathname === '/ethagent/prepare-transaction')) {
278
- const body = await readJson(req)
279
- assertSessionToken(body, sessionToken)
280
- if (!args.prepareTransaction) {
281
- respondJson(res, 400, { ok: false, error: 'this wallet request does not prepare transactions' })
282
- return
283
- }
284
- respondJson(res, 200, { ok: true, ...(await args.prepareTransaction(body)) })
285
- return
286
- }
287
- if (req.method === 'POST' && (url.pathname === '/complete' || url.pathname === '/ethagent/complete')) {
288
- const body = await readJson(req)
289
- assertSessionToken(body, sessionToken)
290
- const result = args.complete(body)
291
- respondJson(res, 200, { ok: true })
292
- finish(() => resolve(result))
293
- return
294
- }
295
- if (req.method === 'POST' && (url.pathname === '/cancel' || url.pathname === '/ethagent/cancel')) {
296
- const body = await readJson(req)
297
- assertSessionToken(body, sessionToken)
298
- respondJson(res, 200, { ok: true })
299
- fail(new Error('browser wallet request was cancelled'))
300
- return
301
- }
302
- respondJson(res, 404, { ok: false, error: 'wallet session not found' })
303
- }
304
-
305
- server.once('error', fail)
306
- server.listen(0, '127.0.0.1', () => {
307
- const address = server.address()
308
- if (!address || typeof address === 'string') {
309
- fail(new Error('could not start browser wallet server'))
310
- return
311
- }
312
- const url = `http://localhost:${address.port}/`
313
- args.onReady?.({ url })
314
- })
315
- })
316
- }
317
-
318
- async function readJson(req: http.IncomingMessage): Promise<Record<string, unknown>> {
319
- const chunks: Buffer[] = []
320
- for await (const chunk of req) {
321
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
322
- }
323
- const raw = Buffer.concat(chunks).toString('utf8')
324
- const parsed = raw ? JSON.parse(raw) as unknown : {}
325
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('request body must be a JSON object')
326
- return parsed as Record<string, unknown>
327
- }
328
-
329
- function respondHtml(res: http.ServerResponse, body: string): void {
330
- res.writeHead(200, {
331
- 'content-type': 'text/html; charset=utf-8',
332
- 'cache-control': 'no-store',
333
- })
334
- res.end(body)
335
- }
336
-
337
- function respondJson(res: http.ServerResponse, status: number, body: Record<string, unknown>): void {
338
- res.writeHead(status, {
339
- 'content-type': 'application/json; charset=utf-8',
340
- 'cache-control': 'no-store',
341
- })
342
- res.end(JSON.stringify(body))
343
- }
344
-
345
- function parseAccount(value: unknown): Address {
346
- if (typeof value !== 'string') throw new Error('wallet account is missing')
347
- return getAddress(value)
348
- }
349
-
350
- function parseHex(value: unknown, label: string): Hex {
351
- if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is invalid`)
352
- return value as Hex
353
- }
354
-
355
- function assertSessionToken(body: Record<string, unknown>, sessionToken: string): void {
356
- if (body.sessionToken !== sessionToken) throw new Error('wallet session token is invalid')
357
- }
358
-
359
- function chainIdHex(chainId: number): Hex {
360
- return `0x${chainId.toString(16)}` as Hex
361
- }
362
-
363
- function loadWalletHtml(): string {
364
- try {
365
- return readFileSync(join(WALLET_PAGE_DIR, 'wallet.html'), 'utf8')
366
- } catch (err) {
367
- const sourcePath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', 'src', 'identity', 'wallet', 'wallet-page', 'wallet.html')
368
- try {
369
- return readFileSync(sourcePath, 'utf8')
370
- } catch {
371
- throw err
372
- }
373
- }
374
- }
375
-
376
- export function __testWalletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
377
- return walletPage(title, sessionToken, payload)
378
- }
379
-
380
- function walletPage(title: string, sessionToken: string, payload: Record<string, unknown>): string {
381
- const config = JSON.stringify({ sessionToken, ...payload }).replaceAll('<', '\\u003c')
382
- const injection = `<script>window.__WALLET_CONFIG__ = ${config};</script>`
383
- return WALLET_HTML
384
- .replace(/<title>.*?<\/title>/, `<title>${escapeHtml(title)}</title>`)
385
- .replace('<head>', `<head>\n ${injection}`)
386
- }
387
- function escapeHtml(value: string): string {
388
- return value
389
- .replaceAll('&', '&amp;')
390
- .replaceAll('<', '&lt;')
391
- .replaceAll('>', '&gt;')
392
- .replaceAll('"', '&quot;')
393
- }
1
+ export {
2
+ BrowserWalletError,
3
+ } from './browserWallet/types.js'
4
+ export type {
5
+ AccountRequest,
6
+ BrowserWalletAccount,
7
+ BrowserWalletErrorPayload,
8
+ BrowserWalletReady,
9
+ BrowserWalletSession,
10
+ BrowserWalletSignAndTransaction,
11
+ BrowserWalletSignature,
12
+ BrowserWalletTransaction,
13
+ PreparedGasFee,
14
+ PrepareTransactionGasFeeArgs,
15
+ PrepareTransactionGasFeeClient,
16
+ ReadyHandler,
17
+ SignAndTransactionRequest,
18
+ SignatureRequest,
19
+ TransactionRequest,
20
+ WalletPurpose,
21
+ } from './browserWallet/types.js'
22
+ export { prepareTransactionGasFee } from './browserWallet/gas.js'
23
+ export {
24
+ requestBrowserWalletAccount,
25
+ requestBrowserWalletSignature,
26
+ requestBrowserWalletSignatureAndTransaction,
27
+ sendBrowserWalletTransaction,
28
+ } from './browserWallet/requests.js'
29
+ export { openBrowserWalletSession } from './browserWallet/session.js'
30
+ export { __testWalletPage } from './browserWallet/html.js'
@@ -0,0 +1,5 @@
1
+ export const CLOSE_DELAY_MS = 10000
2
+ export const TX_CLOSE_DELAY_MS = 10000
3
+ export const CANCEL_CLOSE_DELAY_MS = 10000
4
+ export const WALLET_PROVIDER_WAIT_MS = 3000
5
+ export const WALLET_PROVIDER_POLL_MS = 100
@@ -0,0 +1,251 @@
1
+ import { config } from './state.js'
2
+ import type { WalletConfig, WalletTx } from './types.js'
3
+ import { CANCEL_CLOSE_DELAY_MS, CLOSE_DELAY_MS, TX_CLOSE_DELAY_MS } from './constants.js'
4
+ import { shortAddr } from './copy.js'
5
+ import {
6
+ applyFlowChrome,
7
+ approve,
8
+ cancel,
9
+ clearLastWalletError,
10
+ currentState,
11
+ errorSlot,
12
+ getLastWalletError,
13
+ initializeViewElements,
14
+ serializeWalletError,
15
+ setState,
16
+ showPreparedMessage,
17
+ statusHint,
18
+ statusText,
19
+ } from './view.js'
20
+ import {
21
+ buildTxParams,
22
+ clearCurrentWalletMethod,
23
+ currentWalletMethod,
24
+ setActiveEthereum,
25
+ waitForEthereumProvider,
26
+ walletRequest,
27
+ } from './walletProvider.js'
28
+
29
+ let closeCountdown: ReturnType<typeof setInterval> | null = null
30
+ let handlersAttached = false
31
+
32
+ async function post(path: string, body: Record<string, unknown>): Promise<any> {
33
+ const r = await fetch(path, {
34
+ method: "POST",
35
+ headers: { "content-type": "application/json" },
36
+ body: JSON.stringify({ ...(body || {}), sessionToken: config.sessionToken }),
37
+ });
38
+ const d = await r.json();
39
+ if (!r.ok || !d.ok) throw new Error(d.error || "Wallet request failed.");
40
+ return d;
41
+ }
42
+
43
+ function showCloseCountdown(delayMs: number): void {
44
+ if (closeCountdown) clearInterval(closeCountdown);
45
+ const deadline = Date.now() + delayMs;
46
+ const update = () => {
47
+ const seconds = Math.max(1, Math.ceil((deadline - Date.now()) / 1000));
48
+ statusHint.textContent = "Return to your terminal. Closing in " + seconds + "s.";
49
+ };
50
+ update();
51
+ closeCountdown = setInterval(update, 250);
52
+ }
53
+
54
+ let __sessionMode = false;
55
+ function closeSoon(delayMs?: number): void {
56
+ if (__sessionMode) {
57
+ if (closeCountdown) clearInterval(closeCountdown);
58
+ statusHint.textContent = "Waiting for next request from terminal...";
59
+ return;
60
+ }
61
+ const ms = delayMs == null ? CLOSE_DELAY_MS : delayMs;
62
+ showCloseCountdown(ms);
63
+ setTimeout(() => {
64
+ if (closeCountdown) clearInterval(closeCountdown);
65
+ try { if (window.opener && !window.opener.closed) window.opener.focus(); } catch (_) { }
66
+ window.close();
67
+ window.open("", "_self");
68
+ window.close();
69
+ }, ms);
70
+ }
71
+
72
+ async function ensureWallet(): Promise<string> {
73
+ setState("connecting");
74
+ const provider = await waitForEthereumProvider();
75
+ setActiveEthereum(provider);
76
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
77
+ const account = accounts && accounts[0];
78
+ if (!account) throw new Error("No wallet account was selected.");
79
+ if (config.expectedAccount && account.toLowerCase() !== String(config.expectedAccount).toLowerCase()) {
80
+ throw new Error(
81
+ "Switch to " + shortAddr(config.expectedAccount) + " in your wallet, then press Enter to retry."
82
+ );
83
+ }
84
+ if (!config.chainIdHex) return account;
85
+ try {
86
+ await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: config.chainIdHex }] });
87
+ } catch (err) {
88
+ const cur = await provider.request({ method: "eth_chainId" });
89
+ if (String(cur).toLowerCase() !== String(config.chainIdHex).toLowerCase()) throw err;
90
+ }
91
+ return account;
92
+ }
93
+
94
+ export async function runWalletFlow(): Promise<void> {
95
+ approve.disabled = true;
96
+ approve.hidden = true;
97
+ cancel.disabled = false;
98
+ errorSlot.innerHTML = "";
99
+ clearLastWalletError();
100
+ try {
101
+ const account = await ensureWallet();
102
+ if (config.kind === "account") {
103
+ setState("submitting");
104
+ await post("/complete", { account });
105
+ setState("done");
106
+ closeSoon();
107
+ return;
108
+ }
109
+ if (config.kind === "sign") {
110
+ const prepared = config.message ? { message: config.message } : await post("/prepare", { account });
111
+ showPreparedMessage(prepared.message);
112
+ setState("approve");
113
+ const signature = await walletRequest("personal_sign", [prepared.message, account]);
114
+ setState("submitting");
115
+ await post("/complete", { account, message: prepared.message, signature });
116
+ setState("done");
117
+ closeSoon();
118
+ return;
119
+ }
120
+ if (config.kind === "sign-transaction") {
121
+ const prepared = config.message ? { message: config.message } : await post("/prepare", { account });
122
+ showPreparedMessage(prepared.message);
123
+ setState("approve-sign", { account });
124
+ const signature = await walletRequest("personal_sign", [prepared.message, account]);
125
+ setState("preparing-transaction", { account });
126
+ const txPayload = await post("/prepare-transaction", { account, message: prepared.message, signature });
127
+ setState("approve-transaction", { account, tx: txPayload.tx });
128
+ const tx = txPayload.tx || {};
129
+ const txHash = await walletRequest("eth_sendTransaction", [buildTxParams(account, tx)]);
130
+ setState("submitting", { account, tx, txHash });
131
+ await post("/complete", { account, txHash });
132
+ setState("done", { account, tx, txHash });
133
+ closeSoon(TX_CLOSE_DELAY_MS);
134
+ return;
135
+ }
136
+ if (config.kind === "transaction") {
137
+ setState("approve", { account, tx: config.tx });
138
+ const txHash = await walletRequest("eth_sendTransaction", [buildTxParams(account, config.tx as WalletTx)]);
139
+ setState("submitting", { account, tx: config.tx, txHash });
140
+ await post("/complete", { account, txHash });
141
+ setState("done", { account, tx: config.tx, txHash });
142
+ closeSoon(TX_CLOSE_DELAY_MS);
143
+ return;
144
+ }
145
+ throw new Error("Unknown wallet request type.");
146
+ } catch (err) {
147
+ approve.disabled = false;
148
+ approve.hidden = false;
149
+ cancel.disabled = false;
150
+ const serialized = serializeWalletError(err, currentWalletMethod);
151
+ clearCurrentWalletMethod();
152
+ setState("error", serialized as Record<string, unknown>);
153
+ }
154
+ }
155
+
156
+ export async function cancelFlow(): Promise<void> {
157
+ approve.disabled = true;
158
+ cancel.disabled = true;
159
+ const lastWalletError = getLastWalletError();
160
+ if (lastWalletError) {
161
+ await post("/error", lastWalletError as Record<string, unknown>).catch(() => { });
162
+ } else {
163
+ await post("/cancel", {}).catch(() => { });
164
+ }
165
+ setState("done");
166
+ statusText.textContent = lastWalletError ? "Aborted · returning" : "Cancelled · returning";
167
+ closeSoon(CANCEL_CLOSE_DELAY_MS);
168
+ }
169
+
170
+ function escapeAllowed(): boolean {
171
+ return currentState !== "submitting" && currentState !== "done";
172
+ }
173
+
174
+ export function startSessionMode(): void {
175
+ __sessionMode = true;
176
+ setState("connecting");
177
+ statusText.textContent = "Waiting for terminal...";
178
+ const reconnectHint = document.getElementById("reconnect-hint") as HTMLElement | null;
179
+ const showReconnecting = (visible: boolean) => {
180
+ if (!reconnectHint) return;
181
+ reconnectHint.hidden = !visible;
182
+ };
183
+ const events = new EventSource("/events");
184
+ const clearReconnect = () => showReconnecting(false);
185
+ events.addEventListener("prompt", (ev: MessageEvent) => {
186
+ clearReconnect();
187
+ try {
188
+ const next = JSON.parse(ev.data);
189
+ for (const k of Object.keys(config)) delete (config as any)[k];
190
+ Object.assign(config, next);
191
+ applyFlowChrome();
192
+ setTimeout(runWalletFlow, 50);
193
+ } catch (err) {
194
+ setState("error", { message: (err && (err as Error).message) || String(err) });
195
+ }
196
+ });
197
+ events.addEventListener("done", () => {
198
+ clearReconnect();
199
+ events.close();
200
+ __sessionMode = false;
201
+ setState("done");
202
+ statusText.textContent = "All set · Returning";
203
+ closeSoon();
204
+ });
205
+ events.onerror = () => {
206
+ if (events.readyState === EventSource.CLOSED) {
207
+ clearReconnect();
208
+ return;
209
+ }
210
+ showReconnecting(true);
211
+ };
212
+ }
213
+
214
+ function attachWalletHandlers(): void {
215
+ if (handlersAttached) return
216
+ handlersAttached = true
217
+ approve.onclick = runWalletFlow
218
+ cancel.onclick = cancelFlow
219
+ window.addEventListener('keydown', (e) => {
220
+ if (e.key === 'Escape') {
221
+ if (!escapeAllowed()) { e.preventDefault(); return }
222
+ e.preventDefault()
223
+ cancelFlow()
224
+ } else if (e.key === 'Enter') {
225
+ if (!approve.hidden && !approve.disabled) { e.preventDefault(); runWalletFlow(); return }
226
+ }
227
+ })
228
+ }
229
+
230
+ export function bootWallet(): void {
231
+ initializeViewElements()
232
+ attachWalletHandlers()
233
+ applyFlowChrome()
234
+ if (!window.__WALLET_PREVIEW__) {
235
+ if (document.readyState === 'loading') {
236
+ document.addEventListener('DOMContentLoaded', bootWallet, { once: true })
237
+ } else {
238
+ if (config && config.kind === 'session-wait') {
239
+ startSessionMode()
240
+ } else {
241
+ runWalletFlow()
242
+ }
243
+ }
244
+ } else {
245
+ window.__walletPreview = {
246
+ setState,
247
+ setConfig: (c: Partial<WalletConfig>) => { Object.assign(config, c); applyFlowChrome() },
248
+ }
249
+ runWalletFlow()
250
+ }
251
+ }