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