@fibscope/agent 0.1.3 → 0.1.4

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 +8 -2
  2. package/index.mjs +198 -8
  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.4",
78
78
  "chain": "testnet",
79
79
  "collectedAt": 1710000000000,
80
80
  "publicIp": "54.179.226.154",
@@ -94,6 +94,12 @@ 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
+
97
103
  ## Resilience
98
104
 
99
105
  Collection and transmission are independent loops:
@@ -119,7 +125,7 @@ The Agent Setup page generates this command from a masked password field. The `f
119
125
  ## NPM Usage
120
126
 
121
127
  ```bash
122
- npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter node -- start && pm2 save
128
+ npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter none -- start && pm2 save
123
129
  ```
124
130
 
125
131
  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.4";
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;
@@ -1551,12 +1551,111 @@ async function acceptApiNodePayment(runtime, params) {
1551
1551
  const depositTxHash = textValue(params.depositTxHash);
1552
1552
  const deposit = depositTxHash ? await waitForCkbTransaction(config, depositTxHash, params) : undefined;
1553
1553
  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
- });
1554
+ const peerAddress = firstTextValue(params.peerAddress, params.receiverPeerAddress, params.address);
1555
+ const provisionChannel = boolValue(params.provisionChannel, true);
1556
+ const closeChannelAfterPayment = provisionChannel && boolValue(params.closeChannelAfterPayment, false);
1557
+ let existingChannel;
1558
+ let routeProbe;
1559
+ try {
1560
+ existingChannel = await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs).catch(() => undefined);
1561
+ try {
1562
+ routeProbe = await simulateFiberRoute(config, {
1563
+ destinationNode: targetPubkey,
1564
+ amountShannons: textValue(params.amountShannons ?? params.amount),
1565
+ assetType: textValue(params.assetType ?? params.asset) || "CKB",
1566
+ });
1567
+ } catch (error) {
1568
+ routeProbe = { status: "failed", error: message(error) };
1569
+ }
1570
+ } catch (error) {
1571
+ routeProbe = { status: "failed", error: message(error) };
1572
+ }
1573
+ const existingRouteReady = routeProbe?.status === "success" || Boolean(existingChannel);
1574
+ let channelOpen;
1575
+ let paymentChannel = existingChannel;
1576
+ let channelBeforePayment;
1577
+ let fiberPayment;
1578
+ try {
1579
+ if (!existingRouteReady) {
1580
+ if (!provisionChannel) {
1581
+ throw new Error(`No viable Fiber route to ${shortText(targetPubkey, 10, 8)} and direct channel provisioning is disabled.`);
1582
+ }
1583
+ if (!peerAddress) {
1584
+ throw new Error(`No viable Fiber route to ${shortText(targetPubkey, 10, 8)} and the receiving node has no public peer address for automatic connection.`);
1585
+ }
1586
+ channelOpen = await openFiberChannel(config, {
1587
+ peerPubkey: targetPubkey,
1588
+ peerAddress,
1589
+ fundingAmount: firstTextValue(params.channelFundingAmount, params.totalDebit, params.amountShannons, params.amount),
1590
+ connectFirst: true,
1591
+ waitUntilReady: true,
1592
+ waitTimeoutMs: positiveNumber(params.channelReadyTimeoutMs, 10 * 60_000),
1593
+ pollMs: positiveNumber(params.channelPollMs, 5_000),
1594
+ public: true,
1595
+ });
1596
+ paymentChannel = channelOpen.channel ?? await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs);
1597
+ }
1598
+ channelBeforePayment = channelLiquiditySnapshot(paymentChannel);
1599
+ fiberPayment = await sendFiberPayment(config, {
1600
+ ...params,
1601
+ targetPubkey,
1602
+ amountShannons: textValue(params.amountShannons ?? params.amount),
1603
+ keysend: true,
1604
+ });
1605
+ } catch (error) {
1606
+ let refund;
1607
+ let refundError;
1608
+ try {
1609
+ const openedChannelId = channelIdOf(paymentChannel);
1610
+ if (channelOpen && openedChannelId) {
1611
+ await settleFiberChannel(runtime, { channelId: openedChannelId, force: false, feeRate: "1000" });
1612
+ await waitForChannelClosed(config.fiberRpcUrl, openedChannelId, 10 * 60_000, 5_000);
1613
+ }
1614
+ refund = await executeProviderWalletPayout(config, {
1615
+ receiverWallet: textValue(params.senderWallet),
1616
+ amount: textValue(params.totalDebit ?? params.depositAmount),
1617
+ paymentId: textValue(params.paymentId),
1618
+ depositTxHash,
1619
+ });
1620
+ } catch (failure) {
1621
+ refundError = message(failure);
1622
+ }
1623
+ const refundDetail = refund?.txHash
1624
+ ? ` Refund broadcast to the sender: ${refund.txHash}.`
1625
+ : ` Automatic refund also failed: ${refundError ?? "unknown refund error"}.`;
1626
+ throw new Error(`Fiber node delivery failed after deposit confirmation: ${message(error)}.${refundDetail}`);
1627
+ }
1628
+ const paymentChannelId = channelIdOf(paymentChannel);
1629
+ const channelAfterPaymentRaw = paymentChannelId
1630
+ ? await waitForChannelBalanceChange(
1631
+ config.fiberRpcUrl,
1632
+ paymentChannelId,
1633
+ channelBeforePayment,
1634
+ positiveNumber(params.channelBalanceTimeoutMs, 60_000),
1635
+ positiveNumber(params.channelBalancePollMs, 2_000),
1636
+ ).catch(() => undefined)
1637
+ : await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs).catch(() => undefined);
1638
+ const channelAfterPayment = channelLiquiditySnapshot(channelAfterPaymentRaw);
1639
+ let channelClose;
1640
+ let channelClosed;
1641
+ if (channelOpen && closeChannelAfterPayment && paymentChannelId) {
1642
+ try {
1643
+ channelClose = await settleFiberChannel(runtime, {
1644
+ channelId: paymentChannelId,
1645
+ force: false,
1646
+ feeRate: params.channelCloseFeeRate ?? params.feeRate ?? "1000",
1647
+ });
1648
+ const closed = await waitForChannelClosed(
1649
+ config.fiberRpcUrl,
1650
+ paymentChannelId,
1651
+ positiveNumber(params.channelCloseTimeoutMs, 10 * 60_000),
1652
+ positiveNumber(params.channelPollMs, 5_000),
1653
+ );
1654
+ channelClosed = channelLiquiditySnapshot(closed);
1655
+ } catch (error) {
1656
+ channelClose = { status: "failed", error: message(error) };
1657
+ }
1658
+ }
1560
1659
  let refreshed;
1561
1660
  if (typeof runtime.collect === "function" && typeof runtime.push === "function") {
1562
1661
  refreshed = await pushWithRegistrationRetry(runtime, await runtime.collect(), {
@@ -1588,10 +1687,29 @@ async function acceptApiNodePayment(runtime, params) {
1588
1687
  depositAmount: textValue(params.depositAmount) || undefined,
1589
1688
  deposit,
1590
1689
  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 },
1695
+ routeProbe,
1696
+ channelOpen,
1697
+ channelId: paymentChannelId || undefined,
1698
+ providerBeforePayment: channelBeforePayment?.provider,
1699
+ receiverBeforePayment: channelBeforePayment?.receiver,
1700
+ providerAfterPayment: channelAfterPayment?.provider,
1701
+ receiverAfterPayment: channelAfterPayment?.receiver,
1702
+ channelClose,
1703
+ providerAtClose: channelClosed?.provider,
1704
+ receiverAtClose: channelClosed?.receiver,
1705
+ finalChannelState: channelClosed?.state ?? channelAfterPayment?.state,
1706
+ },
1591
1707
  fundingRefresh: refreshed,
1592
1708
  routeExecution: {
1593
1709
  status: "fiber_send_payment",
1594
- reason: "Fiber node recipients are channel endpoints, so the provider uses outbound Fiber liquidity after confirming the payer deposit.",
1710
+ 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.",
1595
1713
  },
1596
1714
  node: {
1597
1715
  pubkey: normalizePeerPubkey(readNodeId(info)),
@@ -2050,6 +2168,78 @@ async function findChannelById(rpcUrl, channelId, timeoutMs) {
2050
2168
  return channels.find((channel) => textValue(channel?.channel_id ?? channel?.channelId ?? channel?.id).toLowerCase() === normalized);
2051
2169
  }
2052
2170
 
2171
+ async function findReadyChannelForPeer(rpcUrl, pubkey, timeoutMs) {
2172
+ const response = await rpcCall(rpcUrl, "list_channels", [{ pubkey, include_closed: true }], timeoutMs);
2173
+ const channels = Array.isArray(response?.channels) ? response.channels : Array.isArray(response) ? response : [];
2174
+ return channels.find((channel) => {
2175
+ const state = channelStateName(channel).toLowerCase();
2176
+ return state === "channelready" || state === "open";
2177
+ });
2178
+ }
2179
+
2180
+ async function waitForChannelClosed(rpcUrl, channelId, timeoutMs, pollMs) {
2181
+ const deadline = Date.now() + timeoutMs;
2182
+ let lastChannel;
2183
+ while (Date.now() < deadline) {
2184
+ lastChannel = await findChannelById(rpcUrl, channelId, Math.min(pollMs, 10_000)).catch(() => lastChannel);
2185
+ const state = channelStateName(lastChannel).toLowerCase();
2186
+ if (["closed", "abandoned"].includes(state)) return lastChannel;
2187
+ await sleep(pollMs);
2188
+ }
2189
+ return {
2190
+ ...asObject(lastChannel),
2191
+ channel_id: channelId,
2192
+ close_wait_status: "timeout",
2193
+ close_wait_timeout_ms: timeoutMs,
2194
+ };
2195
+ }
2196
+
2197
+ async function waitForChannelBalanceChange(rpcUrl, channelId, before, timeoutMs, pollMs) {
2198
+ const deadline = Date.now() + timeoutMs;
2199
+ let lastChannel;
2200
+ while (Date.now() < deadline) {
2201
+ lastChannel = await findChannelById(rpcUrl, channelId, Math.min(pollMs, 10_000)).catch(() => lastChannel);
2202
+ const current = channelLiquiditySnapshot(lastChannel);
2203
+ if (!before || channelBalanceChanged(before, current)) return lastChannel;
2204
+ await sleep(pollMs);
2205
+ }
2206
+ return lastChannel;
2207
+ }
2208
+
2209
+ function channelBalanceChanged(before, after) {
2210
+ if (!before || !after) return false;
2211
+ return before.provider.localBalance !== after.provider.localBalance
2212
+ || before.provider.remoteBalance !== after.provider.remoteBalance;
2213
+ }
2214
+
2215
+ function channelIdOf(channel) {
2216
+ return firstTextValue(channel?.channel_id, channel?.channelId, channel?.id);
2217
+ }
2218
+
2219
+ function channelLiquiditySnapshot(channel) {
2220
+ if (!channel || typeof channel !== "object") return undefined;
2221
+ const localBalance = amountString(channel.local_balance ?? channel.localBalance ?? channel.outbound_liquidity ?? channel.outboundLiquidity);
2222
+ const remoteBalance = amountString(channel.remote_balance ?? channel.remoteBalance ?? channel.inbound_liquidity ?? channel.inboundLiquidity);
2223
+ const capacity = amountString(channel.funding_amount ?? channel.fundingAmount ?? channel.capacity);
2224
+ return {
2225
+ channelId: channelIdOf(channel) || undefined,
2226
+ state: channelStateName(channel),
2227
+ capacity,
2228
+ provider: {
2229
+ localBalance,
2230
+ outbound: localBalance,
2231
+ remoteBalance,
2232
+ inbound: remoteBalance,
2233
+ },
2234
+ receiver: {
2235
+ localBalance: remoteBalance,
2236
+ outbound: remoteBalance,
2237
+ remoteBalance: localBalance,
2238
+ inbound: localBalance,
2239
+ },
2240
+ };
2241
+ }
2242
+
2053
2243
  async function assertNotLocalPeer(config, pubkey) {
2054
2244
  try {
2055
2245
  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.4",
4
4
  "type": "module",
5
5
  "description": "Fibscope Agent for Fiber node owners.",
6
6
  "bin": {