@azeth/sdk 0.2.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/dist/account/balance.d.ts +41 -0
  4. package/dist/account/balance.d.ts.map +1 -0
  5. package/dist/account/balance.js +264 -0
  6. package/dist/account/balance.js.map +1 -0
  7. package/dist/account/create.d.ts +27 -0
  8. package/dist/account/create.d.ts.map +1 -0
  9. package/dist/account/create.js +116 -0
  10. package/dist/account/create.js.map +1 -0
  11. package/dist/account/deposit.d.ts +34 -0
  12. package/dist/account/deposit.d.ts.map +1 -0
  13. package/dist/account/deposit.js +88 -0
  14. package/dist/account/deposit.js.map +1 -0
  15. package/dist/account/guardian-approval.d.ts +111 -0
  16. package/dist/account/guardian-approval.d.ts.map +1 -0
  17. package/dist/account/guardian-approval.js +223 -0
  18. package/dist/account/guardian-approval.js.map +1 -0
  19. package/dist/account/guardian.d.ts +27 -0
  20. package/dist/account/guardian.d.ts.map +1 -0
  21. package/dist/account/guardian.js +67 -0
  22. package/dist/account/guardian.js.map +1 -0
  23. package/dist/account/history.d.ts +22 -0
  24. package/dist/account/history.d.ts.map +1 -0
  25. package/dist/account/history.js +144 -0
  26. package/dist/account/history.js.map +1 -0
  27. package/dist/account/transfer.d.ts +28 -0
  28. package/dist/account/transfer.d.ts.map +1 -0
  29. package/dist/account/transfer.js +137 -0
  30. package/dist/account/transfer.js.map +1 -0
  31. package/dist/auth/erc8128.d.ts +14 -0
  32. package/dist/auth/erc8128.d.ts.map +1 -0
  33. package/dist/auth/erc8128.js +92 -0
  34. package/dist/auth/erc8128.js.map +1 -0
  35. package/dist/client.d.ts +394 -0
  36. package/dist/client.d.ts.map +1 -0
  37. package/dist/client.js +970 -0
  38. package/dist/client.js.map +1 -0
  39. package/dist/events/emitter.d.ts +96 -0
  40. package/dist/events/emitter.d.ts.map +1 -0
  41. package/dist/events/emitter.js +90 -0
  42. package/dist/events/emitter.js.map +1 -0
  43. package/dist/index.d.ts +29 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +33 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/messaging/message-router.d.ts +69 -0
  48. package/dist/messaging/message-router.d.ts.map +1 -0
  49. package/dist/messaging/message-router.js +307 -0
  50. package/dist/messaging/message-router.js.map +1 -0
  51. package/dist/messaging/rate-limiter.d.ts +31 -0
  52. package/dist/messaging/rate-limiter.d.ts.map +1 -0
  53. package/dist/messaging/rate-limiter.js +74 -0
  54. package/dist/messaging/rate-limiter.js.map +1 -0
  55. package/dist/messaging/xmtp.d.ts +144 -0
  56. package/dist/messaging/xmtp.d.ts.map +1 -0
  57. package/dist/messaging/xmtp.js +473 -0
  58. package/dist/messaging/xmtp.js.map +1 -0
  59. package/dist/payments/agreements.d.ts +87 -0
  60. package/dist/payments/agreements.d.ts.map +1 -0
  61. package/dist/payments/agreements.js +337 -0
  62. package/dist/payments/agreements.js.map +1 -0
  63. package/dist/payments/budget.d.ts +118 -0
  64. package/dist/payments/budget.d.ts.map +1 -0
  65. package/dist/payments/budget.js +176 -0
  66. package/dist/payments/budget.js.map +1 -0
  67. package/dist/payments/smart-fetch.d.ts +65 -0
  68. package/dist/payments/smart-fetch.d.ts.map +1 -0
  69. package/dist/payments/smart-fetch.js +115 -0
  70. package/dist/payments/smart-fetch.js.map +1 -0
  71. package/dist/payments/x402.d.ts +89 -0
  72. package/dist/payments/x402.d.ts.map +1 -0
  73. package/dist/payments/x402.js +620 -0
  74. package/dist/payments/x402.js.map +1 -0
  75. package/dist/registry/discover.d.ts +43 -0
  76. package/dist/registry/discover.d.ts.map +1 -0
  77. package/dist/registry/discover.js +272 -0
  78. package/dist/registry/discover.js.map +1 -0
  79. package/dist/registry/register.d.ts +44 -0
  80. package/dist/registry/register.d.ts.map +1 -0
  81. package/dist/registry/register.js +126 -0
  82. package/dist/registry/register.js.map +1 -0
  83. package/dist/reputation/opinion.d.ts +52 -0
  84. package/dist/reputation/opinion.d.ts.map +1 -0
  85. package/dist/reputation/opinion.js +198 -0
  86. package/dist/reputation/opinion.js.map +1 -0
  87. package/dist/utils/addresses.d.ts +6 -0
  88. package/dist/utils/addresses.d.ts.map +1 -0
  89. package/dist/utils/addresses.js +53 -0
  90. package/dist/utils/addresses.js.map +1 -0
  91. package/dist/utils/errors.d.ts +23 -0
  92. package/dist/utils/errors.d.ts.map +1 -0
  93. package/dist/utils/errors.js +188 -0
  94. package/dist/utils/errors.js.map +1 -0
  95. package/dist/utils/execution.d.ts +20 -0
  96. package/dist/utils/execution.d.ts.map +1 -0
  97. package/dist/utils/execution.js +28 -0
  98. package/dist/utils/execution.js.map +1 -0
  99. package/dist/utils/paymaster.d.ts +35 -0
  100. package/dist/utils/paymaster.d.ts.map +1 -0
  101. package/dist/utils/paymaster.js +115 -0
  102. package/dist/utils/paymaster.js.map +1 -0
  103. package/dist/utils/retry.d.ts +19 -0
  104. package/dist/utils/retry.d.ts.map +1 -0
  105. package/dist/utils/retry.js +68 -0
  106. package/dist/utils/retry.js.map +1 -0
  107. package/dist/utils/userop.d.ts +55 -0
  108. package/dist/utils/userop.d.ts.map +1 -0
  109. package/dist/utils/userop.js +201 -0
  110. package/dist/utils/userop.js.map +1 -0
  111. package/dist/utils/validation.d.ts +8 -0
  112. package/dist/utils/validation.d.ts.map +1 -0
  113. package/dist/utils/validation.js +35 -0
  114. package/dist/utils/validation.js.map +1 -0
  115. package/package.json +63 -0
