@kaleidorg/wallet-engine 1.0.0-beta.4 → 1.0.0-beta.42
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/LICENSE +21 -0
- package/README.md +29 -10
- package/dist/adapters/ArkadeAdapter.d.ts +78 -14
- package/dist/adapters/ArkadeAdapter.d.ts.map +1 -1
- package/dist/adapters/ArkadeAdapter.js +653 -161
- package/dist/adapters/ArkadeAdapter.js.map +1 -1
- package/dist/adapters/IProtocolAdapter.d.ts +195 -18
- package/dist/adapters/IProtocolAdapter.d.ts.map +1 -1
- package/dist/adapters/IProtocolAdapter.js +6 -2
- package/dist/adapters/IProtocolAdapter.js.map +1 -1
- package/dist/adapters/RgbAdapter.d.ts +70 -27
- package/dist/adapters/RgbAdapter.d.ts.map +1 -1
- package/dist/adapters/RgbAdapter.js +464 -370
- package/dist/adapters/RgbAdapter.js.map +1 -1
- package/dist/adapters/SparkAdapter.d.ts +93 -15
- package/dist/adapters/SparkAdapter.d.ts.map +1 -1
- package/dist/adapters/SparkAdapter.js +833 -168
- package/dist/adapters/SparkAdapter.js.map +1 -1
- package/dist/adapters/arkade.d.ts +15 -0
- package/dist/adapters/arkade.d.ts.map +1 -0
- package/dist/adapters/arkade.js +15 -0
- package/dist/adapters/arkade.js.map +1 -0
- package/dist/adapters/flashnet.d.ts +15 -0
- package/dist/adapters/flashnet.d.ts.map +1 -0
- package/dist/adapters/flashnet.js +17 -0
- package/dist/adapters/flashnet.js.map +1 -0
- package/dist/adapters/native.d.ts +17 -0
- package/dist/adapters/native.d.ts.map +1 -0
- package/dist/adapters/native.js +17 -0
- package/dist/adapters/native.js.map +1 -0
- package/dist/adapters/rgb.d.ts +11 -0
- package/dist/adapters/rgb.d.ts.map +1 -0
- package/dist/adapters/rgb.js +11 -0
- package/dist/adapters/rgb.js.map +1 -0
- package/dist/adapters/spark.d.ts +12 -0
- package/dist/adapters/spark.d.ts.map +1 -0
- package/dist/adapters/spark.js +14 -0
- package/dist/adapters/spark.js.map +1 -0
- package/dist/adapters/wdk/ArkadeWdkAdapter.d.ts +53 -19
- package/dist/adapters/wdk/ArkadeWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/ArkadeWdkAdapter.js +366 -90
- package/dist/adapters/wdk/ArkadeWdkAdapter.js.map +1 -1
- package/dist/adapters/wdk/BaseWdkAdapter.d.ts +40 -0
- package/dist/adapters/wdk/BaseWdkAdapter.d.ts.map +1 -0
- package/dist/adapters/wdk/BaseWdkAdapter.js +71 -0
- package/dist/adapters/wdk/BaseWdkAdapter.js.map +1 -0
- package/dist/adapters/wdk/LiquidWdkAdapter.d.ts +6 -13
- package/dist/adapters/wdk/LiquidWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/LiquidWdkAdapter.js +14 -32
- package/dist/adapters/wdk/LiquidWdkAdapter.js.map +1 -1
- package/dist/adapters/wdk/RgbCore.d.ts +64 -0
- package/dist/adapters/wdk/RgbCore.d.ts.map +1 -0
- package/dist/adapters/wdk/RgbCore.js +111 -0
- package/dist/adapters/wdk/RgbCore.js.map +1 -0
- package/dist/adapters/wdk/RgbLibWasmAdapter.d.ts +277 -0
- package/dist/adapters/wdk/RgbLibWasmAdapter.d.ts.map +1 -0
- package/dist/adapters/wdk/RgbLibWasmAdapter.js +731 -0
- package/dist/adapters/wdk/RgbLibWasmAdapter.js.map +1 -0
- package/dist/adapters/wdk/RgbLibWdkAdapter.d.ts +104 -0
- package/dist/adapters/wdk/RgbLibWdkAdapter.d.ts.map +1 -0
- package/dist/adapters/wdk/RgbLibWdkAdapter.js +249 -0
- package/dist/adapters/wdk/RgbLibWdkAdapter.js.map +1 -0
- package/dist/adapters/wdk/RlnWdkAdapter.d.ts +27 -14
- package/dist/adapters/wdk/RlnWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/RlnWdkAdapter.js +124 -89
- package/dist/adapters/wdk/RlnWdkAdapter.js.map +1 -1
- package/dist/adapters/wdk/SparkWdkAdapter.d.ts +74 -41
- package/dist/adapters/wdk/SparkWdkAdapter.d.ts.map +1 -1
- package/dist/adapters/wdk/SparkWdkAdapter.js +706 -249
- package/dist/adapters/wdk/SparkWdkAdapter.js.map +1 -1
- package/dist/adapters/wdk/index.d.ts +17 -0
- package/dist/adapters/wdk/index.d.ts.map +1 -0
- package/dist/adapters/wdk/index.js +17 -0
- package/dist/adapters/wdk/index.js.map +1 -0
- package/dist/adapters/wdk/wasm-rgb.d.ts +15 -0
- package/dist/adapters/wdk/wasm-rgb.d.ts.map +1 -0
- package/dist/adapters/wdk/wasm-rgb.js +15 -0
- package/dist/adapters/wdk/wasm-rgb.js.map +1 -0
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.d.ts.map +1 -1
- package/dist/capabilities/index.js +17 -2
- package/dist/capabilities/index.js.map +1 -1
- package/dist/capabilities/operations.d.ts +22 -0
- package/dist/capabilities/operations.d.ts.map +1 -0
- package/dist/capabilities/operations.js +62 -0
- package/dist/capabilities/operations.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/disclosure/index.d.ts +1 -1
- package/dist/disclosure/index.js +1 -1
- package/dist/disclosure/index.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/index.d.ts +21 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -32
- 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/flashnet-client-manager.d.ts +26 -9
- package/dist/lib/flashnet-client-manager.d.ts.map +1 -1
- package/dist/lib/flashnet-client-manager.js +97 -13
- package/dist/lib/flashnet-client-manager.js.map +1 -1
- package/dist/lib/kaleido-client-manager.d.ts +38 -3
- package/dist/lib/kaleido-client-manager.d.ts.map +1 -1
- package/dist/lib/kaleido-client-manager.js +79 -10
- package/dist/lib/kaleido-client-manager.js.map +1 -1
- 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/orchestra-client.d.ts +149 -0
- package/dist/lib/orchestra-client.d.ts.map +1 -0
- package/dist/lib/orchestra-client.js +178 -0
- package/dist/lib/orchestra-client.js.map +1 -0
- package/dist/lib/psbt-signer.d.ts +60 -0
- package/dist/lib/psbt-signer.d.ts.map +1 -0
- package/dist/lib/psbt-signer.js +161 -0
- package/dist/lib/psbt-signer.js.map +1 -0
- package/dist/lib/rgb-converters.d.ts +62 -0
- package/dist/lib/rgb-converters.d.ts.map +1 -0
- package/dist/lib/rgb-converters.js +179 -0
- package/dist/lib/rgb-converters.js.map +1 -0
- package/dist/lib/rgb-fee-policy.d.ts +41 -0
- package/dist/lib/rgb-fee-policy.d.ts.map +1 -0
- package/dist/lib/rgb-fee-policy.js +52 -0
- package/dist/lib/rgb-fee-policy.js.map +1 -0
- package/dist/lib/rgb-helpers.d.ts +54 -0
- package/dist/lib/rgb-helpers.d.ts.map +1 -0
- package/dist/lib/rgb-helpers.js +89 -0
- package/dist/lib/rgb-helpers.js.map +1 -0
- package/dist/lib/spark-activity.d.ts +5 -0
- package/dist/lib/spark-activity.d.ts.map +1 -0
- package/dist/lib/spark-activity.js +11 -0
- package/dist/lib/spark-activity.js.map +1 -0
- package/dist/lib/spark-balance-cache.d.ts +58 -0
- package/dist/lib/spark-balance-cache.d.ts.map +1 -0
- package/dist/lib/spark-balance-cache.js +86 -0
- package/dist/lib/spark-balance-cache.js.map +1 -0
- package/dist/lib/spark-client-manager.d.ts +64 -10
- package/dist/lib/spark-client-manager.d.ts.map +1 -1
- package/dist/lib/spark-client-manager.js +191 -35
- package/dist/lib/spark-client-manager.js.map +1 -1
- package/dist/lib/spark-converters.d.ts +64 -0
- package/dist/lib/spark-converters.d.ts.map +1 -0
- package/dist/lib/spark-converters.js +242 -0
- package/dist/lib/spark-converters.js.map +1 -0
- package/dist/lib/spark-helpers.d.ts +72 -0
- package/dist/lib/spark-helpers.d.ts.map +1 -0
- package/dist/lib/spark-helpers.js +151 -0
- package/dist/lib/spark-helpers.js.map +1 -0
- package/dist/lib/spark-sent-token-records.d.ts +43 -0
- package/dist/lib/spark-sent-token-records.d.ts.map +1 -0
- package/dist/lib/spark-sent-token-records.js +105 -0
- package/dist/lib/spark-sent-token-records.js.map +1 -0
- 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/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/dist/manager/ProtocolManager.d.ts +54 -3
- package/dist/manager/ProtocolManager.d.ts.map +1 -1
- package/dist/manager/ProtocolManager.js +118 -41
- package/dist/manager/ProtocolManager.js.map +1 -1
- package/dist/ports/index.d.ts +20 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +23 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/receive/unifiedReceive.d.ts +12 -0
- package/dist/receive/unifiedReceive.d.ts.map +1 -1
- package/dist/receive/unifiedReceive.js +35 -4
- package/dist/receive/unifiedReceive.js.map +1 -1
- package/dist/registry/createWdkRegistry.d.ts +10 -2
- package/dist/registry/createWdkRegistry.d.ts.map +1 -1
- package/dist/registry/createWdkRegistry.js +14 -7
- package/dist/registry/createWdkRegistry.js.map +1 -1
- package/dist/router/destination.d.ts +2 -2
- package/dist/router/destination.d.ts.map +1 -1
- package/dist/router/destination.js +34 -11
- package/dist/router/destination.js.map +1 -1
- package/dist/router/index.d.ts +39 -3
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +113 -4
- package/dist/router/index.js.map +1 -1
- package/dist/router/preference.d.ts +53 -0
- package/dist/router/preference.d.ts.map +1 -0
- package/dist/router/preference.js +81 -0
- package/dist/router/preference.js.map +1 -0
- package/dist/swap/KaleidoswapSwap.d.ts +1 -1
- package/dist/swap/KaleidoswapSwap.d.ts.map +1 -1
- package/dist/swap/KaleidoswapSwap.js +37 -20
- package/dist/swap/KaleidoswapSwap.js.map +1 -1
- package/dist/swap/index.d.ts +8 -0
- package/dist/swap/index.d.ts.map +1 -0
- package/dist/swap/index.js +8 -0
- package/dist/swap/index.js.map +1 -0
- package/dist/types/arkade.d.ts +1 -1
- package/dist/types/base.d.ts +35 -25
- package/dist/types/base.d.ts.map +1 -1
- package/dist/types/base.js +28 -2
- package/dist/types/base.js.map +1 -1
- package/dist/types/cross-l2.d.ts +1 -1
- package/dist/types/flashnet.d.ts +20 -0
- package/dist/types/flashnet.d.ts.map +1 -1
- package/dist/types/flashnet.js +34 -6
- package/dist/types/flashnet.js.map +1 -1
- package/dist/types/rgb.d.ts +18 -4
- package/dist/types/rgb.d.ts.map +1 -1
- package/dist/types/spark.d.ts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +2 -2
- package/dist/utils.js.map +1 -1
- package/package.json +68 -14
|
@@ -1,17 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Spark Protocol Adapter
|
|
3
|
-
* Implements IProtocolAdapter using @buildonspark/spark-sdk.
|
|
4
|
-
*
|
|
3
|
+
* Implements IProtocolAdapter using @buildonspark/spark-sdk (native Spark SDK).
|
|
4
|
+
*
|
|
5
|
+
* Native SDK API reference:
|
|
6
|
+
* - SparkWallet.initialize({ mnemonicOrSeed, options: { network } })
|
|
7
|
+
* - wallet.getBalance() → { balance: bigint }
|
|
8
|
+
* - wallet.getSparkAddress() → SparkAddressFormat (string)
|
|
9
|
+
* - wallet.getSingleUseDepositAddress() → string (BTC on-chain)
|
|
10
|
+
* - wallet.createLightningInvoice({ amountSats, memo? }) → { invoice: { encodedInvoice } }
|
|
11
|
+
* - wallet.payLightningInvoice({ invoice, maxFeeSats }) → LightningSendRequest | WalletTransfer
|
|
12
|
+
* - wallet.transfer({ receiverSparkAddress, amountSats }) → WalletTransfer
|
|
13
|
+
* - wallet.withdraw({ onchainAddress, amountSats, exitSpeed }) → withdrawal result
|
|
14
|
+
* - wallet.getTransfers(limit?, offset?, createdAfter?, createdBefore?) → { transfers: WalletTransfer[], offset: number }
|
|
15
|
+
* - wallet.getTransfer(id) → WalletTransfer | undefined
|
|
16
|
+
* - wallet.cleanupConnections() → void
|
|
17
|
+
*
|
|
18
|
+
* Amounts in the SDK are in SATS. Balance is returned as bigint.
|
|
5
19
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
20
|
+
import { ExitSpeed } from "@buildonspark/spark-sdk/types";
|
|
21
|
+
import { isValidSparkAddress, decodeSparkAddress, getNetworkFromSparkAddress, } from "@buildonspark/spark-sdk";
|
|
22
|
+
import { log } from "../lib/log.js";
|
|
23
|
+
import { loadSentTokenRecords, normalizeTxHash, saveSentTokenRecord, } from "../lib/spark-sent-token-records.js";
|
|
24
|
+
import { sparkClientManager } from "../lib/spark-client-manager.js";
|
|
25
|
+
import { PROTOCOL_OPERATIONS } from "../capabilities/operations.js";
|
|
26
|
+
import { ProtocolError, ConnectionError, } from "../types/base.js";
|
|
27
|
+
import { mnemonicToSeedSync } from "@scure/bip39";
|
|
28
|
+
import { HDKey } from "@scure/bip32";
|
|
29
|
+
import { signLnMessage, verifyLnMessage } from "../lib/ln-message-sign.js";
|
|
30
|
+
/** Default maximum fee for Lightning payments (sats). */
|
|
8
31
|
const DEFAULT_MAX_FEE_SATS = 1000;
|
|
32
|
+
// Pure helpers — timeout wrapper, byte/hex/token utilities, expiry parsing,
|
|
33
|
+
// isEmptyBalance — live in ./helpers.ts. Balance cache state +
|
|
34
|
+
// getSparkBalanceCached / invalidateSparkBalanceCache live in
|
|
35
|
+
// ./balance-cache.ts. Both are re-exported here so existing call sites
|
|
36
|
+
// that import isEmptyBalance / invalidateSparkBalanceCache keep working.
|
|
37
|
+
import { formatAmount, mapTransferStatus, parseSdkExpiryMs, rawTokenIdFromBech32mTokenId, rawTokenIdFromBytes, tokenRefsMatch, txHashFromBytes, withTimeout, } from "../lib/spark-helpers.js";
|
|
38
|
+
import { getSparkBalanceCached, invalidateSparkBalanceCache, SPARK_RPC_TIMEOUT_MS, } from "../lib/spark-balance-cache.js";
|
|
39
|
+
import { buildSentRecordTransaction, convertTokenTransactionToUnified, convertTransferToTransaction, } from "../lib/spark-converters.js";
|
|
40
|
+
export { isEmptyBalance } from "../lib/spark-helpers.js";
|
|
41
|
+
export { invalidateSparkBalanceCache };
|
|
42
|
+
/**
|
|
43
|
+
* Spark Protocol Adapter Implementation
|
|
44
|
+
*/
|
|
9
45
|
export class SparkAdapter {
|
|
10
46
|
constructor() {
|
|
11
|
-
this.protocolName =
|
|
12
|
-
this.supportedLayers = [
|
|
13
|
-
this.version =
|
|
47
|
+
this.protocolName = "SPARK";
|
|
48
|
+
this.supportedLayers = ["SPARK_SPARK", "BTC_LN"];
|
|
49
|
+
this.version = "1.0.0";
|
|
50
|
+
this.capabilities = PROTOCOL_OPERATIONS.SPARK;
|
|
14
51
|
this.config = null;
|
|
52
|
+
/** Maps Lightning invoice string → LightningReceiveRequest ID for status polling. */
|
|
53
|
+
this.invoiceRequestIds = new Map();
|
|
54
|
+
// ========================================================================
|
|
55
|
+
// Helper Methods
|
|
56
|
+
// ========================================================================
|
|
57
|
+
// Pure helpers (mapTransferStatus, formatAmount, byte/hex/token utilities)
|
|
58
|
+
// live in ./helpers.ts; SDK↔unified converters live in ./converters.ts.
|
|
59
|
+
// Covered by tests/unit/spark-helpers.test.ts +
|
|
60
|
+
// tests/unit/spark-converters.test.ts.
|
|
15
61
|
}
|
|
16
62
|
// ========================================================================
|
|
17
63
|
// Connection Management
|
|
@@ -19,41 +65,43 @@ export class SparkAdapter {
|
|
|
19
65
|
async connect(config) {
|
|
20
66
|
const sparkConfig = config;
|
|
21
67
|
if (!sparkConfig.mnemonic) {
|
|
22
|
-
throw new ConnectionError(
|
|
68
|
+
throw new ConnectionError("Mnemonic is required for Spark wallet", "SPARK");
|
|
23
69
|
}
|
|
24
70
|
try {
|
|
25
71
|
await sparkClientManager.initialize(sparkConfig);
|
|
26
72
|
this.config = sparkConfig;
|
|
27
|
-
|
|
73
|
+
log.info("[SparkAdapter] Connected to Spark successfully");
|
|
28
74
|
}
|
|
29
75
|
catch (error) {
|
|
30
|
-
|
|
76
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
77
|
+
throw new ConnectionError(`Failed to connect to Spark: ${msg}`, "SPARK", error);
|
|
31
78
|
}
|
|
32
79
|
}
|
|
33
80
|
async disconnect() {
|
|
34
81
|
await sparkClientManager.disconnect();
|
|
35
82
|
this.config = null;
|
|
36
|
-
|
|
83
|
+
log.info("[SparkAdapter] Disconnected from Spark");
|
|
37
84
|
}
|
|
38
85
|
isConnected() {
|
|
39
86
|
return sparkClientManager.isInitialized();
|
|
40
87
|
}
|
|
41
88
|
async getConnectionInfo() {
|
|
42
89
|
if (!this.isConnected()) {
|
|
43
|
-
throw new ProtocolError(
|
|
90
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
44
91
|
}
|
|
45
92
|
try {
|
|
46
93
|
const wallet = sparkClientManager.getWallet();
|
|
47
|
-
await wallet
|
|
94
|
+
await getSparkBalanceCached(wallet);
|
|
48
95
|
return {
|
|
49
|
-
protocol:
|
|
96
|
+
protocol: "SPARK",
|
|
50
97
|
connected: true,
|
|
51
|
-
network: this.config?.network ||
|
|
98
|
+
network: this.config?.network || "regtest",
|
|
52
99
|
syncStatus: { synced: true, progress: 100 },
|
|
53
100
|
};
|
|
54
101
|
}
|
|
55
102
|
catch (error) {
|
|
56
|
-
|
|
103
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
104
|
+
throw new ProtocolError(`Failed to get connection info: ${msg}`, "SPARK", "CONNECTION_INFO_ERROR", error);
|
|
57
105
|
}
|
|
58
106
|
}
|
|
59
107
|
// ========================================================================
|
|
@@ -61,26 +109,26 @@ export class SparkAdapter {
|
|
|
61
109
|
// ========================================================================
|
|
62
110
|
async listAssets() {
|
|
63
111
|
if (!this.isConnected()) {
|
|
64
|
-
throw new ProtocolError(
|
|
112
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
65
113
|
}
|
|
66
114
|
try {
|
|
67
115
|
const wallet = sparkClientManager.getWallet();
|
|
68
|
-
const { balance, tokenBalances } = await wallet
|
|
116
|
+
const { balance, tokenBalances } = await getSparkBalanceCached(wallet);
|
|
69
117
|
const balanceSats = Number(balance);
|
|
70
118
|
const btcAsset = {
|
|
71
|
-
id:
|
|
72
|
-
name:
|
|
73
|
-
ticker:
|
|
119
|
+
id: "BTC",
|
|
120
|
+
name: "Bitcoin",
|
|
121
|
+
ticker: "BTC",
|
|
74
122
|
precision: 8,
|
|
75
|
-
protocol:
|
|
76
|
-
layer:
|
|
123
|
+
protocol: "SPARK",
|
|
124
|
+
layer: "SPARK_SPARK",
|
|
77
125
|
balance: {
|
|
78
126
|
total: balanceSats,
|
|
79
127
|
available: balanceSats,
|
|
80
128
|
pending: 0,
|
|
81
129
|
locked: 0,
|
|
82
|
-
totalDisplay:
|
|
83
|
-
availableDisplay:
|
|
130
|
+
totalDisplay: formatAmount(balanceSats, 8),
|
|
131
|
+
availableDisplay: formatAmount(balanceSats, 8),
|
|
84
132
|
},
|
|
85
133
|
capabilities: {
|
|
86
134
|
canSend: true,
|
|
@@ -91,27 +139,28 @@ export class SparkAdapter {
|
|
|
91
139
|
},
|
|
92
140
|
};
|
|
93
141
|
const assets = [btcAsset];
|
|
94
|
-
// Add token assets
|
|
142
|
+
// Add token assets from Spark's BTKN token standard
|
|
95
143
|
if (tokenBalances && tokenBalances.size > 0) {
|
|
96
144
|
for (const [tokenId, info] of tokenBalances) {
|
|
97
145
|
const { tokenMetadata: meta } = info;
|
|
98
146
|
const owned = Number(info.ownedBalance);
|
|
99
147
|
const available = Number(info.availableToSendBalance);
|
|
100
|
-
const precision = meta.decimals;
|
|
148
|
+
const precision = meta.decimals ?? 8;
|
|
101
149
|
assets.push({
|
|
102
150
|
id: tokenId,
|
|
103
151
|
name: meta.tokenName,
|
|
104
152
|
ticker: meta.tokenTicker,
|
|
153
|
+
icon: meta.tokenImageUrl,
|
|
105
154
|
precision,
|
|
106
|
-
protocol:
|
|
107
|
-
layer:
|
|
155
|
+
protocol: "SPARK",
|
|
156
|
+
layer: "SPARK_SPARK",
|
|
108
157
|
balance: {
|
|
109
158
|
total: owned,
|
|
110
159
|
available,
|
|
111
160
|
pending: 0,
|
|
112
161
|
locked: owned - available,
|
|
113
|
-
totalDisplay:
|
|
114
|
-
availableDisplay:
|
|
162
|
+
totalDisplay: formatAmount(owned, precision),
|
|
163
|
+
availableDisplay: formatAmount(available, precision),
|
|
115
164
|
},
|
|
116
165
|
capabilities: {
|
|
117
166
|
canSend: true,
|
|
@@ -126,14 +175,15 @@ export class SparkAdapter {
|
|
|
126
175
|
return assets;
|
|
127
176
|
}
|
|
128
177
|
catch (error) {
|
|
129
|
-
|
|
178
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
179
|
+
throw new ProtocolError(`Failed to list assets: ${msg}`, "SPARK", "LIST_ASSETS_ERROR", error);
|
|
130
180
|
}
|
|
131
181
|
}
|
|
132
182
|
async getAsset(assetId) {
|
|
133
183
|
const assets = await this.listAssets();
|
|
134
|
-
const asset = assets.find(a => a.id === assetId || a.ticker === assetId);
|
|
184
|
+
const asset = assets.find((a) => a.id === assetId || a.ticker === assetId);
|
|
135
185
|
if (!asset) {
|
|
136
|
-
throw new ProtocolError(`Asset not found: ${assetId}`,
|
|
186
|
+
throw new ProtocolError(`Asset not found: ${assetId}`, "SPARK", "ASSET_NOT_FOUND");
|
|
137
187
|
}
|
|
138
188
|
return asset;
|
|
139
189
|
}
|
|
@@ -142,44 +192,223 @@ export class SparkAdapter {
|
|
|
142
192
|
return asset.balance;
|
|
143
193
|
}
|
|
144
194
|
async refreshBalances() {
|
|
145
|
-
//
|
|
195
|
+
// Drop the short-TTL coalescing cache so the next call hits the gateway
|
|
196
|
+
// for a fresh snapshot. The cache only exists to collapse the burst of
|
|
197
|
+
// simultaneous reads from a single dashboard render.
|
|
198
|
+
invalidateSparkBalanceCache();
|
|
146
199
|
}
|
|
147
200
|
// ========================================================================
|
|
148
201
|
// Transaction Operations
|
|
149
202
|
// ========================================================================
|
|
150
203
|
async listTransactions(filter) {
|
|
151
204
|
if (!this.isConnected()) {
|
|
152
|
-
throw new ProtocolError(
|
|
205
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
153
206
|
}
|
|
154
207
|
try {
|
|
155
208
|
const wallet = sparkClientManager.getWallet();
|
|
156
|
-
const
|
|
157
|
-
|
|
209
|
+
const limit = filter?.limit ?? 20;
|
|
210
|
+
const offset = filter?.offset ?? 0;
|
|
211
|
+
const requestedAsset = filter?.asset?.trim();
|
|
212
|
+
const createdAfter = filter?.fromTimestamp ? new Date(filter.fromTimestamp) : undefined;
|
|
213
|
+
const createdBefore = !createdAfter && filter?.toTimestamp ? new Date(filter.toTimestamp) : undefined;
|
|
214
|
+
const shouldFetchBtc = !requestedAsset || requestedAsset === "BTC";
|
|
215
|
+
const shouldFetchTokens = !requestedAsset || requestedAsset !== "BTC";
|
|
216
|
+
const requestedTokenRawId = requestedAsset && requestedAsset !== "BTC"
|
|
217
|
+
? rawTokenIdFromBech32mTokenId(requestedAsset)
|
|
218
|
+
: "";
|
|
219
|
+
// Fetch BTC transfers — best effort. A gateway/auth failure here must
|
|
220
|
+
// not hide token activity, and especially not the offline send-record
|
|
221
|
+
// fallback below.
|
|
222
|
+
let btcTxs = [];
|
|
223
|
+
if (shouldFetchBtc) {
|
|
224
|
+
try {
|
|
225
|
+
let btcTransfers = [];
|
|
226
|
+
if (createdAfter || createdBefore) {
|
|
227
|
+
const readonlyClient = await sparkClientManager.getReadonlyClient();
|
|
228
|
+
const sparkAddress = (await wallet.getSparkAddress());
|
|
229
|
+
const readonlyResult = await readonlyClient.getTransfers({
|
|
230
|
+
sparkAddress,
|
|
231
|
+
limit,
|
|
232
|
+
offset,
|
|
233
|
+
createdAfter,
|
|
234
|
+
createdBefore,
|
|
235
|
+
});
|
|
236
|
+
const hydratedTransfers = await Promise.all(readonlyResult.transfers.map((transfer) => wallet.getTransfer(transfer.id)));
|
|
237
|
+
btcTransfers = hydratedTransfers.filter((transfer) => !!transfer);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
btcTransfers = (await wallet.getTransfers(limit, offset)).transfers;
|
|
241
|
+
}
|
|
242
|
+
btcTxs = btcTransfers.map((t) => convertTransferToTransaction(t));
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
log.warn("[SparkAdapter] Failed to fetch BTC transfers:", err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Fetch token transactions. Every Spark RPC below is best-effort and
|
|
249
|
+
// isolated — a transport/auth failure must never hide locally-recorded
|
|
250
|
+
// sends, which are the only reliable record of an outgoing token
|
|
251
|
+
// transfer (a withdrawal with no change output is invisible to the
|
|
252
|
+
// owner-filtered server query).
|
|
253
|
+
const tokenTxs = [];
|
|
254
|
+
try {
|
|
255
|
+
if (shouldFetchTokens) {
|
|
256
|
+
const sparkAddress = (await wallet.getSparkAddress());
|
|
257
|
+
const identityPubKey = await wallet.getIdentityPublicKey();
|
|
258
|
+
let networkType = "";
|
|
259
|
+
try {
|
|
260
|
+
networkType = getNetworkFromSparkAddress(sparkAddress);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Non-fatal — networkType only feeds bech32m encoding fallbacks.
|
|
264
|
+
}
|
|
265
|
+
// Token metadata lookup from current balances — best effort. Empty
|
|
266
|
+
// when the balance is 0 or the balance RPC fails; the converter and
|
|
267
|
+
// the stored-record fallback both tolerate missing metadata.
|
|
268
|
+
const tokenMetaMap = new Map();
|
|
269
|
+
const rawTokenMetaMap = new Map();
|
|
270
|
+
try {
|
|
271
|
+
const { tokenBalances } = await wallet.getBalance();
|
|
272
|
+
if (tokenBalances) {
|
|
273
|
+
for (const [tokenId, info] of tokenBalances) {
|
|
274
|
+
const meta = {
|
|
275
|
+
name: info.tokenMetadata.tokenName,
|
|
276
|
+
ticker: info.tokenMetadata.tokenTicker,
|
|
277
|
+
decimals: info.tokenMetadata.decimals,
|
|
278
|
+
};
|
|
279
|
+
tokenMetaMap.set(tokenId, meta);
|
|
280
|
+
const rawTokenIdentifier = info.tokenMetadata.rawTokenIdentifier;
|
|
281
|
+
const rawTokenId = rawTokenIdFromBytes(rawTokenIdentifier);
|
|
282
|
+
if (rawTokenId) {
|
|
283
|
+
rawTokenMetaMap.set(rawTokenId, { id: tokenId, meta });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
log.warn("[SparkAdapter] Failed to load token balances for activity:", err);
|
|
290
|
+
}
|
|
291
|
+
// Stored send records — written to chrome.storage at send time, so
|
|
292
|
+
// they are available even when the Spark gateway is unreachable.
|
|
293
|
+
const allSentRecords = await loadSentTokenRecords();
|
|
294
|
+
const walletSentRecords = allSentRecords.filter((record) => record.senderSparkAddress === sparkAddress);
|
|
295
|
+
const sentRecords = requestedAsset && requestedAsset !== "BTC"
|
|
296
|
+
? walletSentRecords.filter((record) => tokenRefsMatch(record.assetId, requestedAsset))
|
|
297
|
+
: walletSentRecords;
|
|
298
|
+
const sentHashSet = new Set(sentRecords.map((r) => normalizeTxHash(r.hash)));
|
|
299
|
+
const storedRecordMap = new Map(sentRecords.map((r) => [normalizeTxHash(r.hash), r]));
|
|
300
|
+
const storedAmountMap = new Map(sentRecords.map((r) => [normalizeTxHash(r.hash), BigInt(Math.round(r.amount || 0))]));
|
|
301
|
+
// Server-side history — best effort, isolated from the fallback.
|
|
302
|
+
// Uses the owner-keyed `queryTokenTransactions`, which returns
|
|
303
|
+
// complete output owners and amounts. That lets the converter
|
|
304
|
+
// derive direction from output ownership (see
|
|
305
|
+
// convertTokenTransactionToUnified) — the protocol exposes no
|
|
306
|
+
// direction field for token transactions.
|
|
307
|
+
const txsWithStatus = [];
|
|
308
|
+
try {
|
|
309
|
+
const result = await wallet.queryTokenTransactions({
|
|
310
|
+
ownerPublicKeys: [identityPubKey],
|
|
311
|
+
tokenIdentifiers: requestedAsset && requestedAsset !== "BTC" ? [requestedAsset] : undefined,
|
|
312
|
+
pageSize: limit,
|
|
313
|
+
});
|
|
314
|
+
txsWithStatus.push(...(result.tokenTransactionsWithStatus ?? []));
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
log.warn("[SparkAdapter] Failed to query token transactions:", err);
|
|
318
|
+
}
|
|
319
|
+
// Sends without a change output are invisible to the owner-filtered
|
|
320
|
+
// query above; fetch them explicitly by hash. Also best effort.
|
|
321
|
+
if (sentRecords.length > 0) {
|
|
322
|
+
try {
|
|
323
|
+
const sentResult = await wallet.queryTokenTransactionsByTxHashes(sentRecords.map((r) => normalizeTxHash(r.hash)));
|
|
324
|
+
const existingHashes = new Set(txsWithStatus.map((t) => txHashFromBytes(t.tokenTransactionHash)));
|
|
325
|
+
for (const sentTx of sentResult.tokenTransactionsWithStatus ?? []) {
|
|
326
|
+
const hash = txHashFromBytes(sentTx.tokenTransactionHash);
|
|
327
|
+
if (!existingHashes.has(hash)) {
|
|
328
|
+
txsWithStatus.push(sentTx);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
log.warn("[SparkAdapter] Failed to fetch sent token transactions:", err);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Convert whatever the gateway returned, tracking which recorded
|
|
337
|
+
// sends were successfully rendered.
|
|
338
|
+
const renderedSendHashes = new Set();
|
|
339
|
+
for (const txWithStatus of txsWithStatus) {
|
|
340
|
+
const converted = convertTokenTransactionToUnified(txWithStatus, identityPubKey, tokenMetaMap, rawTokenMetaMap, sentHashSet, storedRecordMap, storedAmountMap, networkType, requestedAsset && requestedAsset !== "BTC" ? requestedAsset : undefined, requestedTokenRawId);
|
|
341
|
+
if (converted) {
|
|
342
|
+
tokenTxs.push(converted);
|
|
343
|
+
const hash = txHashFromBytes(txWithStatus.tokenTransactionHash);
|
|
344
|
+
if (sentHashSet.has(hash))
|
|
345
|
+
renderedSendHashes.add(hash);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Offline / failed-fetch fallback: synthesize a transaction directly
|
|
349
|
+
// from any recorded send the gateway did not return, so a completed
|
|
350
|
+
// withdrawal always shows up in history.
|
|
351
|
+
let synthesizedCount = 0;
|
|
352
|
+
for (const record of sentRecords) {
|
|
353
|
+
const hash = normalizeTxHash(record.hash);
|
|
354
|
+
if (renderedSendHashes.has(hash))
|
|
355
|
+
continue;
|
|
356
|
+
tokenTxs.push(buildSentRecordTransaction(record, requestedAsset && requestedAsset !== "BTC" ? requestedAsset : undefined));
|
|
357
|
+
synthesizedCount += 1;
|
|
358
|
+
}
|
|
359
|
+
// Diagnostic: surfaces whether the send outbox is populated. If a
|
|
360
|
+
// withdrawal is missing from history and these counts are 0, the
|
|
361
|
+
// send was never recorded (e.g. performed on a pre-outbox build).
|
|
362
|
+
log.info(`[SparkAdapter] token activity: ${tokenTxs.length} tx, ` +
|
|
363
|
+
`${allSentRecords.length} stored sends ` +
|
|
364
|
+
`(${sentRecords.length} this wallet, ${renderedSendHashes.size} from gateway, ` +
|
|
365
|
+
`${synthesizedCount} synthesized)`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
log.warn("[SparkAdapter] Failed to fetch token transactions:", err);
|
|
370
|
+
}
|
|
371
|
+
const allTxs = [...btcTxs, ...tokenTxs].sort((a, b) => b.timestamp - a.timestamp);
|
|
372
|
+
return allTxs.filter((tx) => {
|
|
373
|
+
if (!filter)
|
|
374
|
+
return true;
|
|
375
|
+
if (filter.asset &&
|
|
376
|
+
tx.asset?.id !== filter.asset &&
|
|
377
|
+
tx.asset?.ticker !== filter.asset &&
|
|
378
|
+
!tokenRefsMatch(tx.asset?.id, filter.asset))
|
|
379
|
+
return false;
|
|
380
|
+
if (filter.type && tx.type !== filter.type)
|
|
381
|
+
return false;
|
|
382
|
+
if (filter.status && tx.status !== filter.status)
|
|
383
|
+
return false;
|
|
384
|
+
if (filter.fromTimestamp && tx.timestamp < filter.fromTimestamp)
|
|
385
|
+
return false;
|
|
386
|
+
if (filter.toTimestamp && tx.timestamp > filter.toTimestamp)
|
|
387
|
+
return false;
|
|
388
|
+
return true;
|
|
389
|
+
});
|
|
158
390
|
}
|
|
159
391
|
catch (error) {
|
|
160
|
-
|
|
392
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
393
|
+
throw new ProtocolError(`Failed to list transactions: ${msg}`, "SPARK", "LIST_TRANSACTIONS_ERROR", error);
|
|
161
394
|
}
|
|
162
395
|
}
|
|
163
396
|
async getTransaction(txId) {
|
|
164
397
|
if (!this.isConnected()) {
|
|
165
|
-
throw new ProtocolError(
|
|
398
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
166
399
|
}
|
|
167
400
|
try {
|
|
168
401
|
const wallet = sparkClientManager.getWallet();
|
|
169
|
-
const transfer = await wallet.getTransfer(txId);
|
|
402
|
+
const transfer = (await wallet.getTransfer(txId));
|
|
170
403
|
if (!transfer) {
|
|
171
|
-
throw new ProtocolError(`Transaction not found: ${txId}`,
|
|
404
|
+
throw new ProtocolError(`Transaction not found: ${txId}`, "SPARK", "TX_NOT_FOUND");
|
|
172
405
|
}
|
|
173
|
-
|
|
174
|
-
if (!converted) {
|
|
175
|
-
throw new ProtocolError(`Failed to convert transaction: ${txId}`, 'SPARK', 'TX_CONVERT_ERROR');
|
|
176
|
-
}
|
|
177
|
-
return converted;
|
|
406
|
+
return convertTransferToTransaction(transfer);
|
|
178
407
|
}
|
|
179
408
|
catch (error) {
|
|
180
409
|
if (error instanceof ProtocolError)
|
|
181
410
|
throw error;
|
|
182
|
-
throw new ProtocolError(`
|
|
411
|
+
throw new ProtocolError(`Transaction not found: ${txId}`, "SPARK", "TX_NOT_FOUND", error);
|
|
183
412
|
}
|
|
184
413
|
}
|
|
185
414
|
// ========================================================================
|
|
@@ -187,204 +416,640 @@ export class SparkAdapter {
|
|
|
187
416
|
// ========================================================================
|
|
188
417
|
async createInvoice(request) {
|
|
189
418
|
if (!this.isConnected()) {
|
|
190
|
-
throw new ProtocolError(
|
|
419
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
420
|
+
}
|
|
421
|
+
if (request.asset && request.asset !== "BTC") {
|
|
422
|
+
throw new ProtocolError("Spark only supports BTC invoices", "SPARK", "UNSUPPORTED_ASSET");
|
|
191
423
|
}
|
|
192
424
|
try {
|
|
193
425
|
const wallet = sparkClientManager.getWallet();
|
|
194
426
|
const result = await wallet.createLightningInvoice({
|
|
195
|
-
amountSats: request.amount
|
|
427
|
+
amountSats: request.amount ?? 0,
|
|
196
428
|
memo: request.description,
|
|
429
|
+
expirySeconds: request.expirySeconds,
|
|
197
430
|
});
|
|
198
|
-
const
|
|
431
|
+
const inv = result.invoice;
|
|
432
|
+
const encodedInvoice = inv.encodedInvoice;
|
|
433
|
+
// Store request ID for invoice status polling
|
|
434
|
+
if (result.id && encodedInvoice) {
|
|
435
|
+
this.invoiceRequestIds.set(encodedInvoice, result.id);
|
|
436
|
+
}
|
|
437
|
+
const expiresAt = parseSdkExpiryMs("expiryTime" in inv
|
|
438
|
+
? inv.expiryTime
|
|
439
|
+
: "expiresAt" in inv
|
|
440
|
+
? inv.expiresAt
|
|
441
|
+
: undefined);
|
|
199
442
|
return {
|
|
200
|
-
invoice:
|
|
201
|
-
paymentHash:
|
|
443
|
+
invoice: encodedInvoice,
|
|
444
|
+
paymentHash: inv.paymentHash ?? "",
|
|
202
445
|
amount: request.amount,
|
|
203
|
-
expiresAt:
|
|
446
|
+
expiresAt: expiresAt ?? Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
204
447
|
description: request.description,
|
|
205
448
|
};
|
|
206
449
|
}
|
|
207
450
|
catch (error) {
|
|
208
|
-
|
|
451
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
452
|
+
throw new ProtocolError(`Failed to create invoice: ${msg}`, "SPARK", "CREATE_INVOICE_ERROR", error);
|
|
209
453
|
}
|
|
210
454
|
}
|
|
211
|
-
async
|
|
212
|
-
|
|
455
|
+
async createSparkInvoice(request) {
|
|
456
|
+
if (!this.isConnected()) {
|
|
457
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const wallet = sparkClientManager.getWallet();
|
|
461
|
+
const invoice = await wallet.createSatsInvoice({
|
|
462
|
+
amount: request.amount || undefined,
|
|
463
|
+
memo: request.description,
|
|
464
|
+
expiryTime: request.expirySeconds
|
|
465
|
+
? new Date(Date.now() + request.expirySeconds * 1000)
|
|
466
|
+
: undefined,
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
invoice: invoice,
|
|
470
|
+
paymentHash: "",
|
|
471
|
+
amount: request.amount,
|
|
472
|
+
expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
473
|
+
description: request.description,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
478
|
+
throw new ProtocolError(`Failed to create Spark invoice: ${msg}`, "SPARK", "CREATE_SPARK_INVOICE_ERROR", error);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async decodeInvoice(input) {
|
|
482
|
+
// Detect payment type without SDK parse() — bolt11 starts with "ln"
|
|
483
|
+
const lower = input.trim().toLowerCase();
|
|
484
|
+
if (lower.startsWith("ln")) {
|
|
485
|
+
// bolt11 invoice: decode fields heuristically
|
|
486
|
+
return {
|
|
487
|
+
paymentHash: "",
|
|
488
|
+
expiresAt: 0,
|
|
489
|
+
destination: input,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Spark address or BTC address
|
|
213
493
|
return {
|
|
214
|
-
paymentHash:
|
|
494
|
+
paymentHash: "",
|
|
215
495
|
expiresAt: 0,
|
|
216
|
-
destination:
|
|
496
|
+
destination: input,
|
|
217
497
|
};
|
|
218
498
|
}
|
|
219
499
|
async sendPayment(request) {
|
|
220
500
|
if (!this.isConnected()) {
|
|
221
|
-
throw new ProtocolError(
|
|
501
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
222
502
|
}
|
|
223
503
|
try {
|
|
224
504
|
const wallet = sparkClientManager.getWallet();
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
505
|
+
const destination = request.invoice.trim();
|
|
506
|
+
const lower = destination.toLowerCase();
|
|
507
|
+
if (lower.startsWith("ln")) {
|
|
508
|
+
// Lightning payment — payLightningInvoice is synchronous: when it
|
|
509
|
+
// returns without throwing the payment has been dispatched. The
|
|
510
|
+
// result is a LightningSendRequest whose ID is *not* queryable via
|
|
511
|
+
// getTransfer, so we treat a successful return as 'confirmed' to
|
|
512
|
+
// avoid the polling path (which would fail with "Payment not found").
|
|
513
|
+
const extReq = request;
|
|
514
|
+
// For amountless ("0-sat") BOLT-11 invoices the Spark SDK requires
|
|
515
|
+
// `amountSatsToSend` to be passed explicitly. We always forward
|
|
516
|
+
// `request.amount` when present so amountless invoices can be paid
|
|
517
|
+
// with the user-entered amount.
|
|
518
|
+
const result = await wallet.payLightningInvoice({
|
|
519
|
+
invoice: destination,
|
|
520
|
+
maxFeeSats: extReq.maxFee ?? DEFAULT_MAX_FEE_SATS,
|
|
521
|
+
...(request.amount && request.amount > 0 ? { amountSatsToSend: request.amount } : {}),
|
|
232
522
|
});
|
|
523
|
+
const lnResult = result;
|
|
524
|
+
const id = String(lnResult.id ?? "");
|
|
525
|
+
const amountSats = Number(lnResult.amountSats ?? lnResult.totalValue ?? request.amount ?? 0);
|
|
526
|
+
const feeSats = Number(lnResult.feeSats ?? 0);
|
|
527
|
+
const rawStatus = mapTransferStatus(lnResult.status);
|
|
528
|
+
return {
|
|
529
|
+
paymentHash: id,
|
|
530
|
+
amount: amountSats,
|
|
531
|
+
fee: feeSats,
|
|
532
|
+
// If the call returned without throwing, treat as confirmed
|
|
533
|
+
// (Lightning payments settle atomically).
|
|
534
|
+
status: rawStatus === "failed" ? "failed" : "confirmed",
|
|
535
|
+
timestamp: lnResult.createdTime instanceof Date ? lnResult.createdTime.getTime() : Date.now(),
|
|
536
|
+
};
|
|
233
537
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
538
|
+
// Spark address or Spark invoice
|
|
539
|
+
if (isValidSparkAddress(destination)) {
|
|
540
|
+
// Distinguish a plain Spark address from a Spark invoice by checking for sparkInvoiceFields
|
|
541
|
+
const network = getNetworkFromSparkAddress(destination);
|
|
542
|
+
const decoded = decodeSparkAddress(destination, network);
|
|
543
|
+
if (decoded.sparkInvoiceFields) {
|
|
544
|
+
// Spark invoice — use fulfillSparkInvoice
|
|
545
|
+
const response = await wallet.fulfillSparkInvoice([
|
|
546
|
+
{
|
|
547
|
+
invoice: destination,
|
|
548
|
+
amount: request.amount ? BigInt(request.amount) : undefined,
|
|
549
|
+
},
|
|
550
|
+
]);
|
|
551
|
+
if (response.satsTransactionErrors.length > 0) {
|
|
552
|
+
throw new Error(response.satsTransactionErrors[0].error.message);
|
|
553
|
+
}
|
|
554
|
+
const success = response.satsTransactionSuccess[0];
|
|
555
|
+
if (!success) {
|
|
556
|
+
throw new Error("Spark invoice payment returned no result");
|
|
557
|
+
}
|
|
558
|
+
const transfer = success.transferResponse;
|
|
559
|
+
return {
|
|
560
|
+
paymentHash: transfer.id,
|
|
561
|
+
amount: transfer.totalValue,
|
|
562
|
+
fee: 0,
|
|
563
|
+
status: mapTransferStatus(transfer.status),
|
|
564
|
+
timestamp: transfer.createdTime?.getTime() ?? Date.now(),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
// Plain Spark address — use transfer
|
|
568
|
+
const transfer = (await wallet.transfer({
|
|
569
|
+
receiverSparkAddress: destination,
|
|
570
|
+
amountSats: request.amount ?? 0,
|
|
571
|
+
}));
|
|
572
|
+
return {
|
|
573
|
+
paymentHash: transfer.id,
|
|
574
|
+
amount: transfer.totalValue,
|
|
575
|
+
fee: 0,
|
|
576
|
+
status: mapTransferStatus(transfer.status),
|
|
577
|
+
timestamp: transfer.createdTime?.getTime() ?? Date.now(),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// On-chain BTC withdrawal — requires a fee quote first
|
|
581
|
+
const feeQuote = await wallet.getWithdrawalFeeQuote({
|
|
582
|
+
amountSats: request.amount ?? 0,
|
|
583
|
+
withdrawalAddress: destination,
|
|
584
|
+
});
|
|
585
|
+
if (!feeQuote) {
|
|
586
|
+
throw new Error("Failed to get withdrawal fee quote for on-chain exit");
|
|
240
587
|
}
|
|
588
|
+
const feeAmountSats = (feeQuote.l1BroadcastFeeMedium?.originalValue ?? 0) +
|
|
589
|
+
(feeQuote.userFeeMedium?.originalValue ?? 0);
|
|
590
|
+
const result = await wallet.withdraw({
|
|
591
|
+
onchainAddress: destination,
|
|
592
|
+
amountSats: request.amount ?? 0,
|
|
593
|
+
exitSpeed: ExitSpeed.MEDIUM,
|
|
594
|
+
feeQuoteId: feeQuote.id,
|
|
595
|
+
feeAmountSats,
|
|
596
|
+
});
|
|
241
597
|
return {
|
|
242
|
-
paymentHash: result?.id
|
|
243
|
-
amount: request.amount
|
|
244
|
-
fee: 0,
|
|
245
|
-
status:
|
|
598
|
+
paymentHash: result?.id ?? "",
|
|
599
|
+
amount: request.amount ?? 0,
|
|
600
|
+
fee: result?.fee?.originalValue ?? 0,
|
|
601
|
+
status: "pending",
|
|
246
602
|
timestamp: Date.now(),
|
|
247
603
|
};
|
|
248
604
|
}
|
|
249
605
|
catch (error) {
|
|
250
|
-
|
|
606
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
607
|
+
throw new ProtocolError(`Failed to send payment: ${msg}`, "SPARK", "SEND_PAYMENT_ERROR", error);
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
// Any send attempt (success OR failure) makes the cached balance stale;
|
|
611
|
+
// failures may still have produced a partial state change on the gateway.
|
|
612
|
+
invalidateSparkBalanceCache();
|
|
251
613
|
}
|
|
252
614
|
}
|
|
253
|
-
async getPaymentStatus(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
615
|
+
async getPaymentStatus(paymentId) {
|
|
616
|
+
if (!this.isConnected()) {
|
|
617
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const wallet = sparkClientManager.getWallet();
|
|
621
|
+
// The Spark SDK may return entity IDs like "SparkLightningSendRequest:uuid"
|
|
622
|
+
// but getTransfer expects a plain UUID.
|
|
623
|
+
const transferId = paymentId.includes(":") ? paymentId.split(":").pop() : paymentId;
|
|
624
|
+
const transfer = (await wallet.getTransfer(transferId));
|
|
625
|
+
if (!transfer) {
|
|
626
|
+
throw new ProtocolError(`Payment not found: ${paymentId}`, "SPARK", "PAYMENT_STATUS_ERROR");
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
paymentHash: paymentId,
|
|
630
|
+
status: mapTransferStatus(transfer.status),
|
|
631
|
+
amount: transfer.totalValue,
|
|
632
|
+
timestamp: transfer.createdTime?.getTime() ?? 0,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
if (error instanceof ProtocolError)
|
|
637
|
+
throw error;
|
|
638
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
639
|
+
throw new ProtocolError(`Failed to get payment status: ${msg}`, "SPARK", "PAYMENT_STATUS_ERROR", error);
|
|
640
|
+
}
|
|
258
641
|
}
|
|
259
642
|
// ========================================================================
|
|
260
643
|
// Address Operations
|
|
261
644
|
// ========================================================================
|
|
262
645
|
async getReceiveAddress(assetId) {
|
|
263
646
|
if (!this.isConnected()) {
|
|
264
|
-
throw new ProtocolError(
|
|
647
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
265
648
|
}
|
|
266
649
|
try {
|
|
267
650
|
const wallet = sparkClientManager.getWallet();
|
|
268
|
-
|
|
651
|
+
// Spark-to-Spark native address
|
|
652
|
+
if (assetId === "SPARK") {
|
|
653
|
+
const address = await wallet.getSparkAddress();
|
|
654
|
+
return {
|
|
655
|
+
address: address,
|
|
656
|
+
format: "SPARK_ADDRESS",
|
|
657
|
+
asset: "BTC",
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
// BTC on-chain deposit address
|
|
661
|
+
if (!assetId || assetId === "BTC" || assetId.toLowerCase() === "btc") {
|
|
269
662
|
const address = await wallet.getSingleUseDepositAddress();
|
|
270
|
-
return {
|
|
663
|
+
return {
|
|
664
|
+
address,
|
|
665
|
+
format: "BTC_ADDRESS",
|
|
666
|
+
asset: "BTC",
|
|
667
|
+
};
|
|
271
668
|
}
|
|
272
|
-
|
|
273
|
-
return { address, format: 'SPARK_ADDRESS', asset: 'BTC' };
|
|
669
|
+
throw new ProtocolError("Spark only supports BTC", "SPARK", "UNSUPPORTED_ASSET");
|
|
274
670
|
}
|
|
275
671
|
catch (error) {
|
|
276
|
-
|
|
672
|
+
if (error instanceof ProtocolError)
|
|
673
|
+
throw error;
|
|
674
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
675
|
+
throw new ProtocolError(`Failed to get receive address: ${msg}`, "SPARK", "GET_ADDRESS_ERROR", error);
|
|
277
676
|
}
|
|
278
677
|
}
|
|
678
|
+
async claimSparkL1Deposit(params) {
|
|
679
|
+
if (!this.isConnected()) {
|
|
680
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
681
|
+
}
|
|
682
|
+
const address = params.address?.trim();
|
|
683
|
+
if (!address) {
|
|
684
|
+
return { status: "error", error: "address is required" };
|
|
685
|
+
}
|
|
686
|
+
const wallet = sparkClientManager.getWallet();
|
|
687
|
+
let utxos;
|
|
688
|
+
try {
|
|
689
|
+
utxos = await wallet.getUtxosForDepositAddress(address, 10, 0, true);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
return {
|
|
693
|
+
status: "error",
|
|
694
|
+
error: error instanceof Error ? error.message : "utxo lookup failed",
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (!utxos || utxos.length === 0)
|
|
698
|
+
return { status: "awaiting" };
|
|
699
|
+
const claimedTxids = [];
|
|
700
|
+
let lastError;
|
|
701
|
+
for (const utxo of utxos) {
|
|
702
|
+
try {
|
|
703
|
+
await wallet.claimDeposit(utxo.txid);
|
|
704
|
+
claimedTxids.push(utxo.txid);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (claimedTxids.length === 0) {
|
|
711
|
+
return { status: "error", error: lastError ?? "no utxos claimed" };
|
|
712
|
+
}
|
|
713
|
+
return { status: "claimed", txids: claimedTxids };
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Sweep every previously-generated single-use deposit address that is still
|
|
717
|
+
* unclaimed and credit any confirmed UTXOs paid to them. Each call to
|
|
718
|
+
* `getSingleUseDepositAddress()` returns a *new* address, so a deposit sent
|
|
719
|
+
* to an address from a previous session would otherwise stay stranded:
|
|
720
|
+
* the deposit-screen poller only watches the address currently on screen.
|
|
721
|
+
* Run this on unlock (after SPARK connects) and when the user opens the
|
|
722
|
+
* deposit screen so stuck deposits surface as soon as possible.
|
|
723
|
+
*/
|
|
724
|
+
async sweepSparkL1Deposits() {
|
|
725
|
+
if (!this.isConnected()) {
|
|
726
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
727
|
+
}
|
|
728
|
+
const wallet = sparkClientManager.getWallet();
|
|
729
|
+
let unused;
|
|
730
|
+
try {
|
|
731
|
+
unused = await wallet.getUnusedDepositAddresses();
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
return {
|
|
735
|
+
addressesChecked: 0,
|
|
736
|
+
claimedTxids: [],
|
|
737
|
+
errors: [error instanceof Error ? error.message : "getUnusedDepositAddresses failed"],
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (!unused || unused.length === 0) {
|
|
741
|
+
return { addressesChecked: 0, claimedTxids: [], errors: [] };
|
|
742
|
+
}
|
|
743
|
+
const claimedTxids = [];
|
|
744
|
+
const errors = [];
|
|
745
|
+
for (const addr of unused) {
|
|
746
|
+
try {
|
|
747
|
+
const utxos = await wallet.getUtxosForDepositAddress(addr, 10, 0, true);
|
|
748
|
+
if (!utxos || utxos.length === 0)
|
|
749
|
+
continue;
|
|
750
|
+
for (const utxo of utxos) {
|
|
751
|
+
try {
|
|
752
|
+
await wallet.claimDeposit(utxo.txid);
|
|
753
|
+
claimedTxids.push(utxo.txid);
|
|
754
|
+
}
|
|
755
|
+
catch (claimErr) {
|
|
756
|
+
errors.push(claimErr instanceof Error ? claimErr.message : String(claimErr));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (lookupErr) {
|
|
761
|
+
errors.push(lookupErr instanceof Error ? lookupErr.message : String(lookupErr));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return { addressesChecked: unused.length, claimedTxids, errors };
|
|
765
|
+
}
|
|
279
766
|
// ========================================================================
|
|
280
767
|
// Node & Balance Operations
|
|
281
768
|
// ========================================================================
|
|
282
769
|
async getNodeInfo() {
|
|
283
770
|
if (!this.isConnected()) {
|
|
284
|
-
throw new ProtocolError(
|
|
771
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const wallet = sparkClientManager.getWallet();
|
|
775
|
+
const { balance } = await getSparkBalanceCached(wallet);
|
|
776
|
+
const balanceSats = Number(balance);
|
|
777
|
+
return {
|
|
778
|
+
channelsBalanceMsat: balanceSats * 1000,
|
|
779
|
+
maxPayableMsat: balanceSats * 1000,
|
|
780
|
+
onchainBalanceMsat: 0,
|
|
781
|
+
pendingOnchainBalanceMsat: 0,
|
|
782
|
+
maxReceivableMsat: 0,
|
|
783
|
+
inboundLiquidityMsats: 0,
|
|
784
|
+
connectedPeers: [],
|
|
785
|
+
utxos: 0,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
790
|
+
throw new ProtocolError(`Failed to get node info: ${msg}`, "SPARK", "NODE_INFO_ERROR", error);
|
|
285
791
|
}
|
|
286
|
-
const wallet = sparkClientManager.getWallet();
|
|
287
|
-
const { balance } = await wallet.getBalance();
|
|
288
|
-
const balanceSats = Number(balance);
|
|
289
|
-
return {
|
|
290
|
-
channelsBalanceMsat: balanceSats * 1000,
|
|
291
|
-
maxPayableMsat: balanceSats * 1000,
|
|
292
|
-
onchainBalanceMsat: 0,
|
|
293
|
-
maxReceivableMsat: 0,
|
|
294
|
-
inboundLiquidityMsats: 0,
|
|
295
|
-
connectedPeers: [],
|
|
296
|
-
blockHeight: 0,
|
|
297
|
-
pendingOnchainBalanceMsat: 0,
|
|
298
|
-
utxos: 0,
|
|
299
|
-
};
|
|
300
792
|
}
|
|
301
793
|
async getBtcBalance() {
|
|
302
794
|
if (!this.isConnected()) {
|
|
303
|
-
throw new ProtocolError(
|
|
795
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const wallet = sparkClientManager.getWallet();
|
|
799
|
+
const { balance } = await getSparkBalanceCached(wallet);
|
|
800
|
+
const balanceSats = Number(balance);
|
|
801
|
+
return {
|
|
802
|
+
confirmed: balanceSats,
|
|
803
|
+
unconfirmed: 0,
|
|
804
|
+
total: balanceSats,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
809
|
+
throw new ProtocolError(`Failed to get BTC balance: ${msg}`, "SPARK", "BALANCE_ERROR", error);
|
|
304
810
|
}
|
|
305
|
-
const wallet = sparkClientManager.getWallet();
|
|
306
|
-
const { balance } = await wallet.getBalance();
|
|
307
|
-
const balanceSats = Number(balance);
|
|
308
|
-
return { confirmed: balanceSats, unconfirmed: 0, total: balanceSats };
|
|
309
811
|
}
|
|
310
812
|
async listChannels() {
|
|
813
|
+
// Spark doesn't have traditional Lightning channels
|
|
311
814
|
return [];
|
|
312
815
|
}
|
|
313
816
|
async listPayments() {
|
|
314
|
-
|
|
315
|
-
|
|
817
|
+
if (!this.isConnected()) {
|
|
818
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const wallet = sparkClientManager.getWallet();
|
|
822
|
+
const { transfers } = (await withTimeout(wallet.getTransfers(), SPARK_RPC_TIMEOUT_MS, "spark.getTransfers"));
|
|
823
|
+
return { transfers: transfers };
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
827
|
+
throw new ProtocolError(`Failed to list payments: ${msg}`, "SPARK", "LIST_PAYMENTS_ERROR", error);
|
|
828
|
+
}
|
|
316
829
|
}
|
|
317
830
|
async listTransfers(_options) {
|
|
831
|
+
// Spark doesn't have RGB-style transfers
|
|
318
832
|
return { transfers: [] };
|
|
319
833
|
}
|
|
834
|
+
async sendBtcOnchain(params) {
|
|
835
|
+
if (!this.isConnected()) {
|
|
836
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const wallet = sparkClientManager.getWallet();
|
|
840
|
+
// Step 1: Get fee quote — required by the SDK for cooperative exit
|
|
841
|
+
const feeQuote = await wallet.getWithdrawalFeeQuote({
|
|
842
|
+
amountSats: params.amount,
|
|
843
|
+
withdrawalAddress: params.address,
|
|
844
|
+
});
|
|
845
|
+
if (!feeQuote) {
|
|
846
|
+
throw new Error("Failed to get withdrawal fee quote");
|
|
847
|
+
}
|
|
848
|
+
// Step 2: Execute withdrawal with the fee quote
|
|
849
|
+
const feeAmountSats = (feeQuote.l1BroadcastFeeMedium?.originalValue ?? 0) +
|
|
850
|
+
(feeQuote.userFeeMedium?.originalValue ?? 0);
|
|
851
|
+
const result = await wallet.withdraw({
|
|
852
|
+
onchainAddress: params.address,
|
|
853
|
+
amountSats: params.amount,
|
|
854
|
+
exitSpeed: ExitSpeed.MEDIUM,
|
|
855
|
+
feeQuoteId: feeQuote.id,
|
|
856
|
+
feeAmountSats,
|
|
857
|
+
});
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
862
|
+
throw new ProtocolError(`Failed to send BTC on-chain: ${msg}`, "SPARK", "SEND_BTC_ERROR", error);
|
|
863
|
+
}
|
|
864
|
+
finally {
|
|
865
|
+
invalidateSparkBalanceCache();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
320
868
|
// ========================================================================
|
|
321
|
-
//
|
|
869
|
+
// PSBT Signing
|
|
322
870
|
// ========================================================================
|
|
323
|
-
|
|
324
|
-
|
|
871
|
+
async signPsbt(psbtHex) {
|
|
872
|
+
if (!this.config?.mnemonic) {
|
|
873
|
+
throw new ProtocolError("Wallet mnemonic not available", "SPARK", "NOT_CONNECTED");
|
|
874
|
+
}
|
|
875
|
+
const { signPsbt: doSign } = await import("../lib/psbt-signer.js");
|
|
876
|
+
const result = doSign(psbtHex, this.config.mnemonic);
|
|
877
|
+
return { psbt: result.psbt, unchanged: result.unchanged };
|
|
325
878
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
879
|
+
// ========================================================================
|
|
880
|
+
// Message Signing
|
|
881
|
+
// ========================================================================
|
|
882
|
+
async signMessage(message) {
|
|
883
|
+
if (!this.config?.mnemonic) {
|
|
884
|
+
throw new ProtocolError("Wallet mnemonic not available", "SPARK", "NOT_CONNECTED");
|
|
329
885
|
}
|
|
330
|
-
const
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
886
|
+
const seed = mnemonicToSeedSync(this.config.mnemonic);
|
|
887
|
+
const root = HDKey.fromMasterSeed(seed);
|
|
888
|
+
// m/138'/1 — wallet-identity message-signing key, distinct from the
|
|
889
|
+
// LNURL-auth hashing key at m/138'/0.
|
|
890
|
+
const node = root.derive("m/138'/1");
|
|
891
|
+
if (!node.privateKey) {
|
|
892
|
+
throw new ProtocolError("Failed to derive message-signing key", "SPARK", "KEY_DERIVATION_ERROR");
|
|
893
|
+
}
|
|
894
|
+
return signLnMessage(message, node.privateKey);
|
|
895
|
+
}
|
|
896
|
+
async verifyMessage(message, signature) {
|
|
897
|
+
return verifyLnMessage(message, signature);
|
|
337
898
|
}
|
|
338
899
|
// ========================================================================
|
|
339
|
-
//
|
|
900
|
+
// RGB-Specific Operations (Not supported by Spark)
|
|
340
901
|
// ========================================================================
|
|
341
|
-
|
|
902
|
+
async createRgbInvoice(_params) {
|
|
903
|
+
throw new ProtocolError("RGB invoices not supported by Spark", "SPARK", "NOT_SUPPORTED");
|
|
904
|
+
}
|
|
905
|
+
async decodeRgbInvoice(_params) {
|
|
906
|
+
throw new ProtocolError("RGB invoice decoding not supported by Spark", "SPARK", "NOT_SUPPORTED");
|
|
907
|
+
}
|
|
908
|
+
async getInvoiceStatus(params) {
|
|
909
|
+
if (!this.isConnected()) {
|
|
910
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
911
|
+
}
|
|
912
|
+
const requestId = this.invoiceRequestIds.get(params.invoice);
|
|
913
|
+
if (!requestId) {
|
|
914
|
+
// Invoice not tracked — might be from a previous session
|
|
915
|
+
return { status: "Pending" };
|
|
916
|
+
}
|
|
342
917
|
try {
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
type: isIncoming ? 'receive' : 'send',
|
|
373
|
-
status: statusMap[transfer.status] || 'pending',
|
|
374
|
-
timestamp,
|
|
375
|
-
amount: amountSats,
|
|
376
|
-
amountDisplay: this.formatAmount(amountSats, 8),
|
|
377
|
-
fee: 0,
|
|
378
|
-
asset: btcAsset,
|
|
379
|
-
protocolData: { sparkTransfer: transfer },
|
|
380
|
-
};
|
|
918
|
+
const wallet = sparkClientManager.getWallet();
|
|
919
|
+
const request = await wallet.getLightningReceiveRequest(requestId);
|
|
920
|
+
if (!request) {
|
|
921
|
+
return { status: "Pending" };
|
|
922
|
+
}
|
|
923
|
+
// Map LightningReceiveRequestStatus to simple status
|
|
924
|
+
const s = request.status;
|
|
925
|
+
if (s === "LIGHTNING_PAYMENT_RECEIVED" ||
|
|
926
|
+
s === "TRANSFER_COMPLETED" ||
|
|
927
|
+
s === "PAYMENT_PREIMAGE_RECOVERED") {
|
|
928
|
+
// Clean up tracked invoice on terminal state
|
|
929
|
+
this.invoiceRequestIds.delete(params.invoice);
|
|
930
|
+
return { status: "Succeeded" };
|
|
931
|
+
}
|
|
932
|
+
if (s === "TRANSFER_FAILED" ||
|
|
933
|
+
s === "TRANSFER_CREATION_FAILED" ||
|
|
934
|
+
s === "REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED" ||
|
|
935
|
+
s === "REFUND_SIGNING_FAILED" ||
|
|
936
|
+
s === "PAYMENT_PREIMAGE_RECOVERING_FAILED") {
|
|
937
|
+
this.invoiceRequestIds.delete(params.invoice);
|
|
938
|
+
return { status: "Failed" };
|
|
939
|
+
}
|
|
940
|
+
// INVOICE_CREATED, TRANSFER_CREATED, etc.
|
|
941
|
+
return { status: "Pending" };
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
945
|
+
log.warn("[SparkAdapter] Invoice status check failed:", msg);
|
|
946
|
+
return { status: "Pending" };
|
|
381
947
|
}
|
|
382
|
-
|
|
383
|
-
|
|
948
|
+
}
|
|
949
|
+
async sendAsset(params) {
|
|
950
|
+
if (!this.isConnected()) {
|
|
951
|
+
throw new ProtocolError("Not connected", "SPARK", "NOT_CONNECTED");
|
|
384
952
|
}
|
|
953
|
+
try {
|
|
954
|
+
const wallet = sparkClientManager.getWallet();
|
|
955
|
+
const senderSparkAddress = (await wallet.getSparkAddress());
|
|
956
|
+
const assignmentAmount = params.assignment?.value;
|
|
957
|
+
const tokenAmount = typeof assignmentAmount === "number" && assignmentAmount > 0
|
|
958
|
+
? assignmentAmount
|
|
959
|
+
: params.amount;
|
|
960
|
+
if (!Number.isFinite(tokenAmount) || tokenAmount <= 0) {
|
|
961
|
+
throw new Error("Spark token amount must be greater than 0");
|
|
962
|
+
}
|
|
963
|
+
const destination = params.recipientId.trim();
|
|
964
|
+
// Resolve token metadata for the sent-record (cached balance is warm from the send UI)
|
|
965
|
+
let sentMeta = { ticker: "TOKEN", name: params.assetId, decimals: 0 };
|
|
966
|
+
try {
|
|
967
|
+
const { tokenBalances } = await getSparkBalanceCached(wallet);
|
|
968
|
+
const info = tokenBalances?.get(params.assetId);
|
|
969
|
+
if (info) {
|
|
970
|
+
sentMeta = {
|
|
971
|
+
ticker: info.tokenMetadata.tokenTicker,
|
|
972
|
+
name: info.tokenMetadata.tokenName,
|
|
973
|
+
decimals: info.tokenMetadata.decimals,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
// Non-critical — falls back to tokenMetaMap in listTransactions
|
|
979
|
+
}
|
|
980
|
+
// Check if the destination is a Spark invoice (contains sparkInvoiceFields)
|
|
981
|
+
if (isValidSparkAddress(destination)) {
|
|
982
|
+
const network = getNetworkFromSparkAddress(destination);
|
|
983
|
+
const decoded = decodeSparkAddress(destination, network);
|
|
984
|
+
if (decoded.sparkInvoiceFields) {
|
|
985
|
+
// Spark token invoice — use fulfillSparkInvoice
|
|
986
|
+
const response = await wallet.fulfillSparkInvoice([
|
|
987
|
+
{
|
|
988
|
+
invoice: destination,
|
|
989
|
+
amount: BigInt(tokenAmount),
|
|
990
|
+
},
|
|
991
|
+
]);
|
|
992
|
+
if (response.tokenTransactionErrors.length > 0) {
|
|
993
|
+
throw new Error(response.tokenTransactionErrors[0].error.message);
|
|
994
|
+
}
|
|
995
|
+
if (response.invalidInvoices.length > 0) {
|
|
996
|
+
throw new Error(response.invalidInvoices[0].error.message);
|
|
997
|
+
}
|
|
998
|
+
const success = response.tokenTransactionSuccess[0];
|
|
999
|
+
if (success) {
|
|
1000
|
+
await saveSentTokenRecord({
|
|
1001
|
+
hash: success.txid,
|
|
1002
|
+
senderSparkAddress,
|
|
1003
|
+
amount: tokenAmount,
|
|
1004
|
+
assetId: params.assetId,
|
|
1005
|
+
...sentMeta,
|
|
1006
|
+
timestamp: Date.now(),
|
|
1007
|
+
});
|
|
1008
|
+
return { txId: success.txid };
|
|
1009
|
+
}
|
|
1010
|
+
// Fallback: maybe it was a sats invoice bundled with token
|
|
1011
|
+
const satsSuccess = response.satsTransactionSuccess[0];
|
|
1012
|
+
if (satsSuccess) {
|
|
1013
|
+
return { txId: satsSuccess.transferResponse.id };
|
|
1014
|
+
}
|
|
1015
|
+
throw new Error("Spark invoice payment returned no result");
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Plain Spark address — use transferTokens
|
|
1019
|
+
const txId = await wallet.transferTokens({
|
|
1020
|
+
tokenIdentifier: params.assetId,
|
|
1021
|
+
tokenAmount: BigInt(tokenAmount),
|
|
1022
|
+
receiverSparkAddress: destination,
|
|
1023
|
+
});
|
|
1024
|
+
await saveSentTokenRecord({
|
|
1025
|
+
hash: txId,
|
|
1026
|
+
senderSparkAddress,
|
|
1027
|
+
amount: tokenAmount,
|
|
1028
|
+
assetId: params.assetId,
|
|
1029
|
+
...sentMeta,
|
|
1030
|
+
timestamp: Date.now(),
|
|
1031
|
+
});
|
|
1032
|
+
return { txId };
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1036
|
+
throw new ProtocolError(`Failed to send Spark token: ${msg}`, "SPARK", "SEND_ASSET_ERROR", error);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
// ========================================================================
|
|
1040
|
+
// Swap Operations (Not supported by Spark)
|
|
1041
|
+
// ========================================================================
|
|
1042
|
+
supportsSwaps() {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
async getSwapQuote(_request) {
|
|
1046
|
+
throw new ProtocolError("Swap operations not supported by Spark", "SPARK", "NOT_SUPPORTED");
|
|
1047
|
+
}
|
|
1048
|
+
async executeSwap(_quote) {
|
|
1049
|
+
throw new ProtocolError("Swap operations not supported by Spark", "SPARK", "NOT_SUPPORTED");
|
|
385
1050
|
}
|
|
386
|
-
|
|
387
|
-
|
|
1051
|
+
async getSwapStatus(_swapId) {
|
|
1052
|
+
throw new ProtocolError("Swap operations not supported by Spark", "SPARK", "NOT_SUPPORTED");
|
|
388
1053
|
}
|
|
389
1054
|
}
|
|
390
1055
|
//# sourceMappingURL=SparkAdapter.js.map
|