lnlink-server 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var require_config_default = __commonJS({
22
22
  LINK_DATA_PATH: "",
23
23
  LINK_FLASH_SITE_BASE_URL: "https://devofflash.unift.xyz",
24
24
  LINK_LNFI_NODE_SITE_URL: "https://devoflnnode.unift.xyz",
25
+ LINK_RGB_PRICE_SERVER_URL: "https://api-oracle.lnfi.network",
25
26
  LINK_OWNER: "",
26
27
  LINK_SETTING_FILE_PATH: ""
27
28
  // LINK_SETTING_FILE_PATH: "",
@@ -880,7 +881,9 @@ var require_constants = __commonJS({
880
881
  "link_status",
881
882
  "list_unspents",
882
883
  "decode_rgb_invoice",
883
- "decode_ln_invoice"
884
+ "decode_ln_invoice",
885
+ "get_exchange_rate",
886
+ "list_exchange_orders"
884
887
  ];
885
888
  var OWNER_PERMISSIONS = [
886
889
  "genseed",
@@ -922,6 +925,7 @@ var require_constants = __commonJS({
922
925
  "pay_rgb_invoice",
923
926
  "disconnect_peer",
924
927
  "create_invoice",
928
+ "create_hodl_invoice",
925
929
  "pay_invoice",
926
930
  "backup_node",
927
931
  "restore_node",
@@ -1286,6 +1290,163 @@ var require_events = __commonJS({
1286
1290
  }
1287
1291
  });
1288
1292
 
1293
+ // business/service/prisma/repositories/exchangeOrderRepository.js
1294
+ var require_exchangeOrderRepository = __commonJS({
1295
+ "business/service/prisma/repositories/exchangeOrderRepository.js"(exports2, module2) {
1296
+ var PrismaService = require_prismaService();
1297
+ var ExchangeOrderRepository = class {
1298
+ static {
1299
+ __name(this, "ExchangeOrderRepository");
1300
+ }
1301
+ constructor() {
1302
+ this.prisma = PrismaService.getInstance().getClient();
1303
+ }
1304
+ async createOrder(data) {
1305
+ const now = Math.floor(Date.now() / 1e3);
1306
+ return this.prisma.exchangeOrder.create({
1307
+ data: {
1308
+ payment_hash: data.payment_hash,
1309
+ btc_invoice: data.btc_invoice,
1310
+ btc_amount: data.btc_amount,
1311
+ rgb_invoice: data.rgb_invoice || null,
1312
+ asset_id: data.asset_id,
1313
+ asset_amount: data.asset_amount,
1314
+ preimage: data.preimage || null,
1315
+ status: data.status,
1316
+ error_message: data.error_message || null,
1317
+ htlc_expiry_at: data.htlc_expiry_at,
1318
+ created_at: data.created_at || now,
1319
+ updated_at: data.updated_at || now
1320
+ }
1321
+ });
1322
+ }
1323
+ async getOrderById(id) {
1324
+ return this.prisma.exchangeOrder.findUnique({ where: { id } });
1325
+ }
1326
+ async getOrderByPaymentHash(payment_hash) {
1327
+ return this.prisma.exchangeOrder.findUnique({ where: { payment_hash } });
1328
+ }
1329
+ async getOrdersByStatus(status) {
1330
+ return this.prisma.exchangeOrder.findMany({
1331
+ where: { status },
1332
+ orderBy: { created_at: "asc" }
1333
+ });
1334
+ }
1335
+ async getOrders({
1336
+ status,
1337
+ page = 1,
1338
+ page_size = 20
1339
+ } = {}) {
1340
+ const where = status ? { status } : {};
1341
+ const [list, total] = await Promise.all([
1342
+ this.prisma.exchangeOrder.findMany({
1343
+ where,
1344
+ orderBy: { created_at: "desc" },
1345
+ skip: (page - 1) * page_size,
1346
+ take: page_size
1347
+ }),
1348
+ this.prisma.exchangeOrder.count({ where })
1349
+ ]);
1350
+ return { list, total };
1351
+ }
1352
+ async updateOrder(id, data) {
1353
+ return this.prisma.exchangeOrder.update({
1354
+ where: { id },
1355
+ data: {
1356
+ ...data,
1357
+ updated_at: Math.floor(Date.now() / 1e3)
1358
+ }
1359
+ });
1360
+ }
1361
+ /**
1362
+ * Optimistic lock: only update if current status matches expectedStatus.
1363
+ * Returns true if update succeeded, false if status already changed.
1364
+ */
1365
+ async updateOrderWithExpectedStatus(id, expectedStatus, data) {
1366
+ const result = await this.prisma.exchangeOrder.updateMany({
1367
+ where: { id, status: expectedStatus },
1368
+ data: {
1369
+ ...data,
1370
+ updated_at: Math.floor(Date.now() / 1e3)
1371
+ }
1372
+ });
1373
+ return result.count > 0;
1374
+ }
1375
+ async getOrdersByStatusIn(statuses) {
1376
+ return this.prisma.exchangeOrder.findMany({
1377
+ where: { status: { in: statuses } },
1378
+ orderBy: { created_at: "asc" }
1379
+ });
1380
+ }
1381
+ };
1382
+ module2.exports = ExchangeOrderRepository;
1383
+ }
1384
+ });
1385
+
1386
+ // business/service/prisma/db/exchangeOrders.js
1387
+ var require_exchangeOrders = __commonJS({
1388
+ "business/service/prisma/db/exchangeOrders.js"(exports2, module2) {
1389
+ var Logger = require_linkLogger();
1390
+ var ExchangeOrderRepository = require_exchangeOrderRepository();
1391
+ var repo = null;
1392
+ function getRepo() {
1393
+ if (!repo) {
1394
+ repo = new ExchangeOrderRepository();
1395
+ }
1396
+ return repo;
1397
+ }
1398
+ __name(getRepo, "getRepo");
1399
+ module2.exports = {
1400
+ createExchangeOrder: /* @__PURE__ */ __name(async (data) => {
1401
+ const logger2 = new Logger("exchangeOrderDB");
1402
+ try {
1403
+ return await getRepo().createOrder(data);
1404
+ } catch (error) {
1405
+ logger2.error("Error creating exchange order:", error.message);
1406
+ throw error;
1407
+ }
1408
+ }, "createExchangeOrder"),
1409
+ getExchangeOrderById: /* @__PURE__ */ __name(async (id) => {
1410
+ return getRepo().getOrderById(id);
1411
+ }, "getExchangeOrderById"),
1412
+ getExchangeOrderByPaymentHash: /* @__PURE__ */ __name(async (payment_hash) => {
1413
+ return getRepo().getOrderByPaymentHash(payment_hash);
1414
+ }, "getExchangeOrderByPaymentHash"),
1415
+ getExchangeOrdersByStatus: /* @__PURE__ */ __name(async (status) => {
1416
+ return getRepo().getOrdersByStatus(status);
1417
+ }, "getExchangeOrdersByStatus"),
1418
+ getExchangeOrdersByStatusIn: /* @__PURE__ */ __name(async (statuses) => {
1419
+ return getRepo().getOrdersByStatusIn(statuses);
1420
+ }, "getExchangeOrdersByStatusIn"),
1421
+ getExchangeOrders: /* @__PURE__ */ __name(async (filters) => {
1422
+ return getRepo().getOrders(filters);
1423
+ }, "getExchangeOrders"),
1424
+ updateExchangeOrder: /* @__PURE__ */ __name(async (id, data) => {
1425
+ const logger2 = new Logger("exchangeOrderDB");
1426
+ try {
1427
+ return await getRepo().updateOrder(id, data);
1428
+ } catch (error) {
1429
+ logger2.error("Error updating exchange order:", error.message);
1430
+ throw error;
1431
+ }
1432
+ }, "updateExchangeOrder"),
1433
+ /**
1434
+ * Optimistic lock update: only succeeds if current status matches expectedStatus.
1435
+ * Returns true if updated, false if status already changed (stale).
1436
+ */
1437
+ updateExchangeOrderWithExpectedStatus: /* @__PURE__ */ __name(async (id, expectedStatus, data) => {
1438
+ const logger2 = new Logger("exchangeOrderDB");
1439
+ try {
1440
+ return await getRepo().updateOrderWithExpectedStatus(id, expectedStatus, data);
1441
+ } catch (error) {
1442
+ logger2.error("Error updating exchange order (optimistic):", error.message);
1443
+ throw error;
1444
+ }
1445
+ }, "updateExchangeOrderWithExpectedStatus")
1446
+ };
1447
+ }
1448
+ });
1449
+
1289
1450
  // business/service/prisma/repositories/orderRepository.js
1290
1451
  var require_orderRepository = __commonJS({
1291
1452
  "business/service/prisma/repositories/orderRepository.js"(exports2, module2) {
@@ -2532,6 +2693,7 @@ var require_dbService = __commonJS({
2532
2693
  "business/service/prisma/dbService.js"(exports2, module2) {
2533
2694
  var config = require_config();
2534
2695
  var events = require_events();
2696
+ var exchangeOrders = require_exchangeOrders();
2535
2697
  var orders = require_orders();
2536
2698
  var transactions = require_transactions();
2537
2699
  var users = require_users();
@@ -2542,6 +2704,7 @@ var require_dbService = __commonJS({
2542
2704
  ...users,
2543
2705
  ...transactions,
2544
2706
  ...orders,
2707
+ ...exchangeOrders,
2545
2708
  ...utils
2546
2709
  };
2547
2710
  }
@@ -2651,6 +2814,7 @@ var require_getConfig = __commonJS({
2651
2814
  officialUniverseServer,
2652
2815
  priceOracle,
2653
2816
  rgbProxy,
2817
+ rgbPriceServer,
2654
2818
  enableTor
2655
2819
  } = settingFileData;
2656
2820
  const litdRgbConfig = {
@@ -2667,6 +2831,7 @@ var require_getConfig = __commonJS({
2667
2831
  LINK_RGB_BITCOIND_RPCPASS: bitcoindPass,
2668
2832
  LINK_RGB_ELECTRS_HOST: bitcoindIndex,
2669
2833
  LINK_RGB_PROXY_ENDPOINT: rgbProxy,
2834
+ LINK_RGB_PRICE_SERVER_URL: rgbPriceServer || void 0,
2670
2835
  LINK_RGB_REMOTE_NODE_PUBKEY: officialRgbPeer,
2671
2836
  LINK_RGB_REMOTE_NODE_HOST: officialRgbPeerHost,
2672
2837
  LINK_BITCOIND_RPCHOST: bitcoindRpcHost && bitcoindRpcPort ? `${bitcoindRpcHost}:${bitcoindRpcPort}` : void 0,
@@ -2900,13 +3065,16 @@ var require_getConfig = __commonJS({
2900
3065
  }
2901
3066
  combinedConfig.LINK_TOR_SOCKS_PORT = combinedConfig.LINK_TOR_SOCKS_PORT || 9050;
2902
3067
  combinedConfig.LINK_TOR_CONTROL_PORT = combinedConfig.LINK_TOR_CONTROL_PORT || 9051;
2903
- try {
2904
- combinedConfig = await assignAvailablePorts(combinedConfig);
2905
- } catch (portError) {
2906
- console.warn(
2907
- "Port assignment failed, using default ports:",
2908
- portError.message
2909
- );
3068
+ const isExternalNodes = combinedConfig.LINK_EXTERNAL_NODES === "true" || combinedConfig.LINK_EXTERNAL_NODES === true;
3069
+ if (!isExternalNodes) {
3070
+ try {
3071
+ combinedConfig = await assignAvailablePorts(combinedConfig);
3072
+ } catch (portError) {
3073
+ console.warn(
3074
+ "Port assignment failed, using default ports:",
3075
+ portError.message
3076
+ );
3077
+ }
2910
3078
  }
2911
3079
  combinedConfig.LINK_GRPC_HOST = `localhost:${combinedConfig.LINK_LND_RPC_PORT}`;
2912
3080
  combinedConfig.LINK_ENABLE_TOR = combinedConfig.LINK_ENABLE_TOR === "true" || combinedConfig.LINK_ENABLE_TOR === true;
@@ -5384,6 +5552,10 @@ var require_config2 = __commonJS({
5384
5552
  }
5385
5553
  try {
5386
5554
  const config = getConfig2();
5555
+ const isExternalNodes = config.LINK_EXTERNAL_NODES === "true" || config.LINK_EXTERNAL_NODES === true;
5556
+ if (isExternalNodes) {
5557
+ return null;
5558
+ }
5387
5559
  const torBinary = path2.join(config.LINK_BINARY_PATH || "", getBinaryName("tor"));
5388
5560
  const output = execSync(`"${torBinary}" --hash-password "${TOR_CONTROL_PASSWORD}"`, {
5389
5561
  encoding: "utf-8",
@@ -6108,7 +6280,8 @@ var require_client4 = __commonJS({
6108
6280
  throw new Error("RGB configuration not available");
6109
6281
  }
6110
6282
  rgbClient = new RgbApiClient({
6111
- baseUrl: `http://127.0.0.1:${config.LINK_RGB_LISTENING_PORT}`
6283
+ // baseUrl: `http://127.0.0.1:${config.LINK_RGB_LISTENING_PORT}`,
6284
+ baseUrl: "http://35.221.95.26:9744"
6112
6285
  });
6113
6286
  }
6114
6287
  return rgbClient;
@@ -7181,7 +7354,7 @@ var require_package = __commonJS({
7181
7354
  "package.json"(exports2, module2) {
7182
7355
  module2.exports = {
7183
7356
  name: "lnlink-server",
7184
- version: "1.1.7",
7357
+ version: "1.1.8",
7185
7358
  private: false,
7186
7359
  main: "dist/index.js",
7187
7360
  files: [
@@ -7193,7 +7366,7 @@ var require_package = __commonJS({
7193
7366
  build: "node build.js && node build.js --mode development --external all --entry electron",
7194
7367
  "start:bin": "node scripts/start-bin.js",
7195
7368
  "start:docker:dev": "dotenv -e .env.dev -- docker compose -f ./docker-compose.dev.yml up --build",
7196
- "start:dev": 'dotenv -e .env.dev -- sh -c "prisma generate && (prisma migrate dev --name auto_update || prisma db push) && clinic heapprof -- node ./app.js"',
7369
+ "start:dev": 'dotenv -e .env.dev -- sh -c "prisma generate && (prisma migrate dev --name auto_update || prisma db push) && node ./app.js"',
7197
7370
  "start:regtest": "docker compose --env-file ./.env.regtest -f ./docker-compose-lnlink.yml up --build",
7198
7371
  "start:testnet": "docker compose --env-file ./.env.testnet -f ./docker-compose-lnlink.yml up --build",
7199
7372
  "start:mainnet": "docker compose --env-file ./.env.mainnet -f ./docker-compose-lnlink.yml up --build",
@@ -8587,7 +8760,11 @@ var require_api = __commonJS({
8587
8760
  socket.setTimeout(TIMEOUT);
8588
8761
  socket.once("connect", () => {
8589
8762
  socket.destroy();
8590
- resolve({ host, port, reachable: true });
8763
+ resolve({
8764
+ host,
8765
+ port,
8766
+ reachable: true
8767
+ });
8591
8768
  });
8592
8769
  socket.once("timeout", () => {
8593
8770
  socket.destroy();
@@ -8604,10 +8781,26 @@ var require_api = __commonJS({
8604
8781
  __name(tryConnect2, "tryConnect");
8605
8782
  const net = require("node:net");
8606
8783
  const TOR_DIR_AUTHORITIES = [
8607
- { host: "128.31.0.34", port: 9131, name: "moria1" },
8608
- { host: "193.23.244.244", port: 443, name: "dannenberg" },
8609
- { host: "199.58.81.140", port: 80, name: "Faravahar" },
8610
- { host: "86.59.21.38", port: 443, name: "gabelmoo" }
8784
+ {
8785
+ host: "128.31.0.34",
8786
+ port: 9131,
8787
+ name: "moria1"
8788
+ },
8789
+ {
8790
+ host: "193.23.244.244",
8791
+ port: 443,
8792
+ name: "dannenberg"
8793
+ },
8794
+ {
8795
+ host: "199.58.81.140",
8796
+ port: 80,
8797
+ name: "Faravahar"
8798
+ },
8799
+ {
8800
+ host: "86.59.21.38",
8801
+ port: 443,
8802
+ name: "gabelmoo"
8803
+ }
8611
8804
  ];
8612
8805
  const TIMEOUT = 5e3;
8613
8806
  let reachable = false;
@@ -9478,10 +9671,20 @@ var require_tapdService = __commonJS({
9478
9671
  const decodedInvoice = decode(payment_request);
9479
9672
  const routingInfo = decodedInvoice.tags.find((tag) => tag.tagName === "routing_info");
9480
9673
  if (routingInfo && routingInfo.data && routingInfo.data.length > 0) {
9481
- const lastHop = routingInfo.data[routingInfo.data.length - 1];
9482
- if (lastHop.pubkey) {
9483
- paymentRequest.last_hop_pubkey = Buffer2.from(lastHop.pubkey, "hex");
9484
- }
9674
+ const route_hints = routingInfo.data.map((hop) => {
9675
+ return {
9676
+ hop_hints: [
9677
+ {
9678
+ node_id: hop.pubkey,
9679
+ chan_id: BigInt(`0x${hop.short_channel_id}`).toString(),
9680
+ fee_base_msat: hop.fee_base_msat,
9681
+ fee_proportional_millionths: hop.fee_proportional_millionths,
9682
+ cltv_expiry_delta: hop.cltv_expiry_delta
9683
+ }
9684
+ ]
9685
+ };
9686
+ });
9687
+ paymentRequest.route_hints = route_hints;
9485
9688
  }
9486
9689
  const sendParams = {
9487
9690
  asset_amount,
@@ -9690,7 +9893,7 @@ var require_tapdService = __commonJS({
9690
9893
  if (!retIsConnectPeer) {
9691
9894
  throw new Error("Peer not connected");
9692
9895
  }
9693
- const invoice = await tapChannelService.addInvoice({
9896
+ const createInvoiceArgs = {
9694
9897
  asset_amount,
9695
9898
  asset_id: Buffer2.from(asset_id, "hex"),
9696
9899
  peer_pubkey: Buffer2.from(remotePubkey, "hex"),
@@ -9699,7 +9902,8 @@ var require_tapdService = __commonJS({
9699
9902
  expiry,
9700
9903
  description_hash: description_hash ? Buffer2.from(description_hash, "hex") : void 0
9701
9904
  }
9702
- });
9905
+ };
9906
+ const invoice = await tapChannelService.addInvoice(createInvoiceArgs);
9703
9907
  const payment_req = invoice?.invoice_result?.payment_request;
9704
9908
  if (payment_req) {
9705
9909
  await sleep(500);
@@ -9853,7 +10057,7 @@ var require_constants2 = __commonJS({
9853
10057
  SOCIAL_ID_MISMATCH: "Social id don't match",
9854
10058
  METHOD_NOT_SUPPORTED: "Method not supported"
9855
10059
  };
9856
- var PRIVILEGED_METHODS = ["make_invoice", "pay_invoice"];
10060
+ var PRIVILEGED_METHODS = ["make_invoice", "pay_invoice", "create_hodl_invoice"];
9857
10061
  var FLASH_ACCOUNT_METHODS = [
9858
10062
  "make_invoice",
9859
10063
  "pay_invoice",
@@ -11354,8 +11558,9 @@ var require_nwcProxy = __commonJS({
11354
11558
  }
11355
11559
  } else {
11356
11560
  const filterChannelList = channelList.filter((item) => {
11357
- const itemAsset = item?.custom_channel_data?.assets?.[0];
11358
- return item.commitment_type.includes("TAPROOT") && item.active === true && itemAsset?.asset_utxo?.asset_genesis?.asset_id === assetId;
11561
+ const asset_genesis = item?.custom_channel_data?.assets?.[0]?.asset_utxo?.asset_genesis || item?.custom_channel_data?.funding_assets?.[0]?.asset_genesis;
11562
+ const custom_channel_asset_id = asset_genesis?.asset_id;
11563
+ return item.commitment_type.includes("TAPROOT") && item.active === true && custom_channel_asset_id === assetId;
11359
11564
  });
11360
11565
  if (filterChannelList.length > 0) {
11361
11566
  return filterChannelList.sort((a, b) => {
@@ -11488,9 +11693,15 @@ var require_nwcProxy = __commonJS({
11488
11693
  }
11489
11694
  const create_at = Math.floor(Date.now() / 1e3);
11490
11695
  const expire_at = create_at + (expiry ?? 5 * 60);
11696
+ const chan_id = await getBestOutgoingChainId(
11697
+ asset_id
11698
+ ).catch(() => {
11699
+ return false;
11700
+ });
11491
11701
  const invoice = await addInvoice({
11492
11702
  asset_amount: amount,
11493
11703
  asset_id,
11704
+ chan_id,
11494
11705
  description,
11495
11706
  description_hash,
11496
11707
  expiry,
@@ -11627,7 +11838,9 @@ var require_nwcProxy = __commonJS({
11627
11838
  result: {
11628
11839
  preimage: payment?.payment_result?.payment_preimage,
11629
11840
  payment_hash: payment?.payment_result?.payment_hash,
11630
- asset_id: lnlinkUser.asset_id
11841
+ asset_id: lnlinkUser.asset_id,
11842
+ status: payment?.payment_result?.status,
11843
+ failure_reason: payment?.payment_result?.failure_reason
11631
11844
  }
11632
11845
  });
11633
11846
  }
@@ -11803,7 +12016,7 @@ var require_nwcProxy = __commonJS({
11803
12016
  user_npub: lnlinkUser.npub,
11804
12017
  node_type: NODE_TYPE.LITD,
11805
12018
  transaction_kind: TRANSACTION_KIND.LIGHTNING,
11806
- asset_id
12019
+ asset_id: lnlinkUser?.asset_id || asset_id
11807
12020
  });
11808
12021
  total = allTotal;
11809
12022
  return allList.map((item) => ({
@@ -12222,6 +12435,7 @@ var require_lightning = __commonJS({
12222
12435
  push_msat,
12223
12436
  asset_amount,
12224
12437
  asset_id,
12438
+ push_asset_amount = 0,
12225
12439
  isPublic = true,
12226
12440
  with_anchors = true,
12227
12441
  fee_base_msat = 1e3,
@@ -12234,12 +12448,11 @@ var require_lightning = __commonJS({
12234
12448
  host,
12235
12449
  capacity_sat,
12236
12450
  push_msat,
12237
- asset_amount,
12238
- asset_id,
12239
12451
  public: isPublic,
12240
12452
  with_anchors,
12241
12453
  fee_base_msat: Number(fee_base_msat),
12242
- fee_proportional_millionths
12454
+ fee_proportional_millionths,
12455
+ ...asset_id ? { asset_id, asset_amount, push_asset_amount } : {}
12243
12456
  };
12244
12457
  const { is_connected, peer } = await isConnectPeer({
12245
12458
  pubkey
@@ -12950,6 +13163,190 @@ var require_rgb = __commonJS({
12950
13163
  }
12951
13164
  });
12952
13165
 
13166
+ // business/service/rgb/exchange.js
13167
+ var require_exchange = __commonJS({
13168
+ "business/service/rgb/exchange.js"(exports2, module2) {
13169
+ var { checkObjectArgs } = require_common();
13170
+ var { getConfigValue } = require_getConfig();
13171
+ var {
13172
+ createExchangeOrder,
13173
+ getExchangeOrderByPaymentHash,
13174
+ getExchangeOrderById,
13175
+ getExchangeOrders,
13176
+ updateExchangeOrder
13177
+ } = require_dbService();
13178
+ var Logger = require_linkLogger();
13179
+ var getRgbClient = require_client4();
13180
+ var { decodeLnInvoice, listChannels } = require_lightning();
13181
+ var EXCHANGE_ORDER_STATUS = {
13182
+ CREATED: "created",
13183
+ RGB_HODL_ISSUED: "rgb_hodl_issued",
13184
+ RGB_PAID: "rgb_paid",
13185
+ BTC_SENDING: "btc_sending",
13186
+ BTC_SENT: "btc_sent",
13187
+ COMPLETED: "completed",
13188
+ FAILED: "failed"
13189
+ };
13190
+ var HODL_BUFFER_SEC = 7200;
13191
+ async function createHodlInvoice({
13192
+ btc_invoice,
13193
+ asset_id,
13194
+ asset_amount
13195
+ }) {
13196
+ const logger2 = new Logger("rgb-exchange");
13197
+ checkObjectArgs({
13198
+ btc_invoice,
13199
+ asset_id,
13200
+ asset_amount
13201
+ }, ["btc_invoice", "asset_id", "asset_amount"]);
13202
+ const parsedAssetAmount = Number(asset_amount);
13203
+ if (!Number.isFinite(parsedAssetAmount) || parsedAssetAmount <= 0 || !Number.isInteger(parsedAssetAmount)) {
13204
+ throw new Error(`asset_amount must be a positive integer, got: ${asset_amount}`);
13205
+ }
13206
+ const decoded = await decodeLnInvoice({ invoice: btc_invoice });
13207
+ const {
13208
+ payment_hash,
13209
+ amt_msat,
13210
+ timestamp,
13211
+ expiry_sec: btc_expiry_sec
13212
+ } = decoded;
13213
+ if (!payment_hash) {
13214
+ throw new Error("Failed to extract payment_hash from BTC invoice");
13215
+ }
13216
+ if (!amt_msat) {
13217
+ throw new Error("BTC invoice has no amount");
13218
+ }
13219
+ const now = Math.floor(Date.now() / 1e3);
13220
+ const btcExpireAt = Number(timestamp) + Number(btc_expiry_sec);
13221
+ const remaining = btcExpireAt - now;
13222
+ if (remaining <= HODL_BUFFER_SEC) {
13223
+ throw new Error(`BTC invoice expires too soon (remaining: ${remaining}s, required > ${HODL_BUFFER_SEC}s)`);
13224
+ }
13225
+ const hodl_expiry_sec = remaining - HODL_BUFFER_SEC;
13226
+ const { channels = [] } = await listChannels();
13227
+ const btcOutbound = channels.filter((ch) => !ch.asset_id && (ch.is_usable !== false && ch.ready !== false)).reduce((sum, ch) => {
13228
+ const balance = Number(ch.outbound_balance_msat || ch.local_balance_msat || 0);
13229
+ return sum + balance;
13230
+ }, 0);
13231
+ if (btcOutbound === 0) {
13232
+ logger2.warn(`BTC outbound balance is 0 \u2014 verify channel field names. Sample channel: ${JSON.stringify(channels[0] || {})}`);
13233
+ }
13234
+ if (btcOutbound < Number(amt_msat)) {
13235
+ throw new Error(`Insufficient BTC outbound liquidity (available: ${btcOutbound} msat, required: ${amt_msat} msat)`);
13236
+ }
13237
+ const existing = await getExchangeOrderByPaymentHash(payment_hash);
13238
+ if (existing) {
13239
+ throw new Error(`Exchange order already exists for payment_hash: ${payment_hash}`);
13240
+ }
13241
+ const RGB_MIN_AMT_MSAT = 3 * 1e3 * 1e3;
13242
+ const rgbClient = getRgbClient();
13243
+ const invoiceResult = await rgbClient.lightning.createHodlInvoice({
13244
+ payment_hash,
13245
+ amt_msat: RGB_MIN_AMT_MSAT,
13246
+ expiry_sec: hodl_expiry_sec,
13247
+ asset_id,
13248
+ asset_amount: parsedAssetAmount
13249
+ });
13250
+ const htlc_expiry_at = now + hodl_expiry_sec;
13251
+ const order = await createExchangeOrder({
13252
+ payment_hash,
13253
+ btc_invoice,
13254
+ btc_amount: String(amt_msat),
13255
+ rgb_invoice: invoiceResult.invoice,
13256
+ asset_id,
13257
+ asset_amount: String(asset_amount),
13258
+ status: EXCHANGE_ORDER_STATUS.RGB_HODL_ISSUED,
13259
+ htlc_expiry_at,
13260
+ created_at: now,
13261
+ updated_at: now
13262
+ });
13263
+ logger2.info(`Exchange order created: id=${order.id}, payment_hash=${payment_hash}`);
13264
+ return {
13265
+ invoice: invoiceResult.invoice,
13266
+ payment_hash,
13267
+ expiry_sec: hodl_expiry_sec,
13268
+ exchange_order_id: order.id,
13269
+ btc_amount: String(amt_msat)
13270
+ };
13271
+ }
13272
+ __name(createHodlInvoice, "createHodlInvoice");
13273
+ var BTC_ASSET_ID = "0000000000000000000000000000000000000000000000000000000000000000";
13274
+ async function getExchangeRate({ asset_id }) {
13275
+ const logger2 = new Logger("rgb-exchange");
13276
+ checkObjectArgs({ asset_id }, ["asset_id"]);
13277
+ const priceServerUrl = getConfigValue("LINK_RGB_PRICE_SERVER_URL");
13278
+ if (!priceServerUrl) {
13279
+ throw new Error("Price server URL not configured (LINK_RGB_PRICE_SERVER_URL)");
13280
+ }
13281
+ if (!priceServerUrl.startsWith("http://") && !priceServerUrl.startsWith("https://")) {
13282
+ throw new Error(`Invalid price server URL scheme: ${priceServerUrl}`);
13283
+ }
13284
+ const baseUrl = priceServerUrl.replace(/\/$/, "");
13285
+ const url = `${baseUrl}/price/getOne?transaction_type=0&subjectAssetId=${encodeURIComponent(asset_id)}&paymentAssetId=${BTC_ASSET_ID}`;
13286
+ const res = await fetch(url);
13287
+ if (!res.ok) {
13288
+ throw new Error(`Price oracle error: ${res.status} ${res.statusText}`);
13289
+ }
13290
+ const body = await res.json();
13291
+ if (!body.ok || !body.ok.assetRates) {
13292
+ throw new Error(`Price oracle returned error: ${JSON.stringify(body)}`);
13293
+ }
13294
+ const { assetRates } = body.ok;
13295
+ logger2.info(`Exchange rate fetched for asset ${asset_id}: coefficient=${assetRates.subjectAssetRate?.coefficient}, scale=${assetRates.subjectAssetRate?.scale}`);
13296
+ return { assetRates };
13297
+ }
13298
+ __name(getExchangeRate, "getExchangeRate");
13299
+ async function listExchangeOrders({
13300
+ status,
13301
+ page = 1,
13302
+ page_size = 20
13303
+ } = {}) {
13304
+ const validStatuses = Object.values(EXCHANGE_ORDER_STATUS);
13305
+ if (status && !validStatuses.includes(status)) {
13306
+ throw new Error(`Invalid status: ${status}. Valid values: ${validStatuses.join(", ")}`);
13307
+ }
13308
+ const safePage = Math.max(1, parseInt(page) || 1);
13309
+ const safePageSize = Math.min(100, Math.max(1, parseInt(page_size) || 20));
13310
+ const { list, total } = await getExchangeOrders({
13311
+ status,
13312
+ page: safePage,
13313
+ page_size: safePageSize
13314
+ });
13315
+ return {
13316
+ orders: list.map((order) => ({
13317
+ id: order.id,
13318
+ payment_hash: order.payment_hash,
13319
+ btc_amount: order.btc_amount,
13320
+ asset_id: order.asset_id,
13321
+ asset_amount: order.asset_amount,
13322
+ status: order.status,
13323
+ error_message: order.error_message,
13324
+ created_at: order.created_at,
13325
+ updated_at: order.updated_at
13326
+ })),
13327
+ total
13328
+ };
13329
+ }
13330
+ __name(listExchangeOrders, "listExchangeOrders");
13331
+ async function getExchangeOrder({ order_id }) {
13332
+ checkObjectArgs({ order_id }, ["order_id"]);
13333
+ const order = await getExchangeOrderById(Number(order_id));
13334
+ if (!order) {
13335
+ throw new Error(`Exchange order not found: ${order_id}`);
13336
+ }
13337
+ return order;
13338
+ }
13339
+ __name(getExchangeOrder, "getExchangeOrder");
13340
+ module2.exports = {
13341
+ EXCHANGE_ORDER_STATUS,
13342
+ createHodlInvoice,
13343
+ getExchangeRate,
13344
+ listExchangeOrders,
13345
+ getExchangeOrder
13346
+ };
13347
+ }
13348
+ });
13349
+
12953
13350
  // business/service/proxy/rgbProxy.js
12954
13351
  var require_rgbProxy = __commonJS({
12955
13352
  "business/service/proxy/rgbProxy.js"(exports2, module2) {
@@ -12984,6 +13381,12 @@ var require_rgbProxy = __commonJS({
12984
13381
  restoreNode,
12985
13382
  getRGBAssetsList
12986
13383
  } = require_rgb();
13384
+ var {
13385
+ createHodlInvoice,
13386
+ getExchangeRate,
13387
+ listExchangeOrders,
13388
+ getExchangeOrder
13389
+ } = require_exchange();
12987
13390
  function rgbProxy() {
12988
13391
  return {
12989
13392
  // ---node manager
@@ -13173,6 +13576,35 @@ var require_rgbProxy = __commonJS({
13173
13576
  readonly: true
13174
13577
  };
13175
13578
  }, "decode_ln_invoice"),
13579
+ // ---exchange
13580
+ create_hodl_invoice: /* @__PURE__ */ __name(async () => {
13581
+ return {
13582
+ method: createHodlInvoice,
13583
+ argNames: ["btc_invoice", "asset_id", "asset_amount"],
13584
+ readonly: false
13585
+ };
13586
+ }, "create_hodl_invoice"),
13587
+ get_exchange_rate: /* @__PURE__ */ __name(async () => {
13588
+ return {
13589
+ method: getExchangeRate,
13590
+ argNames: ["asset_id"],
13591
+ readonly: true
13592
+ };
13593
+ }, "get_exchange_rate"),
13594
+ list_exchange_orders: /* @__PURE__ */ __name(async () => {
13595
+ return {
13596
+ method: listExchangeOrders,
13597
+ argNames: ["status", "page", "page_size"],
13598
+ readonly: true
13599
+ };
13600
+ }, "list_exchange_orders"),
13601
+ get_exchange_order: /* @__PURE__ */ __name(async () => {
13602
+ return {
13603
+ method: getExchangeOrder,
13604
+ argNames: ["order_id"],
13605
+ readonly: true
13606
+ };
13607
+ }, "get_exchange_order"),
13176
13608
  backup_node: /* @__PURE__ */ __name(async () => {
13177
13609
  return {
13178
13610
  method: backupNode,
@@ -14852,8 +15284,8 @@ var require_mempool = __commonJS({
14852
15284
  const { LINK_NETWORK } = getConfig2();
14853
15285
  if ((LINK_NETWORK || "").toLowerCase() === "regtest") {
14854
15286
  return {
14855
- apiBase: "http://34.84.66.29:8889/api",
14856
- pageBase: "http://34.84.66.29:8889/zh"
15287
+ apiBase: "http://34.84.69.164:8889/api",
15288
+ pageBase: "http://34.84.69.164:8889/zh"
14857
15289
  };
14858
15290
  }
14859
15291
  return {
@@ -15748,6 +16180,321 @@ var require_pollBtcTransfers2 = __commonJS({
15748
16180
  }
15749
16181
  });
15750
16182
 
16183
+ // business/job/rgb/pollExchangeOrders.js
16184
+ var require_pollExchangeOrders = __commonJS({
16185
+ "business/job/rgb/pollExchangeOrders.js"(exports2, module2) {
16186
+ var {
16187
+ getExchangeOrdersByStatusIn,
16188
+ getExchangeOrderById,
16189
+ updateExchangeOrderWithExpectedStatus
16190
+ } = require_dbService();
16191
+ var getRgbClient = require_client4();
16192
+ var { EXCHANGE_ORDER_STATUS } = require_exchange();
16193
+ var { listPayments, listChannels } = require_lightning();
16194
+ var Logger = require_linkLogger();
16195
+ var POLL_STALE_MS = 6e4;
16196
+ var pollingStartedAt = 0;
16197
+ async function pollExchangeOrders() {
16198
+ const now = Date.now();
16199
+ if (pollingStartedAt > 0) {
16200
+ if (now - pollingStartedAt < POLL_STALE_MS) return;
16201
+ const logger2 = new Logger("poll-exchange-orders");
16202
+ logger2.warn(`Previous poll stuck for ${now - pollingStartedAt}ms, forcing re-entry`);
16203
+ }
16204
+ pollingStartedAt = now;
16205
+ try {
16206
+ const activeOrders = await getExchangeOrdersByStatusIn([
16207
+ EXCHANGE_ORDER_STATUS.RGB_HODL_ISSUED,
16208
+ EXCHANGE_ORDER_STATUS.RGB_PAID,
16209
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16210
+ EXCHANGE_ORDER_STATUS.BTC_SENT
16211
+ ]);
16212
+ if (!activeOrders || activeOrders.length === 0) {
16213
+ return;
16214
+ }
16215
+ const logger2 = new Logger("poll-exchange-orders");
16216
+ logger2.info(`Polling ${activeOrders.length} active exchange orders`);
16217
+ let payments = [];
16218
+ try {
16219
+ const paymentsResult = await listPayments();
16220
+ payments = paymentsResult && paymentsResult.payments ? paymentsResult.payments : [];
16221
+ } catch (error) {
16222
+ logger2.warn(`Failed to fetch listPayments, detection skipped: ${error.message}`);
16223
+ }
16224
+ const rgbClient = getRgbClient();
16225
+ for (const order of activeOrders) {
16226
+ try {
16227
+ await processOrder(order, payments, rgbClient, logger2);
16228
+ } catch (error) {
16229
+ logger2.error(`Error processing exchange order ${order.id}: ${error.message}`);
16230
+ }
16231
+ }
16232
+ } finally {
16233
+ pollingStartedAt = 0;
16234
+ }
16235
+ }
16236
+ __name(pollExchangeOrders, "pollExchangeOrders");
16237
+ var MAX_BTC_RETRIES = 10;
16238
+ async function processOrder(order, payments, rgbClient, logger2) {
16239
+ const now = Math.floor(Date.now() / 1e3);
16240
+ switch (order.status) {
16241
+ case EXCHANGE_ORDER_STATUS.RGB_HODL_ISSUED: {
16242
+ if (order.htlc_expiry_at && now > order.htlc_expiry_at) {
16243
+ const updated = await updateExchangeOrderWithExpectedStatus(
16244
+ order.id,
16245
+ EXCHANGE_ORDER_STATUS.RGB_HODL_ISSUED,
16246
+ { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: "HODL invoice expired, A never paid" }
16247
+ );
16248
+ if (updated) logger2.info(`Order ${order.id}: expired in rgb_hodl_issued, marked failed`);
16249
+ break;
16250
+ }
16251
+ const hodlPending = payments.find(
16252
+ (p) => p.payment_hash === order.payment_hash && p.inbound === true && p.status === "Pending" && p.asset_id
16253
+ );
16254
+ if (hodlPending) {
16255
+ const updated = await updateExchangeOrderWithExpectedStatus(
16256
+ order.id,
16257
+ EXCHANGE_ORDER_STATUS.RGB_HODL_ISSUED,
16258
+ { status: EXCHANGE_ORDER_STATUS.RGB_PAID, error_message: null }
16259
+ );
16260
+ if (updated) {
16261
+ logger2.info(`Order ${order.id}: RGB HODL payment received (Pending), updated to rgb_paid`);
16262
+ const freshOrder = await getExchangeOrderById(order.id);
16263
+ if (freshOrder) await sendBtcPayment(freshOrder, payments, rgbClient, logger2);
16264
+ } else {
16265
+ logger2.warn(`Order ${order.id}: optimistic lock failed on rgb_hodl_issued -> rgb_paid`);
16266
+ }
16267
+ }
16268
+ break;
16269
+ }
16270
+ case EXCHANGE_ORDER_STATUS.RGB_PAID:
16271
+ case EXCHANGE_ORDER_STATUS.BTC_SENDING: {
16272
+ if (order.status === EXCHANGE_ORDER_STATUS.BTC_SENDING) {
16273
+ const alreadySent = payments.find(
16274
+ (p) => p.payment_hash === order.payment_hash && p.inbound === false && p.status === "Succeeded"
16275
+ );
16276
+ if (alreadySent) {
16277
+ const preimage = alreadySent.preimage || null;
16278
+ await updateExchangeOrderWithExpectedStatus(
16279
+ order.id,
16280
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16281
+ {
16282
+ status: EXCHANGE_ORDER_STATUS.BTC_SENT,
16283
+ preimage,
16284
+ btc_retry_count: 0,
16285
+ error_message: null
16286
+ }
16287
+ );
16288
+ logger2.info(`Order ${order.id}: BTC payment already succeeded (crash recovery), advanced to btc_sent`);
16289
+ break;
16290
+ }
16291
+ }
16292
+ await sendBtcPayment(order, payments, rgbClient, logger2);
16293
+ break;
16294
+ }
16295
+ case EXCHANGE_ORDER_STATUS.BTC_SENT: {
16296
+ const btcSucceeded = payments.find(
16297
+ (p) => p.payment_hash === order.payment_hash && p.inbound === false && p.status === "Succeeded"
16298
+ );
16299
+ if (!btcSucceeded) {
16300
+ const btcFailed = payments.find(
16301
+ (p) => p.payment_hash === order.payment_hash && p.inbound === false && p.status === "Failed"
16302
+ );
16303
+ if (btcFailed) {
16304
+ const updated = await updateExchangeOrderWithExpectedStatus(
16305
+ order.id,
16306
+ EXCHANGE_ORDER_STATUS.BTC_SENT,
16307
+ {
16308
+ status: EXCHANGE_ORDER_STATUS.RGB_PAID,
16309
+ btc_retry_count: (order.btc_retry_count || 0) + 1,
16310
+ error_message: "BTC payment failed asynchronously, will retry"
16311
+ }
16312
+ );
16313
+ if (updated) logger2.warn(`Order ${order.id}: BTC payment failed async (retry ${(order.btc_retry_count || 0) + 1}/${MAX_BTC_RETRIES}), reverting to rgb_paid`);
16314
+ break;
16315
+ }
16316
+ }
16317
+ const hodlSucceeded = payments.find(
16318
+ (p) => p.payment_hash === order.payment_hash && p.inbound === true && p.status === "Succeeded" && p.asset_id
16319
+ );
16320
+ if (hodlSucceeded) {
16321
+ const updated = await updateExchangeOrderWithExpectedStatus(
16322
+ order.id,
16323
+ EXCHANGE_ORDER_STATUS.BTC_SENT,
16324
+ {
16325
+ status: EXCHANGE_ORDER_STATUS.COMPLETED,
16326
+ btc_retry_count: 0,
16327
+ error_message: null
16328
+ }
16329
+ );
16330
+ if (updated) logger2.info(`Order ${order.id}: RGB auto-claimed (Succeeded), marked completed`);
16331
+ } else {
16332
+ if (order.htlc_expiry_at && now > order.htlc_expiry_at) {
16333
+ const updated = await updateExchangeOrderWithExpectedStatus(
16334
+ order.id,
16335
+ EXCHANGE_ORDER_STATUS.BTC_SENT,
16336
+ { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: "HTLC expired: BTC sent but RGB auto-claim never fired (node event missed)" }
16337
+ );
16338
+ if (updated) logger2.error(`[LOSS] Order ${order.id}: HTLC expired in btc_sent, RGB not claimed. payment_hash=${order.payment_hash}`);
16339
+ } else if (order.htlc_expiry_at && now > order.htlc_expiry_at - 600) {
16340
+ logger2.error(`[ALERT] Order ${order.id} approaching HTLC expiry. payment_hash=${order.payment_hash}, preimage=${order.preimage}, htlc_expiry_at=${order.htlc_expiry_at}`);
16341
+ }
16342
+ }
16343
+ break;
16344
+ }
16345
+ }
16346
+ }
16347
+ __name(processOrder, "processOrder");
16348
+ async function sendBtcPayment(order, payments, rgbClient, logger2) {
16349
+ const now = Math.floor(Date.now() / 1e3);
16350
+ if (order.htlc_expiry_at && now > order.htlc_expiry_at - 300) {
16351
+ await updateExchangeOrderWithExpectedStatus(
16352
+ order.id,
16353
+ order.status,
16354
+ { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: "HTLC expiry too close, aborting BTC payment" }
16355
+ );
16356
+ logger2.error(`Order ${order.id}: HTLC expiry imminent (${order.htlc_expiry_at - now}s left), marking failed`);
16357
+ return;
16358
+ }
16359
+ const retries = order.btc_retry_count || 0;
16360
+ if (retries >= MAX_BTC_RETRIES) {
16361
+ await updateExchangeOrderWithExpectedStatus(
16362
+ order.id,
16363
+ order.status,
16364
+ { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: `BTC payment failed after ${MAX_BTC_RETRIES} retries` }
16365
+ );
16366
+ logger2.error(`Order ${order.id}: exceeded max BTC retries (${MAX_BTC_RETRIES})`);
16367
+ return;
16368
+ }
16369
+ if (!order.btc_invoice) {
16370
+ await updateExchangeOrderWithExpectedStatus(order.id, order.status, { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: "Missing btc_invoice" });
16371
+ logger2.error(`Order ${order.id}: btc_invoice is null/empty, marking failed`);
16372
+ return;
16373
+ }
16374
+ try {
16375
+ const { channels = [] } = await listChannels();
16376
+ const btcOutbound = channels.filter((ch) => !ch.asset_id && (ch.is_usable !== false && ch.ready !== false)).reduce((sum, ch) => sum + Number(ch.outbound_balance_msat || ch.local_balance_msat || 0), 0);
16377
+ const requiredMsat = Number(order.btc_amount);
16378
+ if (!Number.isFinite(requiredMsat) || requiredMsat <= 0) {
16379
+ await updateExchangeOrderWithExpectedStatus(order.id, order.status, { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: `Invalid btc_amount: ${order.btc_amount}` });
16380
+ logger2.error(`Order ${order.id}: invalid btc_amount "${order.btc_amount}", marking failed`);
16381
+ return;
16382
+ }
16383
+ if (btcOutbound < requiredMsat) {
16384
+ logger2.warn(`Order ${order.id}: insufficient BTC outbound (available: ${btcOutbound}, required: ${requiredMsat}), skipping this tick`);
16385
+ return;
16386
+ }
16387
+ } catch (error) {
16388
+ logger2.warn(`Order ${order.id}: failed to check BTC liquidity, proceeding anyway: ${error.message}`);
16389
+ }
16390
+ try {
16391
+ const lockAcquired = await updateExchangeOrderWithExpectedStatus(
16392
+ order.id,
16393
+ order.status,
16394
+ { status: EXCHANGE_ORDER_STATUS.BTC_SENDING }
16395
+ );
16396
+ if (!lockAcquired) {
16397
+ logger2.warn(`Order ${order.id}: optimistic lock failed on ${order.status} -> btc_sending, skipping`);
16398
+ return;
16399
+ }
16400
+ const result = await rgbClient.lightning.payInvoice({ invoice: order.btc_invoice });
16401
+ const resultError = result && (result.error || result.failure_reason);
16402
+ if (resultError) {
16403
+ const errMsg = typeof resultError === "string" ? resultError : JSON.stringify(resultError);
16404
+ const isPermanent = /route.*not.*found|no.?route|expired/i.test(errMsg);
16405
+ if (isPermanent) {
16406
+ await updateExchangeOrderWithExpectedStatus(
16407
+ order.id,
16408
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16409
+ { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: errMsg }
16410
+ );
16411
+ logger2.error(`Order ${order.id}: BTC payment permanently failed (from response): ${errMsg}`);
16412
+ } else {
16413
+ await updateExchangeOrderWithExpectedStatus(
16414
+ order.id,
16415
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16416
+ {
16417
+ status: EXCHANGE_ORDER_STATUS.RGB_PAID,
16418
+ btc_retry_count: retries + 1,
16419
+ error_message: errMsg
16420
+ }
16421
+ );
16422
+ logger2.warn(`Order ${order.id}: BTC payment failed (from response, retry ${retries + 1}/${MAX_BTC_RETRIES}): ${errMsg}`);
16423
+ }
16424
+ return;
16425
+ }
16426
+ const preimage = result && result.preimage ? result.preimage : null;
16427
+ await updateExchangeOrderWithExpectedStatus(
16428
+ order.id,
16429
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16430
+ {
16431
+ status: EXCHANGE_ORDER_STATUS.BTC_SENT,
16432
+ preimage,
16433
+ // Keep btc_retry_count — don't reset until completed.
16434
+ error_message: null
16435
+ }
16436
+ );
16437
+ if (!preimage) {
16438
+ logger2.warn(`Order ${order.id}: BTC payInvoice returned without preimage (retry ${retries}/${MAX_BTC_RETRIES}), payment may fail async`);
16439
+ } else {
16440
+ logger2.info(`Order ${order.id}: BTC payment sent, preimage=${preimage}`);
16441
+ }
16442
+ } catch (error) {
16443
+ const isTimeout = /timeout|ETIMEDOUT|ECONNRESET|socket hang up/i.test(error.message);
16444
+ if (isTimeout) {
16445
+ await updateExchangeOrderWithExpectedStatus(
16446
+ order.id,
16447
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16448
+ { error_message: `Ambiguous: ${error.message} \u2014 will re-check next tick` }
16449
+ );
16450
+ logger2.warn(`Order ${order.id}: BTC sendpayment ambiguous timeout, keeping btc_sending for re-check`);
16451
+ return;
16452
+ }
16453
+ const isAlreadyPaid = /already paid/i.test(error.message);
16454
+ if (isAlreadyPaid) {
16455
+ const matchedPayment = payments.find(
16456
+ (p) => p.payment_hash === order.payment_hash && p.inbound === false && p.status === "Succeeded"
16457
+ );
16458
+ await updateExchangeOrderWithExpectedStatus(
16459
+ order.id,
16460
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16461
+ {
16462
+ status: EXCHANGE_ORDER_STATUS.BTC_SENT,
16463
+ preimage: matchedPayment?.preimage || null,
16464
+ btc_retry_count: 0,
16465
+ error_message: null
16466
+ }
16467
+ );
16468
+ logger2.warn(`Order ${order.id}: BTC "already paid" \u2014 treating as success, advanced to btc_sent`);
16469
+ return;
16470
+ }
16471
+ const isPermanent = /expired|no route/i.test(error.message);
16472
+ if (isPermanent) {
16473
+ await updateExchangeOrderWithExpectedStatus(
16474
+ order.id,
16475
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16476
+ { status: EXCHANGE_ORDER_STATUS.FAILED, error_message: error.message }
16477
+ );
16478
+ logger2.error(`Order ${order.id}: BTC sendpayment permanently failed: ${error.message}`);
16479
+ } else {
16480
+ await updateExchangeOrderWithExpectedStatus(
16481
+ order.id,
16482
+ EXCHANGE_ORDER_STATUS.BTC_SENDING,
16483
+ {
16484
+ status: EXCHANGE_ORDER_STATUS.RGB_PAID,
16485
+ btc_retry_count: retries + 1,
16486
+ error_message: error.message
16487
+ }
16488
+ );
16489
+ logger2.warn(`Order ${order.id}: BTC sendpayment transient error (retry ${retries + 1}/${MAX_BTC_RETRIES}), will retry: ${error.message}`);
16490
+ }
16491
+ }
16492
+ }
16493
+ __name(sendBtcPayment, "sendBtcPayment");
16494
+ module2.exports = pollExchangeOrders;
16495
+ }
16496
+ });
16497
+
15751
16498
  // business/job/rgb/pollLightningTransfers.js
15752
16499
  var require_pollLightningTransfers = __commonJS({
15753
16500
  "business/job/rgb/pollLightningTransfers.js"(exports2, module2) {
@@ -16642,6 +17389,7 @@ var require_tasks = __commonJS({
16642
17389
  var pollInvoiceTransfers = require_pollInvoiceTransfers();
16643
17390
  var rgbConnectPeer = require_connectPeer2();
16644
17391
  var pollBtcTransfers = require_pollBtcTransfers2();
17392
+ var pollExchangeOrders = require_pollExchangeOrders();
16645
17393
  var pollLightningTransfers = require_pollLightningTransfers();
16646
17394
  var pollRgbTransfers = require_pollRgbTransfers();
16647
17395
  var refreshTransfers = require_refreshTransfers();
@@ -16770,6 +17518,17 @@ var require_tasks = __commonJS({
16770
17518
  timeout: 3e4,
16771
17519
  description: "Poll RGB Lightning transfers"
16772
17520
  },
17521
+ rgb_poll_exchange_orders: {
17522
+ type: "cron",
17523
+ schedule: "*/5 * * * * *",
17524
+ implementation: pollExchangeOrders,
17525
+ gate: "rgbActive",
17526
+ priority: 1,
17527
+ enabled: true,
17528
+ maxRetries: 3,
17529
+ timeout: 3e4,
17530
+ description: "Poll RGB-BTC exchange orders and trigger BTC sendpayment on RGB receipt"
17531
+ },
16773
17532
  // Event-driven tasks
16774
17533
  bake_tapd_macaroon: {
16775
17534
  type: "event",
@@ -19246,7 +20005,8 @@ var require_setting_mainnet = __commonJS({
19246
20005
  officialRgbPeerHost: "CHANGE_ME_MAINNET_RGB_HOST:9736",
19247
20006
  officialUniverseServer: "CHANGE_ME_MAINNET_UNIVERSE_HOST:10009",
19248
20007
  priceOracle: "grpc-oracle.lnfi.network",
19249
- rgbProxy: "rpc://CHANGE_ME_MAINNET_RGB_PROXY_HOST:5000/json-rpc"
20008
+ rgbProxy: "rpc://CHANGE_ME_MAINNET_RGB_PROXY_HOST:5000/json-rpc",
20009
+ rgbPriceServer: "https://api-oracle.lnfi.network"
19250
20010
  };
19251
20011
  }
19252
20012
  });
@@ -19264,17 +20024,18 @@ var require_setting_regtest = __commonJS({
19264
20024
  bitcoindZmqRawTx: "tcp://regtest.lnfi.network:28335",
19265
20025
  network: "regtest",
19266
20026
  nostrRelays: [
19267
- "wss://relay.snort.social",
19268
- "wss://relay01.lnfi.network"
20027
+ "wss://relay01.lnfi.network",
20028
+ "wss://vault.iris.to"
19269
20029
  ],
19270
20030
  officialLndPeer: "03b24a4bf911ffd26ac1d5e5f2440a3c2f6974e4cc85d2ef54e17ee6d3717433d3",
19271
- officialLndPeerHost: "34.84.66.29:7739",
20031
+ officialLndPeerHost: "34.84.69.164:7739",
19272
20032
  officialNostrPubKey: "npub1me48869w43j30cfry9ayz9dsdl4gj54xppgk9krrv7g6hsq7psuqp3yusn",
19273
20033
  officialRgbPeer: "03b7153e278882e48e690acd0743305cbada86b131ab3388ccd782b45b02f064ef",
19274
20034
  officialRgbPeerHost: "regtest.lnfi.network:9736",
19275
20035
  officialUniverseServer: "regtest.lnfi.network:10009",
19276
20036
  priceOracle: "grpc-oracle.lnfi.network",
19277
- rgbProxy: "rpc://regtest.lnfi.network:5000/json-rpc"
20037
+ rgbProxy: "rpc://regtest.lnfi.network:5000/json-rpc",
20038
+ rgbPriceServer: "https://api-oracle.lnfi.network"
19278
20039
  };
19279
20040
  }
19280
20041
  });
@@ -19302,7 +20063,8 @@ var require_setting_testnet = __commonJS({
19302
20063
  officialRgbPeerHost: "CHANGE_ME_TESTNET_RGB_HOST:9736",
19303
20064
  officialUniverseServer: "CHANGE_ME_TESTNET_UNIVERSE_HOST:10009",
19304
20065
  priceOracle: "grpc-oracle.lnfi.network",
19305
- rgbProxy: "rpc://CHANGE_ME_TESTNET_RGB_PROXY_HOST:5000/json-rpc"
20066
+ rgbProxy: "rpc://CHANGE_ME_TESTNET_RGB_PROXY_HOST:5000/json-rpc",
20067
+ rgbPriceServer: "https://api-oracle.lnfi.network"
19306
20068
  };
19307
20069
  }
19308
20070
  });
@@ -19336,6 +20098,16 @@ var require_initLinkConfig = __commonJS({
19336
20098
  await updateMainLnlinkConfig({ node_name: LINK_NAME });
19337
20099
  await reloadConfig();
19338
20100
  }
20101
+ const networkTemplate = LINK_NETWORK === "testnet" ? testnetSettings : LINK_NETWORK === "mainnet" ? mainnetSettings : regtestSettings;
20102
+ const missingKeys = Object.keys(networkTemplate).filter(
20103
+ (k) => !(k in lndConfig.settings)
20104
+ );
20105
+ if (missingKeys.length > 0) {
20106
+ const patch = Object.fromEntries(missingKeys.map((k) => [k, networkTemplate[k]]));
20107
+ await updateMainLnlinkConfig({ settings: { ...lndConfig.settings, ...patch } });
20108
+ await reloadConfig();
20109
+ logger2.info(`Settings patched with new keys: ${missingKeys.join(", ")}`);
20110
+ }
19339
20111
  return;
19340
20112
  }
19341
20113
  let settings;