@fibscope/agent 0.1.4 → 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 +7 -1
  2. package/index.mjs +209 -53
  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.5",
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 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
+
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.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,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 {
@@ -1603,6 +1615,53 @@ async function acceptApiNodePayment(runtime, params) {
1603
1615
  keysend: true,
1604
1616
  });
1605
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
+ }
1606
1665
  let refund;
1607
1666
  let refundError;
1608
1667
  try {
@@ -1657,19 +1716,22 @@ async function acceptApiNodePayment(runtime, params) {
1657
1716
  }
1658
1717
  }
1659
1718
  let refreshed;
1719
+ const fundingAfter = ownerFunded ? await collectFundingState(config).catch(() => undefined) : undefined;
1660
1720
  if (typeof runtime.collect === "function" && typeof runtime.push === "function") {
1661
1721
  refreshed = await pushWithRegistrationRetry(runtime, await runtime.collect(), {
1662
- intent: "api-node-payment-completed",
1722
+ intent: ownerFunded ? "n2n-payment-completed" : "api-node-payment-completed",
1663
1723
  retryOnRateLimit: true,
1664
1724
  maxRateLimitRetries: 1,
1665
1725
  }).catch((error) => ({ ok: false, error: message(error) }));
1666
1726
  }
1667
1727
  return {
1668
1728
  ok: true,
1669
- action: "API_NODE_PAYMENT",
1670
- 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",
1671
1731
  paymentStatus: "completed",
1672
- 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.",
1673
1735
  paymentId: textValue(params.paymentId) || undefined,
1674
1736
  quoteId: textValue(params.quoteId) || undefined,
1675
1737
  candidateId: textValue(params.candidateId) || undefined,
@@ -1687,29 +1749,36 @@ async function acceptApiNodePayment(runtime, params) {
1687
1749
  depositAmount: textValue(params.depositAmount) || undefined,
1688
1750
  deposit,
1689
1751
  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 },
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,
1695
1763
  routeProbe,
1696
1764
  channelOpen,
1697
- channelId: paymentChannelId || undefined,
1698
- providerBeforePayment: channelBeforePayment?.provider,
1699
- receiverBeforePayment: channelBeforePayment?.receiver,
1700
- providerAfterPayment: channelAfterPayment?.provider,
1701
- receiverAfterPayment: channelAfterPayment?.receiver,
1765
+ paymentChannel,
1766
+ paymentChannelId,
1767
+ channelBeforePayment,
1768
+ channelAfterPayment,
1769
+ fiberPayment,
1702
1770
  channelClose,
1703
- providerAtClose: channelClosed?.provider,
1704
- receiverAtClose: channelClosed?.receiver,
1705
- finalChannelState: channelClosed?.state ?? channelAfterPayment?.state,
1706
- },
1771
+ channelClosed,
1772
+ fundingAfter,
1773
+ }),
1707
1774
  fundingRefresh: refreshed,
1708
1775
  routeExecution: {
1709
1776
  status: "fiber_send_payment",
1710
1777
  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.",
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.",
1713
1782
  },
1714
1783
  node: {
1715
1784
  pubkey: normalizePeerPubkey(readNodeId(info)),
@@ -1719,6 +1788,77 @@ async function acceptApiNodePayment(runtime, params) {
1719
1788
  };
1720
1789
  }
1721
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
+
1722
1862
  async function waitForCkbTransaction(config, txHash, params = {}) {
1723
1863
  const rpcUrl = resolveCkbIndexerUrl(config);
1724
1864
  if (!rpcUrl) throw new Error("No CKB RPC URL is configured for deposit confirmation.");
@@ -2072,37 +2212,53 @@ async function openFiberChannel(config, params) {
2072
2212
 
2073
2213
  const fundingCheck = await assertChannelFundingAvailable(config, openRequest.funding_amount);
2074
2214
  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);
2215
+ let openResult;
2080
2216
  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));
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
+ });
2104
2261
  }
2105
- return result;
2106
2262
  }
2107
2263
 
2108
2264
  async function settleFiberChannel(runtime, params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fibscope/agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Fibscope Agent for Fiber node owners.",
6
6
  "bin": {