@hardkas/toolkit 0.11.2-alpha → 0.11.6-alpha

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.
Files changed (56) hide show
  1. package/dist/fee-estimator.d.ts +9 -0
  2. package/dist/fee-estimator.d.ts.map +1 -0
  3. package/dist/fee-estimator.js +32 -0
  4. package/dist/fee-estimator.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/indexer.d.ts +1 -0
  10. package/dist/indexer.d.ts.map +1 -1
  11. package/dist/indexer.js +7 -0
  12. package/dist/indexer.js.map +1 -1
  13. package/dist/query-api.d.ts +22 -0
  14. package/dist/query-api.d.ts.map +1 -0
  15. package/dist/query-api.js +116 -0
  16. package/dist/query-api.js.map +1 -0
  17. package/dist/snapshot/serialization.d.ts +0 -9
  18. package/dist/snapshot/serialization.d.ts.map +1 -1
  19. package/dist/snapshot/serialization.js +0 -12
  20. package/dist/snapshot/serialization.js.map +1 -1
  21. package/dist/subscriptions.d.ts +22 -0
  22. package/dist/subscriptions.d.ts.map +1 -0
  23. package/dist/subscriptions.js +91 -0
  24. package/dist/subscriptions.js.map +1 -0
  25. package/dist/wallet.d.ts +71 -1
  26. package/dist/wallet.d.ts.map +1 -1
  27. package/dist/wallet.js +296 -44
  28. package/dist/wallet.js.map +1 -1
  29. package/package.json +24 -9
  30. package/.turbo/turbo-build.log +0 -4
  31. package/src/dag/consensus.ts +0 -150
  32. package/src/dag/index.ts +0 -73
  33. package/src/dag/store.ts +0 -40
  34. package/src/dag/topology.ts +0 -108
  35. package/src/dag/types.ts +0 -20
  36. package/src/index.ts +0 -10
  37. package/src/indexer.ts +0 -134
  38. package/src/jobs.ts +0 -63
  39. package/src/payment.ts +0 -100
  40. package/src/silver/index.ts +0 -102
  41. package/src/silver/templates.ts +0 -27
  42. package/src/silver/types.ts +0 -37
  43. package/src/snapshot/backends/FsSnapshotBackend.ts +0 -100
  44. package/src/snapshot/backends/MemorySnapshotBackend.ts +0 -65
  45. package/src/snapshot/index.ts +0 -102
  46. package/src/snapshot/serialization.ts +0 -35
  47. package/src/snapshot/types.ts +0 -45
  48. package/src/stores/invoice-store.ts +0 -62
  49. package/src/stores/utxo-control-store.ts +0 -78
  50. package/src/utxos.ts +0 -451
  51. package/src/wallet.ts +0 -158
  52. package/test/silver.test.ts +0 -59
  53. package/test/snapshot.test.ts +0 -99
  54. package/test/toolkit.test.ts +0 -140
  55. package/tsconfig.json +0 -9
  56. package/vitest.config.ts +0 -8
