@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/CHANGELOG.md +66 -0
- package/README.md +280 -142
- package/dist/index.d.ts +977 -174
- package/dist/index.js +1335 -308
- package/dist/index.js.map +1 -1
- package/package.json +32 -11
- package/src/client.ts +0 -272
- package/src/contract.ts +0 -138
- package/src/index.ts +0 -75
- package/src/wallet.ts +0 -352
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
|
-
|
|
5
|
-
|
|
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
|
|
474
|
+
// Chain and health
|
|
9
475
|
// ---------------------------------------------------------------------------
|
|
10
|
-
/**
|
|
11
|
-
async
|
|
12
|
-
const
|
|
13
|
-
return {
|
|
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
|
-
/**
|
|
34
|
-
async
|
|
35
|
-
return this.
|
|
481
|
+
/** Newest block height on the best chain. */
|
|
482
|
+
async getTipNumber(opts = {}) {
|
|
483
|
+
return (await this.getHeads(opts)).tip;
|
|
36
484
|
}
|
|
37
|
-
/**
|
|
38
|
-
async
|
|
39
|
-
|
|
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
|
-
/**
|
|
43
|
-
async
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
/**
|
|
53
|
-
async
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
507
|
+
return this.chainIdCache;
|
|
67
508
|
}
|
|
68
|
-
/**
|
|
69
|
-
async
|
|
70
|
-
return this.
|
|
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
|
-
/**
|
|
73
|
-
async
|
|
74
|
-
|
|
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
|
-
/**
|
|
77
|
-
async
|
|
78
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
90
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
/**
|
|
103
|
-
async
|
|
104
|
-
|
|
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
|
-
//
|
|
587
|
+
// Blocks
|
|
108
588
|
// ---------------------------------------------------------------------------
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
653
|
+
body: { rawTx: rawTxHex },
|
|
654
|
+
signal: opts.signal,
|
|
655
|
+
retries: opts.retries,
|
|
656
|
+
retryOnServerError: false
|
|
115
657
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
961
|
+
body: { jsonrpc: "2.0", id: 1, method, params },
|
|
962
|
+
signal: opts.signal,
|
|
963
|
+
retries: opts.retries
|
|
134
964
|
});
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
*
|
|
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
|
|
226
|
-
|
|
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
|
|
231
|
-
*
|
|
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(
|
|
1060
|
+
async waitForDeploy(timeoutOrOpts) {
|
|
234
1061
|
if (!this.deployTx) {
|
|
235
|
-
throw new
|
|
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(
|
|
1064
|
+
return this.deployTx.wait(timeoutOrOpts);
|
|
238
1065
|
}
|
|
239
1066
|
};
|
|
240
1067
|
|
|
241
|
-
// src/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
1145
|
+
if (typeof amount === "number") return aseToWei(amount);
|
|
266
1146
|
const s = amount.trim();
|
|
267
|
-
if (/^\d+$/.test(s))
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
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
|
-
|
|
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.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return new _AsentumWallet(client,
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
static fromSeed(client, seed) {
|
|
304
|
-
if (seed.length < 32) throw new
|
|
305
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
1226
|
+
/** The public key as hex. */
|
|
320
1227
|
get publicKeyHex() {
|
|
321
1228
|
return bytesToHex(this.keypair.publicKey);
|
|
322
1229
|
}
|
|
323
|
-
/**
|
|
1230
|
+
/** Balance in wei. */
|
|
324
1231
|
async getBalance() {
|
|
325
|
-
|
|
326
|
-
return BigInt(bal.balance);
|
|
1232
|
+
return BigInt((await this.client.getBalance(this.address)).balance);
|
|
327
1233
|
}
|
|
328
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
1253
|
+
// Send (sign + submit)
|
|
338
1254
|
// ---------------------------------------------------------------------------
|
|
339
|
-
/** Send ASE
|
|
1255
|
+
/** Send ASE. Throws TxRejectedError when the node does not accept it. */
|
|
340
1256
|
async sendTransfer(opts) {
|
|
341
|
-
|
|
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
|
|
1259
|
+
/** Call a contract method (state-changing). Throws TxRejectedError when the node does not accept it. */
|
|
357
1260
|
async sendCall(opts) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
/**
|
|
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
|
|
380
|
-
const
|
|
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:
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
throw new
|
|
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
|
-
|
|
422
|
-
|
|
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
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|