@fibscope/agent 0.1.3 → 0.1.5

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 +14 -2
  2. package/index.mjs +389 -43
  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.3",
77
+ "agentVersion": "0.1.5",
78
78
  "chain": "testnet",
79
79
  "collectedAt": 1710000000000,
80
80
  "publicIp": "54.179.226.154",
@@ -94,6 +94,18 @@ Fibscope authenticates this with `x-agent-token`. This is an agent key for local
94
94
 
95
95
  Before the first push, the agent registers the local Fiber node with `POST /agent/v1/register`. That saves the node owner, Fiber node id, display name, chain, public profile setting, and the latest local node details in the hosted database. If the agent key is already attached to a different registered Fiber node, set `overwriteNodeRegistration` to `true` only when you intentionally want this agent to replace that old node binding.
96
96
 
97
+ ## API Node Payment Lifecycle
98
+
99
+ For an `API_NODE_PAYMENT`, the agent confirms the sender deposit and checks for a viable existing Fiber route. When the quote permits provisioning and no route exists, it connects the receiving peer, opens a direct funded channel, waits for readiness, and sends the payment. A close-after-test policy applies only to the channel opened by that payment; existing channels are never closed automatically.
100
+
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
+
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 direct channel from its own funding when needed, sends over Fiber, and optionally closes only the channel created by that test.
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
+
97
109
  ## Resilience
98
110
 
99
111
  Collection and transmission are independent loops:
@@ -119,7 +131,7 @@ The Agent Setup page generates this command from a masked password field. The `f
119
131
  ## NPM Usage
120
132
 
121
133
  ```bash
