@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.
- package/README.md +8 -2
- package/index.mjs +198 -8
- 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.
|
|
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
|
|
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.
|
|
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
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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:
|
|
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);
|