@kaleidorg/wallet-engine 1.0.0-beta.38 → 1.0.0-beta.41
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/flashnet.d.ts +1 -0
- package/dist/adapters/flashnet.d.ts.map +1 -1
- package/dist/adapters/flashnet.js +3 -0
- package/dist/adapters/flashnet.js.map +1 -1
- package/dist/adapters/wdk/ArkadeWdkAdapter.d.ts +42 -7
- package/dist/adapters/wdk/ArkadeWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/ArkadeWdkAdapter.js +271 -61
- package/dist/adapters/wdk/ArkadeWdkAdapter.js.map +1 -1
- package/dist/adapters/wdk/RlnWdkAdapter.d.ts +21 -1
- package/dist/adapters/wdk/RlnWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/RlnWdkAdapter.js +54 -1
- package/dist/adapters/wdk/RlnWdkAdapter.js.map +1 -1
- package/dist/adapters/wdk/SparkWdkAdapter.d.ts +70 -27
- package/dist/adapters/wdk/SparkWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/SparkWdkAdapter.js +653 -170
- package/dist/adapters/wdk/SparkWdkAdapter.js.map +1 -1
- package/dist/format.d.ts +11 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +10 -0
- package/dist/format.js.map +1 -0
- package/dist/lib/spark-client-manager.d.ts +9 -0
- package/dist/lib/spark-client-manager.d.ts.map +1 -1
- package/dist/lib/spark-client-manager.js +15 -0
- package/dist/lib/spark-client-manager.js.map +1 -1
- package/dist/lib/wallet-seed.d.ts +31 -0
- package/dist/lib/wallet-seed.d.ts.map +1 -0
- package/dist/lib/wallet-seed.js +58 -0
- package/dist/lib/wallet-seed.js.map +1 -0
- package/package.json +16 -18
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SparkWdkAdapter
|
|
3
3
|
* ---------------
|
|
4
|
-
*
|
|
4
|
+
* Adapter mapping the WDK Spark module (@tetherto/wdk-wallet-spark) onto the
|
|
5
5
|
* stable `IProtocolAdapter` contract. This is the reference implementation of the
|
|
6
6
|
* "wrap a WDK module behind the contract" pattern (see docs/WDK_INTEGRATION_PLAN.md).
|
|
7
7
|
*
|
|
8
8
|
* Discipline rules enforced here:
|
|
9
9
|
* - NO WDK/SDK types cross the contract boundary — everything returned is a domain
|
|
10
10
|
* type from ../types/base. The WDK objects are held as `any` internally.
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* syncWalletBalance, dispose, cleanupConnections
|
|
21
|
-
*
|
|
22
|
-
* Status: skeleton — core receive/balance/invoice/send wired to the real WDK calls;
|
|
23
|
-
* remaining contract methods stubbed with explicit ProtocolError until Phase 2.
|
|
11
|
+
* - The WDK **account** surface is the primary path (getAddress, getBalance,
|
|
12
|
+
* payLightningInvoice, sendTransaction, getTransfers, createLightningInvoice, …).
|
|
13
|
+
* - The raw `SparkWallet` the account wraps (`account._wallet`) is reached ONLY for
|
|
14
|
+
* the rich paths the WDK surface does not expose directly — token send + outbox,
|
|
15
|
+
* token history, L1 deposit claiming, and on-chain (cooperative-exit) withdrawal —
|
|
16
|
+
* ported verbatim from the mature native SparkAdapter (identical behaviour).
|
|
17
|
+
* - The sub-path stays free of a *static* `@buildonspark/spark-sdk` import: the SDK
|
|
18
|
+
* address helpers are lazy-loaded in `connect()`, and the one SDK-coupled lib
|
|
19
|
+
* (spark-converters, used for token-history mapping) is dynamic-imported on demand.
|
|
24
20
|
*/
|
|
25
21
|
import { ProtocolError, } from '../../types/base.js';
|
|
26
22
|
import { getCapabilities } from '../../capabilities/index.js';
|
|
@@ -28,6 +24,13 @@ import { PROTOCOL_OPERATIONS } from '../../capabilities/operations.js';
|
|
|
28
24
|
import { loadWdkModule } from './moduleLoader.js';
|
|
29
25
|
import { decodeBolt11, isBolt11 } from '../../lib/bolt11.js';
|
|
30
26
|
import { BaseWdkAdapter } from './BaseWdkAdapter.js';
|
|
27
|
+
import { formatAmount, mapTransferStatus, parseSdkExpiryMs, rawTokenIdFromBech32mTokenId, rawTokenIdFromBytes, tokenRefsMatch, txHashFromBytes, } from '../../lib/spark-helpers.js';
|
|
28
|
+
import { getSparkBalanceCached, invalidateSparkBalanceCache } from '../../lib/spark-balance-cache.js';
|
|
29
|
+
import { loadSentTokenRecords, normalizeTxHash, saveSentTokenRecord, } from '../../lib/spark-sent-token-records.js';
|
|
30
|
+
import { signLnMessage, verifyLnMessage } from '../../lib/ln-message-sign.js';
|
|
31
|
+
import { resolveWalletSeed } from '../../lib/wallet-seed.js';
|
|
32
|
+
/** Default maximum fee for Lightning payments (sats) — mirrors the native adapter. */
|
|
33
|
+
const DEFAULT_MAX_FEE_SATS = 1000;
|
|
31
34
|
/** Lower-case hex string for a Uint8Array / Buffer / hex string (for identity-key compare). */
|
|
32
35
|
function toHexLower(bytes) {
|
|
33
36
|
if (!bytes)
|
|
@@ -75,6 +78,19 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
75
78
|
// since the spark-sdk Transfer proto exposes sender/receiver identity keys
|
|
76
79
|
// rather than an explicit direction flag.
|
|
77
80
|
this.identityPubKeyHex = null;
|
|
81
|
+
/** BIP-39 mnemonic — retained for message/PSBT signing (derives its own keys). */
|
|
82
|
+
this.mnemonic = null;
|
|
83
|
+
/** Lazily-loaded `@buildonspark/spark-sdk` address helpers (kept off the static import graph). */
|
|
84
|
+
this.sdk = null;
|
|
85
|
+
/** Maps a created Lightning invoice string → its receive-request id (for status polling). */
|
|
86
|
+
this.invoiceRequestIds = new Map();
|
|
87
|
+
}
|
|
88
|
+
/** The raw SparkWallet the WDK account wraps — proven surface for token/deposit/withdrawal ops. */
|
|
89
|
+
get rawWallet() {
|
|
90
|
+
const w = this.account?._wallet;
|
|
91
|
+
if (!w)
|
|
92
|
+
throw new ProtocolError('Spark wallet unavailable', 'SPARK', 'NOT_CONNECTED');
|
|
93
|
+
return w;
|
|
78
94
|
}
|
|
79
95
|
// --- Connection ---------------------------------------------------------
|
|
80
96
|
async connect(config) {
|
|
@@ -82,12 +98,15 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
82
98
|
if (!cfg.mnemonic) {
|
|
83
99
|
throw new ProtocolError('SparkWdkAdapter requires a mnemonic', 'SPARK', 'CONFIG');
|
|
84
100
|
}
|
|
101
|
+
this.mnemonic = cfg.mnemonic;
|
|
85
102
|
this.network = cfg.network ?? 'mainnet';
|
|
86
103
|
// Injectable loader (RN injects a static require; Node/Vite use the import fallback).
|
|
87
104
|
// @ts-ignore — declared as a workspace/optional dep; resolved at runtime.
|
|
88
105
|
const mod = await loadWdkModule('@tetherto/wdk-wallet-spark', () => import('@tetherto/wdk-wallet-spark'));
|
|
89
106
|
const WalletManagerSpark = mod.default ?? mod;
|
|
90
|
-
|
|
107
|
+
// Resolve to seed bytes so nsec/hex-rooted wallets bypass the WDK base's
|
|
108
|
+
// BIP-39 string validation (which throws "The seed phrase is invalid").
|
|
109
|
+
this.manager = new WalletManagerSpark(resolveWalletSeed(cfg.mnemonic), {
|
|
91
110
|
network: SPARK_NETWORK_MAP[this.network] ?? 'MAINNET',
|
|
92
111
|
});
|
|
93
112
|
this.account = await this.manager.getAccount(cfg.accountIndex ?? 0);
|
|
@@ -97,45 +116,82 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
97
116
|
catch {
|
|
98
117
|
this.identityPubKeyHex = null;
|
|
99
118
|
}
|
|
119
|
+
// Lazy-load the SDK address helpers used to classify send destinations. Kept
|
|
120
|
+
// out of the static import graph so this sub-path stays SDK-free until used.
|
|
121
|
+
// @ts-ignore — resolved at runtime; a transitive dep of the WDK Spark module.
|
|
122
|
+
this.sdk = await loadWdkModule('@buildonspark/spark-sdk', () => import('@buildonspark/spark-sdk'));
|
|
123
|
+
// Back the native sparkClientManager singleton with this adapter's underlying
|
|
124
|
+
// SparkWallet, so host glue that reads Spark through it (Flashnet AMM, the
|
|
125
|
+
// Orchestra bridge) keeps working under the WDK backend — no second wallet, no
|
|
126
|
+
// derivation drift. Lazy-imported so spark-client-manager (which statically
|
|
127
|
+
// imports spark-sdk) never enters this sub-path's static graph.
|
|
128
|
+
try {
|
|
129
|
+
const { sparkClientManager } = await import('../../lib/spark-client-manager.js');
|
|
130
|
+
sparkClientManager.adoptExternalWallet(this.account?._wallet, this.network);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* flashnet/bridge glue is optional — never block connect on it */
|
|
134
|
+
}
|
|
100
135
|
this.connected = true;
|
|
101
136
|
}
|
|
102
137
|
async getConnectionInfo() {
|
|
103
|
-
|
|
138
|
+
this.assertConnected();
|
|
139
|
+
// Warm the balance cache so the dashboard's first read is coalesced.
|
|
140
|
+
await getSparkBalanceCached(this.rawWallet).catch(() => { });
|
|
141
|
+
return {
|
|
142
|
+
protocol: 'SPARK',
|
|
143
|
+
connected: this.connected,
|
|
144
|
+
network: this.network,
|
|
145
|
+
syncStatus: { synced: true, progress: 100 },
|
|
146
|
+
};
|
|
104
147
|
}
|
|
105
148
|
// --- Address / receive --------------------------------------------------
|
|
106
|
-
async getReceiveAddress() {
|
|
149
|
+
async getReceiveAddress(assetId) {
|
|
107
150
|
this.assertConnected();
|
|
108
|
-
|
|
109
|
-
|
|
151
|
+
// Spark-to-Spark native address.
|
|
152
|
+
if (assetId === 'SPARK') {
|
|
153
|
+
const address = await this.account.getAddress();
|
|
154
|
+
return { address, format: 'SPARK_ADDRESS', asset: 'BTC' };
|
|
155
|
+
}
|
|
156
|
+
// BTC on-chain deposit address (default).
|
|
157
|
+
if (!assetId || assetId.toLowerCase() === 'btc') {
|
|
158
|
+
const address = await this.account.getSingleUseDepositAddress();
|
|
159
|
+
return { address, format: 'BTC_ADDRESS', asset: 'BTC' };
|
|
160
|
+
}
|
|
161
|
+
throw new ProtocolError('Spark only supports BTC', 'SPARK', 'UNSUPPORTED_ASSET');
|
|
110
162
|
}
|
|
111
163
|
// --- Balance ------------------------------------------------------------
|
|
112
164
|
async getBtcBalance() {
|
|
113
165
|
this.assertConnected();
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const total = Number(bal);
|
|
166
|
+
const { balance } = await getSparkBalanceCached(this.rawWallet);
|
|
167
|
+
const total = Number(balance);
|
|
117
168
|
return { confirmed: total, unconfirmed: 0, total };
|
|
118
169
|
}
|
|
119
170
|
async refreshBalances() {
|
|
120
171
|
this.assertConnected();
|
|
121
|
-
|
|
172
|
+
// Drop the short-TTL coalescing cache so the next read hits the gateway,
|
|
173
|
+
// then reconcile server-side state (best-effort).
|
|
174
|
+
invalidateSparkBalanceCache();
|
|
175
|
+
await this.account.syncWalletBalance?.().catch(() => { });
|
|
122
176
|
}
|
|
123
177
|
async listAssets() {
|
|
124
178
|
this.assertConnected();
|
|
125
|
-
const {
|
|
179
|
+
const { balance, tokenBalances } = await getSparkBalanceCached(this.rawWallet);
|
|
180
|
+
const balanceSats = Number(balance);
|
|
126
181
|
const btc = {
|
|
127
182
|
id: 'BTC',
|
|
128
183
|
name: 'Bitcoin',
|
|
129
184
|
ticker: 'BTC',
|
|
130
185
|
precision: 8,
|
|
131
186
|
protocol: 'SPARK',
|
|
132
|
-
layer: '
|
|
187
|
+
layer: 'SPARK_SPARK',
|
|
133
188
|
balance: {
|
|
134
|
-
total,
|
|
135
|
-
available:
|
|
189
|
+
total: balanceSats,
|
|
190
|
+
available: balanceSats,
|
|
136
191
|
pending: 0,
|
|
137
|
-
|
|
138
|
-
|
|
192
|
+
locked: 0,
|
|
193
|
+
totalDisplay: formatAmount(balanceSats, 8),
|
|
194
|
+
availableDisplay: formatAmount(balanceSats, 8),
|
|
139
195
|
},
|
|
140
196
|
capabilities: {
|
|
141
197
|
canSend: true,
|
|
@@ -145,90 +201,73 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
145
201
|
supportsOnchain: true,
|
|
146
202
|
},
|
|
147
203
|
};
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
total: owned,
|
|
179
|
-
available,
|
|
180
|
-
pending: 0,
|
|
181
|
-
totalDisplay: String(owned),
|
|
182
|
-
availableDisplay: String(available),
|
|
183
|
-
},
|
|
184
|
-
capabilities: {
|
|
185
|
-
canSend: true,
|
|
186
|
-
canReceive: true,
|
|
187
|
-
canSwap: true,
|
|
188
|
-
supportsLightning: false,
|
|
189
|
-
supportsOnchain: false,
|
|
190
|
-
},
|
|
191
|
-
});
|
|
204
|
+
const assets = [btc];
|
|
205
|
+
if (tokenBalances && tokenBalances.size > 0) {
|
|
206
|
+
for (const [tokenId, info] of tokenBalances) {
|
|
207
|
+
const meta = info.tokenMetadata;
|
|
208
|
+
const owned = Number(info.ownedBalance);
|
|
209
|
+
const available = Number(info.availableToSendBalance);
|
|
210
|
+
const precision = meta.decimals ?? 8;
|
|
211
|
+
assets.push({
|
|
212
|
+
id: tokenId,
|
|
213
|
+
name: meta.tokenName,
|
|
214
|
+
ticker: meta.tokenTicker,
|
|
215
|
+
icon: meta.tokenImageUrl,
|
|
216
|
+
precision,
|
|
217
|
+
protocol: 'SPARK',
|
|
218
|
+
layer: 'SPARK_SPARK',
|
|
219
|
+
balance: {
|
|
220
|
+
total: owned,
|
|
221
|
+
available,
|
|
222
|
+
pending: 0,
|
|
223
|
+
locked: owned - available,
|
|
224
|
+
totalDisplay: formatAmount(owned, precision),
|
|
225
|
+
availableDisplay: formatAmount(available, precision),
|
|
226
|
+
},
|
|
227
|
+
capabilities: {
|
|
228
|
+
canSend: true,
|
|
229
|
+
canReceive: true,
|
|
230
|
+
canSwap: false,
|
|
231
|
+
supportsLightning: false,
|
|
232
|
+
supportsOnchain: false,
|
|
233
|
+
},
|
|
192
234
|
});
|
|
193
235
|
}
|
|
194
236
|
}
|
|
195
|
-
|
|
196
|
-
// tokens are best-effort — keep BTC even if the token map is unavailable
|
|
197
|
-
}
|
|
198
|
-
return out;
|
|
199
|
-
}
|
|
200
|
-
async getAssetBalance(assetId) {
|
|
201
|
-
const assets = await this.listAssets();
|
|
202
|
-
const found = assets.find((a) => a.id === assetId);
|
|
203
|
-
if (!found)
|
|
204
|
-
throw new ProtocolError(`Unknown asset ${assetId}`, 'SPARK', 'NO_ASSET');
|
|
205
|
-
return found.balance;
|
|
237
|
+
return assets;
|
|
206
238
|
}
|
|
207
239
|
async getAsset(assetId) {
|
|
208
240
|
const assets = await this.listAssets();
|
|
209
|
-
const found = assets.find((a) => a.id === assetId);
|
|
241
|
+
const found = assets.find((a) => a.id === assetId || a.ticker === assetId);
|
|
210
242
|
if (!found)
|
|
211
|
-
throw new ProtocolError(`
|
|
243
|
+
throw new ProtocolError(`Asset not found: ${assetId}`, 'SPARK', 'ASSET_NOT_FOUND');
|
|
212
244
|
return found;
|
|
213
245
|
}
|
|
246
|
+
async getAssetBalance(assetId) {
|
|
247
|
+
const found = await this.getAsset(assetId);
|
|
248
|
+
return found.balance;
|
|
249
|
+
}
|
|
214
250
|
// --- Invoices / receive amounts ----------------------------------------
|
|
215
251
|
async createInvoice(request) {
|
|
216
252
|
this.assertConnected();
|
|
217
253
|
const expiresAt = Date.now() + (request.expirySeconds ?? 3600) * 1000;
|
|
218
254
|
// 1) Lightning receive (BOLT11) — when the caller targets the LN layer.
|
|
219
255
|
if (request.layer === 'BTC_LN') {
|
|
220
|
-
// WDK createLightningInvoice({ amountSats, memo, expirySeconds }): LightningReceiveRequest
|
|
221
256
|
const r = await this.account.createLightningInvoice({
|
|
222
257
|
amountSats: request.amount ?? 0,
|
|
223
258
|
memo: request.description,
|
|
224
259
|
expirySeconds: request.expirySeconds,
|
|
225
260
|
});
|
|
226
|
-
const
|
|
261
|
+
const inv = r?.invoice ?? {};
|
|
262
|
+
const encoded = inv?.encodedInvoice ?? r?.encodedInvoice ?? r?.invoice ?? '';
|
|
263
|
+
// Track the receive-request id so getInvoiceStatus can poll it later.
|
|
264
|
+
if (r?.id && encoded)
|
|
265
|
+
this.invoiceRequestIds.set(encoded, r.id);
|
|
227
266
|
return {
|
|
228
267
|
invoice: encoded,
|
|
229
|
-
paymentHash:
|
|
268
|
+
paymentHash: inv?.paymentHash ?? r?.id ?? '',
|
|
230
269
|
amount: request.amount,
|
|
231
|
-
expiresAt,
|
|
270
|
+
expiresAt: parseSdkExpiryMs(inv?.expiryTime ?? inv?.expiresAt) ?? expiresAt,
|
|
232
271
|
description: request.description,
|
|
233
272
|
};
|
|
234
273
|
}
|
|
@@ -243,65 +282,27 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
243
282
|
}
|
|
244
283
|
// 3) Default: native Spark sats invoice — returns a SparkAddressFormat string.
|
|
245
284
|
const invoice = await this.account.createSparkSatsInvoice({
|
|
246
|
-
amount: request.amount,
|
|
285
|
+
amount: request.amount || undefined,
|
|
247
286
|
memo: request.description,
|
|
248
287
|
});
|
|
249
288
|
return { invoice, paymentHash: '', amount: request.amount, expiresAt, description: request.description };
|
|
250
289
|
}
|
|
251
|
-
|
|
252
|
-
async
|
|
290
|
+
/** Optional: explicit native Spark sats invoice (used by the receive UI). */
|
|
291
|
+
async createSparkInvoice(request) {
|
|
253
292
|
this.assertConnected();
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const r = await this.account.payLightningInvoice({
|
|
260
|
-
invoice: dest,
|
|
261
|
-
maxFeeSats: request.maxFeeSats ?? this.defaultMaxFeeSats(request.amount),
|
|
262
|
-
// Required by the underlying Spark SDK for 0-amount (amountless) invoices;
|
|
263
|
-
// for invoices that already carry an amount this must be omitted. The WDK
|
|
264
|
-
// module forwards these options verbatim to spark-sdk's payLightningInvoice.
|
|
265
|
-
...(request.amount && request.amount > 0 ? { amountSatsToSend: request.amount } : {}),
|
|
266
|
-
});
|
|
267
|
-
return {
|
|
268
|
-
paymentHash: r?.paymentHash ?? r?.id ?? '',
|
|
269
|
-
preimage: r?.preimage,
|
|
270
|
-
amount: Number(r?.amountSats ?? request.amount ?? 0),
|
|
271
|
-
fee: Number(r?.feeSats ?? 0),
|
|
272
|
-
status: 'confirmed',
|
|
273
|
-
timestamp,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
// 2) Plain Spark address + explicit amount → direct transfer (zero-fee).
|
|
277
|
-
if (request.amount != null) {
|
|
278
|
-
const r = await this.account.sendTransaction({ to: dest, value: request.amount });
|
|
279
|
-
return {
|
|
280
|
-
paymentHash: r?.id ?? r?.transferId ?? '',
|
|
281
|
-
amount: request.amount,
|
|
282
|
-
fee: 0, // Spark transfers are zero-fee (capability flag)
|
|
283
|
-
status: 'confirmed',
|
|
284
|
-
timestamp,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
// 3) Encoded Spark invoice (amount embedded) → fulfill. Takes an ARRAY.
|
|
288
|
-
const res = await this.account.paySparkInvoice([{ invoice: dest }]);
|
|
289
|
-
const ok = res?.satsTransactionSuccess?.[0];
|
|
293
|
+
const invoice = await this.account.createSparkSatsInvoice({
|
|
294
|
+
amount: request.amount || undefined,
|
|
295
|
+
memo: request.description,
|
|
296
|
+
expiryTime: request.expirySeconds ? new Date(Date.now() + request.expirySeconds * 1000) : undefined,
|
|
297
|
+
});
|
|
290
298
|
return {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
invoice,
|
|
300
|
+
paymentHash: '',
|
|
301
|
+
amount: request.amount,
|
|
302
|
+
expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
303
|
+
description: request.description,
|
|
296
304
|
};
|
|
297
305
|
}
|
|
298
|
-
/** Conservative default LN fee cap: 0.5% of amount, min 5 sats. */
|
|
299
|
-
defaultMaxFeeSats(amount) {
|
|
300
|
-
if (!amount || amount <= 0)
|
|
301
|
-
return 10;
|
|
302
|
-
return Math.max(5, Math.ceil(amount * 0.005));
|
|
303
|
-
}
|
|
304
|
-
// --- Transactions -------------------------------------------------------
|
|
305
306
|
async decodeInvoice(invoice) {
|
|
306
307
|
const dest = invoice.trim();
|
|
307
308
|
if (isBolt11(dest)) {
|
|
@@ -311,46 +312,253 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
311
312
|
// Spark invoice/address — no on-device decode; surface the raw value.
|
|
312
313
|
return { paymentHash: '', expiresAt: 0, destination: dest };
|
|
313
314
|
}
|
|
314
|
-
|
|
315
|
+
// --- Send ---------------------------------------------------------------
|
|
316
|
+
async sendPayment(request) {
|
|
317
|
+
this.assertConnected();
|
|
318
|
+
const destination = request.invoice.trim();
|
|
319
|
+
const timestamp = Date.now();
|
|
320
|
+
try {
|
|
321
|
+
// 1) Lightning send (WDK account). Settles atomically — a clean return
|
|
322
|
+
// means dispatched; its id is not queryable via getTransfer, so we
|
|
323
|
+
// treat a non-failed return as confirmed.
|
|
324
|
+
if (isBolt11(destination)) {
|
|
325
|
+
const maxFee = request.maxFeeSats ?? request.maxFee ?? DEFAULT_MAX_FEE_SATS;
|
|
326
|
+
const result = await this.account.payLightningInvoice({
|
|
327
|
+
invoice: destination,
|
|
328
|
+
maxFeeSats: maxFee,
|
|
329
|
+
// Amountless (0-sat) invoices require an explicit amount; omit otherwise.
|
|
330
|
+
...(request.amount && request.amount > 0 ? { amountSatsToSend: request.amount } : {}),
|
|
331
|
+
});
|
|
332
|
+
const raw = mapTransferStatus(result?.status);
|
|
333
|
+
return {
|
|
334
|
+
paymentHash: String(result?.paymentHash ?? result?.id ?? ''),
|
|
335
|
+
amount: Number(result?.amountSats ?? result?.totalValue ?? request.amount ?? 0),
|
|
336
|
+
fee: Number(result?.feeSats ?? 0),
|
|
337
|
+
status: raw === 'failed' ? 'failed' : 'confirmed',
|
|
338
|
+
timestamp: result?.createdTime instanceof Date ? result.createdTime.getTime() : timestamp,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// 2) Spark address or Spark invoice (WDK account).
|
|
342
|
+
if (this.sdk?.isValidSparkAddress?.(destination)) {
|
|
343
|
+
const network = this.sdk.getNetworkFromSparkAddress(destination);
|
|
344
|
+
const decoded = this.sdk.decodeSparkAddress(destination, network);
|
|
345
|
+
if (decoded.sparkInvoiceFields) {
|
|
346
|
+
const response = await this.account.paySparkInvoice([
|
|
347
|
+
{ invoice: destination, amount: request.amount ? BigInt(request.amount) : undefined },
|
|
348
|
+
]);
|
|
349
|
+
if (response.satsTransactionErrors?.length > 0) {
|
|
350
|
+
throw new Error(response.satsTransactionErrors[0].error.message);
|
|
351
|
+
}
|
|
352
|
+
const success = response.satsTransactionSuccess?.[0];
|
|
353
|
+
if (!success)
|
|
354
|
+
throw new Error('Spark invoice payment returned no result');
|
|
355
|
+
const transfer = success.transferResponse;
|
|
356
|
+
return {
|
|
357
|
+
paymentHash: transfer.id,
|
|
358
|
+
amount: Number(transfer.totalValue ?? 0),
|
|
359
|
+
fee: 0,
|
|
360
|
+
status: mapTransferStatus(transfer.status),
|
|
361
|
+
timestamp: transfer.createdTime?.getTime() ?? timestamp,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// Plain Spark address — zero-fee direct transfer.
|
|
365
|
+
const transfer = await this.account.sendTransaction({ to: destination, value: request.amount ?? 0 });
|
|
366
|
+
return {
|
|
367
|
+
paymentHash: transfer?.id ?? transfer?.transferId ?? '',
|
|
368
|
+
amount: Number(transfer?.totalValue ?? request.amount ?? 0),
|
|
369
|
+
fee: 0, // Spark transfers are zero-fee (capability flag)
|
|
370
|
+
status: transfer?.status ? mapTransferStatus(transfer.status) : 'confirmed',
|
|
371
|
+
timestamp: transfer?.createdTime?.getTime?.() ?? timestamp,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
// 3) On-chain BTC withdrawal (cooperative exit) via the raw wallet — the
|
|
375
|
+
// WDK withdraw option shape differs; use the proven native path.
|
|
376
|
+
const wallet = this.rawWallet;
|
|
377
|
+
const feeQuote = await wallet.getWithdrawalFeeQuote({
|
|
378
|
+
amountSats: request.amount ?? 0,
|
|
379
|
+
withdrawalAddress: destination,
|
|
380
|
+
});
|
|
381
|
+
if (!feeQuote)
|
|
382
|
+
throw new Error('Failed to get withdrawal fee quote for on-chain exit');
|
|
383
|
+
const feeAmountSats = (feeQuote.l1BroadcastFeeMedium?.originalValue ?? 0) + (feeQuote.userFeeMedium?.originalValue ?? 0);
|
|
384
|
+
const result = await wallet.withdraw({
|
|
385
|
+
onchainAddress: destination,
|
|
386
|
+
amountSats: request.amount ?? 0,
|
|
387
|
+
exitSpeed: this.sdk?.ExitSpeed?.MEDIUM ?? 'MEDIUM',
|
|
388
|
+
feeQuoteId: feeQuote.id,
|
|
389
|
+
feeAmountSats,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
paymentHash: result?.id ?? '',
|
|
393
|
+
amount: request.amount ?? 0,
|
|
394
|
+
fee: result?.fee?.originalValue ?? 0,
|
|
395
|
+
status: 'pending',
|
|
396
|
+
timestamp,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
// Any send attempt (success OR failure) makes the cached balance stale.
|
|
401
|
+
invalidateSparkBalanceCache();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async getPaymentStatus(paymentId) {
|
|
315
405
|
this.assertConnected();
|
|
316
|
-
|
|
317
|
-
|
|
406
|
+
// Spark may return entity ids like "SparkLightningSendRequest:uuid"; getTransactionReceipt wants the uuid.
|
|
407
|
+
const id = paymentId.includes(':') ? paymentId.split(':').pop() : paymentId;
|
|
408
|
+
const t = await this.account.getTransactionReceipt(id).catch(() => null);
|
|
409
|
+
if (!t)
|
|
410
|
+
return { paymentHash: paymentId, status: 'pending' };
|
|
411
|
+
return {
|
|
412
|
+
paymentHash: paymentId,
|
|
413
|
+
status: mapSparkStatus(t.status),
|
|
414
|
+
amount: Number(t.totalValue ?? 0),
|
|
415
|
+
timestamp: t.createdTime?.getTime?.() ?? 0,
|
|
416
|
+
};
|
|
318
417
|
}
|
|
418
|
+
// --- Transactions -------------------------------------------------------
|
|
319
419
|
async listTransactions(filter) {
|
|
320
420
|
this.assertConnected();
|
|
321
|
-
const
|
|
322
|
-
|
|
421
|
+
const limit = filter?.limit ?? 20;
|
|
422
|
+
const offset = filter?.offset ?? 0;
|
|
423
|
+
const requestedAsset = filter?.asset?.trim();
|
|
424
|
+
const shouldFetchBtc = !requestedAsset || requestedAsset === 'BTC';
|
|
425
|
+
const shouldFetchTokens = !requestedAsset || requestedAsset !== 'BTC';
|
|
426
|
+
// BTC transfers via the WDK account — best effort; a failure here must not
|
|
427
|
+
// hide token activity (and especially not the offline send-record fallback).
|
|
428
|
+
let btcTxs = [];
|
|
429
|
+
if (shouldFetchBtc) {
|
|
430
|
+
try {
|
|
431
|
+
const transfers = await this.account.getTransfers({ limit, skip: offset });
|
|
432
|
+
btcTxs = (transfers ?? []).map((t) => this.toUnifiedTx(t));
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
/* isolated */
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Token transactions via the raw wallet — every RPC below is best-effort and
|
|
439
|
+
// isolated so a transport failure never hides locally-recorded sends (the only
|
|
440
|
+
// reliable record of an outgoing token transfer with no change output).
|
|
441
|
+
const tokenTxs = [];
|
|
442
|
+
if (shouldFetchTokens) {
|
|
443
|
+
try {
|
|
444
|
+
const wallet = this.rawWallet;
|
|
445
|
+
const requestedTokenRawId = requestedAsset && requestedAsset !== 'BTC' ? rawTokenIdFromBech32mTokenId(requestedAsset) : '';
|
|
446
|
+
// spark-converters statically imports the SDK — dynamic-import so this
|
|
447
|
+
// sub-path stays SDK-free until token history is actually requested.
|
|
448
|
+
const { convertTokenTransactionToUnified, buildSentRecordTransaction } = await import('../../lib/spark-converters.js');
|
|
449
|
+
const sparkAddress = await wallet.getSparkAddress();
|
|
450
|
+
const identityPubKey = await wallet.getIdentityPublicKey();
|
|
451
|
+
let networkType = '';
|
|
452
|
+
try {
|
|
453
|
+
networkType = this.sdk.getNetworkFromSparkAddress(sparkAddress);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
/* non-fatal */
|
|
457
|
+
}
|
|
458
|
+
const tokenMetaMap = new Map();
|
|
459
|
+
const rawTokenMetaMap = new Map();
|
|
460
|
+
try {
|
|
461
|
+
const { tokenBalances } = await wallet.getBalance();
|
|
462
|
+
if (tokenBalances) {
|
|
463
|
+
for (const [tokenId, info] of tokenBalances) {
|
|
464
|
+
const meta = {
|
|
465
|
+
name: info.tokenMetadata.tokenName,
|
|
466
|
+
ticker: info.tokenMetadata.tokenTicker,
|
|
467
|
+
decimals: info.tokenMetadata.decimals,
|
|
468
|
+
};
|
|
469
|
+
tokenMetaMap.set(tokenId, meta);
|
|
470
|
+
const rawTokenId = rawTokenIdFromBytes(info.tokenMetadata.rawTokenIdentifier);
|
|
471
|
+
if (rawTokenId)
|
|
472
|
+
rawTokenMetaMap.set(rawTokenId, { id: tokenId, meta });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
/* isolated */
|
|
478
|
+
}
|
|
479
|
+
const allSentRecords = await loadSentTokenRecords();
|
|
480
|
+
const walletSentRecords = allSentRecords.filter((r) => r.senderSparkAddress === sparkAddress);
|
|
481
|
+
const sentRecords = requestedAsset && requestedAsset !== 'BTC'
|
|
482
|
+
? walletSentRecords.filter((r) => tokenRefsMatch(r.assetId, requestedAsset))
|
|
483
|
+
: walletSentRecords;
|
|
484
|
+
const sentHashSet = new Set(sentRecords.map((r) => normalizeTxHash(r.hash)));
|
|
485
|
+
const storedRecordMap = new Map(sentRecords.map((r) => [normalizeTxHash(r.hash), r]));
|
|
486
|
+
const storedAmountMap = new Map(sentRecords.map((r) => [normalizeTxHash(r.hash), BigInt(Math.round(r.amount || 0))]));
|
|
487
|
+
const txsWithStatus = [];
|
|
488
|
+
try {
|
|
489
|
+
const result = await wallet.queryTokenTransactions({
|
|
490
|
+
ownerPublicKeys: [identityPubKey],
|
|
491
|
+
tokenIdentifiers: requestedAsset && requestedAsset !== 'BTC' ? [requestedAsset] : undefined,
|
|
492
|
+
pageSize: limit,
|
|
493
|
+
});
|
|
494
|
+
txsWithStatus.push(...(result.tokenTransactionsWithStatus ?? []));
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
/* isolated */
|
|
498
|
+
}
|
|
499
|
+
// Sends with no change output are invisible to the owner-filtered query — fetch by hash.
|
|
500
|
+
if (sentRecords.length > 0) {
|
|
501
|
+
try {
|
|
502
|
+
const sentResult = await wallet.queryTokenTransactionsByTxHashes(sentRecords.map((r) => normalizeTxHash(r.hash)));
|
|
503
|
+
const existing = new Set(txsWithStatus.map((t) => txHashFromBytes(t.tokenTransactionHash)));
|
|
504
|
+
for (const sentTx of sentResult.tokenTransactionsWithStatus ?? []) {
|
|
505
|
+
if (!existing.has(txHashFromBytes(sentTx.tokenTransactionHash)))
|
|
506
|
+
txsWithStatus.push(sentTx);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
/* isolated */
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const renderedSendHashes = new Set();
|
|
514
|
+
for (const txWithStatus of txsWithStatus) {
|
|
515
|
+
const converted = convertTokenTransactionToUnified(txWithStatus, identityPubKey, tokenMetaMap, rawTokenMetaMap, sentHashSet, storedRecordMap, storedAmountMap, networkType, requestedAsset && requestedAsset !== 'BTC' ? requestedAsset : undefined, requestedTokenRawId);
|
|
516
|
+
if (converted) {
|
|
517
|
+
tokenTxs.push(converted);
|
|
518
|
+
const hash = txHashFromBytes(txWithStatus.tokenTransactionHash);
|
|
519
|
+
if (sentHashSet.has(hash))
|
|
520
|
+
renderedSendHashes.add(hash);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Offline / failed-fetch fallback: synthesize from any recorded send the gateway did not return.
|
|
524
|
+
for (const record of sentRecords) {
|
|
525
|
+
const hash = normalizeTxHash(record.hash);
|
|
526
|
+
if (renderedSendHashes.has(hash))
|
|
527
|
+
continue;
|
|
528
|
+
tokenTxs.push(buildSentRecordTransaction(record, requestedAsset && requestedAsset !== 'BTC' ? requestedAsset : undefined));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
/* isolated — token history is additive to BTC history */
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const allTxs = [...btcTxs, ...tokenTxs].sort((a, b) => b.timestamp - a.timestamp);
|
|
536
|
+
return allTxs.filter((tx) => {
|
|
537
|
+
if (!filter)
|
|
538
|
+
return true;
|
|
539
|
+
if (filter.asset &&
|
|
540
|
+
tx.asset?.id !== filter.asset &&
|
|
541
|
+
tx.asset?.ticker !== filter.asset &&
|
|
542
|
+
!tokenRefsMatch(tx.asset?.id, filter.asset))
|
|
543
|
+
return false;
|
|
544
|
+
if (filter.type && tx.type !== filter.type)
|
|
545
|
+
return false;
|
|
546
|
+
if (filter.status && tx.status !== filter.status)
|
|
547
|
+
return false;
|
|
548
|
+
if (filter.fromTimestamp && tx.timestamp < filter.fromTimestamp)
|
|
549
|
+
return false;
|
|
550
|
+
if (filter.toTimestamp && tx.timestamp > filter.toTimestamp)
|
|
551
|
+
return false;
|
|
552
|
+
return true;
|
|
553
|
+
});
|
|
323
554
|
}
|
|
324
555
|
async getTransaction(txId) {
|
|
325
556
|
this.assertConnected();
|
|
326
557
|
const t = await this.account.getTransactionReceipt(txId);
|
|
327
558
|
if (!t)
|
|
328
|
-
throw new ProtocolError(`
|
|
559
|
+
throw new ProtocolError(`Transaction not found: ${txId}`, 'SPARK', 'TX_NOT_FOUND');
|
|
329
560
|
return this.toUnifiedTx(t);
|
|
330
561
|
}
|
|
331
|
-
async getNodeInfo() {
|
|
332
|
-
return { protocol: 'SPARK', network: this.network };
|
|
333
|
-
}
|
|
334
|
-
async listChannels() {
|
|
335
|
-
return []; // Spark has no LN channels
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Escape hatch: the underlying spark-sdk SparkWallet, for integrations that need the
|
|
339
|
-
* raw client (e.g. the flashnet Spark-DEX, which piggybacks on a SparkWallet). Returns
|
|
340
|
-
* the same instance this adapter uses (no duplicate wallet). Null if not connected.
|
|
341
|
-
*/
|
|
342
|
-
getUnderlyingSparkWallet() {
|
|
343
|
-
return this.account?._wallet ?? null;
|
|
344
|
-
}
|
|
345
|
-
async listPayments() {
|
|
346
|
-
// Outgoing transfers only.
|
|
347
|
-
const txs = await this.listTransactions();
|
|
348
|
-
return txs.filter((t) => t.type === 'send');
|
|
349
|
-
}
|
|
350
|
-
async listTransfers() {
|
|
351
|
-
this.assertConnected();
|
|
352
|
-
return this.account.getTransfers({ limit: 100 });
|
|
353
|
-
}
|
|
354
562
|
/** Map a spark-sdk Transfer (proto) → domain UnifiedTransaction (fields read defensively). */
|
|
355
563
|
toUnifiedTx(t) {
|
|
356
564
|
// The spark-sdk Transfer proto has no direction flag — direction is whether
|
|
@@ -387,5 +595,280 @@ export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
|
387
595
|
protocolData: t,
|
|
388
596
|
};
|
|
389
597
|
}
|
|
598
|
+
// --- Node & balance -----------------------------------------------------
|
|
599
|
+
async getNodeInfo() {
|
|
600
|
+
this.assertConnected();
|
|
601
|
+
const { balance } = await getSparkBalanceCached(this.rawWallet);
|
|
602
|
+
const balanceSats = Number(balance);
|
|
603
|
+
return {
|
|
604
|
+
channelsBalanceMsat: balanceSats * 1000,
|
|
605
|
+
maxPayableMsat: balanceSats * 1000,
|
|
606
|
+
onchainBalanceMsat: 0,
|
|
607
|
+
pendingOnchainBalanceMsat: 0,
|
|
608
|
+
maxReceivableMsat: 0,
|
|
609
|
+
inboundLiquidityMsats: 0,
|
|
610
|
+
connectedPeers: [],
|
|
611
|
+
utxos: 0,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async listChannels() {
|
|
615
|
+
return []; // Spark has no LN channels
|
|
616
|
+
}
|
|
617
|
+
async listPayments() {
|
|
618
|
+
// Outgoing transfers only.
|
|
619
|
+
const txs = await this.listTransactions();
|
|
620
|
+
return { transfers: txs.filter((t) => t.type === 'send') };
|
|
621
|
+
}
|
|
622
|
+
async listTransfers() {
|
|
623
|
+
// Spark has no RGB-style per-asset transfers.
|
|
624
|
+
return { transfers: [] };
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Escape hatch: the underlying spark-sdk SparkWallet, for integrations that need the
|
|
628
|
+
* raw client (e.g. the flashnet Spark-DEX, which piggybacks on a SparkWallet). Returns
|
|
629
|
+
* the same instance this adapter uses. Null if not connected.
|
|
630
|
+
*/
|
|
631
|
+
getUnderlyingSparkWallet() {
|
|
632
|
+
return this.account?._wallet ?? null;
|
|
633
|
+
}
|
|
634
|
+
// --- Deposits (L1) ------------------------------------------------------
|
|
635
|
+
async claimSparkL1Deposit(params) {
|
|
636
|
+
this.assertConnected();
|
|
637
|
+
const address = params.address?.trim();
|
|
638
|
+
if (!address)
|
|
639
|
+
return { status: 'error', error: 'address is required' };
|
|
640
|
+
const wallet = this.rawWallet;
|
|
641
|
+
let utxos;
|
|
642
|
+
try {
|
|
643
|
+
utxos = await wallet.getUtxosForDepositAddress(address, 10, 0, true);
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
return { status: 'error', error: error instanceof Error ? error.message : 'utxo lookup failed' };
|
|
647
|
+
}
|
|
648
|
+
if (!utxos || utxos.length === 0)
|
|
649
|
+
return { status: 'awaiting' };
|
|
650
|
+
const claimedTxids = [];
|
|
651
|
+
let lastError;
|
|
652
|
+
for (const utxo of utxos) {
|
|
653
|
+
try {
|
|
654
|
+
await wallet.claimDeposit(utxo.txid);
|
|
655
|
+
claimedTxids.push(utxo.txid);
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (claimedTxids.length === 0)
|
|
662
|
+
return { status: 'error', error: lastError ?? 'no utxos claimed' };
|
|
663
|
+
invalidateSparkBalanceCache();
|
|
664
|
+
return { status: 'claimed', txids: claimedTxids };
|
|
665
|
+
}
|
|
666
|
+
async sweepSparkL1Deposits() {
|
|
667
|
+
this.assertConnected();
|
|
668
|
+
const wallet = this.rawWallet;
|
|
669
|
+
let unused;
|
|
670
|
+
try {
|
|
671
|
+
unused = await wallet.getUnusedDepositAddresses();
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
return {
|
|
675
|
+
addressesChecked: 0,
|
|
676
|
+
claimedTxids: [],
|
|
677
|
+
errors: [error instanceof Error ? error.message : 'getUnusedDepositAddresses failed'],
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
if (!unused || unused.length === 0)
|
|
681
|
+
return { addressesChecked: 0, claimedTxids: [], errors: [] };
|
|
682
|
+
const claimedTxids = [];
|
|
683
|
+
const errors = [];
|
|
684
|
+
for (const addr of unused) {
|
|
685
|
+
try {
|
|
686
|
+
const utxos = await wallet.getUtxosForDepositAddress(addr, 10, 0, true);
|
|
687
|
+
if (!utxos || utxos.length === 0)
|
|
688
|
+
continue;
|
|
689
|
+
for (const utxo of utxos) {
|
|
690
|
+
try {
|
|
691
|
+
await wallet.claimDeposit(utxo.txid);
|
|
692
|
+
claimedTxids.push(utxo.txid);
|
|
693
|
+
}
|
|
694
|
+
catch (claimErr) {
|
|
695
|
+
errors.push(claimErr instanceof Error ? claimErr.message : String(claimErr));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (lookupErr) {
|
|
700
|
+
errors.push(lookupErr instanceof Error ? lookupErr.message : String(lookupErr));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (claimedTxids.length > 0)
|
|
704
|
+
invalidateSparkBalanceCache();
|
|
705
|
+
return { addressesChecked: unused.length, claimedTxids, errors };
|
|
706
|
+
}
|
|
707
|
+
// --- On-chain / asset send ---------------------------------------------
|
|
708
|
+
async sendBtcOnchain(params) {
|
|
709
|
+
this.assertConnected();
|
|
710
|
+
const wallet = this.rawWallet;
|
|
711
|
+
try {
|
|
712
|
+
const feeQuote = await wallet.getWithdrawalFeeQuote({
|
|
713
|
+
amountSats: params.amount,
|
|
714
|
+
withdrawalAddress: params.address,
|
|
715
|
+
});
|
|
716
|
+
if (!feeQuote)
|
|
717
|
+
throw new Error('Failed to get withdrawal fee quote');
|
|
718
|
+
const feeAmountSats = (feeQuote.l1BroadcastFeeMedium?.originalValue ?? 0) + (feeQuote.userFeeMedium?.originalValue ?? 0);
|
|
719
|
+
const result = await wallet.withdraw({
|
|
720
|
+
onchainAddress: params.address,
|
|
721
|
+
amountSats: params.amount,
|
|
722
|
+
exitSpeed: this.sdk?.ExitSpeed?.MEDIUM ?? 'MEDIUM',
|
|
723
|
+
feeQuoteId: feeQuote.id,
|
|
724
|
+
feeAmountSats,
|
|
725
|
+
});
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
invalidateSparkBalanceCache();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async sendAsset(params) {
|
|
733
|
+
this.assertConnected();
|
|
734
|
+
const wallet = this.rawWallet;
|
|
735
|
+
const assignmentAmount = params.assignment?.value;
|
|
736
|
+
const tokenAmount = typeof assignmentAmount === 'number' && assignmentAmount > 0 ? assignmentAmount : params.amount;
|
|
737
|
+
if (!Number.isFinite(tokenAmount) || tokenAmount <= 0) {
|
|
738
|
+
throw new ProtocolError('Spark token amount must be greater than 0', 'SPARK', 'SEND_ASSET_ERROR');
|
|
739
|
+
}
|
|
740
|
+
const destination = params.recipientId.trim();
|
|
741
|
+
const senderSparkAddress = await wallet.getSparkAddress();
|
|
742
|
+
// Resolve token metadata for the send-record (cached balance is warm from the send UI).
|
|
743
|
+
let sentMeta = { ticker: 'TOKEN', name: params.assetId, decimals: 0 };
|
|
744
|
+
try {
|
|
745
|
+
const { tokenBalances } = await getSparkBalanceCached(wallet);
|
|
746
|
+
const info = tokenBalances?.get(params.assetId);
|
|
747
|
+
if (info) {
|
|
748
|
+
sentMeta = {
|
|
749
|
+
ticker: info.tokenMetadata.tokenTicker,
|
|
750
|
+
name: info.tokenMetadata.tokenName,
|
|
751
|
+
decimals: info.tokenMetadata.decimals,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
/* non-critical */
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
// Spark token invoice → fulfillSparkInvoice.
|
|
760
|
+
if (this.sdk?.isValidSparkAddress?.(destination)) {
|
|
761
|
+
const network = this.sdk.getNetworkFromSparkAddress(destination);
|
|
762
|
+
const decoded = this.sdk.decodeSparkAddress(destination, network);
|
|
763
|
+
if (decoded.sparkInvoiceFields) {
|
|
764
|
+
const response = await wallet.fulfillSparkInvoice([
|
|
765
|
+
{ invoice: destination, amount: BigInt(tokenAmount) },
|
|
766
|
+
]);
|
|
767
|
+
if (response.tokenTransactionErrors?.length > 0)
|
|
768
|
+
throw new Error(response.tokenTransactionErrors[0].error.message);
|
|
769
|
+
if (response.invalidInvoices?.length > 0)
|
|
770
|
+
throw new Error(response.invalidInvoices[0].error.message);
|
|
771
|
+
const success = response.tokenTransactionSuccess?.[0];
|
|
772
|
+
if (success) {
|
|
773
|
+
await saveSentTokenRecord({
|
|
774
|
+
hash: success.txid,
|
|
775
|
+
senderSparkAddress,
|
|
776
|
+
amount: tokenAmount,
|
|
777
|
+
assetId: params.assetId,
|
|
778
|
+
...sentMeta,
|
|
779
|
+
timestamp: Date.now(),
|
|
780
|
+
});
|
|
781
|
+
invalidateSparkBalanceCache();
|
|
782
|
+
return { txId: success.txid };
|
|
783
|
+
}
|
|
784
|
+
const satsSuccess = response.satsTransactionSuccess?.[0];
|
|
785
|
+
if (satsSuccess)
|
|
786
|
+
return { txId: satsSuccess.transferResponse.id };
|
|
787
|
+
throw new Error('Spark invoice payment returned no result');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Plain Spark address → transferTokens.
|
|
791
|
+
const txId = await wallet.transferTokens({
|
|
792
|
+
tokenIdentifier: params.assetId,
|
|
793
|
+
tokenAmount: BigInt(tokenAmount),
|
|
794
|
+
receiverSparkAddress: destination,
|
|
795
|
+
});
|
|
796
|
+
await saveSentTokenRecord({
|
|
797
|
+
hash: txId,
|
|
798
|
+
senderSparkAddress,
|
|
799
|
+
amount: tokenAmount,
|
|
800
|
+
assetId: params.assetId,
|
|
801
|
+
...sentMeta,
|
|
802
|
+
timestamp: Date.now(),
|
|
803
|
+
});
|
|
804
|
+
invalidateSparkBalanceCache();
|
|
805
|
+
return { txId };
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
809
|
+
throw new ProtocolError(`Failed to send Spark token: ${msg}`, 'SPARK', 'SEND_ASSET_ERROR', error);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// --- Invoice status -----------------------------------------------------
|
|
813
|
+
async getInvoiceStatus(params) {
|
|
814
|
+
this.assertConnected();
|
|
815
|
+
const requestId = this.invoiceRequestIds.get(params.invoice);
|
|
816
|
+
if (!requestId)
|
|
817
|
+
return { status: 'Pending' }; // untracked (e.g. previous session)
|
|
818
|
+
try {
|
|
819
|
+
const request = await this.rawWallet.getLightningReceiveRequest(requestId);
|
|
820
|
+
if (!request)
|
|
821
|
+
return { status: 'Pending' };
|
|
822
|
+
const s = request.status;
|
|
823
|
+
if (s === 'LIGHTNING_PAYMENT_RECEIVED' || s === 'TRANSFER_COMPLETED' || s === 'PAYMENT_PREIMAGE_RECOVERED') {
|
|
824
|
+
this.invoiceRequestIds.delete(params.invoice);
|
|
825
|
+
return { status: 'Succeeded' };
|
|
826
|
+
}
|
|
827
|
+
if (s === 'TRANSFER_FAILED' ||
|
|
828
|
+
s === 'TRANSFER_CREATION_FAILED' ||
|
|
829
|
+
s === 'REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED' ||
|
|
830
|
+
s === 'REFUND_SIGNING_FAILED' ||
|
|
831
|
+
s === 'PAYMENT_PREIMAGE_RECOVERING_FAILED') {
|
|
832
|
+
this.invoiceRequestIds.delete(params.invoice);
|
|
833
|
+
return { status: 'Failed' };
|
|
834
|
+
}
|
|
835
|
+
return { status: 'Pending' };
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
return { status: 'Pending' };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
// --- RGB (not supported by Spark) --------------------------------------
|
|
842
|
+
async createRgbInvoice() {
|
|
843
|
+
throw new ProtocolError('RGB invoices not supported by Spark', 'SPARK', 'NOT_SUPPORTED');
|
|
844
|
+
}
|
|
845
|
+
async decodeRgbInvoice() {
|
|
846
|
+
throw new ProtocolError('RGB invoice decoding not supported by Spark', 'SPARK', 'NOT_SUPPORTED');
|
|
847
|
+
}
|
|
848
|
+
// --- Message / PSBT signing --------------------------------------------
|
|
849
|
+
async signPsbt(psbtHex) {
|
|
850
|
+
if (!this.mnemonic)
|
|
851
|
+
throw new ProtocolError('Wallet mnemonic not available', 'SPARK', 'NOT_CONNECTED');
|
|
852
|
+
const { signPsbt: doSign } = await import('../../lib/psbt-signer.js');
|
|
853
|
+
const result = doSign(psbtHex, this.mnemonic);
|
|
854
|
+
return { psbt: result.psbt, unchanged: result.unchanged };
|
|
855
|
+
}
|
|
856
|
+
async signMessage(message) {
|
|
857
|
+
if (!this.mnemonic)
|
|
858
|
+
throw new ProtocolError('Wallet mnemonic not available', 'SPARK', 'NOT_CONNECTED');
|
|
859
|
+
const { mnemonicToSeedSync } = await import('@scure/bip39');
|
|
860
|
+
const { HDKey } = await import('@scure/bip32');
|
|
861
|
+
const seed = mnemonicToSeedSync(this.mnemonic);
|
|
862
|
+
const root = HDKey.fromMasterSeed(seed);
|
|
863
|
+
// m/138'/1 — wallet-identity message-signing key (distinct from LNURL-auth's m/138'/0).
|
|
864
|
+
const node = root.derive("m/138'/1");
|
|
865
|
+
if (!node.privateKey) {
|
|
866
|
+
throw new ProtocolError('Failed to derive message-signing key', 'SPARK', 'KEY_DERIVATION_ERROR');
|
|
867
|
+
}
|
|
868
|
+
return signLnMessage(message, node.privateKey);
|
|
869
|
+
}
|
|
870
|
+
async verifyMessage(message, signature) {
|
|
871
|
+
return verifyLnMessage(message, signature);
|
|
872
|
+
}
|
|
390
873
|
}
|
|
391
874
|
//# sourceMappingURL=SparkWdkAdapter.js.map
|