ethagent 1.1.1 → 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 (271) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +127 -29
  3. package/package.json +16 -9
  4. package/src/app/FirstRun.tsx +192 -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 +43 -18
  11. package/src/chat/ContextLimitView.tsx +4 -4
  12. package/src/chat/ContinuityEditReviewView.tsx +11 -17
  13. package/src/chat/ConversationStack.tsx +3 -0
  14. package/src/chat/CopyPicker.tsx +0 -1
  15. package/src/chat/MessageList.tsx +62 -45
  16. package/src/chat/PermissionPrompt.tsx +13 -9
  17. package/src/chat/PlanApprovalView.tsx +3 -3
  18. package/src/chat/ResumeView.tsx +1 -4
  19. package/src/chat/RewindView.tsx +2 -2
  20. package/src/chat/TranscriptView.tsx +6 -0
  21. package/src/chat/chatInputState.ts +1 -1
  22. package/src/chat/chatScreenUtils.ts +22 -11
  23. package/src/chat/chatSessionState.ts +2 -2
  24. package/src/chat/chatTurnOrchestrator.ts +16 -81
  25. package/src/chat/commands.ts +1 -1
  26. package/src/chat/textCursor.ts +1 -1
  27. package/src/chat/transcriptViewport.ts +2 -7
  28. package/src/cli/ResetConfirmView.tsx +1 -1
  29. package/src/cli/main.tsx +9 -3
  30. package/src/cli/preview.tsx +0 -5
  31. package/src/cli/updateNotice.ts +5 -3
  32. package/src/identity/continuity/editor.ts +7 -107
  33. package/src/identity/continuity/envelope.ts +1048 -40
  34. package/src/identity/continuity/history.ts +4 -4
  35. package/src/identity/continuity/localBackup.ts +249 -0
  36. package/src/identity/continuity/privateEdit/apply.ts +170 -0
  37. package/src/identity/continuity/privateEdit/diff.ts +82 -0
  38. package/src/identity/continuity/privateEdit/files.ts +23 -0
  39. package/src/identity/continuity/privateEdit/types.ts +28 -0
  40. package/src/identity/continuity/privateEdit.ts +10 -298
  41. package/src/identity/continuity/publicSkills.ts +8 -9
  42. package/src/identity/continuity/snapshots.ts +17 -6
  43. package/src/identity/continuity/storage/defaults.ts +111 -0
  44. package/src/identity/continuity/storage/files.ts +72 -0
  45. package/src/identity/continuity/storage/markdown.ts +81 -0
  46. package/src/identity/continuity/storage/paths.ts +24 -0
  47. package/src/identity/continuity/storage/scaffold.ts +124 -0
  48. package/src/identity/continuity/storage/status.ts +86 -0
  49. package/src/identity/continuity/storage/types.ts +27 -0
  50. package/src/identity/continuity/storage.ts +32 -507
  51. package/src/identity/continuity/zipWriter.ts +95 -0
  52. package/src/identity/crypto/backupEnvelope.ts +14 -247
  53. package/src/identity/crypto/eth.ts +7 -7
  54. package/src/identity/ens/agentRecords.ts +96 -0
  55. package/src/identity/ens/ensAutomation/contracts.ts +38 -0
  56. package/src/identity/ens/ensAutomation/delete.ts +80 -0
  57. package/src/identity/ens/ensAutomation/names.ts +14 -0
  58. package/src/identity/ens/ensAutomation/operators.ts +29 -0
  59. package/src/identity/ens/ensAutomation/read.ts +114 -0
  60. package/src/identity/ens/ensAutomation/root.ts +63 -0
  61. package/src/identity/ens/ensAutomation/setup.ts +284 -0
  62. package/src/identity/ens/ensAutomation/transactions.ts +107 -0
  63. package/src/identity/ens/ensAutomation/types.ts +126 -0
  64. package/src/identity/ens/ensAutomation.ts +29 -0
  65. package/src/identity/ens/ensLookup/client.ts +43 -0
  66. package/src/identity/ens/ensLookup/constants.ts +26 -0
  67. package/src/identity/ens/ensLookup/discovery.ts +70 -0
  68. package/src/identity/ens/ensLookup/names.ts +34 -0
  69. package/src/identity/ens/ensLookup/records.ts +45 -0
  70. package/src/identity/ens/ensLookup/resolve.ts +75 -0
  71. package/src/identity/ens/ensLookup/tokenReference.ts +17 -0
  72. package/src/identity/ens/ensLookup/types.ts +38 -0
  73. package/src/identity/ens/ensLookup/validation.ts +72 -0
  74. package/src/identity/ens/ensLookup.ts +19 -0
  75. package/src/identity/ens/ensRegistration.ts +199 -0
  76. package/src/identity/ens/resolverDelegation.ts +48 -0
  77. package/src/identity/hub/IdentityHub.tsx +13 -815
  78. package/src/identity/hub/OperationalRoutes.tsx +370 -0
  79. package/src/identity/hub/Routes.tsx +361 -0
  80. package/src/identity/hub/advancedEnsValidation.ts +45 -0
  81. package/src/identity/hub/{screens → components}/DetailsScreen.tsx +14 -8
  82. package/src/identity/hub/{screens → components}/ErrorScreen.tsx +15 -5
  83. package/src/identity/hub/components/FlowTimeline.tsx +27 -0
  84. package/src/identity/hub/components/IdentitySummary.tsx +190 -0
  85. package/src/identity/hub/components/MenuScreen.tsx +237 -0
  86. package/src/identity/hub/{screens → components}/NetworkScreen.tsx +3 -3
  87. package/src/identity/hub/{screens/RebackupStorageScreen.tsx → components/PinataJwtInput.tsx} +21 -18
  88. package/src/identity/hub/components/UnlinkedIdentityScreen.tsx +76 -0
  89. package/src/identity/hub/{screens → components}/WalletApprovalScreen.tsx +9 -8
  90. package/src/identity/hub/components/menuFlagsFromReconciliation.ts +68 -0
  91. package/src/identity/hub/effects/create.ts +310 -0
  92. package/src/identity/hub/effects/ens/flows.ts +218 -0
  93. package/src/identity/hub/effects/ens/index.ts +11 -0
  94. package/src/identity/hub/effects/ens/transactions.ts +239 -0
  95. package/src/identity/hub/effects/index.ts +74 -0
  96. package/src/identity/hub/effects/profile/profileState.ts +173 -0
  97. package/src/identity/hub/effects/publicProfile/index.ts +5 -0
  98. package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +646 -0
  99. package/src/identity/hub/effects/rebackup/index.ts +7 -0
  100. package/src/identity/hub/effects/rebackup/operatorVault.ts +378 -0
  101. package/src/identity/hub/effects/rebackup/runRebackup.ts +451 -0
  102. package/src/identity/hub/effects/receipts.ts +46 -0
  103. package/src/identity/hub/effects/restore/apply.ts +112 -0
  104. package/src/identity/hub/effects/restore/auth.ts +159 -0
  105. package/src/identity/hub/effects/restore/discover.ts +86 -0
  106. package/src/identity/hub/effects/restore/envelopes.ts +21 -0
  107. package/src/identity/hub/effects/restore/fetch.ts +25 -0
  108. package/src/identity/hub/effects/restore/index.ts +22 -0
  109. package/src/identity/hub/effects/restore/recovery.ts +135 -0
  110. package/src/identity/hub/effects/restore/resolve.ts +102 -0
  111. package/src/identity/hub/effects/restore/restoreEffects.ts +22 -0
  112. package/src/identity/hub/effects/restore/shared.ts +91 -0
  113. package/src/identity/hub/effects/restoreAdmin.ts +93 -0
  114. package/src/identity/hub/effects/shared/profilePrep.ts +139 -0
  115. package/src/identity/hub/effects/shared/snapshot.ts +336 -0
  116. package/src/identity/hub/effects/shared/sync.ts +190 -0
  117. package/src/identity/hub/effects/token-transfer/index.ts +6 -0
  118. package/src/identity/hub/effects/token-transfer/progress.ts +59 -0
  119. package/src/identity/hub/effects/token-transfer/runTokenTransfer.ts +299 -0
  120. package/src/identity/hub/effects/types.ts +53 -0
  121. package/src/identity/hub/effects/vault/preflight.ts +50 -0
  122. package/src/identity/hub/flows/continuity/ContinuityDashboardScreen.tsx +170 -0
  123. package/src/identity/hub/flows/continuity/RebackupStorageScreen.tsx +28 -0
  124. package/src/identity/hub/flows/continuity/RecoveryConfirmScreen.tsx +104 -0
  125. package/src/identity/hub/flows/continuity/SavePromptScreen.tsx +49 -0
  126. package/src/identity/hub/{screens → flows/create}/CreateFlow.tsx +61 -62
  127. package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +347 -0
  128. package/src/identity/hub/flows/custody/custodyEffects.ts +321 -0
  129. package/src/identity/hub/flows/custody/custodyFlowActions.ts +236 -0
  130. package/src/identity/hub/flows/custody/custodyFlowEffects.ts +163 -0
  131. package/src/identity/hub/flows/custody/custodyFlowHelpers.ts +25 -0
  132. package/src/identity/hub/flows/custody/custodyFlowRoutes.tsx +239 -0
  133. package/src/identity/hub/flows/custody/custodyFlowTypes.ts +45 -0
  134. package/src/identity/hub/flows/custody/useCustodyFlow.tsx +25 -0
  135. package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +336 -0
  136. package/src/identity/hub/flows/ens/EnsEditFlow.tsx +397 -0
  137. package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +332 -0
  138. package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +471 -0
  139. package/src/identity/hub/flows/ens/EnsEditRunners.tsx +198 -0
  140. package/src/identity/hub/flows/ens/EnsEditShared.tsx +162 -0
  141. package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +518 -0
  142. package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +299 -0
  143. package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +398 -0
  144. package/src/identity/hub/flows/ens/ensEditCopy.ts +117 -0
  145. package/src/identity/hub/flows/ens/ensEditTypes.ts +91 -0
  146. package/src/identity/hub/flows/profile/EditProfileFlow.tsx +271 -0
  147. package/src/identity/hub/flows/restore/RestoreFlow.tsx +324 -0
  148. package/src/identity/hub/flows/restore/useRestoreFlowEffects.ts +77 -0
  149. package/src/identity/hub/{screens → flows/settings}/StorageCredentialScreen.tsx +25 -43
  150. package/src/identity/hub/flows/token-transfer/IdentityHubTokenTransferFlow.tsx +162 -0
  151. package/src/identity/hub/flows/token-transfer/TokenTransferScreens.tsx +256 -0
  152. package/src/identity/hub/identityHubReducer.ts +166 -101
  153. package/src/identity/hub/model/continuity.ts +94 -0
  154. package/src/identity/hub/model/copy.ts +35 -0
  155. package/src/identity/hub/model/custody.ts +54 -0
  156. package/src/identity/hub/model/ens.ts +49 -0
  157. package/src/identity/hub/model/errors.ts +140 -0
  158. package/src/identity/hub/model/format.ts +15 -0
  159. package/src/identity/hub/model/identity.ts +94 -0
  160. package/src/identity/hub/model/network.ts +32 -0
  161. package/src/identity/hub/model/transfer.ts +57 -0
  162. package/src/identity/hub/operatorWallets.ts +131 -0
  163. package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +46 -0
  164. package/src/identity/hub/reconciliation/agentReconciliation/ownership.ts +129 -0
  165. package/src/identity/hub/reconciliation/agentReconciliation/run.ts +302 -0
  166. package/src/identity/hub/reconciliation/agentReconciliation/types.ts +17 -0
  167. package/src/identity/hub/reconciliation/index.ts +21 -0
  168. package/src/identity/hub/reconciliation/useAgentReconciliation.ts +10 -0
  169. package/src/identity/hub/reconciliation/walletSetup.ts +220 -0
  170. package/src/identity/hub/txGuard.ts +51 -0
  171. package/src/identity/hub/types.ts +17 -0
  172. package/src/identity/hub/useIdentityHubContinuity.ts +136 -0
  173. package/src/identity/hub/useIdentityHubController.ts +396 -0
  174. package/src/identity/hub/useIdentityHubSideEffects.ts +309 -0
  175. package/src/identity/hub/utils.ts +79 -0
  176. package/src/identity/identityCompat.ts +34 -0
  177. package/src/identity/profile/agentIcon.ts +61 -0
  178. package/src/identity/profile/imagePicker.ts +12 -12
  179. package/src/identity/registry/erc8004/abi.ts +14 -0
  180. package/src/identity/registry/erc8004/chains.ts +150 -0
  181. package/src/identity/registry/erc8004/client.ts +11 -0
  182. package/src/identity/registry/erc8004/discovery.ts +511 -0
  183. package/src/identity/registry/erc8004/metadata.ts +335 -0
  184. package/src/identity/registry/erc8004/ownership.ts +121 -0
  185. package/src/identity/registry/erc8004/preflight.ts +123 -0
  186. package/src/identity/registry/erc8004/transactions.ts +77 -0
  187. package/src/identity/registry/erc8004/types.ts +88 -0
  188. package/src/identity/registry/erc8004/uri.ts +59 -0
  189. package/src/identity/registry/erc8004/utils.ts +58 -0
  190. package/src/identity/registry/erc8004.ts +53 -1106
  191. package/src/identity/registry/fieldParsers.ts +28 -0
  192. package/src/identity/registry/operatorVault/bytecode.ts +98 -0
  193. package/src/identity/registry/operatorVault/constants.ts +38 -0
  194. package/src/identity/registry/operatorVault/read.ts +246 -0
  195. package/src/identity/registry/operatorVault/transactions.ts +81 -0
  196. package/src/identity/registry/operatorVault.ts +44 -0
  197. package/src/identity/storage/ipfs.ts +26 -24
  198. package/src/identity/wallet/browserWallet/gas.ts +41 -0
  199. package/src/identity/wallet/browserWallet/html.ts +106 -0
  200. package/src/identity/wallet/browserWallet/http.ts +28 -0
  201. package/src/identity/wallet/browserWallet/requestServer.ts +106 -0
  202. package/src/identity/wallet/browserWallet/requests.ts +191 -0
  203. package/src/identity/wallet/browserWallet/session.ts +325 -0
  204. package/src/identity/wallet/browserWallet/types.ts +192 -0
  205. package/src/identity/wallet/browserWallet/validation.ts +74 -0
  206. package/src/identity/wallet/browserWallet.ts +30 -393
  207. package/src/identity/wallet/page/constants.ts +5 -0
  208. package/src/identity/wallet/page/controller.ts +251 -0
  209. package/src/identity/wallet/page/copy.ts +340 -0
  210. package/src/identity/wallet/page/grainient.ts +278 -0
  211. package/src/identity/wallet/page/html.ts +28 -0
  212. package/src/identity/wallet/page/markup.ts +50 -0
  213. package/src/identity/wallet/page/state.ts +9 -0
  214. package/src/identity/wallet/page/styles/base.ts +259 -0
  215. package/src/identity/wallet/page/styles/components.ts +262 -0
  216. package/src/identity/wallet/page/styles/index.ts +5 -0
  217. package/src/identity/wallet/page/styles/responsive.ts +247 -0
  218. package/src/identity/wallet/page/types.ts +47 -0
  219. package/src/identity/wallet/page/view.ts +535 -0
  220. package/src/identity/wallet/page/walletProvider.ts +70 -0
  221. package/src/identity/wallet/page.tsx +38 -0
  222. package/src/identity/wallet/walletPurposeCompat.ts +27 -0
  223. package/src/mcp/manager.ts +0 -1
  224. package/src/models/ModelPicker.tsx +36 -30
  225. package/src/models/catalog.ts +5 -2
  226. package/src/models/huggingface.ts +9 -9
  227. package/src/models/llamacpp.ts +13 -13
  228. package/src/models/modelDisplay.ts +75 -0
  229. package/src/models/modelPickerOptions.ts +16 -3
  230. package/src/models/modelRecommendation.ts +0 -1
  231. package/src/providers/errors.ts +16 -0
  232. package/src/providers/gemini.ts +252 -39
  233. package/src/providers/registry.ts +2 -2
  234. package/src/providers/retry.ts +1 -1
  235. package/src/runtime/sessionMode.ts +1 -1
  236. package/src/runtime/systemPrompt.ts +2 -0
  237. package/src/runtime/toolExecution.ts +18 -22
  238. package/src/runtime/toolIntent.ts +0 -20
  239. package/src/runtime/turn.ts +0 -92
  240. package/src/storage/atomicWrite.ts +4 -1
  241. package/src/storage/config.ts +181 -5
  242. package/src/storage/identity.ts +9 -3
  243. package/src/storage/secrets.ts +2 -2
  244. package/src/tools/bashSafety.ts +8 -0
  245. package/src/tools/changeDirectoryTool.ts +1 -1
  246. package/src/tools/deleteFileTool.ts +4 -4
  247. package/src/tools/editTool.ts +4 -4
  248. package/src/tools/editUtils.ts +5 -5
  249. package/src/tools/privateContinuityEditTool.ts +4 -5
  250. package/src/tools/privateContinuityReadTool.ts +1 -2
  251. package/src/tools/registry.ts +30 -0
  252. package/src/tools/writeFileTool.ts +5 -5
  253. package/src/ui/BrandSplash.tsx +20 -85
  254. package/src/ui/ProgressBar.tsx +3 -5
  255. package/src/ui/Select.tsx +21 -9
  256. package/src/ui/Spinner.tsx +38 -3
  257. package/src/ui/Surface.tsx +3 -3
  258. package/src/ui/TextInput.tsx +191 -29
  259. package/src/ui/theme.ts +7 -34
  260. package/src/utils/openExternal.ts +21 -0
  261. package/src/utils/withRetry.ts +47 -3
  262. package/src/identity/hub/identityHubEffects.ts +0 -937
  263. package/src/identity/hub/identityHubModel.ts +0 -291
  264. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +0 -144
  265. package/src/identity/hub/screens/EditProfileFlow.tsx +0 -145
  266. package/src/identity/hub/screens/IdentitySummary.tsx +0 -90
  267. package/src/identity/hub/screens/MenuScreen.tsx +0 -117
  268. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +0 -87
  269. package/src/identity/hub/screens/RestoreFlow.tsx +0 -206
  270. package/src/identity/wallet/wallet-page/wallet.html +0 -1202
  271. /package/src/identity/hub/{screens → components}/BusyScreen.tsx +0 -0
