@kaleidorg/wallet-engine 1.0.0-beta.4 → 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/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 +728 -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 +111 -87
- 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,31 +1,52 @@
|
|
|
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
|
-
import { ProtocolError, } from '../../types/base';
|
|
26
|
-
import { getCapabilities } from '../../capabilities';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
21
|
+
import { ProtocolError, } from '../../types/base.js';
|
|
22
|
+
import { getCapabilities } from '../../capabilities/index.js';
|
|
23
|
+
import { PROTOCOL_OPERATIONS } from '../../capabilities/operations.js';
|
|
24
|
+
import { loadWdkModule } from './moduleLoader.js';
|
|
25
|
+
import { decodeBolt11, isBolt11 } from '../../lib/bolt11.js';
|
|
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;
|
|
34
|
+
/** Lower-case hex string for a Uint8Array / Buffer / hex string (for identity-key compare). */
|
|
35
|
+
function toHexLower(bytes) {
|
|
36
|
+
if (!bytes)
|
|
37
|
+
return '';
|
|
38
|
+
if (typeof bytes === 'string')
|
|
39
|
+
return bytes.toLowerCase();
|
|
40
|
+
try {
|
|
41
|
+
let out = '';
|
|
42
|
+
for (const b of bytes)
|
|
43
|
+
out += b.toString(16).padStart(2, '0');
|
|
44
|
+
return out.toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
29
50
|
/** Map a spark-sdk Transfer proto status → domain TransactionStatus. */
|
|
30
51
|
function mapSparkStatus(s) {
|
|
31
52
|
const v = String(s ?? '').toUpperCase();
|
|
@@ -35,21 +56,41 @@ function mapSparkStatus(s) {
|
|
|
35
56
|
return 'failed';
|
|
36
57
|
return 'pending';
|
|
37
58
|
}
|
|
59
|
+
function isDirectSparkTransfer(t) {
|
|
60
|
+
const type = String(t?.type ?? t?.transferType ?? t?.sparkTransactionType ?? '').toUpperCase();
|
|
61
|
+
const hasUserRequest = t?.userRequest != null || t?.userRequestId != null;
|
|
62
|
+
const hasTransferShape = t?.receiverIdentityPublicKey != null || t?.senderIdentityPublicKey != null || t?.totalValue != null;
|
|
63
|
+
return type === 'TRANSFER' || type === '2' || (!hasUserRequest && hasTransferShape);
|
|
64
|
+
}
|
|
38
65
|
const SPARK_NETWORK_MAP = {
|
|
39
66
|
mainnet: 'MAINNET',
|
|
40
67
|
testnet: 'TESTNET',
|
|
41
68
|
regtest: 'REGTEST',
|
|
42
69
|
signet: 'SIGNET', // Spark supports SIGNET natively
|
|
43
70
|
};
|
|
44
|
-
export class SparkWdkAdapter {
|
|
71
|
+
export class SparkWdkAdapter extends BaseWdkAdapter {
|
|
45
72
|
constructor() {
|
|
73
|
+
super(...arguments);
|
|
46
74
|
this.protocolName = 'SPARK';
|
|
75
|
+
this.capabilities = PROTOCOL_OPERATIONS.SPARK;
|
|
47
76
|
this.supportedLayers = getCapabilities('SPARK').layers;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
77
|
+
// Cached account identity pubkey (hex) — used to derive transfer direction,
|
|
78
|
+
// since the spark-sdk Transfer proto exposes sender/receiver identity keys
|
|
79
|
+
// rather than an explicit direction flag.
|
|
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;
|
|
53
94
|
}
|
|
54
95
|
// --- Connection ---------------------------------------------------------
|
|
55
96
|
async connect(config) {
|
|
@@ -57,168 +98,100 @@ export class SparkWdkAdapter {
|
|
|
57
98
|
if (!cfg.mnemonic) {
|
|
58
99
|
throw new ProtocolError('SparkWdkAdapter requires a mnemonic', 'SPARK', 'CONFIG');
|
|
59
100
|
}
|
|
101
|
+
this.mnemonic = cfg.mnemonic;
|
|
60
102
|
this.network = cfg.network ?? 'mainnet';
|
|
61
103
|
// Injectable loader (RN injects a static require; Node/Vite use the import fallback).
|
|
62
104
|
// @ts-ignore — declared as a workspace/optional dep; resolved at runtime.
|
|
63
105
|
const mod = await loadWdkModule('@tetherto/wdk-wallet-spark', () => import('@tetherto/wdk-wallet-spark'));
|
|
64
106
|
const WalletManagerSpark = mod.default ?? mod;
|
|
65
|
-
|
|
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), {
|
|
66
110
|
network: SPARK_NETWORK_MAP[this.network] ?? 'MAINNET',
|
|
67
111
|
});
|
|
68
112
|
this.account = await this.manager.getAccount(cfg.accountIndex ?? 0);
|
|
69
|
-
this.connected = true;
|
|
70
|
-
}
|
|
71
|
-
async disconnect() {
|
|
72
113
|
try {
|
|
73
|
-
await this.account?.
|
|
74
|
-
await this.account?.cleanupConnections?.();
|
|
114
|
+
this.identityPubKeyHex = toHexLower(await this.account.getIdentityKey?.()) || null;
|
|
75
115
|
}
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
this.manager = null;
|
|
79
|
-
this.connected = false;
|
|
116
|
+
catch {
|
|
117
|
+
this.identityPubKeyHex = null;
|
|
80
118
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
|
135
|
+
this.connected = true;
|
|
84
136
|
}
|
|
85
137
|
async getConnectionInfo() {
|
|
86
|
-
|
|
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
|
+
};
|
|
87
147
|
}
|
|
88
148
|
// --- Address / receive --------------------------------------------------
|
|
89
|
-
// Default → the native Spark address (`sp1…`). When the caller targets the
|
|
90
|
-
// BTC L1 layer (passes 'onchain' or the BTC asset id) we return a real
|
|
91
|
-
// on-chain Bitcoin deposit address instead. We use a SINGLE-USE deposit
|
|
92
|
-
// address (not the static one) so that deposits are recoverable via
|
|
93
|
-
// `sweepL1Deposits()` — the sweep enumerates unused single-use addresses and
|
|
94
|
-
// claims their confirmed UTXOs. Without this branch an "on-chain" receive
|
|
95
|
-
// would incorrectly surface the Spark address.
|
|
96
149
|
async getReceiveAddress(assetId) {
|
|
97
150
|
this.assertConnected();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const address = await this.account.getAddress();
|
|
103
|
-
return { address, format: 'SPARK_ADDRESS' };
|
|
104
|
-
}
|
|
105
|
-
// --- On-chain deposit claim / sweep ------------------------------------
|
|
106
|
-
// Spark on-chain (L1) deposits land at a single-use deposit address and must
|
|
107
|
-
// be CLAIMED into the wallet before they show up in the balance. These mirror
|
|
108
|
-
// rate-extension's claimSparkL1Deposit / sweepSparkL1Deposits, but drive the
|
|
109
|
-
// WDK account API: getUtxosForDepositAddress({depositAddress, …}) →
|
|
110
|
-
// claimDeposit(txid), and getUnusedDepositAddresses() for the sweep.
|
|
111
|
-
/** Claim any confirmed UTXO(s) sent to a single deposit `address`. */
|
|
112
|
-
async claimL1Deposit(address) {
|
|
113
|
-
this.assertConnected();
|
|
114
|
-
const depositAddress = address?.trim();
|
|
115
|
-
if (!depositAddress)
|
|
116
|
-
return { status: 'error', error: 'address is required' };
|
|
117
|
-
let utxos;
|
|
118
|
-
try {
|
|
119
|
-
const res = await this.account.getUtxosForDepositAddress({
|
|
120
|
-
depositAddress,
|
|
121
|
-
limit: 10,
|
|
122
|
-
offset: 0,
|
|
123
|
-
excludeClaimed: true,
|
|
124
|
-
});
|
|
125
|
-
utxos = res?.utxos ?? [];
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
return { status: 'error', error: error?.message ?? 'utxo lookup failed' };
|
|
129
|
-
}
|
|
130
|
-
if (utxos.length === 0)
|
|
131
|
-
return { status: 'awaiting' };
|
|
132
|
-
const txids = [];
|
|
133
|
-
let lastError;
|
|
134
|
-
for (const utxo of utxos) {
|
|
135
|
-
try {
|
|
136
|
-
await this.account.claimDeposit(utxo.txid);
|
|
137
|
-
txids.push(utxo.txid);
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
lastError = error?.message ?? String(error);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (txids.length === 0)
|
|
144
|
-
return { status: 'error', error: lastError ?? 'no utxos claimed' };
|
|
145
|
-
return { status: 'claimed', txids };
|
|
146
|
-
}
|
|
147
|
-
/** Sweep every unclaimed single-use deposit address (recovers earlier deposits). */
|
|
148
|
-
async sweepL1Deposits() {
|
|
149
|
-
this.assertConnected();
|
|
150
|
-
let addresses;
|
|
151
|
-
try {
|
|
152
|
-
const res = await this.account.getUnusedDepositAddresses();
|
|
153
|
-
// WDK returns { depositAddresses: [{ depositAddress, … }], offset }.
|
|
154
|
-
addresses = (res?.depositAddresses ?? [])
|
|
155
|
-
.map((d) => (typeof d === 'string' ? d : d?.depositAddress))
|
|
156
|
-
.filter((a) => !!a);
|
|
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' };
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
errors: [error?.message ?? 'getUnusedDepositAddresses failed'],
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
if (addresses.length === 0)
|
|
166
|
-
return { addressesChecked: 0, claimedTxids: [], errors: [] };
|
|
167
|
-
const claimedTxids = [];
|
|
168
|
-
const errors = [];
|
|
169
|
-
for (const addr of addresses) {
|
|
170
|
-
try {
|
|
171
|
-
const res = await this.account.getUtxosForDepositAddress({
|
|
172
|
-
depositAddress: addr,
|
|
173
|
-
limit: 10,
|
|
174
|
-
offset: 0,
|
|
175
|
-
excludeClaimed: true,
|
|
176
|
-
});
|
|
177
|
-
const utxos = res?.utxos ?? [];
|
|
178
|
-
for (const utxo of utxos) {
|
|
179
|
-
try {
|
|
180
|
-
await this.account.claimDeposit(utxo.txid);
|
|
181
|
-
claimedTxids.push(utxo.txid);
|
|
182
|
-
}
|
|
183
|
-
catch (claimErr) {
|
|
184
|
-
errors.push(claimErr?.message ?? String(claimErr));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch (lookupErr) {
|
|
189
|
-
errors.push(lookupErr?.message ?? String(lookupErr));
|
|
190
|
-
}
|
|
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' };
|
|
191
160
|
}
|
|
192
|
-
|
|
161
|
+
throw new ProtocolError('Spark only supports BTC', 'SPARK', 'UNSUPPORTED_ASSET');
|
|
193
162
|
}
|
|
194
163
|
// --- Balance ------------------------------------------------------------
|
|
195
164
|
async getBtcBalance() {
|
|
196
165
|
this.assertConnected();
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const total = Number(bal);
|
|
166
|
+
const { balance } = await getSparkBalanceCached(this.rawWallet);
|
|
167
|
+
const total = Number(balance);
|
|
200
168
|
return { confirmed: total, unconfirmed: 0, total };
|
|
201
169
|
}
|
|
202
170
|
async refreshBalances() {
|
|
203
171
|
this.assertConnected();
|
|
204
|
-
|
|
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(() => { });
|
|
205
176
|
}
|
|
206
177
|
async listAssets() {
|
|
207
178
|
this.assertConnected();
|
|
208
|
-
const {
|
|
179
|
+
const { balance, tokenBalances } = await getSparkBalanceCached(this.rawWallet);
|
|
180
|
+
const balanceSats = Number(balance);
|
|
209
181
|
const btc = {
|
|
210
182
|
id: 'BTC',
|
|
211
183
|
name: 'Bitcoin',
|
|
212
184
|
ticker: 'BTC',
|
|
213
185
|
precision: 8,
|
|
214
186
|
protocol: 'SPARK',
|
|
215
|
-
layer: '
|
|
187
|
+
layer: 'SPARK_SPARK',
|
|
216
188
|
balance: {
|
|
217
|
-
total,
|
|
218
|
-
available:
|
|
189
|
+
total: balanceSats,
|
|
190
|
+
available: balanceSats,
|
|
219
191
|
pending: 0,
|
|
220
|
-
|
|
221
|
-
|
|
192
|
+
locked: 0,
|
|
193
|
+
totalDisplay: formatAmount(balanceSats, 8),
|
|
194
|
+
availableDisplay: formatAmount(balanceSats, 8),
|
|
222
195
|
},
|
|
223
196
|
capabilities: {
|
|
224
197
|
canSend: true,
|
|
@@ -228,41 +201,73 @@ export class SparkWdkAdapter {
|
|
|
228
201
|
supportsOnchain: true,
|
|
229
202
|
},
|
|
230
203
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return assets;
|
|
240
238
|
}
|
|
241
239
|
async getAsset(assetId) {
|
|
242
240
|
const assets = await this.listAssets();
|
|
243
|
-
const found = assets.find((a) => a.id === assetId);
|
|
241
|
+
const found = assets.find((a) => a.id === assetId || a.ticker === assetId);
|
|
244
242
|
if (!found)
|
|
245
|
-
throw new ProtocolError(`
|
|
243
|
+
throw new ProtocolError(`Asset not found: ${assetId}`, 'SPARK', 'ASSET_NOT_FOUND');
|
|
246
244
|
return found;
|
|
247
245
|
}
|
|
246
|
+
async getAssetBalance(assetId) {
|
|
247
|
+
const found = await this.getAsset(assetId);
|
|
248
|
+
return found.balance;
|
|
249
|
+
}
|
|
248
250
|
// --- Invoices / receive amounts ----------------------------------------
|
|
249
251
|
async createInvoice(request) {
|
|
250
252
|
this.assertConnected();
|
|
251
253
|
const expiresAt = Date.now() + (request.expirySeconds ?? 3600) * 1000;
|
|
252
254
|
// 1) Lightning receive (BOLT11) — when the caller targets the LN layer.
|
|
253
255
|
if (request.layer === 'BTC_LN') {
|
|
254
|
-
// WDK createLightningInvoice({ amountSats, memo, expirySeconds }): LightningReceiveRequest
|
|
255
256
|
const r = await this.account.createLightningInvoice({
|
|
256
257
|
amountSats: request.amount ?? 0,
|
|
257
258
|
memo: request.description,
|
|
258
259
|
expirySeconds: request.expirySeconds,
|
|
259
260
|
});
|
|
260
|
-
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);
|
|
261
266
|
return {
|
|
262
267
|
invoice: encoded,
|
|
263
|
-
paymentHash:
|
|
268
|
+
paymentHash: inv?.paymentHash ?? r?.id ?? '',
|
|
264
269
|
amount: request.amount,
|
|
265
|
-
expiresAt,
|
|
270
|
+
expiresAt: parseSdkExpiryMs(inv?.expiryTime ?? inv?.expiresAt) ?? expiresAt,
|
|
266
271
|
description: request.description,
|
|
267
272
|
};
|
|
268
273
|
}
|
|
@@ -277,61 +282,27 @@ export class SparkWdkAdapter {
|
|
|
277
282
|
}
|
|
278
283
|
// 3) Default: native Spark sats invoice — returns a SparkAddressFormat string.
|
|
279
284
|
const invoice = await this.account.createSparkSatsInvoice({
|
|
280
|
-
amount: request.amount,
|
|
285
|
+
amount: request.amount || undefined,
|
|
281
286
|
memo: request.description,
|
|
282
287
|
});
|
|
283
288
|
return { invoice, paymentHash: '', amount: request.amount, expiresAt, description: request.description };
|
|
284
289
|
}
|
|
285
|
-
|
|
286
|
-
async
|
|
290
|
+
/** Optional: explicit native Spark sats invoice (used by the receive UI). */
|
|
291
|
+
async createSparkInvoice(request) {
|
|
287
292
|
this.assertConnected();
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const r = await this.account.payLightningInvoice({
|
|
294
|
-
invoice: dest,
|
|
295
|
-
maxFeeSats: request.maxFeeSats ?? this.defaultMaxFeeSats(request.amount),
|
|
296
|
-
});
|
|
297
|
-
return {
|
|
298
|
-
paymentHash: r?.paymentHash ?? r?.id ?? '',
|
|
299
|
-
preimage: r?.preimage,
|
|
300
|
-
amount: Number(r?.amountSats ?? request.amount ?? 0),
|
|
301
|
-
fee: Number(r?.feeSats ?? 0),
|
|
302
|
-
status: 'confirmed',
|
|
303
|
-
timestamp,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
// 2) Plain Spark address + explicit amount → direct transfer (zero-fee).
|
|
307
|
-
if (request.amount != null) {
|
|
308
|
-
const r = await this.account.sendTransaction({ to: dest, value: request.amount });
|
|
309
|
-
return {
|
|
310
|
-
paymentHash: r?.id ?? r?.transferId ?? '',
|
|
311
|
-
amount: request.amount,
|
|
312
|
-
fee: 0, // Spark transfers are zero-fee (capability flag)
|
|
313
|
-
status: 'confirmed',
|
|
314
|
-
timestamp,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
// 3) Encoded Spark invoice (amount embedded) → fulfill. Takes an ARRAY.
|
|
318
|
-
const res = await this.account.paySparkInvoice([{ invoice: dest }]);
|
|
319
|
-
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
|
+
});
|
|
320
298
|
return {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
299
|
+
invoice,
|
|
300
|
+
paymentHash: '',
|
|
301
|
+
amount: request.amount,
|
|
302
|
+
expiresAt: Date.now() + (request.expirySeconds ?? 3600) * 1000,
|
|
303
|
+
description: request.description,
|
|
326
304
|
};
|
|
327
305
|
}
|
|
328
|
-
/** Conservative default LN fee cap: 0.5% of amount, min 5 sats. */
|
|
329
|
-
defaultMaxFeeSats(amount) {
|
|
330
|
-
if (!amount || amount <= 0)
|
|
331
|
-
return 10;
|
|
332
|
-
return Math.max(5, Math.ceil(amount * 0.005));
|
|
333
|
-
}
|
|
334
|
-
// --- Transactions -------------------------------------------------------
|
|
335
306
|
async decodeInvoice(invoice) {
|
|
336
307
|
const dest = invoice.trim();
|
|
337
308
|
if (isBolt11(dest)) {
|
|
@@ -341,53 +312,270 @@ export class SparkWdkAdapter {
|
|
|
341
312
|
// Spark invoice/address — no on-device decode; surface the raw value.
|
|
342
313
|
return { paymentHash: '', expiresAt: 0, destination: dest };
|
|
343
314
|
}
|
|
344
|
-
|
|
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) {
|
|
345
405
|
this.assertConnected();
|
|
346
|
-
|
|
347
|
-
|
|
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
|
+
};
|
|
348
417
|
}
|
|
418
|
+
// --- Transactions -------------------------------------------------------
|
|
349
419
|
async listTransactions(filter) {
|
|
350
420
|
this.assertConnected();
|
|
351
|
-
const
|
|
352
|
-
|
|
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
|
+
});
|
|
353
554
|
}
|
|
354
555
|
async getTransaction(txId) {
|
|
355
556
|
this.assertConnected();
|
|
356
557
|
const t = await this.account.getTransactionReceipt(txId);
|
|
357
558
|
if (!t)
|
|
358
|
-
throw new ProtocolError(`
|
|
559
|
+
throw new ProtocolError(`Transaction not found: ${txId}`, 'SPARK', 'TX_NOT_FOUND');
|
|
359
560
|
return this.toUnifiedTx(t);
|
|
360
561
|
}
|
|
361
|
-
async getNodeInfo() {
|
|
362
|
-
return { protocol: 'SPARK', network: this.network };
|
|
363
|
-
}
|
|
364
|
-
async listChannels() {
|
|
365
|
-
return []; // Spark has no LN channels
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Escape hatch: the underlying spark-sdk SparkWallet, for integrations that need the
|
|
369
|
-
* raw client (e.g. the flashnet Spark-DEX, which piggybacks on a SparkWallet). Returns
|
|
370
|
-
* the same instance this adapter uses (no duplicate wallet). Null if not connected.
|
|
371
|
-
*/
|
|
372
|
-
getUnderlyingSparkWallet() {
|
|
373
|
-
return this.account?._wallet ?? null;
|
|
374
|
-
}
|
|
375
|
-
async listPayments() {
|
|
376
|
-
// Outgoing transfers only.
|
|
377
|
-
const txs = await this.listTransactions();
|
|
378
|
-
return txs.filter((t) => t.type === 'send');
|
|
379
|
-
}
|
|
380
|
-
async listTransfers() {
|
|
381
|
-
this.assertConnected();
|
|
382
|
-
return this.account.getTransfers({ limit: 100 });
|
|
383
|
-
}
|
|
384
|
-
supportsSwaps() {
|
|
385
|
-
return getCapabilities('SPARK').supportsSwaps;
|
|
386
|
-
}
|
|
387
562
|
/** Map a spark-sdk Transfer (proto) → domain UnifiedTransaction (fields read defensively). */
|
|
388
563
|
toUnifiedTx(t) {
|
|
389
|
-
|
|
390
|
-
|
|
564
|
+
// The spark-sdk Transfer proto has no direction flag — direction is whether
|
|
565
|
+
// *we* are the receiver. Compare our cached identity pubkey against the
|
|
566
|
+
// transfer's receiver/sender identity keys. Fall back to the (legacy, usually
|
|
567
|
+
// absent) direction fields only when the identity key is unknown.
|
|
568
|
+
const me = this.identityPubKeyHex;
|
|
569
|
+
const receiverHex = toHexLower(t?.receiverIdentityPublicKey);
|
|
570
|
+
const senderHex = toHexLower(t?.senderIdentityPublicKey);
|
|
571
|
+
let isReceive;
|
|
572
|
+
if (me && (receiverHex || senderHex)) {
|
|
573
|
+
isReceive = receiverHex === me && senderHex !== me;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
const dir = String(t?.transferDirection ?? t?.direction ?? '').toUpperCase();
|
|
577
|
+
isReceive = dir.includes('INCOMING') || dir.includes('RECEIV');
|
|
578
|
+
}
|
|
391
579
|
const tsRaw = t?.createdTime ?? t?.updatedTime ?? t?.createdAt;
|
|
392
580
|
const timestamp = typeof tsRaw === 'number'
|
|
393
581
|
? tsRaw
|
|
@@ -399,7 +587,7 @@ export class SparkWdkAdapter {
|
|
|
399
587
|
return {
|
|
400
588
|
id: t?.id ?? t?.sparkId ?? '',
|
|
401
589
|
type: isReceive ? 'receive' : 'send',
|
|
402
|
-
status: mapSparkStatus(t?.status),
|
|
590
|
+
status: isDirectSparkTransfer(t) ? 'confirmed' : mapSparkStatus(t?.status),
|
|
403
591
|
timestamp,
|
|
404
592
|
amount: Number(t?.totalValue ?? t?.value ?? 0),
|
|
405
593
|
amountDisplay: '',
|
|
@@ -407,11 +595,280 @@ export class SparkWdkAdapter {
|
|
|
407
595
|
protocolData: t,
|
|
408
596
|
};
|
|
409
597
|
}
|
|
410
|
-
// ---
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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' };
|
|
414
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);
|
|
415
872
|
}
|
|
416
873
|
}
|
|
417
874
|
//# sourceMappingURL=SparkWdkAdapter.js.map
|