@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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/account/balance.d.ts +41 -0
- package/dist/account/balance.d.ts.map +1 -0
- package/dist/account/balance.js +264 -0
- package/dist/account/balance.js.map +1 -0
- package/dist/account/create.d.ts +27 -0
- package/dist/account/create.d.ts.map +1 -0
- package/dist/account/create.js +116 -0
- package/dist/account/create.js.map +1 -0
- package/dist/account/deposit.d.ts +34 -0
- package/dist/account/deposit.d.ts.map +1 -0
- package/dist/account/deposit.js +88 -0
- package/dist/account/deposit.js.map +1 -0
- package/dist/account/guardian-approval.d.ts +111 -0
- package/dist/account/guardian-approval.d.ts.map +1 -0
- package/dist/account/guardian-approval.js +223 -0
- package/dist/account/guardian-approval.js.map +1 -0
- package/dist/account/guardian.d.ts +27 -0
- package/dist/account/guardian.d.ts.map +1 -0
- package/dist/account/guardian.js +67 -0
- package/dist/account/guardian.js.map +1 -0
- package/dist/account/history.d.ts +22 -0
- package/dist/account/history.d.ts.map +1 -0
- package/dist/account/history.js +144 -0
- package/dist/account/history.js.map +1 -0
- package/dist/account/transfer.d.ts +28 -0
- package/dist/account/transfer.d.ts.map +1 -0
- package/dist/account/transfer.js +137 -0
- package/dist/account/transfer.js.map +1 -0
- package/dist/auth/erc8128.d.ts +14 -0
- package/dist/auth/erc8128.d.ts.map +1 -0
- package/dist/auth/erc8128.js +92 -0
- package/dist/auth/erc8128.js.map +1 -0
- package/dist/client.d.ts +394 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +970 -0
- package/dist/client.js.map +1 -0
- package/dist/events/emitter.d.ts +96 -0
- package/dist/events/emitter.d.ts.map +1 -0
- package/dist/events/emitter.js +90 -0
- package/dist/events/emitter.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/messaging/message-router.d.ts +69 -0
- package/dist/messaging/message-router.d.ts.map +1 -0
- package/dist/messaging/message-router.js +307 -0
- package/dist/messaging/message-router.js.map +1 -0
- package/dist/messaging/rate-limiter.d.ts +31 -0
- package/dist/messaging/rate-limiter.d.ts.map +1 -0
- package/dist/messaging/rate-limiter.js +74 -0
- package/dist/messaging/rate-limiter.js.map +1 -0
- package/dist/messaging/xmtp.d.ts +144 -0
- package/dist/messaging/xmtp.d.ts.map +1 -0
- package/dist/messaging/xmtp.js +473 -0
- package/dist/messaging/xmtp.js.map +1 -0
- package/dist/payments/agreements.d.ts +87 -0
- package/dist/payments/agreements.d.ts.map +1 -0
- package/dist/payments/agreements.js +337 -0
- package/dist/payments/agreements.js.map +1 -0
- package/dist/payments/budget.d.ts +118 -0
- package/dist/payments/budget.d.ts.map +1 -0
- package/dist/payments/budget.js +176 -0
- package/dist/payments/budget.js.map +1 -0
- package/dist/payments/smart-fetch.d.ts +65 -0
- package/dist/payments/smart-fetch.d.ts.map +1 -0
- package/dist/payments/smart-fetch.js +115 -0
- package/dist/payments/smart-fetch.js.map +1 -0
- package/dist/payments/x402.d.ts +89 -0
- package/dist/payments/x402.d.ts.map +1 -0
- package/dist/payments/x402.js +620 -0
- package/dist/payments/x402.js.map +1 -0
- package/dist/registry/discover.d.ts +43 -0
- package/dist/registry/discover.d.ts.map +1 -0
- package/dist/registry/discover.js +272 -0
- package/dist/registry/discover.js.map +1 -0
- package/dist/registry/register.d.ts +44 -0
- package/dist/registry/register.d.ts.map +1 -0
- package/dist/registry/register.js +126 -0
- package/dist/registry/register.js.map +1 -0
- package/dist/reputation/opinion.d.ts +52 -0
- package/dist/reputation/opinion.d.ts.map +1 -0
- package/dist/reputation/opinion.js +198 -0
- package/dist/reputation/opinion.js.map +1 -0
- package/dist/utils/addresses.d.ts +6 -0
- package/dist/utils/addresses.d.ts.map +1 -0
- package/dist/utils/addresses.js +53 -0
- package/dist/utils/addresses.js.map +1 -0
- package/dist/utils/errors.d.ts +23 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +188 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/execution.d.ts +20 -0
- package/dist/utils/execution.d.ts.map +1 -0
- package/dist/utils/execution.js +28 -0
- package/dist/utils/execution.js.map +1 -0
- package/dist/utils/paymaster.d.ts +35 -0
- package/dist/utils/paymaster.d.ts.map +1 -0
- package/dist/utils/paymaster.js +115 -0
- package/dist/utils/paymaster.js.map +1 -0
- package/dist/utils/retry.d.ts +19 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +68 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/userop.d.ts +55 -0
- package/dist/utils/userop.d.ts.map +1 -0
- package/dist/utils/userop.js +201 -0
- package/dist/utils/userop.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +35 -0
- package/dist/utils/validation.js.map +1 -0
- 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
|