@@ -0,0 +1,28 @@
1
+
2
+ export function objectField(input: Record<string, unknown> | null | undefined, key: string): Record<string, unknown> | null {
3
+ if (!input) return null
4
+ const value = input[key]
5
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
6
+ return value as Record<string, unknown>
7
+ }
8
+
9
+ export function arrayField(input: Record<string, unknown> | null | undefined, key: string): Array<unknown> | null {
10
+ if (!input) return null
11
+ const value = input[key]
12
+ return Array.isArray(value) ? value : null
13
+ }
14
+
15
+ export function stringField(input: Record<string, unknown> | null | undefined, key: string): string | undefined {
16
+ const value = input?.[key]
17
+ return typeof value === 'string' && value.length > 0 ? value : undefined
18
+ }
19
+
20
+ export function numberField(input: Record<string, unknown> | null | undefined, key: string): number | undefined {
21
+ const value = input?.[key]
22
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined
23
+ }
24
+
25
+ export function booleanField(input: Record<string, unknown> | null | undefined, key: string): boolean | undefined {
26
+ const value = input?.[key]
27
+ return typeof value === 'boolean' ? value : undefined
28
+ }
@@ -0,0 +1,98 @@
1
+ import { getAddress, keccak256, type Address, type Hex, type PublicClient } from 'viem'
2
+ import { OPERATOR_VAULT_RUNTIME_BYTECODE, OPERATOR_VAULT_RUNTIME_BYTECODE_HASH } from './constants.js'
3
+
4
+ export class OperatorVaultBytecodeMismatchError extends Error {
5
+ readonly vaultAddress: Address
6
+ readonly observedHash: Hex | null
7
+ readonly observedLength: number
8
+ readonly expectedHash: Hex
9
+ readonly expectedLength: number
10
+ readonly txHash?: Hex
11
+ constructor(
12
+ vaultAddress: Address,
13
+ observedHash: Hex | null,
14
+ observedLength: number,
15
+ txHash?: Hex,
16
+ ) {
17
+ super(
18
+ 'Deployed contract bytecode does not match the expected operator delegation vault. The deploy transaction may have been intercepted.',
19
+ )
20
+ this.name = 'OperatorVaultBytecodeMismatchError'
21
+ this.vaultAddress = vaultAddress
22
+ this.observedHash = observedHash
23
+ this.observedLength = observedLength
24
+ this.expectedHash = OPERATOR_VAULT_RUNTIME_BYTECODE_HASH
25
+ this.expectedLength = (OPERATOR_VAULT_RUNTIME_BYTECODE.length - 2) / 2
26
+ if (txHash) this.txHash = txHash
27
+ }
28
+ }
29
+
30
+ export type AssertVaultBytecodeClient = Pick<PublicClient, 'getBytecode'>
31
+
32
+ export const OPERATOR_VAULT_POLL_MAX_ATTEMPTS = 5
33
+ export const OPERATOR_VAULT_POLL_DELAY_MS = 1500
34
+ export function delayMs(ms: number): Promise<void> {
35
+ return new Promise(resolve => setTimeout(resolve, ms))
36
+ }
37
+
38
+ async function readVaultBytecodeWithPoll(
39
+ client: AssertVaultBytecodeClient,
40
+ address: Address,
41
+ ): Promise<Hex | undefined> {
42
+ let lastErr: unknown
43
+ let lastCode: Hex | undefined
44
+ for (let attempt = 0; attempt < OPERATOR_VAULT_POLL_MAX_ATTEMPTS; attempt++) {
45
+ if (attempt > 0) await delayMs(OPERATOR_VAULT_POLL_DELAY_MS)
46
+ try {
47
+ const code = await client.getBytecode({ address })
48
+ lastErr = undefined
49
+ lastCode = code
50
+ const isEmpty = !code || code === '0x'
51
+ if (!isEmpty) return code
52
+ } catch (err) {
53
+ lastErr = err
54
+ lastCode = undefined
55
+ }
56
+ }
57
+ if (lastErr) throw lastErr
58
+ return lastCode
59
+ }
60
+
61
+ export async function assertVaultBytecode(
62
+ client: AssertVaultBytecodeClient,
63
+ vaultAddress: Address,
64
+ txHash?: Hex,
65
+ ): Promise<void> {
66
+ const address = getAddress(vaultAddress)
67
+ const code = await readVaultBytecodeWithPoll(client, address)
68
+ if (!code || code === '0x') {
69
+ throw new OperatorVaultBytecodeMismatchError(address, null, 0, txHash)
70
+ }
71
+ const observedLength = (code.length - 2) / 2
72
+ const observed = keccak256(code).toLowerCase() as Hex
73
+ const expected = OPERATOR_VAULT_RUNTIME_BYTECODE_HASH.toLowerCase() as Hex
74
+ if (observed !== expected) {
75
+ throw new OperatorVaultBytecodeMismatchError(address, observed, observedLength, txHash)
76
+ }
77
+ }
78
+
79
+ function shortHash(hash: Hex): string {
80
+ return `${hash.slice(0, 18)}...${hash.slice(-6)}`
81
+ }
82
+
83
+ export function formatOperatorVaultBytecodeMismatchDetail(
84
+ err: OperatorVaultBytecodeMismatchError,
85
+ ): string {
86
+ const lines = [
87
+ `Vault address: ${err.vaultAddress}`,
88
+ ]
89
+ if (err.txHash) lines.push(`Deploy tx: ${err.txHash}`)
90
+ lines.push(`Expected hash: ${shortHash(err.expectedHash)}`)
91
+ if (err.observedHash) {
92
+ lines.push(`Observed hash: ${shortHash(err.observedHash)}`)
93
+ lines.push(`Observed length: ${err.observedLength} bytes (expected ${err.expectedLength})`)
94
+ } else {
95
+ lines.push(`Observed code: none. Address has no code.`)
96
+ }
97
+ return lines.join('\n')
98
+ }
@@ -0,0 +1,38 @@
1
+ import { getAddress, keccak256, parseAbi, type Address, type Hex } from 'viem'
2
+
3
+ export const OPERATOR_VAULT_ABI = parseAbi([
4
+ 'constructor(address registry, uint256 agentId)',
5
+ 'function agentOwner(address registry, uint256 agentId) view returns (address)',
6
+ 'function metadataOperators(address registry, uint256 agentId, address operator) view returns (bool)',
7
+ 'function heldAgent() view returns (address registry, uint256 agentId, address owner)',
8
+ 'function setMetadataOperator(address registry, uint256 agentId, address operator, bool approved)',
9
+ 'function unwrap(address registry, uint256 agentId, address recipient)',
10
+ 'function rotateAgentURI(address registry, uint256 agentId, string newURI)',
11
+ 'event AgentDeposited(address indexed registry, uint256 indexed agentId, address indexed owner)',
12
+ 'event AgentUnwrapped(address indexed registry, uint256 indexed agentId, address indexed recipient)',
13
+ 'event MetadataOperatorChanged(address indexed registry, uint256 indexed agentId, address indexed operator, bool approved)',
14
+ 'event AgentURIRotated(address indexed registry, uint256 indexed agentId, string newURI, address indexed signer)',
15
+ ])
16
+
17
+ export const OPERATOR_VAULT_ADDRESSES: Record<number, Address> = {
18
+ }
19
+
20
+ export function operatorVaultAddressForChain(chainId: number): Address | undefined {
21
+ const address = OPERATOR_VAULT_ADDRESSES[chainId]
22
+ return address ? getAddress(address) : undefined
23
+ }
24
+
25
+ export function resolveConfiguredOperatorVaultAddress(
26
+ operatorVaults: Readonly<Record<string, string>> | undefined,
27
+ chainId: number,
28
+ ): Address | undefined {
29
+ const fromConfig = operatorVaults?.[String(chainId)]
30
+ if (fromConfig) return getAddress(fromConfig)
31
+ return operatorVaultAddressForChain(chainId)
32
+ }
33
+
34
+ export const OPERATOR_VAULT_DEPLOY_BYTECODE: Hex = '0x608060405234801561001057600080fd5b5060405161090538038061090583398101604081905261002f91610058565b600080546001600160a01b0319166001600160a01b039390931692909217909155600155610092565b6000806040838503121561006b57600080fd5b82516001600160a01b038116811461008257600080fd5b6020939093015192949293505050565b610864806100a16000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c80631c3bb2851161005b5780631c3bb2851461011157806324a3e2a1146101265780639a61ce8114610149578063b68f43451461015c57600080fd5b8063029ae47e146100825780631400a7e2146100b2578063150b7a02146100e5575b600080fd5b610095610090366004610602565b61016f565b6040516001600160a01b0390911681526020015b60405180910390f35b600254600354600454604080516001600160a01b03948516815260208101939093529216918101919091526060016100a9565b6100f86100f3366004610675565b61019a565b6040516001600160e01b031990911681526020016100a9565b61012461011f3660046106e4565b610293565b005b61013961013436600461073e565b610397565b60405190151581526020016100a9565b61012461015736600461077a565b6103d0565b61012461016a36600461073e565b6104a2565b600061017b83836105ab565b610186576000610193565b6004546001600160a01b03165b9392505050565b600080546001600160a01b0316331415806101b757506001548414155b156101d557604051639667ffdf60e01b815260040160405180910390fd5b6004546001600160a01b0316156101ff5760405163d5a8211560e01b815260040160405180910390fd5b60028054336001600160a01b0319918216179091556003859055600480549091166001600160a01b0387161790556005805460018101909155600003600019016102495760016005555b6040516001600160a01b03861690859033907f7b19af5fe09a0be4ed70b569401ce6fa0de072757b228cc2088350eccc2f987390600090a450630a85bd0160e11b95945050505050565b600061029f858561016f565b9050336001600160a01b038216148015906102c257506102c0858533610397565b155b156102e05760405163ea8e4eb560e01b815260040160405180910390fd5b604051630af28bd360e01b81526001600160a01b03861690630af28bd390610310908790879087906004016107f7565b600060405180830381600087803b15801561032a57600080fd5b505af115801561033e573d6000803e3d6000fd5b50505050336001600160a01b031684866001600160a01b03167fa455a129bb4260e5274d34dd213bcd11399667f80a0615e6cb375728d2e75a90868660405161038892919061081a565b60405180910390a45050505050565b60006103a384846105ab565b80156103c857506005546001600160a01b038316600090815260066020526040902054145b949350505050565b6103da848461016f565b6001600160a01b0316336001600160a01b03161461040b576040516330cd747160e01b815260040160405180910390fd5b8015610432576005546001600160a01b03831660009081526006602052604090205561044c565b6001600160a01b0382166000908152600660205260408120555b816001600160a01b031683856001600160a01b03167fea7d7802a5cc5520abde4345007961a980fcb27dca3030ca6aede6bdc58bd79e84604051610494911515815260200190565b60405180910390a450505050565b60006104ae848461016f565b9050336001600160a01b038216146104d9576040516330cd747160e01b815260040160405180910390fd5b600280546001600160a01b03199081169091556000600355600480549091168155604051632142170760e11b815230918101919091526001600160a01b038381166024830152604482018590528516906342842e0e90606401600060405180830381600087803b15801561054c57600080fd5b505af1158015610560573d6000803e3d6000fd5b50505050816001600160a01b031683856001600160a01b03167fa433e9204e6763dff9274d8c1febdbc7c60aa7d7e85c106d5b978372658438b860405160405180910390a450505050565b6004546000906001600160a01b0316158015906105d557506002546001600160a01b038481169116145b801561019357505060035414919050565b80356001600160a01b03811681146105fd57600080fd5b919050565b6000806040838503121561061557600080fd5b61061e836105e6565b946020939093013593505050565b60008083601f84011261063e57600080fd5b50813567ffffffffffffffff81111561065657600080fd5b60208301915083602082850101111561066e57600080fd5b9250929050565b60008060008060006080868803121561068d57600080fd5b610696866105e6565b94506106a4602087016105e6565b935060408601359250606086013567ffffffffffffffff8111156106c757600080fd5b6106d38882890161062c565b969995985093965092949392505050565b600080600080606085870312156106fa57600080fd5b610703856105e6565b935060208501359250604085013567ffffffffffffffff81111561072657600080fd5b6107328782880161062c565b95989497509550505050565b60008060006060848603121561075357600080fd5b61075c846105e6565b925060208401359150610771604085016105e6565b90509250925092565b6000806000806080858703121561079057600080fd5b610799856105e6565b9350602085013592506107ae604086016105e6565b9150606085013580151581146107c357600080fd5b939692955090935050565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b8381526040602082015260006108116040830184866107ce565b95945050505050565b6020815260006103c86020830184866107ce56fea2646970667358221220dd5405c74ca871baa3fc981f170a85015cbe31e79a0e1034acc7154f51ae011664736f6c63430008180033'
35
+
36
+ export const OPERATOR_VAULT_RUNTIME_BYTECODE: Hex = '0x608060405234801561001057600080fd5b506004361061007d5760003560e01c80631c3bb2851161005b5780631c3bb2851461011157806324a3e2a1146101265780639a61ce8114610149578063b68f43451461015c57600080fd5b8063029ae47e146100825780631400a7e2146100b2578063150b7a02146100e5575b600080fd5b610095610090366004610602565b61016f565b6040516001600160a01b0390911681526020015b60405180910390f35b600254600354600454604080516001600160a01b03948516815260208101939093529216918101919091526060016100a9565b6100f86100f3366004610675565b61019a565b6040516001600160e01b031990911681526020016100a9565b61012461011f3660046106e4565b610293565b005b61013961013436600461073e565b610397565b60405190151581526020016100a9565b61012461015736600461077a565b6103d0565b61012461016a36600461073e565b6104a2565b600061017b83836105ab565b610186576000610193565b6004546001600160a01b03165b9392505050565b600080546001600160a01b0316331415806101b757506001548414155b156101d557604051639667ffdf60e01b815260040160405180910390fd5b6004546001600160a01b0316156101ff5760405163d5a8211560e01b815260040160405180910390fd5b60028054336001600160a01b0319918216179091556003859055600480549091166001600160a01b0387161790556005805460018101909155600003600019016102495760016005555b6040516001600160a01b03861690859033907f7b19af5fe09a0be4ed70b569401ce6fa0de072757b228cc2088350eccc2f987390600090a450630a85bd0160e11b95945050505050565b600061029f858561016f565b9050336001600160a01b038216148015906102c257506102c0858533610397565b155b156102e05760405163ea8e4eb560e01b815260040160405180910390fd5b604051630af28bd360e01b81526001600160a01b03861690630af28bd390610310908790879087906004016107f7565b600060405180830381600087803b15801561032a57600080fd5b505af115801561033e573d6000803e3d6000fd5b50505050336001600160a01b031684866001600160a01b03167fa455a129bb4260e5274d34dd213bcd11399667f80a0615e6cb375728d2e75a90868660405161038892919061081a565b60405180910390a45050505050565b60006103a384846105ab565b80156103c857506005546001600160a01b038316600090815260066020526040902054145b949350505050565b6103da848461016f565b6001600160a01b0316336001600160a01b03161461040b576040516330cd747160e01b815260040160405180910390fd5b8015610432576005546001600160a01b03831660009081526006602052604090205561044c565b6001600160a01b0382166000908152600660205260408120555b816001600160a01b031683856001600160a01b03167fea7d7802a5cc5520abde4345007961a980fcb27dca3030ca6aede6bdc58bd79e84604051610494911515815260200190565b60405180910390a450505050565b60006104ae848461016f565b9050336001600160a01b038216146104d9576040516330cd747160e01b815260040160405180910390fd5b600280546001600160a01b03199081169091556000600355600480549091168155604051632142170760e11b815230918101919091526001600160a01b038381166024830152604482018590528516906342842e0e90606401600060405180830381600087803b15801561054c57600080fd5b505af1158015610560573d6000803e3d6000fd5b50505050816001600160a01b031683856001600160a01b03167fa433e9204e6763dff9274d8c1febdbc7c60aa7d7e85c106d5b978372658438b860405160405180910390a450505050565b6004546000906001600160a01b0316158015906105d557506002546001600160a01b038481169116145b801561019357505060035414919050565b80356001600160a01b03811681146105fd57600080fd5b919050565b6000806040838503121561061557600080fd5b61061e836105e6565b946020939093013593505050565b60008083601f84011261063e57600080fd5b50813567ffffffffffffffff81111561065657600080fd5b60208301915083602082850101111561066e57600080fd5b9250929050565b60008060008060006080868803121561068d57600080fd5b610696866105e6565b94506106a4602087016105e6565b935060408601359250606086013567ffffffffffffffff8111156106c757600080fd5b6106d38882890161062c565b969995985093965092949392505050565b600080600080606085870312156106fa57600080fd5b610703856105e6565b935060208501359250604085013567ffffffffffffffff81111561072657600080fd5b6107328782880161062c565b95989497509550505050565b60008060006060848603121561075357600080fd5b61075c846105e6565b925060208401359150610771604085016105e6565b90509250925092565b6000806000806080858703121561079057600080fd5b610799856105e6565b9350602085013592506107ae604086016105e6565b9150606085013580151581146107c357600080fd5b939692955090935050565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b8381526040602082015260006108116040830184866107ce565b95945050505050565b6020815260006103c86020830184866107ce56fea2646970667358221220dd5405c74ca871baa3fc981f170a85015cbe31e79a0e1034acc7154f51ae011664736f6c63430008180033'
37
+
38
+ export const OPERATOR_VAULT_RUNTIME_BYTECODE_HASH: Hex = keccak256(OPERATOR_VAULT_RUNTIME_BYTECODE)
@@ -0,0 +1,246 @@
1
+ import { getAddress, parseAbi, parseAbiItem, type Address, type PublicClient } from 'viem'
2
+ import { OPERATOR_VAULT_ABI } from './constants.js'
3
+ import { delayMs, OPERATOR_VAULT_POLL_DELAY_MS, OPERATOR_VAULT_POLL_MAX_ATTEMPTS } from './bytecode.js'
4
+
5
+ const DISCOVER_LOG_WINDOW_MAX_ATTEMPTS = 3
6
+ const DISCOVER_LOG_WINDOW_DELAY_MS = 2000
7
+
8
+ export type OperatorVaultReadClient = Pick<PublicClient, 'readContract'>
9
+
10
+ const ERC721_OWNER_OF_ABI = parseAbi([
11
+ 'function ownerOf(uint256 tokenId) view returns (address)',
12
+ ])
13
+
14
+ export type DiscoverPriorVaultClient = Pick<PublicClient, 'readContract' | 'getBytecode'>
15
+
16
+ export type DiscoverPriorVaultArgs = {
17
+ client: DiscoverPriorVaultClient
18
+ registry: Address
19
+ agentId: bigint
20
+ expectedOwner: Address
21
+ }
22
+
23
+ export async function discoverPriorVaultFromTokenOwner(
24
+ args: DiscoverPriorVaultArgs,
25
+ ): Promise<{ found: false } | { found: true; vaultAddress: Address }> {
26
+ const registryAddr = getAddress(args.registry)
27
+ const expected = getAddress(args.expectedOwner).toLowerCase()
28
+ const tokenOwner = await args.client.readContract({
29
+ address: registryAddr,
30
+ abi: ERC721_OWNER_OF_ABI,
31
+ functionName: 'ownerOf',
32
+ args: [args.agentId],
33
+ }) as Address
34
+ if (!tokenOwner || tokenOwner.toLowerCase() === '0x0000000000000000000000000000000000000000') {
35
+ return { found: false }
36
+ }
37
+ if (tokenOwner.toLowerCase() === expected) {
38
+ return { found: false }
39
+ }
40
+ const candidate = getAddress(tokenOwner)
41
+ const code = await args.client.getBytecode({ address: candidate })
42
+ if (!code || code === '0x') return { found: false }
43
+ let vaultLevelOwner: Address
44
+ try {
45
+ vaultLevelOwner = await args.client.readContract({
46
+ address: candidate,
47
+ abi: OPERATOR_VAULT_ABI,
48
+ functionName: 'agentOwner',
49
+ args: [registryAddr, args.agentId],
50
+ }) as Address
51
+ } catch {
52
+ return { found: false }
53
+ }
54
+ if (!vaultLevelOwner || vaultLevelOwner.toLowerCase() !== expected) {
55
+ return { found: false }
56
+ }
57
+ return { found: true, vaultAddress: candidate }
58
+ }
59
+
60
+ export type IsAgentInVaultArgs = {
61
+ client: OperatorVaultReadClient
62
+ vaultAddress: Address
63
+ registry: Address
64
+ agentId: bigint
65
+ }
66
+
67
+ export async function isAgentInVault(
68
+ args: IsAgentInVaultArgs,
69
+ ): Promise<{ inVault: boolean; ownerAddress?: Address }> {
70
+ const owner = await args.client.readContract({
71
+ address: getAddress(args.vaultAddress),
72
+ abi: OPERATOR_VAULT_ABI,
73
+ functionName: 'agentOwner',
74
+ args: [getAddress(args.registry), args.agentId],
75
+ }) as Address
76
+ if (!owner || owner.toLowerCase() === '0x0000000000000000000000000000000000000000') {
77
+ return { inVault: false }
78
+ }
79
+ return { inVault: true, ownerAddress: getAddress(owner) }
80
+ }
81
+
82
+ export async function confirmAgentInVault(
83
+ args: IsAgentInVaultArgs,
84
+ ): Promise<{ inVault: true; ownerAddress: Address }> {
85
+ let lastErr: unknown
86
+ for (let attempt = 0; attempt < OPERATOR_VAULT_POLL_MAX_ATTEMPTS; attempt++) {
87
+ if (attempt > 0) await delayMs(OPERATOR_VAULT_POLL_DELAY_MS)
88
+ try {
89
+ const status = await isAgentInVault(args)
90
+ lastErr = undefined
91
+ if (status.inVault && status.ownerAddress) {
92
+ return { inVault: true, ownerAddress: status.ownerAddress }
93
+ }
94
+ } catch (err) {
95
+ lastErr = err
96
+ }
97
+ }
98
+ if (lastErr) throw lastErr
99
+ throw new Error(
100
+ `Operator delegation vault ${getAddress(args.vaultAddress)} does not hold agent token #${args.agentId.toString()} for registry ${getAddress(args.registry)} after the deposit-confirmation budget was exhausted. The deposit transaction may have been re-orged or applied to the wrong vault. Re-run the switch.`,
101
+ )
102
+ }
103
+
104
+ export type ConfirmAgentWithdrawnArgs = IsAgentInVaultArgs & {
105
+ recipient: Address
106
+ }
107
+
108
+ export async function confirmAgentWithdrawnFromVault(
109
+ args: ConfirmAgentWithdrawnArgs,
110
+ ): Promise<{ inVault: false; ownerAddress: Address }> {
111
+ const recipient = getAddress(args.recipient)
112
+ let lastErr: unknown
113
+ let lastObserved: string | undefined
114
+ for (let attempt = 0; attempt < OPERATOR_VAULT_POLL_MAX_ATTEMPTS; attempt++) {
115
+ if (attempt > 0) await delayMs(OPERATOR_VAULT_POLL_DELAY_MS)
116
+ try {
117
+ const status = await isAgentInVault(args)
118
+ const tokenOwner = await args.client.readContract({
119
+ address: getAddress(args.registry),
120
+ abi: ERC721_OWNER_OF_ABI,
121
+ functionName: 'ownerOf',
122
+ args: [args.agentId],
123
+ }) as Address
124
+ const ownerAddress = getAddress(tokenOwner)
125
+ lastErr = undefined
126
+ lastObserved = status.inVault
127
+ ? `vault owner ${status.ownerAddress ?? 'unknown'}, token owner ${ownerAddress}`
128
+ : `token owner ${ownerAddress}`
129
+ if (!status.inVault && ownerAddress.toLowerCase() === recipient.toLowerCase()) {
130
+ return { inVault: false, ownerAddress }
131
+ }
132
+ } catch (err) {
133
+ lastErr = err
134
+ }
135
+ }
136
+ if (lastErr) throw lastErr
137
+ throw new Error(
138
+ `Operator delegation vault ${getAddress(args.vaultAddress)} did not release agent token #${args.agentId.toString()} to ${recipient} after the withdraw-confirmation budget was exhausted. Last observed: ${lastObserved ?? 'unknown'}.`,
139
+ )
140
+ }
141
+
142
+ const AGENT_DEPOSITED_EVENT = parseAbiItem(
143
+ 'event AgentDeposited(address indexed registry, uint256 indexed agentId, address indexed owner)',
144
+ )
145
+
146
+ export type DiscoverVaultedTokensArgs = {
147
+ client: PublicClient
148
+ vaultAddress: Address
149
+ registry: Address
150
+ depositorAddress: Address
151
+ fromBlock?: bigint
152
+ }
153
+
154
+ const DISCOVER_VAULTED_TOKENS_BLOCK_WINDOW = 9_000n
155
+
156
+ export async function discoverVaultedTokens(
157
+ args: DiscoverVaultedTokensArgs,
158
+ ): Promise<Array<{ registry: Address; agentId: bigint }>> {
159
+ const vaultAddr = getAddress(args.vaultAddress)
160
+ const registryAddr = getAddress(args.registry)
161
+ const depositor = getAddress(args.depositorAddress)
162
+ const fromBlock = args.fromBlock ?? 0n
163
+ const latest = await args.client.getBlockNumber()
164
+ const fetchWindow = async (cursorFrom: bigint, cursorTo: bigint) =>
165
+ args.client.getLogs({
166
+ address: vaultAddr,
167
+ event: AGENT_DEPOSITED_EVENT,
168
+ args: { registry: registryAddr, owner: depositor },
169
+ fromBlock: cursorFrom,
170
+ toBlock: cursorTo,
171
+ })
172
+ type DepositLog = Awaited<ReturnType<typeof fetchWindow>>[number]
173
+ const fetchWindowWithRetry = async (cursorFrom: bigint, cursorTo: bigint): Promise<DepositLog[]> => {
174
+ let lastErr: unknown
175
+ for (let attempt = 0; attempt < DISCOVER_LOG_WINDOW_MAX_ATTEMPTS; attempt++) {
176
+ if (attempt > 0) await delayMs(DISCOVER_LOG_WINDOW_DELAY_MS)
177
+ try {
178
+ return await fetchWindow(cursorFrom, cursorTo)
179
+ } catch (err) {
180
+ lastErr = err
181
+ }
182
+ }
183
+ throw lastErr ?? new Error(`failed to fetch AgentDeposited logs for window [${cursorFrom},${cursorTo}]`)
184
+ }
185
+ const logs: DepositLog[] = []
186
+ let cursor = fromBlock
187
+ while (cursor <= latest) {
188
+ const windowEnd = cursor + DISCOVER_VAULTED_TOKENS_BLOCK_WINDOW - 1n
189
+ const toBlock = windowEnd < latest ? windowEnd : latest
190
+ const window = await fetchWindowWithRetry(cursor, toBlock)
191
+ logs.push(...window)
192
+ if (toBlock === latest) break
193
+ cursor = toBlock + 1n
194
+ }
195
+ const seen = new Set<string>()
196
+ const candidates: Array<{ registry: Address; agentId: bigint }> = []
197
+ for (const log of logs) {
198
+ const agentId = log.args.agentId
199
+ if (agentId === undefined) continue
200
+ const key = `${registryAddr.toLowerCase()}:${agentId.toString()}`
201
+ if (seen.has(key)) continue
202
+ seen.add(key)
203
+ candidates.push({ registry: registryAddr, agentId })
204
+ }
205
+ const out: Array<{ registry: Address; agentId: bigint }> = []
206
+ for (const candidate of candidates) {
207
+ const status = await isAgentInVault({
208
+ client: args.client,
209
+ vaultAddress: vaultAddr,
210
+ registry: candidate.registry,
211
+ agentId: candidate.agentId,
212
+ })
213
+ if (status.inVault && status.ownerAddress?.toLowerCase() === depositor.toLowerCase()) {
214
+ out.push(candidate)
215
+ }
216
+ }
217
+ return out
218
+ }
219
+
220
+ export type ReadMetadataOperatorsArgs = {
221
+ client: OperatorVaultReadClient
222
+ vaultAddress: Address
223
+ registry: Address
224
+ agentId: bigint
225
+ candidates: readonly Address[]
226
+ }
227
+
228
+ export async function readMetadataOperators(
229
+ args: ReadMetadataOperatorsArgs,
230
+ ): Promise<Record<Address, boolean>> {
231
+ const out: Record<Address, boolean> = {}
232
+ for (const candidate of args.candidates) {
233
+ try {
234
+ const approved = await args.client.readContract({
235
+ address: getAddress(args.vaultAddress),
236
+ abi: OPERATOR_VAULT_ABI,
237
+ functionName: 'metadataOperators',
238
+ args: [getAddress(args.registry), args.agentId, getAddress(candidate)],
239
+ }) as boolean
240
+ out[candidate] = Boolean(approved)
241
+ } catch {
242
+ out[candidate] = false
243
+ }
244
+ }
245
+ return out
246
+ }
@@ -0,0 +1,81 @@
1
+ import { encodeFunctionData, getAddress, parseAbi, type Address, type Hex } from 'viem'
2
+ import { OPERATOR_VAULT_ABI } from './constants.js'
3
+
4
+ const ERC721_SAFE_TRANSFER_ABI = parseAbi([
5
+ 'function safeTransferFrom(address from, address to, uint256 tokenId)',
6
+ ])
7
+
8
+ export type DepositAgentArgs = {
9
+ registry: Address
10
+ agentId: bigint
11
+ walletAddress: Address
12
+ vaultAddress: Address
13
+ }
14
+
15
+ export function encodeDepositAgent(args: DepositAgentArgs): { to: Address; data: Hex } {
16
+ return {
17
+ to: getAddress(args.registry),
18
+ data: encodeFunctionData({
19
+ abi: ERC721_SAFE_TRANSFER_ABI,
20
+ functionName: 'safeTransferFrom',
21
+ args: [getAddress(args.walletAddress), getAddress(args.vaultAddress), args.agentId],
22
+ }),
23
+ }
24
+ }
25
+
26
+ export type SetMetadataOperatorArgs = {
27
+ registry: Address
28
+ agentId: bigint
29
+ operator: Address
30
+ approved: boolean
31
+ vaultAddress: Address
32
+ }
33
+
34
+ export function encodeSetMetadataOperator(args: SetMetadataOperatorArgs): { to: Address; data: Hex } {
35
+ return {
36
+ to: getAddress(args.vaultAddress),
37
+ data: encodeFunctionData({
38
+ abi: OPERATOR_VAULT_ABI,
39
+ functionName: 'setMetadataOperator',
40
+ args: [getAddress(args.registry), args.agentId, getAddress(args.operator), args.approved],
41
+ }),
42
+ }
43
+ }
44
+
45
+ export type RotateAgentURIArgs = {
46
+ registry: Address
47
+ agentId: bigint
48
+ newURI: string
49
+ vaultAddress: Address
50
+ }
51
+
52
+ export function encodeRotateAgentURI(args: RotateAgentURIArgs): { to: Address; data: Hex } {
53
+ return {
54
+ to: getAddress(args.vaultAddress),
55
+ data: encodeFunctionData({
56
+ abi: OPERATOR_VAULT_ABI,
57
+ functionName: 'rotateAgentURI',
58
+ args: [getAddress(args.registry), args.agentId, args.newURI],
59
+ }),
60
+ }
61
+ }
62
+
63
+ export type UnwrapAgentArgs = {
64
+ registry: Address
65
+ agentId: bigint
66
+ recipient: Address
67
+ vaultAddress: Address
68
+ }
69
+
70
+ export function encodeUnwrapAgent(
71
+ args: UnwrapAgentArgs,
72
+ ): { to: Address; data: Hex } {
73
+ return {
74
+ to: getAddress(args.vaultAddress),
75
+ data: encodeFunctionData({
76
+ abi: OPERATOR_VAULT_ABI,
77
+ functionName: 'unwrap',
78
+ args: [getAddress(args.registry), args.agentId, getAddress(args.recipient)],
79
+ }),
80
+ }
81
+ }
@@ -0,0 +1,44 @@
1
+ export {
2
+ OPERATOR_VAULT_ABI,
3
+ OPERATOR_VAULT_ADDRESSES,
4
+ OPERATOR_VAULT_DEPLOY_BYTECODE,
5
+ OPERATOR_VAULT_RUNTIME_BYTECODE,
6
+ OPERATOR_VAULT_RUNTIME_BYTECODE_HASH,
7
+ operatorVaultAddressForChain,
8
+ resolveConfiguredOperatorVaultAddress,
9
+ } from './operatorVault/constants.js'
10
+ export {
11
+ OperatorVaultBytecodeMismatchError,
12
+ assertVaultBytecode,
13
+ formatOperatorVaultBytecodeMismatchDetail,
14
+ } from './operatorVault/bytecode.js'
15
+ export type { AssertVaultBytecodeClient } from './operatorVault/bytecode.js'
16
+ export {
17
+ encodeDepositAgent,
18
+ encodeRotateAgentURI,
19
+ encodeSetMetadataOperator,
20
+ encodeUnwrapAgent,
21
+ } from './operatorVault/transactions.js'
22
+ export type {
23
+ DepositAgentArgs,
24
+ RotateAgentURIArgs,
25
+ SetMetadataOperatorArgs,
26
+ UnwrapAgentArgs,
27
+ } from './operatorVault/transactions.js'
28
+ export {
29
+ confirmAgentWithdrawnFromVault,
30
+ confirmAgentInVault,
31
+ discoverPriorVaultFromTokenOwner,
32
+ discoverVaultedTokens,
33
+ isAgentInVault,
34
+ readMetadataOperators,
35
+ } from './operatorVault/read.js'
36
+ export type {
37
+ ConfirmAgentWithdrawnArgs,
38
+ DiscoverPriorVaultArgs,
39
+ DiscoverPriorVaultClient,
40
+ DiscoverVaultedTokensArgs,
41
+ IsAgentInVaultArgs,
42
+ OperatorVaultReadClient,
43
+ ReadMetadataOperatorsArgs,
44
+ } from './operatorVault/read.js'
@@ -1,16 +1,10 @@
1
1
  export const PINATA_UPLOAD_API_URL = 'https://uploads.pinata.cloud/v3/files'
