lnlink-server 1.1.6 → 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.6",
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",
@@ -7810,13 +7983,19 @@ var require_lndService = __commonJS({
7810
7983
  }
7811
7984
  if (state >= WALLET_STATE_CODE.RPC_ACTIVE && isMacaroonDecrypted && state !== WALLET_STATE_CODE.WAITING_TO_START) {
7812
7985
  const lightningService = getLightningService();
7813
- const ret = await Promise.allSettled([
7814
- getMainLnlinkConfig(),
7815
- lightningService.getInfo(),
7816
- lightningService.walletBalance(),
7817
- lightningService.listPeers({})
7818
- ]);
7819
- const [configResult, infoResult, balanceResult, peersResult] = ret;
7986
+ const promises = [
7987
+ getMainLnlinkConfig()
7988
+ ];
7989
+ if (state >= WALLET_STATE_CODE.SERVER_ACTIVE) {
7990
+ promises.push(getCacheNodeInfo(true));
7991
+ promises.push(lightningService.walletBalance());
7992
+ promises.push(lightningService.listPeers({}));
7993
+ }
7994
+ const ret = await Promise.allSettled(promises);
7995
+ const configResult = ret[0];
7996
+ const infoResult = state >= WALLET_STATE_CODE.SERVER_ACTIVE ? ret[1] : { status: "rejected" };
7997
+ const balanceResult = state >= WALLET_STATE_CODE.SERVER_ACTIVE ? ret[2] : { status: "rejected" };
7998
+ const peersResult = state >= WALLET_STATE_CODE.SERVER_ACTIVE ? ret[3] : { status: "rejected" };
7820
7999
  let settings = null;
7821
8000
  if (configResult.status === "fulfilled") {
7822
8001
  settings = configResult.value?.settings;
@@ -7835,9 +8014,11 @@ var require_lndService = __commonJS({
7835
8014
  address: peer.address
7836
8015
  };
7837
8016
  });
7838
- logger2.info(
7839
- `LND lndService combineNodeInfoAsync peer:${peers.map((item) => item.pub_key)}`
7840
- );
8017
+ if (state >= WALLET_STATE_CODE.SERVER_ACTIVE) {
8018
+ logger2.info(
8019
+ `LND lndService combineNodeInfoAsync peer:${peers.map((item) => item.pub_key)}`
8020
+ );
8021
+ }
7841
8022
  const errors = ret?.filter((p) => p.status === "rejected")?.map((p) => p.reason);
7842
8023
  if (errors && errors.length > 0) {
7843
8024
  logger2.error(
@@ -8579,7 +8760,11 @@ var require_api = __commonJS({
8579
8760
  socket.setTimeout(TIMEOUT);
8580
8761
  socket.once("connect", () => {
8581
8762
  socket.destroy();
8582
- resolve({ host, port, reachable: true });
8763
+ resolve({
8764
+ host,
8765
+ port,
8766
+ reachable: true
8767
+ });
8583
8768
  });
8584
8769
  socket.once("timeout", () => {
8585
8770
  socket.destroy();
@@ -8596,10 +8781,26 @@ var require_api = __commonJS({
8596
8781
  __name(tryConnect2, "tryConnect");
8597
8782
  const net = require("node:net");
8598
8783
  const TOR_DIR_AUTHORITIES = [
8599
- { host: "128.31.0.34", port: 9131, name: "moria1" },
8600
- { host: "193.23.244.244", port: 443, name: "dannenberg" },
8601
- { host: "199.58.81.140", port: 80, name: "Faravahar" },
8602
- { 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
+ }
8603
8804
  ];
8604
8805
  const TIMEOUT = 5e3;
8605
8806
  let reachable = false;
@@ -9470,10 +9671,20 @@ var require_tapdService = __commonJS({
9470
9671
  const decodedInvoice = decode(payment_request);
9471
9672
  const routingInfo = decodedInvoice.tags.find((tag) => tag.tagName === "routing_info");
9472
9673
  if (routingInfo && routingInfo.data && routingInfo.data.length > 0) {
9473
- const lastHop = routingInfo.data[routingInfo.data.length - 1];
9474
- if (lastHop.pubkey) {
9475
- paymentRequest.last_hop_pubkey = Buffer2.from(lastHop.pubkey, "hex");
9476
- }
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;
9477
9688
  }
9478
9689
  const sendParams = {
9479
9690
  asset_amount,
@@ -9682,7 +9893,7 @@ var require_tapdService = __commonJS({
9682
9893
  if (!retIsConnectPeer) {
9683
9894
  throw new Error("Peer not connected");
9684
9895
  }
9685
- const invoice = await tapChannelService.addInvoice({
9896
+ const createInvoiceArgs = {
9686
9897
  asset_amount,
9687
9898
  asset_id: Buffer2.from(asset_id, "hex"),
9688
9899
  peer_pubkey: Buffer2.from(remotePubkey, "hex"),
@@ -9691,7 +9902,8 @@ var require_tapdService = __commonJS({
9691
9902
  expiry,
9692
9903
  description_hash: description_hash ? Buffer2.from(description_hash, "hex") : void 0
9693
9904
  }
9694
- });
9905
+ };
9906
+ const invoice = await tapChannelService.addInvoice(createInvoiceArgs);
9695
9907
  const payment_req = invoice?.invoice_result?.payment_request;
9696
9908
  if (payment_req) {
9697
9909
  await sleep(500);
@@ -9845,7 +10057,7 @@ var require_constants2 = __commonJS({
9845
10057
  SOCIAL_ID_MISMATCH: "Social id don't match",
9846
10058
  METHOD_NOT_SUPPORTED: "Method not supported"
9847
10059
  };
9848
- var PRIVILEGED_METHODS = ["make_invoice", "pay_invoice"];
10060
+ var PRIVILEGED_METHODS = ["make_invoice", "pay_invoice", "create_hodl_invoice"];
9849
10061
  var FLASH_ACCOUNT_METHODS = [
9850
10062
  "make_invoice",
9851
10063
  "pay_invoice",
@@ -11346,8 +11558,9 @@ var require_nwcProxy = __commonJS({
11346
11558
  }
11347
11559
  } else {
11348
11560
  const filterChannelList = channelList.filter((item) => {
11349
- const itemAsset = item?.custom_channel_data?.assets?.[0];
11350
- 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;
11351
11564
  });
11352
11565
  if (filterChannelList.length > 0) {
11353
11566
  return filterChannelList.sort((a, b) => {
@@ -11480,9 +11693,15 @@ var require_nwcProxy = __commonJS({
11480
11693
  }
11481
11694
  const create_at = Math.floor(Date.now() / 1e3);
11482
11695
  const expire_at = create_at + (expiry ?? 5 * 60);
11696
+ const chan_id = await getBestOutgoingChainId(
11697
+ asset_id
11698
+ ).catch(() => {
11699
+ return false;
11700
+ });
11483
11701
  const invoice = await addInvoice({
11484
11702
  asset_amount: amount,
11485
11703
  asset_id,
11704
+ chan_id,
11486
11705
  description,
11487
11706
  description_hash,
11488
11707
  expiry,
@@ -11619,7 +11838,9 @@ var require_nwcProxy = __commonJS({
11619
11838
  result: {
11620
11839
  preimage: payment?.payment_result?.payment_preimage,
11621
11840
  payment_hash: payment?.payment_result?.payment_hash,
11622
- 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
11623
11844
  }
11624
11845
  });
11625
11846
  }
@@ -11795,7 +12016,7 @@ var require_nwcProxy = __commonJS({
11795
12016
  user_npub: lnlinkUser.npub,
11796
12017
  node_type: NODE_TYPE.LITD,
11797
12018
  transaction_kind: TRANSACTION_KIND.LIGHTNING,
11798
- asset_id
12019
+ asset_id: lnlinkUser?.asset_id || asset_id
11799
12020
  });
11800
12021
  total = allTotal;
11801
12022
  return allList.map((item) => ({
@@ -12072,7 +12293,7 @@ var require_info = __commonJS({
12072
12293
  ] = await Promise.allSettled([
12073
12294
  rgbClient.node.getNodeInfo(),
12074
12295
  rgbClient.onchain.getBtcBalance({
12075
- skip_sync: true
12296
+ skip_sync: false
12076
12297
  // Skip sync for faster response
12077
12298
  }),
12078
12299
  rgbClient.lightning.listPeers({})
@@ -12214,6 +12435,7 @@ var require_lightning = __commonJS({
12214
12435
  push_msat,
12215
12436
  asset_amount,
12216
12437
  asset_id,
12438
+ push_asset_amount = 0,
12217
12439
  isPublic = true,
12218
12440
  with_anchors = true,
12219
12441
  fee_base_msat = 1e3,
@@ -12226,12 +12448,11 @@ var require_lightning = __commonJS({
12226
12448
  host,
12227
12449
  capacity_sat,
12228
12450
  push_msat,
12229
- asset_amount,
12230
- asset_id,
12231
12451
  public: isPublic,
12232
12452
  with_anchors,
12233
12453
  fee_base_msat: Number(fee_base_msat),
12234
- fee_proportional_millionths
12454
+ fee_proportional_millionths,
12455
+ ...asset_id ? { asset_id, asset_amount, push_asset_amount } : {}
12235
12456
  };
12236
12457
  const { is_connected, peer } = await isConnectPeer({
12237
12458
  pubkey
@@ -12942,6 +13163,190 @@ var require_rgb = __commonJS({
12942
13163
  }
12943
13164
  });
12944
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
+
12945
13350
  // business/service/proxy/rgbProxy.js
12946
13351
  var require_rgbProxy = __commonJS({
12947
13352
  "business/service/proxy/rgbProxy.js"(exports2, module2) {
@@ -12976,6 +13381,12 @@ var require_rgbProxy = __commonJS({
12976
13381
  restoreNode,
12977
13382
  getRGBAssetsList
12978
13383
  } = require_rgb();
13384
+ var {
13385
+ createHodlInvoice,
13386
+ getExchangeRate,
13387
+ listExchangeOrders,
13388
+ getExchangeOrder
13389
+ } = require_exchange();
12979
13390
  function rgbProxy() {
12980
13391
  return {
12981
13392
  // ---node manager
@@ -13165,6 +13576,35 @@ var require_rgbProxy = __commonJS({
13165
13576
  readonly: true
13166
13577
  };
13167
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"),
13168
13608
  backup_node: /* @__PURE__ */ __name(async () => {
13169
13609
  return {
13170
13610
  method: backupNode,
@@ -14844,8 +15284,8 @@ var require_mempool = __commonJS({
14844
15284
  const { LINK_NETWORK } = getConfig2();
14845
15285
  if ((LINK_NETWORK || "").toLowerCase() === "regtest") {
14846
15286
  return {
14847
- apiBase: "http://34.84.66.29:8889/api",
14848
- 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"
14849
15289
  };
14850
15290
  }
14851
15291
  return {
@@ -15740,6 +16180,321 @@ var require_pollBtcTransfers2 = __commonJS({
15740
16180
  }
15741
16181
  });
15742
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
+
15743
16498
  // business/job/rgb/pollLightningTransfers.js
15744
16499
  var require_pollLightningTransfers = __commonJS({
15745
16500
  "business/job/rgb/pollLightningTransfers.js"(exports2, module2) {
@@ -16634,6 +17389,7 @@ var require_tasks = __commonJS({
16634
17389
  var pollInvoiceTransfers = require_pollInvoiceTransfers();
16635
17390
  var rgbConnectPeer = require_connectPeer2();
16636
17391
  var pollBtcTransfers = require_pollBtcTransfers2();
17392
+ var pollExchangeOrders = require_pollExchangeOrders();
16637
17393
  var pollLightningTransfers = require_pollLightningTransfers();
16638
17394
  var pollRgbTransfers = require_pollRgbTransfers();
16639
17395
  var refreshTransfers = require_refreshTransfers();
@@ -16762,6 +17518,17 @@ var require_tasks = __commonJS({
16762
17518
  timeout: 3e4,
16763
17519
  description: "Poll RGB Lightning transfers"
16764
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
+ },
16765
17532
  // Event-driven tasks
16766
17533
  bake_tapd_macaroon: {
16767
17534
  type: "event",
@@ -19238,7 +20005,8 @@ var require_setting_mainnet = __commonJS({
19238
20005
  officialRgbPeerHost: "CHANGE_ME_MAINNET_RGB_HOST:9736",
19239
20006
  officialUniverseServer: "CHANGE_ME_MAINNET_UNIVERSE_HOST:10009",
19240
20007
  priceOracle: "grpc-oracle.lnfi.network",
19241
- 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"
19242
20010
  };
19243
20011
  }
19244
20012
  });
@@ -19256,17 +20024,18 @@ var require_setting_regtest = __commonJS({
19256
20024
  bitcoindZmqRawTx: "tcp://regtest.lnfi.network:28335",
19257
20025
  network: "regtest",
19258
20026
  nostrRelays: [
19259
- "wss://relay.snort.social",
19260
- "wss://relay01.lnfi.network"
20027
+ "wss://relay01.lnfi.network",
20028
+ "wss://vault.iris.to"
19261
20029
  ],
19262
20030
  officialLndPeer: "03b24a4bf911ffd26ac1d5e5f2440a3c2f6974e4cc85d2ef54e17ee6d3717433d3",
19263
- officialLndPeerHost: "34.84.66.29:7739",
20031
+ officialLndPeerHost: "34.84.69.164:7739",
19264
20032
  officialNostrPubKey: "npub1me48869w43j30cfry9ayz9dsdl4gj54xppgk9krrv7g6hsq7psuqp3yusn",
19265
20033
  officialRgbPeer: "03b7153e278882e48e690acd0743305cbada86b131ab3388ccd782b45b02f064ef",
19266
20034
  officialRgbPeerHost: "regtest.lnfi.network:9736",
19267
20035
  officialUniverseServer: "regtest.lnfi.network:10009",
19268
20036
  priceOracle: "grpc-oracle.lnfi.network",
19269
- rgbProxy: "rpc://regtest.lnfi.network:5000/json-rpc"
20037
+ rgbProxy: "rpc://regtest.lnfi.network:5000/json-rpc",
20038
+ rgbPriceServer: "https://api-oracle.lnfi.network"
19270
20039
  };
19271
20040
  }
19272
20041
  });
@@ -19294,7 +20063,8 @@ var require_setting_testnet = __commonJS({
19294
20063
  officialRgbPeerHost: "CHANGE_ME_TESTNET_RGB_HOST:9736",
19295
20064
  officialUniverseServer: "CHANGE_ME_TESTNET_UNIVERSE_HOST:10009",
19296
20065
  priceOracle: "grpc-oracle.lnfi.network",
19297
- 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"
19298
20068
  };
19299
20069
  }
19300
20070
  });
@@ -19328,6 +20098,16 @@ var require_initLinkConfig = __commonJS({
19328
20098
  await updateMainLnlinkConfig({ node_name: LINK_NAME });
19329
20099
  await reloadConfig();
19330
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
+ }
19331
20111
  return;
19332
20112
  }
19333
20113
  let settings;