@asentum/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +202 -0
- package/dist/index.d.ts +307 -0
- package/dist/index.js +444 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/client.ts +272 -0
- package/src/contract.ts +138 -0
- package/src/index.ts +75 -0
- package/src/wallet.ts +352 -0
package/src/wallet.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// Copyright 2026 The AsentumChain Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AsentumWallet — a keypair-backed client that can sign and submit
|
|
6
|
+
* transactions. Extends AsentumClient with signing capabilities.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* // Create a random wallet
|
|
11
|
+
* const wallet = AsentumWallet.create(client);
|
|
12
|
+
*
|
|
13
|
+
* // Or from an existing private key (hex)
|
|
14
|
+
* const wallet = AsentumWallet.fromSecretKey(client, '0x...');
|
|
15
|
+
*
|
|
16
|
+
* // Or from a 32-byte seed
|
|
17
|
+
* const wallet = AsentumWallet.fromSeed(client, seed);
|
|
18
|
+
*
|
|
19
|
+
* // Send ASE
|
|
20
|
+
* const tx = await wallet.sendTransfer({ to: '0x...', amount: '1.5' });
|
|
21
|
+
*
|
|
22
|
+
* // Deploy a contract
|
|
23
|
+
* const contract = await wallet.deploy('function init() { ... }');
|
|
24
|
+
*
|
|
25
|
+
* // Call a contract method (write)
|
|
26
|
+
* const tx2 = await wallet.sendCall({
|
|
27
|
+
* to: contract.address,
|
|
28
|
+
* method: 'transfer',
|
|
29
|
+
* args: ['0xbob...', '100'],
|
|
30
|
+
* });
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
generateKeypair,
|
|
35
|
+
addressFromPublicKey,
|
|
36
|
+
type Dilithium3Keypair,
|
|
37
|
+
} from '@asentum/crypto';
|
|
38
|
+
import {
|
|
39
|
+
SignedTransactionSchema,
|
|
40
|
+
TransactionBodySchema,
|
|
41
|
+
} from '@asentum/types';
|
|
42
|
+
import { blake3, sign } from '@asentum/crypto';
|
|
43
|
+
import { AsentumClient, type TxResult } from './client.js';
|
|
44
|
+
import { AsentumContract } from './contract.js';
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Hex helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
51
|
+
let hex = '0x';
|
|
52
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
53
|
+
hex += (bytes[i] ?? 0).toString(16).padStart(2, '0');
|
|
54
|
+
}
|
|
55
|
+
return hex;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
59
|
+
let h = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
60
|
+
if (h.length % 2 !== 0) h = '0' + h;
|
|
61
|
+
const out = new Uint8Array(h.length / 2);
|
|
62
|
+
for (let i = 0; i < out.length; i++) {
|
|
63
|
+
out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hexToAddress20(hex: string): Uint8Array {
|
|
69
|
+
const bytes = hexToBytes(hex);
|
|
70
|
+
if (bytes.length !== 20) throw new Error(`expected 20-byte address, got ${bytes.length}`);
|
|
71
|
+
return bytes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Amount parsing
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse an ASE amount string into wei (bigint).
|
|
80
|
+
* Accepts: "1", "1.5", "0.001", or a raw wei integer string.
|
|
81
|
+
*/
|
|
82
|
+
export function parseAse(amount: string | number | bigint): bigint {
|
|
83
|
+
if (typeof amount === 'bigint') return amount;
|
|
84
|
+
if (typeof amount === 'number') return BigInt(Math.floor(amount * 1e18));
|
|
85
|
+
const s = amount.trim();
|
|
86
|
+
if (/^\d+$/.test(s)) {
|
|
87
|
+
// If it's a very large integer, treat as wei. Otherwise ASE.
|
|
88
|
+
if (s.length > 18) return BigInt(s); // likely wei
|
|
89
|
+
return BigInt(s) * 1000000000000000000n; // ASE → wei
|
|
90
|
+
}
|
|
91
|
+
if (/^\d+\.\d{1,18}$/.test(s)) {
|
|
92
|
+
const [whole, frac = ''] = s.split('.');
|
|
93
|
+
const padded = frac.padEnd(18, '0').slice(0, 18);
|
|
94
|
+
return BigInt(whole + padded);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`invalid amount: "${amount}" — use "1", "1.5", or a wei integer`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format a wei bigint as a human-readable ASE string.
|
|
101
|
+
*/
|
|
102
|
+
export function formatAse(wei: bigint | string): string {
|
|
103
|
+
const w = typeof wei === 'bigint' ? wei : BigInt(wei);
|
|
104
|
+
const whole = w / 1000000000000000000n;
|
|
105
|
+
const frac = w % 1000000000000000000n;
|
|
106
|
+
if (frac === 0n) return whole.toString();
|
|
107
|
+
const fracStr = frac.toString().padStart(18, '0').replace(/0+$/, '');
|
|
108
|
+
return `${whole}.${fracStr}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Transaction building
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export interface SendTransferOpts {
|
|
116
|
+
/** Recipient address (0x-prefixed, 40 hex chars). */
|
|
117
|
+
to: string;
|
|
118
|
+
/** Amount in ASE (e.g. "1.5") or wei (bigint). */
|
|
119
|
+
amount: string | number | bigint;
|
|
120
|
+
/** Override the gas limit. Default: 21,000. */
|
|
121
|
+
gasLimit?: bigint;
|
|
122
|
+
/** Override the nonce. Default: auto-fetched from the chain. */
|
|
123
|
+
nonce?: bigint;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SendCallOpts {
|
|
127
|
+
/** Contract address (0x-prefixed, 40 hex chars). */
|
|
128
|
+
to: string;
|
|
129
|
+
/** Method name to call on the contract. */
|
|
130
|
+
method: string;
|
|
131
|
+
/** Method arguments (passed as JSON to the contract). */
|
|
132
|
+
args?: unknown[];
|
|
133
|
+
/** ASE to attach to the call (default: 0). */
|
|
134
|
+
value?: string | number | bigint;
|
|
135
|
+
/** Override the gas limit. Default: 2,000,000. */
|
|
136
|
+
gasLimit?: bigint;
|
|
137
|
+
/** Override the nonce. Default: auto-fetched. */
|
|
138
|
+
nonce?: bigint;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface DeployOpts {
|
|
142
|
+
/** The JavaScript source code of the contract. */
|
|
143
|
+
source: string;
|
|
144
|
+
/** ASE to attach to the deploy (default: 0). */
|
|
145
|
+
value?: string | number | bigint;
|
|
146
|
+
/** Override the gas limit. Default: based on source length. */
|
|
147
|
+
gasLimit?: bigint;
|
|
148
|
+
/** Override the nonce. Default: auto-fetched. */
|
|
149
|
+
nonce?: bigint;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface SentTx {
|
|
153
|
+
/** The transaction hash (0x-prefixed). */
|
|
154
|
+
hash: string;
|
|
155
|
+
/** Wait for the transaction to be included in a block. */
|
|
156
|
+
wait(timeoutMs?: number): Promise<import('./client.js').Receipt>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// AsentumWallet
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export class AsentumWallet {
|
|
164
|
+
readonly client: AsentumClient;
|
|
165
|
+
readonly keypair: Dilithium3Keypair;
|
|
166
|
+
readonly address: string;
|
|
167
|
+
|
|
168
|
+
private constructor(client: AsentumClient, keypair: Dilithium3Keypair) {
|
|
169
|
+
this.client = client;
|
|
170
|
+
this.keypair = keypair;
|
|
171
|
+
this.address = addressFromPublicKey(keypair.publicKey);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Create a wallet with a random Dilithium3 keypair. */
|
|
175
|
+
static create(client: AsentumClient): AsentumWallet {
|
|
176
|
+
const seed = new Uint8Array(32);
|
|
177
|
+
crypto.getRandomValues(seed);
|
|
178
|
+
const keypair = generateKeypair(seed);
|
|
179
|
+
return new AsentumWallet(client, keypair);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Create a wallet from a 32-byte seed. */
|
|
183
|
+
static fromSeed(client: AsentumClient, seed: Uint8Array): AsentumWallet {
|
|
184
|
+
if (seed.length < 32) throw new Error('seed must be at least 32 bytes');
|
|
185
|
+
const keypair = generateKeypair(seed.slice(0, 32));
|
|
186
|
+
return new AsentumWallet(client, keypair);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Create a wallet from a hex-encoded secret key. */
|
|
190
|
+
static fromSecretKey(client: AsentumClient, secretKeyHex: string, publicKeyHex: string): AsentumWallet {
|
|
191
|
+
return new AsentumWallet(client, {
|
|
192
|
+
secretKey: hexToBytes(secretKeyHex),
|
|
193
|
+
publicKey: hexToBytes(publicKeyHex),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Get the secret key as a hex string (for export/backup). */
|
|
198
|
+
get secretKeyHex(): string {
|
|
199
|
+
return bytesToHex(this.keypair.secretKey);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Get the public key as a hex string. */
|
|
203
|
+
get publicKeyHex(): string {
|
|
204
|
+
return bytesToHex(this.keypair.publicKey);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Get the current balance in wei. */
|
|
208
|
+
async getBalance(): Promise<bigint> {
|
|
209
|
+
const bal = await this.client.getBalance(this.address);
|
|
210
|
+
return BigInt(bal.balance);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Get the current nonce. */
|
|
214
|
+
async getNonce(): Promise<bigint> {
|
|
215
|
+
return this.client.getNonce(this.address);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Request test ASE from the faucet. */
|
|
219
|
+
async requestFaucet(amount: string = '100000000000000000000'): Promise<TxResult> {
|
|
220
|
+
return this.client.faucet(this.address, amount);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Sending transactions
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
/** Send ASE to another address. */
|
|
228
|
+
async sendTransfer(opts: SendTransferOpts): Promise<SentTx> {
|
|
229
|
+
const chainId = await this.client.getChainId();
|
|
230
|
+
const nonce = opts.nonce ?? await this.getNonce();
|
|
231
|
+
const value = parseAse(opts.amount);
|
|
232
|
+
|
|
233
|
+
const body = {
|
|
234
|
+
chainId,
|
|
235
|
+
nonce,
|
|
236
|
+
maxPriorityFeePerGas: 0n,
|
|
237
|
+
maxFeePerGas: 1n,
|
|
238
|
+
gasLimit: opts.gasLimit ?? 21_000n,
|
|
239
|
+
to: hexToAddress20(opts.to),
|
|
240
|
+
value,
|
|
241
|
+
data: new Uint8Array(0),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return this.signAndSubmit(body);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Call a method on a deployed contract (state-changing). */
|
|
248
|
+
async sendCall(opts: SendCallOpts): Promise<SentTx> {
|
|
249
|
+
const chainId = await this.client.getChainId();
|
|
250
|
+
const nonce = opts.nonce ?? await this.getNonce();
|
|
251
|
+
const data = new TextEncoder().encode(
|
|
252
|
+
JSON.stringify({ method: opts.method, args: opts.args ?? [] }),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const body = {
|
|
256
|
+
chainId,
|
|
257
|
+
nonce,
|
|
258
|
+
maxPriorityFeePerGas: 0n,
|
|
259
|
+
maxFeePerGas: 1n,
|
|
260
|
+
gasLimit: opts.gasLimit ?? 2_000_000n,
|
|
261
|
+
to: hexToAddress20(opts.to),
|
|
262
|
+
value: opts.value ? parseAse(opts.value) : 0n,
|
|
263
|
+
data,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return this.signAndSubmit(body);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Deploy a new smart contract. Returns an AsentumContract instance. */
|
|
270
|
+
async deploy(sourceOrOpts: string | DeployOpts): Promise<AsentumContract> {
|
|
271
|
+
const opts: DeployOpts = typeof sourceOrOpts === 'string'
|
|
272
|
+
? { source: sourceOrOpts }
|
|
273
|
+
: sourceOrOpts;
|
|
274
|
+
|
|
275
|
+
const chainId = await this.client.getChainId();
|
|
276
|
+
const nonce = opts.nonce ?? await this.getNonce();
|
|
277
|
+
const data = new TextEncoder().encode(opts.source);
|
|
278
|
+
const gasLimit = opts.gasLimit ?? BigInt(Math.max(2_000_000, 1000 * opts.source.length));
|
|
279
|
+
|
|
280
|
+
const body = {
|
|
281
|
+
chainId,
|
|
282
|
+
nonce,
|
|
283
|
+
maxPriorityFeePerGas: 0n,
|
|
284
|
+
maxFeePerGas: 1n,
|
|
285
|
+
gasLimit,
|
|
286
|
+
to: new Uint8Array(20), // zero address = deploy
|
|
287
|
+
value: opts.value ? parseAse(opts.value) : 0n,
|
|
288
|
+
data,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const sentTx = await this.signAndSubmit(body);
|
|
292
|
+
|
|
293
|
+
// Derive the contract address: blake3(sender || nonce), take first 20 bytes.
|
|
294
|
+
const senderBytes = hexToAddress20(this.address);
|
|
295
|
+
const nonceBytes = new Uint8Array(8);
|
|
296
|
+
new DataView(nonceBytes.buffer).setBigUint64(0, nonce, true);
|
|
297
|
+
const combined = new Uint8Array(20 + 8);
|
|
298
|
+
combined.set(senderBytes);
|
|
299
|
+
combined.set(nonceBytes, 20);
|
|
300
|
+
const hash = blake3(combined);
|
|
301
|
+
const contractAddress = bytesToHex(hash.slice(0, 20));
|
|
302
|
+
|
|
303
|
+
return new AsentumContract(this.client, contractAddress, sentTx, this);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Internal: sign + submit
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
private async signAndSubmit(body: {
|
|
311
|
+
chainId: bigint;
|
|
312
|
+
nonce: bigint;
|
|
313
|
+
maxPriorityFeePerGas: bigint;
|
|
314
|
+
maxFeePerGas: bigint;
|
|
315
|
+
gasLimit: bigint;
|
|
316
|
+
to: Uint8Array;
|
|
317
|
+
value: bigint;
|
|
318
|
+
data: Uint8Array;
|
|
319
|
+
}): Promise<SentTx> {
|
|
320
|
+
// Serialize → hash → sign → encode → submit
|
|
321
|
+
const bodyBytes = TransactionBodySchema.serialize(body);
|
|
322
|
+
const digest = blake3(bodyBytes);
|
|
323
|
+
const signature = sign(digest, this.keypair.secretKey);
|
|
324
|
+
const signedTx = {
|
|
325
|
+
body,
|
|
326
|
+
publicKey: this.keypair.publicKey,
|
|
327
|
+
signature,
|
|
328
|
+
};
|
|
329
|
+
const rawTxHex = bytesToHex(SignedTransactionSchema.serialize(signedTx));
|
|
330
|
+
const result = await this.client.submitRawTx(rawTxHex);
|
|
331
|
+
|
|
332
|
+
if (!result.accepted) {
|
|
333
|
+
throw new Error(`transaction rejected: ${result.reason || 'unknown reason'}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const txHash = result.txHash ?? '(unknown)';
|
|
337
|
+
const client = this.client;
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
hash: txHash,
|
|
341
|
+
async wait(timeoutMs = 15_000): Promise<import('./client.js').Receipt> {
|
|
342
|
+
const start = Date.now();
|
|
343
|
+
while (Date.now() - start < timeoutMs) {
|
|
344
|
+
const receipt = await client.getReceipt(txHash);
|
|
345
|
+
if (receipt) return receipt;
|
|
346
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
347
|
+
}
|
|
348
|
+
throw new Error(`transaction ${txHash} not confirmed within ${timeoutMs}ms`);
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|