@fibscope/agent 0.1.5 → 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 +2 -2
  2. package/index.mjs +61 -10
  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.5",
77
+ "agentVersion": "0.1.6",
78
78
  "chain": "testnet",
79
79
  "collectedAt": 1710000000000,
80
80
  "publicIp": "54.179.226.154",
@@ -102,7 +102,7 @@ The command result includes provider and receiver balance views before payment,
102
102
 
103
103
  ## Owner N2N Test Lifecycle
104
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.
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
106
 
107
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
108
 
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.5";
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;
@@ -1599,11 +1599,14 @@ async function acceptApiNodePayment(runtime, params, options = {}) {
1599
1599
  peerPubkey: targetPubkey,
1600
1600
  peerAddress,
1601
1601
  fundingAmount: firstTextValue(params.channelFundingAmount, params.totalDebit, params.amountShannons, params.amount),
1602
+ oneWay: ownerFunded ? true : params.oneWay,
1602
1603
  connectFirst: true,
1603
1604
  waitUntilReady: true,
1604
1605
  waitTimeoutMs: positiveNumber(params.channelReadyTimeoutMs, 10 * 60_000),
1605
1606
  pollMs: positiveNumber(params.channelPollMs, 5_000),
1606
- 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,
1607
1610
  });
1608
1611
  paymentChannel = channelOpen.channel ?? await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs);
1609
1612
  }
@@ -1616,14 +1619,28 @@ async function acceptApiNodePayment(runtime, params, options = {}) {
1616
1619
  });
1617
1620
  } catch (error) {
1618
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;
1619
1624
  if (ownerFunded) {
1620
1625
  let recovery;
1621
1626
  let recoveryError;
1622
1627
  try {
1623
1628
  const openedChannelId = channelIdOf(paymentChannel);
1624
1629
  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);
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
+ }
1627
1644
  }
1628
1645
  } catch (failure) {
1629
1646
  recoveryError = message(failure);
@@ -1791,12 +1808,17 @@ async function acceptApiNodePayment(runtime, params, options = {}) {
1791
1808
  export function nodePaymentLifecycle(input) {
1792
1809
  const channelOpen = input.channelOpen && typeof input.channelOpen === "object" ? input.channelOpen : undefined;
1793
1810
  const connectResult = channelOpen?.connectResult;
1794
- const channelId = input.paymentChannelId || channelIdOf(input.paymentChannel) || channelIdOf(channelOpen?.channel);
1811
+ const observedChannel = input.paymentChannel ?? channelOpen?.channel;
1812
+ const channelId = input.paymentChannelId || channelIdOf(observedChannel);
1813
+ const observedChannelState = channelStateName(observedChannel);
1795
1814
  const existingRoute = Boolean(input.existingChannel) || input.routeProbe?.status === "success";
1796
1815
  const peerStatus = existingRoute ? "skipped" : connectResult ? "completed" : input.failure ? "failed" : "pending";
1797
- const channelStatus = existingRoute ? "skipped" : channelId || channelOpen?.openResult ? "completed" : input.failure ? "failed" : "pending";
1816
+ const channelStatus = existingRoute ? "skipped" : isChannelReadyState(observedChannelState) ? "completed" : input.failure ? "failed" : "pending";
1798
1817
  const paymentStatus = input.fiberPayment ? "completed" : input.failure ? "failed" : "pending";
1799
- const closeStatus = !input.closeChannelAfterPayment
1818
+ const failedChannelAbandoned = input.recovery?.action === "ABANDON_CHANNEL" && input.recovery?.ok !== false;
1819
+ const closeStatus = failedChannelAbandoned
1820
+ ? "completed"
1821
+ : !input.closeChannelAfterPayment
1800
1822
  ? "skipped"
1801
1823
  : input.channelClosed ? "completed"
1802
1824
  : input.channelClose?.status === "failed" || input.failure ? "failed" : "pending";
@@ -1835,9 +1857,13 @@ export function nodePaymentLifecycle(input) {
1835
1857
  steps: [
1836
1858
  { key: "source_selected", label: "Source node selected", status: "completed", detail: input.source?.displayName || shortText(input.source?.pubkey, 10, 8) },
1837
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 },
1838
- { key: "channel_open", label: "Channel ready", status: channelStatus, detail: existingRoute ? "Existing Fiber liquidity selected." : channelId ? `Channel ${shortText(channelId, 10, 8)}` : input.failure },
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 },
1839
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 },
1840
- ...(input.closeChannelAfterPayment ? [{ key: "channel_close", label: "Provisioned channel closed", status: closeStatus, detail: input.channelClose?.error ?? input.channelClosed?.state } ] : []),
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
+ : []),
1841
1867
  ],
1842
1868
  });
1843
1869
  }
@@ -2201,6 +2227,9 @@ async function openFiberChannel(config, params) {
2201
2227
  public: boolValue(params.public ?? params.publicChannel, true),
2202
2228
  };
2203
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
+ }
2204
2233
  if (params.fundingUdtTypeScript ?? params.funding_udt_type_script) openRequest.funding_udt_type_script = params.fundingUdtTypeScript ?? params.funding_udt_type_script;
2205
2234
  if (params.commitmentFeeRate ?? params.commitment_fee_rate) openRequest.commitment_fee_rate = quantity(params.commitmentFeeRate ?? params.commitment_fee_rate);
2206
2235
  if (params.fundingFeeRate ?? params.funding_fee_rate) openRequest.funding_fee_rate = quantity(params.fundingFeeRate ?? params.funding_fee_rate);
@@ -2234,6 +2263,7 @@ async function openFiberChannel(config, params) {
2234
2263
  peerAddress: peer.address,
2235
2264
  fundingAmount: openRequest.funding_amount,
2236
2265
  public: openRequest.public,
2266
+ oneWay: openRequest.one_way === true,
2237
2267
  fundingCheck,
2238
2268
  connectResult,
2239
2269
  openResult,
@@ -2244,6 +2274,7 @@ async function openFiberChannel(config, params) {
2244
2274
  }
2245
2275
  return result;
2246
2276
  } catch (error) {
2277
+ const observed = error?.commandResult && typeof error.commandResult === "object" ? error.commandResult : undefined;
2247
2278
  throw commandResultError(message(error), {
2248
2279
  ok: false,
2249
2280
  action: "OPEN_CHANNEL",
@@ -2252,10 +2283,12 @@ async function openFiberChannel(config, params) {
2252
2283
  peerAddress: peer.address,
2253
2284
  fundingAmount: openRequest.funding_amount,
2254
2285
  public: openRequest.public,
2286
+ oneWay: openRequest.one_way === true,
2255
2287
  fundingCheck,
2256
2288
  connectResult,
2257
2289
  openResult,
2258
2290
  channelWatch,
2291
+ channel: observed?.channel,
2259
2292
  failure: message(error),
2260
2293
  });
2261
2294
  }
@@ -3159,7 +3192,25 @@ async function waitForChannel(rpcUrl, pubkey, timeoutMs, pollMs) {
3159
3192
  if (failed) throw new Error(describeChannelTerminalFailure(failed));
3160
3193
  await sleep(pollMs);
3161
3194
  }
3162
- 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";
3163
3214
  }
3164
3215
 
3165
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.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Fibscope Agent for Fiber node owners.",
6
6
  "bin": {