@asentum/sdk 0.1.0 → 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/dist/index.js CHANGED
@@ -1,444 +1,1471 @@
1
+ // src/internal/crypto.ts
2
+ import { ml_dsa65 } from "@noble/post-quantum/ml-dsa";
3
+ import { blake3 as nobleBlake3 } from "@noble/hashes/blake3";
4
+ var DILITHIUM3_SEED_BYTES = 32;
5
+ var DILITHIUM3_PUBLIC_KEY_BYTES = 1952;
6
+ var DILITHIUM3_SIGNATURE_BYTES = 3309;
7
+ var ADDRESS_BYTES = 20;
8
+ var ASE_HRP = "ase";
9
+ function generateKeypair(seed) {
10
+ let s = seed;
11
+ if (!s) {
12
+ s = new Uint8Array(DILITHIUM3_SEED_BYTES);
13
+ globalThis.crypto.getRandomValues(s);
14
+ }
15
+ if (s.length !== DILITHIUM3_SEED_BYTES) {
16
+ throw new Error(`Dilithium3 seed must be ${DILITHIUM3_SEED_BYTES} bytes, got ${s.length}`);
17
+ }
18
+ const keys = ml_dsa65.keygen(s);
19
+ return { publicKey: keys.publicKey, secretKey: keys.secretKey };
20
+ }
21
+ function sign(message, secretKey) {
22
+ return ml_dsa65.sign(secretKey, message);
23
+ }
24
+ function blake3(data, dkLen = 32) {
25
+ return nobleBlake3(data, { dkLen });
26
+ }
27
+ function addressBytesFromPublicKey(publicKey) {
28
+ return blake3(publicKey).slice(0, ADDRESS_BYTES);
29
+ }
30
+ var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
31
+ var GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
32
+ function polymod(values) {
33
+ let chk = 1;
34
+ for (const v of values) {
35
+ const top = chk >> 25;
36
+ chk = (chk & 33554431) << 5 ^ v;
37
+ for (let i = 0; i < 5; i++) if (top >> i & 1) chk ^= GENERATOR[i];
38
+ }
39
+ return chk;
40
+ }
41
+ function hrpExpand(hrp) {
42
+ const ret = [];
43
+ for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) >> 5);
44
+ ret.push(0);
45
+ for (let i = 0; i < hrp.length; i++) ret.push(hrp.charCodeAt(i) & 31);
46
+ return ret;
47
+ }
48
+ function convertBits(data, fromBits, toBits, pad) {
49
+ let acc = 0;
50
+ let bits = 0;
51
+ const ret = [];
52
+ const maxv = (1 << toBits) - 1;
53
+ const maxAcc = (1 << fromBits + toBits - 1) - 1;
54
+ for (let i = 0; i < data.length; i++) {
55
+ const value = data[i];
56
+ if (value < 0 || value >> fromBits !== 0) return null;
57
+ acc = (acc << fromBits | value) & maxAcc;
58
+ bits += fromBits;
59
+ while (bits >= toBits) {
60
+ bits -= toBits;
61
+ ret.push(acc >> bits & maxv);
62
+ }
63
+ }
64
+ if (pad) {
65
+ if (bits > 0) ret.push(acc << toBits - bits & maxv);
66
+ } else if (bits >= fromBits || (acc << toBits - bits & maxv) !== 0) {
67
+ return null;
68
+ }
69
+ return ret;
70
+ }
71
+ function bech32Encode(hrp, bytes) {
72
+ const data = convertBits(bytes, 8, 5, true);
73
+ if (!data) throw new Error("bech32: convertBits failed on encode");
74
+ const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
75
+ const mod = polymod(values) ^ 1;
76
+ let out = `${hrp}1`;
77
+ for (const d of data) out += CHARSET.charAt(d);
78
+ for (let p = 0; p < 6; p++) out += CHARSET.charAt(mod >> 5 * (5 - p) & 31);
79
+ return out;
80
+ }
81
+ function bech32Decode(input) {
82
+ if (typeof input !== "string" || input.length < 8 || input.length > 90) return null;
83
+ const lower = input.toLowerCase();
84
+ if (input !== lower && input !== input.toUpperCase()) return null;
85
+ const sep = lower.lastIndexOf("1");
86
+ if (sep < 1 || sep + 7 > lower.length) return null;
87
+ const hrp = lower.slice(0, sep);
88
+ const data = [];
89
+ for (const c of lower.slice(sep + 1)) {
90
+ const v = CHARSET.indexOf(c);
91
+ if (v < 0) return null;
92
+ data.push(v);
93
+ }
94
+ if (polymod(hrpExpand(hrp).concat(data)) !== 1) return null;
95
+ const payload = convertBits(data.slice(0, -6), 5, 8, false);
96
+ if (!payload) return null;
97
+ return { hrp, bytes: new Uint8Array(payload) };
98
+ }
99
+ function bytesToHex(bytes) {
100
+ let hex = "0x";
101
+ for (let i = 0; i < bytes.length; i++) hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
102
+ return hex;
103
+ }
104
+ function hexToBytes(hex) {
105
+ const h = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
106
+ if (h.length % 2 !== 0) throw new Error("invalid hex: odd number of characters");
107
+ if (!/^[0-9a-fA-F]*$/.test(h)) throw new Error("invalid hex: non-hex character");
108
+ const out = new Uint8Array(h.length / 2);
109
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
110
+ return out;
111
+ }
112
+
113
+ // src/ase1.ts
114
+ var HEX_ADDR = /^0x[0-9a-fA-F]{40}$/;
115
+ function ase1ToHexOrNull(s) {
116
+ if (typeof s !== "string") return null;
117
+ if (s.length < 4 || s.slice(0, 4).toLowerCase() !== `${ASE_HRP}1`) return null;
118
+ const decoded = bech32Decode(s);
119
+ if (!decoded || decoded.hrp !== ASE_HRP || decoded.bytes.length !== ADDRESS_BYTES) return null;
120
+ return bytesToHex(decoded.bytes);
121
+ }
122
+ function ase1ToHex(s) {
123
+ return ase1ToHexOrNull(s);
124
+ }
125
+ function isAse1(s) {
126
+ return typeof s === "string" && ase1ToHexOrNull(s) !== null;
127
+ }
128
+ function isHexAddress(s) {
129
+ return typeof s === "string" && HEX_ADDR.test(s);
130
+ }
131
+ function isAddress(s) {
132
+ return isAse1(s) || isHexAddress(s);
133
+ }
134
+ function hexToAse1(hex) {
135
+ if (!isHexAddress(hex)) return "";
136
+ return bech32Encode(ASE_HRP, addressToBytes(hex));
137
+ }
138
+ function toAse1(a) {
139
+ if (a === null || a === void 0) return "";
140
+ const s = String(a).trim();
141
+ if (isHexAddress(s)) return hexToAse1(s);
142
+ if (isAse1(s)) return s.toLowerCase();
143
+ return s;
144
+ }
145
+ function toHexAddress(a) {
146
+ return bytesToHex(addressToBytes(a));
147
+ }
148
+ function addressToBytes(a) {
149
+ if (typeof a !== "string" || a.trim() === "") throw new Error("invalid address: empty");
150
+ const s = a.trim();
151
+ if (isHexAddress(s)) {
152
+ const out = new Uint8Array(ADDRESS_BYTES);
153
+ for (let i = 0; i < ADDRESS_BYTES; i++) out[i] = parseInt(s.slice(2 + i * 2, 4 + i * 2), 16);
154
+ return out;
155
+ }
156
+ const hex = ase1ToHexOrNull(s);
157
+ if (hex) return addressToBytes(hex);
158
+ throw new Error(`invalid address: "${s.length > 14 ? s.slice(0, 14) + "..." : s}" (expected ase1... or 0x + 40 hex)`);
159
+ }
160
+ function bytesToAse1(bytes) {
161
+ if (bytes.length !== ADDRESS_BYTES) throw new Error(`address must be ${ADDRESS_BYTES} bytes, got ${bytes.length}`);
162
+ return bech32Encode(ASE_HRP, bytes);
163
+ }
164
+ function shortAse1(a, lead = 10, tail = 6) {
165
+ const s = toAse1(a);
166
+ return s.length > lead + tail ? `${s.slice(0, lead)}...${s.slice(-tail)}` : s;
167
+ }
168
+ function maybeAse1(v) {
169
+ return typeof v === "string" && isHexAddress(v) ? hexToAse1(v) : v;
170
+ }
171
+ function isPlainObject(v) {
172
+ if (v === null || typeof v !== "object") return false;
173
+ const proto = Object.getPrototypeOf(v);
174
+ return proto === Object.prototype || proto === null;
175
+ }
176
+ function ase1ArgsToHex(value) {
177
+ if (typeof value === "string") {
178
+ return ase1ToHexOrNull(value) ?? value;
179
+ }
180
+ if (Array.isArray(value)) {
181
+ return value.map((v) => ase1ArgsToHex(v));
182
+ }
183
+ if (isPlainObject(value)) {
184
+ const out = {};
185
+ for (const k of Object.keys(value)) out[k] = ase1ArgsToHex(value[k]);
186
+ return out;
187
+ }
188
+ return value;
189
+ }
190
+
191
+ // src/errors.ts
192
+ var AsentumError = class extends Error {
193
+ code;
194
+ constructor(code, message, options) {
195
+ super(message);
196
+ this.name = "AsentumError";
197
+ this.code = code;
198
+ if (options && "cause" in options) this.cause = options.cause;
199
+ }
200
+ };
201
+ var HttpError = class extends AsentumError {
202
+ status;
203
+ url;
204
+ /** Parsed JSON body when there was one, otherwise the text (the gateway sends HTML for 429). */
205
+ body;
206
+ constructor(status, url, body, message, code = "HTTP_ERROR") {
207
+ const detail = body && typeof body === "object" && "error" in body ? `: ${String(body.error)}` : "";
208
+ super(code, message ?? `HTTP ${status} from ${url}${detail}`);
209
+ this.name = "HttpError";
210
+ this.status = status;
211
+ this.url = url;
212
+ this.body = body;
213
+ }
214
+ };
215
+ var RateLimitError = class extends HttpError {
216
+ /** From the Retry-After header, when the server sent one. */
217
+ retryAfterMs;
218
+ constructor(url, body, retryAfterMs) {
219
+ super(429, url, body, `rate limited (HTTP 429) by ${url}${retryAfterMs ? `, retry after ${retryAfterMs} ms` : ""}`, "RATE_LIMITED");
220
+ this.name = "RateLimitError";
221
+ this.retryAfterMs = retryAfterMs;
222
+ }
223
+ };
224
+ var NetworkError = class extends AsentumError {
225
+ url;
226
+ constructor(url, cause) {
227
+ super("NETWORK_ERROR", `network error calling ${url}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
228
+ this.name = "NetworkError";
229
+ this.url = url;
230
+ }
231
+ };
232
+ var RpcError = class extends AsentumError {
233
+ path;
234
+ body;
235
+ constructor(path, message, body) {
236
+ super("RPC_ERROR", `${path}: ${message}`);
237
+ this.name = "RpcError";
238
+ this.path = path;
239
+ this.body = body;
240
+ }
241
+ };
242
+ var JsonRpcError = class extends AsentumError {
243
+ rpcCode;
244
+ method;
245
+ data;
246
+ constructor(method, rpcCode, message, data) {
247
+ super("JSON_RPC_ERROR", `JSON-RPC ${method} failed (${rpcCode}): ${message}`);
248
+ this.name = "JsonRpcError";
249
+ this.method = method;
250
+ this.rpcCode = rpcCode;
251
+ this.data = data;
252
+ }
253
+ };
254
+ var TxRejectedError = class extends AsentumError {
255
+ reason;
256
+ /** The locally computed hash of the rejected transaction (it is not on chain). */
257
+ txHash;
258
+ constructor(reason, txHash) {
259
+ super("TX_REJECTED", `transaction rejected: ${reason || "unknown reason"}`);
260
+ this.name = "TxRejectedError";
261
+ this.reason = reason || "unknown reason";
262
+ this.txHash = txHash;
263
+ }
264
+ /** True when the reason is about the nonce (stale, too far ahead, duplicate nonce). */
265
+ get isNonceError() {
266
+ return /nonce/i.test(this.reason);
267
+ }
268
+ };
269
+ var TxFailedError = class extends AsentumError {
270
+ receipt;
271
+ constructor(receipt) {
272
+ super("TX_FAILED", receipt.error ? `transaction failed on chain: ${receipt.error}` : "transaction failed on chain");
273
+ this.name = "TxFailedError";
274
+ this.receipt = receipt;
275
+ }
276
+ };
277
+ var TimeoutError = class extends AsentumError {
278
+ txHash;
279
+ lastReceipt;
280
+ constructor(txHash, timeoutMs, until, lastReceipt) {
281
+ super(
282
+ "TIMEOUT",
283
+ lastReceipt ? `transaction ${txHash} is in block ${lastReceipt.blockNumber} but was not final within ${timeoutMs} ms` : `transaction ${txHash} not ${until === "final" ? "final" : "in a block"} within ${timeoutMs} ms; it may still go through, check it on the explorer before sending again`
284
+ );
285
+ this.name = "TimeoutError";
286
+ this.txHash = txHash;
287
+ this.lastReceipt = lastReceipt;
288
+ }
289
+ };
290
+ var AbortError = class extends AsentumError {
291
+ constructor(message = "aborted") {
292
+ super("ABORTED", message);
293
+ this.name = "AbortError";
294
+ }
295
+ };
296
+ var ViewError = class extends AsentumError {
297
+ method;
298
+ reason;
299
+ constructor(method, reason) {
300
+ super("VIEW_FAILED", `view call ${method}() failed: ${reason || "unknown"}`);
301
+ this.name = "ViewError";
302
+ this.method = method;
303
+ this.reason = reason || "unknown";
304
+ }
305
+ };
306
+
307
+ // src/http.ts
308
+ var IN_BROWSER = typeof globalThis.document !== "undefined";
309
+ function sleep(ms, signal) {
310
+ return new Promise((resolve, reject) => {
311
+ if (signal?.aborted) return reject(new AbortError());
312
+ const t = setTimeout(() => {
313
+ signal?.removeEventListener("abort", onAbort);
314
+ resolve();
315
+ }, ms);
316
+ const onAbort = () => {
317
+ clearTimeout(t);
318
+ reject(new AbortError());
319
+ };
320
+ signal?.addEventListener("abort", onAbort, { once: true });
321
+ });
322
+ }
323
+ function parseRetryAfter(h) {
324
+ if (!h) return void 0;
325
+ const secs = Number(h);
326
+ if (Number.isFinite(secs) && secs >= 0) return secs * 1e3;
327
+ const date = Date.parse(h);
328
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
329
+ return void 0;
330
+ }
331
+ var Transport = class {
332
+ baseUrl;
333
+ fetchImpl;
334
+ retries;
335
+ timeoutMs;
336
+ baseMs;
337
+ maxMs;
338
+ headers;
339
+ constructor(baseUrl, opts = {}) {
340
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
341
+ this.fetchImpl = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
342
+ this.retries = opts.retries ?? 3;
343
+ this.timeoutMs = opts.requestTimeoutMs ?? 3e4;
344
+ this.baseMs = opts.backoffBaseMs ?? 500;
345
+ this.maxMs = opts.backoffMaxMs ?? 1e4;
346
+ this.headers = opts.headers ?? {};
347
+ }
348
+ backoffDelay(attempt, retryAfterMs) {
349
+ if (retryAfterMs !== void 0) return Math.min(this.maxMs, Math.max(retryAfterMs, 0));
350
+ const exp = Math.min(this.maxMs, this.baseMs * 2 ** attempt);
351
+ return Math.round(exp * (0.75 + Math.random() * 0.5));
352
+ }
353
+ async request(path, opts = {}) {
354
+ const url = `${this.baseUrl}${path}`;
355
+ const method = opts.method ?? (opts.body === void 0 ? "GET" : "POST");
356
+ const retries = opts.retries ?? this.retries;
357
+ const retryOnServerError = opts.retryOnServerError ?? true;
358
+ for (let attempt = 0; ; attempt++) {
359
+ if (opts.signal?.aborted) throw new AbortError();
360
+ let res;
361
+ const ctrl = new AbortController();
362
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
363
+ const onAbort = () => ctrl.abort();
364
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
365
+ try {
366
+ const headers = { accept: "application/json", ...this.headers };
367
+ if (opts.body !== void 0) headers["content-type"] = "application/json";
368
+ const init = {
369
+ method,
370
+ headers,
371
+ body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body),
372
+ signal: ctrl.signal
373
+ };
374
+ if (IN_BROWSER) init.cache = "no-store";
375
+ res = await this.fetchImpl(url, init);
376
+ } catch (err) {
377
+ clearTimeout(timer);
378
+ opts.signal?.removeEventListener("abort", onAbort);
379
+ if (opts.signal?.aborted) throw new AbortError();
380
+ if (retryOnServerError && attempt < retries) {
381
+ await sleep(this.backoffDelay(attempt), opts.signal);
382
+ continue;
383
+ }
384
+ throw new NetworkError(url, err);
385
+ }
386
+ clearTimeout(timer);
387
+ opts.signal?.removeEventListener("abort", onAbort);
388
+ const body = await readBody(res);
389
+ if (res.ok || opts.allowStatus?.includes(res.status)) {
390
+ return { status: res.status, body, headers: res.headers };
391
+ }
392
+ const retryAfterMs = parseRetryAfter(res.headers?.get?.("retry-after") ?? null);
393
+ const serverHiccup = res.status === 502 || res.status === 503 || res.status === 504 || res.status === 500 && typeof body === "string";
394
+ const retryable = res.status === 429 || retryOnServerError && serverHiccup;
395
+ if (retryable && attempt < retries) {
396
+ await sleep(this.backoffDelay(attempt, retryAfterMs), opts.signal);
397
+ continue;
398
+ }
399
+ if (res.status === 429) throw new RateLimitError(url, body, retryAfterMs);
400
+ throw new HttpError(res.status, url, body);
401
+ }
402
+ }
403
+ };
404
+ async function readBody(res) {
405
+ let text;
406
+ try {
407
+ text = await res.text();
408
+ } catch {
409
+ return null;
410
+ }
411
+ if (text === "") return null;
412
+ try {
413
+ return JSON.parse(text);
414
+ } catch {
415
+ return text;
416
+ }
417
+ }
418
+
1
419
  // src/client.ts
420
+ var DEFAULT_RPC = "https://testnet.asentum.com";
421
+ var GOVERNANCE_CONTRACT = hexToAse1("0x0000000000000000000000000000000000000003");
422
+ var MAX_LOG_RANGE = 1e4;
423
+ function asRecord(v) {
424
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
425
+ }
426
+ function errorField(body) {
427
+ const r = asRecord(body);
428
+ if (!r || r.error === void 0 || r.error === null || r.error === "") return null;
429
+ const msg = r.message ? `${String(r.error)} (${String(r.message)})` : String(r.error);
430
+ return msg;
431
+ }
432
+ function normReceipt(r) {
433
+ return { ...r, sender: maybeAse1(r.sender), recipient: maybeAse1(r.recipient) };
434
+ }
435
+ function normTx(t) {
436
+ const out = { ...t, body: { ...t.body, to: maybeAse1(t.body?.to) } };
437
+ if (t.sender) out.sender = maybeAse1(t.sender);
438
+ return out;
439
+ }
440
+ function normBlock(b) {
441
+ return {
442
+ ...b,
443
+ header: { ...b.header, proposer: maybeAse1(b.header.proposer) },
444
+ transactions: (b.transactions ?? []).map(normTx),
445
+ receipts: (b.receipts ?? []).map(normReceipt)
446
+ };
447
+ }
448
+ function toBlockParam(v) {
449
+ if (v === void 0) return void 0;
450
+ if (typeof v === "number" || typeof v === "bigint") return "0x" + BigInt(v).toString(16);
451
+ if (/^\d+$/.test(v)) return "0x" + BigInt(v).toString(16);
452
+ return v;
453
+ }
454
+ function hexToUtf8(hex) {
455
+ return new TextDecoder().decode(hexToBytes(hex));
456
+ }
457
+ function eventTopic(name) {
458
+ return bytesToHex(blake3(new TextEncoder().encode(name)));
459
+ }
460
+ function hidden() {
461
+ const d = globalThis.document;
462
+ return !!d && d.visibilityState === "hidden";
463
+ }
2
464
  var AsentumClient = class {
3
465
  rpcUrl;
4
- constructor(rpcUrl) {
5
- this.rpcUrl = rpcUrl.replace(/\/$/, "");
466
+ /** @internal */
467
+ transport;
468
+ chainIdCache;
469
+ constructor(rpcUrl = DEFAULT_RPC, options = {}) {
470
+ this.rpcUrl = rpcUrl.replace(/\/+$/, "");
471
+ this.transport = new Transport(this.rpcUrl, options);
6
472
  }
7
473
  // ---------------------------------------------------------------------------
8
- // Chain reads
474
+ // Chain and health
9
475
  // ---------------------------------------------------------------------------
10
- /** Get chain info including the latest block header. */
11
- async getChainInfo() {
12
- const raw = await this.get("/chain");
13
- return { ...raw, height: Number(BigInt(raw.height)) };
14
- }
15
- /** Get the current block height. */
16
- async getBlockNumber() {
17
- const info = await this.getChainInfo();
18
- return info.height;
19
- }
20
- /** Get the chain ID as a bigint. */
21
- async getChainId() {
22
- const info = await this.getChainInfo();
23
- return BigInt(info.chainId);
24
- }
25
- /** Get a block by number, including transactions and receipts. */
26
- async getBlock(number) {
27
- try {
28
- return await this.get(`/block/${number}`);
29
- } catch {
30
- return null;
31
- }
476
+ /** Finalized height and tip in one small call (GET /v3/heads). */
477
+ async getHeads(opts = {}) {
478
+ const r = await this.get("/v3/heads", opts);
479
+ return { finalized: Number(r.finalized), tip: Number(r.tip) };
32
480
  }
33
- /** Get the balance and nonce for an address. */
34
- async getBalance(address) {
35
- return this.get(`/balance/${address}`);
481
+ /** Newest block height on the best chain. */
482
+ async getTipNumber(opts = {}) {
483
+ return (await this.getHeads(opts)).tip;
36
484
  }
37
- /** Get the nonce for an address (shorthand for getBalance().nonce). */
38
- async getNonce(address) {
39
- const bal = await this.getBalance(address);
40
- return BigInt(bal.nonce);
485
+ /** Finalized height (same as eth_blockNumber and getBlockNumber). */
486
+ async getFinalizedNumber(opts = {}) {
487
+ return (await this.getHeads(opts)).finalized;
41
488
  }
42
- /** Get the receipt for a transaction by hash. */
43
- async getReceipt(txHash) {
44
- try {
45
- const r = await this.get(`/receipts/${txHash}`);
46
- if (r && typeof r === "object" && "error" in r) return null;
47
- return r;
48
- } catch {
49
- return null;
50
- }
489
+ /** Chain id, finalized height, this node's mempool size and the finalized header (GET /chain, cached 1 s on the gateway). */
490
+ async getChainInfo(opts = {}) {
491
+ const raw = await this.get("/chain", opts);
492
+ return {
493
+ ...raw,
494
+ height: Number(raw.height),
495
+ latestHeader: { ...raw.latestHeader, proposer: maybeAse1(raw.latestHeader?.proposer) }
496
+ };
51
497
  }
52
- /** Get the source code of a deployed contract (hex-encoded). */
53
- async getCode(address) {
54
- const res = await this.jsonRpc("eth_getCode", [address, "latest"]);
55
- return res;
56
- }
57
- /** Get the source code of a deployed contract as a UTF-8 string. */
58
- async getContractSource(address) {
59
- const hex = await this.getCode(address);
60
- if (!hex || hex === "0x" || hex.length <= 2) return null;
61
- const raw = hex.startsWith("0x") ? hex.slice(2) : hex;
62
- const bytes = new Uint8Array(raw.length / 2);
63
- for (let i = 0; i < bytes.length; i++) {
64
- bytes[i] = parseInt(raw.substr(i * 2, 2), 16);
498
+ /** The FINALIZED height. Use getTipNumber() for the newest block. */
499
+ async getBlockNumber(opts = {}) {
500
+ return (await this.getChainInfo(opts)).height;
501
+ }
502
+ /** Chain id (1423 on the public testnet). Cached after the first call. */
503
+ async getChainId(opts = {}) {
504
+ if (this.chainIdCache === void 0) {
505
+ this.chainIdCache = BigInt((await this.getChainInfo(opts)).chainId);
65
506
  }
66
- return new TextDecoder().decode(bytes);
507
+ return this.chainIdCache;
67
508
  }
68
- /** Call a contract view function (read-only, no transaction). */
69
- async viewCall(contract, method, args = []) {
70
- return this.post("/view", { contract, method, args });
509
+ /** Chain details in EIP-3085 shape (GET /metadata, cached 30 s on the gateway). */
510
+ async getMetadata(opts = {}) {
511
+ return this.get("/metadata", opts);
71
512
  }
72
- /** Get the current active validator set. */
73
- async getValidators() {
74
- return this.get("/validators");
513
+ /** Node identity (GET /api). `version` is a fixed placeholder, not the node release. */
514
+ async getApiInfo(opts = {}) {
515
+ const r = await this.get("/api", opts);
516
+ return { ...r, faucetAddress: r.faucetAddress ? maybeAse1(r.faucetAddress) : r.faucetAddress };
75
517
  }
76
- /** Get the chain metadata (EIP-3085 shape). */
77
- async getMetadata() {
78
- return this.get("/metadata");
518
+ /** The genesis spec (GET /genesis, about 17 KB). Address fields converted to ase1. */
519
+ async getGenesis(opts = {}) {
520
+ const r = await this.getChecked("/genesis", opts);
521
+ const fix = (list) => Array.isArray(list) ? list.map((e) => {
522
+ const o = asRecord(e);
523
+ if (!o) return e;
524
+ const c = { ...o };
525
+ for (const k of Object.keys(c)) if (/address$/i.test(k)) c[k] = maybeAse1(c[k]);
526
+ return c;
527
+ }) : list;
528
+ return { ...r, initialAccounts: fix(r.initialAccounts), initialValidators: fix(r.initialValidators), initialAseHolders: fix(r.initialAseHolders) };
79
529
  }
80
- /** Health check. */
81
- async isHealthy() {
530
+ /** Liveness probe (GET /health). True as long as the HTTP server answers. Does not check sync. */
531
+ async isHealthy(opts = {}) {
82
532
  try {
83
- const res = await this.get("/health");
84
- return res.status === "ok";
85
- } catch {
533
+ const res = await this.get("/health", { ...opts, retries: opts.retries ?? 0 });
534
+ return res?.status === "ok";
535
+ } catch (err) {
536
+ if (err instanceof AbortError) throw err;
86
537
  return false;
87
538
  }
88
539
  }
89
- /** Readiness check (chain advancing). */
90
- async isReady() {
540
+ /**
541
+ * Readiness probe (GET /ready): true on 200 "ready", false on 503 "stale"
542
+ * or no answer. Caveat: node 0.8.1 compares seconds with milliseconds
543
+ * here, so it reports ready even on a stalled node. To detect a stall,
544
+ * check that getHeads() keeps moving.
545
+ */
546
+ async isReady(opts = {}) {
91
547
  try {
92
- await fetch(`${this.rpcUrl}/ready`);
93
- return true;
94
- } catch {
548
+ const r = await this.transport.request("/ready", {
549
+ signal: opts.signal,
550
+ retries: opts.retries ?? 0,
551
+ allowStatus: [503]
552
+ });
553
+ return r.status === 200 && r.body?.status === "ready";
554
+ } catch (err) {
555
+ if (err instanceof AbortError) throw err;
95
556
  return false;
96
557
  }
97
558
  }
98
- /** Request test ASE from the faucet. */
99
- async faucet(to, amount = "100000000000000000000") {
100
- return this.post("/dev/faucet", { to, amount });
559
+ // ---------------------------------------------------------------------------
560
+ // Accounts
561
+ // ---------------------------------------------------------------------------
562
+ /** Balance (wei), nonce and pendingNonce at the tip (GET /balance). */
563
+ async getBalance(address, opts = {}) {
564
+ const r = await this.getChecked(`/balance/${encodeURIComponent(address)}`, opts);
565
+ return { ...r, address: maybeAse1(r.address) };
566
+ }
567
+ /**
568
+ * The nonce to SEND with. Default: the node's mempool-aware pendingNonce,
569
+ * so several sends in a row get n, n+1, n+2. `{ pending: false }` gives
570
+ * the account nonce at the tip.
571
+ */
572
+ async getNonce(address, opts = {}) {
573
+ const bal = await this.getBalance(address, opts);
574
+ return BigInt(opts.pending === false ? bal.nonce : bal.pendingNonce ?? bal.nonce);
101
575
  }
102
- /** Submit a raw signed transaction (hex-encoded SSZ bytes). */
103
- async submitRawTx(rawTxHex) {
104
- return this.post("/tx", { rawTx: rawTxHex });
576
+ /** Full account record: balance, nonce, codeHash, storageRoot (GET /asentum/account). */
577
+ async getAccount(address, opts = {}) {
578
+ const r = await this.getChecked(`/asentum/account/${encodeURIComponent(address)}`, opts);
579
+ return { ...r, address: maybeAse1(r.address) };
580
+ }
581
+ /** Whether the chain has this account's public key on record, so it may send key-less transactions (GET /public-key). */
582
+ async getPublicKeyStatus(address, opts = {}) {
583
+ const r = await this.getChecked(`/public-key/${encodeURIComponent(address)}`, opts);
584
+ return { ...r, address: maybeAse1(r.address) };
105
585
  }
106
586
  // ---------------------------------------------------------------------------
107
- // Ethereum JSON-RPC (subset)
587
+ // Blocks
108
588
  // ---------------------------------------------------------------------------
109
- /** Send a raw JSON-RPC request. */
110
- async jsonRpc(method, params = []) {
111
- const res = await fetch(`${this.rpcUrl}/`, {
589
+ /**
590
+ * A block with decoded transactions and receipts, or null when this node
591
+ * does not have it. Finalized blocks only, unless `{ tip: true }`.
592
+ * (0.1.x returned the node's error object here instead of null.)
593
+ */
594
+ async getBlock(number, opts = {}) {
595
+ const q = opts.tip ? "?tip=1" : "";
596
+ const r = await this.get(`/block/${BigInt(number)}${q}`, opts);
597
+ const err = errorField(r);
598
+ if (err) {
599
+ if (/not found/i.test(err)) return null;
600
+ throw new RpcError(`/block/${number}`, err, r);
601
+ }
602
+ return normBlock(r);
603
+ }
604
+ /** Only the receipts of a block (GET /block/<n>/receipts). Check `available` before trusting an empty list. */
605
+ async getBlockReceipts(number, opts = {}) {
606
+ const q = opts.tip ? "?tip=1" : "";
607
+ const r = await this.getChecked(`/block/${BigInt(number)}/receipts${q}`, opts);
608
+ return { ...r, receipts: (r.receipts ?? []).map(normReceipt), available: r.finalized !== void 0 };
609
+ }
610
+ /** The block as SSZ bytes (0x hex), or null. Note: also serves pending heights without saying so. */
611
+ async getRawBlock(number, opts = {}) {
612
+ const r = await this.get(`/block-raw/${BigInt(number)}`, opts);
613
+ if (errorField(r)) return null;
614
+ return r.bytes ?? null;
615
+ }
616
+ /** Whether a height is finalized on this node, and the Aura hash finalized there (GET /v3/finalized). */
617
+ async getFinalizedStatus(height, opts = {}) {
618
+ return this.getChecked(`/v3/finalized?height=${BigInt(height)}`, opts);
619
+ }
620
+ // ---------------------------------------------------------------------------
621
+ // Transactions and receipts
622
+ // ---------------------------------------------------------------------------
623
+ /**
624
+ * The receipt of a transaction, or null when it is not in a block (yet).
625
+ * Tip receipts are included by default (finalized: false); see
626
+ * `finalizedOnly`. Check `success`, not `status`.
627
+ *
628
+ * Throws RpcError for a malformed hash and HttpError / NetworkError after
629
+ * retries (0.1.x returned null for every error).
630
+ */
631
+ async getReceipt(txHash, opts = {}) {
632
+ const q = opts.finalizedOnly ? "?tip=0" : "";
633
+ const r = await this.get(`/receipts/${encodeURIComponent(txHash)}${q}`, opts);
634
+ const rec = asRecord(r);
635
+ if (rec && rec.blockNumber !== void 0 && rec.blockNumber !== null) return normReceipt(r);
636
+ const err = errorField(r);
637
+ if (!err || /receipt not found/i.test(err)) return null;
638
+ throw new RpcError(`/receipts/${txHash}`, err, r);
639
+ }
640
+ /** The transaction body from eth_getTransactionByHash (in-block transactions only, tip too), or null. */
641
+ async getTransaction(txHash, opts = {}) {
642
+ const t = await this.jsonRpc("eth_getTransactionByHash", [txHash], opts);
643
+ if (!t) return null;
644
+ return { ...t, from: toAse1(t.from), to: t.to ? toAse1(t.to) : null };
645
+ }
646
+ /**
647
+ * Submit one signed transaction (POST /tx). Does not throw on rejection:
648
+ * check `accepted`. `accepted: true` only means "in this node's mempool".
649
+ */
650
+ async submitRawTx(rawTxHex, opts = {}) {
651
+ const r = await this.transport.request("/tx", {
112
652
  method: "POST",
113
- headers: { "Content-Type": "application/json" },
114
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
653
+ body: { rawTx: rawTxHex },
654
+ signal: opts.signal,
655
+ retries: opts.retries,
656
+ retryOnServerError: false
115
657
  });
116
- if (!res.ok) throw new Error(`JSON-RPC ${method} failed: ${res.status}`);
117
- const body = await res.json();
118
- if (body.error) throw new Error(`JSON-RPC error: ${body.error.message}`);
119
- return body.result;
658
+ return r.body;
659
+ }
660
+ /**
661
+ * Submit several signed transactions in one request (POST /tx/batch).
662
+ * They are admitted in order; one rejection does not stop the rest.
663
+ */
664
+ async submitRawTxBatch(rawTxs, opts = {}) {
665
+ if (rawTxs.length === 0) return { accepted: 0, rejected: 0, total: 0, results: [] };
666
+ const r = await this.transport.request("/tx/batch", {
667
+ method: "POST",
668
+ body: { transactions: rawTxs.map((rawTx) => ({ rawTx })) },
669
+ signal: opts.signal,
670
+ retries: opts.retries,
671
+ retryOnServerError: false
672
+ });
673
+ return r.body;
674
+ }
675
+ /**
676
+ * Wait for a transaction's receipt. Polls GET /receipts about once a
677
+ * second, backs off on 429 / 5xx, and follows the receipt through a
678
+ * reorg (onReorg, then onIncluded again for the new block).
679
+ *
680
+ * Resolves with the receipt (until: "included", the default) or with the
681
+ * final receipt (until: "final"). Rejects with TimeoutError (not a
682
+ * failure: the tx may still land), AbortError, or TxFailedError when
683
+ * throwOnFailure is set.
684
+ */
685
+ async waitForReceipt(txHash, opts = {}) {
686
+ const until = opts.until ?? "included";
687
+ const timeoutMs = opts.timeoutMs ?? (until === "final" ? 12e4 : 6e4);
688
+ const interval = opts.pollIntervalMs ?? 1e3;
689
+ const pause = opts.pauseWhenHidden ?? true;
690
+ const deadline = Date.now() + timeoutMs;
691
+ let seen = null;
692
+ let delay = interval;
693
+ while (Date.now() < deadline) {
694
+ if (opts.signal?.aborted) throw new AbortError();
695
+ if (pause && hidden()) {
696
+ await sleep(Math.min(1e3, Math.max(0, deadline - Date.now())), opts.signal);
697
+ continue;
698
+ }
699
+ let r = null;
700
+ let polled = false;
701
+ try {
702
+ r = await this.getReceipt(txHash, { signal: opts.signal, retries: 0 });
703
+ polled = true;
704
+ delay = interval;
705
+ } catch (err) {
706
+ if (err instanceof AbortError || err instanceof RpcError) throw err;
707
+ if (err instanceof RateLimitError) {
708
+ delay = Math.min(1e4, err.retryAfterMs ?? delay * 2);
709
+ } else if (err instanceof NetworkError || err instanceof HttpError && err.status >= 500) {
710
+ delay = Math.min(1e4, delay * 2);
711
+ } else {
712
+ throw err;
713
+ }
714
+ }
715
+ if (polled) {
716
+ if (r) {
717
+ if (seen && seen.blockHash && r.blockHash && seen.blockHash !== r.blockHash) {
718
+ opts.onReorg?.(seen);
719
+ seen = null;
720
+ }
721
+ if (!seen) {
722
+ seen = r;
723
+ opts.onIncluded?.(r);
724
+ } else {
725
+ seen = r;
726
+ }
727
+ const isFinal = r.finalized !== false;
728
+ if (until === "included" || isFinal) {
729
+ if (r.success === false && opts.throwOnFailure) throw new TxFailedError(r);
730
+ if (until === "final") opts.onFinal?.(r);
731
+ return r;
732
+ }
733
+ } else if (seen) {
734
+ opts.onReorg?.(seen);
735
+ seen = null;
736
+ }
737
+ }
738
+ const left = deadline - Date.now();
739
+ if (left <= 0) break;
740
+ await sleep(Math.min(delay, left), opts.signal);
741
+ }
742
+ throw new TimeoutError(txHash, timeoutMs, until, seen);
120
743
  }
121
744
  // ---------------------------------------------------------------------------
122
- // Internal HTTP helpers
745
+ // Contracts
746
+ // ---------------------------------------------------------------------------
747
+ /**
748
+ * Run a contract method read-only (POST /view). ase1 addresses inside
749
+ * `args` are converted to 0x hex first (the node does not convert them,
750
+ * and contracts store accounts as hex). Reads the tip unless
751
+ * `{ blockTag: 'finalized' }`. Does not throw on ok: false; see view().
752
+ */
753
+ async viewCall(contract, method, args = [], opts = {}) {
754
+ const body = { contract, method, args: ase1ArgsToHex(args) };
755
+ if (opts.blockTag) body.blockTag = opts.blockTag;
756
+ const r = await this.transport.request("/view", {
757
+ method: "POST",
758
+ body,
759
+ signal: opts.signal,
760
+ retries: opts.retries
761
+ });
762
+ return r.body;
763
+ }
764
+ /** Like viewCall, but returns the method's return value and throws ViewError on ok: false. */
765
+ async view(contract, method, args = [], opts = {}) {
766
+ const r = await this.viewCall(contract, method, args, opts);
767
+ if (!r.ok) throw new ViewError(method, r.reason ?? "unknown");
768
+ return r.returnValue;
769
+ }
770
+ /** A contract's source, hex-encoded UTF-8 (eth_getCode). "0x" for an account without code. */
771
+ async getCode(address, opts = {}) {
772
+ return this.jsonRpc("eth_getCode", [address, "latest"], opts);
773
+ }
774
+ /** A contract's JavaScript source as text, or null when the address has no code. */
775
+ async getContractSource(address, opts = {}) {
776
+ const hex = await this.getCode(address, opts);
777
+ if (!hex || hex === "0x") return null;
778
+ return hexToUtf8(hex);
779
+ }
780
+ /** A contract storage value by key (a plain string such as "self"), as text, or null when empty (eth_getStorageAt). */
781
+ async getStorageAt(address, key, opts = {}) {
782
+ const hex = await this.jsonRpc("eth_getStorageAt", [address, key, "latest"], opts);
783
+ if (!hex || hex === "0x") return null;
784
+ return hexToUtf8(hex);
785
+ }
786
+ /**
787
+ * Contract events from eth_getLogs, decoded. Finalized blocks only. A
788
+ * range wider than 10,000 blocks is split into several calls.
789
+ *
790
+ * await client.getLogs({ address: 'ase1...', event: 'Transfer', fromBlock: 570000, toBlock: 571000 })
791
+ */
792
+ async getLogs(filter = {}, opts = {}) {
793
+ const topics = filter.event ? [eventTopic(filter.event)] : filter.topics;
794
+ const address = filter.address;
795
+ const from = filter.fromBlock;
796
+ const to = filter.toBlock;
797
+ const numeric = (v) => typeof v === "number" || typeof v === "bigint" || typeof v === "string" && /^\d+$/.test(v);
798
+ const ranges = [];
799
+ if (numeric(from) && numeric(to)) {
800
+ let a = BigInt(from);
801
+ const b = BigInt(to);
802
+ if (a > b) return [];
803
+ while (a <= b) {
804
+ const end = a + BigInt(MAX_LOG_RANGE) - 1n < b ? a + BigInt(MAX_LOG_RANGE) - 1n : b;
805
+ ranges.push([toBlockParam(a), toBlockParam(end)]);
806
+ a = end + 1n;
807
+ }
808
+ } else {
809
+ ranges.push([toBlockParam(from), toBlockParam(to)]);
810
+ }
811
+ const out = [];
812
+ for (const [fromBlock, toBlock] of ranges) {
813
+ const params = {};
814
+ if (fromBlock !== void 0) params.fromBlock = fromBlock;
815
+ if (toBlock !== void 0) params.toBlock = toBlock;
816
+ if (address !== void 0) params.address = address;
817
+ if (topics !== void 0) params.topics = topics;
818
+ const logs = await this.jsonRpc("eth_getLogs", [params], opts);
819
+ for (const l of logs ?? []) {
820
+ const raw = String(l.data ?? "0x");
821
+ let data = null;
822
+ if (raw !== "0x") {
823
+ const text = hexToUtf8(raw);
824
+ try {
825
+ data = JSON.parse(text);
826
+ } catch {
827
+ data = text;
828
+ }
829
+ }
830
+ const ts = l.topics ?? [];
831
+ out.push({
832
+ address: toAse1(String(l.address)),
833
+ topic: ts[0] ?? "",
834
+ topics: ts,
835
+ ...filter.event ? { eventName: filter.event } : {},
836
+ data,
837
+ rawData: raw,
838
+ blockNumber: Number(l.blockNumber),
839
+ blockHash: String(l.blockHash),
840
+ transactionHash: String(l.transactionHash),
841
+ transactionIndex: Number(l.transactionIndex),
842
+ logIndex: Number(l.logIndex),
843
+ removed: Boolean(l.removed)
844
+ });
845
+ }
846
+ }
847
+ return out;
848
+ }
849
+ // ---------------------------------------------------------------------------
850
+ // Validators and consensus
851
+ // ---------------------------------------------------------------------------
852
+ /** Every bonded validator with stake and committee state, plus the epoch (GET /validators, about 260 KB). */
853
+ async getValidators(opts = {}) {
854
+ const r = await this.getChecked("/validators", opts);
855
+ return {
856
+ ...r,
857
+ stakingContract: r.stakingContract ? maybeAse1(r.stakingContract) : r.stakingContract,
858
+ validators: (r.validators ?? []).map((v) => ({ ...v, address: maybeAse1(v.address) }))
859
+ };
860
+ }
861
+ /** Who signed recent blocks (GET /participation). lookback 1 to 10,000, default 1,000. */
862
+ async getParticipation(opts = {}) {
863
+ const q = opts.lookback !== void 0 ? `?lookback=${opts.lookback}` : "";
864
+ const r = await this.getChecked(`/participation${q}`, opts);
865
+ return { ...r, validators: (r.validators ?? []).map((v) => ({ ...v, address: maybeAse1(v.address) })) };
866
+ }
867
+ /** The voting committee of an epoch (default: current), ase1, in canonical order (GET /v3/committee). */
868
+ async getCommittee(opts = {}) {
869
+ const q = opts.epoch !== void 0 ? `?epoch=${BigInt(opts.epoch)}` : "";
870
+ const r = await this.getChecked(`/v3/committee${q}`, { ...opts, allowStatus: [404] });
871
+ return { ...r, committee: (r.committee ?? []).map(maybeAse1), leaderBase: (r.leaderBase ?? []).map(maybeAse1) };
872
+ }
873
+ /** The node's liveness tracker (GET /consensus/liveness, about 40 KB). */
874
+ async getLiveness(opts = {}) {
875
+ const r = await this.getChecked("/consensus/liveness", opts);
876
+ return {
877
+ ...r,
878
+ baked: (r.baked ?? []).map(maybeAse1),
879
+ tracked: (r.tracked ?? []).map((t) => ({ ...t, address: maybeAse1(t.address) }))
880
+ };
881
+ }
882
+ /** Slashed and tombstoned validators (GET /slashing/tombstones). */
883
+ async getTombstones(opts = {}) {
884
+ const r = await this.getChecked("/slashing/tombstones", opts);
885
+ return { ...r, slashingContract: maybeAse1(r.slashingContract) };
886
+ }
887
+ // ---------------------------------------------------------------------------
888
+ // Governance (reads; voting and proposing are contract calls)
889
+ // ---------------------------------------------------------------------------
890
+ /** The newest proposals, up to 50 (GET /governance/proposals). */
891
+ async getProposals(opts = {}) {
892
+ const r = await this.getChecked("/governance/proposals", opts);
893
+ return (r.proposals ?? []).map((p) => ({ ...p, proposer: maybeAse1(p.proposer) }));
894
+ }
895
+ /** One proposal with its amendments, or null for an unknown id (GET /governance/proposal/<id>). */
896
+ async getProposal(id, opts = {}) {
897
+ const r = await this.getChecked(
898
+ `/governance/proposal/${encodeURIComponent(String(id))}`,
899
+ opts
900
+ );
901
+ if (!r.proposal) return null;
902
+ const amendments = (r.amendments ?? []).map((a) => {
903
+ const o = asRecord(a);
904
+ if (!o) return a;
905
+ const c = { ...o };
906
+ for (const k of ["proposer", "author", "sender"]) if (k in c) c[k] = maybeAse1(c[k]);
907
+ return c;
908
+ });
909
+ return { proposal: { ...r.proposal, proposer: maybeAse1(r.proposal.proposer) }, amendments };
910
+ }
911
+ /** ASE (wei) an address has locked in governance votes (GET /governance/locked). */
912
+ async getGovernanceLocked(address, opts = {}) {
913
+ const r = await this.getChecked(`/governance/locked/${encodeURIComponent(address)}`, opts);
914
+ return { ...r, address: toAse1(r.address ?? address) };
915
+ }
916
+ /** Vote weight still locked vs ready to withdraw (GET /governance/withdrawable). */
917
+ async getGovernanceWithdrawable(address, opts = {}) {
918
+ const r = await this.getChecked(`/governance/withdrawable/${encodeURIComponent(address)}`, opts);
919
+ return { ...r, address: toAse1(r.address ?? address) };
920
+ }
921
+ /** Approved libraries (GET /governance/libraries). Library approval is disabled, so always empty today. */
922
+ async getGovernanceLibraries(opts = {}) {
923
+ return (await this.getChecked("/governance/libraries", opts)).libraries ?? [];
924
+ }
925
+ /** One approved library by name, or null (GET /governance/library/<name>). */
926
+ async getGovernanceLibrary(name, opts = {}) {
927
+ return (await this.getChecked(`/governance/library/${encodeURIComponent(name)}`, opts)).library ?? null;
928
+ }
929
+ // ---------------------------------------------------------------------------
930
+ // Dev faucet (devnets only)
123
931
  // ---------------------------------------------------------------------------
124
- async get(path) {
125
- const res = await fetch(`${this.rpcUrl}${path}`);
126
- if (!res.ok) throw new Error(`GET ${path} failed: ${res.status} ${res.statusText}`);
127
- return await res.json();
932
+ /**
933
+ * @deprecated The node's POST /dev/faucet is OFF unless the operator sets
934
+ * ASENTUM_DEV_FAUCET=1, which only makes sense on a local devnet. Every
935
+ * public node answers `{ accepted: false, reason: "faucet not enabled on
936
+ * this node" }`. For test ASE use the airdrop dashboard (see the docs page
937
+ * "Get Test ASE"). Kept for local devnets; never throws on 404 / 429.
938
+ */
939
+ async faucet(to, amount = "100000000000000000000", opts = {}) {
940
+ const r = await this.transport.request("/dev/faucet", {
941
+ method: "POST",
942
+ body: { to, amount },
943
+ signal: opts.signal,
944
+ retries: 0,
945
+ retryOnServerError: false,
946
+ allowStatus: [404, 410, 429]
947
+ });
948
+ const b = asRecord(r.body);
949
+ if (!b || typeof b.accepted !== "boolean") {
950
+ return { accepted: false, reason: `faucet not available on this node (HTTP ${r.status})` };
951
+ }
952
+ return r.body;
128
953
  }
129
- async post(path, body) {
130
- const res = await fetch(`${this.rpcUrl}${path}`, {
954
+ // ---------------------------------------------------------------------------
955
+ // JSON-RPC
956
+ // ---------------------------------------------------------------------------
957
+ /** Send one Ethereum-style JSON-RPC request to POST /. Throws JsonRpcError on an error object. */
958
+ async jsonRpc(method, params = [], opts = {}) {
959
+ const r = await this.transport.request("/", {
131
960
  method: "POST",
132
- headers: { "Content-Type": "application/json" },
133
- body: JSON.stringify(body)
961
+ body: { jsonrpc: "2.0", id: 1, method, params },
962
+ signal: opts.signal,
963
+ retries: opts.retries
134
964
  });
135
- if (!res.ok) throw new Error(`POST ${path} failed: ${res.status} ${res.statusText}`);
136
- return await res.json();
965
+ const body = asRecord(r.body);
966
+ if (!body) throw new AsentumError("JSON_RPC_ERROR", `JSON-RPC ${method}: unexpected response`);
967
+ const e = asRecord(body.error);
968
+ if (e) throw new JsonRpcError(method, Number(e.code ?? 0), String(e.message ?? "error"), e.data);
969
+ return body.result;
970
+ }
971
+ // ---------------------------------------------------------------------------
972
+ // Internal HTTP helpers
973
+ // ---------------------------------------------------------------------------
974
+ /** GET and return the body as is. */
975
+ async get(path, opts = {}) {
976
+ const r = await this.transport.request(path, { signal: opts.signal, retries: opts.retries, allowStatus: opts.allowStatus });
977
+ return r.body;
978
+ }
979
+ /** GET and throw RpcError when the body carries an `error` field (the node's HTTP 200 errors). */
980
+ async getChecked(path, opts = {}) {
981
+ const body = await this.get(path, opts);
982
+ const err = errorField(body);
983
+ if (err) throw new RpcError(path.split("?")[0], err, body);
984
+ if (body === null || typeof body !== "object") throw new RpcError(path, "unexpected non-JSON response", body);
985
+ return body;
137
986
  }
138
987
  };
139
988
 
140
- // src/wallet.ts
141
- import {
142
- generateKeypair,
143
- addressFromPublicKey
144
- } from "@asentum/crypto";
145
- import {
146
- SignedTransactionSchema,
147
- TransactionBodySchema
148
- } from "@asentum/types";
149
- import { blake3, sign } from "@asentum/crypto";
150
-
151
989
  // src/contract.ts
152
990
  var AsentumContract = class _AsentumContract {
153
991
  client;
992
+ /** ase1. (0.1.x kept whatever was passed in; a deploy gave 0x hex.) */
154
993
  address;
155
- /** The deploy tx, if this contract was deployed in this session. */
994
+ /** Lowercase 0x hex, what other contracts and msg.sender comparisons use. */
995
+ addressHex;
996
+ /** The deploy transaction, when this contract was deployed in this session. */
156
997
  deployTx;
157
- /** The wallet used to deploy this contract, if any. Used as default signer for send(). */
158
998
  wallet;
159
999
  constructor(client, address, deployTx, wallet) {
160
1000
  this.client = client;
161
- this.address = address;
1001
+ this.addressHex = toHexAddress(address);
1002
+ this.address = toAse1(this.addressHex);
162
1003
  this.deployTx = deployTx;
163
1004
  this.wallet = wallet;
164
1005
  }
165
- /**
166
- * Call a view function (read-only, free, no transaction).
167
- *
168
- * @param method - The function name to call.
169
- * @param args - Arguments to pass to the function.
170
- * @returns The return value of the function.
171
- */
172
- async view(method, args = []) {
173
- const result = await this.client.viewCall(this.address, method, args);
174
- if (!result.ok) {
175
- throw new Error(`view call ${method}() failed: ${result.reason || "unknown"}`);
176
- }
1006
+ /** Call a view method (free, no transaction) and return its value. Throws ViewError on ok: false. */
1007
+ async view(method, args = [], opts = {}) {
1008
+ const result = await this.client.viewCall(this.address, method, args, opts);
1009
+ if (!result.ok) throw new ViewError(method, result.reason ?? "unknown");
177
1010
  return result.returnValue;
178
1011
  }
179
- /**
180
- * Call a view function and return the full result including gas + events.
181
- */
182
- async viewFull(method, args = []) {
183
- return this.client.viewCall(this.address, method, args);
1012
+ /** Call a view method and return the full result: value, gas, events, blockNumber, finalized. */
1013
+ async viewFull(method, args = [], opts = {}) {
1014
+ return this.client.viewCall(this.address, method, args, opts);
184
1015
  }
185
- /**
186
- * Send a state-changing transaction to the contract.
187
- *
188
- * @param method - The function name to call.
189
- * @param args - Arguments to pass.
190
- * @param opts - Optional: value to attach, gas limit, nonce, wallet override.
191
- * @returns A SentTx with `.hash` and `.wait()`.
192
- */
1016
+ /** Send a state-changing call. Needs a wallet (connect() or opts.wallet). */
193
1017
  async send(method, args = [], opts = {}) {
194
1018
  const signer = opts.wallet ?? this.wallet;
195
1019
  if (!signer) {
196
- throw new Error(
197
- "no wallet available \u2014 pass a wallet in opts or create the contract via wallet.deploy()"
198
- );
1020
+ throw new AsentumError("INVALID_ARGUMENT", "no wallet available: pass opts.wallet or use contract.connect(wallet)");
199
1021
  }
200
1022
  return signer.sendCall({
201
1023
  to: this.address,
202
1024
  method,
203
1025
  args,
204
1026
  value: opts.value,
1027
+ valueWei: opts.valueWei,
205
1028
  gasLimit: opts.gasLimit,
206
- nonce: opts.nonce
1029
+ nonce: opts.nonce,
1030
+ signal: opts.signal
207
1031
  });
208
1032
  }
209
- /**
210
- * Attach a wallet to this contract for future send() calls.
211
- */
1033
+ /** A copy of this contract that signs with `wallet`. */
212
1034
  connect(wallet) {
213
1035
  return new _AsentumContract(this.client, this.address, this.deployTx, wallet);
214
1036
  }
215
- /**
216
- * Get the source code of this contract as a UTF-8 string.
217
- * Returns null if the address has no deployed code.
218
- */
219
- async getSource() {
220
- return this.client.getContractSource(this.address);
1037
+ /** The contract's JavaScript source, or null if the address has no code. */
1038
+ async getSource(opts = {}) {
1039
+ return this.client.getContractSource(this.address, opts);
1040
+ }
1041
+ /** The contract's ASE balance in wei. */
1042
+ async getBalance(opts = {}) {
1043
+ return BigInt((await this.client.getBalance(this.address, opts)).balance);
1044
+ }
1045
+ /** The account record (codeHash = BLAKE3 of the source, storageRoot). */
1046
+ async getAccount(opts = {}) {
1047
+ return this.client.getAccount(this.address, opts);
221
1048
  }
222
1049
  /**
223
- * Get the ASE balance of this contract.
1050
+ * Events this contract emitted, decoded (eth_getLogs, finalized blocks
1051
+ * only, ranges over 10,000 blocks are split). `eventName` undefined = all.
224
1052
  */
225
- async getBalance() {
226
- const bal = await this.client.getBalance(this.address);
227
- return BigInt(bal.balance);
1053
+ async getEvents(eventName, range = {}, opts = {}) {
1054
+ return this.client.getLogs({ address: this.address, event: eventName, ...range }, opts);
228
1055
  }
229
1056
  /**
230
- * Wait for the deploy transaction to be confirmed (if this contract
231
- * was deployed in this session). Useful after `wallet.deploy()`.
1057
+ * Wait for the deploy transaction (only when deployed in this session).
1058
+ * Accepts a timeout in ms (0.1.x) or WaitOptions, e.g. { until: 'final' }.
232
1059
  */
233
- async waitForDeploy(timeoutMs = 15e3) {
1060
+ async waitForDeploy(timeoutOrOpts) {
234
1061
  if (!this.deployTx) {
235
- throw new Error("this contract was not deployed in this session \u2014 no deploy tx to wait for");
1062
+ throw new AsentumError("INVALID_ARGUMENT", "this contract was not deployed in this session: there is no deploy tx to wait for");
236
1063
  }
237
- return this.deployTx.wait(timeoutMs);
1064
+ return this.deployTx.wait(timeoutOrOpts);
238
1065
  }
239
1066
  };
240
1067
 
241
- // src/wallet.ts
242
- function bytesToHex(bytes) {
243
- let hex = "0x";
244
- for (let i = 0; i < bytes.length; i++) {
245
- hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
246
- }
247
- return hex;
248
- }
249
- function hexToBytes(hex) {
250
- let h = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
251
- if (h.length % 2 !== 0) h = "0" + h;
252
- const out = new Uint8Array(h.length / 2);
253
- for (let i = 0; i < out.length; i++) {
254
- out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
255
- }
1068
+ // src/internal/tx.ts
1069
+ import { ByteListType, ByteVectorType, ContainerType, UintBigintType } from "@chainsafe/ssz";
1070
+ var MAX_TX_DATA_BYTES = 1048576;
1071
+ var Uint64 = new UintBigintType(8);
1072
+ var Uint256 = new UintBigintType(32);
1073
+ var AddressBytes = new ByteVectorType(ADDRESS_BYTES);
1074
+ var TransactionBodySchema = new ContainerType({
1075
+ chainId: Uint64,
1076
+ nonce: Uint64,
1077
+ maxPriorityFeePerGas: Uint256,
1078
+ maxFeePerGas: Uint256,
1079
+ gasLimit: Uint64,
1080
+ to: AddressBytes,
1081
+ value: Uint256,
1082
+ data: new ByteListType(MAX_TX_DATA_BYTES)
1083
+ });
1084
+ var LegacySignedTransactionSchema = new ContainerType({
1085
+ body: TransactionBodySchema,
1086
+ publicKey: new ByteVectorType(DILITHIUM3_PUBLIC_KEY_BYTES),
1087
+ signature: new ByteVectorType(DILITHIUM3_SIGNATURE_BYTES)
1088
+ });
1089
+ var KeylessSignedTransactionSchema = new ContainerType({
1090
+ body: TransactionBodySchema,
1091
+ sender: AddressBytes,
1092
+ signature: new ByteVectorType(DILITHIUM3_SIGNATURE_BYTES)
1093
+ });
1094
+ var KEYLESS_TX_SIGNING_DOMAIN = (() => {
1095
+ const tag = new TextEncoder().encode("asentum/tx/keyless/v1");
1096
+ const out = new Uint8Array(32);
1097
+ out.set(tag, 0);
1098
+ return out;
1099
+ })();
1100
+ function keylessSigningPreimage(body, sender) {
1101
+ if (sender.length !== ADDRESS_BYTES) throw new Error(`key-less sender must be ${ADDRESS_BYTES} bytes`);
1102
+ const bodyBytes = TransactionBodySchema.serialize(body);
1103
+ const out = new Uint8Array(32 + ADDRESS_BYTES + bodyBytes.length);
1104
+ out.set(KEYLESS_TX_SIGNING_DOMAIN, 0);
1105
+ out.set(sender, 32);
1106
+ out.set(bodyBytes, 32 + ADDRESS_BYTES);
256
1107
  return out;
257
1108
  }
258
- function hexToAddress20(hex) {
259
- const bytes = hexToBytes(hex);
260
- if (bytes.length !== 20) throw new Error(`expected 20-byte address, got ${bytes.length}`);
261
- return bytes;
1109
+ function signLegacy(body, publicKey, secretKey) {
1110
+ const digest = blake3(TransactionBodySchema.serialize(body));
1111
+ const signature = sign(digest, secretKey);
1112
+ const bytes = LegacySignedTransactionSchema.serialize({ body, publicKey, signature });
1113
+ return { rawTx: bytesToHex(bytes), hash: bytesToHex(blake3(bytes)) };
1114
+ }
1115
+ function signKeyless(body, sender, secretKey) {
1116
+ const digest = blake3(keylessSigningPreimage(body, sender));
1117
+ const signature = sign(digest, secretKey);
1118
+ const bytes = KeylessSignedTransactionSchema.serialize({ body, sender, signature });
1119
+ return { rawTx: bytesToHex(bytes), hash: bytesToHex(blake3(bytes)) };
1120
+ }
1121
+ function contractAddressBytes(sender, nonce) {
1122
+ const buf = new Uint8Array(ADDRESS_BYTES + 8);
1123
+ buf.set(sender, 0);
1124
+ new DataView(buf.buffer).setBigUint64(ADDRESS_BYTES, nonce, false);
1125
+ return blake3(buf).slice(0, ADDRESS_BYTES);
1126
+ }
1127
+
1128
+ // src/units.ts
1129
+ var WEI_PER_ASE = 1000000000000000000n;
1130
+ function numberToDecimalString(n) {
1131
+ if (!Number.isFinite(n) || n < 0) throw new AsentumError("INVALID_ARGUMENT", `invalid amount: ${n}`);
1132
+ return n.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 });
1133
+ }
1134
+ function aseToWei(amount) {
1135
+ const s = typeof amount === "number" ? numberToDecimalString(amount) : String(amount).trim();
1136
+ const m = /^(\d+)(?:\.(\d*))?$/.exec(s);
1137
+ if (!m) throw new AsentumError("INVALID_ARGUMENT", `invalid ASE amount: "${amount}" (use "1" or "1.5")`);
1138
+ const whole = m[1];
1139
+ const frac = (m[2] ?? "").replace(/0+$/, "");
1140
+ if (frac.length > 18) throw new AsentumError("INVALID_ARGUMENT", `invalid ASE amount: "${amount}" has more than 18 decimals`);
1141
+ return BigInt(whole) * WEI_PER_ASE + BigInt(frac.padEnd(18, "0") || "0");
262
1142
  }
263
1143
  function parseAse(amount) {
264
1144
  if (typeof amount === "bigint") return amount;
265
- if (typeof amount === "number") return BigInt(Math.floor(amount * 1e18));
1145
+ if (typeof amount === "number") return aseToWei(amount);
266
1146
  const s = amount.trim();
267
- if (/^\d+$/.test(s)) {
268
- if (s.length > 18) return BigInt(s);
269
- return BigInt(s) * 1000000000000000000n;
270
- }
271
- if (/^\d+\.\d{1,18}$/.test(s)) {
272
- const [whole, frac = ""] = s.split(".");
273
- const padded = frac.padEnd(18, "0").slice(0, 18);
274
- return BigInt(whole + padded);
275
- }
276
- throw new Error(`invalid amount: "${amount}" \u2014 use "1", "1.5", or a wei integer`);
1147
+ if (/^\d+$/.test(s) && s.length > 18) return BigInt(s);
1148
+ if (/^\d+(\.\d{1,18})?$/.test(s)) return aseToWei(s);
1149
+ throw new AsentumError("INVALID_ARGUMENT", `invalid amount: "${amount}" (use "1", "1.5", or a bigint in wei)`);
277
1150
  }
278
1151
  function formatAse(wei) {
279
1152
  const w = typeof wei === "bigint" ? wei : BigInt(wei);
280
- const whole = w / 1000000000000000000n;
281
- const frac = w % 1000000000000000000n;
282
- if (frac === 0n) return whole.toString();
283
- const fracStr = frac.toString().padStart(18, "0").replace(/0+$/, "");
284
- return `${whole}.${fracStr}`;
1153
+ const neg = w < 0n;
1154
+ const abs = neg ? -w : w;
1155
+ const whole = abs / WEI_PER_ASE;
1156
+ const frac = abs % WEI_PER_ASE;
1157
+ const out = frac === 0n ? whole.toString() : `${whole}.${frac.toString().padStart(18, "0").replace(/0+$/, "")}`;
1158
+ return neg ? `-${out}` : out;
1159
+ }
1160
+ function toWei(v) {
1161
+ if (typeof v === "bigint") return v;
1162
+ if (typeof v === "number") {
1163
+ if (!Number.isSafeInteger(v) || v < 0) throw new AsentumError("INVALID_ARGUMENT", `wei must be a non-negative integer, got ${v}`);
1164
+ return BigInt(v);
1165
+ }
1166
+ if (!/^\d+$/.test(v.trim())) throw new AsentumError("INVALID_ARGUMENT", `wei must be a decimal integer string, got "${v}"`);
1167
+ return BigInt(v.trim());
1168
+ }
1169
+
1170
+ // src/wallet.ts
1171
+ function formatAddress(input) {
1172
+ const bytes = typeof input === "string" ? addressToBytes(input) : input;
1173
+ const wantHex = typeof process !== "undefined" && process?.env?.ASENTUM_ADDR_FORMAT === "hex";
1174
+ return wantHex ? bytesToHex(bytes) : bytesToAse1(bytes);
1175
+ }
1176
+ function makeWait(client, hash) {
1177
+ return (timeoutOrOpts) => client.waitForReceipt(hash, typeof timeoutOrOpts === "number" ? { timeoutMs: timeoutOrOpts } : timeoutOrOpts ?? {});
1178
+ }
1179
+ function pickWei(wei, loose) {
1180
+ if (wei !== void 0) return toWei(wei);
1181
+ if (loose !== void 0 && loose !== null && loose !== "") return parseAse(loose);
1182
+ return 0n;
285
1183
  }
286
1184
  var AsentumWallet = class _AsentumWallet {
287
1185
  client;
288
1186
  keypair;
1187
+ /**
1188
+ * The wallet's address as ase1. (0.1.x returned checksummed 0x hex here;
1189
+ * use `addressHex` for the hex form.)
1190
+ */
289
1191
  address;
290
- constructor(client, keypair) {
1192
+ /** The same address as lowercase 0x hex: what contracts see as msg.sender. */
1193
+ addressHex;
1194
+ /** Key-less mode for transactions from this wallet. */
1195
+ keyless;
1196
+ addressBytes;
1197
+ nextNonce;
1198
+ queue = Promise.resolve();
1199
+ keyOnRecord = false;
1200
+ keyCheckedAt = 0;
1201
+ constructor(client, keypair, opts = {}) {
291
1202
  this.client = client;
292
1203
  this.keypair = keypair;
293
- this.address = addressFromPublicKey(keypair.publicKey);
294
- }
295
- /** Create a wallet with a random Dilithium3 keypair. */
296
- static create(client) {
297
- const seed = new Uint8Array(32);
298
- crypto.getRandomValues(seed);
299
- const keypair = generateKeypair(seed);
300
- return new _AsentumWallet(client, keypair);
301
- }
302
- /** Create a wallet from a 32-byte seed. */
303
- static fromSeed(client, seed) {
304
- if (seed.length < 32) throw new Error("seed must be at least 32 bytes");
305
- const keypair = generateKeypair(seed.slice(0, 32));
306
- return new _AsentumWallet(client, keypair);
307
- }
308
- /** Create a wallet from a hex-encoded secret key. */
309
- static fromSecretKey(client, secretKeyHex, publicKeyHex) {
310
- return new _AsentumWallet(client, {
311
- secretKey: hexToBytes(secretKeyHex),
312
- publicKey: hexToBytes(publicKeyHex)
313
- });
1204
+ this.addressBytes = addressBytesFromPublicKey(keypair.publicKey);
1205
+ this.address = bytesToAse1(this.addressBytes);
1206
+ this.addressHex = bytesToHex(this.addressBytes);
1207
+ this.keyless = opts.keyless ?? "off";
1208
+ }
1209
+ /** A wallet with a random keypair. */
1210
+ static create(client, opts) {
1211
+ return new _AsentumWallet(client, generateKeypair(), opts);
1212
+ }
1213
+ /** A wallet from a seed (the first 32 bytes are used). Same seed, same keys. */
1214
+ static fromSeed(client, seed, opts) {
1215
+ if (seed.length < 32) throw new AsentumError("INVALID_ARGUMENT", "seed must be at least 32 bytes");
1216
+ return new _AsentumWallet(client, generateKeypair(seed.slice(0, 32)), opts);
314
1217
  }
315
- /** Get the secret key as a hex string (for export/backup). */
1218
+ /** A wallet from exported hex keys (secretKeyHex / publicKeyHex). */
1219
+ static fromSecretKey(client, secretKeyHex, publicKeyHex, opts) {
1220
+ return new _AsentumWallet(client, { secretKey: hexToBytes(secretKeyHex), publicKey: hexToBytes(publicKeyHex) }, opts);
1221
+ }
1222
+ /** The secret key as hex, for backup. Treat it like a password. */
316
1223
  get secretKeyHex() {
317
1224
  return bytesToHex(this.keypair.secretKey);
318
1225
  }
319
- /** Get the public key as a hex string. */
1226
+ /** The public key as hex. */
320
1227
  get publicKeyHex() {
321
1228
  return bytesToHex(this.keypair.publicKey);
322
1229
  }
323
- /** Get the current balance in wei. */
1230
+ /** Balance in wei. */
324
1231
  async getBalance() {
325
- const bal = await this.client.getBalance(this.address);
326
- return BigInt(bal.balance);
1232
+ return BigInt((await this.client.getBalance(this.address)).balance);
327
1233
  }
328
- /** Get the current nonce. */
1234
+ /** The node's pendingNonce for this wallet (the nonce the next transaction would use, ignoring local allocations). */
329
1235
  async getNonce() {
330
1236
  return this.client.getNonce(this.address);
331
1237
  }
332
- /** Request test ASE from the faucet. */
1238
+ /**
1239
+ * Forget locally allocated nonces; the next transaction asks the node
1240
+ * again. The wallet does this itself after a rejection.
1241
+ */
1242
+ resetNonce() {
1243
+ this.nextNonce = void 0;
1244
+ }
1245
+ /**
1246
+ * @deprecated POST /dev/faucet is off on every public node (see
1247
+ * AsentumClient.faucet). Use the airdrop dashboard for test ASE.
1248
+ */
333
1249
  async requestFaucet(amount = "100000000000000000000") {
334
1250
  return this.client.faucet(this.address, amount);
335
1251
  }
336
1252
  // ---------------------------------------------------------------------------
337
- // Sending transactions
1253
+ // Send (sign + submit)
338
1254
  // ---------------------------------------------------------------------------
339
- /** Send ASE to another address. */
1255
+ /** Send ASE. Throws TxRejectedError when the node does not accept it. */
340
1256
  async sendTransfer(opts) {
341
- const chainId = await this.client.getChainId();
342
- const nonce = opts.nonce ?? await this.getNonce();
343
- const value = parseAse(opts.amount);
344
- const body = {
345
- chainId,
346
- nonce,
347
- maxPriorityFeePerGas: 0n,
348
- maxFeePerGas: 1n,
349
- gasLimit: opts.gasLimit ?? 21000n,
350
- to: hexToAddress20(opts.to),
351
- value,
352
- data: new Uint8Array(0)
353
- };
354
- return this.signAndSubmit(body);
1257
+ return this.serial(async () => this.submit(await this.buildTransfer(opts), opts.signal));
355
1258
  }
356
- /** Call a method on a deployed contract (state-changing). */
1259
+ /** Call a contract method (state-changing). Throws TxRejectedError when the node does not accept it. */
357
1260
  async sendCall(opts) {
358
- const chainId = await this.client.getChainId();
359
- const nonce = opts.nonce ?? await this.getNonce();
360
- const data = new TextEncoder().encode(
361
- JSON.stringify({ method: opts.method, args: opts.args ?? [] })
362
- );
363
- const body = {
364
- chainId,
365
- nonce,
366
- maxPriorityFeePerGas: 0n,
367
- maxFeePerGas: 1n,
368
- gasLimit: opts.gasLimit ?? 2000000n,
369
- to: hexToAddress20(opts.to),
370
- value: opts.value ? parseAse(opts.value) : 0n,
371
- data
372
- };
373
- return this.signAndSubmit(body);
1261
+ return this.serial(async () => this.submit(await this.buildCall(opts), opts.signal));
1262
+ }
1263
+ /**
1264
+ * Same as sendCall, but `value` is in wei. Use this when you hold wei
1265
+ * amounts (balances, contract return values) so nothing is reinterpreted.
1266
+ */
1267
+ async sendCallWei(opts) {
1268
+ const { value, ...rest } = opts;
1269
+ return this.sendCall({ ...rest, valueWei: value ?? 0n });
374
1270
  }
375
- /** Deploy a new smart contract. Returns an AsentumContract instance. */
1271
+ /** Same as sendTransfer, but `amount` is in wei. */
1272
+ async sendTransferWei(opts) {
1273
+ const { amount, ...rest } = opts;
1274
+ return this.sendTransfer({ ...rest, amountWei: amount });
1275
+ }
1276
+ /**
1277
+ * Deploy a contract. Returns an AsentumContract at the derived address
1278
+ * right away; the deploy is only accepted, so `await contract.waitForDeploy()`
1279
+ * before using it.
1280
+ */
376
1281
  async deploy(sourceOrOpts) {
377
1282
  const opts = typeof sourceOrOpts === "string" ? { source: sourceOrOpts } : sourceOrOpts;
1283
+ const sent = await this.serial(async () => this.submit(await this.buildDeploy(opts), opts.signal));
1284
+ return new AsentumContract(this.client, sent.contractAddress, sent, this);
1285
+ }
1286
+ // ---------------------------------------------------------------------------
1287
+ // Sign only, and batches
1288
+ // ---------------------------------------------------------------------------
1289
+ /** Sign a transfer without submitting it (allocates the next nonce unless given). */
1290
+ async signTransfer(opts) {
1291
+ return this.serial(() => this.buildTransfer(opts));
1292
+ }
1293
+ /** Sign a contract call without submitting it. */
1294
+ async signCall(opts) {
1295
+ return this.serial(() => this.buildCall(opts));
1296
+ }
1297
+ /** Sign a deploy without submitting it. `contractAddress` is the address it will get. */
1298
+ async signDeploy(opts) {
1299
+ return this.serial(() => this.buildDeploy(opts));
1300
+ }
1301
+ /**
1302
+ * Submit several signed transactions in one request (POST /tx/batch), in
1303
+ * the given order. Sign them first with signTransfer / signCall /
1304
+ * signDeploy (consecutive calls get consecutive nonces). One rejection
1305
+ * does not stop the rest, but a nonce gap leaves later ones waiting.
1306
+ * Does not throw on rejections: check each result.
1307
+ */
1308
+ async sendBatch(txs, opts = {}) {
1309
+ const r = await this.client.submitRawTxBatch(txs.map((t) => t.rawTx), { signal: opts.signal });
1310
+ if (r.rejected > 0) this.resetNonce();
1311
+ return {
1312
+ accepted: r.accepted,
1313
+ rejected: r.rejected,
1314
+ total: r.total,
1315
+ results: txs.map((t, i) => {
1316
+ const res = r.results[i] ?? { accepted: false, reason: "no result from node" };
1317
+ const hash = res.txHash ?? t.hash;
1318
+ return { ...res, hash, nonce: t.nonce, wait: makeWait(this.client, hash) };
1319
+ })
1320
+ };
1321
+ }
1322
+ // ---------------------------------------------------------------------------
1323
+ // Internal
1324
+ // ---------------------------------------------------------------------------
1325
+ /** Run fn after every earlier send/sign of this wallet, so nonces are handed out in order. */
1326
+ serial(fn) {
1327
+ const run = this.queue.then(fn, fn);
1328
+ this.queue = run.catch(() => void 0);
1329
+ return run;
1330
+ }
1331
+ async allocateNonce(explicit) {
1332
+ if (explicit !== void 0) return explicit;
1333
+ const fromNode = await this.client.getNonce(this.address);
1334
+ const n = this.nextNonce !== void 0 && this.nextNonce > fromNode ? this.nextNonce : fromNode;
1335
+ this.nextNonce = n + 1n;
1336
+ return n;
1337
+ }
1338
+ async useKeyless(mode) {
1339
+ const m = mode ?? this.keyless;
1340
+ if (m === "off") return false;
1341
+ if (m === "always") return true;
1342
+ if (this.keyOnRecord) return true;
1343
+ if (Date.now() - this.keyCheckedAt < 6e4) return false;
1344
+ this.keyCheckedAt = Date.now();
1345
+ try {
1346
+ this.keyOnRecord = (await this.client.getPublicKeyStatus(this.address)).canOmitKey === true;
1347
+ } catch {
1348
+ this.keyOnRecord = false;
1349
+ }
1350
+ return this.keyOnRecord;
1351
+ }
1352
+ async build(fields, common, isDeploy = false) {
378
1353
  const chainId = await this.client.getChainId();
379
- const nonce = opts.nonce ?? await this.getNonce();
380
- const data = new TextEncoder().encode(opts.source);
381
- const gasLimit = opts.gasLimit ?? BigInt(Math.max(2e6, 1e3 * opts.source.length));
1354
+ const keyless = await this.useKeyless(common.keyless);
1355
+ const nonce = await this.allocateNonce(common.nonce);
382
1356
  const body = {
383
1357
  chainId,
384
1358
  nonce,
385
1359
  maxPriorityFeePerGas: 0n,
386
- maxFeePerGas: 1n,
387
- gasLimit,
388
- to: new Uint8Array(20),
389
- // zero address = deploy
390
- value: opts.value ? parseAse(opts.value) : 0n,
391
- data
1360
+ maxFeePerGas: common.maxFeePerGas ?? 1n,
1361
+ gasLimit: common.gasLimit ?? fields.gasLimit,
1362
+ to: fields.to,
1363
+ value: fields.value,
1364
+ data: fields.data
392
1365
  };
393
- const sentTx = await this.signAndSubmit(body);
394
- const senderBytes = hexToAddress20(this.address);
395
- const nonceBytes = new Uint8Array(8);
396
- new DataView(nonceBytes.buffer).setBigUint64(0, nonce, true);
397
- const combined = new Uint8Array(20 + 8);
398
- combined.set(senderBytes);
399
- combined.set(nonceBytes, 20);
400
- const hash = blake3(combined);
401
- const contractAddress = bytesToHex(hash.slice(0, 20));
402
- return new AsentumContract(this.client, contractAddress, sentTx, this);
403
- }
404
- // ---------------------------------------------------------------------------
405
- // Internal: sign + submit
406
- // ---------------------------------------------------------------------------
407
- async signAndSubmit(body) {
408
- const bodyBytes = TransactionBodySchema.serialize(body);
409
- const digest = blake3(bodyBytes);
410
- const signature = sign(digest, this.keypair.secretKey);
411
- const signedTx = {
412
- body,
413
- publicKey: this.keypair.publicKey,
414
- signature
1366
+ const signed = keyless ? signKeyless(body, this.addressBytes, this.keypair.secretKey) : signLegacy(body, this.keypair.publicKey, this.keypair.secretKey);
1367
+ return {
1368
+ ...signed,
1369
+ nonce,
1370
+ from: this.address,
1371
+ ...isDeploy ? { contractAddress: bytesToAse1(contractAddressBytes(this.addressBytes, nonce)) } : {}
415
1372
  };
416
- const rawTxHex = bytesToHex(SignedTransactionSchema.serialize(signedTx));
417
- const result = await this.client.submitRawTx(rawTxHex);
418
- if (!result.accepted) {
419
- throw new Error(`transaction rejected: ${result.reason || "unknown reason"}`);
1373
+ }
1374
+ buildTransfer(opts) {
1375
+ if (opts.amountWei === void 0 && opts.amount === void 0) {
1376
+ throw new AsentumError("INVALID_ARGUMENT", "sendTransfer needs amount or amountWei");
420
1377
  }
421
- const txHash = result.txHash ?? "(unknown)";
422
- const client = this.client;
1378
+ return this.build(
1379
+ { to: addressToBytes(opts.to), value: pickWei(opts.amountWei, opts.amount), data: new Uint8Array(0), gasLimit: 21000n },
1380
+ opts
1381
+ );
1382
+ }
1383
+ buildCall(opts) {
1384
+ const data = new TextEncoder().encode(JSON.stringify({ method: opts.method, args: ase1ArgsToHex(opts.args ?? []) }));
1385
+ return this.build(
1386
+ { to: addressToBytes(opts.to), value: pickWei(opts.valueWei, opts.value), data, gasLimit: 2000000n },
1387
+ opts
1388
+ );
1389
+ }
1390
+ buildDeploy(opts) {
1391
+ const data = new TextEncoder().encode(opts.source);
1392
+ return this.build(
1393
+ {
1394
+ to: new Uint8Array(20),
1395
+ // zero address = deploy
1396
+ value: pickWei(opts.valueWei, opts.value),
1397
+ data,
1398
+ gasLimit: BigInt(Math.max(2e6, 1e3 * opts.source.length))
1399
+ },
1400
+ opts,
1401
+ true
1402
+ );
1403
+ }
1404
+ async submit(signed, signal) {
1405
+ let result;
1406
+ try {
1407
+ result = await this.client.submitRawTx(signed.rawTx, { signal });
1408
+ } catch (err) {
1409
+ this.resetNonce();
1410
+ throw err;
1411
+ }
1412
+ if (!result || !result.accepted) {
1413
+ this.resetNonce();
1414
+ throw new TxRejectedError(result?.reason ?? "unknown reason", signed.hash);
1415
+ }
1416
+ const hash = result.txHash ?? signed.hash;
423
1417
  return {
424
- hash: txHash,
425
- async wait(timeoutMs = 15e3) {
426
- const start = Date.now();
427
- while (Date.now() - start < timeoutMs) {
428
- const receipt = await client.getReceipt(txHash);
429
- if (receipt) return receipt;
430
- await new Promise((r) => setTimeout(r, 1e3));
431
- }
432
- throw new Error(`transaction ${txHash} not confirmed within ${timeoutMs}ms`);
433
- }
1418
+ hash,
1419
+ nonce: signed.nonce,
1420
+ from: signed.from,
1421
+ ...signed.contractAddress ? { contractAddress: signed.contractAddress } : {},
1422
+ wait: makeWait(this.client, hash)
434
1423
  };
435
1424
  }
436
1425
  };
1426
+ function getContractAddress(sender, nonce) {
1427
+ return bytesToAse1(contractAddressBytes(addressToBytes(sender), BigInt(nonce)));
1428
+ }
1429
+
1430
+ // src/index.ts
1431
+ var VERSION = "0.2.0";
437
1432
  export {
1433
+ AbortError,
438
1434
  AsentumClient,
439
1435
  AsentumContract,
1436
+ AsentumError,
440
1437
  AsentumWallet,
1438
+ DEFAULT_RPC,
1439
+ GOVERNANCE_CONTRACT,
1440
+ HttpError,
1441
+ JsonRpcError,
1442
+ MAX_LOG_RANGE,
1443
+ NetworkError,
1444
+ RateLimitError,
1445
+ RpcError,
1446
+ TimeoutError,
1447
+ TxFailedError,
1448
+ TxRejectedError,
1449
+ VERSION,
1450
+ ViewError,
1451
+ addressToBytes,
1452
+ ase1ArgsToHex,
1453
+ ase1ToHex,
1454
+ ase1ToHexOrNull,
1455
+ aseToWei,
1456
+ bytesToAse1,
1457
+ eventTopic,
1458
+ formatAddress,
441
1459
  formatAse,
442
- parseAse
1460
+ getContractAddress,
1461
+ hexToAse1,
1462
+ isAddress,
1463
+ isAse1,
1464
+ isHexAddress,
1465
+ parseAse,
1466
+ shortAse1,
1467
+ toAse1,
1468
+ toHexAddress,
1469
+ toWei
443
1470
  };
444
1471
  //# sourceMappingURL=index.js.map