@awarizon/web3 1.0.1 → 1.1.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/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  var viem = require('viem');
4
4
  var walletEngine = require('@awarizon/wallet-engine');
5
+ var txEngine = require('@awarizon/tx-engine');
5
6
  var chains = require('viem/chains');
6
7
  var abiEngine = require('@awarizon/abi-engine');
7
- var txEngine = require('@awarizon/tx-engine');
8
8
 
9
9
  // src/sdk.ts
10
10
  var CHAINS = {
@@ -57,6 +57,7 @@ var CHAINS = {
57
57
  fantom: chains.fantom,
58
58
  moonbeam: chains.moonbeam
59
59
  };
60
+ var _chainAliases = Object.keys(CHAINS).sort().join(", ");
60
61
  function resolveChain(chain) {
61
62
  if (typeof chain === "object" && "id" in chain) {
62
63
  return chain;
@@ -64,10 +65,9 @@ function resolveChain(chain) {
64
65
  if (typeof chain === "string") {
65
66
  const resolved = CHAINS[chain.toLowerCase()] ?? CHAINS[chain];
66
67
  if (!resolved) {
67
- const available = Object.keys(CHAINS).filter((k, i, arr) => arr.indexOf(k) === i).sort().join(", ");
68
68
  throw new Error(
69
69
  `[awarizon/web3] Unsupported chain: "${chain}".
70
- Available chains: ${available}`
70
+ Available chains: ${_chainAliases}`
71
71
  );
72
72
  }
73
73
  return resolved;
@@ -82,37 +82,58 @@ function getSupportedChainIds() {
82
82
  return true;
83
83
  }).map((c) => c.id);
84
84
  }
85
- function buildContractInstance(address, abi, publicClient, getWalletClient) {
85
+ function trackEvent(telemetry, type, chain, functionName, success, start) {
86
+ telemetry?.track({
87
+ type,
88
+ chain: chain?.name ?? "unknown",
89
+ chainId: chain?.id ?? 0,
90
+ functionName,
91
+ success,
92
+ durationMs: Date.now() - start,
93
+ ts: Date.now()
94
+ });
95
+ }
96
+ function buildContractInstance(address, abi, publicClient, txEngine, telemetry, chain) {
86
97
  const parsed = abiEngine.parseABI(abi);
87
- const txEngine$1 = new txEngine.TransactionEngine(publicClient, getWalletClient);
98
+ const eventsMap = new Map(parsed.events.map((e) => [e.name, e]));
88
99
  const instance = {
89
100
  _address: address,
90
101
  _abi: abi
91
102
  };
92
103
  for (const fn of parsed.readFunctions) {
93
- instance[fn.name] = buildReadMethod(fn, address, abi, txEngine$1);
104
+ instance[fn.name] = buildReadMethod(fn, address, abi, txEngine, telemetry, chain);
94
105
  }
95
106
  for (const fn of parsed.writeFunctions) {
96
- instance[fn.name] = buildWriteMethod(fn, address, abi, txEngine$1);
107
+ instance[fn.name] = buildWriteMethod(fn, address, abi, txEngine, telemetry, chain);
97
108
  }
98
- instance["on"] = buildEventSubscription(address, abi, parsed.events, publicClient);
109
+ instance["on"] = buildEventSubscription(address, abi, eventsMap, publicClient);
99
110
  instance["estimateGas"] = async (method, ...args) => {
100
- const estimate = await txEngine$1.estimateGas({
101
- address,
102
- abi,
103
- functionName: method,
104
- args
105
- });
106
- return estimate.gas;
111
+ const start = Date.now();
112
+ try {
113
+ const estimate = await txEngine.estimateGas({ address, abi, functionName: method, args });
114
+ trackEvent(telemetry, "contract.gas_estimate", chain, method, true, start);
115
+ return estimate.gas;
116
+ } catch (error) {
117
+ trackEvent(telemetry, "contract.gas_estimate", chain, method, false, start);
118
+ throw error;
119
+ }
107
120
  };
108
121
  return instance;
109
122
  }
110
- function buildReadMethod(fn, address, abi, txEngine) {
123
+ function buildReadMethod(fn, address, abi, txEngine, telemetry, chain) {
111
124
  return async (...args) => {
112
- return txEngine.read({ address, abi, functionName: fn.name, args });
125
+ const start = Date.now();
126
+ try {
127
+ const result = await txEngine.read({ address, abi, functionName: fn.name, args });
128
+ trackEvent(telemetry, "contract.read", chain, fn.name, true, start);
129
+ return result;
130
+ } catch (error) {
131
+ trackEvent(telemetry, "contract.read", chain, fn.name, false, start);
132
+ throw error;
133
+ }
113
134
  };
114
135
  }
115
- function buildWriteMethod(fn, address, abi, txEngine) {
136
+ function buildWriteMethod(fn, address, abi, txEngine, telemetry, chain) {
116
137
  const isPayable = fn.stateMutability === "payable";
117
138
  return async (...args) => {
118
139
  let callArgs = args;
@@ -126,14 +147,22 @@ function buildWriteMethod(fn, address, abi, txEngine) {
126
147
  gas = last.gas;
127
148
  }
128
149
  }
129
- return txEngine.write({
130
- address,
131
- abi,
132
- functionName: fn.name,
133
- args: callArgs,
134
- value,
135
- gas
136
- });
150
+ const start = Date.now();
151
+ try {
152
+ const result = await txEngine.write({
153
+ address,
154
+ abi,
155
+ functionName: fn.name,
156
+ args: callArgs,
157
+ value,
158
+ gas
159
+ });
160
+ trackEvent(telemetry, "contract.write", chain, fn.name, true, start);
161
+ return result;
162
+ } catch (error) {
163
+ trackEvent(telemetry, "contract.write", chain, fn.name, false, start);
164
+ throw error;
165
+ }
137
166
  };
138
167
  }
139
168
  function isPayableOptions(val) {
@@ -143,11 +172,11 @@ function isPayableOptions(val) {
143
172
  const hasGas = "gas" in obj && (obj.gas === void 0 || typeof obj.gas === "bigint");
144
173
  return hasValue || hasGas;
145
174
  }
146
- function buildEventSubscription(address, abi, events, publicClient) {
175
+ function buildEventSubscription(address, abi, eventsMap, publicClient) {
147
176
  return (eventName, callback) => {
148
- const abiEvent = events.find((e) => e.name === eventName);
177
+ const abiEvent = eventsMap.get(eventName);
149
178
  if (!abiEvent) {
150
- const available = events.map((e) => e.name).join(", ") || "none";
179
+ const available = [...eventsMap.keys()].join(", ") || "none";
151
180
  throw new Error(
152
181
  `[awarizon/web3] Event "${eventName}" not found on contract ${address}.
153
182
  Available events: ${available}`
@@ -184,7 +213,7 @@ var ProviderError = class extends Error {
184
213
  };
185
214
  var ContractNotLoadedError = class extends Error {
186
215
  constructor(address) {
187
- super(`[awarizon/web3] Contract at ${address} is not loaded. Call sdk.contract({ address, abi }) first.`);
216
+ super(`[awarizon/web3] Contract at ${address} is not loaded. Call awarizon.contract({ address, abi }) first.`);
188
217
  this.code = "CONTRACT_NOT_LOADED";
189
218
  this.name = "ContractNotLoadedError";
190
219
  }
@@ -197,101 +226,562 @@ var UnsupportedChainError = class extends Error {
197
226
  this.chain = chain;
198
227
  }
199
228
  };
229
+ var ApiKeyRequiredError = class extends Error {
230
+ constructor() {
231
+ super(
232
+ '[awarizon/web3] An API key is required. Pass apiKey: "awz_live_..." to AwarizonWeb3({ ... }). Get yours at https://awarizon.com/dashboard/api-keys'
233
+ );
234
+ this.code = "API_KEY_REQUIRED";
235
+ this.name = "ApiKeyRequiredError";
236
+ }
237
+ };
238
+ var InvalidApiKeyError = class extends Error {
239
+ constructor(message = "Invalid or revoked API key.") {
240
+ super(`[awarizon/web3] ${message}`);
241
+ this.code = "INVALID_API_KEY";
242
+ this.name = "InvalidApiKeyError";
243
+ }
244
+ };
200
245
 
201
- // src/sdk.ts
246
+ // src/telemetry.ts
247
+ var DEFAULT_BASE_URL = "https://awarizon.com";
248
+ var FLUSH_INTERVAL_MS = 1e4;
249
+ var MAX_BATCH_SIZE = 20;
250
+ var VALIDATION_TIMEOUT = 5e3;
251
+ var TelemetryClient = class {
252
+ constructor(apiKey, baseUrl = DEFAULT_BASE_URL) {
253
+ this.validated = false;
254
+ this.validationPromise = null;
255
+ this.queue = [];
256
+ this.flushTimer = null;
257
+ this.unloadListener = null;
258
+ this.apiKey = apiKey;
259
+ this.baseUrl = baseUrl.replace(/\/$/, "");
260
+ }
261
+ /**
262
+ * Ensures the API key has been validated against the Awarizon API.
263
+ * Idempotent — safe to call on every operation; only hits the network once.
264
+ * Times out after 5 seconds to avoid hanging in degraded network conditions.
265
+ *
266
+ * @throws {InvalidApiKeyError} if the key is invalid, revoked, or unreachable
267
+ */
268
+ async ensureValidated() {
269
+ if (this.validated) return;
270
+ if (this.validationPromise) return this.validationPromise;
271
+ this.validationPromise = this._validate().catch((err) => {
272
+ this.validationPromise = null;
273
+ throw err;
274
+ });
275
+ return this.validationPromise;
276
+ }
277
+ /**
278
+ * Queue a usage event. Events are batched and sent periodically.
279
+ * Must only be called after ensureValidated() has resolved.
280
+ */
281
+ track(event) {
282
+ if (!this.validated) return;
283
+ this.queue.push(event);
284
+ if (this.queue.length >= MAX_BATCH_SIZE) this._flush(false);
285
+ }
286
+ /**
287
+ * Flush pending events and stop background timers.
288
+ * Call when tearing down a long-lived SDK instance (e.g. server shutdown).
289
+ */
290
+ async destroy() {
291
+ this._stopTimers();
292
+ await this._flush(true);
293
+ }
294
+ // ─── Private ─────────────────────────────────────────────────────────────────
295
+ async _validate() {
296
+ const controller = new AbortController();
297
+ const timeout = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT);
298
+ let res;
299
+ try {
300
+ res = await fetch(`${this.baseUrl}/api/v1/auth`, {
301
+ method: "GET",
302
+ headers: { Authorization: `Bearer ${this.apiKey}` },
303
+ signal: controller.signal
304
+ });
305
+ } catch (err) {
306
+ const msg = controller.signal.aborted ? `Awarizon API did not respond within ${VALIDATION_TIMEOUT / 1e3}s.` : `Could not reach Awarizon API: ${err.message}`;
307
+ throw new InvalidApiKeyError(msg);
308
+ } finally {
309
+ clearTimeout(timeout);
310
+ }
311
+ if (!res.ok) {
312
+ const body = await res.json().catch(() => ({}));
313
+ throw new InvalidApiKeyError(body.error ?? "Invalid or revoked API key.");
314
+ }
315
+ this.validated = true;
316
+ this._startTimers();
317
+ }
318
+ _startTimers() {
319
+ this.flushTimer = setInterval(() => {
320
+ if (this.queue.length > 0) this._flush(false);
321
+ }, FLUSH_INTERVAL_MS);
322
+ const timer = this.flushTimer;
323
+ if (typeof timer.unref === "function") timer.unref();
324
+ if (typeof window !== "undefined" && typeof navigator !== "undefined") {
325
+ this.unloadListener = () => this._flushBeacon();
326
+ window.addEventListener("beforeunload", this.unloadListener);
327
+ }
328
+ }
329
+ _stopTimers() {
330
+ if (this.flushTimer !== null) {
331
+ clearInterval(this.flushTimer);
332
+ this.flushTimer = null;
333
+ }
334
+ if (this.unloadListener !== null && typeof window !== "undefined") {
335
+ window.removeEventListener("beforeunload", this.unloadListener);
336
+ this.unloadListener = null;
337
+ }
338
+ }
339
+ _flush(awaited) {
340
+ if (this.queue.length === 0) return awaited ? Promise.resolve() : void 0;
341
+ const batch = this.queue.splice(0, MAX_BATCH_SIZE);
342
+ const promise = fetch(`${this.baseUrl}/api/v1/usage`, {
343
+ method: "POST",
344
+ headers: {
345
+ Authorization: `Bearer ${this.apiKey}`,
346
+ "Content-Type": "application/json"
347
+ },
348
+ body: JSON.stringify({ events: batch })
349
+ }).then(() => {
350
+ }, () => {
351
+ });
352
+ return awaited ? promise : void 0;
353
+ }
354
+ /** Uses sendBeacon for guaranteed delivery on page unload */
355
+ _flushBeacon() {
356
+ if (this.queue.length === 0 || typeof navigator === "undefined") return;
357
+ const batch = this.queue.splice(0);
358
+ navigator.sendBeacon(
359
+ `${this.baseUrl}/api/v1/usage`,
360
+ new Blob([JSON.stringify({ events: batch })], { type: "application/json" })
361
+ );
362
+ }
363
+ };
364
+ var ERC20_ABI = viem.erc20Abi;
365
+ var ERC721_ABI = [
366
+ { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
367
+ { type: "function", name: "symbol", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
368
+ { type: "function", name: "tokenURI", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ type: "string" }] },
369
+ { type: "function", name: "totalSupply", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
370
+ { type: "function", name: "ownerOf", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ type: "address" }] },
371
+ { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] },
372
+ { type: "function", name: "getApproved", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ type: "address" }] },
373
+ { type: "function", name: "isApprovedForAll", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "operator", type: "address" }], outputs: [{ type: "bool" }] },
374
+ { type: "function", name: "transferFrom", stateMutability: "nonpayable", inputs: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "tokenId", type: "uint256" }], outputs: [] },
375
+ { type: "function", name: "safeTransferFrom", stateMutability: "nonpayable", inputs: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "tokenId", type: "uint256" }], outputs: [] },
376
+ { type: "function", name: "approve", stateMutability: "nonpayable", inputs: [{ name: "to", type: "address" }, { name: "tokenId", type: "uint256" }], outputs: [] },
377
+ { type: "function", name: "setApprovalForAll", stateMutability: "nonpayable", inputs: [{ name: "operator", type: "address" }, { name: "approved", type: "bool" }], outputs: [] },
378
+ { type: "event", name: "Transfer", inputs: [{ indexed: true, name: "from", type: "address" }, { indexed: true, name: "to", type: "address" }, { indexed: true, name: "tokenId", type: "uint256" }] },
379
+ { type: "event", name: "Approval", inputs: [{ indexed: true, name: "owner", type: "address" }, { indexed: true, name: "approved", type: "address" }, { indexed: true, name: "tokenId", type: "uint256" }] },
380
+ { type: "event", name: "ApprovalForAll", inputs: [{ indexed: true, name: "owner", type: "address" }, { indexed: true, name: "operator", type: "address" }, { name: "approved", type: "bool" }] }
381
+ ];
382
+ var ERC1155_ABI = [
383
+ { type: "function", name: "uri", stateMutability: "view", inputs: [{ name: "id", type: "uint256" }], outputs: [{ type: "string" }] },
384
+ { type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ name: "account", type: "address" }, { name: "id", type: "uint256" }], outputs: [{ type: "uint256" }] },
385
+ { type: "function", name: "balanceOfBatch", stateMutability: "view", inputs: [{ name: "accounts", type: "address[]" }, { name: "ids", type: "uint256[]" }], outputs: [{ type: "uint256[]" }] },
386
+ { type: "function", name: "isApprovedForAll", stateMutability: "view", inputs: [{ name: "account", type: "address" }, { name: "operator", type: "address" }], outputs: [{ type: "bool" }] },
387
+ { type: "function", name: "setApprovalForAll", stateMutability: "nonpayable", inputs: [{ name: "operator", type: "address" }, { name: "approved", type: "bool" }], outputs: [] },
388
+ { type: "function", name: "safeTransferFrom", stateMutability: "nonpayable", inputs: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "id", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "data", type: "bytes" }], outputs: [] },
389
+ { type: "function", name: "safeBatchTransferFrom", stateMutability: "nonpayable", inputs: [{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "ids", type: "uint256[]" }, { name: "amounts", type: "uint256[]" }, { name: "data", type: "bytes" }], outputs: [] },
390
+ { type: "event", name: "TransferSingle", inputs: [{ indexed: true, name: "operator", type: "address" }, { indexed: true, name: "from", type: "address" }, { indexed: true, name: "to", type: "address" }, { name: "id", type: "uint256" }, { name: "value", type: "uint256" }] },
391
+ { type: "event", name: "TransferBatch", inputs: [{ indexed: true, name: "operator", type: "address" }, { indexed: true, name: "from", type: "address" }, { indexed: true, name: "to", type: "address" }, { name: "ids", type: "uint256[]" }, { name: "values", type: "uint256[]" }] },
392
+ { type: "event", name: "ApprovalForAll", inputs: [{ indexed: true, name: "account", type: "address" }, { indexed: true, name: "operator", type: "address" }, { name: "approved", type: "bool" }] }
393
+ ];
394
+ var WalletProxy = class {
395
+ constructor(engine, ensureReady) {
396
+ this.engine = engine;
397
+ this.ensureReady = ensureReady;
398
+ }
399
+ // ── Gated async operations ──────────────────────────────────────────────────
400
+ async create() {
401
+ await this.ensureReady();
402
+ return this.engine.create();
403
+ }
404
+ async importMnemonic(mnemonic, accountIndex) {
405
+ await this.ensureReady();
406
+ return this.engine.importMnemonic(mnemonic, accountIndex);
407
+ }
408
+ async importPrivateKey(privateKey) {
409
+ await this.ensureReady();
410
+ return this.engine.importPrivateKey(privateKey);
411
+ }
412
+ // ── Pass-throughs (sync, no network) ───────────────────────────────────────
413
+ address() {
414
+ return this.engine.address();
415
+ }
416
+ getSigner() {
417
+ return this.engine.getWalletClient();
418
+ }
419
+ getWalletClient() {
420
+ return this.engine.getWalletClient();
421
+ }
422
+ isConnected() {
423
+ return this.engine.isConnected();
424
+ }
425
+ hasExternalWallet() {
426
+ return this.engine.hasExternalWallet();
427
+ }
428
+ hasInternalWallet() {
429
+ return this.engine.hasInternalWallet();
430
+ }
431
+ connectExternal(c) {
432
+ this.engine.connectExternal(c);
433
+ }
434
+ disconnectExternal() {
435
+ this.engine.disconnectExternal();
436
+ }
437
+ disconnect() {
438
+ this.engine.disconnect();
439
+ }
440
+ async switchChain(chain) {
441
+ return this.engine.switchChain(chain);
442
+ }
443
+ };
202
444
  var AwarizonWeb3 = class {
203
445
  constructor(config) {
446
+ // Contract instances are cached by address — same address returns same instance.
447
+ // Cache is cleared on switchChain() since contracts are chain-specific.
448
+ this._contractCache = /* @__PURE__ */ new Map();
449
+ // Named contract registry — register once, reference by name anywhere.
450
+ this._registry = /* @__PURE__ */ new Map();
451
+ if (!config.apiKey) throw new ApiKeyRequiredError();
204
452
  this.chain = resolveChain(config.chain);
205
453
  this.publicClient = viem.createPublicClient({
206
454
  chain: this.chain,
207
455
  transport: viem.http(config.rpcUrl)
208
456
  });
209
- this.wallet = new walletEngine.WalletEngine({
457
+ this._engine = new walletEngine.WalletEngine({
210
458
  chain: this.chain,
211
459
  publicClient: this.publicClient,
212
460
  rpcUrl: config.rpcUrl
213
461
  });
462
+ this._txEngine = new txEngine.TransactionEngine(
463
+ this.publicClient,
464
+ () => this._engine.getWalletClient()
465
+ );
466
+ this._telemetry = new TelemetryClient(config.apiKey, config.baseUrl);
467
+ this.wallet = new WalletProxy(
468
+ this._engine,
469
+ () => this._telemetry.ensureValidated()
470
+ );
214
471
  if (config.signer) {
215
- this.wallet.connectExternal(config.signer);
472
+ this._engine.connectExternal(config.signer);
216
473
  }
217
474
  }
218
475
  // ─── Wallet API surface ─────────────────────────────────────────────────────
219
- /**
220
- * Connect an external wallet client (wagmi, WalletConnect, viem, EIP-1193).
221
- * The connected signer takes priority over any internal wallet.
222
- *
223
- * @example
224
- * sdk.connectWallet(walletClient)
225
- */
226
476
  connectWallet(walletClient) {
227
- this.wallet.connectExternal(walletClient);
477
+ this._engine.connectExternal(walletClient);
228
478
  return this;
229
479
  }
230
- /**
231
- * Disconnect the externally-connected wallet.
232
- * Falls back to the internal wallet if one is loaded.
233
- */
234
480
  disconnectWallet() {
235
- this.wallet.disconnectExternal();
481
+ this._engine.disconnectExternal();
236
482
  return this;
237
483
  }
238
- /**
239
- * Switch the active chain.
240
- * Rebuilds the PublicClient and internal WalletClient for the new chain.
241
- */
242
484
  async switchChain(chain) {
485
+ await this._telemetry.ensureValidated();
243
486
  const resolved = resolveChain(chain);
244
- await this.wallet.switchChain(resolved);
487
+ await this._engine.switchChain(resolved);
488
+ this._contractCache.clear();
245
489
  return this;
246
490
  }
247
- /**
248
- * Returns a summary of the currently active wallet.
249
- * @throws {WalletNotConnectedError} if no wallet is connected
250
- */
251
491
  getWalletInfo() {
252
492
  return {
253
- address: this.wallet.address(),
493
+ address: this._engine.address(),
254
494
  chain: this.chain,
255
- isExternal: this.wallet.hasExternalWallet()
495
+ isExternal: this._engine.hasExternalWallet()
256
496
  };
257
497
  }
258
498
  // ─── Contract API ───────────────────────────────────────────────────────────
259
499
  /**
260
- * Load a deployed smart contract and return a fully typed, ABI-powered
261
- * instance. Every function in the ABI is exposed as a direct method.
262
- *
263
- * **Read functions** (view / pure) use the PublicClient — no signer needed.
264
- * **Write functions** (nonpayable / payable) use the active WalletClient.
500
+ * Load a deployed contract and return a fully typed, ABI-powered instance.
501
+ * Validates the API key on the first call subsequent calls are instant.
265
502
  *
266
- * @example
267
- * const token = await sdk.contract({ address: "0x...", abi: ERC20_ABI })
268
- * const balance = await token.balanceOf(address) // read
269
- * await token.transfer(to, 100n) // write
270
- * token.on("Transfer", handler) // events
503
+ * Results are cached by address — calling contract() twice with the same
504
+ * address and ABI object returns the same instance without rebuilding.
271
505
  */
272
506
  async contract(config) {
507
+ await this._telemetry.ensureValidated();
273
508
  if (!config.address || !config.abi) {
274
- throw new Error("[awarizon/web3] sdk.contract() requires both address and abi");
509
+ throw new Error("[awarizon/web3] awarizon.contract() requires both address and abi");
275
510
  }
276
- return buildContractInstance(
511
+ const key = config.address.toLowerCase();
512
+ const cached = this._contractCache.get(key);
513
+ if (cached && cached.abiRef === config.abi) return cached.instance;
514
+ const instance = buildContractInstance(
277
515
  config.address,
278
516
  config.abi,
279
517
  this.publicClient,
280
- () => this.wallet.getWalletClient()
518
+ this._txEngine,
519
+ this._telemetry,
520
+ this.chain
281
521
  );
522
+ this._contractCache.set(key, { abiRef: config.abi, instance });
523
+ return instance;
524
+ }
525
+ /**
526
+ * Load multiple contracts in parallel. Validates the API key once, then
527
+ * instantiates all contracts concurrently.
528
+ *
529
+ * @example
530
+ * ```ts
531
+ * const [token, staking, nft] = await awarizon.contracts([
532
+ * { address: tokenAddr, abi: ERC20_ABI },
533
+ * { address: stakingAddr, abi: STAKING_ABI },
534
+ * { address: nftAddr, abi: ERC721_ABI },
535
+ * ])
536
+ * ```
537
+ */
538
+ async contracts(configs) {
539
+ return Promise.all(configs.map((c) => this.contract(c)));
540
+ }
541
+ /**
542
+ * Call a read-only (view/pure) contract function directly without loading
543
+ * a full contract instance. Best for one-off reads.
544
+ *
545
+ * @example
546
+ * ```ts
547
+ * const balance = await awarizon.read<bigint>({
548
+ * address: tokenAddress,
549
+ * abi: ERC20_ABI,
550
+ * method: "balanceOf",
551
+ * args: [userAddress],
552
+ * })
553
+ * ```
554
+ */
555
+ async read(params) {
556
+ await this._telemetry.ensureValidated();
557
+ const start = Date.now();
558
+ try {
559
+ const result = await this._txEngine.read({
560
+ address: params.address,
561
+ abi: params.abi,
562
+ functionName: params.method,
563
+ args: params.args ?? []
564
+ });
565
+ this._telemetry.track({
566
+ type: "contract.read",
567
+ chain: this.chain.name,
568
+ chainId: this.chain.id,
569
+ functionName: params.method,
570
+ success: true,
571
+ durationMs: Date.now() - start,
572
+ ts: Date.now()
573
+ });
574
+ return result;
575
+ } catch (err) {
576
+ this._telemetry.track({
577
+ type: "contract.read",
578
+ chain: this.chain.name,
579
+ chainId: this.chain.id,
580
+ functionName: params.method,
581
+ success: false,
582
+ durationMs: Date.now() - start,
583
+ ts: Date.now()
584
+ });
585
+ throw err;
586
+ }
587
+ }
588
+ /**
589
+ * Execute a state-mutating contract function directly without loading a full
590
+ * contract instance. Best for one-off writes.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * const { hash, receipt } = await awarizon.write({
595
+ * address: tokenAddress,
596
+ * abi: ERC20_ABI,
597
+ * method: "transfer",
598
+ * args: [recipient, 100n],
599
+ * })
600
+ * ```
601
+ */
602
+ async write(params) {
603
+ await this._telemetry.ensureValidated();
604
+ const start = Date.now();
605
+ try {
606
+ const result = await this._txEngine.write({
607
+ address: params.address,
608
+ abi: params.abi,
609
+ functionName: params.method,
610
+ args: params.args ?? [],
611
+ value: params.value,
612
+ gas: params.gas
613
+ });
614
+ this._telemetry.track({
615
+ type: "contract.write",
616
+ chain: this.chain.name,
617
+ chainId: this.chain.id,
618
+ functionName: params.method,
619
+ success: true,
620
+ durationMs: Date.now() - start,
621
+ ts: Date.now()
622
+ });
623
+ return result;
624
+ } catch (err) {
625
+ this._telemetry.track({
626
+ type: "contract.write",
627
+ chain: this.chain.name,
628
+ chainId: this.chain.id,
629
+ functionName: params.method,
630
+ success: false,
631
+ durationMs: Date.now() - start,
632
+ ts: Date.now()
633
+ });
634
+ throw err;
635
+ }
636
+ }
637
+ /**
638
+ * Batch multiple read calls into a single RPC round-trip using multicall3.
639
+ * Falls back to parallel individual reads on chains where multicall3 is
640
+ * not deployed.
641
+ *
642
+ * @example
643
+ * ```ts
644
+ * const [balance, supply, allowance] = await awarizon.multicall([
645
+ * { address: tokenAddr, abi: ERC20_ABI, method: "balanceOf", args: [userAddr] },
646
+ * { address: tokenAddr, abi: ERC20_ABI, method: "totalSupply" },
647
+ * { address: tokenAddr, abi: ERC20_ABI, method: "allowance", args: [userAddr, spenderAddr] },
648
+ * ])
649
+ * ```
650
+ */
651
+ async multicall(calls) {
652
+ await this._telemetry.ensureValidated();
653
+ const contracts = calls.map((c) => ({
654
+ address: c.address,
655
+ abi: c.abi,
656
+ functionName: c.method,
657
+ args: c.args ?? []
658
+ }));
659
+ try {
660
+ const results = await this.publicClient.multicall({
661
+ contracts,
662
+ allowFailure: false
663
+ });
664
+ return results;
665
+ } catch {
666
+ return Promise.all(
667
+ contracts.map(
668
+ (c) => this.publicClient.readContract(c)
669
+ )
670
+ );
671
+ }
672
+ }
673
+ // ─── ERC Standard Clients ───────────────────────────────────────────────────
674
+ /**
675
+ * Load an ERC-20 token with fully typed methods — no ABI import needed.
676
+ *
677
+ * ```ts
678
+ * const token = await awarizon.erc20("0x...")
679
+ * const balance = await token.balanceOf(userAddress)
680
+ * const symbol = await token.symbol()
681
+ * await token.transfer(recipient, 100n)
682
+ * ```
683
+ */
684
+ async erc20(address) {
685
+ const instance = await this.contract({ address, abi: ERC20_ABI });
686
+ return instance;
282
687
  }
283
688
  /**
284
- * Returns the chain ID of the current chain.
285
- * Useful for runtime network validation.
689
+ * Load an ERC-721 NFT contract with fully typed methods — no ABI import needed.
690
+ *
691
+ * ```ts
692
+ * const nft = await awarizon.erc721("0x...")
693
+ * const owner = await nft.ownerOf(1n)
694
+ * const uri = await nft.tokenURI(1n)
695
+ * await nft.transferFrom(from, to, 1n)
696
+ * ```
286
697
  */
698
+ async erc721(address) {
699
+ const instance = await this.contract({ address, abi: ERC721_ABI });
700
+ return instance;
701
+ }
702
+ /**
703
+ * Load an ERC-1155 multi-token contract with fully typed methods — no ABI import needed.
704
+ *
705
+ * ```ts
706
+ * const items = await awarizon.erc1155("0x...")
707
+ * const balance = await items.balanceOf(userAddress, 42n)
708
+ * const uri = await items.uri(42n)
709
+ * await items.safeTransferFrom(from, to, 42n, 1n, "0x")
710
+ * ```
711
+ */
712
+ async erc1155(address) {
713
+ const instance = await this.contract({ address, abi: ERC1155_ABI });
714
+ return instance;
715
+ }
716
+ // ─── Named Contract Registry ────────────────────────────────────────────────
717
+ /**
718
+ * Register a contract under a human-readable name.
719
+ * After registering, use `awarizon.use(name)` anywhere instead of repeating
720
+ * address + ABI every time.
721
+ *
722
+ * ```ts
723
+ * awarizon.register("USDC", { address: "0x...", abi: erc20Abi })
724
+ * awarizon.register("Vault", { address: "0x...", abi: vaultAbi })
725
+ *
726
+ * const usdc = await awarizon.use("USDC")
727
+ * const vault = await awarizon.use("Vault")
728
+ * ```
729
+ */
730
+ register(name, config) {
731
+ this._registry.set(name, config);
732
+ return this;
733
+ }
734
+ /**
735
+ * Retrieve a previously registered contract by name.
736
+ * Throws a descriptive error if the name was never registered.
737
+ *
738
+ * ```ts
739
+ * const usdc = await awarizon.use("USDC")
740
+ * await usdc.transfer(recipient, 100n)
741
+ * ```
742
+ */
743
+ async use(name) {
744
+ const config = this._registry.get(name);
745
+ if (!config) {
746
+ const registered = [...this._registry.keys()].join(", ") || "none";
747
+ throw new Error(
748
+ `[awarizon/web3] Contract "${name}" is not registered.
749
+ Call awarizon.register("${name}", { address, abi }) first.
750
+ Currently registered: ${registered}`
751
+ );
752
+ }
753
+ return this.contract(config);
754
+ }
755
+ /**
756
+ * Remove a registered contract from the registry.
757
+ *
758
+ * ```ts
759
+ * awarizon.unregister("USDC")
760
+ * ```
761
+ */
762
+ unregister(name) {
763
+ this._registry.delete(name);
764
+ return this;
765
+ }
766
+ /**
767
+ * Return all registered contract names.
768
+ */
769
+ registeredContracts() {
770
+ return [...this._registry.keys()];
771
+ }
772
+ // ─── Accessors ──────────────────────────────────────────────────────────────
287
773
  get chainId() {
288
774
  return this.chain.id;
289
775
  }
776
+ get isConnected() {
777
+ return this._engine.isConnected();
778
+ }
290
779
  /**
291
- * Check whether any wallet (internal or external) is connected.
780
+ * Flush pending telemetry and stop background timers.
781
+ * Call when tearing down a long-lived SDK instance (e.g. on server shutdown).
292
782
  */
293
- get isConnected() {
294
- return this.wallet.isConnected();
783
+ async destroy() {
784
+ await this._telemetry.destroy();
295
785
  }
296
786
  };
297
787
 
@@ -315,6 +805,26 @@ Object.defineProperty(exports, "WalletNotConnectedError", {
315
805
  enumerable: true,
316
806
  get: function () { return walletEngine.WalletNotConnectedError; }
317
807
  });
808
+ Object.defineProperty(exports, "ContractExecutionError", {
809
+ enumerable: true,
810
+ get: function () { return txEngine.ContractExecutionError; }
811
+ });
812
+ Object.defineProperty(exports, "GasEstimationError", {
813
+ enumerable: true,
814
+ get: function () { return txEngine.GasEstimationError; }
815
+ });
816
+ Object.defineProperty(exports, "SimulationError", {
817
+ enumerable: true,
818
+ get: function () { return txEngine.SimulationError; }
819
+ });
820
+ Object.defineProperty(exports, "TransactionEngine", {
821
+ enumerable: true,
822
+ get: function () { return txEngine.TransactionEngine; }
823
+ });
824
+ Object.defineProperty(exports, "TransactionTimeoutError", {
825
+ enumerable: true,
826
+ get: function () { return txEngine.TransactionTimeoutError; }
827
+ });
318
828
  Object.defineProperty(exports, "DuplicateFunctionError", {
319
829
  enumerable: true,
320
830
  get: function () { return abiEngine.DuplicateFunctionError; }
@@ -347,31 +857,17 @@ Object.defineProperty(exports, "parseABI", {
347
857
  enumerable: true,
348
858
  get: function () { return abiEngine.parseABI; }
349
859
  });
350
- Object.defineProperty(exports, "ContractExecutionError", {
351
- enumerable: true,
352
- get: function () { return txEngine.ContractExecutionError; }
353
- });
354
- Object.defineProperty(exports, "GasEstimationError", {
355
- enumerable: true,
356
- get: function () { return txEngine.GasEstimationError; }
357
- });
358
- Object.defineProperty(exports, "SimulationError", {
359
- enumerable: true,
360
- get: function () { return txEngine.SimulationError; }
361
- });
362
- Object.defineProperty(exports, "TransactionEngine", {
363
- enumerable: true,
364
- get: function () { return txEngine.TransactionEngine; }
365
- });
366
- Object.defineProperty(exports, "TransactionTimeoutError", {
367
- enumerable: true,
368
- get: function () { return txEngine.TransactionTimeoutError; }
369
- });
860
+ exports.ApiKeyRequiredError = ApiKeyRequiredError;
370
861
  exports.AwarizonWeb3 = AwarizonWeb3;
371
862
  exports.CHAINS = CHAINS;
372
863
  exports.ContractNotLoadedError = ContractNotLoadedError;
864
+ exports.ERC1155_ABI = ERC1155_ABI;
865
+ exports.ERC20_ABI = ERC20_ABI;
866
+ exports.ERC721_ABI = ERC721_ABI;
867
+ exports.InvalidApiKeyError = InvalidApiKeyError;
373
868
  exports.NetworkMismatchError = NetworkMismatchError;
374
869
  exports.ProviderError = ProviderError;
870
+ exports.TelemetryClient = TelemetryClient;
375
871
  exports.UnsupportedChainError = UnsupportedChainError;
376
872
  exports.getSupportedChainIds = getSupportedChainIds;
377
873
  exports.resolveChain = resolveChain;