2
2
  export const PINATA_AUTH_TEST_URL = 'https://api.pinata.cloud/data/testAuthentication'
3
- export const DEFAULT_PINATA_GATEWAY_URL = 'https://gateway.pinata.cloud'
3
+ const DEFAULT_PINATA_GATEWAY_URL = 'https://gateway.pinata.cloud'
4
4
  export const DEFAULT_IPFS_API_URL = process.env.ETHAGENT_IPFS_API_URL?.trim() || PINATA_UPLOAD_API_URL
5
5
 
6
6
  export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>
7
7
 
8
- export type IpfsClient = {
9
- apiUrl: string
10
- add: (content: string | Uint8Array) => Promise<IpfsAddResult>
11
- cat: (cid: string) => Promise<Uint8Array>
12
- }
13
-
14
8
  export type IpfsAddResult = {
15
9
  cid: string
16
10
  pinVerified: boolean
@@ -21,19 +15,6 @@ type IpfsOptions = {
21
15
  pinataJwt?: string
22
16
  }
23
17
 
24
- export function createIpfsClient(apiUrl = DEFAULT_IPFS_API_URL, fetchImpl: FetchLike = fetch, options: IpfsOptions = {}): IpfsClient {
25
- const base = normalizeApiUrl(apiUrl)
26
- return {
27
- apiUrl: base,
28
- add: content => addToIpfs(base, content, fetchImpl, options),
29
- cat: cid => catFromIpfs(base, cid, fetchImpl),
30
- }
31
- }
32
-
33
- export function needsPinataJwt(apiUrl = DEFAULT_IPFS_API_URL, options: IpfsOptions = {}): boolean {
34
- return isPinataUploadUrl(apiUrl) && !pinataJwt(options)
35
- }
36
-
37
18
  export function extractPinataJwt(input: string): string {
38
19
  const trimmed = input.trim()
39
20
  const matches = trimmed.match(/\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g) ?? []
@@ -111,11 +92,13 @@ export async function catFromIpfs(
111
92
  apiUrl: string,
112
93
  cid: string,
113
94
  fetchImpl: FetchLike = fetch,
95
+ options: { signal?: AbortSignal } = {},
114
96
  ): Promise<Uint8Array> {
115
- if (isPinataUploadUrl(apiUrl)) return catFromPinata(cid, fetchImpl)
97
+ if (isPinataUploadUrl(apiUrl)) return catFromPinata(cid, fetchImpl, options.signal)
116
98
  const arg = encodeURIComponent(cid.trim())
117
99
  const response = await fetchImpl(`${normalizeApiUrl(apiUrl)}/api/v0/cat?arg=${arg}`, {
118
100
  method: 'POST',
101
+ ...(options.signal ? { signal: options.signal } : {}),
119
102
  })
120
103
  if (!response.ok) throw new Error(`IPFS cat failed: ${response.status} ${response.statusText}`)
121
104
  return new Uint8Array(await response.arrayBuffer())
@@ -163,17 +146,36 @@ async function addFileToPinata(
163
146
  const data = await response.json() as { data?: { cid?: string }; IpfsHash?: string; Hash?: string; Cid?: string }
164
147
  const cid = data.data?.cid ?? data.IpfsHash ?? data.Hash ?? data.Cid
165
148
  if (!cid) throw new Error('IPFS upload response did not include a CID')
166
- return { cid, pinVerified: true, provider: 'pinata' }
149
+ const verified = await verifyCidReachable(cid, fetchImpl)
150
+ return { cid, pinVerified: verified, provider: 'pinata' }
151
+ }
152
+
153
+ async function verifyCidReachable(
154
+ cid: string,
155
+ fetchImpl: FetchLike,
156
+ ): Promise<boolean> {
157
+ const gateway = normalizeApiUrl(process.env.PINATA_GATEWAY_URL?.trim() || DEFAULT_PINATA_GATEWAY_URL)
158
+ const path = cid.trim().split('/').map(part => encodeURIComponent(part)).join('/')
159
+ const url = `${gateway}/ipfs/${path}`
160
+ for (const delayMs of [0, 1500]) {
161
+ if (delayMs > 0) await new Promise(resolve => setTimeout(resolve, delayMs))
162
+ try {
163
+ const response = await fetchImpl(url, { method: 'HEAD' })
164
+ if (response.ok) return true
165
+ } catch {
166
+ }
167
+ }
168
+ return false
167
169
  }
168
170
 
169
171
  function pinataJwt(options: IpfsOptions): string | undefined {
170
172
  return options.pinataJwt?.trim() || process.env.PINATA_JWT?.trim() || undefined
171
173
  }
172
174
 
173
- async function catFromPinata(cid: string, fetchImpl: FetchLike): Promise<Uint8Array> {
175
+ async function catFromPinata(cid: string, fetchImpl: FetchLike, signal?: AbortSignal): Promise<Uint8Array> {
174
176
  const gateway = normalizeApiUrl(process.env.PINATA_GATEWAY_URL?.trim() || DEFAULT_PINATA_GATEWAY_URL)
175
177
  const path = cid.trim().split('/').map(part => encodeURIComponent(part)).join('/')
176
- const response = await fetchImpl(`${gateway}/ipfs/${path}`)
178
+ const response = await fetchImpl(`${gateway}/ipfs/${path}`, signal ? { signal } : {})
177
179
  if (!response.ok) throw new Error(`IPFS fetch failed: ${response.status} ${response.statusText}`)
178
180
  return new Uint8Array(await response.arrayBuffer())
179
181
  }
@@ -0,0 +1,41 @@
1
+ import { numberToHex } from 'viem'
2
+ import type {
3
+ PreparedGasFee,
4
+ PrepareTransactionGasFeeArgs,
5
+ PrepareTransactionGasFeeClient,
6
+ } from './types.js'
7
+
8
+ const GAS_FEE_PREP_MAX_ATTEMPTS = 5
9
+ const GAS_FEE_PREP_DELAY_MS = 1500
10
+
11
+ function gasFeeDelay(ms: number): Promise<void> {
12
+ return new Promise(resolve => setTimeout(resolve, ms))
13
+ }
14
+
15
+ export async function prepareTransactionGasFee(args: PrepareTransactionGasFeeArgs): Promise<PreparedGasFee> {
16
+ let lastErr: unknown
17
+ for (let attempt = 0; attempt < GAS_FEE_PREP_MAX_ATTEMPTS; attempt++) {
18
+ if (attempt > 0) await gasFeeDelay(GAS_FEE_PREP_DELAY_MS)
19
+ try {
20
+ const estimateArgs: Parameters<PrepareTransactionGasFeeClient['estimateGas']>[0] = {
21
+ account: args.account,
22
+ data: args.data,
23
+ ...(args.to ? { to: args.to } : {}),
24
+ ...(args.value !== undefined ? { value: args.value } : {}),
25
+ }
26
+ const [gas, fees] = await Promise.all([
27
+ args.client.estimateGas(estimateArgs),
28
+ args.client.estimateFeesPerGas(),
29
+ ])
30
+ const gasWithBuffer = (gas * 12n) / 10n
31
+ return {
32
+ gas: numberToHex(gasWithBuffer),
33
+ maxFeePerGas: numberToHex(fees.maxFeePerGas),
34
+ maxPriorityFeePerGas: numberToHex(fees.maxPriorityFeePerGas),
35
+ }
36
+ } catch (err) {
37
+ lastErr = err
38
+ }
39
+ }
40
+ throw lastErr ?? new Error('failed to prepare transaction gas/fee')
41
+ }