@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.
- package/dist/fee-estimator.d.ts +9 -0
- package/dist/fee-estimator.d.ts.map +1 -0
- package/dist/fee-estimator.js +32 -0
- package/dist/fee-estimator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts +1 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +7 -0
- package/dist/indexer.js.map +1 -1
- package/dist/query-api.d.ts +22 -0
- package/dist/query-api.d.ts.map +1 -0
- package/dist/query-api.js +116 -0
- package/dist/query-api.js.map +1 -0
- package/dist/snapshot/serialization.d.ts +0 -9
- package/dist/snapshot/serialization.d.ts.map +1 -1
- package/dist/snapshot/serialization.js +0 -12
- package/dist/snapshot/serialization.js.map +1 -1
- package/dist/subscriptions.d.ts +22 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +91 -0
- package/dist/subscriptions.js.map +1 -0
- package/dist/wallet.d.ts +71 -1
- package/dist/wallet.d.ts.map +1 -1
- package/dist/wallet.js +296 -44
- package/dist/wallet.js.map +1 -1
- package/package.json +24 -9
- package/.turbo/turbo-build.log +0 -4
- package/src/dag/consensus.ts +0 -150
- package/src/dag/index.ts +0 -73
- package/src/dag/store.ts +0 -40
- package/src/dag/topology.ts +0 -108
- package/src/dag/types.ts +0 -20
- package/src/index.ts +0 -10
- package/src/indexer.ts +0 -134
- package/src/jobs.ts +0 -63
- package/src/payment.ts +0 -100
- package/src/silver/index.ts +0 -102
- package/src/silver/templates.ts +0 -27
- package/src/silver/types.ts +0 -37
- package/src/snapshot/backends/FsSnapshotBackend.ts +0 -100
- package/src/snapshot/backends/MemorySnapshotBackend.ts +0 -65
- package/src/snapshot/index.ts +0 -102
- package/src/snapshot/serialization.ts +0 -35
- package/src/snapshot/types.ts +0 -45
- package/src/stores/invoice-store.ts +0 -62
- package/src/stores/utxo-control-store.ts +0 -78
- package/src/utxos.ts +0 -451
- package/src/wallet.ts +0 -158
- package/test/silver.test.ts +0 -59
- package/test/snapshot.test.ts +0 -99
- package/test/toolkit.test.ts +0 -140
- package/tsconfig.json +0 -9
- 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
|
-
}
|
package/test/silver.test.ts
DELETED
|
@@ -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
|
-
});
|