@fibscope/agent 0.1.2 → 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/.env.example +2 -0
- package/README.md +11 -3
- package/index.mjs +231 -17
- package/lib/public-peer-address.mjs +38 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -5,3 +5,5 @@ FIBSCOPE_AGENT_CHAIN=testnet
|
|
|
5
5
|
FIBSCOPE_AGENT_FIBER_RPC_URL=http://127.0.0.1:8227
|
|
6
6
|
FIBSCOPE_AGENT_PUSH_INTERVAL_MS=15000
|
|
7
7
|
FIBSCOPE_AGENT_PUBLIC_PROFILE=false
|
|
8
|
+
# Optional override; by default the agent discovers its own public IPv4.
|
|
9
|
+
# FIBSCOPE_AGENT_PUBLIC_IP=54.179.226.154
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ The agent also supports `.fiber/agent.json`:
|
|
|
48
48
|
}
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
The
|
|
51
|
+
The agent discovers its own public IPv4 through `https://api.ipify.org`, combines it with the local FNN P2P port, and includes both the IP and resulting public multiaddr in registration and report requests. Loopback, wildcard, private, and documentation addresses are never published. Set `FIBSCOPE_AGENT_PUBLIC_IP` only when intentionally overriding automatic discovery, or `FIBSCOPE_AGENT_PUBLIC_IP_URL` to use another compatible JSON or plain-text discovery endpoint.
|
|
52
52
|
|
|
53
53
|
## Commands
|
|
54
54
|
|
|
@@ -74,9 +74,11 @@ 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
|
+
"publicIp": "54.179.226.154",
|
|
81
|
+
"peerAddress": "/ip4/54.179.226.154/tcp/8228",
|
|
80
82
|
"node": {},
|
|
81
83
|
"peers": [],
|
|
82
84
|
"channels": [],
|
|
@@ -92,6 +94,12 @@ Fibscope authenticates this with `x-agent-token`. This is an agent key for local
|
|
|
92
94
|
|
|
93
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.
|
|
94
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
|
+
|
|
95
103
|
## Resilience
|
|
96
104
|
|
|
97
105
|
Collection and transmission are independent loops:
|
|
@@ -117,7 +125,7 @@ The Agent Setup page generates this command from a masked password field. The `f
|
|
|
117
125
|
## NPM Usage
|
|
118
126
|
|
|
119
127
|
```bash
|
|
120
|
-
npm install -g @fibscope/agent
|
|
128
|
+
npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter none -- start && pm2 save
|
|
121
129
|
```
|
|
122
130
|
|
|
123
131
|
Or run without a global install:
|
package/index.mjs
CHANGED
|
@@ -31,15 +31,17 @@ import { createTelemetryCollector } from "./lib/observe/collector.mjs";
|
|
|
31
31
|
import { recommend } from "./lib/bridge/recommendation.mjs";
|
|
32
32
|
import { virtualizePayment } from "./lib/route/route.mjs";
|
|
33
33
|
import { chooseFnnInstance, describeFnnInstance, discoverFnnInstances, rpcResponds } from "./lib/fnn-discovery.mjs";
|
|
34
|
-
import { buildObservedPeerAddress,
|
|
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;
|
|
40
40
|
const MAX_BACKOFF_MS = 300_000;
|
|
41
41
|
const DEFAULT_PULSE_MODULE = new URL("./lib/pulse/index.mjs", import.meta.url).href;
|
|
42
42
|
const DEFAULT_FNN_PREFLIGHT_TIMEOUT_MS = 2_500;
|
|
43
|
+
const DEFAULT_PUBLIC_IP_DISCOVERY_URL = "https://api.ipify.org?format=json";
|
|
44
|
+
const DEFAULT_PUBLIC_IP_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
43
45
|
const DEFAULT_REPORT_URL = "https://ejchwtvdxpyojxsursmh.supabase.co/functions/v1/fibscope-gateway/agent/v1/report";
|
|
44
46
|
const AGENT_ENV_PATHS = [resolve("agent/.env"), resolve(".env")];
|
|
45
47
|
const ENV_REPORT_URL = "FIBGATE";
|
|
@@ -367,9 +369,8 @@ export async function createRuntime(options = {}) {
|
|
|
367
369
|
const statusPath = resolve(options.status ?? DEFAULT_STATUS_PATH);
|
|
368
370
|
const config = await loadConfig(configPath, options);
|
|
369
371
|
await ensureFnnPreflight(config, statusPath, options);
|
|
372
|
+
await resolveAgentPublicIdentity(config);
|
|
370
373
|
const remoteConfig = await fetchAgentConfig(config).catch(() => undefined);
|
|
371
|
-
config.observedPublicIp = remoteConfig?.observedPublicIp ?? config.observedPublicIp;
|
|
372
|
-
config.observedPeerAddress = remoteConfig?.observedPeerAddress ?? config.observedPeerAddress;
|
|
373
374
|
if (!config.nodeDisplayName && remoteConfig?.node?.displayName) {
|
|
374
375
|
config.nodeDisplayName = remoteConfig.node.displayName;
|
|
375
376
|
}
|
|
@@ -481,6 +482,8 @@ export async function collectReport(config, previousReport = undefined) {
|
|
|
481
482
|
agentVersion: AGENT_VERSION,
|
|
482
483
|
chain: config.chain ?? "testnet",
|
|
483
484
|
collectedAt,
|
|
485
|
+
publicIp: config.publicIp,
|
|
486
|
+
peerAddress: config.publicPeerAddress,
|
|
484
487
|
node: state.node,
|
|
485
488
|
peers: state.peers ?? [],
|
|
486
489
|
channels: state.channels ?? [],
|
|
@@ -509,8 +512,8 @@ async function enrichReportNode(node, config) {
|
|
|
509
512
|
...await inferPeerAddressesFromLocalFnn(config),
|
|
510
513
|
]);
|
|
511
514
|
const addresses = publishablePeerAddresses([
|
|
512
|
-
config.
|
|
513
|
-
buildObservedPeerAddress(config.
|
|
515
|
+
config.publicPeerAddress,
|
|
516
|
+
buildObservedPeerAddress(config.publicIp, [config.fnnP2pListenAddr, ...candidates]),
|
|
514
517
|
...candidates,
|
|
515
518
|
]);
|
|
516
519
|
const enriched = {
|
|
@@ -1548,12 +1551,111 @@ async function acceptApiNodePayment(runtime, params) {
|
|
|
1548
1551
|
const depositTxHash = textValue(params.depositTxHash);
|
|
1549
1552
|
const deposit = depositTxHash ? await waitForCkbTransaction(config, depositTxHash, params) : undefined;
|
|
1550
1553
|
const targetPubkey = normalizePeerPubkey(firstTextValue(params.receiverNode, params.targetPubkey, params.destinationNode, params.pubkey));
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
+
}
|
|
1557
1659
|
let refreshed;
|
|
1558
1660
|
if (typeof runtime.collect === "function" && typeof runtime.push === "function") {
|
|
1559
1661
|
refreshed = await pushWithRegistrationRetry(runtime, await runtime.collect(), {
|
|
@@ -1585,10 +1687,29 @@ async function acceptApiNodePayment(runtime, params) {
|
|
|
1585
1687
|
depositAmount: textValue(params.depositAmount) || undefined,
|
|
1586
1688
|
deposit,
|
|
1587
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
|
+
},
|
|
1588
1707
|
fundingRefresh: refreshed,
|
|
1589
1708
|
routeExecution: {
|
|
1590
1709
|
status: "fiber_send_payment",
|
|
1591
|
-
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.",
|
|
1592
1713
|
},
|
|
1593
1714
|
node: {
|
|
1594
1715
|
pubkey: normalizePeerPubkey(readNodeId(info)),
|
|
@@ -2047,6 +2168,78 @@ async function findChannelById(rpcUrl, channelId, timeoutMs) {
|
|
|
2047
2168
|
return channels.find((channel) => textValue(channel?.channel_id ?? channel?.channelId ?? channel?.id).toLowerCase() === normalized);
|
|
2048
2169
|
}
|
|
2049
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
|
+
|
|
2050
2243
|
async function assertNotLocalPeer(config, pubkey) {
|
|
2051
2244
|
try {
|
|
2052
2245
|
const info = await rpcCall(config.fiberRpcUrl, "node_info", [], config.timeoutMs);
|
|
@@ -2294,6 +2487,8 @@ async function loadConfig(configPath, options = {}) {
|
|
|
2294
2487
|
ckbIndexerUrl: process.env.FIBSCOPE_AGENT_CKB_INDEXER_URL,
|
|
2295
2488
|
walletPath: process.env.FIBSCOPE_AGENT_WALLET_PATH ?? envValue(ENV_WALLET_PATH),
|
|
2296
2489
|
peerAddress: process.env.FIBSCOPE_AGENT_PEER_ADDRESS,
|
|
2490
|
+
publicIp: process.env.FIBSCOPE_AGENT_PUBLIC_IP,
|
|
2491
|
+
publicIpDiscoveryUrl: process.env.FIBSCOPE_AGENT_PUBLIC_IP_URL,
|
|
2297
2492
|
};
|
|
2298
2493
|
label = "agent/.env";
|
|
2299
2494
|
} else if (!existsSync(configPath)) {
|
|
@@ -2315,10 +2510,6 @@ function resolveReportUrl(config = {}) {
|
|
|
2315
2510
|
|
|
2316
2511
|
async function fetchAgentConfig(config) {
|
|
2317
2512
|
const configUrl = new URL(config.configUrl ?? config.reportUrl.replace(/\/report\/?$/, "/config"));
|
|
2318
|
-
configUrl.searchParams.set("p2pPort", String(peerTcpPort([
|
|
2319
|
-
config.fnnP2pListenAddr,
|
|
2320
|
-
...configuredPeerAddresses(config),
|
|
2321
|
-
], 8228)));
|
|
2322
2513
|
const response = await fetch(configUrl, {
|
|
2323
2514
|
headers: {
|
|
2324
2515
|
"x-agent-token": config.agentToken,
|
|
@@ -2368,6 +2559,8 @@ async function registerAgentNode(config, report, remoteConfig) {
|
|
|
2368
2559
|
displayName: config.nodeDisplayName ?? remoteConfig?.node?.displayName,
|
|
2369
2560
|
publicProfileEnabled: Boolean(report.publicProfileEnabled),
|
|
2370
2561
|
overwrite: Boolean(config.overwriteNodeRegistration),
|
|
2562
|
+
publicIp: report.publicIp ?? config.publicIp,
|
|
2563
|
+
peerAddress: report.peerAddress ?? config.publicPeerAddress,
|
|
2371
2564
|
node: report.node ?? {},
|
|
2372
2565
|
}),
|
|
2373
2566
|
});
|
|
@@ -2505,6 +2698,8 @@ function normalizeConfig(config, label) {
|
|
|
2505
2698
|
ckbRpcUrl: process.env.FIBSCOPE_AGENT_CKB_RPC_URL ?? envValue(ENV_CKB_RPC_URL) ?? config.ckbRpcUrl,
|
|
2506
2699
|
ckbIndexerUrl: process.env.FIBSCOPE_AGENT_CKB_INDEXER_URL ?? config.ckbIndexerUrl,
|
|
2507
2700
|
walletPath: process.env.FIBSCOPE_AGENT_WALLET_PATH ?? envValue(ENV_WALLET_PATH) ?? config.walletPath,
|
|
2701
|
+
publicIp: process.env.FIBSCOPE_AGENT_PUBLIC_IP ?? config.publicIp,
|
|
2702
|
+
publicIpDiscoveryUrl: process.env.FIBSCOPE_AGENT_PUBLIC_IP_URL ?? config.publicIpDiscoveryUrl,
|
|
2508
2703
|
pushIntervalMs: positiveNumber(config.pushIntervalMs, DEFAULT_PUSH_INTERVAL_MS),
|
|
2509
2704
|
commandPollMs: positiveNumber(config.commandPollMs, DEFAULT_COMMAND_POLL_MS),
|
|
2510
2705
|
knownPeerReconnectMs: positiveNumber(config.knownPeerReconnectMs, DEFAULT_KNOWN_PEER_RECONNECT_MS),
|
|
@@ -2516,6 +2711,25 @@ function normalizeConfig(config, label) {
|
|
|
2516
2711
|
return normalized;
|
|
2517
2712
|
}
|
|
2518
2713
|
|
|
2714
|
+
async function resolveAgentPublicIdentity(config) {
|
|
2715
|
+
try {
|
|
2716
|
+
const publicIp = await discoverPublicIpv4({
|
|
2717
|
+
publicIp: config.publicIp,
|
|
2718
|
+
endpoint: config.publicIpDiscoveryUrl ?? DEFAULT_PUBLIC_IP_DISCOVERY_URL,
|
|
2719
|
+
timeoutMs: config.publicIpDiscoveryTimeoutMs ?? DEFAULT_PUBLIC_IP_DISCOVERY_TIMEOUT_MS,
|
|
2720
|
+
});
|
|
2721
|
+
config.publicIp = publicIp;
|
|
2722
|
+
config.publicPeerAddress = buildObservedPeerAddress(publicIp, [
|
|
2723
|
+
config.fnnP2pListenAddr,
|
|
2724
|
+
...configuredPeerAddresses(config),
|
|
2725
|
+
]);
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
console.warn(`Fibscope agent public IP unavailable: ${message(error)}`);
|
|
2728
|
+
config.publicIp = undefined;
|
|
2729
|
+
config.publicPeerAddress = publishablePeerAddresses(configuredPeerAddresses(config))[0];
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2519
2733
|
async function bindAgentConfigToFnn(config, label, options = {}) {
|
|
2520
2734
|
const next = { ...config };
|
|
2521
2735
|
if (options.fiberRpcUrl) {
|
|
@@ -5,6 +5,39 @@ export function buildObservedPeerAddress(observedPublicIp, candidates = [], fall
|
|
|
5
5
|
return `/ip4/${ip}/tcp/${port}`;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export async function discoverPublicIpv4(options = {}) {
|
|
9
|
+
const configured = normalizePublicIpv4(options.publicIp);
|
|
10
|
+
if (configured) return configured;
|
|
11
|
+
|
|
12
|
+
const endpoint = String(options.endpoint ?? "https://api.ipify.org?format=json").trim();
|
|
13
|
+
if (!endpoint) throw new Error("Public IPv4 discovery endpoint is not configured.");
|
|
14
|
+
const timeoutMs = positiveTimeout(options.timeoutMs, 5_000);
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
const response = await (options.fetchImpl ?? fetch)(endpoint, {
|
|
19
|
+
headers: { accept: "application/json, text/plain;q=0.9" },
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
23
|
+
const body = await response.text();
|
|
24
|
+
let candidate = body;
|
|
25
|
+
try {
|
|
26
|
+
candidate = JSON.parse(body)?.ip ?? body;
|
|
27
|
+
} catch {
|
|
28
|
+
// Configurable plain-text IP endpoints are supported too.
|
|
29
|
+
}
|
|
30
|
+
const publicIp = normalizePublicIpv4(candidate);
|
|
31
|
+
if (!publicIp) throw new Error("response did not contain a public IPv4 address");
|
|
32
|
+
return publicIp;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
35
|
+
throw new Error(`Public IPv4 discovery failed at ${endpoint}: ${detail}`);
|
|
36
|
+
} finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
8
41
|
export function peerTcpPort(values, fallback = 8228) {
|
|
9
42
|
for (const value of flatStrings(values)) {
|
|
10
43
|
const match = value.match(/\/tcp\/(\d{1,5})(?:\/|$)/i);
|
|
@@ -52,3 +85,8 @@ function flatStrings(values) {
|
|
|
52
85
|
const input = Array.isArray(values) ? values.flat(Infinity) : [values];
|
|
53
86
|
return input.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
54
87
|
}
|
|
88
|
+
|
|
89
|
+
function positiveTimeout(value, fallback) {
|
|
90
|
+
const number = Number(value);
|
|
91
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
92
|
+
}
|