@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.
Files changed (242) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -10
  3. package/dist/adapters/ArkadeAdapter.d.ts +78 -14
  4. package/dist/adapters/ArkadeAdapter.d.ts.map +1 -1
  5. package/dist/adapters/ArkadeAdapter.js +653 -161
  6. package/dist/adapters/ArkadeAdapter.js.map +1 -1
  7. package/dist/adapters/IProtocolAdapter.d.ts +195 -18
  8. package/dist/adapters/IProtocolAdapter.d.ts.map +1 -1
  9. package/dist/adapters/IProtocolAdapter.js +6 -2
  10. package/dist/adapters/IProtocolAdapter.js.map +1 -1
  11. package/dist/adapters/RgbAdapter.d.ts +70 -27
  12. package/dist/adapters/RgbAdapter.d.ts.map +1 -1
  13. package/dist/adapters/RgbAdapter.js +464 -370
  14. package/dist/adapters/RgbAdapter.js.map +1 -1
  15. package/dist/adapters/SparkAdapter.d.ts +93 -15
  16. package/dist/adapters/SparkAdapter.d.ts.map +1 -1
  17. package/dist/adapters/SparkAdapter.js +833 -168
  18. package/dist/adapters/SparkAdapter.js.map +1 -1
  19. package/dist/adapters/arkade.d.ts +15 -0
  20. package/dist/adapters/arkade.d.ts.map +1 -0
  21. package/dist/adapters/arkade.js +15 -0
  22. package/dist/adapters/arkade.js.map +1 -0
  23. package/dist/adapters/flashnet.d.ts +15 -0
  24. package/dist/adapters/flashnet.d.ts.map +1 -0
  25. package/dist/adapters/flashnet.js +17 -0
  26. package/dist/adapters/flashnet.js.map +1 -0
  27. package/dist/adapters/native.d.ts +17 -0
  28. package/dist/adapters/native.d.ts.map +1 -0
  29. package/dist/adapters/native.js +17 -0
  30. package/dist/adapters/native.js.map +1 -0
  31. package/dist/adapters/rgb.d.ts +11 -0
  32. package/dist/adapters/rgb.d.ts.map +1 -0
  33. package/dist/adapters/rgb.js +11 -0
  34. package/dist/adapters/rgb.js.map +1 -0
  35. package/dist/adapters/spark.d.ts +12 -0
  36. package/dist/adapters/spark.d.ts.map +1 -0
  37. package/dist/adapters/spark.js +14 -0
  38. package/dist/adapters/spark.js.map +1 -0
  39. package/dist/adapters/wdk/ArkadeWdkAdapter.d.ts +53 -19
  40. package/dist/adapters/wdk/ArkadeWdkAdapter.d.ts.map +1 -1
  41. package/dist/adapters/wdk/ArkadeWdkAdapter.js +366 -90
  42. package/dist/adapters/wdk/ArkadeWdkAdapter.js.map +1 -1
  43. package/dist/adapters/wdk/BaseWdkAdapter.d.ts +40 -0
  44. package/dist/adapters/wdk/BaseWdkAdapter.d.ts.map +1 -0
  45. package/dist/adapters/wdk/BaseWdkAdapter.js +71 -0
  46. package/dist/adapters/wdk/BaseWdkAdapter.js.map +1 -0
  47. package/dist/adapters/wdk/LiquidWdkAdapter.d.ts +6 -13
  48. package/dist/adapters/wdk/LiquidWdkAdapter.d.ts.map +1 -1
  49. package/dist/adapters/wdk/LiquidWdkAdapter.js +14 -32
  50. package/dist/adapters/wdk/LiquidWdkAdapter.js.map +1 -1
  51. package/dist/adapters/wdk/RgbCore.d.ts +64 -0
  52. package/dist/adapters/wdk/RgbCore.d.ts.map +1 -0
  53. package/dist/adapters/wdk/RgbCore.js +111 -0
  54. package/dist/adapters/wdk/RgbCore.js.map +1 -0
  55. package/dist/adapters/wdk/RgbLibWasmAdapter.d.ts +277 -0
  56. package/dist/adapters/wdk/RgbLibWasmAdapter.d.ts.map +1 -0
  57. package/dist/adapters/wdk/RgbLibWasmAdapter.js +731 -0
  58. package/dist/adapters/wdk/RgbLibWasmAdapter.js.map +1 -0
  59. package/dist/adapters/wdk/RgbLibWdkAdapter.d.ts +104 -0
  60. package/dist/adapters/wdk/RgbLibWdkAdapter.d.ts.map +1 -0
  61. package/dist/adapters/wdk/RgbLibWdkAdapter.js +249 -0
  62. package/dist/adapters/wdk/RgbLibWdkAdapter.js.map +1 -0
  63. package/dist/adapters/wdk/RlnWdkAdapter.d.ts +27 -14
  64. package/dist/adapters/wdk/RlnWdkAdapter.d.ts.map +1 -1
  65. package/dist/adapters/wdk/RlnWdkAdapter.js +124 -89
  66. package/dist/adapters/wdk/RlnWdkAdapter.js.map +1 -1
  67. package/dist/adapters/wdk/SparkWdkAdapter.d.ts +74 -41
  68. package/dist/adapters/wdk/SparkWdkAdapter.d.ts.map +1 -1
  69. package/dist/adapters/wdk/SparkWdkAdapter.js +706 -249
  70. package/dist/adapters/wdk/SparkWdkAdapter.js.map +1 -1
  71. package/dist/adapters/wdk/index.d.ts +17 -0
  72. package/dist/adapters/wdk/index.d.ts.map +1 -0
  73. package/dist/adapters/wdk/index.js +17 -0
  74. package/dist/adapters/wdk/index.js.map +1 -0
  75. package/dist/adapters/wdk/wasm-rgb.d.ts +15 -0
  76. package/dist/adapters/wdk/wasm-rgb.d.ts.map +1 -0
  77. package/dist/adapters/wdk/wasm-rgb.js +15 -0
  78. package/dist/adapters/wdk/wasm-rgb.js.map +1 -0
  79. package/dist/capabilities/index.d.ts +1 -1
  80. package/dist/capabilities/index.d.ts.map +1 -1
  81. package/dist/capabilities/index.js +17 -2
  82. package/dist/capabilities/index.js.map +1 -1
  83. package/dist/capabilities/operations.d.ts +22 -0
  84. package/dist/capabilities/operations.d.ts.map +1 -0
  85. package/dist/capabilities/operations.js +62 -0
  86. package/dist/capabilities/operations.js.map +1 -0
  87. package/dist/constants.d.ts +8 -0
  88. package/dist/constants.d.ts.map +1 -0
  89. package/dist/constants.js +8 -0
  90. package/dist/constants.js.map +1 -0
  91. package/dist/disclosure/index.d.ts +1 -1
  92. package/dist/disclosure/index.js +1 -1
  93. package/dist/disclosure/index.js.map +1 -1
  94. package/dist/format.d.ts +11 -0
  95. package/dist/format.d.ts.map +1 -0
  96. package/dist/format.js +10 -0
  97. package/dist/format.js.map +1 -0
  98. package/dist/index.d.ts +21 -31
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +32 -32
  101. package/dist/index.js.map +1 -1
  102. package/dist/lib/arkade-client-manager.d.ts +64 -24
  103. package/dist/lib/arkade-client-manager.d.ts.map +1 -1
  104. package/dist/lib/arkade-client-manager.js +240 -65
  105. package/dist/lib/arkade-client-manager.js.map +1 -1
  106. package/dist/lib/arkade-converters.d.ts +39 -0
  107. package/dist/lib/arkade-converters.d.ts.map +1 -0
  108. package/dist/lib/arkade-converters.js +148 -0
  109. package/dist/lib/arkade-converters.js.map +1 -0
  110. package/dist/lib/arkade-helpers.d.ts +110 -0
  111. package/dist/lib/arkade-helpers.d.ts.map +1 -0
  112. package/dist/lib/arkade-helpers.js +227 -0
  113. package/dist/lib/arkade-helpers.js.map +1 -0
  114. package/dist/lib/arkade-swaps-client-manager.d.ts +55 -0
  115. package/dist/lib/arkade-swaps-client-manager.d.ts.map +1 -0
  116. package/dist/lib/arkade-swaps-client-manager.js +127 -0
  117. package/dist/lib/arkade-swaps-client-manager.js.map +1 -0
  118. package/dist/lib/arkade-vtxo-lifecycle.d.ts +116 -0
  119. package/dist/lib/arkade-vtxo-lifecycle.d.ts.map +1 -0
  120. package/dist/lib/arkade-vtxo-lifecycle.js +184 -0
  121. package/dist/lib/arkade-vtxo-lifecycle.js.map +1 -0
  122. package/dist/lib/flashnet-client-manager.d.ts +26 -9
  123. package/dist/lib/flashnet-client-manager.d.ts.map +1 -1
  124. package/dist/lib/flashnet-client-manager.js +97 -13
  125. package/dist/lib/flashnet-client-manager.js.map +1 -1
  126. package/dist/lib/kaleido-client-manager.d.ts +38 -3
  127. package/dist/lib/kaleido-client-manager.d.ts.map +1 -1
  128. package/dist/lib/kaleido-client-manager.js +79 -10
  129. package/dist/lib/kaleido-client-manager.js.map +1 -1
  130. package/dist/lib/ln-message-sign.d.ts +20 -0
  131. package/dist/lib/ln-message-sign.d.ts.map +1 -0
  132. package/dist/lib/ln-message-sign.js +90 -0
  133. package/dist/lib/ln-message-sign.js.map +1 -0
  134. package/dist/lib/log.d.ts +15 -0
  135. package/dist/lib/log.d.ts.map +1 -0
  136. package/dist/lib/log.js +16 -0
  137. package/dist/lib/log.js.map +1 -0
  138. package/dist/lib/orchestra-client.d.ts +149 -0
  139. package/dist/lib/orchestra-client.d.ts.map +1 -0
  140. package/dist/lib/orchestra-client.js +178 -0
  141. package/dist/lib/orchestra-client.js.map +1 -0
  142. package/dist/lib/psbt-signer.d.ts +60 -0
  143. package/dist/lib/psbt-signer.d.ts.map +1 -0
  144. package/dist/lib/psbt-signer.js +161 -0
  145. package/dist/lib/psbt-signer.js.map +1 -0
  146. package/dist/lib/rgb-converters.d.ts +62 -0
  147. package/dist/lib/rgb-converters.d.ts.map +1 -0
  148. package/dist/lib/rgb-converters.js +179 -0
  149. package/dist/lib/rgb-converters.js.map +1 -0
  150. package/dist/lib/rgb-fee-policy.d.ts +41 -0
  151. package/dist/lib/rgb-fee-policy.d.ts.map +1 -0
  152. package/dist/lib/rgb-fee-policy.js +52 -0
  153. package/dist/lib/rgb-fee-policy.js.map +1 -0
  154. package/dist/lib/rgb-helpers.d.ts +54 -0
  155. package/dist/lib/rgb-helpers.d.ts.map +1 -0
  156. package/dist/lib/rgb-helpers.js +89 -0
  157. package/dist/lib/rgb-helpers.js.map +1 -0
  158. package/dist/lib/spark-activity.d.ts +5 -0
  159. package/dist/lib/spark-activity.d.ts.map +1 -0
  160. package/dist/lib/spark-activity.js +11 -0
  161. package/dist/lib/spark-activity.js.map +1 -0
  162. package/dist/lib/spark-balance-cache.d.ts +58 -0
  163. package/dist/lib/spark-balance-cache.d.ts.map +1 -0
  164. package/dist/lib/spark-balance-cache.js +86 -0
  165. package/dist/lib/spark-balance-cache.js.map +1 -0
  166. package/dist/lib/spark-client-manager.d.ts +64 -10
  167. package/dist/lib/spark-client-manager.d.ts.map +1 -1
  168. package/dist/lib/spark-client-manager.js +191 -35
  169. package/dist/lib/spark-client-manager.js.map +1 -1
  170. package/dist/lib/spark-converters.d.ts +64 -0
  171. package/dist/lib/spark-converters.d.ts.map +1 -0
  172. package/dist/lib/spark-converters.js +242 -0
  173. package/dist/lib/spark-converters.js.map +1 -0
  174. package/dist/lib/spark-helpers.d.ts +72 -0
  175. package/dist/lib/spark-helpers.d.ts.map +1 -0
  176. package/dist/lib/spark-helpers.js +151 -0
  177. package/dist/lib/spark-helpers.js.map +1 -0
  178. package/dist/lib/spark-sent-token-records.d.ts +43 -0
  179. package/dist/lib/spark-sent-token-records.d.ts.map +1 -0
  180. package/dist/lib/spark-sent-token-records.js +105 -0
  181. package/dist/lib/spark-sent-token-records.js.map +1 -0
  182. package/dist/lib/wallet-seed.d.ts +31 -0
  183. package/dist/lib/wallet-seed.d.ts.map +1 -0
  184. package/dist/lib/wallet-seed.js +58 -0
  185. package/dist/lib/wallet-seed.js.map +1 -0
  186. package/dist/lib/zbase32.d.ts +3 -0
  187. package/dist/lib/zbase32.d.ts.map +1 -0
  188. package/dist/lib/zbase32.js +64 -0
  189. package/dist/lib/zbase32.js.map +1 -0
  190. package/dist/manager/ProtocolManager.d.ts +54 -3
  191. package/dist/manager/ProtocolManager.d.ts.map +1 -1
  192. package/dist/manager/ProtocolManager.js +118 -41
  193. package/dist/manager/ProtocolManager.js.map +1 -1
  194. package/dist/ports/index.d.ts +20 -0
  195. package/dist/ports/index.d.ts.map +1 -1
  196. package/dist/ports/index.js +23 -1
  197. package/dist/ports/index.js.map +1 -1
  198. package/dist/receive/unifiedReceive.d.ts +12 -0
  199. package/dist/receive/unifiedReceive.d.ts.map +1 -1
  200. package/dist/receive/unifiedReceive.js +35 -4
  201. package/dist/receive/unifiedReceive.js.map +1 -1
  202. package/dist/registry/createWdkRegistry.d.ts +10 -2
  203. package/dist/registry/createWdkRegistry.d.ts.map +1 -1
  204. package/dist/registry/createWdkRegistry.js +14 -7
  205. package/dist/registry/createWdkRegistry.js.map +1 -1
  206. package/dist/router/destination.d.ts +2 -2
  207. package/dist/router/destination.d.ts.map +1 -1
  208. package/dist/router/destination.js +34 -11
  209. package/dist/router/destination.js.map +1 -1
  210. package/dist/router/index.d.ts +39 -3
  211. package/dist/router/index.d.ts.map +1 -1
  212. package/dist/router/index.js +113 -4
  213. package/dist/router/index.js.map +1 -1
  214. package/dist/router/preference.d.ts +53 -0
  215. package/dist/router/preference.d.ts.map +1 -0
  216. package/dist/router/preference.js +81 -0
  217. package/dist/router/preference.js.map +1 -0
  218. package/dist/swap/KaleidoswapSwap.d.ts +1 -1
  219. package/dist/swap/KaleidoswapSwap.d.ts.map +1 -1
  220. package/dist/swap/KaleidoswapSwap.js +37 -20
  221. package/dist/swap/KaleidoswapSwap.js.map +1 -1
  222. package/dist/swap/index.d.ts +8 -0
  223. package/dist/swap/index.d.ts.map +1 -0
  224. package/dist/swap/index.js +8 -0
  225. package/dist/swap/index.js.map +1 -0
  226. package/dist/types/arkade.d.ts +1 -1
  227. package/dist/types/base.d.ts +35 -25
  228. package/dist/types/base.d.ts.map +1 -1
  229. package/dist/types/base.js +28 -2
  230. package/dist/types/base.js.map +1 -1
  231. package/dist/types/cross-l2.d.ts +1 -1
  232. package/dist/types/flashnet.d.ts +20 -0
  233. package/dist/types/flashnet.d.ts.map +1 -1
  234. package/dist/types/flashnet.js +34 -6
  235. package/dist/types/flashnet.js.map +1 -1
  236. package/dist/types/rgb.d.ts +18 -4
  237. package/dist/types/rgb.d.ts.map +1 -1
  238. package/dist/types/spark.d.ts +1 -1
  239. package/dist/utils.d.ts +1 -1
  240. package/dist/utils.js +2 -2
  241. package/dist/utils.js.map +1 -1
  242. package/package.json +68 -14
