@fibscope/agent 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/index.mjs +262 -55
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -74,7 +74,7 @@ The agent pushes to `POST /agent/v1/report` with:
74
74
 
75
75
  ```json
76
76
  {
77
- "agentVersion": "0.1.4",
77
+ "agentVersion": "0.1.6",
78
78
  "chain": "testnet",
79
79
  "collectedAt": 1710000000000,
80
80
  "publicIp": "54.179.226.154",
@@ -100,6 +100,12 @@ For an `API_NODE_PAYMENT`, the agent confirms the sender deposit and checks for
100
100
 
101
101
  The command result includes provider and receiver balance views before payment, after payment, and at close. The receiver view reverses the provider-local and provider-remote balances so the receiving channel's credited local balance and remaining inbound capacity are explicit.
102
102
 
103
+ ## Owner N2N Test Lifecycle
104
+
105
+ `N2N_PAYMENT` is an owner-authorized test command. It never confirms or charges an API test-wallet deposit. The selected source node checks its own funding wallet and existing routes, connects the receiving peer, opens a private one-way direct channel when needed, sends over Fiber, and optionally closes only the channel created by that test. FNN v0.8.1 uses dual-funded CKB channel construction, so the receiving node wallet must still provide its configured reserved capacity (commonly about 99 CKB) when a new channel is opened.
106
+
107
+ When both FNN instances publish the same public IPv4 address, the agent dials the receiving P2P port through loopback so same-VPS nodes can complete the Fiber handshake. Command results include structured source selection, peer connection, channel creation, balance movement, Fiber transfer, optional close, and partial failure steps.
108
+
103
109
  ## Resilience
104
110
 
105
111
  Collection and transmission are independent loops:
package/index.mjs CHANGED
@@ -33,7 +33,7 @@ import { virtualizePayment } from "./lib/route/route.mjs";
33
33
  import { chooseFnnInstance, describeFnnInstance, discoverFnnInstances, rpcResponds } from "./lib/fnn-discovery.mjs";
34
34
  import { buildObservedPeerAddress, discoverPublicIpv4, publishablePeerAddresses } from "./lib/public-peer-address.mjs";
35
35
 
36
- const AGENT_VERSION = "0.1.4";
36
+ const AGENT_VERSION = "0.1.6";
37
37
  const DEFAULT_CONFIG_PATH = ".fiber/agent.json";
38
38
  const DEFAULT_STATUS_PATH = ".fiber/agent-status.json";
39
39
  const DEFAULT_PUSH_INTERVAL_MS = 15_000;
@@ -1145,6 +1145,7 @@ async function executeAndReportCommand(runtime, command) {
1145
1145
  command: command.command,
1146
1146
  status: "failed",
1147
1147
  error: message(error),
1148
+ result: error?.commandResult && typeof error.commandResult === "object" ? error.commandResult : undefined,
1148
1149
  };
1149
1150
  await postCommandResult(config, body);
1150
1151
  console.warn(formatCommandEvent("failed", command, config, "error", body.error));
@@ -1180,6 +1181,9 @@ async function executeAgentCommand(runtime, command) {
1180
1181
  if (name === "API_NODE_PAYMENT" || name === "NODE_PAYMENT") {
1181
1182
  return acceptApiNodePayment(runtime, params);
1182
1183
  }
1184
+ if (name === "N2N_PAYMENT" || name === "NODE_TO_NODE_PAYMENT") {
1185
+ return acceptApiNodePayment(runtime, params, { ownerFunded: true });
1186
+ }
1183
1187
  if (name === "API_WALLET_PAYMENT" || name === "WALLET_PAYMENT") {
1184
1188
  return acceptApiWalletPayment(runtime, params);
1185
1189
  }
@@ -1545,15 +1549,23 @@ async function acceptApiWalletPayment(runtime, params) {
1545
1549
  };
1546
1550
  }
1547
1551
 
1548
- async function acceptApiNodePayment(runtime, params) {
1552
+ async function acceptApiNodePayment(runtime, params, options = {}) {
1549
1553
  const config = runtime.config ?? runtime;
1554
+ const ownerFunded = options.ownerFunded === true;
1550
1555
  const info = await rpcCall(config.fiberRpcUrl, "node_info", [], config.timeoutMs);
1551
1556
  const depositTxHash = textValue(params.depositTxHash);
1552
- const deposit = depositTxHash ? await waitForCkbTransaction(config, depositTxHash, params) : undefined;
1557
+ const deposit = !ownerFunded && depositTxHash ? await waitForCkbTransaction(config, depositTxHash, params) : undefined;
1553
1558
  const targetPubkey = normalizePeerPubkey(firstTextValue(params.receiverNode, params.targetPubkey, params.destinationNode, params.pubkey));
1554
- const peerAddress = firstTextValue(params.peerAddress, params.receiverPeerAddress, params.address);
1559
+ const publishedPeerAddress = firstTextValue(params.peerAddress, params.receiverPeerAddress, params.address);
1560
+ const peerAddress = resolveN2nPeerAddress(config, publishedPeerAddress, boolValue(params.sameHost, false));
1555
1561
  const provisionChannel = boolValue(params.provisionChannel, true);
1556
1562
  const closeChannelAfterPayment = provisionChannel && boolValue(params.closeChannelAfterPayment, false);
1563
+ const source = {
1564
+ nodeId: textValue(params.sourceNodeId) || undefined,
1565
+ pubkey: normalizePeerPubkey(readNodeId(info)),
1566
+ displayName: textValue(params.sourceNodeName) || undefined,
1567
+ fundingBefore: ownerFunded ? await collectFundingState(config).catch(() => undefined) : undefined,
1568
+ };
1557
1569
  let existingChannel;
1558
1570
  let routeProbe;
1559
1571
  try {
@@ -1587,11 +1599,14 @@ async function acceptApiNodePayment(runtime, params) {
1587
1599
  peerPubkey: targetPubkey,
1588
1600
  peerAddress,
1589
1601
  fundingAmount: firstTextValue(params.channelFundingAmount, params.totalDebit, params.amountShannons, params.amount),
1602
+ oneWay: ownerFunded ? true : params.oneWay,
1590
1603
  connectFirst: true,
1591
1604
  waitUntilReady: true,
1592
1605
  waitTimeoutMs: positiveNumber(params.channelReadyTimeoutMs, 10 * 60_000),
1593
1606
  pollMs: positiveNumber(params.channelPollMs, 5_000),
1594
- public: true,
1607
+ // FNN v0.8.1 rejects public one-way channels. N2N tests only need a
1608
+ // direct source-to-receiver path, so keep their temporary channel private.
1609
+ public: ownerFunded ? false : true,
1595
1610
  });
1596
1611
  paymentChannel = channelOpen.channel ?? await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs);
1597
1612
  }
@@ -1603,6 +1618,67 @@ async function acceptApiNodePayment(runtime, params) {
1603
1618
  keysend: true,
1604
1619
  });
1605
1620
  } catch (error) {
1621
+ const partialOpen = error?.commandResult && typeof error.commandResult === "object" ? error.commandResult : undefined;
1622
+ if (!channelOpen && partialOpen) channelOpen = partialOpen;
1623
+ if (!paymentChannel && partialOpen?.channel) paymentChannel = partialOpen.channel;
1624
+ if (ownerFunded) {
1625
+ let recovery;
1626
+ let recoveryError;
1627
+ try {
1628
+ const openedChannelId = channelIdOf(paymentChannel);
1629
+ if (channelOpen && openedChannelId) {
1630
+ const openedState = channelStateName(paymentChannel);
1631
+ if (isChannelReadyState(openedState)) {
1632
+ recovery = await settleFiberChannel(runtime, { channelId: openedChannelId, force: false, feeRate: "1000" });
1633
+ await waitForChannelClosed(config.fiberRpcUrl, openedChannelId, 10 * 60_000, 5_000);
1634
+ } else {
1635
+ const result = await rpcCall(config.fiberRpcUrl, "abandon_channel", [{ channel_id: openedChannelId }], config.timeoutMs);
1636
+ recovery = {
1637
+ ok: true,
1638
+ action: "ABANDON_CHANNEL",
1639
+ channelId: openedChannelId,
1640
+ previousState: openedState || undefined,
1641
+ result,
1642
+ };
1643
+ }
1644
+ }
1645
+ } catch (failure) {
1646
+ recoveryError = message(failure);
1647
+ }
1648
+ const fundingAfter = await collectFundingState(config).catch(() => undefined);
1649
+ const lifecycle = nodePaymentLifecycle({
1650
+ source,
1651
+ targetPubkey,
1652
+ publishedPeerAddress,
1653
+ peerAddress,
1654
+ provisionChannel,
1655
+ closeChannelAfterPayment,
1656
+ existingChannel,
1657
+ routeProbe,
1658
+ channelOpen: channelOpen ?? partialOpen,
1659
+ paymentChannel,
1660
+ channelBeforePayment,
1661
+ fiberPayment,
1662
+ fundingAfter,
1663
+ failure: message(error),
1664
+ recovery,
1665
+ recoveryError,
1666
+ });
1667
+ throw commandResultError(`N2N transfer failed while preparing the peer, channel, or Fiber payment: ${message(error)}. No API test wallet was charged.`, {
1668
+ ok: false,
1669
+ action: "N2N_PAYMENT",
1670
+ mode: "Owner-funded node-to-node test",
1671
+ paymentStatus: "failed",
1672
+ source,
1673
+ receiverNode: targetPubkey,
1674
+ amount: textValue(params.amountShannons ?? params.amount) || undefined,
1675
+ assetType: textValue(params.assetType ?? params.asset) || "CKB",
1676
+ fiberPayment,
1677
+ channelLifecycle: lifecycle,
1678
+ fundingBefore: source.fundingBefore,
1679
+ fundingAfter,
1680
+ });
1681
+ }
1606
1682
  let refund;
1607
1683
  let refundError;
1608
1684
  try {
@@ -1657,19 +1733,22 @@ async function acceptApiNodePayment(runtime, params) {
1657
1733
  }
1658
1734
  }
1659
1735
  let refreshed;
1736
+ const fundingAfter = ownerFunded ? await collectFundingState(config).catch(() => undefined) : undefined;
1660
1737
  if (typeof runtime.collect === "function" && typeof runtime.push === "function") {
1661
1738
  refreshed = await pushWithRegistrationRetry(runtime, await runtime.collect(), {
1662
- intent: "api-node-payment-completed",
1739
+ intent: ownerFunded ? "n2n-payment-completed" : "api-node-payment-completed",
1663
1740
  retryOnRateLimit: true,
1664
1741
  maxRateLimitRetries: 1,
1665
1742
  }).catch((error) => ({ ok: false, error: message(error) }));
1666
1743
  }
1667
1744
  return {
1668
1745
  ok: true,
1669
- action: "API_NODE_PAYMENT",
1670
- mode: "API-managed Fiber node payment",
1746
+ action: ownerFunded ? "N2N_PAYMENT" : "API_NODE_PAYMENT",
1747
+ mode: ownerFunded ? "Owner-funded node-to-node test" : "API-managed Fiber node payment",
1671
1748
  paymentStatus: "completed",
1672
- message: "Sender deposit is confirmed and the provider agent submitted the Fiber payment to the recipient node.",
1749
+ message: ownerFunded
1750
+ ? "The selected source node funded or reused the Fiber route and sent the N2N payment without charging an API test wallet."
1751
+ : "Sender deposit is confirmed and the provider agent submitted the Fiber payment to the recipient node.",
1673
1752
  paymentId: textValue(params.paymentId) || undefined,
1674
1753
  quoteId: textValue(params.quoteId) || undefined,
1675
1754
  candidateId: textValue(params.candidateId) || undefined,
@@ -1687,29 +1766,36 @@ async function acceptApiNodePayment(runtime, params) {
1687
1766
  depositAmount: textValue(params.depositAmount) || undefined,
1688
1767
  deposit,
1689
1768
  fiberPayment,
1690
- channelLifecycle: {
1691
- mode: channelOpen ? "provisioned_direct_channel" : existingChannel ? "existing_direct_channel" : "existing_routed_liquidity",
1692
- provisionRequested: provisionChannel,
1693
- closeRequested: closeChannelAfterPayment,
1694
- peer: { pubkey: targetPubkey, address: peerAddress || undefined },
1769
+ source: ownerFunded ? { ...source, fundingAfter } : undefined,
1770
+ fundingBefore: ownerFunded ? source.fundingBefore : undefined,
1771
+ fundingAfter,
1772
+ channelLifecycle: nodePaymentLifecycle({
1773
+ source,
1774
+ targetPubkey,
1775
+ publishedPeerAddress,
1776
+ peerAddress,
1777
+ provisionChannel,
1778
+ closeChannelAfterPayment,
1779
+ existingChannel,
1695
1780
  routeProbe,
1696
1781
  channelOpen,
1697
- channelId: paymentChannelId || undefined,
1698
- providerBeforePayment: channelBeforePayment?.provider,
1699
- receiverBeforePayment: channelBeforePayment?.receiver,
1700
- providerAfterPayment: channelAfterPayment?.provider,
1701
- receiverAfterPayment: channelAfterPayment?.receiver,
1782
+ paymentChannel,
1783
+ paymentChannelId,
1784
+ channelBeforePayment,
1785
+ channelAfterPayment,
1786
+ fiberPayment,
1702
1787
  channelClose,
1703
- providerAtClose: channelClosed?.provider,
1704
- receiverAtClose: channelClosed?.receiver,
1705
- finalChannelState: channelClosed?.state ?? channelAfterPayment?.state,
1706
- },
1788
+ channelClosed,
1789
+ fundingAfter,
1790
+ }),
1707
1791
  fundingRefresh: refreshed,
1708
1792
  routeExecution: {
1709
1793
  status: "fiber_send_payment",
1710
1794
  reason: channelOpen
1711
- ? "No viable route existed, so the provider connected the receiving peer, opened a direct channel, sent the payment, and applied the requested close policy."
1712
- : "The provider used existing outbound Fiber liquidity after confirming the payer deposit.",
1795
+ ? "No viable route existed, so the source connected the receiving peer, opened a direct channel, sent the payment, and applied the requested close policy."
1796
+ : ownerFunded
1797
+ ? "The selected source node used its existing outbound Fiber liquidity."
1798
+ : "The provider used existing outbound Fiber liquidity after confirming the payer deposit.",
1713
1799
  },
1714
1800
  node: {
1715
1801
  pubkey: normalizePeerPubkey(readNodeId(info)),
@@ -1719,6 +1805,86 @@ async function acceptApiNodePayment(runtime, params) {
1719
1805
  };
1720
1806
  }
1721
1807
 
1808
+ export function nodePaymentLifecycle(input) {
1809
+ const channelOpen = input.channelOpen && typeof input.channelOpen === "object" ? input.channelOpen : undefined;
1810
+ const connectResult = channelOpen?.connectResult;
1811
+ const observedChannel = input.paymentChannel ?? channelOpen?.channel;
1812
+ const channelId = input.paymentChannelId || channelIdOf(observedChannel);
1813
+ const observedChannelState = channelStateName(observedChannel);
1814
+ const existingRoute = Boolean(input.existingChannel) || input.routeProbe?.status === "success";
1815
+ const peerStatus = existingRoute ? "skipped" : connectResult ? "completed" : input.failure ? "failed" : "pending";
1816
+ const channelStatus = existingRoute ? "skipped" : isChannelReadyState(observedChannelState) ? "completed" : input.failure ? "failed" : "pending";
1817
+ const paymentStatus = input.fiberPayment ? "completed" : input.failure ? "failed" : "pending";
1818
+ const failedChannelAbandoned = input.recovery?.action === "ABANDON_CHANNEL" && input.recovery?.ok !== false;
1819
+ const closeStatus = failedChannelAbandoned
1820
+ ? "completed"
1821
+ : !input.closeChannelAfterPayment
1822
+ ? "skipped"
1823
+ : input.channelClosed ? "completed"
1824
+ : input.channelClose?.status === "failed" || input.failure ? "failed" : "pending";
1825
+ return omitUndefined({
1826
+ mode: channelOpen && !existingRoute ? "provisioned_direct_channel" : input.existingChannel ? "existing_direct_channel" : "existing_routed_liquidity",
1827
+ provisionRequested: input.provisionChannel,
1828
+ closeRequested: input.closeChannelAfterPayment,
1829
+ source: {
1830
+ nodeId: input.source?.nodeId,
1831
+ pubkey: input.source?.pubkey,
1832
+ displayName: input.source?.displayName,
1833
+ fundingBefore: input.source?.fundingBefore,
1834
+ fundingAfter: input.fundingAfter,
1835
+ },
1836
+ peer: {
1837
+ pubkey: input.targetPubkey,
1838
+ publishedAddress: input.publishedPeerAddress || undefined,
1839
+ dialAddress: input.peerAddress || undefined,
1840
+ sameHostLoopback: Boolean(input.publishedPeerAddress && input.peerAddress && input.publishedPeerAddress !== input.peerAddress),
1841
+ },
1842
+ routeProbe: input.routeProbe,
1843
+ peerConnect: connectResult,
1844
+ channelOpen,
1845
+ channelId: channelId || undefined,
1846
+ providerBeforePayment: input.channelBeforePayment?.provider,
1847
+ receiverBeforePayment: input.channelBeforePayment?.receiver,
1848
+ providerAfterPayment: input.channelAfterPayment?.provider,
1849
+ receiverAfterPayment: input.channelAfterPayment?.receiver,
1850
+ channelClose: input.channelClose,
1851
+ providerAtClose: input.channelClosed?.provider,
1852
+ receiverAtClose: input.channelClosed?.receiver,
1853
+ finalChannelState: input.channelClosed?.state ?? input.channelAfterPayment?.state,
1854
+ recovery: input.recovery,
1855
+ recoveryError: input.recoveryError,
1856
+ failure: input.failure,
1857
+ steps: [
1858
+ { key: "source_selected", label: "Source node selected", status: "completed", detail: input.source?.displayName || shortText(input.source?.pubkey, 10, 8) },
1859
+ { key: "peer_connect", label: "Receiving peer connected", status: peerStatus, detail: existingRoute ? "Existing route does not require a new direct peer connection." : input.failure && !connectResult ? input.failure : input.peerAddress },
1860
+ { key: "channel_open", label: "Channel ready", status: channelStatus, detail: existingRoute ? "Existing Fiber liquidity selected." : channelStatus === "completed" && channelId ? `Channel ${shortText(channelId, 10, 8)}` : input.failure },
1861
+ { key: "fiber_transfer", label: "Funds moved over Fiber", status: paymentStatus, detail: input.fiberPayment ? `Payment ${shortText(input.fiberPayment.paymentHash ?? input.fiberPayment.payment_hash ?? input.fiberPayment.targetPubkey, 10, 8)}` : input.failure },
1862
+ ...(failedChannelAbandoned
1863
+ ? [{ key: "channel_cleanup", label: "Failed channel abandoned", status: closeStatus, detail: `Released pending channel ${shortText(channelId, 10, 8)}` }]
1864
+ : input.closeChannelAfterPayment
1865
+ ? [{ key: "channel_close", label: "Provisioned channel closed", status: closeStatus, detail: input.channelClose?.error ?? input.channelClosed?.state ?? (input.failure ? "Not attempted because the channel never became ready." : undefined) }]
1866
+ : []),
1867
+ ],
1868
+ });
1869
+ }
1870
+
1871
+ export function resolveN2nPeerAddress(config, address, sameHost) {
1872
+ const normalized = textValue(address);
1873
+ if (!normalized) return "";
1874
+ const addressIp = /\/ip4\/([^/]+)/i.exec(normalized)?.[1];
1875
+ const localPublicIp = textValue(config.publicIp);
1876
+ if ((sameHost || (addressIp && localPublicIp && addressIp === localPublicIp)) && addressIp) {
1877
+ return normalized.replace(/\/ip4\/[^/]+/i, "/ip4/127.0.0.1");
1878
+ }
1879
+ return normalized;
1880
+ }
1881
+
1882
+ function commandResultError(errorMessage, commandResult) {
1883
+ const error = new Error(errorMessage);
1884
+ error.commandResult = commandResult;
1885
+ return error;
1886
+ }
1887
+
1722
1888
  async function waitForCkbTransaction(config, txHash, params = {}) {
1723
1889
  const rpcUrl = resolveCkbIndexerUrl(config);
1724
1890
  if (!rpcUrl) throw new Error("No CKB RPC URL is configured for deposit confirmation.");
@@ -2061,6 +2227,9 @@ async function openFiberChannel(config, params) {
2061
2227
  public: boolValue(params.public ?? params.publicChannel, true),
2062
2228
  };
2063
2229
  if (params.oneWay !== undefined || params.one_way !== undefined) openRequest.one_way = boolValue(params.oneWay ?? params.one_way, false);
2230
+ if (openRequest.one_way && openRequest.public) {
2231
+ throw new Error("FNN does not allow a channel to be both public and one-way.");
2232
+ }
2064
2233
  if (params.fundingUdtTypeScript ?? params.funding_udt_type_script) openRequest.funding_udt_type_script = params.fundingUdtTypeScript ?? params.funding_udt_type_script;
2065
2234
  if (params.commitmentFeeRate ?? params.commitment_fee_rate) openRequest.commitment_fee_rate = quantity(params.commitmentFeeRate ?? params.commitment_fee_rate);
2066
2235
  if (params.fundingFeeRate ?? params.funding_fee_rate) openRequest.funding_fee_rate = quantity(params.fundingFeeRate ?? params.funding_fee_rate);
@@ -2072,37 +2241,57 @@ async function openFiberChannel(config, params) {
2072
2241
 
2073
2242
  const fundingCheck = await assertChannelFundingAvailable(config, openRequest.funding_amount);
2074
2243
  let connectResult;
2075
- const connectFirst = boolValue(params.connectFirst, true) && !boolValue(params.skipConnect, false);
2076
- if (connectFirst) {
2077
- connectResult = await ensurePeerConnected(config, peer, pubkey);
2078
- }
2079
- const openResult = await openChannelWhenPeerReady(config, peer, pubkey, openRequest, params);
2244
+ let openResult;
2080
2245
  let channelWatch;
2081
- if (!boolValue(params.wait, false) && !boolValue(params.waitUntilReady, false)) {
2082
- channelWatch = await watchForImmediateChannelFailure(
2083
- config.fiberRpcUrl,
2084
- pubkey,
2085
- positiveNumber(params.failureWatchMs ?? config.channelFailureWatchMs, DEFAULT_CHANNEL_FAILURE_WATCH_MS),
2086
- positiveNumber(params.failurePollMs ?? params.pollMs ?? config.channelFailurePollMs, DEFAULT_CHANNEL_FAILURE_POLL_MS),
2087
- );
2088
- }
2089
- const result = {
2090
- ok: true,
2091
- action: "OPEN_CHANNEL",
2092
- peerPubkey: pubkey,
2093
- peer: peer.meta,
2094
- peerAddress: peer.address,
2095
- fundingAmount: openRequest.funding_amount,
2096
- public: openRequest.public,
2097
- fundingCheck,
2098
- connectResult,
2099
- openResult,
2100
- channelWatch,
2101
- };
2102
- if (boolValue(params.wait, false) || boolValue(params.waitUntilReady, false)) {
2103
- result.channel = await waitForChannel(config.fiberRpcUrl, pubkey, positiveNumber(params.waitTimeoutMs, 20 * 60_000), positiveNumber(params.pollMs, 10_000));
2246
+ const connectFirst = boolValue(params.connectFirst, true) && !boolValue(params.skipConnect, false);
2247
+ try {
2248
+ if (connectFirst) connectResult = await ensurePeerConnected(config, peer, pubkey);
2249
+ openResult = await openChannelWhenPeerReady(config, peer, pubkey, openRequest, params);
2250
+ if (!boolValue(params.wait, false) && !boolValue(params.waitUntilReady, false)) {
2251
+ channelWatch = await watchForImmediateChannelFailure(
2252
+ config.fiberRpcUrl,
2253
+ pubkey,
2254
+ positiveNumber(params.failureWatchMs ?? config.channelFailureWatchMs, DEFAULT_CHANNEL_FAILURE_WATCH_MS),
2255
+ positiveNumber(params.failurePollMs ?? params.pollMs ?? config.channelFailurePollMs, DEFAULT_CHANNEL_FAILURE_POLL_MS),
2256
+ );
2257
+ }
2258
+ const result = {
2259
+ ok: true,
2260
+ action: "OPEN_CHANNEL",
2261
+ peerPubkey: pubkey,
2262
+ peer: peer.meta,
2263
+ peerAddress: peer.address,
2264
+ fundingAmount: openRequest.funding_amount,
2265
+ public: openRequest.public,
2266
+ oneWay: openRequest.one_way === true,
2267
+ fundingCheck,
2268
+ connectResult,
2269
+ openResult,
2270
+ channelWatch,
2271
+ };
2272
+ if (boolValue(params.wait, false) || boolValue(params.waitUntilReady, false)) {
2273
+ result.channel = await waitForChannel(config.fiberRpcUrl, pubkey, positiveNumber(params.waitTimeoutMs, 20 * 60_000), positiveNumber(params.pollMs, 10_000));
2274
+ }
2275
+ return result;
2276
+ } catch (error) {
2277
+ const observed = error?.commandResult && typeof error.commandResult === "object" ? error.commandResult : undefined;
2278
+ throw commandResultError(message(error), {
2279
+ ok: false,
2280
+ action: "OPEN_CHANNEL",
2281
+ peerPubkey: pubkey,
2282
+ peer: peer.meta,
2283
+ peerAddress: peer.address,
2284
+ fundingAmount: openRequest.funding_amount,
2285
+ public: openRequest.public,
2286
+ oneWay: openRequest.one_way === true,
2287
+ fundingCheck,
2288
+ connectResult,
2289
+ openResult,
2290
+ channelWatch,
2291
+ channel: observed?.channel,
2292
+ failure: message(error),
2293
+ });
2104
2294
  }
2105
- return result;
2106
2295
  }
2107
2296
 
2108
2297
  async function settleFiberChannel(runtime, params) {
@@ -3003,7 +3192,25 @@ async function waitForChannel(rpcUrl, pubkey, timeoutMs, pollMs) {
3003
3192
  if (failed) throw new Error(describeChannelTerminalFailure(failed));
3004
3193
  await sleep(pollMs);
3005
3194
  }
3006
- throw new Error(`Timed out waiting for channel readiness. Last channel: ${JSON.stringify(lastChannel ?? null)}`);
3195
+ const fundingHint = isAwaitingRemoteFundingCollaboration(lastChannel)
3196
+ ? " The receiving FNN did not complete funding collaboration. A CKB channel acceptor must provide its configured reserved capacity (commonly about 99 CKB), even for a private one-way channel. Fund the receiving node wallet and inspect its FNN logs."
3197
+ : "";
3198
+ throw commandResultError(`Timed out waiting for channel readiness.${fundingHint} Last channel: ${JSON.stringify(lastChannel ?? null)}`, {
3199
+ channel: lastChannel,
3200
+ channelId: channelIdOf(lastChannel) || undefined,
3201
+ });
3202
+ }
3203
+
3204
+ function isAwaitingRemoteFundingCollaboration(channel) {
3205
+ const state = channel?.state && typeof channel.state === "object" ? channel.state : {};
3206
+ const name = textValue(state.state_name ?? state.stateName ?? channel?.state_name ?? channel?.stateName).toLowerCase();
3207
+ const flags = textValue(state.state_flags ?? state.stateFlags ?? channel?.state_flags ?? channel?.stateFlags).toLowerCase();
3208
+ return name === "collaboratingfundingtx" && flags.includes("awaiting_remote_tx_collaboration_msg");
3209
+ }
3210
+
3211
+ function isChannelReadyState(state) {
3212
+ const normalized = textValue(state).toLowerCase();
3213
+ return normalized === "channelready" || normalized === "open";
3007
3214
  }
3008
3215
 
3009
3216
  async function watchForImmediateChannelFailure(rpcUrl, pubkey, timeoutMs, pollMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fibscope/agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Fibscope Agent for Fiber node owners.",
6
6
  "bin": {