@arkade-os/sdk 0.4.21 → 0.4.23
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/README.md +95 -12
- package/dist/cjs/contracts/arkcontract.js +2 -1
- package/dist/cjs/contracts/contractWatcher.js +16 -18
- package/dist/cjs/identity/descriptor.js +75 -4
- package/dist/cjs/identity/hdCapableIdentity.js +2 -0
- package/dist/cjs/identity/seedIdentity.js +225 -103
- package/dist/cjs/identity/serialize.js +5 -0
- package/dist/cjs/identity/staticDescriptorProvider.js +1 -1
- package/dist/cjs/index.js +12 -3
- package/dist/cjs/providers/electrum.js +285 -79
- package/dist/cjs/providers/expoIndexer.js +1 -1
- package/dist/cjs/providers/indexer.js +2 -2
- package/dist/cjs/providers/onchain.js +9 -3
- package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +6 -2
- package/dist/cjs/repositories/realm/walletRepository.js +2 -2
- package/dist/cjs/repositories/serialization.js +34 -1
- package/dist/cjs/repositories/sqlite/walletRepository.js +4 -2
- package/dist/cjs/script/address.js +2 -1
- package/dist/cjs/utils/transactionHistory.js +4 -4
- package/dist/cjs/wallet/asset-manager.js +18 -18
- package/dist/cjs/wallet/asset.js +10 -8
- package/dist/cjs/wallet/delegator.js +29 -20
- package/dist/cjs/wallet/hdDescriptorProvider.js +159 -0
- package/dist/cjs/wallet/index.js +5 -1
- package/dist/cjs/wallet/onchain.js +2 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +4 -2
- package/dist/cjs/wallet/serviceWorker/wallet.js +5 -4
- package/dist/cjs/wallet/validation.js +2 -3
- package/dist/cjs/wallet/vtxo-manager.js +7 -5
- package/dist/cjs/wallet/wallet.js +13 -14
- package/dist/esm/contracts/arkcontract.js +2 -1
- package/dist/esm/contracts/contractWatcher.js +14 -16
- package/dist/esm/identity/descriptor.js +74 -5
- package/dist/esm/identity/hdCapableIdentity.js +1 -0
- package/dist/esm/identity/seedIdentity.js +225 -103
- package/dist/esm/identity/serialize.js +5 -0
- package/dist/esm/identity/staticDescriptorProvider.js +1 -1
- package/dist/esm/index.js +7 -4
- package/dist/esm/providers/electrum.js +284 -78
- package/dist/esm/providers/expoIndexer.js +1 -1
- package/dist/esm/providers/indexer.js +2 -2
- package/dist/esm/providers/onchain.js +9 -3
- package/dist/esm/repositories/migrations/walletRepositoryImpl.js +6 -2
- package/dist/esm/repositories/realm/walletRepository.js +3 -3
- package/dist/esm/repositories/serialization.js +27 -0
- package/dist/esm/repositories/sqlite/walletRepository.js +5 -3
- package/dist/esm/script/address.js +2 -1
- package/dist/esm/utils/transactionHistory.js +4 -4
- package/dist/esm/wallet/asset-manager.js +18 -18
- package/dist/esm/wallet/asset.js +10 -8
- package/dist/esm/wallet/delegator.js +29 -20
- package/dist/esm/wallet/hdDescriptorProvider.js +155 -0
- package/dist/esm/wallet/index.js +4 -0
- package/dist/esm/wallet/onchain.js +2 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +4 -2
- package/dist/esm/wallet/serviceWorker/wallet.js +5 -4
- package/dist/esm/wallet/validation.js +2 -3
- package/dist/esm/wallet/vtxo-manager.js +7 -5
- package/dist/esm/wallet/wallet.js +12 -14
- package/dist/types/contracts/arkcontract.d.ts +1 -1
- package/dist/types/contracts/types.d.ts +5 -5
- package/dist/types/identity/descriptor.d.ts +26 -0
- package/dist/types/identity/descriptorProvider.d.ts +11 -4
- package/dist/types/identity/hdCapableIdentity.d.ts +44 -0
- package/dist/types/identity/index.d.ts +1 -0
- package/dist/types/identity/seedIdentity.d.ts +113 -29
- package/dist/types/identity/serialize.d.ts +12 -0
- package/dist/types/identity/staticDescriptorProvider.d.ts +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/providers/electrum.d.ts +115 -15
- package/dist/types/providers/onchain.d.ts +6 -0
- package/dist/types/repositories/serialization.d.ts +26 -2
- package/dist/types/script/address.d.ts +1 -1
- package/dist/types/wallet/delegator.d.ts +8 -3
- package/dist/types/wallet/hdDescriptorProvider.d.ts +93 -0
- package/dist/types/wallet/index.d.ts +19 -10
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/wallet.d.ts +1 -1
- package/dist/types/wallet/wallet.d.ts +4 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, } from '../serialization.js';
|
|
1
|
+
import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
|
|
2
2
|
import { scriptFromArkAddress } from '../scriptFromAddress.js';
|
|
3
3
|
/**
|
|
4
4
|
* SQLite-based implementation of WalletRepository.
|
|
@@ -299,7 +299,9 @@ export class SQLiteWalletRepository {
|
|
|
299
299
|
tx.amount,
|
|
300
300
|
tx.settled ? 1 : 0,
|
|
301
301
|
tx.createdAt,
|
|
302
|
-
tx.assets
|
|
302
|
+
tx.assets
|
|
303
|
+
? JSON.stringify(serializeAssets(tx.assets))
|
|
304
|
+
: null,
|
|
303
305
|
]);
|
|
304
306
|
}
|
|
305
307
|
}
|
|
@@ -406,7 +408,7 @@ function txRowToDomain(row) {
|
|
|
406
408
|
createdAt: row.created_at,
|
|
407
409
|
};
|
|
408
410
|
if (row.assets_json) {
|
|
409
|
-
tx.assets = JSON.parse(row.assets_json);
|
|
411
|
+
tx.assets = deserializeAssets(JSON.parse(row.assets_json));
|
|
410
412
|
}
|
|
411
413
|
return tx;
|
|
412
414
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bech32m } from "@scure/base";
|
|
2
2
|
import { Script } from "@scure/btc-signer/script.js";
|
|
3
|
+
import { DEFAULT_ARKADE_HRP } from '../wallet/index.js';
|
|
3
4
|
/**
|
|
4
5
|
* ArkAddress allows creating and decoding bech32m-encoded Arkade addresses.
|
|
5
6
|
*
|
|
@@ -40,7 +41,7 @@ export class ArkAddress {
|
|
|
40
41
|
* @defaultValue `version = 0`
|
|
41
42
|
* @throws Error if either public key is not 32 bytes long
|
|
42
43
|
*/
|
|
43
|
-
constructor(serverPubKey, vtxoTaprootKey, hrp, version = 0) {
|
|
44
|
+
constructor(serverPubKey, vtxoTaprootKey, hrp = DEFAULT_ARKADE_HRP, version = 0) {
|
|
44
45
|
this.serverPubKey = serverPubKey;
|
|
45
46
|
this.vtxoTaprootKey = vtxoTaprootKey;
|
|
46
47
|
this.hrp = hrp;
|
|
@@ -9,7 +9,7 @@ function collectAssets(vtxos) {
|
|
|
9
9
|
for (const vtxo of vtxos) {
|
|
10
10
|
if (vtxo.assets) {
|
|
11
11
|
for (const a of vtxo.assets) {
|
|
12
|
-
map.set(a.assetId, (map.get(a.assetId) ??
|
|
12
|
+
map.set(a.assetId, (map.get(a.assetId) ?? 0n) + a.amount);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -22,16 +22,16 @@ function subtractAssets(spent, change) {
|
|
|
22
22
|
for (const vtxo of change) {
|
|
23
23
|
if (vtxo.assets) {
|
|
24
24
|
for (const a of vtxo.assets) {
|
|
25
|
-
map.set(a.assetId, (map.get(a.assetId) ??
|
|
25
|
+
map.set(a.assetId, (map.get(a.assetId) ?? 0n) + a.amount);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
for (const vtxo of spent) {
|
|
30
30
|
if (vtxo.assets) {
|
|
31
31
|
for (const a of vtxo.assets) {
|
|
32
|
-
const current = map.get(a.assetId) ??
|
|
32
|
+
const current = map.get(a.assetId) ?? 0n;
|
|
33
33
|
const remaining = current - a.amount;
|
|
34
|
-
if (remaining !==
|
|
34
|
+
if (remaining !== 0n) {
|
|
35
35
|
map.set(a.assetId, remaining);
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
@@ -40,7 +40,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
42
|
async issue(params) {
|
|
43
|
-
if (params.amount <=
|
|
43
|
+
if (params.amount <= 0n) {
|
|
44
44
|
throw new Error(`Issue amount must be greater than 0, got ${params.amount}`);
|
|
45
45
|
}
|
|
46
46
|
const metadata = castMetadata(params.metadata);
|
|
@@ -60,12 +60,12 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
60
60
|
continue;
|
|
61
61
|
for (const { assetId, amount } of coin.assets) {
|
|
62
62
|
const existing = assetChanges.get(assetId) ?? 0n;
|
|
63
|
-
assetChanges.set(assetId, existing +
|
|
63
|
+
assetChanges.set(assetId, existing + amount);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
const groups = [];
|
|
67
67
|
// issued asset group
|
|
68
|
-
const issuedAssetOutput = AssetOutput.create(0,
|
|
68
|
+
const issuedAssetOutput = AssetOutput.create(0, params.amount);
|
|
69
69
|
const issuedAssetGroup = AssetGroup.create(null, controlAssetRef, [], [issuedAssetOutput], metadata);
|
|
70
70
|
groups.push(issuedAssetGroup);
|
|
71
71
|
// add asset groups for each asset change
|
|
@@ -78,7 +78,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
78
78
|
for (const asset of assets) {
|
|
79
79
|
if (asset.assetId !== assetId)
|
|
80
80
|
continue;
|
|
81
|
-
changeInputs.push(AssetInput.create(inputIndex,
|
|
81
|
+
changeInputs.push(AssetInput.create(inputIndex, asset.amount));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
// add the change asset group
|
|
@@ -119,7 +119,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
121
|
async reissue(params) {
|
|
122
|
-
if (params.amount <=
|
|
122
|
+
if (params.amount <= 0n) {
|
|
123
123
|
throw new Error(`Reissuance amount must be greater than 0, got ${params.amount}`);
|
|
124
124
|
}
|
|
125
125
|
const { controlAssetId } = await this.getAssetDetails(params.assetId);
|
|
@@ -140,11 +140,11 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
140
140
|
continue;
|
|
141
141
|
for (const { assetId, amount } of coin.assets) {
|
|
142
142
|
if (assetId === params.assetId) {
|
|
143
|
-
assetToReissueAmount +=
|
|
143
|
+
assetToReissueAmount += amount;
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
146
|
const existing = assetChanges.get(assetId) ?? 0n;
|
|
147
|
-
assetChanges.set(assetId, existing +
|
|
147
|
+
assetChanges.set(assetId, existing + amount);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
// select at least dust amount
|
|
@@ -159,11 +159,11 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
159
159
|
continue;
|
|
160
160
|
for (const { assetId, amount } of coin.assets) {
|
|
161
161
|
if (assetId === params.assetId) {
|
|
162
|
-
assetToReissueAmount +=
|
|
162
|
+
assetToReissueAmount += amount;
|
|
163
163
|
continue;
|
|
164
164
|
}
|
|
165
165
|
const existing = assetChanges.get(assetId) ?? 0n;
|
|
166
|
-
assetChanges.set(assetId, existing +
|
|
166
|
+
assetChanges.set(assetId, existing + amount);
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
selectedCoins = [...selectedCoins, ...additional.inputs];
|
|
@@ -176,11 +176,11 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
176
176
|
for (const asset of assets) {
|
|
177
177
|
if (asset.assetId !== params.assetId)
|
|
178
178
|
continue;
|
|
179
|
-
reissueInputs.push(AssetInput.create(inputIndex,
|
|
179
|
+
reissueInputs.push(AssetInput.create(inputIndex, asset.amount));
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
// the total output amount of the asset to reissue = new + (optional) selected amount
|
|
183
|
-
const totalAssetAmount = assetToReissueAmount +
|
|
183
|
+
const totalAssetAmount = assetToReissueAmount + params.amount;
|
|
184
184
|
const reissueAssetIdObj = AssetId.fromString(params.assetId);
|
|
185
185
|
// create the reissuance asset group
|
|
186
186
|
const reissueAssetGroup = AssetGroup.create(reissueAssetIdObj, null, reissueInputs, [AssetOutput.create(0, totalAssetAmount)], []);
|
|
@@ -192,7 +192,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
192
192
|
for (const asset of assets) {
|
|
193
193
|
if (asset.assetId !== assetId)
|
|
194
194
|
continue;
|
|
195
|
-
changeInputs.push(AssetInput.create(inputIndex,
|
|
195
|
+
changeInputs.push(AssetInput.create(inputIndex, asset.amount));
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
groups.push(AssetGroup.create(AssetId.fromString(assetId), null, changeInputs, [AssetOutput.create(0, amount)], []));
|
|
@@ -226,7 +226,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
226
226
|
* ```
|
|
227
227
|
*/
|
|
228
228
|
async burn(params) {
|
|
229
|
-
if (params.amount <=
|
|
229
|
+
if (params.amount <= 0n) {
|
|
230
230
|
throw new Error(`Burn amount must be greater than 0, got ${params.amount}`);
|
|
231
231
|
}
|
|
232
232
|
const virtualCoins = await this.wallet.getVtxos({
|
|
@@ -234,7 +234,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
234
234
|
});
|
|
235
235
|
const assetChanges = new Map();
|
|
236
236
|
// select virtual outputs with the asset to burn
|
|
237
|
-
const { selected: assetCoins } = selectCoinsWithAsset(virtualCoins, params.assetId,
|
|
237
|
+
const { selected: assetCoins } = selectCoinsWithAsset(virtualCoins, params.assetId, params.amount);
|
|
238
238
|
const selectedCoins = [...assetCoins];
|
|
239
239
|
let totalBtcSelected = 0;
|
|
240
240
|
// add the selected coins to asset changes, including the asset to burn
|
|
@@ -244,11 +244,11 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
244
244
|
continue;
|
|
245
245
|
for (const { assetId, amount } of coin.assets) {
|
|
246
246
|
const existing = assetChanges.get(assetId) ?? 0n;
|
|
247
|
-
assetChanges.set(assetId, existing +
|
|
247
|
+
assetChanges.set(assetId, existing + amount);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
// subtract the amount to burn from the asset change
|
|
251
|
-
assetChanges.set(params.assetId, (assetChanges.get(params.assetId) ?? 0n) -
|
|
251
|
+
assetChanges.set(params.assetId, (assetChanges.get(params.assetId) ?? 0n) - params.amount);
|
|
252
252
|
const minBtcNeeded = Number(this.wallet.dustAmount);
|
|
253
253
|
// we need to ensure at least dust amount is selected
|
|
254
254
|
// if not, select additional coins
|
|
@@ -262,7 +262,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
262
262
|
continue;
|
|
263
263
|
for (const { assetId, amount } of coin.assets) {
|
|
264
264
|
const existing = assetChanges.get(assetId) ?? 0n;
|
|
265
|
-
assetChanges.set(assetId, existing +
|
|
265
|
+
assetChanges.set(assetId, existing + amount);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
selectedCoins.push(...additional.inputs);
|
|
@@ -276,7 +276,7 @@ export class AssetManager extends ReadonlyAssetManager {
|
|
|
276
276
|
for (const asset of assets) {
|
|
277
277
|
if (asset.assetId !== assetId)
|
|
278
278
|
continue;
|
|
279
|
-
changeInputs.push(AssetInput.create(inputIndex,
|
|
279
|
+
changeInputs.push(AssetInput.create(inputIndex, asset.amount));
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
groups.push(AssetGroup.create(AssetId.fromString(assetId), null, changeInputs, amount > 0n ? [AssetOutput.create(0, amount)] : [], []));
|
package/dist/esm/wallet/asset.js
CHANGED
|
@@ -15,7 +15,7 @@ export function createAssetPacket(assetInputs, receivers, changeReceiver) {
|
|
|
15
15
|
const existing = inputsByAssetId.get(asset.assetId);
|
|
16
16
|
inputsByAssetId.set(asset.assetId, [
|
|
17
17
|
...(existing ?? []),
|
|
18
|
-
AssetInput.create(inputIndex,
|
|
18
|
+
AssetInput.create(inputIndex, asset.amount),
|
|
19
19
|
]);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -29,7 +29,7 @@ export function createAssetPacket(assetInputs, receivers, changeReceiver) {
|
|
|
29
29
|
const existing = outputsByAssetId.get(asset.assetId);
|
|
30
30
|
outputsByAssetId.set(asset.assetId, [
|
|
31
31
|
...(existing ?? []),
|
|
32
|
-
AssetOutput.create(outputIndex,
|
|
32
|
+
AssetOutput.create(outputIndex, asset.amount),
|
|
33
33
|
]);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -41,7 +41,7 @@ export function createAssetPacket(assetInputs, receivers, changeReceiver) {
|
|
|
41
41
|
const existing = outputsByAssetId.get(asset.assetId);
|
|
42
42
|
outputsByAssetId.set(asset.assetId, [
|
|
43
43
|
...(existing ?? []),
|
|
44
|
-
AssetOutput.create(outputIndex,
|
|
44
|
+
AssetOutput.create(outputIndex, asset.amount),
|
|
45
45
|
]);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -69,9 +69,11 @@ export function selectCoinsWithAsset(coins, assetId, requiredAmount) {
|
|
|
69
69
|
const coinsWithAsset = coins.filter((coin) => coin.assets?.some((a) => a.assetId === assetId));
|
|
70
70
|
// sort by asset amount (smallest first for better selection)
|
|
71
71
|
coinsWithAsset.sort((a, b) => {
|
|
72
|
-
const amountA = a.assets?.find((asset) => asset.assetId === assetId)?.amount ??
|
|
73
|
-
const amountB = b.assets?.find((asset) => asset.assetId === assetId)?.amount ??
|
|
74
|
-
|
|
72
|
+
const amountA = a.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0n;
|
|
73
|
+
const amountB = b.assets?.find((asset) => asset.assetId === assetId)?.amount ?? 0n;
|
|
74
|
+
// Array.sort callback returns number; reduce the bigint diff to
|
|
75
|
+
// -1/0/1 (the only thing sort actually consults).
|
|
76
|
+
return amountA < amountB ? -1 : amountA > amountB ? 1 : 0;
|
|
75
77
|
});
|
|
76
78
|
const selected = [];
|
|
77
79
|
let totalAssetAmount = 0n;
|
|
@@ -79,8 +81,8 @@ export function selectCoinsWithAsset(coins, assetId, requiredAmount) {
|
|
|
79
81
|
if (totalAssetAmount >= requiredAmount)
|
|
80
82
|
break;
|
|
81
83
|
selected.push(coin);
|
|
82
|
-
const assetAmount = coin.assets?.find((a) => a.assetId === assetId)?.amount ??
|
|
83
|
-
totalAssetAmount +=
|
|
84
|
+
const assetAmount = coin.assets?.find((a) => a.assetId === assetId)?.amount ?? 0n;
|
|
85
|
+
totalAssetAmount += assetAmount;
|
|
84
86
|
}
|
|
85
87
|
if (totalAssetAmount < requiredAmount) {
|
|
86
88
|
throw new Error(`Insufficient asset balance: have ${totalAssetAmount}, need ${requiredAmount}`);
|
|
@@ -25,20 +25,30 @@ export class DelegatorManagerImpl {
|
|
|
25
25
|
// fetch server and delegator info once, shared across all groups
|
|
26
26
|
const arkInfo = await this.arkInfoProvider.getInfo();
|
|
27
27
|
const delegateInfo = await this.delegatorProvider.getDelegateInfo();
|
|
28
|
+
// keep only vtxos that can be signed by the delegate
|
|
29
|
+
const eligible = vtxos
|
|
30
|
+
.filter((v) => findDelegateTapLeaf(v, delegateInfo.pubkey) !== undefined)
|
|
31
|
+
.map((v) => v);
|
|
32
|
+
if (eligible.length === 0) {
|
|
33
|
+
return { delegated: [], failed: [] };
|
|
34
|
+
}
|
|
28
35
|
// if explicit delegateAt is provided, delegate all virtual outputs at once without sorting
|
|
29
36
|
if (delegateAt) {
|
|
30
37
|
try {
|
|
31
|
-
await delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo,
|
|
38
|
+
await delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo, eligible, destinationScript, delegateAt);
|
|
32
39
|
}
|
|
33
40
|
catch (error) {
|
|
34
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
delegated: [],
|
|
43
|
+
failed: [{ outpoints: eligible, error }],
|
|
44
|
+
};
|
|
35
45
|
}
|
|
36
|
-
return { delegated:
|
|
46
|
+
return { delegated: eligible, failed: [] };
|
|
37
47
|
}
|
|
38
48
|
// if no explicit delegateAt is provided, sort virtual outputs by expiry and delegate in groups of the same expiry day
|
|
39
49
|
const groupByExpiry = new Map();
|
|
40
50
|
let recoverableVtxos = [];
|
|
41
|
-
for (const vtxo of
|
|
51
|
+
for (const vtxo of eligible) {
|
|
42
52
|
if (isRecoverable(vtxo)) {
|
|
43
53
|
recoverableVtxos.push(vtxo);
|
|
44
54
|
continue;
|
|
@@ -185,20 +195,7 @@ async function delegate(identity, delegatorProvider, arkInfo, delegateInfo, vtxo
|
|
|
185
195
|
await delegatorProvider.delegate(registerIntent, forfeits);
|
|
186
196
|
}
|
|
187
197
|
async function makeDelegateForfeitTx(input, connectorAmount, delegatePubkey, forfeitOutputScript, identity) {
|
|
188
|
-
|
|
189
|
-
delegatePubkey = delegatePubkey.slice(2);
|
|
190
|
-
}
|
|
191
|
-
const vtxoScript = VtxoScript.decode(input.tapTree);
|
|
192
|
-
const delegateTapLeaf = vtxoScript.leaves.find((tapLeaf) => {
|
|
193
|
-
const arkTapscript = decodeTapscript(scriptFromTapLeafScript(tapLeaf));
|
|
194
|
-
if (!MultisigTapscript.is(arkTapscript))
|
|
195
|
-
return false;
|
|
196
|
-
if (!arkTapscript.params.pubkeys
|
|
197
|
-
.map(hex.encode)
|
|
198
|
-
.includes(delegatePubkey))
|
|
199
|
-
return false;
|
|
200
|
-
return true;
|
|
201
|
-
});
|
|
198
|
+
const delegateTapLeaf = findDelegateTapLeaf(input, delegatePubkey);
|
|
202
199
|
if (!delegateTapLeaf) {
|
|
203
200
|
throw new Error(`delegate tap leaf not found for input: ${input.txid}:${input.vout}`);
|
|
204
201
|
}
|
|
@@ -243,12 +240,12 @@ async function makeSignedDelegateIntent(identity, coins, outputs, onchainOutputs
|
|
|
243
240
|
for (const [, assets] of assetInputs) {
|
|
244
241
|
for (const asset of assets) {
|
|
245
242
|
const existing = allAssets.get(asset.assetId) ?? 0n;
|
|
246
|
-
allAssets.set(asset.assetId, existing +
|
|
243
|
+
allAssets.set(asset.assetId, existing + asset.amount);
|
|
247
244
|
}
|
|
248
245
|
}
|
|
249
246
|
outputAssets = [];
|
|
250
247
|
for (const [assetId, amount] of allAssets) {
|
|
251
|
-
outputAssets.push({ assetId, amount
|
|
248
|
+
outputAssets.push({ assetId, amount });
|
|
252
249
|
}
|
|
253
250
|
}
|
|
254
251
|
const recipients = outputs.map((output, i) => ({
|
|
@@ -286,3 +283,15 @@ function getDayTimestamp(timestamp) {
|
|
|
286
283
|
date.setUTCHours(0, 0, 0, 0);
|
|
287
284
|
return date.getTime();
|
|
288
285
|
}
|
|
286
|
+
function findDelegateTapLeaf(vtxo, delegatePubkey) {
|
|
287
|
+
if (!vtxo.tapTree)
|
|
288
|
+
return undefined;
|
|
289
|
+
const pk = delegatePubkey.length === 66 ? delegatePubkey.slice(2) : delegatePubkey;
|
|
290
|
+
const vtxoScript = VtxoScript.decode(vtxo.tapTree);
|
|
291
|
+
return vtxoScript.leaves.find((tapLeaf) => {
|
|
292
|
+
const arkTapscript = decodeTapscript(scriptFromTapLeafScript(tapLeaf));
|
|
293
|
+
if (!MultisigTapscript.is(arkTapscript))
|
|
294
|
+
return false;
|
|
295
|
+
return arkTapscript.params.pubkeys.map(hex.encode).includes(pk);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { expand, networks } from "@bitcoinerlab/descriptors-scure";
|
|
2
|
+
import { isMainnetDescriptor } from '../identity/descriptor.js';
|
|
3
|
+
import { updateWalletState } from '../utils/syncCursors.js';
|
|
4
|
+
/** Settings key under {@link WalletState.settings} where HD state lives. */
|
|
5
|
+
const HD_SETTINGS_KEY = "hd";
|
|
6
|
+
/**
|
|
7
|
+
* HD-wallet {@link DescriptorProvider} that allocates a fresh signing
|
|
8
|
+
* descriptor on every call. The provider holds no notion of "current" — it
|
|
9
|
+
* is a pure rotating allocator. The question of "which descriptor is the
|
|
10
|
+
* wallet currently bound to?" is answered by querying the contract
|
|
11
|
+
* repository for active contracts, not by asking this provider.
|
|
12
|
+
*
|
|
13
|
+
* State is persisted under `WalletRepository.getWalletState().settings.hd` so
|
|
14
|
+
* that no storage-schema migration is required when switching a wallet from
|
|
15
|
+
* single-key to HD. The provider is backed by an {@link HDCapableIdentity},
|
|
16
|
+
* which carries the wildcard account descriptor template (for derivation)
|
|
17
|
+
* and the signing primitives.
|
|
18
|
+
*
|
|
19
|
+
* The read-modify-write of the persisted index runs inside the shared per-
|
|
20
|
+
* repo `updateWalletState` mutex, so two `getNextSigningDescriptor` callers
|
|
21
|
+
* — including those driving separate `HDDescriptorProvider` instances on
|
|
22
|
+
* the same repo — can never observe the same index.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const provider = await HDDescriptorProvider.create(identity, walletRepo);
|
|
27
|
+
* const descriptor = await provider.getNextSigningDescriptor();
|
|
28
|
+
* // descriptor: tr([fp/86'/0'/0']xpub/0/0)
|
|
29
|
+
* const next = await provider.getNextSigningDescriptor();
|
|
30
|
+
* // next: tr([fp/86'/0'/0']xpub/0/1)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class HDDescriptorProvider {
|
|
34
|
+
constructor(identity, walletRepository) {
|
|
35
|
+
this.identity = identity;
|
|
36
|
+
this.walletRepository = walletRepository;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Construct an HDDescriptorProvider. No I/O is performed here;
|
|
40
|
+
* persisted state is read lazily on the first call to
|
|
41
|
+
* `getNextSigningDescriptor`. A descriptor-mismatch error surfaces on
|
|
42
|
+
* first use rather than at boot.
|
|
43
|
+
*/
|
|
44
|
+
static async create(identity, walletRepository) {
|
|
45
|
+
return new HDDescriptorProvider(identity, walletRepository);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Allocate the next descriptor and return it. The first call on a fresh
|
|
49
|
+
* wallet returns descriptor at index 0; subsequent calls return 1, 2, 3,
|
|
50
|
+
* ... in order. Each call is atomic with respect to other rotations on
|
|
51
|
+
* the same repo: two concurrent callers can never observe the same
|
|
52
|
+
* index.
|
|
53
|
+
*/
|
|
54
|
+
async getNextSigningDescriptor() {
|
|
55
|
+
return this.mutate((settings) => {
|
|
56
|
+
const next = settings.lastIndexUsed === undefined
|
|
57
|
+
? 0
|
|
58
|
+
: settings.lastIndexUsed + 1;
|
|
59
|
+
settings.lastIndexUsed = next;
|
|
60
|
+
return this.materializeAt(next);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns true when the given descriptor is derivable from this wallet's
|
|
65
|
+
* seed. Delegates to the underlying identity, which handles both HD and
|
|
66
|
+
* simple `tr(pubkey)` descriptors.
|
|
67
|
+
*/
|
|
68
|
+
isOurs(descriptor) {
|
|
69
|
+
return this.identity.isOurs(descriptor);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Signs each request with the key derived from its descriptor. Delegates
|
|
73
|
+
* to the identity's signing primitives — the identity, not the provider,
|
|
74
|
+
* holds the seed.
|
|
75
|
+
*/
|
|
76
|
+
async signWithDescriptor(requests) {
|
|
77
|
+
return this.identity.signWithDescriptor(requests);
|
|
78
|
+
}
|
|
79
|
+
/** Signs a message using the key derived from `descriptor`. */
|
|
80
|
+
async signMessageWithDescriptor(descriptor, message, signatureType = "schnorr") {
|
|
81
|
+
return this.identity.signMessageWithDescriptor(descriptor, message, signatureType);
|
|
82
|
+
}
|
|
83
|
+
// ── internals ────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Substitute the wildcard in the identity's account-descriptor template
|
|
86
|
+
* with a concrete index, going through the descriptors-scure parser
|
|
87
|
+
* rather than ad-hoc string substitution. The parser's `expand({ index })`
|
|
88
|
+
* call validates that the input is a ranged template AND produces a
|
|
89
|
+
* canonical materialized key expression at the given index.
|
|
90
|
+
*/
|
|
91
|
+
materializeAt(index) {
|
|
92
|
+
const descriptor = this.identity.descriptor;
|
|
93
|
+
const network = isMainnetDescriptor(descriptor)
|
|
94
|
+
? networks.bitcoin
|
|
95
|
+
: networks.testnet;
|
|
96
|
+
const expansion = expand({ descriptor, network, index });
|
|
97
|
+
const keyInfo = expansion.expansionMap?.["@0"];
|
|
98
|
+
if (!keyInfo?.keyExpression) {
|
|
99
|
+
throw new Error(`HDDescriptorProvider: cannot materialize descriptor at index ${index}`);
|
|
100
|
+
}
|
|
101
|
+
return `tr(${keyInfo.keyExpression})`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Run the read-modify-write of HD settings inside the shared per-repo
|
|
105
|
+
* wallet-state mutex. The closure receives a freshly-validated settings
|
|
106
|
+
* snapshot, mutates it, and returns whatever value the caller wants to
|
|
107
|
+
* surface; the mutated settings are then persisted as part of the same
|
|
108
|
+
* atomic update.
|
|
109
|
+
*
|
|
110
|
+
* Doing the read inside the lock is what prevents two providers (or two
|
|
111
|
+
* concurrent callers on the same provider) from racing on a stale index.
|
|
112
|
+
*/
|
|
113
|
+
async mutate(fn) {
|
|
114
|
+
let result;
|
|
115
|
+
await updateWalletState(this.walletRepository, (state) => {
|
|
116
|
+
const settings = this.parseSettings(state);
|
|
117
|
+
result = fn(settings);
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
settings: {
|
|
121
|
+
...(state.settings ?? {}),
|
|
122
|
+
[HD_SETTINGS_KEY]: settings,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Validate the persisted HD settings (or initialize a fresh record when
|
|
130
|
+
* absent) and return a clone safe for the caller to mutate.
|
|
131
|
+
*
|
|
132
|
+
* The cast to `HDWalletSettings` trusts storage; a corrupted or
|
|
133
|
+
* partially-migrated repo could otherwise produce `NaN` descriptors.
|
|
134
|
+
* Fail loud rather than silently derive garbage.
|
|
135
|
+
*/
|
|
136
|
+
parseSettings(state) {
|
|
137
|
+
const stored = state.settings?.[HD_SETTINGS_KEY];
|
|
138
|
+
const expected = this.identity.descriptor;
|
|
139
|
+
if (!stored) {
|
|
140
|
+
return { descriptor: expected };
|
|
141
|
+
}
|
|
142
|
+
if (stored.descriptor !== expected) {
|
|
143
|
+
throw new Error(`HD descriptor mismatch: stored "${stored.descriptor}", expected "${expected}". ` +
|
|
144
|
+
`Refusing to reuse HD state from a different identity.`);
|
|
145
|
+
}
|
|
146
|
+
if (stored.lastIndexUsed !== undefined &&
|
|
147
|
+
(typeof stored.lastIndexUsed !== "number" ||
|
|
148
|
+
!Number.isInteger(stored.lastIndexUsed) ||
|
|
149
|
+
stored.lastIndexUsed < 0)) {
|
|
150
|
+
throw new Error(`Corrupt HD settings: lastIndexUsed is not a non-negative integer (got ${String(stored.lastIndexUsed)}).`);
|
|
151
|
+
}
|
|
152
|
+
// Shallow clone so the closure may mutate without aliasing the repo's copy.
|
|
153
|
+
return { ...stored };
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/esm/wallet/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { p2tr } from "@scure/btc-signer";
|
|
2
|
+
import { DEFAULT_NETWORK_NAME } from './index.js';
|
|
2
3
|
import { getNetwork } from '../networks.js';
|
|
3
4
|
import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
4
5
|
import { findP2AOutput, P2A } from '../utils/anchor.js';
|
|
@@ -39,7 +40,7 @@ export class OnchainWallet {
|
|
|
39
40
|
* @defaultValue `provider = new EsploraProvider('https://mempool.space/api')`
|
|
40
41
|
* @throws Error if the configured identity cannot produce a valid x-only public key
|
|
41
42
|
*/
|
|
42
|
-
static async create(identity, networkName, provider) {
|
|
43
|
+
static async create(identity, networkName = DEFAULT_NETWORK_NAME, provider) {
|
|
43
44
|
const pubkey = await identity.xOnlyPublicKey();
|
|
44
45
|
if (!pubkey) {
|
|
45
46
|
throw new Error("Invalid configured public key");
|
|
@@ -508,7 +508,7 @@ export class WalletMessageHandler {
|
|
|
508
508
|
for (const vtxo of spendableVtxos) {
|
|
509
509
|
if (vtxo.assets) {
|
|
510
510
|
for (const a of vtxo.assets) {
|
|
511
|
-
const current = assetBalances.get(a.assetId) ??
|
|
511
|
+
const current = assetBalances.get(a.assetId) ?? 0n;
|
|
512
512
|
assetBalances.set(a.assetId, current + a.amount);
|
|
513
513
|
}
|
|
514
514
|
}
|
|
@@ -700,7 +700,9 @@ export class WalletMessageHandler {
|
|
|
700
700
|
const { vtxoOutpoints, destination, delegateAt } = message.payload;
|
|
701
701
|
const allVtxos = await wallet.getVtxos();
|
|
702
702
|
const outpointSet = new Set(vtxoOutpoints.map((o) => `${o.txid}:${o.vout}`));
|
|
703
|
-
const filtered = allVtxos
|
|
703
|
+
const filtered = allVtxos
|
|
704
|
+
.filter((v) => outpointSet.has(`${v.txid}:${v.vout}`))
|
|
705
|
+
.map((v) => ({ ...v, contractScript: v.script }));
|
|
704
706
|
const result = await delegatorManager.delegate(filtered, destination, delegateAt !== undefined ? new Date(delegateAt) : undefined);
|
|
705
707
|
return {
|
|
706
708
|
tag: this.messageTag,
|
|
@@ -5,6 +5,7 @@ import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../../r
|
|
|
5
5
|
import { DEFAULT_MESSAGE_TAG, } from './wallet-message-handler.js';
|
|
6
6
|
import { getRandomId } from '../utils.js';
|
|
7
7
|
import { MESSAGE_BUS_NOT_INITIALIZED, ServiceWorkerTimeoutError, } from '../../worker/errors.js';
|
|
8
|
+
import { getArkadeServerUrl } from '../wallet.js';
|
|
8
9
|
// Check by error message content instead of instanceof because postMessage uses the
|
|
9
10
|
// structured clone algorithm which strips the prototype chain — the page
|
|
10
11
|
// receives a plain Error, not the original MessageBusNotInitializedError.
|
|
@@ -211,7 +212,7 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
211
212
|
.then(hex.encode);
|
|
212
213
|
const initWalletPayload = {
|
|
213
214
|
key: { publicKey },
|
|
214
|
-
arkServerUrl: options
|
|
215
|
+
arkServerUrl: getArkadeServerUrl(options),
|
|
215
216
|
arkServerPublicKey: options.arkServerPublicKey,
|
|
216
217
|
delegatorUrl: options.delegatorUrl,
|
|
217
218
|
};
|
|
@@ -226,7 +227,7 @@ export class ServiceWorkerReadonlyWallet {
|
|
|
226
227
|
const busInitConfig = {
|
|
227
228
|
wallet: serializedWallet,
|
|
228
229
|
arkServer: {
|
|
229
|
-
url: options
|
|
230
|
+
url: getArkadeServerUrl(options),
|
|
230
231
|
publicKey: options.arkServerPublicKey,
|
|
231
232
|
},
|
|
232
233
|
delegatorUrl: options.delegatorUrl,
|
|
@@ -884,7 +885,7 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
884
885
|
: null;
|
|
885
886
|
const initWalletPayload = {
|
|
886
887
|
key: legacyPrivateKey ? { privateKey: legacyPrivateKey } : {},
|
|
887
|
-
arkServerUrl: options
|
|
888
|
+
arkServerUrl: getArkadeServerUrl(options),
|
|
888
889
|
arkServerPublicKey: options.arkServerPublicKey,
|
|
889
890
|
delegatorUrl: options.delegatorUrl,
|
|
890
891
|
};
|
|
@@ -899,7 +900,7 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
899
900
|
const busInitConfig = {
|
|
900
901
|
wallet: serializedWallet,
|
|
901
902
|
arkServer: {
|
|
902
|
-
url: options
|
|
903
|
+
url: getArkadeServerUrl(options),
|
|
903
904
|
publicKey: options.arkServerPublicKey,
|
|
904
905
|
},
|
|
905
906
|
delegatorUrl: options.delegatorUrl,
|
|
@@ -136,8 +136,7 @@ function validateAssetGroupOutput(packet, outputIndex, assetId, expectedAmount)
|
|
|
136
136
|
if (!assetOutput) {
|
|
137
137
|
throw ErrAssetOutputNotFound(assetId, outputIndex);
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
throw ErrInvalidAssetOutputAmount(assetOutput.amount, expectedAmountBigInt, assetId);
|
|
139
|
+
if (assetOutput.amount !== expectedAmount) {
|
|
140
|
+
throw ErrInvalidAssetOutputAmount(assetOutput.amount, expectedAmount, assetId);
|
|
142
141
|
}
|
|
143
142
|
}
|
|
@@ -693,11 +693,13 @@ export class VtxoManager {
|
|
|
693
693
|
console.error("Error renewing VTXOs:", e);
|
|
694
694
|
});
|
|
695
695
|
}
|
|
696
|
-
delegatorManager
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
696
|
+
if (delegatorManager) {
|
|
697
|
+
delegatorManager
|
|
698
|
+
.delegate(event.vtxos, destination)
|
|
699
|
+
.catch((e) => {
|
|
700
|
+
console.error("Error delegating VTXOs:", e);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
701
703
|
});
|
|
702
704
|
return stopWatching;
|
|
703
705
|
}
|