@@ -1,31 +1,52 @@
1
1
  /**
2
2
  * SparkWdkAdapter
3
3
  * ---------------
4
- * Thin adapter mapping the WDK Spark module (@tetherto/wdk-wallet-spark) onto the
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
- * - Protocol quirks (zero-fee, static address) live in the capability manifest,
12
- * not in this interface.
13
- *
14
- * WDK Spark account surface (captured via Spike A, 2026-06-03):
15
- * manager: getAccount, getAccountByPath, getFeeRates
16
- * account: getAddress, getBalance, sendTransaction, transfer,
17
- * getStaticDepositAddress, getSingleUseDepositAddress, quoteWithdraw,
18
- * withdraw, createLightningInvoice, payLightningInvoice,
19
- * createSparkSatsInvoice, createSparkTokensInvoice, paySparkInvoice,
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 { loadWdkModule } from './moduleLoader';
28
- import { decodeBolt11, isBolt11 } from '../../lib/bolt11';
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
- this.version = '0.1.0-wdk';
49
- this.manager = null;
50
- this.account = null;
51
- this.connected = false;
52
- this.network = 'mainnet';
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
- this.manager = new WalletManagerSpark(cfg.mnemonic, {
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?.dispose?.();
74
- await this.account?.cleanupConnections?.();
114
+ this.identityPubKeyHex = toHexLower(await this.account.getIdentityKey?.()) || null;
75
115
  }
76
- finally {
77
- this.account = null;
78
- this.manager = null;
79
- this.connected = false;
116
+ catch {
117
+ this.identityPubKeyHex = null;
80
118
  }
81
- }
82
- isConnected() {
83
- return this.connected;
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
- return { protocol: 'SPARK', connected: this.connected, network: this.network };
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
- if (assetId === 'onchain' || assetId === 'BTC') {
99
- const address = await this.account.getSingleUseDepositAddress();
100
- return { address, format: 'BTC_ADDRESS' };
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
- catch (error) {
159
- return {
160
- addressesChecked: 0,
161
- claimedTxids: [],
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
- return { addressesChecked: addresses.length, claimedTxids, errors };
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
- // WDK: getBalance(): Promise<bigint> sats, settled balance.
198
- const bal = await this.account.getBalance();
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
- await this.account.syncWalletBalance?.();
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 { total } = await this.getBtcBalance();
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: 'BTC_SPARK',
187
+ layer: 'SPARK_SPARK',
216
188
  balance: {
217
- total,
218
- available: total,
189
+ total: balanceSats,
190
+ available: balanceSats,
219
191
  pending: 0,
220
- totalDisplay: String(total),
221
- availableDisplay: String(total),
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
- // TODO(Phase 2): enumerate Spark tokens from account.getBalance() token map.
232
- return [btc];
233
- }
234
- async getAssetBalance(assetId) {
235
- const assets = await this.listAssets();
236
- const found = assets.find((a) => a.id === assetId);
237
- if (!found)
238
- throw new ProtocolError(`Unknown asset ${assetId}`, 'SPARK', 'NO_ASSET');
239
- return found.balance;
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(`Unknown asset ${assetId}`, 'SPARK', 'NO_ASSET');
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 encoded = r?.invoice?.encodedInvoice ?? r?.encodedInvoice ?? r?.invoice ?? '';
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: r?.invoice?.paymentHash ?? r?.id ?? '',
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
- // --- Send ---------------------------------------------------------------
286
- async sendPayment(request) {
290
+ /** Optional: explicit native Spark sats invoice (used by the receive UI). */
291
+ async createSparkInvoice(request) {
287
292
  this.assertConnected();
288
- const dest = request.invoice.trim();
289
- const isBolt11 = /^ln(bc|tb|bcrt)/i.test(dest);
290
- const timestamp = Date.now();
291
- // 1) Lightning send WDK requires a maxFeeSats cap.
292
- if (isBolt11) {
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
- paymentHash: ok?.transferResponse?.id ?? '',
322
- amount: Number(request.amount ?? 0),
323
- fee: 0,
324
- status: ok ? 'confirmed' : 'failed',
325
- timestamp,
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
- async getPaymentStatus(paymentHash) {
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
- const t = await this.account.getTransactionReceipt(paymentHash).catch(() => null);
347
- return { paymentHash, status: mapSparkStatus(t?.status), amount: t ? Number(t.totalValue ?? 0) : undefined };
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 transfers = await this.account.getTransfers({ limit: filter?.limit ?? 50, skip: filter?.offset ?? 0 });
352
- return (transfers ?? []).map((t) => this.toUnifiedTx(t));
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(`Unknown tx ${txId}`, 'SPARK', 'NO_TX');
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
- const dir = String(t?.transferDirection ?? t?.direction ?? '').toUpperCase();
390
- const isReceive = dir.includes('INCOMING') || dir.includes('RECEIV');
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
- // --- helpers ------------------------------------------------------------
411
- assertConnected() {
412
- if (!this.connected || !this.account) {
413
- throw new ProtocolError('SparkWdkAdapter not connected', 'SPARK', 'NOT_CONNECTED');
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