@kaleidorg/wallet-engine 1.0.0-beta.31 → 1.0.0-beta.32
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/adapters/ArkadeAdapter.d.ts +78 -15
- package/dist/adapters/ArkadeAdapter.d.ts.map +1 -1
- package/dist/adapters/ArkadeAdapter.js +652 -162
- package/dist/adapters/ArkadeAdapter.js.map +1 -1
- package/dist/adapters/native.d.ts +1 -0
- package/dist/adapters/native.d.ts.map +1 -1
- package/dist/adapters/native.js +1 -0
- package/dist/adapters/native.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/arkade-client-manager.d.ts +64 -24
- package/dist/lib/arkade-client-manager.d.ts.map +1 -1
- package/dist/lib/arkade-client-manager.js +240 -65
- package/dist/lib/arkade-client-manager.js.map +1 -1
- package/dist/lib/arkade-converters.d.ts +39 -0
- package/dist/lib/arkade-converters.d.ts.map +1 -0
- package/dist/lib/arkade-converters.js +148 -0
- package/dist/lib/arkade-converters.js.map +1 -0
- package/dist/lib/arkade-helpers.d.ts +110 -0
- package/dist/lib/arkade-helpers.d.ts.map +1 -0
- package/dist/lib/arkade-helpers.js +227 -0
- package/dist/lib/arkade-helpers.js.map +1 -0
- package/dist/lib/arkade-swaps-client-manager.d.ts +55 -0
- package/dist/lib/arkade-swaps-client-manager.d.ts.map +1 -0
- package/dist/lib/arkade-swaps-client-manager.js +127 -0
- package/dist/lib/arkade-swaps-client-manager.js.map +1 -0
- package/dist/lib/arkade-vtxo-lifecycle.d.ts +116 -0
- package/dist/lib/arkade-vtxo-lifecycle.d.ts.map +1 -0
- package/dist/lib/arkade-vtxo-lifecycle.js +184 -0
- package/dist/lib/arkade-vtxo-lifecycle.js.map +1 -0
- package/dist/lib/ln-message-sign.d.ts +20 -0
- package/dist/lib/ln-message-sign.d.ts.map +1 -0
- package/dist/lib/ln-message-sign.js +90 -0
- package/dist/lib/ln-message-sign.js.map +1 -0
- package/dist/lib/log.d.ts +15 -0
- package/dist/lib/log.d.ts.map +1 -0
- package/dist/lib/log.js +16 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/zbase32.d.ts +3 -0
- package/dist/lib/zbase32.d.ts.map +1 -0
- package/dist/lib/zbase32.js +64 -0
- package/dist/lib/zbase32.js.map +1 -0
- package/package.json +8 -3
|
@@ -1,316 +1,806 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Arkade Protocol Adapter
|
|
3
|
-
* Implements IProtocolAdapter using @arkade-os/sdk.
|
|
4
|
-
*
|
|
3
|
+
* Implements IProtocolAdapter using @arkade-os/sdk v0.4.x.
|
|
4
|
+
*
|
|
5
|
+
* SDK API facts for v0.4.x:
|
|
6
|
+
* - `wallet.getBalance()` includes `assets: { assetId, amount }[]`
|
|
7
|
+
* - `wallet.assetManager.getAssetDetails(assetId)` resolves supply + metadata
|
|
8
|
+
* - `wallet.send({ address, assets: [...] })` sends Arkade-native assets
|
|
9
|
+
* - `wallet.sendBitcoin({ address, amount })` still sends BTC
|
|
10
|
+
* - `wallet.getTransactionHistory()` → ArkTransaction[]
|
|
11
|
+
* where ArkTransaction = { key, type: TxType, amount: number, settled: boolean, createdAt: number }
|
|
12
|
+
* - `TxType.TxSent = "SENT"`, `TxType.TxReceived = "RECEIVED"`
|
|
13
|
+
* - `WalletBalance.boarding.total` (number), `.settled`, `.preconfirmed`, `.available`, `.recoverable`, `.total`
|
|
5
14
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
15
|
+
import { mnemonicToSeedSync } from "@scure/bip39";
|
|
16
|
+
import { HDKey } from "@scure/bip32";
|
|
17
|
+
import { signLnMessage, verifyLnMessage } from "../lib/ln-message-sign.js";
|
|
18
|
+
import { log } from "../lib/log.js";
|
|
19
|
+
import { arkadeClientManager } from "../lib/arkade-client-manager.js";
|
|
20
|
+
import { arkadeSwapsClientManager } from "../lib/arkade-swaps-client-manager.js";
|
|
21
|
+
import { Ramps, isSpendable, } from "@arkade-os/sdk";
|
|
22
|
+
import { PROTOCOL_OPERATIONS } from "../capabilities/operations.js";
|
|
23
|
+
import { formatSats, formatUnits, getAssetMetadata, getAssetName, getAssetPrecision, getAssetTicker, normalizeVtxos, selectVtxosByExpiry, sortVtxosByExpiry, toNumber, toPositiveIntegerBigInt, toStringValue, } from "../lib/arkade-helpers.js";
|
|
24
|
+
import { convertArkTxToUnifiedAll } from "../lib/arkade-converters.js";
|
|
25
|
+
import { ProtocolError, ConnectionError, } from "../types/base.js";
|
|
26
|
+
/** Bare bolt11 prefixes (lnbc / lntb / lnbcrt / lnsb) — case-insensitive. */
|
|
27
|
+
function isLightningInvoice(value) {
|
|
28
|
+
if (!value)
|
|
29
|
+
return false;
|
|
30
|
+
const lower = value.trim().toLowerCase();
|
|
31
|
+
// Strip a `lightning:` URI prefix if present.
|
|
32
|
+
const body = lower.startsWith("lightning:") ? lower.slice("lightning:".length) : lower;
|
|
33
|
+
return /^ln(bc|tb|bcrt|sb)/.test(body);
|
|
34
|
+
}
|
|
35
|
+
function stripLightningPrefix(value) {
|
|
36
|
+
const trimmed = value.trim();
|
|
37
|
+
return trimmed.toLowerCase().startsWith("lightning:")
|
|
38
|
+
? trimmed.slice("lightning:".length)
|
|
39
|
+
: trimmed;
|
|
40
|
+
}
|
|
41
|
+
function isArkadeAddress(value) {
|
|
42
|
+
return /^(ark1|tark1)/i.test(value.trim());
|
|
43
|
+
}
|
|
9
44
|
export class ArkadeAdapter {
|
|
10
45
|
constructor() {
|
|
11
|
-
this.protocolName =
|
|
46
|
+
this.protocolName = "ARKADE";
|
|
47
|
+
this.supportedLayers = ["BTC_ARKADE", "BTC_L1", "ARKADE_ARKADE"];
|
|
48
|
+
this.version = "1.0.0";
|
|
12
49
|
this.capabilities = PROTOCOL_OPERATIONS.ARKADE;
|
|
13
|
-
this.supportedLayers = ['BTC_ARKADE', 'BTC_L1', 'ARKADE_ARKADE'];
|
|
14
|
-
this.version = '1.0.0';
|
|
15
50
|
this.config = null;
|
|
51
|
+
this.assetDetailsCache = new Map();
|
|
16
52
|
}
|
|
17
|
-
//
|
|
53
|
+
// =========================================================================
|
|
18
54
|
// Connection Management
|
|
19
|
-
//
|
|
55
|
+
// =========================================================================
|
|
20
56
|
async connect(config) {
|
|
21
57
|
const arkadeConfig = config;
|
|
22
58
|
if (!arkadeConfig.mnemonic) {
|
|
23
|
-
throw new ConnectionError(
|
|
59
|
+
throw new ConnectionError("Wallet recovery secret is required for Arkade wallet", "ARKADE");
|
|
24
60
|
}
|
|
25
61
|
if (!arkadeConfig.arkServerUrl) {
|
|
26
|
-
throw new ConnectionError(
|
|
62
|
+
throw new ConnectionError("arkServerUrl is required for Arkade wallet", "ARKADE");
|
|
27
63
|
}
|
|
28
64
|
try {
|
|
29
65
|
await arkadeClientManager.initialize(arkadeConfig);
|
|
30
66
|
this.config = arkadeConfig;
|
|
31
|
-
|
|
67
|
+
this.assetDetailsCache.clear();
|
|
68
|
+
log.info("[ArkadeAdapter] Connected to Arkade successfully");
|
|
69
|
+
// Initialize the Boltz swap client in the background. Failures are
|
|
70
|
+
// non-fatal — swaps just stay unavailable until the next connect.
|
|
71
|
+
const wallet = arkadeClientManager.getWallet();
|
|
72
|
+
arkadeSwapsClientManager.initialize(wallet).catch((error) => {
|
|
73
|
+
log.warn("[ArkadeAdapter] Boltz swaps init failed (Lightning swaps unavailable):", error);
|
|
74
|
+
});
|
|
32
75
|
}
|
|
33
76
|
catch (error) {
|
|
34
77
|
const msg = error instanceof Error ? error.message : String(error);
|
|
35
|
-
throw new ConnectionError(`Failed to connect to Arkade: ${msg}`,
|
|
78
|
+
throw new ConnectionError(`Failed to connect to Arkade: ${msg}`, "ARKADE");
|
|
36
79
|
}
|
|
37
80
|
}
|
|
38
81
|
async disconnect() {
|
|
82
|
+
// Dispose Boltz swaps client first (stops SwapManager monitoring) so it
|
|
83
|
+
// doesn't try to use the wallet after we tear it down.
|
|
84
|
+
await arkadeSwapsClientManager.dispose();
|
|
39
85
|
await arkadeClientManager.disconnect();
|
|
40
86
|
this.config = null;
|
|
41
|
-
|
|
87
|
+
this.assetDetailsCache.clear();
|
|
88
|
+
log.info("[ArkadeAdapter] Disconnected from Arkade");
|
|
42
89
|
}
|
|
43
90
|
isConnected() {
|
|
44
91
|
return arkadeClientManager.isInitialized();
|
|
45
92
|
}
|
|
46
93
|
async getConnectionInfo() {
|
|
47
94
|
if (!this.isConnected()) {
|
|
48
|
-
throw new ProtocolError(
|
|
95
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
49
96
|
}
|
|
50
97
|
return {
|
|
51
|
-
protocol:
|
|
98
|
+
protocol: "ARKADE",
|
|
52
99
|
connected: true,
|
|
53
|
-
network: this.config?.network ??
|
|
100
|
+
network: this.config?.network ?? "signet",
|
|
54
101
|
syncStatus: { synced: true, progress: 100 },
|
|
55
102
|
};
|
|
56
103
|
}
|
|
57
|
-
//
|
|
104
|
+
// =========================================================================
|
|
58
105
|
// Asset Operations
|
|
59
|
-
//
|
|
106
|
+
// =========================================================================
|
|
60
107
|
async listAssets() {
|
|
61
108
|
if (!this.isConnected()) {
|
|
62
|
-
throw new ProtocolError(
|
|
109
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
63
110
|
}
|
|
64
111
|
try {
|
|
65
112
|
const wallet = arkadeClientManager.getWallet();
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const preconfirmed =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
const rawBalance = await wallet.getBalance();
|
|
114
|
+
const balance = await this.getWalletBalanceSummary(wallet);
|
|
115
|
+
const totalSats = balance.total;
|
|
116
|
+
const preconfirmed = balance.preconfirmed;
|
|
117
|
+
const btcAsset = {
|
|
118
|
+
id: "BTC",
|
|
119
|
+
name: "Bitcoin (Arkade)",
|
|
120
|
+
ticker: "BTC",
|
|
121
|
+
precision: 8,
|
|
122
|
+
protocol: "ARKADE",
|
|
123
|
+
layer: "BTC_ARKADE",
|
|
124
|
+
balance: {
|
|
125
|
+
total: totalSats,
|
|
126
|
+
// preconfirmed VTXOs are spendable (isSpendable = !vtxo.isSpent in the SDK),
|
|
127
|
+
// so they count as available just like settled ones.
|
|
128
|
+
available: balance.available,
|
|
129
|
+
pending: 0,
|
|
130
|
+
locked: 0,
|
|
131
|
+
totalDisplay: formatSats(totalSats),
|
|
132
|
+
availableDisplay: formatSats(balance.available),
|
|
133
|
+
},
|
|
134
|
+
capabilities: {
|
|
135
|
+
canSend: true,
|
|
136
|
+
canReceive: true,
|
|
137
|
+
canSwap: false,
|
|
138
|
+
supportsLightning: false,
|
|
139
|
+
supportsOnchain: true,
|
|
140
|
+
},
|
|
141
|
+
metadata: {
|
|
142
|
+
boarding: balance.boardingTotal,
|
|
143
|
+
settled: balance.settled,
|
|
144
|
+
preconfirmed,
|
|
145
|
+
recoverable: balance.recoverable,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const rawAssets = Array.isArray(rawBalance?.assets) ? rawBalance.assets : [];
|
|
149
|
+
const arkadeAssets = await Promise.all(rawAssets
|
|
150
|
+
.filter((entry) => toStringValue(entry?.assetId) !== "" && toNumber(entry?.amount) > 0)
|
|
151
|
+
.map(async (entry) => {
|
|
152
|
+
const assetId = toStringValue(entry.assetId);
|
|
153
|
+
const amount = toNumber(entry.amount);
|
|
154
|
+
const details = await this.getCachedAssetDetails(wallet, assetId);
|
|
155
|
+
const metadata = getAssetMetadata(details);
|
|
156
|
+
const precision = getAssetPrecision(metadata);
|
|
157
|
+
const ticker = getAssetTicker(assetId, metadata);
|
|
158
|
+
const name = getAssetName(assetId, ticker, metadata);
|
|
159
|
+
const icon = typeof metadata?.icon === "string" ? metadata.icon : undefined;
|
|
160
|
+
const asset = {
|
|
161
|
+
id: assetId,
|
|
162
|
+
name,
|
|
163
|
+
ticker,
|
|
164
|
+
precision,
|
|
165
|
+
protocol: "ARKADE",
|
|
166
|
+
layer: "ARKADE_ARKADE",
|
|
77
167
|
balance: {
|
|
78
|
-
total:
|
|
79
|
-
available:
|
|
80
|
-
pending:
|
|
168
|
+
total: amount,
|
|
169
|
+
available: amount,
|
|
170
|
+
pending: 0,
|
|
81
171
|
locked: 0,
|
|
82
|
-
totalDisplay:
|
|
83
|
-
availableDisplay:
|
|
172
|
+
totalDisplay: formatUnits(amount, precision),
|
|
173
|
+
availableDisplay: formatUnits(amount, precision),
|
|
84
174
|
},
|
|
175
|
+
icon,
|
|
85
176
|
capabilities: {
|
|
86
177
|
canSend: true,
|
|
87
178
|
canReceive: true,
|
|
88
179
|
canSwap: false,
|
|
89
180
|
supportsLightning: false,
|
|
90
|
-
supportsOnchain:
|
|
181
|
+
supportsOnchain: false,
|
|
91
182
|
},
|
|
183
|
+
// Don't spread `details` — the Arkade SDK returns BigInt /
|
|
184
|
+
// Uint8Array fields (totalSupply, raw identifiers) that crash
|
|
185
|
+
// chrome.runtime.sendMessage with "Could not serialize message".
|
|
92
186
|
metadata: {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
preconfirmed,
|
|
96
|
-
recoverable: this.toNumber(balance?.recoverable),
|
|
187
|
+
arkadeAssetId: assetId,
|
|
188
|
+
decimals: precision,
|
|
97
189
|
},
|
|
98
|
-
}
|
|
190
|
+
};
|
|
191
|
+
return asset;
|
|
192
|
+
}));
|
|
193
|
+
return [btcAsset, ...arkadeAssets];
|
|
99
194
|
}
|
|
100
195
|
catch (error) {
|
|
101
196
|
const msg = error instanceof Error ? error.message : String(error);
|
|
102
|
-
throw new ProtocolError(`Failed to list assets: ${msg}`,
|
|
197
|
+
throw new ProtocolError(`Failed to list assets: ${msg}`, "ARKADE", "LIST_ASSETS_ERROR");
|
|
103
198
|
}
|
|
104
199
|
}
|
|
105
200
|
async getAsset(assetId) {
|
|
106
201
|
const assets = await this.listAssets();
|
|
107
|
-
const asset = assets.find(a => a.id === assetId || a.ticker === assetId);
|
|
202
|
+
const asset = assets.find((a) => a.id === assetId || a.ticker === assetId);
|
|
108
203
|
if (!asset) {
|
|
109
|
-
throw new ProtocolError(`Asset not found: ${assetId}`,
|
|
204
|
+
throw new ProtocolError(`Asset not found: ${assetId}`, "ARKADE", "ASSET_NOT_FOUND");
|
|
110
205
|
}
|
|
111
206
|
return asset;
|
|
112
207
|
}
|
|
113
208
|
async getAssetBalance(assetId) {
|
|
209
|
+
if (assetId === "BTC" || assetId.toLowerCase() === "btc") {
|
|
210
|
+
const asset = await this.getAsset("BTC");
|
|
211
|
+
return asset.balance;
|
|
212
|
+
}
|
|
114
213
|
const asset = await this.getAsset(assetId);
|
|
115
214
|
return asset.balance;
|
|
116
215
|
}
|
|
117
|
-
async refreshBalances() {
|
|
118
|
-
|
|
216
|
+
async refreshBalances() {
|
|
217
|
+
// Balances are fetched live on each call
|
|
218
|
+
}
|
|
219
|
+
// =========================================================================
|
|
119
220
|
// Transaction Operations
|
|
120
|
-
//
|
|
221
|
+
// =========================================================================
|
|
121
222
|
async listTransactions(filter) {
|
|
122
223
|
if (!this.isConnected()) {
|
|
123
|
-
throw new ProtocolError(
|
|
224
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
124
225
|
}
|
|
125
226
|
try {
|
|
126
227
|
const wallet = arkadeClientManager.getWallet();
|
|
127
228
|
const history = await wallet.getTransactionHistory();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
229
|
+
const resolveDetails = (assetId) => this.getCachedAssetDetails(wallet, assetId);
|
|
230
|
+
const expanded = await Promise.all((history ?? []).map((item) => convertArkTxToUnifiedAll(item, resolveDetails)));
|
|
231
|
+
const validTxs = expanded.flat();
|
|
232
|
+
return validTxs
|
|
233
|
+
.filter((tx) => {
|
|
132
234
|
if (!filter)
|
|
133
235
|
return true;
|
|
236
|
+
if (filter.asset && tx.asset?.id !== filter.asset)
|
|
237
|
+
return false;
|
|
134
238
|
if (filter.type && tx.type !== filter.type)
|
|
135
239
|
return false;
|
|
136
240
|
if (filter.status && tx.status !== filter.status)
|
|
137
241
|
return false;
|
|
242
|
+
if (filter.fromTimestamp && tx.timestamp < filter.fromTimestamp)
|
|
243
|
+
return false;
|
|
244
|
+
if (filter.toTimestamp && tx.timestamp > filter.toTimestamp)
|
|
245
|
+
return false;
|
|
138
246
|
return true;
|
|
139
247
|
})
|
|
140
248
|
.slice(filter?.offset ?? 0, filter?.limit ? (filter.offset ?? 0) + filter.limit : undefined);
|
|
141
249
|
}
|
|
142
250
|
catch (error) {
|
|
143
251
|
const msg = error instanceof Error ? error.message : String(error);
|
|
144
|
-
throw new ProtocolError(`Failed to list transactions: ${msg}`,
|
|
252
|
+
throw new ProtocolError(`Failed to list transactions: ${msg}`, "ARKADE", "LIST_TRANSACTIONS_ERROR");
|
|
145
253
|
}
|
|
146
254
|
}
|
|
147
255
|
async getTransaction(txId) {
|
|
148
256
|
const txs = await this.listTransactions();
|
|
149
|
-
const tx = txs.find(t => t.id === txId);
|
|
257
|
+
const tx = txs.find((t) => t.id === txId);
|
|
150
258
|
if (!tx) {
|
|
151
|
-
throw new ProtocolError(`Transaction not found: ${txId}`,
|
|
259
|
+
throw new ProtocolError(`Transaction not found: ${txId}`, "ARKADE", "TX_NOT_FOUND");
|
|
152
260
|
}
|
|
153
261
|
return tx;
|
|
154
262
|
}
|
|
155
|
-
//
|
|
263
|
+
// =========================================================================
|
|
156
264
|
// Payment Operations
|
|
157
|
-
//
|
|
265
|
+
// =========================================================================
|
|
158
266
|
async createInvoice(request) {
|
|
159
267
|
if (!this.isConnected()) {
|
|
160
|
-
throw new ProtocolError(
|
|
268
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const wallet = arkadeClientManager.getWallet();
|
|
272
|
+
const address = await wallet.getAddress();
|
|
273
|
+
return {
|
|
274
|
+
invoice: address,
|
|
275
|
+
paymentHash: "",
|
|
276
|
+
amount: request.amount,
|
|
277
|
+
expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
278
|
+
description: request.description ?? "Arkade receiving address",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
283
|
+
throw new ProtocolError(`Failed to create invoice: ${msg}`, "ARKADE", "CREATE_INVOICE_ERROR");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generate a Boltz reverse-swap Lightning invoice that, when paid, lands
|
|
288
|
+
* the funds in this Arkade wallet as a VTXO. Requires amount > 0 — Boltz
|
|
289
|
+
* can't issue an amountless invoice. The embedded `SwapManager` claims
|
|
290
|
+
* the VHTLC automatically once the LN payment settles.
|
|
291
|
+
*/
|
|
292
|
+
async createArkadeLightningInvoice(request) {
|
|
293
|
+
if (!this.isConnected()) {
|
|
294
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
295
|
+
}
|
|
296
|
+
if (!request.amount || request.amount <= 0) {
|
|
297
|
+
throw new ProtocolError("Amount is required for Boltz Lightning invoices into Arkade", "ARKADE", "INVALID_AMOUNT");
|
|
298
|
+
}
|
|
299
|
+
if (!arkadeSwapsClientManager.isInitialized()) {
|
|
300
|
+
throw new ProtocolError("Lightning swaps are not ready yet. Try again in a moment.", "ARKADE", "SWAPS_NOT_READY");
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const swaps = arkadeSwapsClientManager.getClient();
|
|
304
|
+
const result = await swaps.createLightningInvoice({
|
|
305
|
+
amount: request.amount,
|
|
306
|
+
description: request.description,
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
invoice: result.invoice,
|
|
310
|
+
paymentHash: result.paymentHash ?? "",
|
|
311
|
+
amount: request.amount,
|
|
312
|
+
expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
313
|
+
description: request.description ?? "Boltz reverse swap into Arkade",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
318
|
+
throw new ProtocolError(`Failed to create Boltz Lightning invoice: ${msg}`, "ARKADE", "CREATE_INVOICE_ERROR");
|
|
161
319
|
}
|
|
162
|
-
const wallet = arkadeClientManager.getWallet();
|
|
163
|
-
const address = await wallet.getAddress();
|
|
164
|
-
return {
|
|
165
|
-
invoice: address,
|
|
166
|
-
paymentHash: '',
|
|
167
|
-
amount: request.amount,
|
|
168
|
-
expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
169
|
-
description: request.description ?? 'Arkade receiving address',
|
|
170
|
-
};
|
|
171
320
|
}
|
|
172
321
|
async decodeInvoice(invoice) {
|
|
173
|
-
|
|
322
|
+
// Arkade uses addresses, not bolt11 invoices
|
|
323
|
+
return {
|
|
324
|
+
paymentHash: "",
|
|
325
|
+
expiresAt: 0,
|
|
326
|
+
destination: invoice,
|
|
327
|
+
};
|
|
174
328
|
}
|
|
175
329
|
async sendPayment(request) {
|
|
176
330
|
if (!this.isConnected()) {
|
|
177
|
-
throw new ProtocolError(
|
|
331
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
332
|
+
}
|
|
333
|
+
// Lightning invoice → Boltz submarine swap (Arkade → Lightning).
|
|
334
|
+
// The swap library extracts the amount from the invoice itself, so
|
|
335
|
+
// amountless invoices cannot be paid this way (Boltz rejects them
|
|
336
|
+
// with "0 is less than minimal of 333"). Reject early with a clear
|
|
337
|
+
// error rather than letting Boltz's cryptic message bubble up.
|
|
338
|
+
if (isLightningInvoice(request.invoice)) {
|
|
339
|
+
if (!arkadeSwapsClientManager.isInitialized()) {
|
|
340
|
+
throw new ProtocolError("Lightning swaps are not ready yet. Try again in a moment.", "ARKADE", "SWAPS_NOT_READY");
|
|
341
|
+
}
|
|
342
|
+
const invoiceBody = stripLightningPrefix(request.invoice);
|
|
343
|
+
try {
|
|
344
|
+
const swaps = arkadeSwapsClientManager.getClient();
|
|
345
|
+
const result = await swaps.sendLightningPayment({ invoice: invoiceBody });
|
|
346
|
+
return {
|
|
347
|
+
paymentHash: result.preimage ?? result.txid ?? "",
|
|
348
|
+
amount: result.amount ?? request.amount ?? 0,
|
|
349
|
+
fee: 0,
|
|
350
|
+
// Boltz submarine swap; the swap can still fail in the HODL/claim
|
|
351
|
+
// phase. Caller polls `getPaymentStatus` to reach a terminal state.
|
|
352
|
+
status: "pending",
|
|
353
|
+
timestamp: Date.now(),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
358
|
+
// Translate Boltz error strings to actionable messages.
|
|
359
|
+
if (/less than minimal/i.test(msg)) {
|
|
360
|
+
throw new ProtocolError("Arkade can't pay amountless Lightning invoices. Please use a different route or ask the recipient for an invoice with an amount.", "ARKADE", "INVALID_AMOUNT");
|
|
361
|
+
}
|
|
362
|
+
if (/vHTLC.*already exists/i.test(msg)) {
|
|
363
|
+
throw new ProtocolError("A swap for this invoice is already in progress. Wait for it to complete or refund before retrying.", "ARKADE", "SWAP_IN_PROGRESS");
|
|
364
|
+
}
|
|
365
|
+
throw new ProtocolError(`Failed to send Lightning payment via Boltz: ${msg}`, "ARKADE", "SEND_PAYMENT_ERROR");
|
|
366
|
+
}
|
|
178
367
|
}
|
|
368
|
+
// Non-Lightning destinations: existing Ark / on-chain BTC path.
|
|
179
369
|
if (!request.amount || request.amount <= 0) {
|
|
180
|
-
throw new ProtocolError(
|
|
370
|
+
throw new ProtocolError("Amount is required for Arkade payments", "ARKADE", "INVALID_AMOUNT");
|
|
181
371
|
}
|
|
182
372
|
try {
|
|
183
373
|
const wallet = arkadeClientManager.getWallet();
|
|
374
|
+
const selectedVtxos = await this.selectSpendableBtcVtxos(wallet, request.amount);
|
|
184
375
|
const txid = await wallet.sendBitcoin({
|
|
185
|
-
address: request.invoice,
|
|
186
|
-
amount: request.amount,
|
|
376
|
+
address: request.invoice, // Ark or on-chain address
|
|
377
|
+
amount: request.amount, // satoshis
|
|
378
|
+
...(selectedVtxos ? { selectedVtxos } : {}),
|
|
187
379
|
});
|
|
188
380
|
return {
|
|
189
381
|
paymentHash: txid,
|
|
190
382
|
amount: request.amount,
|
|
191
383
|
fee: 0,
|
|
192
|
-
|
|
384
|
+
// Ark VTXO sends are immediately valid once sendBitcoin resolves.
|
|
385
|
+
// On-chain destinations still need confirmation, so callers should
|
|
386
|
+
// keep polling via getPaymentStatus.
|
|
387
|
+
status: (isArkadeAddress(request.invoice) ? "confirmed" : "pending"),
|
|
193
388
|
timestamp: Date.now(),
|
|
194
389
|
};
|
|
195
390
|
}
|
|
196
391
|
catch (error) {
|
|
197
392
|
const msg = error instanceof Error ? error.message : String(error);
|
|
198
|
-
throw new ProtocolError(`Failed to send payment: ${msg}`,
|
|
393
|
+
throw new ProtocolError(`Failed to send payment: ${msg}`, "ARKADE", "SEND_PAYMENT_ERROR");
|
|
199
394
|
}
|
|
200
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Resolve a payment's terminal state from the SDK's transaction history.
|
|
398
|
+
* `paymentHash` is the txid returned by `sendBitcoin` / `sendLightningPayment`
|
|
399
|
+
* (Boltz returns a preimage as a fallback if there's no on-chain txid yet —
|
|
400
|
+
* in that case we don't have a history row and the payment stays pending).
|
|
401
|
+
*/
|
|
201
402
|
async getPaymentStatus(paymentHash) {
|
|
202
|
-
|
|
403
|
+
if (!this.isConnected() || !paymentHash) {
|
|
404
|
+
return { paymentHash, status: "pending" };
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const wallet = arkadeClientManager.getWallet();
|
|
408
|
+
const history = (await wallet.getTransactionHistory());
|
|
409
|
+
const match = history.find((entry) => {
|
|
410
|
+
const key = entry?.key;
|
|
411
|
+
const id = typeof key === "string" ? key : key?.txid;
|
|
412
|
+
return id === paymentHash;
|
|
413
|
+
});
|
|
414
|
+
if (!match) {
|
|
415
|
+
return { paymentHash, status: "pending" };
|
|
416
|
+
}
|
|
417
|
+
// Arkade's reference wallet treats SENT history rows as settled while
|
|
418
|
+
// leaving unsettled RECEIVED rows as preconfirmed/pending.
|
|
419
|
+
const isSent = match.type === "SENT";
|
|
420
|
+
return {
|
|
421
|
+
paymentHash,
|
|
422
|
+
status: (isSent || match.settled ? "confirmed" : "pending"),
|
|
423
|
+
amount: match.amount,
|
|
424
|
+
timestamp: Date.now(),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
log.warn("[ArkadeAdapter] getPaymentStatus history lookup failed:", error);
|
|
429
|
+
return { paymentHash, status: "pending" };
|
|
430
|
+
}
|
|
203
431
|
}
|
|
204
|
-
//
|
|
432
|
+
// =========================================================================
|
|
205
433
|
// Address Operations
|
|
206
|
-
//
|
|
434
|
+
// =========================================================================
|
|
207
435
|
async getReceiveAddress(assetId) {
|
|
208
436
|
if (!this.isConnected()) {
|
|
209
|
-
throw new ProtocolError(
|
|
437
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
210
438
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
439
|
+
try {
|
|
440
|
+
const wallet = arkadeClientManager.getWallet();
|
|
441
|
+
// 'onchain' or 'boarding' → return on-chain boarding address
|
|
442
|
+
if (assetId === "onchain" || assetId === "boarding") {
|
|
443
|
+
const address = await wallet.getBoardingAddress();
|
|
444
|
+
return { address, format: "BTC_ADDRESS", asset: "BTC" };
|
|
445
|
+
}
|
|
446
|
+
// Default → Ark address (off-chain)
|
|
447
|
+
const address = await wallet.getAddress();
|
|
448
|
+
return {
|
|
449
|
+
address,
|
|
450
|
+
format: "ARKADE_ADDRESS",
|
|
451
|
+
asset: assetId && assetId !== "BTC" ? assetId : "BTC",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
456
|
+
throw new ProtocolError(`Failed to get receive address: ${msg}`, "ARKADE", "GET_ADDRESS_ERROR");
|
|
215
457
|
}
|
|
216
|
-
const address = await wallet.getAddress();
|
|
217
|
-
return { address, format: 'ARKADE_ADDRESS', asset: 'BTC' };
|
|
218
458
|
}
|
|
219
|
-
//
|
|
459
|
+
// =========================================================================
|
|
220
460
|
// Node & Balance Operations
|
|
221
|
-
//
|
|
461
|
+
// =========================================================================
|
|
222
462
|
async getNodeInfo() {
|
|
223
463
|
if (!this.isConnected()) {
|
|
224
|
-
throw new ProtocolError(
|
|
464
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const wallet = arkadeClientManager.getWallet();
|
|
468
|
+
const balance = await this.getWalletBalanceSummary(wallet);
|
|
469
|
+
const spendableSats = balance.available;
|
|
470
|
+
return {
|
|
471
|
+
channelsBalanceMsat: spendableSats * 1000,
|
|
472
|
+
maxPayableMsat: spendableSats * 1000,
|
|
473
|
+
onchainBalanceMsat: balance.boardingConfirmed * 1000,
|
|
474
|
+
pendingOnchainBalanceMsat: balance.boardingUnconfirmed * 1000,
|
|
475
|
+
maxReceivableMsat: 0,
|
|
476
|
+
inboundLiquidityMsats: 0,
|
|
477
|
+
connectedPeers: [],
|
|
478
|
+
utxos: 0,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
483
|
+
throw new ProtocolError(`Failed to get node info: ${msg}`, "ARKADE", "NODE_INFO_ERROR");
|
|
225
484
|
}
|
|
226
|
-
const wallet = arkadeClientManager.getWallet();
|
|
227
|
-
const balance = await wallet.getBalance();
|
|
228
|
-
const spendable = this.toNumber(balance?.available);
|
|
229
|
-
return {
|
|
230
|
-
channelsBalanceMsat: spendable * 1000,
|
|
231
|
-
maxPayableMsat: spendable * 1000,
|
|
232
|
-
onchainBalanceMsat: this.toNumber(balance?.boarding?.total) * 1000,
|
|
233
|
-
pendingOnchainBalanceMsat: 0,
|
|
234
|
-
maxReceivableMsat: 0,
|
|
235
|
-
inboundLiquidityMsats: 0,
|
|
236
|
-
connectedPeers: [],
|
|
237
|
-
utxos: 0,
|
|
238
|
-
};
|
|
239
485
|
}
|
|
240
486
|
async getBtcBalance() {
|
|
241
487
|
if (!this.isConnected()) {
|
|
242
|
-
throw new ProtocolError(
|
|
488
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
243
489
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
490
|
+
try {
|
|
491
|
+
const wallet = arkadeClientManager.getWallet();
|
|
492
|
+
const balance = await this.getWalletBalanceSummary(wallet);
|
|
493
|
+
// preconfirmed VTXOs are spendable (isSpendable = !vtxo.isSpent in the SDK),
|
|
494
|
+
// so include them in `confirmed` so the Withdraw UI sees the full spendable balance.
|
|
495
|
+
const confirmed = balance.available; // settled + preconfirmed
|
|
496
|
+
const total = balance.total;
|
|
497
|
+
const unconfirmed = Math.max(total - confirmed, 0);
|
|
498
|
+
return { confirmed, unconfirmed, total };
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
502
|
+
throw new ProtocolError(`Failed to get BTC balance: ${msg}`, "ARKADE", "BALANCE_ERROR");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async listChannels() {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
async listPayments() {
|
|
509
|
+
const txs = await this.listTransactions();
|
|
510
|
+
return { payments: txs };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get all VTXOs, sorted by batchExpiry ascending (expiry-first).
|
|
514
|
+
* This ensures UI consumers see soon-to-expire VTXOs first, and any
|
|
515
|
+
* manual coin selection naturally picks the shortest-lived coins.
|
|
516
|
+
*/
|
|
517
|
+
async getVtxos() {
|
|
518
|
+
if (!this.isConnected()) {
|
|
519
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const wallet = arkadeClientManager.getWallet();
|
|
523
|
+
const vtxos = await wallet.getVtxos();
|
|
524
|
+
const sorted = sortVtxosByExpiry(vtxos);
|
|
525
|
+
return normalizeVtxos(sorted).map((vtxo) => ({
|
|
526
|
+
txid: vtxo.txid,
|
|
527
|
+
vout: vtxo.vout,
|
|
528
|
+
value: vtxo.value,
|
|
529
|
+
state: vtxo.state,
|
|
530
|
+
batchTxid: vtxo.batchTxid,
|
|
531
|
+
batchExpiry: vtxo.batchExpiry,
|
|
532
|
+
createdAt: vtxo.createdAt,
|
|
533
|
+
assets: vtxo.assets,
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
538
|
+
throw new ProtocolError(`Failed to get VTXOs: ${msg}`, "ARKADE", "GET_VTXOS_ERROR");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async getBoardingUtxos() {
|
|
542
|
+
if (!this.isConnected()) {
|
|
543
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const wallet = arkadeClientManager.getWallet();
|
|
547
|
+
const utxos = await wallet.getBoardingUtxos();
|
|
548
|
+
return (utxos ?? []).map((u) => ({
|
|
549
|
+
txid: u.txid,
|
|
550
|
+
vout: u.vout,
|
|
551
|
+
value: u.value,
|
|
552
|
+
confirmed: u.status?.confirmed ?? false,
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
557
|
+
throw new ProtocolError(`Failed to get boarding UTXOs: ${msg}`, "ARKADE", "GET_BOARDING_UTXOS_ERROR");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Onboard — settle boarding UTXOs into VTXOs via a Commitment Transaction.
|
|
562
|
+
* Requires at least one confirmed boarding UTXO.
|
|
563
|
+
* Returns the commitment txid.
|
|
564
|
+
*/
|
|
565
|
+
async onboard() {
|
|
566
|
+
if (!this.isConnected()) {
|
|
567
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const wallet = arkadeClientManager.getWallet();
|
|
571
|
+
// Get current fee info from server
|
|
572
|
+
const info = await wallet.arkProvider.getInfo();
|
|
573
|
+
const commitmentTxid = await new Ramps(wallet).onboard(info.fees);
|
|
574
|
+
return { txid: commitmentTxid };
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
578
|
+
throw new ProtocolError(`Onboard failed: ${msg}`, "ARKADE", "ONBOARD_ERROR");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Offboard — collaborative exit: convert VTXOs back to an on-chain Bitcoin UTXO.
|
|
583
|
+
* @param address Bitcoin on-chain destination (bc1/tb1)
|
|
584
|
+
* @param amount Optional sats to offboard; undefined = exit all
|
|
585
|
+
*/
|
|
586
|
+
async offboard(address, amount) {
|
|
587
|
+
if (!this.isConnected()) {
|
|
588
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
589
|
+
}
|
|
590
|
+
if (!address) {
|
|
591
|
+
throw new ProtocolError("Destination address required for offboard", "ARKADE", "INVALID_ADDRESS");
|
|
592
|
+
}
|
|
593
|
+
if (amount !== undefined && (!Number.isInteger(amount) || amount <= 0)) {
|
|
594
|
+
throw new ProtocolError(`Invalid offboard amount: ${amount} (must be a positive integer of sats)`, "ARKADE", "INVALID_AMOUNT");
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const wallet = arkadeClientManager.getWallet();
|
|
598
|
+
const info = await wallet.arkProvider.getInfo();
|
|
599
|
+
const exitTxid = await new Ramps(wallet).offboard(address, info.fees, amount !== undefined ? BigInt(amount) : undefined);
|
|
600
|
+
return { txid: exitTxid };
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
604
|
+
throw new ProtocolError(`Offboard failed: ${msg}`, "ARKADE", "OFFBOARD_ERROR");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async listTransfers(_options) {
|
|
608
|
+
return { transfers: [] };
|
|
609
|
+
}
|
|
610
|
+
// =========================================================================
|
|
611
|
+
// Asset / On-chain Send
|
|
612
|
+
// =========================================================================
|
|
613
|
+
async sendAsset(params) {
|
|
614
|
+
if (!this.isConnected()) {
|
|
615
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
616
|
+
}
|
|
617
|
+
const request = (params ?? {});
|
|
618
|
+
const assetId = toStringValue(request.assetId);
|
|
619
|
+
const amount = toPositiveIntegerBigInt(request.amount);
|
|
620
|
+
const recipientId = toStringValue(request.recipientId);
|
|
621
|
+
if (!assetId) {
|
|
622
|
+
throw new ProtocolError("Asset ID is required", "ARKADE", "INVALID_ASSET");
|
|
623
|
+
}
|
|
624
|
+
if (!recipientId) {
|
|
625
|
+
throw new ProtocolError("Recipient address is required", "ARKADE", "INVALID_ADDRESS");
|
|
626
|
+
}
|
|
627
|
+
if (amount <= 0n) {
|
|
628
|
+
throw new ProtocolError("Amount must be greater than zero", "ARKADE", "INVALID_AMOUNT");
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const wallet = arkadeClientManager.getWallet();
|
|
632
|
+
const txid = await wallet.send({
|
|
633
|
+
address: recipientId,
|
|
634
|
+
assets: [{ assetId, amount }],
|
|
635
|
+
});
|
|
636
|
+
return { txid };
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
640
|
+
throw new ProtocolError(`Failed to send Arkade asset: ${msg}`, "ARKADE", "SEND_ASSET_ERROR");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
257
643
|
async sendBtcOnchain(params) {
|
|
258
644
|
if (!this.isConnected()) {
|
|
259
|
-
throw new ProtocolError(
|
|
645
|
+
throw new ProtocolError("Not connected", "ARKADE", "NOT_CONNECTED");
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const wallet = arkadeClientManager.getWallet();
|
|
649
|
+
const selectedVtxos = await this.selectSpendableBtcVtxos(wallet, params.amount);
|
|
650
|
+
const txid = await wallet.sendBitcoin({
|
|
651
|
+
address: params.address,
|
|
652
|
+
amount: params.amount,
|
|
653
|
+
...(selectedVtxos ? { selectedVtxos } : {}),
|
|
654
|
+
});
|
|
655
|
+
return { txid };
|
|
260
656
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
657
|
+
catch (error) {
|
|
658
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
659
|
+
throw new ProtocolError(`Failed to send BTC: ${msg}`, "ARKADE", "SEND_BTC_ERROR");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// =========================================================================
|
|
663
|
+
// Swap Operations (Not supported)
|
|
664
|
+
// =========================================================================
|
|
665
|
+
supportsSwaps() {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
async getSwapQuote(_request) {
|
|
669
|
+
throw new ProtocolError("Not supported", "ARKADE", "NOT_SUPPORTED");
|
|
670
|
+
}
|
|
671
|
+
async executeSwap(_quote) {
|
|
672
|
+
throw new ProtocolError("Not supported", "ARKADE", "NOT_SUPPORTED");
|
|
264
673
|
}
|
|
265
|
-
|
|
674
|
+
async getSwapStatus(_swapId) {
|
|
675
|
+
throw new ProtocolError("Not supported", "ARKADE", "NOT_SUPPORTED");
|
|
676
|
+
}
|
|
677
|
+
// =========================================================================
|
|
678
|
+
// Message Signing
|
|
679
|
+
// =========================================================================
|
|
680
|
+
async signMessage(message) {
|
|
681
|
+
if (!this.config?.mnemonic) {
|
|
682
|
+
throw new ProtocolError("Wallet mnemonic not available", "ARKADE", "NOT_CONNECTED");
|
|
683
|
+
}
|
|
684
|
+
const seed = mnemonicToSeedSync(this.config.mnemonic);
|
|
685
|
+
const node = HDKey.fromMasterSeed(seed).derive("m/138'/1");
|
|
686
|
+
if (!node.privateKey) {
|
|
687
|
+
throw new ProtocolError("Failed to derive message-signing key", "ARKADE", "KEY_DERIVATION_ERROR");
|
|
688
|
+
}
|
|
689
|
+
return signLnMessage(message, node.privateKey);
|
|
690
|
+
}
|
|
691
|
+
async verifyMessage(message, signature) {
|
|
692
|
+
return verifyLnMessage(message, signature);
|
|
693
|
+
}
|
|
694
|
+
// =========================================================================
|
|
266
695
|
// Private Helpers
|
|
267
|
-
//
|
|
268
|
-
|
|
696
|
+
// =========================================================================
|
|
697
|
+
/**
|
|
698
|
+
* Pre-select spendable BTC VTXOs for a sendBitcoin call using the
|
|
699
|
+
* expiry-first policy. Returns `undefined` (not an empty array) when no
|
|
700
|
+
* override should be applied — that lets the SDK fall back to its own
|
|
701
|
+
* selection when the fetch fails, the spendable set can't cover the target,
|
|
702
|
+
* or the target is non-positive. Mixed-asset VTXOs are filtered out.
|
|
703
|
+
*/
|
|
704
|
+
async selectSpendableBtcVtxos(wallet, targetSats) {
|
|
705
|
+
if (!Number.isFinite(targetSats) || targetSats <= 0)
|
|
706
|
+
return undefined;
|
|
269
707
|
try {
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
type: isSend ? 'send' : 'receive',
|
|
289
|
-
status: tx.settled || !isSend ? 'confirmed' : 'pending',
|
|
290
|
-
timestamp, amount: amountSats,
|
|
291
|
-
amountDisplay: this.formatSats(amountSats),
|
|
292
|
-
fee: 0, feeDisplay: '0.00000000',
|
|
293
|
-
asset: btcAsset,
|
|
294
|
-
protocolData: { type: tx.type, settled: tx.settled, key: tx.key },
|
|
295
|
-
};
|
|
708
|
+
const raw = await wallet.getVtxos();
|
|
709
|
+
const list = Array.isArray(raw)
|
|
710
|
+
? raw
|
|
711
|
+
: Array.isArray(raw?.vtxos)
|
|
712
|
+
? raw.vtxos
|
|
713
|
+
: [];
|
|
714
|
+
const spendableBtc = list.filter((vtxo) => {
|
|
715
|
+
if (!isSpendable(vtxo))
|
|
716
|
+
return false;
|
|
717
|
+
// Exclude VTXOs carrying assets — sendBitcoin would either reject
|
|
718
|
+
// them or accidentally burn the asset side. Pure BTC only.
|
|
719
|
+
const assets = vtxo.assets;
|
|
720
|
+
if (Array.isArray(assets) && assets.length > 0)
|
|
721
|
+
return false;
|
|
722
|
+
return true;
|
|
723
|
+
});
|
|
724
|
+
const selected = selectVtxosByExpiry(spendableBtc, targetSats);
|
|
725
|
+
return selected ?? undefined;
|
|
296
726
|
}
|
|
297
|
-
catch {
|
|
298
|
-
|
|
727
|
+
catch (error) {
|
|
728
|
+
log.warn("[ArkadeAdapter] selectSpendableBtcVtxos failed; falling back to SDK default selection:", error);
|
|
729
|
+
return undefined;
|
|
299
730
|
}
|
|
300
731
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
732
|
+
async getWalletBalanceSummary(wallet) {
|
|
733
|
+
const balance = await wallet.getBalance();
|
|
734
|
+
const normalized = {
|
|
735
|
+
boardingConfirmed: toNumber(balance?.boarding?.confirmed),
|
|
736
|
+
boardingUnconfirmed: toNumber(balance?.boarding?.unconfirmed),
|
|
737
|
+
boardingTotal: toNumber(balance?.boarding?.total),
|
|
738
|
+
settled: toNumber(balance?.settled),
|
|
739
|
+
preconfirmed: toNumber(balance?.preconfirmed),
|
|
740
|
+
available: toNumber(balance?.available),
|
|
741
|
+
recoverable: toNumber(balance?.recoverable),
|
|
742
|
+
total: toNumber(balance?.total),
|
|
743
|
+
};
|
|
744
|
+
let normalizedVtxos = [];
|
|
745
|
+
try {
|
|
746
|
+
normalizedVtxos = normalizeVtxos(await wallet.getVtxos());
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
log.warn("[ArkadeAdapter] Failed to derive balance from VTXOs, falling back to wallet.getBalance()", error);
|
|
309
750
|
}
|
|
310
|
-
|
|
751
|
+
if (normalizedVtxos.length === 0) {
|
|
752
|
+
// Mirror the vtxo path: boarding UTXOs must be counted in total even
|
|
753
|
+
// when there are no VTXOs. The SDK's top-level balance.total omits the
|
|
754
|
+
// boarding portion, so we compute it the same way as the vtxo path below.
|
|
755
|
+
const available = normalized.settled + normalized.preconfirmed;
|
|
756
|
+
return {
|
|
757
|
+
...normalized,
|
|
758
|
+
available,
|
|
759
|
+
total: normalized.boardingTotal + available + normalized.recoverable,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const vtxoSummary = normalizedVtxos.reduce((summary, vtxo) => {
|
|
763
|
+
if (vtxo.state === "swept") {
|
|
764
|
+
summary.recoverable += vtxo.value;
|
|
765
|
+
}
|
|
766
|
+
else if (vtxo.state === "preconfirmed") {
|
|
767
|
+
summary.preconfirmed += vtxo.value;
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
summary.settled += vtxo.value;
|
|
771
|
+
}
|
|
772
|
+
return summary;
|
|
773
|
+
}, {
|
|
774
|
+
settled: 0,
|
|
775
|
+
preconfirmed: 0,
|
|
776
|
+
recoverable: 0,
|
|
777
|
+
});
|
|
778
|
+
const available = vtxoSummary.settled + vtxoSummary.preconfirmed;
|
|
779
|
+
const total = normalized.boardingTotal + available + vtxoSummary.recoverable;
|
|
780
|
+
return {
|
|
781
|
+
...normalized,
|
|
782
|
+
settled: vtxoSummary.settled,
|
|
783
|
+
preconfirmed: vtxoSummary.preconfirmed,
|
|
784
|
+
available,
|
|
785
|
+
recoverable: vtxoSummary.recoverable,
|
|
786
|
+
total,
|
|
787
|
+
};
|
|
311
788
|
}
|
|
312
|
-
|
|
313
|
-
|
|
789
|
+
async getCachedAssetDetails(wallet, assetId) {
|
|
790
|
+
if (this.assetDetailsCache.has(assetId)) {
|
|
791
|
+
return this.assetDetailsCache.get(assetId) ?? null;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const details = await wallet.assetManager.getAssetDetails(assetId);
|
|
795
|
+
const normalized = details && typeof details === "object" ? details : null;
|
|
796
|
+
this.assetDetailsCache.set(assetId, normalized);
|
|
797
|
+
return normalized;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
log.warn("[ArkadeAdapter] Failed to fetch asset details for", assetId, error);
|
|
801
|
+
this.assetDetailsCache.set(assetId, null);
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
314
804
|
}
|
|
315
805
|
}
|
|
316
806
|
//# sourceMappingURL=ArkadeAdapter.js.map
|