122
- npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter node -- start && pm2 save
134
+ npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter none -- start && pm2 save
123
135
  ```
124
136
 
125
137
  Or run without a global install:
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.3";
36
+ const AGENT_VERSION = "0.1.5";
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,32 +1549,189 @@ 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 fiberPayment = await sendFiberPayment(config, {
1555
- ...params,
1556
- targetPubkey,
1557
- amountShannons: textValue(params.amountShannons ?? params.amount),
1558
- keysend: true,
1559
- });
1559
+ const publishedPeerAddress = firstTextValue(params.peerAddress, params.receiverPeerAddress, params.address);
1560
+ const peerAddress = resolveN2nPeerAddress(config, publishedPeerAddress, boolValue(params.sameHost, false));
1561
+ const provisionChannel = boolValue(params.provisionChannel, true);
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
+ };
1569
+ let existingChannel;
1570
+ let routeProbe;
1571
+ try {
1572
+ existingChannel = await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs).catch(() => undefined);
1573
+ try {
1574
+ routeProbe = await simulateFiberRoute(config, {
1575
+ destinationNode: targetPubkey,
1576
+ amountShannons: textValue(params.amountShannons ?? params.amount),
1577
+ assetType: textValue(params.assetType ?? params.asset) || "CKB",
1578
+ });
1579
+ } catch (error) {
1580
+ routeProbe = { status: "failed", error: message(error) };
1581
+ }
1582
+ } catch (error) {
1583
+ routeProbe = { status: "failed", error: message(error) };
1584
+ }
1585
+ const existingRouteReady = routeProbe?.status === "success" || Boolean(existingChannel);
1586
+ let channelOpen;
1587
+ let paymentChannel = existingChannel;
1588
+ let channelBeforePayment;
1589
+ let fiberPayment;
1590
+ try {
1591
+ if (!existingRouteReady) {
1592
+ if (!provisionChannel) {
1593
+ throw new Error(`No viable Fiber route to ${shortText(targetPubkey, 10, 8)} and direct channel provisioning is disabled.`);
1594
+ }
1595
+ if (!peerAddress) {
1596
+ throw new Error(`No viable Fiber route to ${shortText(targetPubkey, 10, 8)} and the receiving node has no public peer address for automatic connection.`);
1597
+ }
1598
+ channelOpen = await openFiberChannel(config, {
1599
+ peerPubkey: targetPubkey,
1600
+ peerAddress,
1601
+ fundingAmount: firstTextValue(params.channelFundingAmount, params.totalDebit, params.amountShannons, params.amount),
1602
+ connectFirst: true,
1603
+ waitUntilReady: true,
1604
+ waitTimeoutMs: positiveNumber(params.channelReadyTimeoutMs, 10 * 60_000),
1605
+ pollMs: positiveNumber(params.channelPollMs, 5_000),
1606
+ public: true,
1607
+ });
1608
+ paymentChannel = channelOpen.channel ?? await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs);
1609
+ }
1610
+ channelBeforePayment = channelLiquiditySnapshot(paymentChannel);
1611
+ fiberPayment = await sendFiberPayment(config, {
1612
+ ...params,
1613
+ targetPubkey,
1614
+ amountShannons: textValue(params.amountShannons ?? params.amount),
1615
+ keysend: true,
1616
+ });
1617
+ } catch (error) {
1618
+ const partialOpen = error?.commandResult && typeof error.commandResult === "object" ? error.commandResult : undefined;
1619
+ if (ownerFunded) {
1620
+ let recovery;
1621
+ let recoveryError;
1622
+ try {
1623
+ const openedChannelId = channelIdOf(paymentChannel);
1624
+ if (channelOpen && openedChannelId) {
1625
+ recovery = await settleFiberChannel(runtime, { channelId: openedChannelId, force: false, feeRate: "1000" });
1626
+ await waitForChannelClosed(config.fiberRpcUrl, openedChannelId, 10 * 60_000, 5_000);
1627
+ }
1628
+ } catch (failure) {
1629
+ recoveryError = message(failure);
1630
+ }
1631
+ const fundingAfter = await collectFundingState(config).catch(() => undefined);
1632
+ const lifecycle = nodePaymentLifecycle({
1633
+ source,
1634
+ targetPubkey,
1635
+ publishedPeerAddress,
1636
+ peerAddress,
1637
+ provisionChannel,
1638
+ closeChannelAfterPayment,
1639
+ existingChannel,
1640
+ routeProbe,
1641
+ channelOpen: channelOpen ?? partialOpen,
1642
+ paymentChannel,
1643
+ channelBeforePayment,
1644
+ fiberPayment,
1645
+ fundingAfter,
1646
+ failure: message(error),
1647
+ recovery,
1648
+ recoveryError,
1649
+ });
1650
+ throw commandResultError(`N2N transfer failed while preparing the peer, channel, or Fiber payment: ${message(error)}. No API test wallet was charged.`, {
1651
+ ok: false,
1652
+ action: "N2N_PAYMENT",
1653
+ mode: "Owner-funded node-to-node test",
1654
+ paymentStatus: "failed",
1655
+ source,
1656
+ receiverNode: targetPubkey,
1657
+ amount: textValue(params.amountShannons ?? params.amount) || undefined,
1658
+ assetType: textValue(params.assetType ?? params.asset) || "CKB",
1659
+ fiberPayment,
1660
+ channelLifecycle: lifecycle,
1661
+ fundingBefore: source.fundingBefore,
1662
+ fundingAfter,
1663
+ });
1664
+ }
1665
+ let refund;
1666
+ let refundError;
1667
+ try {
1668
+ const openedChannelId = channelIdOf(paymentChannel);
1669
+ if (channelOpen && openedChannelId) {
1670
+ await settleFiberChannel(runtime, { channelId: openedChannelId, force: false, feeRate: "1000" });
1671
+ await waitForChannelClosed(config.fiberRpcUrl, openedChannelId, 10 * 60_000, 5_000);
1672
+ }
1673
+ refund = await executeProviderWalletPayout(config, {
1674
+ receiverWallet: textValue(params.senderWallet),
1675
+ amount: textValue(params.totalDebit ?? params.depositAmount),
1676
+ paymentId: textValue(params.paymentId),
1677
+ depositTxHash,
1678
+ });
1679
+ } catch (failure) {
1680
+ refundError = message(failure);
1681
+ }
1682
+ const refundDetail = refund?.txHash
1683
+ ? ` Refund broadcast to the sender: ${refund.txHash}.`
1684
+ : ` Automatic refund also failed: ${refundError ?? "unknown refund error"}.`;
1685
+ throw new Error(`Fiber node delivery failed after deposit confirmation: ${message(error)}.${refundDetail}`);
1686
+ }
1687
+ const paymentChannelId = channelIdOf(paymentChannel);
1688
+ const channelAfterPaymentRaw = paymentChannelId
1689
+ ? await waitForChannelBalanceChange(
1690
+ config.fiberRpcUrl,
1691
+ paymentChannelId,
1692
+ channelBeforePayment,
1693
+ positiveNumber(params.channelBalanceTimeoutMs, 60_000),
1694
+ positiveNumber(params.channelBalancePollMs, 2_000),
1695
+ ).catch(() => undefined)
1696
+ : await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs).catch(() => undefined);
1697
+ const channelAfterPayment = channelLiquiditySnapshot(channelAfterPaymentRaw);
1698
+ let channelClose;
1699
+ let channelClosed;
1700
+ if (channelOpen && closeChannelAfterPayment && paymentChannelId) {
1701
+ try {
1702
+ channelClose = await settleFiberChannel(runtime, {
1703
+ channelId: paymentChannelId,
1704
+ force: false,
1705
+ feeRate: params.channelCloseFeeRate ?? params.feeRate ?? "1000",
1706
+ });
1707
+ const closed = await waitForChannelClosed(
1708
+ config.fiberRpcUrl,
1709
+ paymentChannelId,
1710
+ positiveNumber(params.channelCloseTimeoutMs, 10 * 60_000),
1711
+ positiveNumber(params.channelPollMs, 5_000),
1712
+ );
1713
+ channelClosed = channelLiquiditySnapshot(closed);
1714
+ } catch (error) {
1715
+ channelClose = { status: "failed", error: message(error) };
1716
+ }
1717
+ }
1560
1718
  let refreshed;
1719
+ const fundingAfter = ownerFunded ? await collectFundingState(config).catch(() => undefined) : undefined;
1561
1720
  if (typeof runtime.collect === "function" && typeof runtime.push === "function") {
1562
1721
  refreshed = await pushWithRegistrationRetry(runtime, await runtime.collect(), {
1563
- intent: "api-node-payment-completed",
1722
+ intent: ownerFunded ? "n2n-payment-completed" : "api-node-payment-completed",
1564
1723
  retryOnRateLimit: true,
1565
1724
  maxRateLimitRetries: 1,
1566
1725
  }).catch((error) => ({ ok: false, error: message(error) }));
1567
1726
  }
1568
1727
  return {
1569
1728
  ok: true,
1570
- action: "API_NODE_PAYMENT",
1571
- mode: "API-managed Fiber node payment",
1729
+ action: ownerFunded ? "N2N_PAYMENT" : "API_NODE_PAYMENT",
1730
+ mode: ownerFunded ? "Owner-funded node-to-node test" : "API-managed Fiber node payment",
1572
1731
  paymentStatus: "completed",
1573
- message: "Sender deposit is confirmed and the provider agent submitted the Fiber payment to the recipient node.",
1732
+ message: ownerFunded
1733
+ ? "The selected source node funded or reused the Fiber route and sent the N2N payment without charging an API test wallet."
1734
+ : "Sender deposit is confirmed and the provider agent submitted the Fiber payment to the recipient node.",
1574
1735
  paymentId: textValue(params.paymentId) || undefined,
1575
1736
  quoteId: textValue(params.quoteId) || undefined,
1576
1737
  candidateId: textValue(params.candidateId) || undefined,
@@ -1588,10 +1749,36 @@ async function acceptApiNodePayment(runtime, params) {
1588
1749
  depositAmount: textValue(params.depositAmount) || undefined,
1589
1750
  deposit,
1590
1751
  fiberPayment,
1752
+ source: ownerFunded ? { ...source, fundingAfter } : undefined,
1753
+ fundingBefore: ownerFunded ? source.fundingBefore : undefined,
1754
+ fundingAfter,
1755
+ channelLifecycle: nodePaymentLifecycle({
1756
+ source,
1757
+ targetPubkey,
1758
+ publishedPeerAddress,
1759
+ peerAddress,
1760
+ provisionChannel,
1761
+ closeChannelAfterPayment,
1762
+ existingChannel,
1763
+ routeProbe,
1764
+ channelOpen,
1765
+ paymentChannel,
1766
+ paymentChannelId,
1767
+ channelBeforePayment,
1768
+ channelAfterPayment,
1769
+ fiberPayment,
1770
+ channelClose,
1771
+ channelClosed,
1772
+ fundingAfter,
1773
+ }),
1591
1774
  fundingRefresh: refreshed,
1592
1775
  routeExecution: {
1593
1776
  status: "fiber_send_payment",
1594
- reason: "Fiber node recipients are channel endpoints, so the provider uses outbound Fiber liquidity after confirming the payer deposit.",
1777
+ reason: channelOpen
1778
+ ? "No viable route existed, so the source connected the receiving peer, opened a direct channel, sent the payment, and applied the requested close policy."
1779
+ : ownerFunded
1780
+ ? "The selected source node used its existing outbound Fiber liquidity."
1781
+ : "The provider used existing outbound Fiber liquidity after confirming the payer deposit.",
1595
1782
  },
1596
1783
  node: {
1597
1784
  pubkey: normalizePeerPubkey(readNodeId(info)),
@@ -1601,6 +1788,77 @@ async function acceptApiNodePayment(runtime, params) {
1601
1788
  };
1602
1789
  }
1603
1790
 
1791
+ export function nodePaymentLifecycle(input) {
1792
+ const channelOpen = input.channelOpen && typeof input.channelOpen === "object" ? input.channelOpen : undefined;
1793
+ const connectResult = channelOpen?.connectResult;
1794
+ const channelId = input.paymentChannelId || channelIdOf(input.paymentChannel) || channelIdOf(channelOpen?.channel);
1795
+ const existingRoute = Boolean(input.existingChannel) || input.routeProbe?.status === "success";
1796
+ const peerStatus = existingRoute ? "skipped" : connectResult ? "completed" : input.failure ? "failed" : "pending";
1797
+ const channelStatus = existingRoute ? "skipped" : channelId || channelOpen?.openResult ? "completed" : input.failure ? "failed" : "pending";
1798
+ const paymentStatus = input.fiberPayment ? "completed" : input.failure ? "failed" : "pending";
1799
+ const closeStatus = !input.closeChannelAfterPayment
1800
+ ? "skipped"
1801
+ : input.channelClosed ? "completed"
1802
+ : input.channelClose?.status === "failed" || input.failure ? "failed" : "pending";
1803
+ return omitUndefined({
1804
+ mode: channelOpen && !existingRoute ? "provisioned_direct_channel" : input.existingChannel ? "existing_direct_channel" : "existing_routed_liquidity",
1805
+ provisionRequested: input.provisionChannel,
1806
+ closeRequested: input.closeChannelAfterPayment,
1807
+ source: {
1808
+ nodeId: input.source?.nodeId,
1809
+ pubkey: input.source?.pubkey,
1810
+ displayName: input.source?.displayName,
1811
+ fundingBefore: input.source?.fundingBefore,
1812
+ fundingAfter: input.fundingAfter,
1813
+ },
1814
+ peer: {
1815
+ pubkey: input.targetPubkey,
1816
+ publishedAddress: input.publishedPeerAddress || undefined,
1817
+ dialAddress: input.peerAddress || undefined,
1818
+ sameHostLoopback: Boolean(input.publishedPeerAddress && input.peerAddress && input.publishedPeerAddress !== input.peerAddress),
1819
+ },
1820
+ routeProbe: input.routeProbe,
1821
+ peerConnect: connectResult,
1822
+ channelOpen,
1823
+ channelId: channelId || undefined,
1824
+ providerBeforePayment: input.channelBeforePayment?.provider,
1825
+ receiverBeforePayment: input.channelBeforePayment?.receiver,
1826
+ providerAfterPayment: input.channelAfterPayment?.provider,
1827
+ receiverAfterPayment: input.channelAfterPayment?.receiver,
1828
+ channelClose: input.channelClose,
1829
+ providerAtClose: input.channelClosed?.provider,
1830
+ receiverAtClose: input.channelClosed?.receiver,
1831
+ finalChannelState: input.channelClosed?.state ?? input.channelAfterPayment?.state,
1832
+ recovery: input.recovery,
1833
+ recoveryError: input.recoveryError,
1834
+ failure: input.failure,
1835
+ steps: [
1836
+ { key: "source_selected", label: "Source node selected", status: "completed", detail: input.source?.displayName || shortText(input.source?.pubkey, 10, 8) },
1837
+ { 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 },
1838
+ { key: "channel_open", label: "Channel ready", status: channelStatus, detail: existingRoute ? "Existing Fiber liquidity selected." : channelId ? `Channel ${shortText(channelId, 10, 8)}` : input.failure },
1839
+ { 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 },
1840
+ ...(input.closeChannelAfterPayment ? [{ key: "channel_close", label: "Provisioned channel closed", status: closeStatus, detail: input.channelClose?.error ?? input.channelClosed?.state } ] : []),
1841
+ ],
1842
+ });
1843
+ }
1844
+
1845
+ export function resolveN2nPeerAddress(config, address, sameHost) {
1846
+ const normalized = textValue(address);
1847
+ if (!normalized) return "";
1848
+ const addressIp = /\/ip4\/([^/]+)/i.exec(normalized)?.[1];
1849
+ const localPublicIp = textValue(config.publicIp);
1850
+ if ((sameHost || (addressIp && localPublicIp && addressIp === localPublicIp)) && addressIp) {
1851
+ return normalized.replace(/\/ip4\/[^/]+/i, "/ip4/127.0.0.1");
1852
+ }
1853
+ return normalized;
1854
+ }
1855
+
1856
+ function commandResultError(errorMessage, commandResult) {
1857
+ const error = new Error(errorMessage);
1858
+ error.commandResult = commandResult;
1859
+ return error;
1860
+ }
1861
+
1604
1862
  async function waitForCkbTransaction(config, txHash, params = {}) {
1605
1863
  const rpcUrl = resolveCkbIndexerUrl(config);
1606
1864
  if (!rpcUrl) throw new Error("No CKB RPC URL is configured for deposit confirmation.");
@@ -1954,37 +2212,53 @@ async function openFiberChannel(config, params) {
1954
2212
 
1955
2213
  const fundingCheck = await assertChannelFundingAvailable(config, openRequest.funding_amount);
1956
2214
  let connectResult;
1957
- const connectFirst = boolValue(params.connectFirst, true) && !boolValue(params.skipConnect, false);
1958
- if (connectFirst) {
1959
- connectResult = await ensurePeerConnected(config, peer, pubkey);
1960
- }
1961
- const openResult = await openChannelWhenPeerReady(config, peer, pubkey, openRequest, params);
2215
+ let openResult;
1962
2216
  let channelWatch;
1963
- if (!boolValue(params.wait, false) && !boolValue(params.waitUntilReady, false)) {
1964
- channelWatch = await watchForImmediateChannelFailure(
1965
- config.fiberRpcUrl,
1966
- pubkey,
1967
- positiveNumber(params.failureWatchMs ?? config.channelFailureWatchMs, DEFAULT_CHANNEL_FAILURE_WATCH_MS),
1968
- positiveNumber(params.failurePollMs ?? params.pollMs ?? config.channelFailurePollMs, DEFAULT_CHANNEL_FAILURE_POLL_MS),
1969
- );
1970
- }
1971
- const result = {
1972
- ok: true,
1973
- action: "OPEN_CHANNEL",
1974
- peerPubkey: pubkey,
1975
- peer: peer.meta,
1976
- peerAddress: peer.address,
1977
- fundingAmount: openRequest.funding_amount,
1978
- public: openRequest.public,
1979
- fundingCheck,
1980
- connectResult,
1981
- openResult,
1982
- channelWatch,
1983
- };
1984
- if (boolValue(params.wait, false) || boolValue(params.waitUntilReady, false)) {
1985
- result.channel = await waitForChannel(config.fiberRpcUrl, pubkey, positiveNumber(params.waitTimeoutMs, 20 * 60_000), positiveNumber(params.pollMs, 10_000));
2217
+ const connectFirst = boolValue(params.connectFirst, true) && !boolValue(params.skipConnect, false);
2218
+ try {
2219
+ if (connectFirst) connectResult = await ensurePeerConnected(config, peer, pubkey);
2220
+ openResult = await openChannelWhenPeerReady(config, peer, pubkey, openRequest, params);
2221
+ if (!boolValue(params.wait, false) && !boolValue(params.waitUntilReady, false)) {
2222
+ channelWatch = await watchForImmediateChannelFailure(
2223
+ config.fiberRpcUrl,
2224
+ pubkey,
2225
+ positiveNumber(params.failureWatchMs ?? config.channelFailureWatchMs, DEFAULT_CHANNEL_FAILURE_WATCH_MS),
2226
+ positiveNumber(params.failurePollMs ?? params.pollMs ?? config.channelFailurePollMs, DEFAULT_CHANNEL_FAILURE_POLL_MS),
2227
+ );
2228
+ }
2229
+ const result = {
2230
+ ok: true,
2231
+ action: "OPEN_CHANNEL",
2232
+ peerPubkey: pubkey,
2233
+ peer: peer.meta,
2234
+ peerAddress: peer.address,
2235
+ fundingAmount: openRequest.funding_amount,
2236
+ public: openRequest.public,
2237
+ fundingCheck,
2238
+ connectResult,
2239
+ openResult,
2240
+ channelWatch,
2241
+ };
2242
+ if (boolValue(params.wait, false) || boolValue(params.waitUntilReady, false)) {
2243
+ result.channel = await waitForChannel(config.fiberRpcUrl, pubkey, positiveNumber(params.waitTimeoutMs, 20 * 60_000), positiveNumber(params.pollMs, 10_000));
2244
+ }
2245
+ return result;
2246
+ } catch (error) {
2247
+ throw commandResultError(message(error), {
2248
+ ok: false,
2249
+ action: "OPEN_CHANNEL",
2250
+ peerPubkey: pubkey,
2251
+ peer: peer.meta,
2252
+ peerAddress: peer.address,
2253
+ fundingAmount: openRequest.funding_amount,
2254
+ public: openRequest.public,
2255
+ fundingCheck,
2256
+ connectResult,
2257
+ openResult,
2258
+ channelWatch,
2259
+ failure: message(error),
2260
+ });
1986
2261
  }
1987
- return result;
1988
2262
  }
1989
2263
 
1990
2264
  async function settleFiberChannel(runtime, params) {
@@ -2050,6 +2324,78 @@ async function findChannelById(rpcUrl, channelId, timeoutMs) {
2050
2324
  return channels.find((channel) => textValue(channel?.channel_id ?? channel?.channelId ?? channel?.id).toLowerCase() === normalized);
2051
2325
  }
2052
2326
 
2327
+ async function findReadyChannelForPeer(rpcUrl, pubkey, timeoutMs) {
2328
+ const response = await rpcCall(rpcUrl, "list_channels", [{ pubkey, include_closed: true }], timeoutMs);
2329
+ const channels = Array.isArray(response?.channels) ? response.channels : Array.isArray(response) ? response : [];
2330
+ return channels.find((channel) => {
2331
+ const state = channelStateName(channel).toLowerCase();
2332
+ return state === "channelready" || state === "open";
2333
+ });
2334
+ }
2335
+
2336
+ async function waitForChannelClosed(rpcUrl, channelId, timeoutMs, pollMs) {
2337
+ const deadline = Date.now() + timeoutMs;
2338
+ let lastChannel;
2339
+ while (Date.now() < deadline) {
2340
+ lastChannel = await findChannelById(rpcUrl, channelId, Math.min(pollMs, 10_000)).catch(() => lastChannel);
2341
+ const state = channelStateName(lastChannel).toLowerCase();
2342
+ if (["closed", "abandoned"].includes(state)) return lastChannel;
2343
+ await sleep(pollMs);
2344
+ }
2345
+ return {
2346
+ ...asObject(lastChannel),
2347
+ channel_id: channelId,
2348
+ close_wait_status: "timeout",
2349
+ close_wait_timeout_ms: timeoutMs,
2350
+ };
2351
+ }
2352
+
2353
+ async function waitForChannelBalanceChange(rpcUrl, channelId, before, timeoutMs, pollMs) {
2354
+ const deadline = Date.now() + timeoutMs;
2355
+ let lastChannel;
2356
+ while (Date.now() < deadline) {
2357
+ lastChannel = await findChannelById(rpcUrl, channelId, Math.min(pollMs, 10_000)).catch(() => lastChannel);
2358
+ const current = channelLiquiditySnapshot(lastChannel);
2359
+ if (!before || channelBalanceChanged(before, current)) return lastChannel;
2360
+ await sleep(pollMs);
2361
+ }
2362
+ return lastChannel;
2363
+ }
2364
+
2365
+ function channelBalanceChanged(before, after) {
2366
+ if (!before || !after) return false;
2367
+ return before.provider.localBalance !== after.provider.localBalance
2368
+ || before.provider.remoteBalance !== after.provider.remoteBalance;
2369
+ }
2370
+
2371
+ function channelIdOf(channel) {
2372
+ return firstTextValue(channel?.channel_id, channel?.channelId, channel?.id);
2373
+ }
2374
+
2375
+ function channelLiquiditySnapshot(channel) {
2376
+ if (!channel || typeof channel !== "object") return undefined;
2377
+ const localBalance = amountString(channel.local_balance ?? channel.localBalance ?? channel.outbound_liquidity ?? channel.outboundLiquidity);
2378
+ const remoteBalance = amountString(channel.remote_balance ?? channel.remoteBalance ?? channel.inbound_liquidity ?? channel.inboundLiquidity);
2379
+ const capacity = amountString(channel.funding_amount ?? channel.fundingAmount ?? channel.capacity);
2380
+ return {
2381
+ channelId: channelIdOf(channel) || undefined,
2382
+ state: channelStateName(channel),
2383
+ capacity,
2384
+ provider: {
2385
+ localBalance,
2386
+ outbound: localBalance,
2387
+ remoteBalance,
2388
+ inbound: remoteBalance,
2389
+ },
2390
+ receiver: {
2391
+ localBalance: remoteBalance,
2392
+ outbound: remoteBalance,
2393
+ remoteBalance: localBalance,
2394
+ inbound: localBalance,
2395
+ },
2396
+ };
2397
+ }
2398
+
2053
2399
  async function assertNotLocalPeer(config, pubkey) {
2054
2400
  try {
2055
2401
  const info = await rpcCall(config.fiberRpcUrl, "node_info", [], config.timeoutMs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fibscope/agent",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Fibscope Agent for Fiber node owners.",
6
6
  "bin": {