package/src/utxos.ts DELETED
@@ -1,451 +0,0 @@
1
- import { Utxo } from "@hardkas/query";
2
-
3
- export interface UtxoStatistics {
4
- totalUtxos: number;
5
- availableUtxos: number;
6
- frozenUtxos: number;
7
- totalValue: bigint;
8
- availableValue: bigint;
9
- frozenValue: bigint;
10
- averageValue: bigint;
11
- largest: bigint;
12
- smallest: bigint;
13
- includeFrozen: boolean;
14
- }
15
-
16
- export interface DustAnalysis {
17
- dustUtxos: Utxo[];
18
- normalUtxos: Utxo[];
19
- totalDustValue: bigint;
20
- dustPercentage: number;
21
- }
22
-
23
- export type Recommendation = "consolidate" | "mergeDust" | "splitLarge" | "none";
24
-
25
- export interface WalletAnalysis {
26
- utxoCount: number;
27
- fragmentationScore: number; // 0.0 to 1.0 (e.g. dustCount / totalUtxos)
28
- dustCount: number;
29
- totalValue: bigint;
30
- recommendedActions: Recommendation[];
31
- }
32
-
33
- export interface ConsolidationPlan {
34
- strategy: string;
35
- inputs: Utxo[];
36
- outputs: any[]; // Target outputs (usually one consolidated output back to the wallet)
37
- estimatedFee: bigint;
38
- estimatedSavings: bigint; // Approximated future fee savings
39
- warnings: string[];
40
- }
41
-
42
- export interface SplitPlan extends ConsolidationPlan {
43
- strategy: "split";
44
- }
45
-
46
- export interface MergePlan extends ConsolidationPlan {
47
- strategy: "merge";
48
- }
49
-
50
- export interface SweepPlan extends ConsolidationPlan {
51
- strategy: "sweep";
52
- }
53
-
54
- import { UtxoControlStore, UtxoControlState } from './stores/utxo-control-store.js';
55
-
56
- export interface UtxoListOpts { includeFrozen?: boolean | undefined }
57
-
58
- export class WalletUtxoApi {
59
- constructor(
60
- private readonly fetchUtxos: () => Promise<Utxo[]>,
61
- private readonly address: () => Promise<string>,
62
- private readonly estimateFee: (inputs: number, outputs: number) => Promise<bigint>,
63
- private readonly store: UtxoControlStore
64
- ) {}
65
-
66
- /**
67
- * List UTXOs currently available in the wallet.
68
- * Frozen UTXOs are excluded by default unless includeFrozen: true.
69
- */
70
- async list(opts?: UtxoListOpts): Promise<Utxo[]> {
71
- const all = await this.fetchUtxos();
72
- const state = this.store.getState();
73
- if (opts?.includeFrozen) {
74
- return all;
75
- }
76
- return all.filter(u => !state.frozen[u.transactionId] && !state.frozen[`${u.transactionId}:${u.outputIndex}`]);
77
- }
78
-
79
- /**
80
- * Return basic statistics about the UTXO set.
81
- */
82
- async statistics(opts?: UtxoListOpts): Promise<UtxoStatistics> {
83
- const allUtxos = await this.fetchUtxos();
84
- const state = this.store.getState();
85
-
86
- let totalUtxos = 0;
87
- let availableUtxos = 0;
88
- let frozenUtxos = 0;
89
-
90
- let totalValue = 0n;
91
- let availableValue = 0n;
92
- let frozenValue = 0n;
93
-
94
- let largest = -1n;
95
- let smallest = -1n;
96
-
97
- const includeFrozen = opts?.includeFrozen ?? false;
98
-
99
- for (const u of allUtxos) {
100
- const amt = BigInt(u.amountSompi);
101
- const isFrozen = !!(state.frozen[u.transactionId] || state.frozen[`${u.transactionId}:${u.outputIndex}`]);
102
-
103
- totalUtxos++;
104
- totalValue += amt;
105
-
106
- if (isFrozen) {
107
- frozenUtxos++;
108
- frozenValue += amt;
109
- } else {
110
- availableUtxos++;
111
- availableValue += amt;
112
- }
113
-
114
- // Largest/smallest only counts what is included
115
- if (includeFrozen || !isFrozen) {
116
- if (largest === -1n || amt > largest) largest = amt;
117
- if (smallest === -1n || amt < smallest) smallest = amt;
118
- }
119
- }
120
-
121
- const countForAvg = includeFrozen ? totalUtxos : availableUtxos;
122
- const valueForAvg = includeFrozen ? totalValue : availableValue;
123
-
124
- return {
125
- totalUtxos,
126
- availableUtxos,
127
- frozenUtxos,
128
- totalValue,
129
- availableValue,
130
- frozenValue,
131
- averageValue: countForAvg === 0 ? 0n : valueForAvg / BigInt(countForAvg),
132
- largest: largest === -1n ? 0n : largest,
133
- smallest: smallest === -1n ? 0n : smallest,
134
- includeFrozen
135
- };
136
- }
137
-
138
- /**
139
- * Classify UTXOs into dust and normal based on a threshold.
140
- * Default threshold: 10 KAS (1_000_000_000 sompi).
141
- */
142
- async analyzeDust(opts?: { thresholdSompi?: bigint, includeFrozen?: boolean | undefined }): Promise<DustAnalysis> {
143
- const threshold = opts?.thresholdSompi ?? 1_000_000_000n;
144
- const utxos = await this.list({ includeFrozen: opts?.includeFrozen });
145
-
146
- const dustUtxos: Utxo[] = [];
147
- const normalUtxos: Utxo[] = [];
148
- let totalDustValue = 0n;
149
-
150
- for (const u of utxos) {
151
- const amt = BigInt(u.amountSompi);
152
- if (amt < threshold) {
153
- dustUtxos.push(u);
154
- totalDustValue += amt;
155
- } else {
156
- normalUtxos.push(u);
157
- }
158
- }
159
-
160
- return {
161
- dustUtxos,
162
- normalUtxos,
163
- totalDustValue,
164
- dustPercentage: utxos.length > 0 ? dustUtxos.length / utxos.length : 0
165
- };
166
- }
167
-
168
- /**
169
- * High-level diagnostic analysis of the wallet's UTXO health.
170
- */
171
- async analyze(opts?: UtxoListOpts): Promise<WalletAnalysis> {
172
- const stats = await this.statistics(opts);
173
- const dust = await this.analyzeDust(opts);
174
-
175
- const fragmentationScore = dust.dustPercentage;
176
- const actions: Recommendation[] = [];
177
-
178
- // We base actions on available counts
179
- const activeUtxoCount = opts?.includeFrozen ? stats.totalUtxos : stats.availableUtxos;
180
-
181
- if (fragmentationScore > 0.5 && activeUtxoCount > 10) {
182
- actions.push("consolidate");
183
- } else if (dust.dustUtxos.length > 5) {
184
- actions.push("mergeDust");
185
- }
186
-
187
- // Simplistic threshold for splitLarge: if there are very few UTXOs but high value
188
- if (activeUtxoCount < 3 && stats.largest > 10_000_000_000n) {
189
- actions.push("splitLarge");
190
- }
191
-
192
- if (actions.length === 0) {
193
- actions.push("none");
194
- }
195
-
196
- return {
197
- utxoCount: activeUtxoCount,
198
- fragmentationScore,
199
- dustCount: dust.dustUtxos.length,
200
- totalValue: opts?.includeFrozen ? stats.totalValue : stats.availableValue,
201
- recommendedActions: actions
202
- };
203
- }
204
-
205
- /**
206
- * Plan a UTXO management operation without executing it.
207
- */
208
- async plan(opts: { strategy: "consolidation" | "split" | "sweep", includeFrozen?: boolean | undefined }): Promise<ConsolidationPlan> {
209
- const utxos = await this.list({ includeFrozen: opts.includeFrozen });
210
- const myAddress = await this.address();
211
-
212
- if (opts.strategy === "consolidation") {
213
- // In a real scenario, the CoinSelector would chunk this to respect mass limits.
214
- // For Phase 1A, we will just select up to 84 (a safe mass limit approximation for Kaspa).
215
- const MAX_INPUTS = 84;
216
-
217
- // Sort smallest first to consolidate dust
218
- const sorted = [...utxos].sort((a, b) => (BigInt(a.amountSompi) < BigInt(b.amountSompi) ? -1 : 1));
219
- const inputs = sorted.slice(0, MAX_INPUTS);
220
-
221
- const estimatedFee = await this.estimateFee(inputs.length, 1);
222
-
223
- const totalInputValue = inputs.reduce((sum, u) => sum + BigInt(u.amountSompi), 0n);
224
-
225
- const warnings: string[] = [];
226
- if (inputs.length < 2) {
227
- warnings.push("Not enough UTXOs to consolidate.");
228
- }
229
- if (totalInputValue <= estimatedFee) {
230
- warnings.push("Consolidation fee exceeds the value of the selected UTXOs.");
231
- }
232
-
233
- // If we spent these 84 UTXOs individually in the future, we would pay for 1 input each time.
234
- // 84 txs with 1 input vs 1 tx with 84 inputs.
235
- // This is a naive calculation for "estimated savings".
236
- const individualFee = await this.estimateFee(1, 2);
237
- const estimatedSavings = (individualFee * BigInt(inputs.length)) - estimatedFee;
238
-
239
- return {
240
- strategy: opts.strategy,
241
- inputs,
242
- outputs: [{ address: myAddress, amount: totalInputValue - estimatedFee }],
243
- estimatedFee,
244
- estimatedSavings: estimatedSavings > 0n ? estimatedSavings : 0n,
245
- warnings
246
- };
247
- }
248
-
249
- throw new Error(`Strategy '${opts.strategy}' not fully implemented yet.`);
250
- }
251
-
252
- /**
253
- * Convenience alias for planning a consolidation.
254
- */
255
- async consolidate(opts?: { includeFrozen?: boolean | undefined }): Promise<ConsolidationPlan> {
256
- return this.plan({ strategy: "consolidation", includeFrozen: opts?.includeFrozen });
257
- }
258
-
259
- private _assertNotFrozen(utxoId: string, includeFrozen?: boolean) {
260
- if (!includeFrozen && this.store.getState().frozen[utxoId]) {
261
- throw new Error(`UTXO ${utxoId} is frozen and cannot be planned for execution`);
262
- }
263
- }
264
-
265
- /**
266
- * Plans the splitting of a specific large UTXO.
267
- * Supports symmetric splits (intoCount) OR asymmetric splits (outputs).
268
- */
269
- async splitPlan(opts: {
270
- utxoId: string;
271
- intoCount?: number;
272
- outputs?: { amountSompi: bigint }[];
273
- includeFrozen?: boolean | undefined;
274
- }): Promise<SplitPlan> {
275
- this._assertNotFrozen(opts.utxoId, opts.includeFrozen);
276
- const utxos = await this.list({ includeFrozen: opts.includeFrozen });
277
- const targetUtxo = utxos.find(u => u.transactionId === opts.utxoId || `${u.transactionId}:${u.outputIndex}` === opts.utxoId);
278
-
279
- if (!targetUtxo) {
280
- throw new Error(`UTXO ${opts.utxoId} not found in wallet.`);
281
- }
282
-
283
- const myAddress = await this.address();
284
- const warnings: string[] = [];
285
- const planOutputs: { address: string, amount: bigint }[] = [];
286
-
287
- const targetAmount = BigInt(targetUtxo.amountSompi);
288
- let outputCount = 0;
289
-
290
- if (opts.intoCount !== undefined && opts.outputs === undefined) {
291
- outputCount = opts.intoCount;
292
- if (outputCount < 2) throw new Error("splitPlan requires intoCount >= 2.");
293
-
294
- const estimatedFee = await this.estimateFee(1, outputCount);
295
- const remainingAmount = targetAmount - estimatedFee;
296
-
297
- if (remainingAmount <= 0n) {
298
- throw new Error("Target UTXO does not have enough funds to cover the split and fees.");
299
- }
300
-
301
- const chunkAmount = remainingAmount / BigInt(outputCount);
302
- for (let i = 0; i < outputCount; i++) {
303
- planOutputs.push({ address: myAddress, amount: chunkAmount });
304
- }
305
- } else if (opts.outputs !== undefined && opts.intoCount === undefined) {
306
- outputCount = opts.outputs.length;
307
- if (outputCount < 2) throw new Error("splitPlan requires outputs.length >= 2.");
308
-
309
- const estimatedFee = await this.estimateFee(1, outputCount + 1); // +1 for change
310
- let sumOutputs = 0n;
311
-
312
- for (const out of opts.outputs) {
313
- sumOutputs += out.amountSompi;
314
- planOutputs.push({ address: myAddress, amount: out.amountSompi });
315
- }
316
-
317
- const changeAmount = targetAmount - sumOutputs - estimatedFee;
318
- if (changeAmount < 0n) {
319
- throw new Error("Target UTXO does not have enough funds to cover the requested outputs and fees.");
320
- }
321
-
322
- if (changeAmount > 0n) {
323
- // Add change output
324
- planOutputs.push({ address: myAddress, amount: changeAmount });
325
- }
326
- } else {
327
- throw new Error("splitPlan requires exactly one of 'intoCount' or 'outputs' to be defined.");
328
- }
329
-
330
- const estimatedFee = await this.estimateFee(1, planOutputs.length);
331
-
332
- return {
333
- strategy: "split",
334
- inputs: [targetUtxo],
335
- outputs: planOutputs,
336
- estimatedFee,
337
- estimatedSavings: 0n,
338
- warnings
339
- };
340
- }
341
-
342
- /**
343
- * Plans the explicit merging of specific UTXOs.
344
- */
345
- async mergePlan(opts: {
346
- utxoIds: string[];
347
- includeFrozen?: boolean | undefined;
348
- }): Promise<MergePlan> {
349
- for (const id of opts.utxoIds) {
350
- this._assertNotFrozen(id, opts.includeFrozen);
351
- }
352
- const utxos = await this.list({ includeFrozen: opts.includeFrozen });
353
- const inputs: Utxo[] = [];
354
-
355
- for (const id of opts.utxoIds) {
356
- const u = utxos.find(utxo => utxo.transactionId === id || `${utxo.transactionId}:${utxo.outputIndex}` === id);
357
- if (!u) {
358
- throw new Error(`UTXO ${id} not found in wallet.`);
359
- }
360
- inputs.push(u);
361
- }
362
-
363
- if (inputs.length < 2) {
364
- throw new Error("mergePlan requires at least 2 UTXOs to merge.");
365
- }
366
-
367
- const estimatedFee = await this.estimateFee(inputs.length, 1);
368
- const totalInputValue = inputs.reduce((sum, u) => sum + BigInt(u.amountSompi), 0n);
369
- const myAddress = await this.address();
370
-
371
- const warnings: string[] = [];
372
- if (totalInputValue <= estimatedFee) {
373
- warnings.push("Merge fee exceeds the value of the selected UTXOs.");
374
- }
375
-
376
- const individualFee = await this.estimateFee(1, 2);
377
- const estimatedSavings = (individualFee * BigInt(inputs.length)) - estimatedFee;
378
-
379
- return {
380
- strategy: "merge",
381
- inputs,
382
- outputs: [{ address: myAddress, amount: totalInputValue > estimatedFee ? totalInputValue - estimatedFee : 0n }],
383
- estimatedFee,
384
- estimatedSavings: estimatedSavings > 0n ? estimatedSavings : 0n,
385
- warnings
386
- };
387
- }
388
-
389
- /**
390
- * Plans sweeping the entire wallet to a single destination address.
391
- */
392
- async sweepPlan(opts: {
393
- destinationAddress: string;
394
- includeFrozen?: boolean | undefined;
395
- }): Promise<SweepPlan> {
396
- const utxos = await this.list({ includeFrozen: opts.includeFrozen });
397
-
398
- if (utxos.length === 0) {
399
- throw new Error("Cannot sweep an empty wallet.");
400
- }
401
-
402
- const estimatedFee = await this.estimateFee(utxos.length, 1);
403
- const totalInputValue = utxos.reduce((sum, u) => sum + BigInt(u.amountSompi), 0n);
404
- const warnings: string[] = [];
405
-
406
- if (totalInputValue <= estimatedFee) {
407
- warnings.push("Sweep fee exceeds the entire wallet balance.");
408
- }
409
-
410
- return {
411
- strategy: "sweep",
412
- inputs: utxos,
413
- outputs: [{ address: opts.destinationAddress, amount: totalInputValue > estimatedFee ? totalInputValue - estimatedFee : 0n }],
414
- estimatedFee,
415
- estimatedSavings: 0n,
416
- warnings
417
- };
418
- }
419
-
420
- // --- Coin Control Persistence ---
421
-
422
- async controlState(): Promise<UtxoControlState> {
423
- return this.store.getState();
424
- }
425
-
426
- async freeze(utxoId: string, reason?: string): Promise<void> {
427
- this.store.freeze(utxoId, reason);
428
- }
429
-
430
- async unfreeze(utxoId: string): Promise<void> {
431
- this.store.unfreeze(utxoId);
432
- }
433
-
434
- public labels = {
435
- set: async (utxoId: string, label: string): Promise<void> => {
436
- this.store.setLabel(utxoId, label);
437
- },
438
- get: async (utxoId: string): Promise<string | undefined> => {
439
- return this.store.getLabel(utxoId);
440
- }
441
- };
442
-
443
- public notes = {
444
- set: async (utxoId: string, note: string): Promise<void> => {
445
- this.store.setNote(utxoId, note);
446
- },
447
- get: async (utxoId: string): Promise<string | undefined> => {
448
- return this.store.getNote(utxoId);
449
- }
450
- };
451
- }
package/src/wallet.ts DELETED
@@ -1,158 +0,0 @@
1
- import { WalletManagerImpl, WalletStateStoreJson, AddressManager } from '@hardkas/accounts';
2
- import { WalletQuery, WalletQueryProvider } from '@hardkas/query';
3
- import { selectCoins, estimateFee, buildPaymentPlan } from '@hardkas/tx-builder';
4
- import { WalletUtxoApi } from './utxos.js';
5
- import { UtxoControlStore } from './stores/utxo-control-store.js';
6
- import { logger, metrics, tracer } from '@hardkas/observability';
7
-
8
- metrics.register({
9
- name: "wallet_tx_generated_total",
10
- help: "Total tx generated by wallet",
11
- type: "counter"
12
- });
13
- metrics.register({
14
- name: "wallet_tx_submitted_total",
15
- help: "Total tx submitted by wallet",
16
- type: "counter"
17
- });
18
- metrics.register({
19
- name: "wallet_tx_failed_total",
20
- help: "Total tx failed by wallet",
21
- type: "counter"
22
- });
23
-
24
- export interface WalletToolkitOptions {
25
- storePath?: string;
26
- network?: "simnet" | "testnet" | "mainnet";
27
- provider?: WalletQueryProvider;
28
- }
29
-
30
- export class WalletToolkit {
31
- private walletManager: WalletManagerImpl;
32
- private addressManager: typeof AddressManager;
33
- private walletQuery: WalletQuery;
34
-
35
- private _utxosApi: WalletUtxoApi;
36
-
37
- private constructor(
38
- public readonly name: string,
39
- private readonly store: WalletStateStoreJson,
40
- private readonly options: WalletToolkitOptions
41
- ) {
42
- this.walletManager = new WalletManagerImpl();
43
- this.addressManager = AddressManager;
44
-
45
- const dummyProvider: WalletQueryProvider = {
46
- source: "dummy",
47
- async getBalances() { return {}; },
48
- async getUtxos() { return {}; },
49
- async getHistory() { return { items: [] }; }
50
- };
51
-
52
- this.walletQuery = new WalletQuery({ provider: options.provider || dummyProvider, network: options.network || "simnet" });
53
-
54
- const utxoControlPath = options.storePath
55
- ? options.storePath.replace('.json', '-utxo-control.json')
56
- : 'default-utxo-control.json';
57
- const utxoStore = new UtxoControlStore(utxoControlPath);
58
-
59
- this._utxosApi = new WalletUtxoApi(
60
- async () => {
61
- const addr = await this.address();
62
- const res = await this.walletQuery.getUtxos([addr]);
63
- if (!res.ok) throw new Error(res.error || "Degraded query result");
64
- return res.utxos[addr] || [];
65
- },
66
- async () => this.address(),
67
- async (inputs: number, outputs: number) => BigInt(inputs * 1000 + outputs * 1000), // Dummy fee estimation
68
- utxoStore
69
- );
70
- }
71
-
72
- public static open(name: string, options: WalletToolkitOptions = {}): WalletToolkit {
73
- const store = new WalletStateStoreJson({ filePath: options.storePath || 'default.json' });
74
- return new WalletToolkit(name, store, options);
75
- }
76
-
77
- public async create(): Promise<void> {
78
- this.walletManager.create({ walletId: this.name });
79
- }
80
-
81
- public async address(): Promise<string> {
82
- // High level facade: gets receive address
83
- const seedRef = this.walletManager.getSeedRef(this.name);
84
- return this.addressManager.derive({
85
- seedRef,
86
- accountIndex: 0,
87
- chain: 'receive',
88
- addressIndex: 0,
89
- network: this.options.network || 'simnet'
90
- }).address;
91
- }
92
-
93
- public async balance(): Promise<bigint> {
94
- const addr = await this.address();
95
- return (await this.walletQuery.getBalance([addr])) as any;
96
- }
97
-
98
- public get utxos(): WalletUtxoApi {
99
- return this._utxosApi;
100
- }
101
-
102
- public async history(): Promise<any[]> {
103
- const addr = await this.address();
104
- return (await this.walletQuery.getHistory({ addresses: [addr] })) as any;
105
- }
106
-
107
- public async estimateFee(opts: { to: string; amount: bigint; feeRate?: bigint }): Promise<{ fee: bigint; totalOut: bigint; plan: any }> {
108
- const addr = await this.address();
109
- const availableUtxos = await this.utxos.list();
110
-
111
- const plan = buildPaymentPlan({
112
- fromAddress: addr,
113
- outputs: [{ address: opts.to, amountSompi: opts.amount }],
114
- availableUtxos: availableUtxos as any[],
115
- feeRateSompiPerMass: opts.feeRate || 1n,
116
- changeAddress: addr
117
- });
118
-
119
- return {
120
- fee: plan.estimatedFeeSompi,
121
- totalOut: opts.amount + plan.estimatedFeeSompi,
122
- plan
123
- };
124
- }
125
-
126
- public async planSend(opts: { to: string; amount: bigint; feeRate?: bigint }): Promise<any> {
127
- try {
128
- const { plan } = await this.estimateFee(opts);
129
- metrics.inc("wallet_tx_generated_total");
130
- logger.info("Wallet tx planned", { to: opts.to });
131
- return plan;
132
- } catch (e: any) {
133
- metrics.inc("wallet_tx_failed_total");
134
- logger.error("Wallet tx planning failed", { error: e.message });
135
- throw e;
136
- }
137
- }
138
-
139
- public async sendSimulated(opts: { to: string; amount: bigint; feeRate?: bigint }): Promise<string> {
140
- const span = tracer.start("wallet.send", { to: opts.to });
141
- try {
142
- const plan = await this.planSend(opts);
143
- const hashStr = `sim_tx_${Date.now()}_${plan.inputs.length}_${plan.outputs.length}`;
144
- for (const u of plan.inputs) {
145
- await this.utxos.freeze(`${u.outpoint.transactionId}:${u.outpoint.index}`, 'simulated_send');
146
- }
147
- metrics.inc("wallet_tx_submitted_total");
148
- logger.info("Wallet tx simulated successfully", { txid: hashStr });
149
- span.end({ txid: hashStr });
150
- return hashStr;
151
- } catch (e: any) {
152
- metrics.inc("wallet_tx_failed_total");
153
- logger.error("Wallet sendSimulated failed", { error: e.message });
154
- span.fail(e);
155
- throw e;
156
- }
157
- }
158
- }
@@ -1,59 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { SilverToolkit } from '../src/index.js';
3
-
4
- describe('SilverToolkit', () => {
5
- it('should list all templates', () => {
6
- const silver = SilverToolkit.open();
7
- const templates = silver.templates();
8
- expect(templates).toContain('htlc');
9
- expect(templates).toContain('op-true');
10
- expect(templates).toContain('multisig');
11
- });
12
-
13
- it('should extract template parameters', () => {
14
- const silver = SilverToolkit.open();
15
- const tpl = silver.template('htlc');
16
- const params = tpl.parameters();
17
- expect(params).toEqual(expect.arrayContaining(['secret_hash', 'receiver_pubkey', 'locktime', 'sender_pubkey']));
18
- });
19
-
20
- it('should fill template with parameters', () => {
21
- const silver = SilverToolkit.open();
22
- const tpl = silver.template('timelock');
23
- const source = tpl.fill({ locktime: 500000, pubkey: 'abcd123' });
24
- expect(source).toContain('500000 OP_CHECKLOCKTIMEVERIFY OP_DROP');
25
- expect(source).toContain('abcd123 OP_CHECKSIG');
26
- });
27
-
28
- it('should throw if missing parameters during fill', () => {
29
- const silver = SilverToolkit.open();
30
- const tpl = silver.template('timelock');
31
- expect(() => tpl.fill({ locktime: 500000 })).toThrow(/Missing parameter/);
32
- });
33
-
34
- it('should enforce restrictive claims on build', async () => {
35
- const silver = SilverToolkit.open();
36
- const build = await silver.build('OP_TRUE');
37
- expect(build.bytecode).toBeDefined();
38
- expect(build.claims.realSilverCompiler).toBe(false);
39
- expect(build.claims.simulatedOnly).toBe(true);
40
- });
41
-
42
- it('should enforce restrictive claims on simulate, artifact, and evidence', async () => {
43
- const silver = SilverToolkit.open();
44
- const build = await silver.build('OP_TRUE');
45
-
46
- const simulation = await silver.simulate(build);
47
- expect(simulation.claims.vmConsensusEquivalence).toBe(false);
48
-
49
- const artifact = await silver.artifact(build, "test-script");
50
- expect(artifact.claims.productionSafe).toBe(false);
51
- expect(artifact.name).toBe("test-script");
52
-
53
- const evidence = await silver.evidence(build, simulation);
54
- expect(evidence.claims.mainnetReady).toBe(false);
55
- expect(evidence.schema).toBe("hardkas.script-evidence.v1");
56
- expect(evidence.simulationResult).not.toHaveProperty('claims'); // internal property
57
- expect(evidence.simulationResult.success).toBe(true);
58
- });
59
- });