@fibscope/agent 0.1.3 → 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.
- package/README.md +14 -2
- package/index.mjs +389 -43
- 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.5",
|
|
78
78
|
"chain": "testnet",
|
|
79
79
|
"collectedAt": 1710000000000,
|
|
80
80
|
"publicIp": "54.179.226.154",
|
|
@@ -94,6 +94,18 @@ 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
|
+
|
|
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
|
+
|
|
97
109
|
## Resilience
|
|
98
110
|
|
|
99
111
|
Collection and transmission are independent loops:
|
|
@@ -119,7 +131,7 @@ The Agent Setup page generates this command from a masked password field. The `f
|
|
|
119
131
|
## NPM Usage
|
|
120
132
|
|
|
121
133
|
```bash
|
|
122
|
-
npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter
|
|
134
|
+
npm install -g @fibscope/agent pm2 && agent init && pm2 start "$(command -v agent)" --name fibscope-agent --interpreter none -- start && pm2 save
|
|
123
135
|
```
|
|
124
136
|
|
|
125
137
|
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.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,32 +1549,189 @@ 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
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1559
|
+
const publishedPeerAddress = firstTextValue(params.peerAddress, params.receiverPeerAddress, params.address);
|
|
1560
|
+
const peerAddress = resolveN2nPeerAddress(config, publishedPeerAddress, boolValue(params.sameHost, false));
|
|
1561
|
+
const provisionChannel = boolValue(params.provisionChannel, true);
|
|
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
|
+
};
|
|
1569
|
+
let existingChannel;
|
|
1570
|
+
let routeProbe;
|
|
1571
|
+
try {
|
|
1572
|
+
existingChannel = await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs).catch(() => undefined);
|
|
1573
|
+
try {
|
|
1574
|
+
routeProbe = await simulateFiberRoute(config, {
|
|
1575
|
+
destinationNode: targetPubkey,
|
|
1576
|
+
amountShannons: textValue(params.amountShannons ?? params.amount),
|
|
1577
|
+
assetType: textValue(params.assetType ?? params.asset) || "CKB",
|
|
1578
|
+
});
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
routeProbe = { status: "failed", error: message(error) };
|
|
1581
|
+
}
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
routeProbe = { status: "failed", error: message(error) };
|
|
1584
|
+
}
|
|
1585
|
+
const existingRouteReady = routeProbe?.status === "success" || Boolean(existingChannel);
|
|
1586
|
+
let channelOpen;
|
|
1587
|
+
let paymentChannel = existingChannel;
|
|
1588
|
+
let channelBeforePayment;
|
|
1589
|
+
let fiberPayment;
|
|
1590
|
+
try {
|
|
1591
|
+
if (!existingRouteReady) {
|
|
1592
|
+
if (!provisionChannel) {
|
|
1593
|
+
throw new Error(`No viable Fiber route to ${shortText(targetPubkey, 10, 8)} and direct channel provisioning is disabled.`);
|
|
1594
|
+
}
|
|
1595
|
+
if (!peerAddress) {
|
|
1596
|
+
throw new Error(`No viable Fiber route to ${shortText(targetPubkey, 10, 8)} and the receiving node has no public peer address for automatic connection.`);
|
|
1597
|
+
}
|
|
1598
|
+
channelOpen = await openFiberChannel(config, {
|
|
1599
|
+
peerPubkey: targetPubkey,
|
|
1600
|
+
peerAddress,
|
|
1601
|
+
fundingAmount: firstTextValue(params.channelFundingAmount, params.totalDebit, params.amountShannons, params.amount),
|
|
1602
|
+
connectFirst: true,
|
|
1603
|
+
waitUntilReady: true,
|
|
1604
|
+
waitTimeoutMs: positiveNumber(params.channelReadyTimeoutMs, 10 * 60_000),
|
|
1605
|
+
pollMs: positiveNumber(params.channelPollMs, 5_000),
|
|
1606
|
+
public: true,
|
|
1607
|
+
});
|
|
1608
|
+
paymentChannel = channelOpen.channel ?? await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs);
|
|
1609
|
+
}
|
|
1610
|
+
channelBeforePayment = channelLiquiditySnapshot(paymentChannel);
|
|
1611
|
+
fiberPayment = await sendFiberPayment(config, {
|
|
1612
|
+
...params,
|
|
1613
|
+
targetPubkey,
|
|
1614
|
+
amountShannons: textValue(params.amountShannons ?? params.amount),
|
|
1615
|
+
keysend: true,
|
|
1616
|
+
});
|
|
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
|
+
}
|
|
1665
|
+
let refund;
|
|
1666
|
+
let refundError;
|
|
1667
|
+
try {
|
|
1668
|
+
const openedChannelId = channelIdOf(paymentChannel);
|
|
1669
|
+
if (channelOpen && openedChannelId) {
|
|
1670
|
+
await settleFiberChannel(runtime, { channelId: openedChannelId, force: false, feeRate: "1000" });
|
|
1671
|
+
await waitForChannelClosed(config.fiberRpcUrl, openedChannelId, 10 * 60_000, 5_000);
|
|
1672
|
+
}
|
|
1673
|
+
refund = await executeProviderWalletPayout(config, {
|
|
1674
|
+
receiverWallet: textValue(params.senderWallet),
|
|
1675
|
+
amount: textValue(params.totalDebit ?? params.depositAmount),
|
|
1676
|
+
paymentId: textValue(params.paymentId),
|
|
1677
|
+
depositTxHash,
|
|
1678
|
+
});
|
|
1679
|
+
} catch (failure) {
|
|
1680
|
+
refundError = message(failure);
|
|
1681
|
+
}
|
|
1682
|
+
const refundDetail = refund?.txHash
|
|
1683
|
+
? ` Refund broadcast to the sender: ${refund.txHash}.`
|
|
1684
|
+
: ` Automatic refund also failed: ${refundError ?? "unknown refund error"}.`;
|
|
1685
|
+
throw new Error(`Fiber node delivery failed after deposit confirmation: ${message(error)}.${refundDetail}`);
|
|
1686
|
+
}
|
|
1687
|
+
const paymentChannelId = channelIdOf(paymentChannel);
|
|
1688
|
+
const channelAfterPaymentRaw = paymentChannelId
|
|
1689
|
+
? await waitForChannelBalanceChange(
|
|
1690
|
+
config.fiberRpcUrl,
|
|
1691
|
+
paymentChannelId,
|
|
1692
|
+
channelBeforePayment,
|
|
1693
|
+
positiveNumber(params.channelBalanceTimeoutMs, 60_000),
|
|
1694
|
+
positiveNumber(params.channelBalancePollMs, 2_000),
|
|
1695
|
+
).catch(() => undefined)
|
|
1696
|
+
: await findReadyChannelForPeer(config.fiberRpcUrl, targetPubkey, config.timeoutMs).catch(() => undefined);
|
|
1697
|
+
const channelAfterPayment = channelLiquiditySnapshot(channelAfterPaymentRaw);
|
|
1698
|
+
let channelClose;
|
|
1699
|
+
let channelClosed;
|
|
1700
|
+
if (channelOpen && closeChannelAfterPayment && paymentChannelId) {
|
|
1701
|
+
try {
|
|
1702
|
+
channelClose = await settleFiberChannel(runtime, {
|
|
1703
|
+
channelId: paymentChannelId,
|
|
1704
|
+
force: false,
|
|
1705
|
+
feeRate: params.channelCloseFeeRate ?? params.feeRate ?? "1000",
|
|
1706
|
+
});
|
|
1707
|
+
const closed = await waitForChannelClosed(
|
|
1708
|
+
config.fiberRpcUrl,
|
|
1709
|
+
paymentChannelId,
|
|
1710
|
+
positiveNumber(params.channelCloseTimeoutMs, 10 * 60_000),
|
|
1711
|
+
positiveNumber(params.channelPollMs, 5_000),
|
|
1712
|
+
);
|
|
1713
|
+
channelClosed = channelLiquiditySnapshot(closed);
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
channelClose = { status: "failed", error: message(error) };
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1560
1718
|
let refreshed;
|
|
1719
|
+
const fundingAfter = ownerFunded ? await collectFundingState(config).catch(() => undefined) : undefined;
|
|
1561
1720
|
if (typeof runtime.collect === "function" && typeof runtime.push === "function") {
|
|
1562
1721
|
refreshed = await pushWithRegistrationRetry(runtime, await runtime.collect(), {
|
|
1563
|
-
intent: "api-node-payment-completed",
|
|
1722
|
+
intent: ownerFunded ? "n2n-payment-completed" : "api-node-payment-completed",
|
|
1564
1723
|
retryOnRateLimit: true,
|
|
1565
1724
|
maxRateLimitRetries: 1,
|
|
1566
1725
|
}).catch((error) => ({ ok: false, error: message(error) }));
|
|
1567
1726
|
}
|
|
1568
1727
|
return {
|
|
1569
1728
|
ok: true,
|
|
1570
|
-
action: "API_NODE_PAYMENT",
|
|
1571
|
-
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",
|
|
1572
1731
|
paymentStatus: "completed",
|
|
1573
|
-
message:
|
|
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.",
|
|
1574
1735
|
paymentId: textValue(params.paymentId) || undefined,
|
|
1575
1736
|
quoteId: textValue(params.quoteId) || undefined,
|
|
1576
1737
|
candidateId: textValue(params.candidateId) || undefined,
|
|
@@ -1588,10 +1749,36 @@ async function acceptApiNodePayment(runtime, params) {
|
|
|
1588
1749
|
depositAmount: textValue(params.depositAmount) || undefined,
|
|
1589
1750
|
deposit,
|
|
1590
1751
|
fiberPayment,
|
|
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,
|
|
1763
|
+
routeProbe,
|
|
1764
|
+
channelOpen,
|
|
1765
|
+
paymentChannel,
|
|
1766
|
+
paymentChannelId,
|
|
1767
|
+
channelBeforePayment,
|
|
1768
|
+
channelAfterPayment,
|
|
1769
|
+
fiberPayment,
|
|
1770
|
+
channelClose,
|
|
1771
|
+
channelClosed,
|
|
1772
|
+
fundingAfter,
|
|
1773
|
+
}),
|
|
1591
1774
|
fundingRefresh: refreshed,
|
|
1592
1775
|
routeExecution: {
|
|
1593
1776
|
status: "fiber_send_payment",
|
|
1594
|
-
reason:
|
|
1777
|
+
reason: channelOpen
|
|
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.",
|
|
1595
1782
|
},
|
|
1596
1783
|
node: {
|
|
1597
1784
|
pubkey: normalizePeerPubkey(readNodeId(info)),
|
|
@@ -1601,6 +1788,77 @@ async function acceptApiNodePayment(runtime, params) {
|
|
|
1601
1788
|
};
|
|
1602
1789
|
}
|
|
1603
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
|
+
|
|
1604
1862
|
async function waitForCkbTransaction(config, txHash, params = {}) {
|
|
1605
1863
|
const rpcUrl = resolveCkbIndexerUrl(config);
|
|
1606
1864
|
if (!rpcUrl) throw new Error("No CKB RPC URL is configured for deposit confirmation.");
|
|
@@ -1954,37 +2212,53 @@ async function openFiberChannel(config, params) {
|
|
|
1954
2212
|
|
|
1955
2213
|
const fundingCheck = await assertChannelFundingAvailable(config, openRequest.funding_amount);
|
|
1956
2214
|
let connectResult;
|
|
1957
|
-
|
|
1958
|
-
if (connectFirst) {
|
|
1959
|
-
connectResult = await ensurePeerConnected(config, peer, pubkey);
|
|
1960
|
-
}
|
|
1961
|
-
const openResult = await openChannelWhenPeerReady(config, peer, pubkey, openRequest, params);
|
|
2215
|
+
let openResult;
|
|
1962
2216
|
let channelWatch;
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
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
|
+
});
|
|
1986
2261
|
}
|
|
1987
|
-
return result;
|
|
1988
2262
|
}
|
|
1989
2263
|
|
|
1990
2264
|
async function settleFiberChannel(runtime, params) {
|
|
@@ -2050,6 +2324,78 @@ async function findChannelById(rpcUrl, channelId, timeoutMs) {
|
|
|
2050
2324
|
return channels.find((channel) => textValue(channel?.channel_id ?? channel?.channelId ?? channel?.id).toLowerCase() === normalized);
|
|
2051
2325
|
}
|
|
2052
2326
|
|
|
2327
|
+
async function findReadyChannelForPeer(rpcUrl, pubkey, timeoutMs) {
|
|
2328
|
+
const response = await rpcCall(rpcUrl, "list_channels", [{ pubkey, include_closed: true }], timeoutMs);
|
|
2329
|
+
const channels = Array.isArray(response?.channels) ? response.channels : Array.isArray(response) ? response : [];
|
|
2330
|
+
return channels.find((channel) => {
|
|
2331
|
+
const state = channelStateName(channel).toLowerCase();
|
|
2332
|
+
return state === "channelready" || state === "open";
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
async function waitForChannelClosed(rpcUrl, channelId, timeoutMs, pollMs) {
|
|
2337
|
+
const deadline = Date.now() + timeoutMs;
|
|
2338
|
+
let lastChannel;
|
|
2339
|
+
while (Date.now() < deadline) {
|
|
2340
|
+
lastChannel = await findChannelById(rpcUrl, channelId, Math.min(pollMs, 10_000)).catch(() => lastChannel);
|
|
2341
|
+
const state = channelStateName(lastChannel).toLowerCase();
|
|
2342
|
+
if (["closed", "abandoned"].includes(state)) return lastChannel;
|
|
2343
|
+
await sleep(pollMs);
|
|
2344
|
+
}
|
|
2345
|
+
return {
|
|
2346
|
+
...asObject(lastChannel),
|
|
2347
|
+
channel_id: channelId,
|
|
2348
|
+
close_wait_status: "timeout",
|
|
2349
|
+
close_wait_timeout_ms: timeoutMs,
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
async function waitForChannelBalanceChange(rpcUrl, channelId, before, timeoutMs, pollMs) {
|
|
2354
|
+
const deadline = Date.now() + timeoutMs;
|
|
2355
|
+
let lastChannel;
|
|
2356
|
+
while (Date.now() < deadline) {
|
|
2357
|
+
lastChannel = await findChannelById(rpcUrl, channelId, Math.min(pollMs, 10_000)).catch(() => lastChannel);
|
|
2358
|
+
const current = channelLiquiditySnapshot(lastChannel);
|
|
2359
|
+
if (!before || channelBalanceChanged(before, current)) return lastChannel;
|
|
2360
|
+
await sleep(pollMs);
|
|
2361
|
+
}
|
|
2362
|
+
return lastChannel;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function channelBalanceChanged(before, after) {
|
|
2366
|
+
if (!before || !after) return false;
|
|
2367
|
+
return before.provider.localBalance !== after.provider.localBalance
|
|
2368
|
+
|| before.provider.remoteBalance !== after.provider.remoteBalance;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function channelIdOf(channel) {
|
|
2372
|
+
return firstTextValue(channel?.channel_id, channel?.channelId, channel?.id);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function channelLiquiditySnapshot(channel) {
|
|
2376
|
+
if (!channel || typeof channel !== "object") return undefined;
|
|
2377
|
+
const localBalance = amountString(channel.local_balance ?? channel.localBalance ?? channel.outbound_liquidity ?? channel.outboundLiquidity);
|
|
2378
|
+
const remoteBalance = amountString(channel.remote_balance ?? channel.remoteBalance ?? channel.inbound_liquidity ?? channel.inboundLiquidity);
|
|
2379
|
+
const capacity = amountString(channel.funding_amount ?? channel.fundingAmount ?? channel.capacity);
|
|
2380
|
+
return {
|
|
2381
|
+
channelId: channelIdOf(channel) || undefined,
|
|
2382
|
+
state: channelStateName(channel),
|
|
2383
|
+
capacity,
|
|
2384
|
+
provider: {
|
|
2385
|
+
localBalance,
|
|
2386
|
+
outbound: localBalance,
|
|
2387
|
+
remoteBalance,
|
|
2388
|
+
inbound: remoteBalance,
|
|
2389
|
+
},
|
|
2390
|
+
receiver: {
|
|
2391
|
+
localBalance: remoteBalance,
|
|
2392
|
+
outbound: remoteBalance,
|
|
2393
|
+
remoteBalance: localBalance,
|
|
2394
|
+
inbound: localBalance,
|
|
2395
|
+
},
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2053
2399
|
async function assertNotLocalPeer(config, pubkey) {
|
|
2054
2400
|
try {
|
|
2055
2401
|
const info = await rpcCall(config.fiberRpcUrl, "node_info", [], config.timeoutMs);
|