package/dist/client.js ADDED
@@ -0,0 +1,970 @@
1
+ import { createPublicClient, createWalletClient, http, hexToBytes, bytesToHex, encodeFunctionData, getAddress, } from 'viem';
2
+ import { base, baseSepolia, sepolia as ethereumSepoliaChain, mainnet as ethereumChain } from 'viem/chains';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { AzethError, SUPPORTED_CHAINS, } from '@azeth/common';
5
+ import { validateAddress, validateUrl, validatePositiveAmount } from './utils/validation.js';
6
+ import { resolveAddresses, requireAddress } from './utils/addresses.js';
7
+ import { withRetry } from './utils/retry.js';
8
+ import { AzethFactoryAbi, PaymentAgreementModuleAbi } from '@azeth/common/abis';
9
+ import { createAccount, getAccountAddress } from './account/create.js';
10
+ import { setTokenWhitelist as setTokenWhitelistFn, setProtocolWhitelist as setProtocolWhitelistFn } from './account/guardian.js';
11
+ import { getBalance, getAllBalances } from './account/balance.js';
12
+ import { transfer } from './account/transfer.js';
13
+ import { getHistory } from './account/history.js';
14
+ import { deposit } from './account/deposit.js';
15
+ import { registerOnRegistry, updateMetadata, updateMetadataBatch } from './registry/register.js';
16
+ import { submitOpinion as submitOnChainOpinion, getWeightedReputation as getWeightedRep, getNetPaid as getNetPaidFn, getTotalNetPaidUSD as getTotalNetPaidUSDFn, getActiveOpinion as getActiveOpinionFn, readOpinion as readOnChainOpinion, } from './reputation/opinion.js';
17
+ import { discoverServices } from './registry/discover.js';
18
+ import { fetch402 } from './payments/x402.js';
19
+ import { smartFetch402 as smartFetch402Fn, computeFeedbackValue, FAILURE_PENALTY_VALUE } from './payments/smart-fetch.js';
20
+ import { createPaymentAgreement, getAgreement, executeAgreement, executeAgreementAsKeeper, cancelAgreement as cancelAgreementFn, findAgreementWithPayee, getAgreementCount as getAgreementCountFn, canExecutePayment as canExecutePaymentFn, getNextExecutionTime as getNextExecutionTimeFn, getAgreementData as getAgreementDataFn } from './payments/agreements.js';
21
+ import { createSignedFetch } from './auth/erc8128.js';
22
+ import { XMTPClient } from './messaging/xmtp.js';
23
+ import { AzethEventEmitter } from './events/emitter.js';
24
+ import { BudgetManager } from './payments/budget.js';
25
+ import { createAzethSmartAccountClient } from './utils/userop.js';
26
+ /** AzethKit -- Trust Infrastructure SDK for the Machine Economy
27
+ *
28
+ * Provides Phase 0 methods for machine participants:
29
+ * - create: Deploy a smart account + trust registry entry
30
+ * - transfer: Send ETH or tokens to another participant
31
+ * - getBalance: Check account balances
32
+ * - getHistory: Get transaction history
33
+ * - fetch402: Pay for x402-gated services (with budget enforcement)
34
+ * - publishService: Register on the trust registry
35
+ * - discoverServices: Find services by capability + reputation
36
+ * - createPaymentAgreement: Set up recurring payments
37
+ * - submitOpinion: Submit reputation opinion for a service
38
+ * - getWeightedReputation: Get payment-weighted reputation for an agent
39
+ * - getNetPaid: Get net payment delta with a counterparty
40
+ * - getActiveOpinion: Check active opinion for an agent
41
+ * - sendMessage: Send XMTP encrypted messages
42
+ * - onMessage: Listen for incoming messages
43
+ * - canReach: Check if an address is reachable on XMTP
44
+ *
45
+ * Events: on('beforePayment'), on('afterPayment'), on('paymentError'),
46
+ * on('beforeTransfer'), on('afterTransfer'), on('transferError')
47
+ */
48
+ export class AzethKit {
49
+ address;
50
+ chainName;
51
+ addresses;
52
+ publicClient;
53
+ walletClient;
54
+ serverUrl;
55
+ /** All smart account addresses owned by this EOA, resolved from factory.
56
+ * null until createAccount() is called or resolveSmartAccount() resolves them. */
57
+ _smartAccounts = null;
58
+ /** H-2 fix: Private key stored as Uint8Array for proper zeroing in destroy() */
59
+ _privateKeyBytes;
60
+ /** MEDIUM-7 fix: Track destroyed state to prevent use-after-destroy.
61
+ * H-6 fix (Audit #8): All state-changing methods now check this flag. */
62
+ _destroyed = false;
63
+ _xmtpConfig;
64
+ _messaging = null;
65
+ _messagingInitPromise = null;
66
+ /** Event emitter for lifecycle hooks */
67
+ events;
68
+ /** Budget manager for x402 spending limits */
69
+ budget;
70
+ /** ERC-4337 bundler URL for UserOperation submission */
71
+ _bundlerUrl;
72
+ /** ERC-4337 paymaster URL for gas sponsorship */
73
+ _paymasterUrl;
74
+ /** Client-side paymaster sponsorship policy */
75
+ _paymasterPolicy;
76
+ /** Guardian co-signing key — used for address derivation and optional auto-signing */
77
+ _guardianKey;
78
+ /** When true, auto-append guardian co-signature to every UserOp */
79
+ _guardianAutoSign;
80
+ /** Cached SmartAccountClient instances keyed by smart account address */
81
+ _smartAccountClients = new Map();
82
+ constructor(address, chainName, addresses, publicClient, walletClient, serverUrl, privateKey, xmtpConfig, budgetConfig, bundlerUrl, paymasterUrl, paymasterPolicy, guardianKey, guardianAutoSign) {
83
+ this.address = address;
84
+ this.chainName = chainName;
85
+ this.addresses = addresses;
86
+ this.publicClient = publicClient;
87
+ this.walletClient = walletClient;
88
+ this.serverUrl = serverUrl;
89
+ this._privateKeyBytes = hexToBytes(privateKey);
90
+ this._xmtpConfig = xmtpConfig;
91
+ this.events = new AzethEventEmitter();
92
+ this.budget = new BudgetManager(budgetConfig);
93
+ this._bundlerUrl = bundlerUrl;
94
+ this._paymasterUrl = paymasterUrl;
95
+ this._paymasterPolicy = paymasterPolicy;
96
+ this._guardianKey = guardianKey;
97
+ this._guardianAutoSign = guardianAutoSign === true;
98
+ }
99
+ /** Create an AzethKit instance from a private key
100
+ *
101
+ * This connects to the chain and sets up clients for the owner's address.
102
+ * Call publishService() to register on the trust registry if not already registered.
103
+ */
104
+ static async create(config) {
105
+ // LOW-1 fix: Validate private key format before viem's privateKeyToAccount,
106
+ // which gives an unhelpful error for malformed keys.
107
+ if (!/^0x[0-9a-fA-F]{64}$/.test(config.privateKey)) {
108
+ throw new AzethError('Invalid private key format — expected 0x-prefixed 32-byte hex string (66 chars total)', 'INVALID_INPUT', { field: 'privateKey' });
109
+ }
110
+ const viemChains = {
111
+ base,
112
+ baseSepolia,
113
+ ethereumSepolia: ethereumSepoliaChain,
114
+ ethereum: ethereumChain,
115
+ };
116
+ const chain = viemChains[config.chain];
117
+ const rpcUrl = config.rpcUrl ?? SUPPORTED_CHAINS[config.chain].rpcDefault;
118
+ const account = privateKeyToAccount(config.privateKey);
119
+ const addresses = resolveAddresses(config.chain, config.contractAddresses);
120
+ const publicClient = createPublicClient({
121
+ chain,
122
+ transport: http(rpcUrl),
123
+ });
124
+ const walletClient = createWalletClient({
125
+ account,
126
+ chain,
127
+ transport: http(rpcUrl),
128
+ });
129
+ const serverUrl = config.serverUrl ?? 'https://api.azeth.ai';
130
+ // Validate server URL format
131
+ if (config.serverUrl) {
132
+ try {
133
+ const parsed = new URL(config.serverUrl);
134
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
135
+ throw new AzethError('Server URL must use HTTP or HTTPS', 'INVALID_INPUT', { field: 'serverUrl' });
136
+ }
137
+ }
138
+ catch (e) {
139
+ if (e instanceof AzethError)
140
+ throw e;
141
+ throw new AzethError('Invalid server URL format', 'INVALID_INPUT', { field: 'serverUrl' });
142
+ }
143
+ }
144
+ return new AzethKit(account.address, config.chain, addresses, publicClient, walletClient, serverUrl, config.privateKey, config.xmtp, config.budget, config.bundlerUrl, config.paymasterUrl, config.paymasterPolicy, config.guardianKey, config.guardianAutoSign);
145
+ }
146
+ // ──────────────────────────────────────────────
147
+ // Event system
148
+ // ──────────────────────────────────────────────
149
+ /** Subscribe to a lifecycle event. Returns an unsubscribe function. */
150
+ on(event, listener) {
151
+ return this.events.on(event, listener);
152
+ }
153
+ /** Subscribe to a lifecycle event for a single firing. */
154
+ once(event, listener) {
155
+ return this.events.once(event, listener);
156
+ }
157
+ // ──────────────────────────────────────────────
158
+ // Account operations
159
+ // ──────────────────────────────────────────────
160
+ /** H-6 fix (Audit #8): Guard all state-changing methods against use-after-destroy.
161
+ * After destroy(), the walletClient may still hold a working key copy in memory.
162
+ * This prevents accidental operations on a "destroyed" instance. */
163
+ _requireNotDestroyed() {
164
+ if (this._destroyed) {
165
+ throw new AzethError('AzethKit instance has been destroyed', 'INVALID_INPUT');
166
+ }
167
+ }
168
+ /** The first (default) smart account address owned by this EOA, or null if not yet resolved */
169
+ get smartAccount() {
170
+ return this._smartAccounts?.[0] ?? null;
171
+ }
172
+ /** All smart account addresses owned by this EOA, or null if not yet resolved */
173
+ get smartAccounts() {
174
+ return this._smartAccounts;
175
+ }
176
+ /** Resolve all smart account addresses from the on-chain factory.
177
+ * Queries factory.getAccountsByOwner(ownerAddress).
178
+ * Caches the result for subsequent calls.
179
+ *
180
+ * @returns Array of smart account addresses owned by this EOA
181
+ */
182
+ async getSmartAccounts() {
183
+ if (this._smartAccounts)
184
+ return this._smartAccounts;
185
+ this._requireNotDestroyed();
186
+ const factoryAddress = requireAddress(this.addresses, 'factory');
187
+ const accounts = await withRetry(() => this.publicClient.readContract({
188
+ address: factoryAddress,
189
+ abi: AzethFactoryAbi,
190
+ functionName: 'getAccountsByOwner',
191
+ args: [this.address],
192
+ }));
193
+ this._smartAccounts = [...(accounts ?? [])];
194
+ return this._smartAccounts;
195
+ }
196
+ /** Resolve the default (first) smart account address from the on-chain factory.
197
+ * Caches the result for subsequent calls.
198
+ *
199
+ * @throws AzethError if no account is found for this owner
200
+ */
201
+ async resolveSmartAccount() {
202
+ const accounts = await this.getSmartAccounts();
203
+ if (accounts.length === 0) {
204
+ throw new AzethError('No smart account found for this owner. Call createAccount() first.', 'ACCOUNT_NOT_FOUND', { owner: this.address });
205
+ }
206
+ return accounts[0];
207
+ }
208
+ /** Deploy a new Azeth smart account via the AzethFactory v11 (one-call setup).
209
+ *
210
+ * Single atomic transaction: deploys ERC-1967 proxy, installs all 4 modules,
211
+ * registers on ERC-8004 trust registry (optional), and permanently revokes factory access.
212
+ */
213
+ async createAccount(params) {
214
+ this._requireNotDestroyed();
215
+ const result = await createAccount(this.publicClient, this.walletClient, this.addresses, params);
216
+ // Cache the newly created account
217
+ if (!this._smartAccounts) {
218
+ this._smartAccounts = [result.account];
219
+ }
220
+ else {
221
+ this._smartAccounts.push(result.account);
222
+ }
223
+ return result;
224
+ }
225
+ /** Compute the deterministic address for an account without deploying */
226
+ async getAccountAddress(salt) {
227
+ return getAccountAddress(this.publicClient, this.addresses, this.address, salt);
228
+ }
229
+ /** Transfer ETH or ERC-20 tokens from a smart account to another address.
230
+ *
231
+ * Executes via AzethAccount.execute() so transfers go through the smart account,
232
+ * not the EOA directly. Defaults to the first smart account if none specified.
233
+ *
234
+ * @param params - Transfer parameters (to, amount, token)
235
+ * @param fromAccount - Optional: specific smart account to transfer from (defaults to first)
236
+ */
237
+ async transfer(params, fromAccount) {
238
+ this._requireNotDestroyed();
239
+ validateAddress(params.to, 'to');
240
+ if (params.amount <= 0n) {
241
+ throw new AzethError('Transfer amount must be positive', 'INVALID_INPUT', { field: 'amount' });
242
+ }
243
+ await this.events.emit('beforeTransfer', {
244
+ to: params.to,
245
+ amount: params.amount,
246
+ token: params.token,
247
+ });
248
+ let result;
249
+ try {
250
+ const account = fromAccount ?? await this.resolveSmartAccount();
251
+ const smartAccountClient = await this._getSmartAccountClient(account);
252
+ result = await transfer(smartAccountClient, account, params, this.publicClient, this.addresses);
253
+ }
254
+ catch (err) {
255
+ await this.events.emit('transferError', {
256
+ operation: 'transfer',
257
+ error: err instanceof Error ? err : new Error(String(err)),
258
+ context: { to: params.to, amount: params.amount.toString() },
259
+ });
260
+ throw err;
261
+ }
262
+ await this.events.emit('afterTransfer', {
263
+ to: params.to,
264
+ amount: params.amount,
265
+ token: params.token,
266
+ txHash: result.txHash,
267
+ });
268
+ return result;
269
+ }
270
+ /** Get ETH and token balances for the smart account (primary) and EOA (gas).
271
+ *
272
+ * @param forAccount - Optional: specific smart account to check (defaults to first)
273
+ */
274
+ async getBalance(forAccount) {
275
+ const account = forAccount ?? await this.resolveSmartAccount();
276
+ return getBalance(this.publicClient, this.chainName, account, this.address);
277
+ }
278
+ /** Get balances for ALL accounts (EOA + all smart accounts) with USD values.
279
+ * Single RPC call via AzethFactory.getOwnerBalancesAndUSD().
280
+ *
281
+ * Returns: EOA at index 0, smart accounts at index 1+.
282
+ * Each account has per-token balances with USD values and a total.
283
+ * Grand total USD sums across all accounts.
284
+ */
285
+ async getAllBalances() {
286
+ const factoryAddress = requireAddress(this.addresses, 'factory');
287
+ return getAllBalances(this.publicClient, this.chainName, factoryAddress, this.address);
288
+ }
289
+ /** Get transaction history for a smart account.
290
+ *
291
+ * @param params - History params (limit, offset)
292
+ * @param forAccount - Optional: specific smart account (defaults to first)
293
+ */
294
+ async getHistory(params, forAccount) {
295
+ const account = forAccount ?? await this.resolveSmartAccount();
296
+ // Pass known token addresses for ERC-20 deposit tracking
297
+ const { TOKENS } = await import('@azeth/common');
298
+ const tokens = TOKENS[this.chainName];
299
+ const tokenAddresses = [tokens.USDC, tokens.WETH].filter(Boolean);
300
+ return getHistory(this.publicClient, account, this.serverUrl, params, this.addresses.reputationModule, tokenAddresses);
301
+ }
302
+ /** Deposit ETH or ERC-20 tokens from the owner EOA to a self-owned smart account.
303
+ *
304
+ * SECURITY: On-chain validation ensures the target is:
305
+ * 1. A real Azeth smart account (factory.isAzethAccount)
306
+ * 2. Owned by this EOA (factory.getOwnerOf)
307
+ */
308
+ async deposit(params) {
309
+ this._requireNotDestroyed();
310
+ validateAddress(params.to, 'to');
311
+ if (params.amount <= 0n) {
312
+ throw new AzethError('Deposit amount must be positive', 'INVALID_INPUT', { field: 'amount' });
313
+ }
314
+ await this.events.emit('beforeDeposit', {
315
+ to: params.to,
316
+ amount: params.amount,
317
+ token: params.token,
318
+ });
319
+ let result;
320
+ try {
321
+ result = await deposit(this.publicClient, this.walletClient, this.addresses, this.address, params);
322
+ }
323
+ catch (err) {
324
+ await this.events.emit('depositError', {
325
+ operation: 'deposit',
326
+ error: err instanceof Error ? err : new Error(String(err)),
327
+ context: { to: params.to, amount: params.amount.toString() },
328
+ });
329
+ throw err;
330
+ }
331
+ await this.events.emit('afterDeposit', {
332
+ to: params.to,
333
+ amount: params.amount,
334
+ token: params.token,
335
+ txHash: result.txHash,
336
+ });
337
+ return result;
338
+ }
339
+ /** Deposit ETH or ERC-20 tokens to this account's smart account.
340
+ * Convenience wrapper that auto-resolves the smart account address. */
341
+ async depositToSelf(params) {
342
+ const account = await this.resolveSmartAccount();
343
+ return this.deposit({ ...params, to: account });
344
+ }
345
+ // ──────────────────────────────────────────────
346
+ // Guardian management
347
+ // ──────────────────────────────────────────────
348
+ /** Update the token whitelist on the GuardianModule.
349
+ * Tokens must be whitelisted for executor-module operations (e.g., PaymentAgreementModule).
350
+ *
351
+ * @param token - Token address (use 0x0...0 for native ETH)
352
+ * @param allowed - true to whitelist, false to remove
353
+ * @param account - Optional: specific smart account (defaults to first)
354
+ */
355
+ async setTokenWhitelist(token, allowed, account) {
356
+ this._requireNotDestroyed();
357
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
358
+ const smartAccountClient = await this._getSmartAccountClient(resolvedAccount);
359
+ return setTokenWhitelistFn(smartAccountClient, this.addresses, token, allowed);
360
+ }
361
+ /** Update the protocol whitelist on the GuardianModule.
362
+ * Protocols must be whitelisted for executor-module operations.
363
+ *
364
+ * @param protocol - Protocol/contract address
365
+ * @param allowed - true to whitelist, false to remove
366
+ * @param account - Optional: specific smart account (defaults to first)
367
+ */
368
+ async setProtocolWhitelist(protocol, allowed, account) {
369
+ this._requireNotDestroyed();
370
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
371
+ const smartAccountClient = await this._getSmartAccountClient(resolvedAccount);
372
+ return setProtocolWhitelistFn(smartAccountClient, this.addresses, protocol, allowed);
373
+ }
374
+ // ──────────────────────────────────────────────
375
+ // x402 payments
376
+ // ──────────────────────────────────────────────
377
+ /** Fetch a URL, automatically paying x402 requirements.
378
+ *
379
+ * If the service returns 402, the SDK signs an ERC-3009 payment authorization,
380
+ * retries the request with the payment proof.
381
+ *
382
+ * Budget checking: If a BudgetManager is configured (default), the payment amount is
383
+ * checked against reputation-aware spending tiers before signing.
384
+ *
385
+ * @param url - Service URL to fetch
386
+ * @param options - Fetch options including budget overrides
387
+ * @param serviceReputation - Optional reputation score (0-100) for budget tier lookup
388
+ */
389
+ async fetch402(url, options, serviceReputation) {
390
+ this._requireNotDestroyed();
391
+ validateUrl(url, 'url');
392
+ const method = options?.method ?? 'GET';
393
+ // C-1 fix: Wrap budget check + payment + recordSpend in an atomic lock
394
+ // to prevent concurrent async payments from bypassing limits (TOCTOU race).
395
+ return this.budget.acquireBudgetLock(async () => {
396
+ // Pre-flight: only check session-level budget capacity.
397
+ // Reputation-tier per-tx limits are deferred to post-402 when the actual
398
+ // price is known — the pre-flight effectiveMaxAmount ($10 default) would
399
+ // always exceed the unknown-reputation tier limit ($0.10), blocking all
400
+ // payments to unrated services.
401
+ const effectiveMaxAmount = options?.maxAmount ?? 10000000n; // DEFAULT_MAX_AMOUNT from x402.ts
402
+ const sessionRemaining = this.budget.getRemaining();
403
+ if (effectiveMaxAmount > sessionRemaining) {
404
+ throw new AzethError(`Session budget insufficient: ${effectiveMaxAmount} requested, ${sessionRemaining} remaining`, 'BUDGET_EXCEEDED', { sessionRemaining: sessionRemaining.toString() });
405
+ }
406
+ // Audit #13 M-13 fix: Pre-payment budget tier estimate check.
407
+ // Advisory only — emits event if the maxAmount would exceed the tier limit.
408
+ // The on-chain maxAmount is the hard limit; this gives early UX feedback.
409
+ if (options?.maxAmount && serviceReputation !== undefined) {
410
+ const preTierCheck = this.budget.checkBudget(options.maxAmount, serviceReputation);
411
+ if (!preTierCheck.allowed) {
412
+ await this.events.emit('paymentError', {
413
+ operation: 'budget_tier_pre_check',
414
+ error: new Error(preTierCheck.reason ?? 'Pre-payment tier check: maxAmount may exceed tier limit'),
415
+ context: { url, maxAmount: options.maxAmount.toString(), tier: preTierCheck.tier },
416
+ });
417
+ }
418
+ }
419
+ await this.events.emit('beforePayment', { url, method });
420
+ const startTime = Date.now();
421
+ // Lazily resolve smart account for SIWx identity.
422
+ // Uses cached _smartAccounts if available, otherwise makes one on-chain call
423
+ // to the factory (getAccountsByOwner). Catches gracefully — if no smart account
424
+ // exists yet, SIWx is skipped and the agent pays directly via EOA.
425
+ let smartAccount = this._smartAccounts?.[0];
426
+ if (smartAccount === undefined) {
427
+ try {
428
+ smartAccount = await this.resolveSmartAccount();
429
+ }
430
+ catch {
431
+ // No smart account found — proceed without SIWx
432
+ }
433
+ }
434
+ // Build smartAccountTransfer callback when smart account + bundler are available.
435
+ // Routes x402 payments through PaymentAgreementModule.pay() to capture
436
+ // protocol fees on-chain. The module validates the transferWithAuth calldata,
437
+ // checks guardian spending limits, executes the payment, and adds the fee.
438
+ let smartAccountTransfer;
439
+ const payModuleAddr = this.addresses.paymentAgreementModule;
440
+ if (smartAccount && this._bundlerUrl && payModuleAddr) {
441
+ smartAccountTransfer = async (params) => {
442
+ const sac = await this._getSmartAccountClient(smartAccount);
443
+ const payData = encodeFunctionData({
444
+ abi: PaymentAgreementModuleAbi,
445
+ functionName: 'pay',
446
+ args: [
447
+ getAddress(params.usdcAddress),
448
+ getAddress(params.payTo),
449
+ params.amount,
450
+ params.calldata,
451
+ ],
452
+ });
453
+ return sac.sendTransaction({
454
+ to: getAddress(payModuleAddr),
455
+ value: 0n,
456
+ data: payData,
457
+ });
458
+ };
459
+ }
460
+ let result;
461
+ try {
462
+ result = await fetch402(this.publicClient, this.walletClient, this.address, url, {
463
+ ...options,
464
+ smartAccount,
465
+ smartAccountTransfer,
466
+ });
467
+ }
468
+ catch (err) {
469
+ await this.events.emit('paymentError', {
470
+ operation: 'fetch402',
471
+ error: err instanceof Error ? err : new Error(String(err)),
472
+ context: { url, method },
473
+ });
474
+ throw err;
475
+ }
476
+ const responseTimeMs = result.responseTimeMs ?? (Date.now() - startTime);
477
+ await this.events.emit('afterPayment', {
478
+ url,
479
+ method,
480
+ paymentMade: result.paymentMade,
481
+ statusCode: result.response.status,
482
+ responseTimeMs,
483
+ amount: result.amount,
484
+ txHash: result.txHash,
485
+ chainId: this.publicClient.chain?.id,
486
+ });
487
+ // Record spending in budget manager (inside lock for atomicity).
488
+ // Post-payment tier check: now that the actual amount is known, validate
489
+ // against reputation-aware tiers. This is advisory — the on-chain maxAmount
490
+ // in fetch402 is the hard limit. We still record the spend either way.
491
+ if (result.paymentMade && result.amount) {
492
+ const tierCheck = this.budget.checkBudget(result.amount, serviceReputation);
493
+ if (!tierCheck.allowed) {
494
+ // Log for observability but don't throw — payment already signed.
495
+ await this.events.emit('paymentError', {
496
+ operation: 'budget_tier_exceeded',
497
+ error: new Error(tierCheck.reason ?? 'Post-payment tier check failed'),
498
+ context: { url, amount: result.amount.toString(), tier: tierCheck.tier },
499
+ });
500
+ }
501
+ this.budget.recordSpend(result.amount, url);
502
+ }
503
+ return result;
504
+ });
505
+ }
506
+ /** Discover, pay, and rate a service in one call.
507
+ *
508
+ * Given a capability (e.g., "price-feed"), discovers the best-reputation service,
509
+ * pays for it via x402, falls back to alternatives on failure, and submits
510
+ * reputation feedback automatically.
511
+ *
512
+ * Budget lock: Held for the entire retry sequence to prevent concurrent calls
513
+ * from exhausting budget between retries.
514
+ *
515
+ * Feedback lifecycle: Feedback is awaited (not fire-and-forget) so that callers
516
+ * like the MCP tool can safely call destroy() after this method returns without
517
+ * killing in-flight UserOps. Feedback errors are caught and never propagate.
518
+ *
519
+ * @param capability - Service capability to discover (e.g., 'price-feed', 'market-data')
520
+ * @param options - Smart fetch options (minReputation, maxRetries, autoFeedback, etc.)
521
+ */
522
+ async smartFetch402(capability, options) {
523
+ this._requireNotDestroyed();
524
+ return this.budget.acquireBudgetLock(async () => {
525
+ // Pre-flight: only check session-level budget capacity.
526
+ // Same rationale as fetch402 — actual amount unknown until 402 response.
527
+ const effectiveMaxAmount = options?.maxAmount ?? 10000000n;
528
+ const sessionRemaining = this.budget.getRemaining();
529
+ if (effectiveMaxAmount > sessionRemaining) {
530
+ throw new AzethError(`Session budget insufficient: ${effectiveMaxAmount} requested, ${sessionRemaining} remaining`, 'BUDGET_EXCEEDED', { sessionRemaining: sessionRemaining.toString() });
531
+ }
532
+ // Resolve smart account for SIWx identity
533
+ let smartAccount = this._smartAccounts?.[0];
534
+ if (smartAccount === undefined) {
535
+ try {
536
+ smartAccount = await this.resolveSmartAccount();
537
+ }
538
+ catch {
539
+ // No smart account — proceed without SIWx, feedback will be skipped
540
+ }
541
+ }
542
+ // Build smartAccountTransfer callback — routes through pay() for fee capture
543
+ let smartAccountTransfer;
544
+ const payModuleAddr2 = this.addresses.paymentAgreementModule;
545
+ if (smartAccount && this._bundlerUrl && payModuleAddr2) {
546
+ smartAccountTransfer = async (params) => {
547
+ const sac = await this._getSmartAccountClient(smartAccount);
548
+ const payData = encodeFunctionData({
549
+ abi: PaymentAgreementModuleAbi,
550
+ functionName: 'pay',
551
+ args: [
552
+ getAddress(params.usdcAddress),
553
+ getAddress(params.payTo),
554
+ params.amount,
555
+ params.calldata,
556
+ ],
557
+ });
558
+ return sac.sendTransaction({
559
+ to: getAddress(payModuleAddr2),
560
+ value: 0n,
561
+ data: payData,
562
+ });
563
+ };
564
+ }
565
+ const result = await smartFetch402Fn(this.publicClient, this.walletClient, this.address, this.serverUrl, capability, {
566
+ ...options,
567
+ smartAccount,
568
+ smartAccountTransfer,
569
+ }, this.chainName);
570
+ await this.events.emit('afterPayment', {
571
+ url: result.service.endpoint ?? '',
572
+ method: options?.method ?? 'GET',
573
+ paymentMade: result.paymentMade,
574
+ statusCode: result.response.status,
575
+ responseTimeMs: result.responseTimeMs ?? 0,
576
+ amount: result.amount,
577
+ txHash: result.txHash,
578
+ chainId: this.publicClient.chain?.id,
579
+ });
580
+ // Record spending in budget manager (inside lock for atomicity)
581
+ if (result.paymentMade && result.amount) {
582
+ this.budget.recordSpend(result.amount, result.service.endpoint ?? capability);
583
+ }
584
+ // Reputation feedback — only if we have a smart account and a payment was made.
585
+ // Awaited (not fire-and-forget) so destroy() is safe to call after this returns.
586
+ // Errors are swallowed — feedback must never fail the payment flow.
587
+ const autoFeedback = options?.autoFeedback ?? true;
588
+ if (autoFeedback && result.paymentMade && smartAccount) {
589
+ try {
590
+ const sac = await this._getSmartAccountClient(smartAccount);
591
+ // Submit positive feedback for the successful service
592
+ const feedbackValue = computeFeedbackValue(result.responseTimeMs ?? 0);
593
+ await this._submitSmartFeedback(sac, result.service, feedbackValue, 'quality', 'x402');
594
+ // Submit negative feedback for services that failed during routing
595
+ if (result.failedServices) {
596
+ for (const failed of result.failedServices) {
597
+ await this._submitSmartFeedback(sac, failed.service, FAILURE_PENALTY_VALUE, 'reliability', 'x402');
598
+ }
599
+ }
600
+ }
601
+ catch {
602
+ // Feedback must never block or fail the payment result
603
+ }
604
+ }
605
+ return result;
606
+ });
607
+ }
608
+ /** Submit reputation feedback for smartFetch402 routing.
609
+ * Errors are swallowed — this must never fail the payment flow.
610
+ *
611
+ * opinionURI: empty string (auto-generated opinions have no external URI)
612
+ * opinionHash: zero bytes32 (no off-chain content to hash)
613
+ */
614
+ async _submitSmartFeedback(smartAccountClient, service, value, tag1, tag2) {
615
+ try {
616
+ const opinion = {
617
+ agentId: service.tokenId,
618
+ value: BigInt(value) * 10n ** 18n, // WAD: convert integer rating to 18-decimal
619
+ valueDecimals: 18, // WAD: always store in 18-decimal format
620
+ tag1,
621
+ tag2,
622
+ endpoint: service.endpoint ?? '',
623
+ // Auto-generated opinions: no external URI or off-chain content to reference
624
+ opinionURI: '',
625
+ opinionHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
626
+ };
627
+ await submitOnChainOpinion(this.publicClient, smartAccountClient, this.addresses, this.address, opinion);
628
+ }
629
+ catch {
630
+ // Fire-and-forget semantics — never propagate
631
+ }
632
+ }
633
+ // ──────────────────────────────────────────────
634
+ // Trust registry
635
+ // ──────────────────────────────────────────────
636
+ /** Register this account on the ERC-8004 trust registry */
637
+ async publishService(params) {
638
+ this._requireNotDestroyed();
639
+ const account = await this.resolveSmartAccount();
640
+ const smartAccountClient = await this._getSmartAccountClient(account);
641
+ return registerOnRegistry(this.publicClient, smartAccountClient, this.addresses, this.address, params);
642
+ }
643
+ /** Discover services by capability and reputation */
644
+ async discoverServices(params) {
645
+ // Clamp limit to [1, 100] and offset to >= 0
646
+ const clamped = { ...params };
647
+ if (clamped.limit !== undefined) {
648
+ clamped.limit = Math.max(1, Math.min(100, clamped.limit));
649
+ }
650
+ if (clamped.offset !== undefined) {
651
+ clamped.offset = Math.max(0, clamped.offset);
652
+ }
653
+ return discoverServices(this.serverUrl, clamped);
654
+ }
655
+ /** Update metadata for this account's trust registry entry.
656
+ *
657
+ * @param key - Metadata key (e.g., 'endpoint', 'description', 'capabilities')
658
+ * @param value - Metadata value as string (will be hex-encoded internally)
659
+ * @returns Transaction hash
660
+ */
661
+ async updateServiceMetadata(key, value) {
662
+ this._requireNotDestroyed();
663
+ const account = await this.resolveSmartAccount();
664
+ const smartAccountClient = await this._getSmartAccountClient(account);
665
+ return updateMetadata(this.publicClient, smartAccountClient, this.addresses, this.address, key, value);
666
+ }
667
+ /** Update multiple metadata fields in a single batch transaction.
668
+ *
669
+ * @param updates - Array of { key, value } pairs to update
670
+ * @returns Transaction hash of the batch UserOp
671
+ */
672
+ async updateServiceMetadataBatch(updates) {
673
+ this._requireNotDestroyed();
674
+ const account = await this.resolveSmartAccount();
675
+ const smartAccountClient = await this._getSmartAccountClient(account);
676
+ return updateMetadataBatch(this.publicClient, smartAccountClient, this.addresses, this.address, updates);
677
+ }
678
+ // ──────────────────────────────────────────────
679
+ // Payment agreements
680
+ // ──────────────────────────────────────────────
681
+ /** Create a recurring payment agreement */
682
+ async createPaymentAgreement(params) {
683
+ this._requireNotDestroyed();
684
+ validateAddress(params.payee, 'payee');
685
+ validatePositiveAmount(params.amount, 'amount');
686
+ const account = await this.resolveSmartAccount();
687
+ const smartAccountClient = await this._getSmartAccountClient(account);
688
+ return createPaymentAgreement(this.publicClient, smartAccountClient, this.addresses, this.address, params);
689
+ }
690
+ /** Find an active payment agreement with a specific payee.
691
+ * Searches from newest to oldest, returning the first match.
692
+ *
693
+ * @param payee - The payee address to search for
694
+ * @param token - Optional token address to filter by
695
+ * @returns The first matching active agreement, or null
696
+ */
697
+ async findAgreementWithPayee(payee, token) {
698
+ validateAddress(payee, 'payee');
699
+ const account = await this.resolveSmartAccount();
700
+ return findAgreementWithPayee(this.publicClient, this.addresses, account, payee, token);
701
+ }
702
+ /** Get details of a specific payment agreement */
703
+ async getAgreement(agreementId, account) {
704
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
705
+ return getAgreement(this.publicClient, this.addresses, resolvedAccount, agreementId);
706
+ }
707
+ /** Execute a due payment agreement.
708
+ *
709
+ * Auto-detects own vs foreign account:
710
+ * - Own account: executes via UserOp from the payer's smart account (self-execution)
711
+ * - Foreign account: executes as keeper — routes via the caller's own smart account
712
+ * or falls back to direct EOA call if the caller has no smart account
713
+ */
714
+ async executeAgreement(agreementId, account) {
715
+ this._requireNotDestroyed();
716
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
717
+ // Check if this is our own account or a foreign one
718
+ const ownAccounts = await this.getSmartAccounts();
719
+ const isOwnAccount = ownAccounts.some((a) => a.toLowerCase() === resolvedAccount.toLowerCase());
720
+ if (isOwnAccount) {
721
+ // Self-execution: build UserOp from the payer's own smart account
722
+ const smartAccountClient = await this._getSmartAccountClient(resolvedAccount);
723
+ return executeAgreement(this.publicClient, smartAccountClient, this.addresses, resolvedAccount, agreementId);
724
+ }
725
+ // Keeper execution: the resolved account belongs to someone else.
726
+ // Route via the caller's own smart account if available, else direct EOA.
727
+ let keeperSmartAccountClient = null;
728
+ if (ownAccounts.length > 0) {
729
+ keeperSmartAccountClient = await this._getSmartAccountClient(ownAccounts[0]);
730
+ }
731
+ return executeAgreementAsKeeper(this.publicClient, keeperSmartAccountClient, this.walletClient, this.addresses, resolvedAccount, agreementId);
732
+ }
733
+ /** Cancel an active payment agreement. Only the payer can cancel.
734
+ * @param account - Optional: specific smart account that owns the agreement (defaults to first)
735
+ */
736
+ async cancelAgreement(agreementId, account) {
737
+ this._requireNotDestroyed();
738
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
739
+ const smartAccountClient = await this._getSmartAccountClient(resolvedAccount);
740
+ return cancelAgreementFn(this.publicClient, smartAccountClient, this.addresses, agreementId);
741
+ }
742
+ /** Get the total number of agreements for an account */
743
+ async getAgreementCount(account) {
744
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
745
+ return getAgreementCountFn(this.publicClient, this.addresses, resolvedAccount);
746
+ }
747
+ /** Check if a payment agreement can be executed right now */
748
+ async canExecutePayment(agreementId, account) {
749
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
750
+ return canExecutePaymentFn(this.publicClient, this.addresses, resolvedAccount, agreementId);
751
+ }
752
+ /** Get the next execution timestamp for a payment agreement */
753
+ async getNextExecutionTime(agreementId, account) {
754
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
755
+ return getNextExecutionTimeFn(this.publicClient, this.addresses, resolvedAccount, agreementId);
756
+ }
757
+ /** Get comprehensive agreement data in a single RPC call.
758
+ * Combines agreement details + executability + isDue + nextExecutionTime + count. */
759
+ async getAgreementData(agreementId, account) {
760
+ const resolvedAccount = account ?? await this.resolveSmartAccount();
761
+ return getAgreementDataFn(this.publicClient, this.addresses, resolvedAccount, agreementId);
762
+ }
763
+ // ──────────────────────────────────────────────
764
+ // Reputation
765
+ // ──────────────────────────────────────────────
766
+ /** Submit a reputation opinion for an agent via the ReputationModule.
767
+ *
768
+ * Requires a positive net USD payment from this account to the target agent
769
+ * (aggregated on-chain via Chainlink). Value is int128 with configurable decimal precision.
770
+ */
771
+ async submitOpinion(opinion) {
772
+ this._requireNotDestroyed();
773
+ const account = await this.resolveSmartAccount();
774
+ const smartAccountClient = await this._getSmartAccountClient(account);
775
+ return submitOnChainOpinion(this.publicClient, smartAccountClient, this.addresses, this.address, opinion);
776
+ }
777
+ /** Get payment-weighted reputation for an agent.
778
+ *
779
+ * @param agentId - Target agent's ERC-8004 token ID
780
+ * @param raters - Optional list of rater addresses. If omitted, defaults to empty array.
781
+ */
782
+ async getWeightedReputation(agentId, raters) {
783
+ return getWeightedRep(this.publicClient, this.addresses, agentId, raters ?? []);
784
+ }
785
+ /** Get the net payment between this account and a counterparty.
786
+ *
787
+ * - **No token** (default): Returns total net paid in 18-decimal USD, aggregated across
788
+ * all tokens via the on-chain oracle. Always >= 0. This is what the contract uses
789
+ * to gate reputation opinions.
790
+ * - **With token**: Returns the signed per-token delta. Positive means this account
791
+ * has paid more; negative means the counterparty has paid more.
792
+ *
793
+ * @param counterparty - The other account address
794
+ * @param token - Optional token address. Omit for total USD. Use 0x0 for native ETH.
795
+ */
796
+ async getNetPaid(counterparty, token) {
797
+ if (token) {
798
+ return getNetPaidFn(this.publicClient, this.addresses, this.address, counterparty, token);
799
+ }
800
+ return getTotalNetPaidUSDFn(this.publicClient, this.addresses, this.address, counterparty);
801
+ }
802
+ /** Get active opinion state for this account's opinion on an agent.
803
+ *
804
+ * @param agentId - Target agent's ERC-8004 token ID
805
+ * @param account - Smart account address to query. Defaults to first smart account.
806
+ * @returns Active opinion index and existence flag
807
+ */
808
+ async getActiveOpinion(agentId, account) {
809
+ const smartAccount = account ?? await this.resolveSmartAccount();
810
+ return getActiveOpinionFn(this.publicClient, this.addresses, smartAccount, agentId);
811
+ }
812
+ /** Read a single opinion entry from the on-chain registry */
813
+ async readOpinion(agentId, clientAddress, opinionIndex) {
814
+ return readOnChainOpinion(this.publicClient, this.chainName, agentId, clientAddress, opinionIndex);
815
+ }
816
+ // ──────────────────────────────────────────────
817
+ // Messaging
818
+ // ──────────────────────────────────────────────
819
+ /** Send an encrypted message via XMTP.
820
+ *
821
+ * Lazy-initializes the XMTP client on first call.
822
+ *
823
+ * @param params - Message parameters (to, content)
824
+ * @returns The conversation ID
825
+ */
826
+ async sendMessage(params) {
827
+ this._requireNotDestroyed();
828
+ validateAddress(params.to, 'to');
829
+ const client = await this._ensureMessaging();
830
+ return client.sendMessage(params);
831
+ }
832
+ /** Listen for incoming XMTP messages.
833
+ *
834
+ * The handler is registered immediately. If the XMTP client has not been
835
+ * initialized yet, it will be initialized asynchronously when the first
836
+ * handler is registered.
837
+ *
838
+ * @param handler - Async function called for each incoming message
839
+ * @returns Unsubscribe function
840
+ */
841
+ onMessage(handler) {
842
+ this._requireNotDestroyed();
843
+ const client = this._getOrCreateMessaging();
844
+ const unsub = client.onMessage(handler);
845
+ // Kick off initialization if not already started
846
+ if (!client.isReady()) {
847
+ void this._ensureMessaging();
848
+ }
849
+ return unsub;
850
+ }
851
+ /** Check if an address is reachable on the XMTP network.
852
+ *
853
+ * @param address - Ethereum address to check
854
+ * @returns Whether the address can receive XMTP messages
855
+ */
856
+ async canReach(address) {
857
+ validateAddress(address, 'address');
858
+ const client = await this._ensureMessaging();
859
+ return client.canReach(address);
860
+ }
861
+ /** List active XMTP conversations.
862
+ *
863
+ * Lazy-initializes the XMTP client on first call.
864
+ *
865
+ * @returns Array of conversation summaries with peer address and creation time
866
+ */
867
+ async getConversations() {
868
+ this._requireNotDestroyed();
869
+ const client = await this._ensureMessaging();
870
+ return client.getConversations();
871
+ }
872
+ /** Read recent messages from a conversation with a peer.
873
+ *
874
+ * @param peerAddress - Ethereum address of the conversation peer
875
+ * @param limit - Max messages to return (default 20, max 100)
876
+ * @returns Array of messages sorted by timestamp
877
+ */
878
+ async getMessages(peerAddress, limit) {
879
+ this._requireNotDestroyed();
880
+ validateAddress(peerAddress, 'peerAddress');
881
+ const client = await this._ensureMessaging();
882
+ return client.getMessagesByPeer(peerAddress, limit);
883
+ }
884
+ // ──────────────────────────────────────────────
885
+ // Auth helpers
886
+ // ──────────────────────────────────────────────
887
+ /** Get a fetch function that automatically adds ERC-8128 auth headers */
888
+ getSignedFetch() {
889
+ return createSignedFetch(this.walletClient, this.address);
890
+ }
891
+ // ──────────────────────────────────────────────
892
+ // Cleanup
893
+ // ──────────────────────────────────────────────
894
+ /** Clean up resources (XMTP agent, timers, etc.) and zero sensitive material.
895
+ * IMPORTANT: Call this when done with the AzethKit instance to zero private key
896
+ * bytes from memory. Use in a try/finally block for safety. */
897
+ async destroy() {
898
+ this._destroyed = true;
899
+ this.events.removeAllListeners();
900
+ if (this._messaging) {
901
+ await this._messaging.destroy();
902
+ this._messaging = null;
903
+ this._messagingInitPromise = null;
904
+ }
905
+ // H-2 fix: Zero the private key bytes in-place (Uint8Array.fill mutates the buffer)
906
+ this._privateKeyBytes.fill(0);
907
+ // H-6 fix (Audit #8): walletClient retains its own internal copy of the key.
908
+ // Nulling it ensures subsequent calls throw rather than silently succeeding.
909
+ // Note: JS strings are immutable and cannot be reliably zeroed — zeroing of
910
+ // _privateKeyBytes is best-effort defense-in-depth, not a guarantee.
911
+ this['walletClient'] = null;
912
+ }
913
+ // ──────────────────────────────────────────────
914
+ // Private
915
+ // ──────────────────────────────────────────────
916
+ /** Get or create a SmartAccountClient for a specific smart account address.
917
+ *
918
+ * Uses permissionless's createSmartAccountClient with a custom viem SmartAccount
919
+ * implementation that routes all state-changing calls through ERC-4337 UserOperations.
920
+ *
921
+ * Lazily created and cached per smart account address.
922
+ */
923
+ async _getSmartAccountClient(smartAccountAddress) {
924
+ const key = smartAccountAddress.toLowerCase();
925
+ const cached = this._smartAccountClients.get(key);
926
+ if (cached)
927
+ return cached;
928
+ const client = await createAzethSmartAccountClient({
929
+ publicClient: this.publicClient,
930
+ walletClient: this.walletClient,
931
+ smartAccountAddress,
932
+ bundlerUrl: this._bundlerUrl,
933
+ paymasterUrl: this._paymasterUrl,
934
+ paymasterPolicy: this._paymasterPolicy,
935
+ // Only pass guardian key for auto-signing when explicitly enabled.
936
+ // When guardianAutoSign is false, operations exceeding limits will require
937
+ // interactive approval (XMTP) rather than being auto-signed.
938
+ guardianKey: this._guardianAutoSign ? this._guardianKey : undefined,
939
+ // Pass serverUrl so the bundler URL resolution can fall back to the
940
+ // Azeth server's bundler proxy on testnet (zero-friction onboarding).
941
+ serverUrl: this.serverUrl,
942
+ });
943
+ this._smartAccountClients.set(key, client);
944
+ return client;
945
+ }
946
+ /** Get or create the XMTPClient instance (without initialization) */
947
+ _getOrCreateMessaging() {
948
+ if (!this._messaging) {
949
+ this._messaging = new XMTPClient();
950
+ }
951
+ return this._messaging;
952
+ }
953
+ /** Ensure the XMTP client is initialized. Returns the ready client. */
954
+ async _ensureMessaging() {
955
+ // MEDIUM-7 fix: Prevent using zeroed private key after destroy()
956
+ if (this._destroyed) {
957
+ throw new AzethError('AzethKit has been destroyed — cannot initialize messaging', 'INVALID_INPUT');
958
+ }
959
+ const client = this._getOrCreateMessaging();
960
+ if (client.isReady())
961
+ return client;
962
+ // Deduplicate concurrent init calls
963
+ if (!this._messagingInitPromise) {
964
+ this._messagingInitPromise = client.initialize(bytesToHex(this._privateKeyBytes), this._xmtpConfig);
965
+ }
966
+ await this._messagingInitPromise;
967
+ return client;
968
+ }
969
+ }
970
+ //# sourceMappingURL=client.js.map