diff --git a/.changeset/unlucky-tools-deliver.md b/.changeset/unlucky-tools-deliver.md new file mode 100644 index 00000000..6845fc09 --- /dev/null +++ b/.changeset/unlucky-tools-deliver.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/ccc-playground": minor +"@ckb-ccc/fiber": patch +--- + +Wrap fiber RPCs as fiber-sdk into CCC, with playable presentation diff --git a/.env b/.env new file mode 100644 index 00000000..34dc658f --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +process.env.PRIVATE_KEY = + "0x0000000000000000000000000000000000000000000000000000000000000000"; diff --git a/packages/demo/next.config.mjs b/packages/demo/next.config.mjs index c3f0428b..ee6cd32a 100644 --- a/packages/demo/next.config.mjs +++ b/packages/demo/next.config.mjs @@ -3,6 +3,14 @@ const nextConfig = { experimental: { optimizePackageImports: ["@ckb-ccc/core", "@ckb-ccc/core/bundle"], }, + async rewrites() { + return [ + { + source: "/api/fiber/:path*", + destination: "http://127.0.0.1:8227/:path*", + }, + ]; + }, }; export default nextConfig; diff --git a/packages/demo/package.json b/packages/demo/package.json index 47c0d551..7bd58a75 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -27,6 +27,7 @@ "@ckb-ccc/lumos-patches": "workspace:*", "@ckb-ccc/ssri": "workspace:*", "@ckb-ccc/udt": "workspace:*", + "@ckb-ccc/fiber": "workspace:*", "@ckb-lumos/ckb-indexer": "^0.24.0-next.1", "@ckb-lumos/common-scripts": "^0.24.0-next.1", "@ckb-lumos/config-manager": "^0.24.0-next.1", diff --git a/packages/demo/src/app/fiber/channel/page.tsx b/packages/demo/src/app/fiber/channel/page.tsx new file mode 100644 index 00000000..9ea2bfa9 --- /dev/null +++ b/packages/demo/src/app/fiber/channel/page.tsx @@ -0,0 +1,532 @@ +"use client"; + +import { useEffect, useState, useCallback, useRef } from "react"; +import { Button } from "@/src/components/Button"; +import { TextInput } from "@/src/components/Input"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { useFiber } from "../context/FiberContext"; +import { hexToDecimal, decimalToHex } from '@/src/utils/hex'; +import { shannonToCKB } from "../utils/numbers" + +interface ChannelState { + fundingAmount: string; + feeRate: string; + tlcExpiryDelta: string; + tlcMinValue: string; + tlcFeeProportionalMillionths: string; + isPublic: boolean; + isEnabled: boolean; + forceClose: boolean; +} + +interface OpenChannelForm { + peerAddress: string; + fundingAmount: string; + isPublic: boolean; +} + +export default function Channel() { + const { fiber } = useFiber(); + const [nodeInfo, setNodeInfo] = useState(null); + const [channels, setChannels] = useState([]); + const [peers, setPeers] = useState([]); + const [peerAddress, setPeerAddress] = useState(""); + const [channelStates, setChannelStates] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const channelStatesRef = useRef(channelStates); + const initialized = useRef(false); + const [openChannelForm, setOpenChannelForm] = useState({ + peerAddress: + "/ip4/18.162.235.225/tcp/8119/p2p/QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo", + fundingAmount: "50000000000", + isPublic: true, + }); + + // 更新 ref 当 channelStates 改变时 + useEffect(() => { + channelStatesRef.current = channelStates; + }, [channelStates]); + + const listChannels = useCallback(async () => { + if (!fiber) return; + setIsLoading(true); + try { + const channelList = await fiber.listChannels(); + setChannels(channelList); + + // 只在需要时更新 channelStates + const newChannelStates: Record = {}; + let hasChanges = false; + + channelList.forEach((channel: any) => { + const existingState = channelStatesRef.current[channel.channel_id]; + if (!existingState) { + hasChanges = true; + newChannelStates[channel.channel_id] = { + fundingAmount: channel.funding_amount || "0xba43b7400", + feeRate: channel.fee_rate || "0x3FC", + tlcExpiryDelta: "0x100", + tlcMinValue: "0x0", + tlcFeeProportionalMillionths: "0x0", + isPublic: true, + isEnabled: true, + forceClose: false, + }; + } else { + newChannelStates[channel.channel_id] = existingState; + } + }); + + if (hasChanges) { + setChannelStates(newChannelStates); + } + } catch (error) { + console.error("Failed to list channels:", error); + } finally { + setIsLoading(false); + } + }, [fiber]); // 只依赖 fiber + + const updateChannel = async (channelId: string) => { + if (!fiber || !channelId) return; + const state = channelStates[channelId]; + if (!state) return; + try { + // 首先检查通道是否存在 + const channel = channels.find((c) => c.channel_id === channelId); + if (!channel) { + console.error("Channel not found:", channelId); + alert("通道不存在或已被关闭"); + return; + } + + // 检查通道状态是否允许更新 + if (channel.state.state_name !== "Normal") { + console.error( + "Channel is not in normal state:", + channel.state.state_name, + ); + alert(`通道状态为 ${channel.state.state_name},无法更新`); + return; + } + + await fiber.channel.updateChannel({ + channel_id: channelId, + enabled: state.isEnabled, + tlc_expiry_delta: BigInt(state.tlcExpiryDelta), + tlc_minimum_value: BigInt(state.tlcMinValue), + tlc_fee_proportional_millionths: BigInt( + state.tlcFeeProportionalMillionths, + ), + }); + console.log("Channel updated successfully"); + // Refresh channel list + await listChannels(); + } catch (error) { + console.error("Failed to update channel:", error); + if (error instanceof Error) { + alert(`更新通道失败: ${error.message}`); + } else { + alert("更新通道失败,请检查通道状态"); + } + } + }; + + const abandonChannel = async (channelId: string) => { + if (!fiber || !channelId) return; + try { + await fiber.abandonChannel(channelId); + console.log("Channel abandoned successfully"); + // Refresh channel list + await listChannels(); + } catch (error) { + console.error("Failed to abandon channel:", error); + } + }; + + const shutdownChannel = async (channelId: string) => { + if (!fiber || !channelId) return; + const state = channelStates[channelId]; + if (!state) return; + try { + // 确保channelId是有效的十六进制字符串 + if (!channelId.startsWith('0x')) { + channelId = '0x' + channelId; + } + + const params = { + channel_id: channelId, + close_script: { + code_hash: + "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + hash_type: "type", + args: "0xcc015401df73a3287d8b2b19f0cc23572ac8b14d" + }, + force: state.forceClose, + fee_rate: state.feeRate, + }; + + console.log("Shutdown channel params:", JSON.stringify(params, null, 2)); + console.log("Fee rate type:", typeof state.feeRate); + console.log("Fee rate value:", state.feeRate); + + await fiber.channel.shutdownChannel(params); + console.log("Channel shutdown initiated successfully"); + // Refresh channel list + await listChannels(); + } catch (error) { + console.error("Failed to shutdown channel:", error); + if (error instanceof Error) { + alert(`关闭通道失败: ${error.message}`); + } else { + alert("关闭通道失败,请检查通道状态"); + } + } + }; + + + const handleOpenChannel = async () => { + if (!fiber) return; + try { + // 先连接 peer + await fiber.connectPeer(openChannelForm.peerAddress); + console.log("Peer connected successfully"); + + // 然后创建通道 + const peerId = openChannelForm.peerAddress.split("/p2p/")[1]; + await fiber.channel.openChannel({ + peer_id: peerId, + funding_amount: openChannelForm.fundingAmount, + public: openChannelForm.isPublic, + }); + console.log("Channel opened successfully"); + // 刷新通道列表 + await listChannels(); + } catch (error) { + console.error("Failed to open channel:", error); + if (error instanceof Error) { + alert(`创建通道失败: ${error.message}`); + } else { + alert("创建通道失败,请检查网络连接"); + } + } + }; + + useEffect(() => { + if (fiber && !initialized.current) { + initialized.current = true; + listChannels(); + } + }, [fiber, listChannels]); + + return ( +
+
+

通道管理

+ +
+ +
+

创建新通道

+
+ + setOpenChannelForm((prev) => ({ + ...prev, + peerAddress: value, + })), + ]} + placeholder="输入 peer 地址" + /> + + setOpenChannelForm((prev) => ({ + ...prev, + fundingAmount: value, + })), + ]} + placeholder="输入资金数量(单位:CKB)" + type="number" + /> +
+ +
+ +
+
+ + {channels.length > 0 && ( +
+

Channel List

+
+ {channels.map((channel, index) => ( +
+
+

+ Channel ID:{" "} + {channel.channel_id} +

+

+ Peer ID:{" "} + {channel.peer_id} +

+

+ State:{" "} + {channel.state.state_name} +

+

+ Local Balance:{" "} + {hexToDecimal(channel.local_balance).toString()} +

+

+ Remote Balance:{" "} + {hexToDecimal(channel.remote_balance).toString()} +

+
+
+
+ { + const newState = { + ...channelStates[channel.channel_id], + fundingAmount: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="50000000000" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + feeRate: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="1020" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + tlcExpiryDelta: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="256" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + tlcMinValue: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="0" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + tlcFeeProportionalMillionths: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="0" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + isPublic: e.target.checked, + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }} + className="mr-2" + /> + +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + isEnabled: e.target.checked, + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }} + className="mr-2" + /> + +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + forceClose: e.target.checked, + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }} + className="mr-2" + /> + +
+
+ +
+ + + +
+
+ ))} +
+
+ )} + + {nodeInfo && ( + <> +
+ +
+ + )} + + + {fiber && ( + <> + + + )} + +
+ ); +} diff --git a/packages/demo/src/app/fiber/context/FiberContext.tsx b/packages/demo/src/app/fiber/context/FiberContext.tsx new file mode 100644 index 00000000..8ef9b729 --- /dev/null +++ b/packages/demo/src/app/fiber/context/FiberContext.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { createContext, useContext, ReactNode, useState, useEffect } from "react"; +import { FiberSDK } from "@ckb-ccc/fiber"; + +interface FiberContextType { + fiber: FiberSDK | null; + setFiber: (fiber: FiberSDK | null) => void; +} + +const FiberContext = createContext(null); + +export function FiberProvider({ children }: { children: ReactNode }) { + const [fiber, setFiber] = useState(() => { + // 从 localStorage 中获取存储的 fiber 配置 + if (typeof window !== 'undefined') { + const savedConfig = localStorage.getItem('fiberConfig'); + if (savedConfig) { + try { + const config = JSON.parse(savedConfig); + return new FiberSDK(config); + } catch (error) { + console.error('Failed to restore fiber from localStorage:', error); + return null; + } + } + } + return null; + }); + + // 当 fiber 更新时,保存到 localStorage + useEffect(() => { + if (fiber) { + // 保存配置到 localStorage + const config = { + endpoint: '/api/fiber', // 使用默认的 endpoint + timeout: 5000 // 使用默认的 timeout + }; + localStorage.setItem('fiberConfig', JSON.stringify(config)); + } else { + localStorage.removeItem('fiberConfig'); + } + }, [fiber]); + + return ( + + {children} + + ); +} + +export function useFiber() { + const context = useContext(FiberContext); + if (!context) { + throw new Error("useFiber must be used within a FiberProvider"); + } + return context; +} diff --git a/packages/demo/src/app/fiber/invoice/page.tsx b/packages/demo/src/app/fiber/invoice/page.tsx new file mode 100644 index 00000000..5cfd42ec --- /dev/null +++ b/packages/demo/src/app/fiber/invoice/page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Button } from "@/src/components/Button"; +import { TextInput } from "@/src/components/Input"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { useFiber } from "../context/FiberContext"; +import { useRouter } from "next/navigation"; + +interface Invoice { + amount: bigint; + memo: string; + invoice: string; + status: string; + created_at: bigint; +} + +export default function InvoicePage() { + const { fiber } = useFiber(); + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [amount, setAmount] = useState(""); + const [memo, setMemo] = useState(""); + const [invoices, setInvoices] = useState([]); + + const createInvoice = async () => { + if (!fiber) return; + try { + setLoading(true); + const amountBigInt = BigInt(amount); + const invoice = await fiber.invoice.newInvoice({ + amount: amountBigInt, + description: memo, + }); + alert("发票创建成功!"); + // 刷新发票列表 + await listInvoices(); + } catch (error) { + console.error("创建发票失败:", error); + alert("创建发票失败,请重试"); + } finally { + setLoading(false); + } + }; + + const listInvoices = useCallback(async () => { + if (!fiber) return; + try { + // 由于没有 listInvoices 方法,我们暂时返回空数组 + setInvoices([]); + } catch (error) { + console.error("获取发票列表失败:", error); + } + }, [fiber]); + + useEffect(() => { + if (fiber) { + listInvoices(); + } + }, [fiber, listInvoices]); + + return ( +
+
+

发票管理

+
+ +
+
+ + +
+
+ + + + + +
+

发票列表

+ {invoices.length === 0 ? ( +

暂无发票记录

+ ) : ( +
+ {invoices.map((invoice, index) => ( +
+
+
+

+ 金额: {invoice.amount.toString()} +

+

+ 备注: {invoice.memo} +

+

+ 状态: {invoice.status} +

+

+ 创建时间:{" "} + {new Date(Number(invoice.created_at)).toLocaleString()} +

+
+
+ {invoice.invoice} +
+
+
+ ))} +
+ )} + + + +
+
+ ); +} diff --git a/packages/demo/src/app/fiber/layout.tsx b/packages/demo/src/app/fiber/layout.tsx new file mode 100644 index 00000000..d13e66c8 --- /dev/null +++ b/packages/demo/src/app/fiber/layout.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { FiberProvider } from "./context/FiberContext"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/packages/demo/src/app/fiber/page.tsx b/packages/demo/src/app/fiber/page.tsx new file mode 100644 index 00000000..f780d333 --- /dev/null +++ b/packages/demo/src/app/fiber/page.tsx @@ -0,0 +1,237 @@ +"use client"; + +import { Button } from "@/src/components/Button"; +import { useEffect, useState, useCallback } from "react"; +import { TextInput } from "@/src/components/Input"; +import { useRouter } from "next/navigation"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { FiberSDK } from "@ckb-ccc/fiber"; +import { useFiber } from "./context/FiberContext"; +import { BigButton } from "@/src/components/BigButton"; +import { shannonToCKB } from "./utils/numbers" + +export default function Page() { + const router = useRouter(); + const { fiber, setFiber } = useFiber(); + const [endpoint, setEndpoint] = useState(""); + const [nodeInfo, setNodeInfo] = useState(null); + const initSdk = () => { + const newFiber = new FiberSDK({ + endpoint: endpoint || `/api/fiber`, + timeout: 5000, + }); + if (newFiber) { + console.log("Fiber SDK initialized"); + setFiber(newFiber); + } else { + console.log("Fiber SDK initialization failed"); + } + }; + + const getNodeInfo = useCallback(async () => { + if (!fiber) return; + try { + const info = await fiber.nodeInfo(); + console.log(info); + setNodeInfo(info); + } catch (error) { + console.error("Failed to get node info:", error); + } + }, [fiber]); + + useEffect(() => { + if (fiber) { + getNodeInfo(); + } + }, [fiber, getNodeInfo]); + + return ( +
+
+

Fiber

+
+ {fiber ? ( +
+ router.push("/fiber/channel")} + className={"text-yellow-500"} + > + channel + + router.push("/fiber/peer")} + className={"text-yellow-500"} + > + Peer + + router.push("/fiber/payment")} + className={"text-yellow-500"} + > + Payment + + router.push("/fiber/invoice")} + className={"text-yellow-500"} + > + Invoice + +
+ ) : ( +
+
+ +
+
+ )} + + {nodeInfo && ( +
+

Node Information

+
+ {/* 基本信息 */} +
+
+

+ Node Name:{" "} + {nodeInfo.node_name || "Not set"} +

+

+ Node ID:{" "} + {nodeInfo.node_id} +

+

+ Chain Hash:{" "} + {nodeInfo.chain_hash} +

+

+ Timestamp:{" "} + {new Date(Number(nodeInfo.timestamp)).toLocaleString()} +

+
+
+ + {/* 网络统计 */} +
+

Network Statistics

+
+

+ Channel Count:{" "} + {nodeInfo.channel_count || "0"} +

+

+ Pending Channels:{" "} + {nodeInfo.pending_channel_count || "0"} +

+

+ Connected Peers:{" "} + {nodeInfo.peers_count || "0"} +

+
+
+ + {/* 通道配置 */} +
+

Channel Configuration

+
+

+ Min CKB Funding Amount:{" "} + {shannonToCKB(nodeInfo.auto_accept_min_ckb_funding_amount) || "0"} +

+

+ + Channel CKB Funding Amount: + {" "} + {shannonToCKB(nodeInfo.auto_accept_channel_ckb_funding_amount) || "0"} +

+

+ TLC Expiry Delta:{" "} + {shannonToCKB(nodeInfo.tlc_expiry_delta) || "0"} +

+

+ TLC Min Value:{" "} + {shannonToCKB(nodeInfo.tlc_min_value) || "0"} +

+

+ + TLC Fee Proportional Millionths: + {" "} + {nodeInfo.tlc_fee_proportional_millionths + ? `${shannonToCKB(nodeInfo.tlc_fee_proportional_millionths)}` + : "0%"} +

+
+
+ + {/* 节点地址 */} +
+

Node Addresses

+
+ {nodeInfo.addresses && nodeInfo.addresses.length > 0 ? ( + nodeInfo.addresses.map((address: string, index: number) => ( +

+ {address} +

+ )) + ) : ( +

No addresses configured

+ )} +
+
+ + {/* 默认资金锁定脚本 */} + {nodeInfo.default_funding_lock_script && ( +
+

Default Funding Lock Script

+
+

+ Code Hash:{" "} + {nodeInfo.default_funding_lock_script.code_hash} +

+

+ Hash Type:{" "} + {nodeInfo.default_funding_lock_script.hash_type} +

+

+ Args:{" "} + {nodeInfo.default_funding_lock_script.args} +

+
+
+ )} + + {/* UDT配置 */} + {nodeInfo.udt_cfg_infos && Object.keys(nodeInfo.udt_cfg_infos).length > 0 && ( +
+

UDT Configuration

+
+                  {JSON.stringify(nodeInfo.udt_cfg_infos, null, 2)}
+                
+
+ )} +
+
+ )} + + + + +
+ ); +} diff --git a/packages/demo/src/app/fiber/payment/page.tsx b/packages/demo/src/app/fiber/payment/page.tsx new file mode 100644 index 00000000..4056826b --- /dev/null +++ b/packages/demo/src/app/fiber/payment/page.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/src/components/Button"; +import { TextInput } from "@/src/components/Input"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; + +interface PaymentForm { + amount: string; + recipient: string; +} + +export default function PaymentPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [amount, setAmount] = useState(""); + const [recipient, setRecipient] = useState(""); + + const handlePayment = async () => { + try { + setLoading(true); + // TODO: 实现支付逻辑 + alert("支付成功!"); + router.push("/fiber"); + } catch (error) { + alert("支付失败,请重试"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

支付

+
+ +
+
+ + +
+
+ + + + + +
+ ); +} diff --git a/packages/demo/src/app/fiber/peer/[peerid]/page.tsx b/packages/demo/src/app/fiber/peer/[peerid]/page.tsx new file mode 100644 index 00000000..7f402a05 --- /dev/null +++ b/packages/demo/src/app/fiber/peer/[peerid]/page.tsx @@ -0,0 +1,429 @@ +"use client"; + +import { useEffect, useState, useCallback, useRef } from "react"; +import { useParams } from "next/navigation"; +import { Button } from "@/src/components/Button"; +import { TextInput } from "@/src/components/Input"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { useFiber } from "../../context/FiberContext"; +import { hexToDecimal, decimalToHex } from '@/src/utils/hex'; +import { shannonToCKB } from "../../utils/numbers" + +interface ChannelState { + fundingAmount: string; + feeRate: string; + tlcExpiryDelta: string; + tlcMinValue: string; + tlcFeeProportionalMillionths: string; + isPublic: boolean; + isEnabled: boolean; + forceClose: boolean; +} + +export default function peerDetail() { + const params = useParams(); + const peerId = params?.peerid as string; + const { fiber } = useFiber(); + const [channels, setChannels] = useState([]); + const [channelStates, setChannelStates] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const channelStatesRef = useRef(channelStates); + const initialized = useRef(false); + + // 更新 ref 当 channelStates 改变时 + useEffect(() => { + channelStatesRef.current = channelStates; + }, [channelStates]); + + const listChannels = useCallback(async () => { + if (!fiber) return; + setIsLoading(true); + try { + const channelList = await fiber.listChannels(); + const filteredChannelList = channelList.filter((channel: any) => channel.peer_id === peerId); + setChannels(filteredChannelList); + + // 只在需要时更新 channelStates + const newChannelStates: Record = {}; + let hasChanges = false; + + channelList.forEach((channel: any) => { + const existingState = channelStatesRef.current[channel.channel_id]; + if (!existingState) { + hasChanges = true; + newChannelStates[channel.channel_id] = { + fundingAmount: channel.funding_amount || "0xba43b7400", + feeRate: channel.fee_rate || "0x3FC", + tlcExpiryDelta: "0x100", + tlcMinValue: "0x0", + tlcFeeProportionalMillionths: "0x0", + isPublic: true, + isEnabled: true, + forceClose: false, + }; + } else { + newChannelStates[channel.channel_id] = existingState; + } + }); + + if (hasChanges) { + setChannelStates(newChannelStates); + } + } catch (error) { + console.error("Failed to list channels:", error); + } finally { + setIsLoading(false); + } + }, [fiber]); // 只依赖 fiber + + const updateChannel = async (channelId: string) => { + if (!fiber || !channelId) return; + const state = channelStates[channelId]; + if (!state) return; + try { + // 首先检查通道是否存在 + const channel = channels.find((c) => c.channel_id === channelId); + if (!channel) { + console.error("Channel not found:", channelId); + alert("通道不存在或已被关闭"); + return; + } + + // 检查通道状态是否允许更新 + if (channel.state.state_name !== "Normal") { + console.error( + "Channel is not in normal state:", + channel.state.state_name, + ); + alert(`通道状态为 ${channel.state.state_name},无法更新`); + return; + } + + await fiber.channel.updateChannel({ + channel_id: channelId, + enabled: state.isEnabled, + tlc_expiry_delta: BigInt(state.tlcExpiryDelta), + tlc_minimum_value: BigInt(state.tlcMinValue), + tlc_fee_proportional_millionths: BigInt( + state.tlcFeeProportionalMillionths, + ), + }); + console.log("Channel updated successfully"); + // Refresh channel list + await listChannels(); + } catch (error) { + console.error("Failed to update channel:", error); + if (error instanceof Error) { + alert(`更新通道失败: ${error.message}`); + } else { + alert("更新通道失败,请检查通道状态"); + } + } + }; + + const abandonChannel = async (channelId: string) => { + if (!fiber || !channelId) return; + try { + await fiber.abandonChannel(channelId); + console.log("Channel abandoned successfully"); + // Refresh channel list + await listChannels(); + } catch (error) { + console.error("Failed to abandon channel:", error); + } + }; + const shutdownChannel = async (channelId: string) => { + if (!fiber || !channelId) return; + const state = channelStates[channelId]; + if (!state) return; + try { + if (state.forceClose) { + await fiber.channel.shutdownChannel({ + channel_id: channelId, + close_script: { + code_hash: + "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + hash_type: "type", + args: "0xcc015401df73a3287d8b2b19f0cc23572ac8b14d" + }, + force: true, + fee_rate: BigInt(state.feeRate), + }); + } else { + await fiber.channel.shutdownChannel({ + channel_id: channelId, + close_script: { + code_hash: + "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + hash_type: "type", + args: "0xcc015401df73a3287d8b2b19f0cc23572ac8b14d" + }, + force: false, + fee_rate: BigInt(state.feeRate), + }); + } + console.log("Channel shutdown initiated successfully"); + // Refresh channel list + await listChannels(); + } catch (error) { + console.error("Failed to shutdown channel:", error); + } + }; + useEffect(() => { + if (fiber && !initialized.current) { + initialized.current = true; + listChannels(); + } + }, [fiber, listChannels]); + + return ( +
+ <> +

Peer Info

+

{peerId}

+ + + {channels.length > 0 && ( +
+

Channel List

+
+ {channels.map((channel, index) => ( +
+
+

+ Channel ID:{" "} + {channel.channel_id} +

+

+ Peer ID:{" "} + {channel.peer_id} +

+

+ State:{" "} + {channel.state.state_name} +

+

+ Local Balance:{" "} + {shannonToCKB(hexToDecimal(channel.local_balance).toString())} +

+

+ Remote Balance:{" "} + {shannonToCKB(hexToDecimal(channel.remote_balance).toString())} +

+
+
+
+ { + const newState = { + ...channelStates[channel.channel_id], + fundingAmount: shannonToCKB(decimalToHex(parseInt(value) || 0)), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="500" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + feeRate: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="1020" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + tlcExpiryDelta: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="256" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + tlcMinValue: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="0" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + tlcFeeProportionalMillionths: decimalToHex(parseInt(value) || 0), + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }, + ]} + placeholder="0" + type="number" + /> +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + isPublic: e.target.checked, + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }} + className="mr-2" + /> + +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + isEnabled: e.target.checked, + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }} + className="mr-2" + /> + +
+
+ { + const newState = { + ...channelStates[channel.channel_id], + forceClose: e.target.checked, + }; + setChannelStates((prev) => ({ + ...prev, + [channel.channel_id]: newState, + })); + }} + className="mr-2" + /> + +
+
+ +
+ + + +
+
+ ))} +
+
+ )} + + + + + {fiber && ( + <> + + + )} + +
+ ); +} diff --git a/packages/demo/src/app/fiber/peer/page.tsx b/packages/demo/src/app/fiber/peer/page.tsx new file mode 100644 index 00000000..dc04c528 --- /dev/null +++ b/packages/demo/src/app/fiber/peer/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/src/components/Button"; +import { TextInput } from "@/src/components/Input"; +import { FiberSDK } from "@ckb-ccc/fiber"; +import { useRouter } from "next/navigation"; + +interface PeerInfo { + pubkey: string; + peer_id: string; + addresses: string[]; +} + +export default function Peer() { + const [fiber, setFiber] = useState(null); + const [peers, setPeers] = useState([]); + const [peerAddress, setPeerAddress] = useState( + "/ip4/54.215.49.61/tcp/8080/p2p/QmNXkndsz6NT6A4kuXRg4mgk5DpEP33m8vRUqe2iwouEru", + ); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + useEffect(() => { + const initFiber = () => { + const newFiber = new FiberSDK({ + endpoint: `/api/fiber`, + timeout: 5000, + }); + setFiber(newFiber); + }; + initFiber(); + }, []); + + const listPeers = async () => { + if (!fiber) return; + setIsLoading(true); + try { + const peerList = await fiber.listPeers(); + console.log(peerList); + //@ts-expect-error + setPeers(peerList.peers); + } catch (error) { + console.error("Failed to list peers:", error); + } finally { + setIsLoading(false); + } + }; + + const connectPeer = async () => { + if (!fiber || !peerAddress) return; + setIsLoading(true); + try { + await fiber.connectPeer(peerAddress); + console.log("Peer connected successfully"); + // 连接成功后立即获取并更新 peers 列表 + await listPeers(); + } catch (error) { + console.error("Failed to connect peer:", error); + if (error instanceof Error) { + alert(`连接 peer 失败: ${error.message}`); + } else { + alert("连接 peer 失败,请检查网络连接"); + } + } finally { + setIsLoading(false); + } + }; + + const disconnectPeer = async (peerId: string) => { + if (!fiber) return; + setIsLoading(true); + try { + await fiber.disconnectPeer(peerId); + console.log("Peer disconnected successfully"); + // 断开连接后立即获取并更新 peers 列表 + await listPeers(); + } catch (error) { + console.error("Failed to disconnect peer:", error); + if (error instanceof Error) { + alert(`断开连接失败: ${error.message}`); + } else { + alert("断开连接失败,请检查网络连接"); + } + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

Peer 管理

+
+ setPeerAddress(e.target.value)} + placeholder="输入 peer 地址" + state={[peerAddress, setPeerAddress]} + className="flex-1" + /> + + + +
+
+ +
+

已连接的 Peers

+ {peers.length === 0 ? ( +

暂无连接的 peers

+ ) : ( +
+ {peers.length > 0 && + peers.map((peer) => ( +
router.push(`/fiber/peer/${peer.peer_id}`)} + > +
+

Peer ID: {peer.peer_id}

+

+ Pubkey: {peer.pubkey} +

+

+ Addresses: {peer.addresses.join(", ")} +

+
+ +
+ ))} +
+ )} +
+
+ ); +} diff --git a/packages/demo/src/app/fiber/utils/numbers.ts b/packages/demo/src/app/fiber/utils/numbers.ts new file mode 100644 index 00000000..99d0c49e --- /dev/null +++ b/packages/demo/src/app/fiber/utils/numbers.ts @@ -0,0 +1,11 @@ +import { ccc } from "@ckb-ccc/connector-react"; + +const CKB_UNIT = BigInt(100000000); // 1 CKB = 10^8 shannon + +export const shannonToCKB = (shannon: string) => { + if (!shannon) return "0"; + // 将浮点数转换为整数(shannon) + const shannonValue = Math.floor(parseFloat(shannon) * 100000000); + const shannonBigInt = BigInt(shannonValue); + return ccc.fixedPointToString(shannonBigInt / CKB_UNIT); +}; \ No newline at end of file diff --git a/packages/demo/src/app/page.tsx b/packages/demo/src/app/page.tsx index c20210d6..78309344 100644 --- a/packages/demo/src/app/page.tsx +++ b/packages/demo/src/app/page.tsx @@ -3,7 +3,6 @@ import { ccc } from "@ckb-ccc/connector-react"; import React, { useEffect } from "react"; -import { Key, Wallet } from "lucide-react"; import { BigButton } from "@/src/components/BigButton"; import { useRouter } from "next/navigation"; import { useApp } from "@/src/context"; @@ -38,6 +37,13 @@ export default function Home() { > Private Key + router.push("/fiber")} + iconName="Key" + className="text-emerald-500" + > + Fiber + + + + + + Fiber 通道测试 + + + +
+

Fiber 通道测试

+ +
+

节点信息

+ +

+      
+ +
+

节点连接

+
+ + +
+ +
+
+ +
+

通道操作

+
+ + +
+
+ + +
+ + + +

+      
+
+ + + + + + + + + diff --git a/packages/fiber/eslint.config.mjs b/packages/fiber/eslint.config.mjs new file mode 100644 index 00000000..8dae5ed1 --- /dev/null +++ b/packages/fiber/eslint.config.mjs @@ -0,0 +1,47 @@ +// @ts-check + +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import tseslint from "typescript-eslint"; + +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +export default [ + ...tseslint.config({ + files: ["**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }], + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/require-await": "off", + "no-empty": "off", + "prefer-const": [ + "error", + { ignoreReadBeforeAssign: true, destructuring: "all" }, + ], + }, + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, + }), + eslintPluginPrettierRecommended, +]; diff --git a/packages/fiber/examples/basic-usage.html b/packages/fiber/examples/basic-usage.html new file mode 100644 index 00000000..5b7987e5 --- /dev/null +++ b/packages/fiber/examples/basic-usage.html @@ -0,0 +1,159 @@ + + + + + + Fiber 基本使用示例 + + + +
+

Fiber 基本使用示例

+ +
+

节点信息

+ +

+      
+ +
+

通道操作

+
+ + + + +
+

+      
+
+ + + + + + + + + diff --git a/packages/fiber/misc/basedirs/dist.commonjs/package.json b/packages/fiber/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/packages/fiber/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/fiber/misc/basedirs/dist/package.json b/packages/fiber/misc/basedirs/dist/package.json new file mode 100644 index 00000000..aead43de --- /dev/null +++ b/packages/fiber/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/fiber/package.json b/packages/fiber/package.json new file mode 100644 index 00000000..ebd02c53 --- /dev/null +++ b/packages/fiber/package.json @@ -0,0 +1,63 @@ +{ + "name": "@ckb-ccc/fiber", + "version": "1.0.0", + "type": "module", + "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Fiber SDK", + "author": "author: Jack ", + "license": "MIT", + "private": false, + "homepage": "https://github.com/ckb-devrel/ccc", + "repository": { + "type": "git", + "url": "git://github.com/ckb-devrel/ccc.git" + }, + "sideEffects": false, + "main": "dist.commonjs/index.js", + "module": "dist/index.js", + "browser": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist.commonjs/index.js", + "types": "./dist/index.d.ts", + "default": "./dist.commonjs/index.js" + } + }, + "scripts": { + "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json && copyfiles -u 2 misc/basedirs/**/* .", + "lint": "eslint ./src", + "format": "prettier --write . && eslint --fix ./src", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "@eslint/js": "^9.1.1", + "@types/node": "^22.10.0", + "@types/chai": "^5.2.0", + "@types/mocha": "^10.0.10", + "chai": "^5.2.0", + "mocha": "^11.1.0", + "zod": "^3.22.4", + "copyfiles": "^2.4.1", + "dotenv": "^16.4.5", + "eslint": "^9.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^3.2.4", + "rimraf": "^5.0.5", + "typescript": "^5.4.5", + "typescript-eslint": "^7.7.0", + "@types/jest": "^29.5.0", + "jest": "^29.5.0", + "ts-jest": "^29.1.0" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@ckb-ccc/core": "workspace:*", + "axios": "^1.7.7" + } +} diff --git a/packages/fiber/src/client.ts b/packages/fiber/src/client.ts new file mode 100644 index 00000000..3de5043a --- /dev/null +++ b/packages/fiber/src/client.ts @@ -0,0 +1,142 @@ +import { RequestorJsonRpc, RequestorJsonRpcConfig } from "@ckb-ccc/core"; + +export interface ClientConfig extends RequestorJsonRpcConfig { + endpoint: string; +} + +interface AcceptChannelParams { + temporary_channel_id: string; + funding_amount: bigint; + max_tlc_value_in_flight: bigint; + max_tlc_number_in_flight: bigint; + tlc_min_value: bigint; + tlc_fee_proportional_millionths: bigint; + tlc_expiry_delta: bigint; +} + +interface AcceptChannelResponse { + channel_id: string; +} + +export class RPCError extends Error { + constructor( + public error: { + code: number; + message: string; + data?: unknown; + }, + ) { + super(`[RPC Error ${error.code}] ${error.message}`); + this.name = "RPCError"; + } +} + +export class FiberClient { + private requestor: RequestorJsonRpc; + + constructor(config: ClientConfig) { + this.requestor = new RequestorJsonRpc(config.endpoint, { + timeout: config.timeout, + maxConcurrent: config.maxConcurrent, + fallbacks: config.fallbacks, + transport: config.transport, + }); + } + + private serializeBigInt(obj: unknown): unknown { + if (typeof obj === "bigint") { + const hex = obj.toString(16); + return "0x" + hex; + } + if (typeof obj === "number") { + const hex = obj.toString(16); + return "0x" + hex; + } + if (Array.isArray(obj)) { + return obj.map((item) => this.serializeBigInt(item)); + } + if (obj !== null && typeof obj === "object") { + if (Object.keys(obj).length === 0) { + return obj; + } + const result: Record = {}; + const typedObj = obj as Record; + for (const key in typedObj) { + if (key === "peer_id") { + result[key] = typedObj[key]; + } else if (key === "channel_id") { + result[key] = typedObj[key]; + } else if ( + typeof typedObj[key] === "bigint" || + typeof typedObj[key] === "number" + ) { + result[key] = "0x" + typedObj[key].toString(16); + } else { + result[key] = this.serializeBigInt(typedObj[key]); + } + } + return result; + } + return obj; + } + + async call(method: string, params: unknown[]): Promise { + if (params.length === 0 || (params.length === 1 && params[0] === null)) { + params = []; + } + + const serializedParams = params.map((param) => { + if (param === null || param === undefined) { + return {}; + } + return this.serializeBigInt(param); + }); + + try { + const result = await this.requestor.request(method, serializedParams); + if (!result && result !== null) { + throw new RPCError({ + code: -1, + message: `RPC method "${method}" failed`, + data: undefined, + }); + } + return result as T; + } catch (error) { + console.log(error); + if (error instanceof Error) { + if (error.message.includes("Method not found")) { + throw new RPCError({ + code: -32601, + message: `RPC method "${method}" not found`, + data: undefined, + }); + } + throw new RPCError({ + code: -1, + message: error.message, + data: undefined, + }); + } + throw error; + } + } + + async acceptChannel( + params: AcceptChannelParams, + ): Promise { + const transformedParams = { + temporary_channel_id: params.temporary_channel_id, + funding_amount: params.funding_amount, + max_tlc_value_in_flight: params.max_tlc_value_in_flight, + max_tlc_number_in_flight: params.max_tlc_number_in_flight, + tlc_min_value: params.tlc_min_value, + tlc_fee_proportional_millionths: params.tlc_fee_proportional_millionths, + tlc_expiry_delta: params.tlc_expiry_delta, + }; + + return this.call("accept_channel", [ + transformedParams, + ]); + } +} diff --git a/packages/fiber/src/core/types.ts b/packages/fiber/src/core/types.ts new file mode 100644 index 00000000..71db4efe --- /dev/null +++ b/packages/fiber/src/core/types.ts @@ -0,0 +1,90 @@ +import { Script } from "../types.js"; + +export type Hash256 = string; +export type Pubkey = string; + +export interface Channel { + channel_id: Hash256; + is_public: boolean; + channel_outpoint?: { + tx_hash: Hash256; + index: bigint; + }; + peer_id: string; + funding_udt_type_script?: Script; + state: string; + local_balance: bigint; + offered_tlc_balance: bigint; + remote_balance: bigint; + received_tlc_balance: bigint; + latest_commitment_transaction_hash?: Hash256; + created_at: bigint; + enabled: boolean; + tlc_expiry_delta: bigint; + tlc_fee_proportional_millionths: bigint; +} + +export interface ChannelInfo { + channel_outpoint: { + tx_hash: Hash256; + index: bigint; + }; + node1: Pubkey; + node2: Pubkey; + created_timestamp: bigint; + last_updated_timestamp_of_node1?: bigint; + last_updated_timestamp_of_node2?: bigint; + fee_rate_of_node1?: bigint; + fee_rate_of_node2?: bigint; + capacity: bigint; + chain_hash: Hash256; + udt_type_script?: Script; +} + +export interface NodeInfo { + node_name: string; + addresses: string[]; + node_id: Pubkey; + timestamp: bigint; + chain_hash: Hash256; + auto_accept_min_ckb_funding_amount: bigint; + udt_cfg_infos: Record; +} + +export interface PaymentSessionStatus { + status: "Created" | "Inflight" | "Success" | "Failed"; + payment_hash: Hash256; + created_at: bigint; + last_updated_at: bigint; + failed_error?: string; + fee: bigint; + custom_records?: Record; + router: { + node_id: string; + fee: bigint; + }; +} + +export interface CkbInvoice { + currency: "Fibb" | "Fibt" | "Fibd"; + amount?: bigint; + signature?: string; + data: Record; +} + +export interface CkbInvoiceStatus { + status: "Open" | "Cancelled" | "Expired" | "Received" | "Paid"; + invoice_address: string; + invoice: CkbInvoice; +} + +export interface RPCResponse { + jsonrpc: string; + id: string; + result?: T; + error?: { + code: number; + message: string; + data?: unknown; + }; +} diff --git a/packages/fiber/src/index.ts b/packages/fiber/src/index.ts new file mode 100644 index 00000000..5699c75d --- /dev/null +++ b/packages/fiber/src/index.ts @@ -0,0 +1,193 @@ +import { FiberClient } from "./client.js"; +import { ChannelModule } from "./modules/channel.js"; +import { InfoModule } from "./modules/info.js"; +import { InvoiceModule } from "./modules/invoice.js"; +import { PaymentModule } from "./modules/payment.js"; +import { PeerInfo, PeerModule } from "./modules/peer.js"; +import { + Channel, + CkbInvoice, + Hash256, + NodeInfo, + PaymentSessionStatus, + Script, +} from "./types.js"; + +export { FiberClient } from "./client.js"; +export { ChannelModule } from "./modules/channel.js"; +export { InfoModule } from "./modules/info.js"; +export { InvoiceModule } from "./modules/invoice.js"; +export { PaymentModule } from "./modules/payment.js"; +export { PeerModule } from "./modules/peer.js"; +export * from "./types.js"; + +export interface FiberSDKConfig { + endpoint: string; + timeout?: number; +} + +export class FiberSDK { + public channel: ChannelModule; + public payment: PaymentModule; + public invoice: InvoiceModule; + public peer: PeerModule; + public info: InfoModule; + // public graph: GraphModule; + // public dev: DevModule; + // public cch: CchModule; + + constructor(config: FiberSDKConfig) { + const client = new FiberClient({ + endpoint: config.endpoint, + timeout: config.timeout, + }); + + this.channel = new ChannelModule(client); + this.payment = new PaymentModule(client); + this.invoice = new InvoiceModule(client); + this.peer = new PeerModule(client); + this.info = new InfoModule(client); + // this.graph = new GraphModule(client); + // this.dev = new DevModule(client); + // this.cch = new CchModule(client); + } + + /** + * List all channels + */ + async listChannels(): Promise { + return this.channel.listChannels(); + } + + /** + * Get node information + */ + async nodeInfo(): Promise { + return this.info.nodeInfo(); + } + + /** + * Open channel + */ + async openChannel(params: { + peer_id: string; + funding_amount: string; + public?: boolean; + funding_udt_type_script?: Script; + shutdown_script?: Script; + commitment_delay_epoch?: string; + commitment_fee_rate?: string; + funding_fee_rate?: string; + tlc_expiry_delta?: string; + tlc_min_value?: string; + tlc_fee_proportional_millionths?: string; + max_tlc_value_in_flight?: string; + max_tlc_number_in_flight?: string; + }): Promise { + return this.channel.openChannel(params); + } + + /** + * Close channel + */ + async shutdownChannel(params: { + channel_id: Hash256; + close_script: Script; + force?: boolean; + fee_rate: bigint; + }): Promise { + return this.channel.shutdownChannel(params); + } + + /** + * Close channel + */ + async abandonChannel(channel_id: Hash256): Promise { + return this.channel.abandonChannel(channel_id); + } + + /** + * Send payment + */ + async sendPayment(params: { + payment_hash: string; + amount: bigint; + fee_rate: bigint; + }): Promise { + return this.payment.sendPayment(params); + } + + /** + * Parse invoice + */ + async parseInvoice(invoice: string): Promise { + return this.invoice.parseInvoice(invoice); + } + + /** + * Create new invoice + */ + async newInvoice(params: { + amount: bigint; + description?: string; + expiry?: bigint; + payment_secret?: string; + }): Promise { + return this.invoice.newInvoice(params); + } + + /** + * Get invoice information + */ + async getInvoice(payment_hash: string): Promise<{ + status: string; + invoice_address: string; + invoice: CkbInvoice; + }> { + return this.invoice.getInvoice(payment_hash); + } + + /** + * Cancel invoice + */ + async cancelInvoice(payment_hash: string): Promise { + return this.invoice.cancelInvoice(payment_hash); + } + + /** + * Get payment information + */ + async getPayment(payment_hash: string): Promise<{ + status: PaymentSessionStatus; + payment_hash: Hash256; + created_at: bigint; + last_updated_at: bigint; + failed_error?: string; + fee: bigint; + }> { + return this.payment.getPayment(payment_hash); + } + + /** + * Connect to node + */ + async connectPeer(address: string): Promise { + return this.peer.connectPeer(address); + } + + /** + * Disconnect from node + */ + async disconnectPeer(peer_id: string): Promise { + return this.peer.disconnectPeer(peer_id); + } + + /** + * List all connected nodes + */ + async listPeers(): Promise { + return this.peer.listPeers(); + } +} + +export default FiberSDK; diff --git a/packages/fiber/src/modules/cch.ts b/packages/fiber/src/modules/cch.ts new file mode 100644 index 00000000..9d71cc22 --- /dev/null +++ b/packages/fiber/src/modules/cch.ts @@ -0,0 +1,66 @@ +import { FiberClient } from "../client.js"; +import { Currency, Script } from "../types.js"; + +export class CchModule { + constructor(private client: FiberClient) {} + + /** + * Send BTC + */ + async sendBtc(params: { btc_pay_req: string; currency: Currency }): Promise<{ + timestamp: bigint; + expiry: bigint; + ckb_final_tlc_expiry_delta: bigint; + currency: Currency; + wrapped_btc_type_script: Script; + btc_pay_req: string; + ckb_pay_req: string; + payment_hash: string; + amount_sats: bigint; + fee_sats: bigint; + status: string; + }> { + return this.client.call("send_btc", [params]); + } + + /** + * Receive BTC + */ + async receiveBtc(params: { + payment_hash: string; + channel_id: string; + amount_sats: bigint; + final_tlc_expiry: bigint; + }): Promise<{ + timestamp: bigint; + expiry: bigint; + ckb_final_tlc_expiry_delta: bigint; + wrapped_btc_type_script: Script; + btc_pay_req: string; + payment_hash: string; + channel_id: string; + tlc_id?: bigint; + amount_sats: bigint; + status: string; + }> { + return this.client.call("receive_btc", [params]); + } + + /** + * Get receive BTC order + */ + async getReceiveBtcOrder(payment_hash: string): Promise<{ + timestamp: bigint; + expiry: bigint; + ckb_final_tlc_expiry_delta: bigint; + wrapped_btc_type_script: Script; + btc_pay_req: string; + payment_hash: string; + channel_id: string; + tlc_id?: bigint; + amount_sats: bigint; + status: string; + }> { + return this.client.call("get_receive_btc_order", [payment_hash]); + } +} diff --git a/packages/fiber/src/modules/channel.ts b/packages/fiber/src/modules/channel.ts new file mode 100644 index 00000000..21a30a63 --- /dev/null +++ b/packages/fiber/src/modules/channel.ts @@ -0,0 +1,174 @@ +import { FiberClient } from "../client.js"; +import { Channel, Hash256, Script } from "../types.js"; +import { + decimalToU128, + decimalToU64, + u128ToDecimal, + u64ToDecimal, +} from "../utils/number.js"; + +export class ChannelModule { + constructor(private client: FiberClient) {} + + /** + * Open a channel + */ + async openChannel(params: { + peer_id: string; + funding_amount: string; + public?: boolean; + funding_udt_type_script?: Script; + shutdown_script?: Script; + commitment_delay_epoch?: string; + commitment_fee_rate?: string; + funding_fee_rate?: string; + tlc_expiry_delta?: string; + tlc_min_value?: string; + tlc_fee_proportional_millionths?: string; + max_tlc_value_in_flight?: string; + max_tlc_number_in_flight?: string; + }): Promise { + const u128Params = { + ...params, + funding_amount: decimalToU128(params.funding_amount), + commitment_delay_epoch: params.commitment_delay_epoch + ? decimalToU128(params.commitment_delay_epoch) + : undefined, + commitment_fee_rate: params.commitment_fee_rate + ? decimalToU128(params.commitment_fee_rate) + : undefined, + funding_fee_rate: params.funding_fee_rate + ? decimalToU64(params.funding_fee_rate) + : undefined, + tlc_expiry_delta: params.tlc_expiry_delta + ? decimalToU64(params.tlc_expiry_delta) + : undefined, + tlc_min_value: params.tlc_min_value + ? decimalToU128(params.tlc_min_value) + : undefined, + tlc_fee_proportional_millionths: params.tlc_fee_proportional_millionths + ? decimalToU128(params.tlc_fee_proportional_millionths) + : undefined, + max_tlc_value_in_flight: params.max_tlc_value_in_flight + ? decimalToU64(params.max_tlc_value_in_flight) + : undefined, + }; + return this.client.call("open_channel", [u128Params]); + } + + /** + * Accept a channel + */ + async acceptChannel(params: { + temporary_channel_id: string; + funding_amount: string; + max_tlc_value_in_flight: string; + max_tlc_number_in_flight: string; + tlc_min_value: string; + tlc_fee_proportional_millionths: string; + tlc_expiry_delta: string; + }): Promise { + const u128Params = { + ...params, + funding_amount: decimalToU128(params.funding_amount), + max_tlc_value_in_flight: decimalToU128(params.max_tlc_value_in_flight), + max_tlc_number_in_flight: decimalToU128(params.max_tlc_number_in_flight), + tlc_min_value: decimalToU128(params.tlc_min_value), + tlc_fee_proportional_millionths: decimalToU128( + params.tlc_fee_proportional_millionths, + ), + tlc_expiry_delta: decimalToU128(params.tlc_expiry_delta), + }; + return this.client.call("accept_channel", [u128Params]); + } + + /** + * Abandon a channel + * @param channelId - Channel ID, must be a valid Hash256 format + * @throws {Error} Throws error when channel ID is invalid or channel does not exist + * @returns Promise + */ + async abandonChannel(channelId: Hash256): Promise { + if (!channelId) { + throw new Error("Channel ID cannot be empty"); + } + + if (!channelId.startsWith("0x")) { + throw new Error("Channel ID must start with 0x"); + } + + if (channelId.length !== 66) { + // 0x + 64-bit hash + throw new Error("Invalid channel ID length"); + } + + try { + // Check if channel exists + const channels = await this.listChannels(); + const channelExists = channels.some( + (channel) => channel.channel_id === channelId, + ); + + if (!channelExists) { + throw new Error(`Channel with ID ${channelId} not found`); + } + + return this.client.call("abandon_channel", [{ channel_id: channelId }]); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to abandon channel: ${error.message}`); + } + throw error; + } + } + + /** + * List channels + */ + async listChannels(): Promise { + const response = await this.client.call<{ channels: Channel[] }>( + "list_channels", + [{}], + ); + return response.channels.map((channel) => ({ + ...channel, + local_balance: u128ToDecimal(channel.local_balance), + remote_balance: u128ToDecimal(channel.remote_balance), + offered_tlc_balance: u128ToDecimal(channel.offered_tlc_balance), + received_tlc_balance: u128ToDecimal(channel.received_tlc_balance), + tlc_expiry_delta: u128ToDecimal(channel.tlc_expiry_delta), + tlc_fee_proportional_millionths: u128ToDecimal( + channel.tlc_fee_proportional_millionths, + ), + created_at: u64ToDecimal(channel.created_at, true), + last_updated_at: channel.last_updated_at + ? u64ToDecimal(channel.last_updated_at, true) + : "", + })); + } + + /** + * Shutdown channel + */ + async shutdownChannel(params: { + channel_id: Hash256; + close_script: Script; + force?: boolean; + fee_rate: bigint; + }): Promise { + return this.client.call("shutdown_channel", [params]); + } + + /** + * Update channel + */ + async updateChannel(params: { + channel_id: Hash256; + enabled?: boolean; + tlc_expiry_delta?: bigint; + tlc_minimum_value?: bigint; + tlc_fee_proportional_millionths?: bigint; + }): Promise { + return this.client.call("update_channel", [params]); + } +} diff --git a/packages/fiber/src/modules/dev.ts b/packages/fiber/src/modules/dev.ts new file mode 100644 index 00000000..8070a4ae --- /dev/null +++ b/packages/fiber/src/modules/dev.ts @@ -0,0 +1,58 @@ +import { FiberClient } from "../client.js"; +import { Hash256, RemoveTlcReason } from "../types.js"; + +export class DevModule { + constructor(private client: FiberClient) {} + + /** + * Submit commitment transaction + */ + async commitmentSigned(params: { + channel_id: Hash256; + commitment_transaction: string; + }): Promise { + return this.client.call("commitment_signed", [params]); + } + + /** + * Add time-locked contract + */ + async addTlc(params: { + channel_id: Hash256; + amount: bigint; + payment_hash: string; + expiry: bigint; + }): Promise { + return this.client.call("add_tlc", [params]); + } + + /** + * Remove time-locked contract + */ + async removeTlc(params: { + channel_id: Hash256; + tlc_id: bigint; + reason: RemoveTlcReason; + payment_preimage?: string; + failure_message?: string; + }): Promise { + return this.client.call("remove_tlc", [params]); + } + + /** + * Submit commitment transaction + */ + async submitCommitmentTransaction(params: { + channel_id: Hash256; + commitment_transaction: string; + }): Promise { + return this.client.call("submit_commitment_transaction", [params]); + } + + /** + * Remove watch channel + */ + async removeWatchChannel(channel_id: Hash256): Promise { + return this.client.call("remove_watch_channel", [channel_id]); + } +} diff --git a/packages/fiber/src/modules/graph.ts b/packages/fiber/src/modules/graph.ts new file mode 100644 index 00000000..be43bd4c --- /dev/null +++ b/packages/fiber/src/modules/graph.ts @@ -0,0 +1,20 @@ +import { FiberClient } from "../client.js"; +import { ChannelInfo, Pubkey } from "../types.js"; + +export class GraphModule { + constructor(private client: FiberClient) {} + + /** + * Get node list + */ + async graphNodes(): Promise { + return this.client.call("graph_nodes", []); + } + + /** + * Get channel list + */ + async graphChannels(): Promise { + return this.client.call("graph_channels", []); + } +} diff --git a/packages/fiber/src/modules/info.ts b/packages/fiber/src/modules/info.ts new file mode 100644 index 00000000..dea7ba8c --- /dev/null +++ b/packages/fiber/src/modules/info.ts @@ -0,0 +1,76 @@ +import { fixedPointToString } from "@ckb-ccc/core"; +import { FiberClient } from "../client.js"; +import { NodeInfo } from "../types.js"; +import { u64ToDecimal } from "../utils/number.js"; + +interface RawNodeInfo { + node_name: string; + addresses: string[]; + node_id: string; + timestamp: bigint; + chain_hash: string; + auto_accept_min_ckb_funding_amount: bigint; + auto_accept_channel_ckb_funding_amount: bigint; + tlc_expiry_delta: bigint; + tlc_min_value: bigint; + tlc_fee_proportional_millionths: bigint; + channel_count: string; + pending_channel_count: string; + peers_count: string; + udt_cfg_infos: Record; + default_funding_lock_script?: { + code_hash: string; + hash_type: string; + args: string; + }; +} + +export class InfoModule { + constructor(private client: FiberClient) {} + + /** + * Get node information + * @returns Returns detailed node information, including node name, address, ID, etc. + * @throws {Error} Throws error when unable to get node information + */ + async nodeInfo(): Promise { + const response = await this.client.call("node_info", []); + return { + node_name: response.node_name, + addresses: response.addresses, + node_id: response.node_id, + timestamp: response.timestamp + ? u64ToDecimal(response.timestamp, true) + : "", + chain_hash: response.chain_hash, + auto_accept_min_ckb_funding_amount: + response.auto_accept_min_ckb_funding_amount + ? fixedPointToString(response.auto_accept_min_ckb_funding_amount) + : "", + auto_accept_channel_ckb_funding_amount: + response.auto_accept_channel_ckb_funding_amount + ? fixedPointToString(response.auto_accept_channel_ckb_funding_amount) + : "", + tlc_expiry_delta: response.tlc_expiry_delta + ? fixedPointToString(response.tlc_expiry_delta) + : "", + tlc_min_value: response.tlc_min_value + ? fixedPointToString(response.tlc_min_value) + : "", + tlc_fee_proportional_millionths: response.tlc_fee_proportional_millionths + ? fixedPointToString(response.tlc_fee_proportional_millionths) + : "", + channel_count: response.channel_count + ? Number(response.channel_count).toString() + : "0", + pending_channel_count: response.pending_channel_count + ? Number(response.pending_channel_count).toString() + : "0", + peers_count: response.peers_count + ? Number(response.peers_count).toString() + : "0", + udt_cfg_infos: response.udt_cfg_infos, + default_funding_lock_script: response.default_funding_lock_script, + }; + } +} diff --git a/packages/fiber/src/modules/invoice.ts b/packages/fiber/src/modules/invoice.ts new file mode 100644 index 00000000..c15f4400 --- /dev/null +++ b/packages/fiber/src/modules/invoice.ts @@ -0,0 +1,43 @@ +import { FiberClient } from "../client.js"; +import { CkbInvoice, CkbInvoiceStatus } from "../types.js"; + +export class InvoiceModule { + constructor(private client: FiberClient) {} + + /** + * Create a new invoice + */ + async newInvoice(params: { + amount: bigint; + description?: string; + expiry?: bigint; + payment_secret?: string; + }): Promise { + return this.client.call("new_invoice", [params]); + } + + /** + * Parse an invoice + */ + async parseInvoice(invoice: string): Promise { + return this.client.call("parse_invoice", [{ invoice }]); + } + + /** + * Get invoice details + */ + async getInvoice(payment_hash: string): Promise<{ + status: CkbInvoiceStatus; + invoice_address: string; + invoice: CkbInvoice; + }> { + return this.client.call("get_invoice", [{ payment_hash }]); + } + + /** + * Cancel an invoice + */ + async cancelInvoice(payment_hash: string): Promise { + return this.client.call("cancel_invoice", [{ payment_hash }]); + } +} diff --git a/packages/fiber/src/modules/payment.ts b/packages/fiber/src/modules/payment.ts new file mode 100644 index 00000000..72b87418 --- /dev/null +++ b/packages/fiber/src/modules/payment.ts @@ -0,0 +1,40 @@ +import { FiberClient } from "../client.js"; +import { + Hash256, + PaymentCustomRecords, + PaymentSessionStatus, + SessionRoute, +} from "../types.js"; + +export class PaymentModule { + constructor(private client: FiberClient) {} + + /** + * Send payment + */ + async sendPayment(params: { + payment_hash: string; + amount: bigint; + fee_rate: bigint; + custom_records?: PaymentCustomRecords; + route?: SessionRoute; + }): Promise { + return this.client.call("send_payment", [params]); + } + + /** + * Get payment + */ + async getPayment(payment_hash: string): Promise<{ + status: PaymentSessionStatus; + payment_hash: Hash256; + created_at: bigint; + last_updated_at: bigint; + failed_error?: string; + fee: bigint; + custom_records?: PaymentCustomRecords; + route: SessionRoute; + }> { + return this.client.call("get_payment", [payment_hash]); + } +} diff --git a/packages/fiber/src/modules/peer.ts b/packages/fiber/src/modules/peer.ts new file mode 100644 index 00000000..6706dc69 --- /dev/null +++ b/packages/fiber/src/modules/peer.ts @@ -0,0 +1,34 @@ +import { FiberClient } from "../client.js"; + +export interface PeerInfo { + pubkey: string; + peer_id: string; + addresses: string[]; +} + +export class PeerModule { + constructor(private client: FiberClient) {} + + /** + * Connect to a peer node + * @param address Full peer address including peer ID (e.g. "/ip4/127.0.0.1/tcp/8119/p2p/Qm...") + */ + async connectPeer(address: string): Promise { + return this.client.call("connect_peer", [{ address }]); + } + + /** + * Disconnect from a peer node + */ + async disconnectPeer(peer_id: string): Promise { + return this.client.call("disconnect_peer", [{ peer_id }]); + } + + /** + * List all connected peers + * @returns Array of peer information + */ + async listPeers(): Promise { + return this.client.call("list_peers", []); + } +} diff --git a/packages/fiber/src/types.ts b/packages/fiber/src/types.ts new file mode 100644 index 00000000..564bd4b7 --- /dev/null +++ b/packages/fiber/src/types.ts @@ -0,0 +1,241 @@ +export type Hash256 = string; +export type Pubkey = string; + +export interface RPCRequest { + jsonrpc: string; + method: string; + params: T[]; + id: number; +} + +export interface RPCResponse { + jsonrpc: string; + result?: T; + error?: { + code: number; + message: string; + data?: unknown; + }; + id: number; +} + +export enum Currency { + Fibb = "Fibb", + Fibt = "Fibt", + Fibd = "Fibd", +} + +export enum CkbInvoiceStatus { + Open = "Open", + Cancelled = "Cancelled", + Expired = "Expired", + Received = "Received", + Paid = "Paid", +} + +export enum PaymentStatus { + Pending = "Pending", + Succeeded = "Succeeded", + Failed = "Failed", +} + +export enum PaymentType { + Send = "Send", + Receive = "Receive", +} + +export interface Payment { + payment_hash: string; + payment_preimage: string; + amount: bigint; + fee: bigint; + status: PaymentStatus; + type: PaymentType; + created_at: bigint; + completed_at?: bigint; +} + +export interface PaymentResult { + payment_hash: string; + status: PaymentStatus; + fee: bigint; +} + +export interface Peer { + node_id: Pubkey; + address: string; + is_connected: boolean; + last_connected_at?: bigint; +} + +export enum PaymentSessionStatus { + Created = "Created", + Inflight = "Inflight", + Success = "Success", + Failed = "Failed", +} + +export enum RemoveTlcReason { + RemoveTlcFulfill = "RemoveTlcFulfill", + RemoveTlcFail = "RemoveTlcFail", +} + +export interface Script { + code_hash: string; + hash_type: string; + args: string; +} + +export interface Channel { + channel_id: Hash256; + peer_id: Pubkey; + funding_udt_type_script?: Script; + state: string; + local_balance: string; + offered_tlc_balance: string; + remote_balance: string; + received_tlc_balance: string; + latest_commitment_transaction_hash?: Hash256; + created_at: string; + last_updated_at: string; + enabled: boolean; + tlc_expiry_delta: string; + tlc_fee_proportional_millionths: string; +} + +export interface ChannelInfo { + channel_outpoint: { + tx_hash: Hash256; + index: bigint; + }; + node1: Pubkey; + node2: Pubkey; + created_timestamp: bigint; + last_updated_timestamp_of_node1?: bigint; + last_updated_timestamp_of_node2?: bigint; + fee_rate_of_node1?: bigint; + fee_rate_of_node2?: bigint; + capacity: bigint; + chain_hash: Hash256; + udt_type_script?: Script; +} + +export interface CkbInvoice { + currency: Currency; + amount?: bigint; + signature?: { + pubkey: Pubkey; + signature: string; + }; + data: { + payment_hash: string; + timestamp: bigint; + expiry?: bigint; + description?: string; + description_hash?: string; + payment_secret?: string; + features?: bigint; + route_hints?: Array<{ + pubkey: Pubkey; + channel_outpoint: { + tx_hash: Hash256; + index: bigint; + }; + fee_rate: bigint; + tlc_expiry_delta: bigint; + }>; + }; +} + +export interface NodeInfo { + node_name: string; + addresses: string[]; + node_id: Pubkey; + timestamp: string; + chain_hash: Hash256; + auto_accept_min_ckb_funding_amount: string; + auto_accept_channel_ckb_funding_amount: string; + tlc_expiry_delta: string; + tlc_min_value: string; + tlc_fee_proportional_millionths: string; + channel_count: string; + pending_channel_count: string; + peers_count: string; + udt_cfg_infos: Record; + default_funding_lock_script?: { + code_hash: string; + hash_type: string; + args: string; + }; +} + +export interface PaymentCustomRecords { + data: Record; +} + +export interface SessionRoute { + nodes: Array<{ + pubkey: Pubkey; + amount: bigint; + channel_outpoint?: { + tx_hash: Hash256; + index: bigint; + }; + }>; +} + +export interface NodeStatus { + is_online: boolean; + last_sync_time: bigint; + connected_peers: number; + total_channels: number; +} + +export interface NodeVersion { + version: string; + commit_hash: string; + build_time: string; +} + +export interface NetworkInfo { + network_type: "mainnet" | "testnet" | "devnet"; + chain_hash: string; + block_height: bigint; + block_hash: string; +} + +export interface CchOrder { + timestamp: bigint; + expiry: bigint; + ckb_final_tlc_expiry_delta: bigint; + currency: Currency; + wrapped_btc_type_script?: Script; + btc_pay_req: string; + ckb_pay_req: string; + payment_hash: string; + amount_sats: bigint; + fee_sats: bigint; + status: CchOrderStatus; +} + +export enum CchOrderStatus { + Pending = "Pending", + Processing = "Processing", + Completed = "Completed", + Failed = "Failed", +} + +export enum HashAlgorithm { + CkbHash = "CkbHash", + Sha256 = "Sha256", +} + +export interface HopHint { + pubkey: Pubkey; + channel_outpoint: { + tx_hash: Hash256; + index: bigint; + }; + fee_rate: bigint; + tlc_expiry_delta: bigint; +} diff --git a/packages/fiber/src/utils/number.ts b/packages/fiber/src/utils/number.ts new file mode 100644 index 00000000..86fb0223 --- /dev/null +++ b/packages/fiber/src/utils/number.ts @@ -0,0 +1,66 @@ +import { fixedPointFrom, fixedPointToString } from "@ckb-ccc/core"; + +/** + * 将u128类型的数字转换为十进制字符串 + * @param value - u128类型的数字(bigint或string) + * @param decimals - 小数位数,默认为8 + * @returns 十进制字符串 + * + * @example + * ```typescript + * const decimal = u128ToDecimal(123456789n); // 输出 "1.23456789" + * const decimalWithDecimals = u128ToDecimal(123456789n, 6); // 输出 "123.456789" + * ``` + */ +export function u128ToDecimal( + value: bigint | string, + decimals: number = 8, +): string { + return fixedPointToString(value, decimals); +} + +/** + * 将十进制字符串转换为u128类型 + * @param value - 十进制字符串 + * @param decimals - 小数位数,默认为8 + * @returns u128类型的数字(bigint) + * + * @example + * ```typescript + * const u128 = decimalToU128("1.23456789"); // 输出 123456789n + * const u128WithDecimals = decimalToU128("123.456789", 6); // 输出 123456789n + * ``` + */ +export function decimalToU128(value: string, decimals: number = 8): bigint { + return fixedPointFrom(value, decimals); +} + +/** + * 将十进制字符串转换为U64类型 + * @param value - 十进制字符串 + * @returns U64类型的数字(bigint) + * + * @example + * ```typescript + * const u64 = decimalToU64("1000"); // 输出 1000n + * ``` + */ +export function decimalToU64(value: string): bigint { + return BigInt(value); +} + +/** + * 将U64类型的数字转换为十进制字符串 + * @param value - U64类型的数字(bigint或string) + * @param isTimestamp - 是否为时间戳,如果是则直接返回十进制字符串 + * @returns 十进制字符串 + */ +export function u64ToDecimal( + value: bigint | string, + isTimestamp: boolean = false, +): string { + if (isTimestamp) { + return value.toString(); + } + return u128ToDecimal(value, 0); +} diff --git a/packages/fiber/test/channel.cjs b/packages/fiber/test/channel.cjs new file mode 100644 index 00000000..11962cad --- /dev/null +++ b/packages/fiber/test/channel.cjs @@ -0,0 +1,223 @@ +const { FiberSDK } = require("../dist.commonjs/index.js"); + +// Custom error handling function +function handleRPCError(error) { + if (error.error && error.error.code === -32601) { + console.error( + "Error: Node may not be running or RPC method does not exist", + ); + console.error("Please ensure:"); + console.error("1. Fiber node is started"); + console.error( + "2. Node RPC address is correct (current: http://127.0.0.1:8227)", + ); + console.error("3. Node RPC interface is available"); + } else if (error.error && error.error.code === -32602) { + console.error("Error: Invalid parameters"); + console.error("Please check:"); + console.error("1. Parameter types are correct"); + console.error("2. Parameter values are within valid range"); + console.error("3. All required parameters are provided"); + } else { + console.error("RPC Error:", error.message); + if (error.error && error.error.data) { + console.error("Error details:", error.error.data); + } + } +} + +// Convert hexadecimal string to number +function hexToNumber(hex) { + if (!hex) return 0; + return parseInt(hex.replace("0x", ""), 16); +} + +async function testListChannels() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting channel listing test...\n"); + + try { + // List channels + console.log("Calling listChannels method..."); + const channels = await sdk.listChannels(); + + // Output raw data + console.log("Raw data:", JSON.stringify(channels, null, 2)); + + // Type check + if (!Array.isArray(channels)) { + throw new Error("Invalid channel list format"); + } + + // Output detailed information + if (channels.length > 0) { + console.log("\nChannel details:"); + channels.forEach((channel, index) => { + console.log(`\nChannel ${index + 1}:`); + console.log("Channel ID:", channel.channel_id); + console.log("Peer ID:", channel.peer_id); + console.log("State:", channel.state_name); + console.log("State Flags:", channel.state_flags); + console.log("Local Balance:", hexToNumber(channel.local_balance)); + console.log("Remote Balance:", hexToNumber(channel.remote_balance)); + console.log( + "Created At:", + new Date(hexToNumber(channel.created_at)).toLocaleString(), + ); + console.log("Is Public:", channel.is_public ? "Yes" : "No"); + console.log("Is Enabled:", channel.is_enabled ? "Yes" : "No"); + console.log( + "TLC Expiry Delta:", + hexToNumber(channel.tlc_expiry_delta), + ); + console.log("TLC Min Value:", hexToNumber(channel.tlc_min_value)); + console.log( + "TLC Fee Proportion:", + hexToNumber(channel.tlc_fee_proportional_millionths), + ); + }); + } else { + console.log("No channels available"); + } + + return channels; + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to list channels:", error.message); + } + return []; + } + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Error during test:", error.message); + } + return []; + } +} + +async function testAbandonChannel() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("\nStarting channel closure test...\n"); + + try { + // Get available channels + const channels = await testListChannels(); + + if (channels.length === 0) { + console.log("No channels available to close"); + return; + } + + // Select first channel for closure + const channelToClose = channels[0]; + console.log("\nChannel to close:"); + console.log("Channel ID:", channelToClose.channel_id); + console.log("Peer ID:", channelToClose.peer_id); + console.log("State:", channelToClose.state); + + await sdk.abandonChannel(channelToClose.channel_id); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to close channel:", error.message); + } + } + } catch (error) { + console.error("Error during channel closure test:", error); + } +} + +async function testNewChannel() { + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + const peerId = "QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo"; + console.log("Calling open_channel method, Peer ID:", peerId); + const result = await sdk.openChannel({ + peer_id: peerId, + funding_amount: "0xba43b7400", // 100 CKB + public: true, + }); + console.log("Open channel result:", result); +} + +async function testUpdateAndShutdownChannel() { + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + const channels = await testListChannels(); + + if (channels.length === 0) { + console.log("No channels available to close"); + return; + } + + // Select first channel for closure + const channelToClose = channels[0]; + const channelId = channelToClose.channel_id; + + // Check channel state + if (channelToClose.state_name === "NEGOTIATING_FUNDING") { + console.log("Channel is in funding negotiation stage, cannot close"); + return; + } + + // Ensure channelId is string type + if (typeof channelId !== "string") { + console.error("Invalid channel ID format:", channelId); + return; + } + + console.log("Calling shutdown_channel method, Channel ID:", channelId); + const result2 = await sdk.shutdownChannel({ + channel_id: channelId, + close_script: { + code_hash: + "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + hash_type: "type", + args: "0xcc015401df73a3287d8b2b19f0cc23572ac8b14d", + }, + fee_rate: "0x3FC", + force: false, + }); + console.log("Channel closure result:", result2); +} + +async function main() { + try { + // await testListChannels(); + // await testNewChannel(); + await testUpdateAndShutdownChannel(); + // await testListChannels(); + // await testAbandonChannel(); + console.log("\nAll tests completed!"); + } catch (error) { + console.error("Error during test:", error); + } +} + +// Run tests +console.log("Starting channel-related tests...\n"); + +main() + .then(() => console.log("\nAll tests completed!")) + .catch(console.error); diff --git a/packages/fiber/test/info.cjs b/packages/fiber/test/info.cjs new file mode 100644 index 00000000..c8e5e685 --- /dev/null +++ b/packages/fiber/test/info.cjs @@ -0,0 +1,87 @@ +const { FiberSDK } = require("../dist.commonjs/index.js"); +// Custom error handling function +function handleRPCError(error) { + if (error.error && error.error.code === -32601) { + console.error( + "Error: Node may not be running or RPC method does not exist", + ); + console.error("Please ensure:"); + console.error("1. Fiber node is started"); + console.error( + "2. Node RPC address is correct (current: http://127.0.0.1:8227)", + ); + console.error("3. Node RPC interface is available"); + } else if (error.error && error.error.code === -32602) { + console.error("Error: Invalid parameters"); + console.error("Please check:"); + console.error("1. Parameter types are correct"); + console.error("2. Parameter values are within valid range"); + console.error("3. All required parameters are provided"); + } else { + console.error("RPC Error:", error.message); + if (error.error && error.error.data) { + console.error("Error details:", error.error.data); + } + } +} + +// Convert hexadecimal string to number +function hexToNumber(hex) { + if (!hex) return 0; + return parseInt(hex.replace("0x", ""), 16); +} + +async function testNodeInfo() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting node info test...\n"); + + try { + // Get node information + console.log("Calling node_info method..."); + const info = await sdk.nodeInfo(); + console.log(info); + // // Output node information + // console.log("\nNode Information:"); + // console.log("Node Name:", info.node_name); + // console.log("Node ID:", info.node_id); + // console.log("Addresses:", info.addresses); + // console.log("Chain Hash:", info.chain_hash); + // console.log( + // "Auto Accept Min CKB Funding Amount:", + // info.auto_accept_min_ckb_funding_amount, + // ); + // console.log("UDT Config Info:", info.udt_cfg_infos); + // console.log( + // "Timestamp:", + // new Date(Number(info.timestamp)).toLocaleString(), + // ); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to get node info:", error.message); + } + } + + console.log("\nTest completed!"); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Error during test:", error.message); + } + } +} + +// Run tests +console.log("Starting node info tests...\n"); + +testNodeInfo() + .then(() => console.log("\nAll tests completed!")) + .catch(console.error); diff --git a/packages/fiber/test/invoice.cjs b/packages/fiber/test/invoice.cjs new file mode 100644 index 00000000..c643db7c --- /dev/null +++ b/packages/fiber/test/invoice.cjs @@ -0,0 +1,235 @@ +const { FiberClient, FiberSDK } = require("../dist.commonjs/index.js"); +const crypto = require("crypto"); + +// Generate random payment_preimage +function generatePaymentPreimage() { + const randomBytes = crypto.randomBytes(32); + return "0x" + randomBytes.toString("hex"); +} + +// Custom error handling function +function handleRPCError(error) { + if (error.error && error.error.code === -32601) { + console.error( + "Error: Node may not be running or RPC method does not exist", + ); + console.error("Please ensure:"); + console.error("1. Fiber node is started"); + console.error( + "2. Node RPC address is correct (current: http://127.0.0.1:8227)", + ); + console.error("3. Node RPC interface is available"); + } else if (error.error && error.error.code === -32602) { + console.error("Error: Invalid parameters"); + console.error("Please check:"); + console.error("1. Parameter types are correct"); + console.error("2. Parameter values are within valid range"); + console.error("3. All required parameters are provided"); + } else { + console.error("RPC Error:", error.message); + if (error.error && error.error.data) { + console.error("Error details:", error.error.data); + } + } +} + +// Convert hexadecimal string to number +function hexToNumber(hex) { + return parseInt(hex.replace("0x", ""), 16); +} + +async function testNewInvoice() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting new invoice test...\n"); + + try { + // Create new invoice + console.log("Calling new_invoice method..."); + const amount = 100000000; // 100,000,000 + const response = await sdk.invoice.newInvoice({ + amount: `0x${amount.toString(16)}`, // Convert amount to hexadecimal + currency: "Fibt", + description: "test invoice generated by node2", + expiry: "0xe10", + final_cltv: "0x28", + payment_preimage: generatePaymentPreimage(), + hash_algorithm: "sha256", + }); + console.log("Invoice created successfully:"); + console.log("Payment Hash:", response.payment_hash); + console.log("Amount:", response.amount); + console.log("Description:", response.description); + console.log("Expiry:", response.expiry); + console.log( + "Created At:", + new Date(response.created_at).toLocaleString(), + ); + + return response; + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to create invoice:", error.message); + } + return null; + } + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Error during test:", error.message); + } + return null; + } +} + +async function testParseInvoice() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting test parsing invoice...\n"); + + try { + console.log("Calling parseInvoice method..."); + const invoice = await sdk.invoice.parseInvoice( + "fibt1000000001pcsaug0p0exgfw0pnm6vk0rnt4xefskmrz0k2vqxr4lnrms60qasvc54jagg2hk8v40k88exmp04pn5cpcnrcsw5lk9w0w6l0m3k84e2ax4v6gq9ne2n77u4p8h3npx6tuufqftq8eyqxw9t4upaw4f89xukcee79rm0p0jv92d5ckq7pmvm09ma3psheu3rfyy9atlrdr4el6ys8yqurl2m74msuykljp35j0s47vpw8h3crfp5ldp8kp4xlusqk6rad3ssgwn2a429qlpgfgjrtj3gzy26w50cy7gypgjm6mjgaz2ff5q4am0avf6paxja2gh2wppjagqlg466yzty0r0pfz8qpuzqgq43mkgx", + ); + console.log("Parsing result:", JSON.stringify(invoice, null, 2)); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to parse invoice:", error.message); + } + } + + console.log("\nTest completed!"); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Test failed:", error.message); + } + } +} + +async function testGetInvoice() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("\nStarting get invoice test...\n"); + + try { + // Get invoice + const invoice = await testNewInvoice(); + if (!invoice) { + console.log("No invoice available to retrieve"); + return; + } + + console.log("Calling get_invoice method..."); + const invoiceInfo = await sdk.invoice.getInvoice(invoice.payment_hash); + + // Output invoice information + console.log("\nInvoice details:"); + console.log("Status:", invoiceInfo.status); + console.log("Invoice Address:", invoiceInfo.invoice_address); + console.log("Payment Hash:", invoiceInfo.invoice.payment_hash); + console.log("Amount:", invoiceInfo.invoice.amount); + console.log("Description:", invoiceInfo.invoice.description); + console.log("Expiry:", invoiceInfo.invoice.expiry); + console.log( + "Created At:", + new Date(invoiceInfo.invoice.created_at).toLocaleString(), + ); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to get invoice:", error.message); + } + } + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Test failed:", error.message); + } + } +} + +async function testCancelInvoice() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("\nStarting cancel invoice test...\n"); + + try { + // Get invoice to cancel + const invoice = await testNewInvoice(); + if (!invoice) { + console.log("No invoice available to cancel"); + return; + } + + console.log("Calling cancel_invoice method..."); + await sdk.invoice.cancelInvoice(invoice.payment_hash); + console.log("Invoice cancelled successfully"); + + // Verify invoice status + console.log("\nVerifying invoice status..."); + const cancelledInvoice = await sdk.invoice.getInvoice( + invoice.payment_hash, + ); + console.log( + "Invoice status after cancellation:", + cancelledInvoice.status, + ); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to cancel invoice:", error.message); + } + } + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Test failed:", error.message); + } + } +} + +// Run tests +console.log("Starting invoice-related tests...\n"); + +async function main() { + await testNewInvoice(); + await testParseInvoice(); + await testGetInvoice(); + await testCancelInvoice(); +} + +main() + .then(() => console.log("\nAll tests completed!")) + .catch(console.error); diff --git a/packages/fiber/test/payment.cjs b/packages/fiber/test/payment.cjs new file mode 100644 index 00000000..148c450a --- /dev/null +++ b/packages/fiber/test/payment.cjs @@ -0,0 +1,124 @@ +const { FiberSDK } = require("../dist.commonjs/index.js"); + +// Custom error handling function +function handleRPCError(error) { + if (error.error && error.error.code === -32601) { + console.error( + "Error: Node may not be running or RPC method does not exist", + ); + console.error("Please ensure:"); + console.error("1. Fiber node is started"); + console.error( + "2. Node RPC address is correct (current: http://127.0.0.1:8227)", + ); + console.error("3. Node RPC interface is available"); + } else if (error.error && error.error.code === -32602) { + console.error("Error: Invalid parameter"); + console.error("Please check:"); + console.error("1. Whether the parameter type is correct"); + console.error("2. Whether the parameter value is within the valid range"); + console.error("3. Whether all required parameters are provided"); + } else { + console.error("RPC error:", error.message); + if (error.error && error.error.data) { + console.error("Error details:", error.error.data); + } + } +} + +// Convert hexadecimal string to number +function hexToNumber(hex) { + return parseInt(hex.replace("0x", ""), 16); +} + +async function testSendPayment() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting test to send payment...\n"); + + try { + // Send payment + console.log("Calling sendPayment method..."); + await sdk.payment.sendPayment({ + invoice: + "fibt1000000001pcsaug0p0exgfw0pnm6vkkya5ul6wxurhh09qf9tuwwaufqnr3uzwpplgcrjpeuhe6w4rudppfkytvm4jekf6ymmwqk2h0ajvr5uhjpwfd9aga09ahpy88hz2um4l9t0xnpk3m9wlf22m2yjcshv3k4g5x7c68fn0gs6a35dw5r56cc3uztyf96l55ayeuvnd9fl4yrt68y086xn6qgjhf4n7xkml62gz5ecypm3xz0wdd59tfhtrhwvp5qlps959vmpf4jygdkspxn8xalparwj8h9ts6v6v0rf7vvhhku40z9sa4txxmgsjzwqzme4ddazxrfrlkc9m4uysh27zgqlx7jrfgvjw7rcqpmsrlga", + }); + console.log("Payment sent successfully"); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Payment sending failed:", error.message); + } + } + + console.log("\nTest completed!"); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Error occurred during test:", error.message); + } + } +} + +async function testGetPayment() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting test to get payment...\n"); + + try { + // Get payment + console.log("Calling getPayment method..."); + const paymentHash = "payment_hash"; // Replace with actual payment_hash + const payment = await sdk.payment.getPayment(paymentHash); + console.log("Payment information:", JSON.stringify(payment, null, 2)); + + // Output detailed information + console.log("\nPayment detailed information:"); + console.log("Status:", payment.status); + console.log("Payment hash:", payment.payment_hash); + console.log( + "Created time:", + new Date(hexToNumber(payment.created_at)).toLocaleString(), + ); + console.log( + "Last updated time:", + new Date(hexToNumber(payment.last_updated_at)).toLocaleString(), + ); + if (payment.failed_error) { + console.log("Failure reason:", payment.failed_error); + } + console.log("Fee:", hexToNumber(payment.fee)); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Payment getting failed:", error.message); + } + } + + console.log("\nTest completed!"); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Error occurred during test:", error.message); + } + } +} + +// Run all tests +console.log("Starting to run payment related tests...\n"); + +testSendPayment().catch(console.error); diff --git a/packages/fiber/test/peer.cjs b/packages/fiber/test/peer.cjs new file mode 100644 index 00000000..a8012938 --- /dev/null +++ b/packages/fiber/test/peer.cjs @@ -0,0 +1,132 @@ +const { FiberSDK } = require("../dist.commonjs/index.js"); + +// Custom error handling function +function handleRPCError(error) { + if (error.error && error.error.code === -32601) { + console.error( + "Error: Node may not be running or RPC method does not exist", + ); + console.error("Please ensure:"); + console.error("1. Fiber node is started"); + console.error( + "2. Node RPC address is correct (current: http://127.0.0.1:8227)", + ); + console.error("3. Node RPC interface is available"); + } else if (error.error && error.error.code === -32602) { + console.error("Error: Invalid parameters"); + console.error("Please check:"); + console.error("1. Parameter types are correct"); + console.error("2. Parameter values are within valid range"); + console.error("3. All required parameters are provided"); + } else { + console.error("RPC Error:", error.message); + if (error.error && error.error.data) { + console.error("Error details:", error.error.data); + } + } +} + +async function testConnectPeer() { + try { + // Initialize SDK + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 5000, + }); + + console.log("Starting peer connection test...\n"); + + try { + // Connect to peer + console.log("Calling connect_peer method..."); + const peerAddress = + "/ip4/18.162.235.225/tcp/8119/p2p/QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo"; + + try { + const response = await sdk.connectPeer({ + address: peerAddress, + }); + console.log("Successfully connected to peer:", response); + } catch (error) { + // Check error message, if already connected, consider it a success + if (error.message && error.message.includes("already connected")) { + console.log("Peer is already connected, proceeding with next steps"); + } else { + throw error; + } + } + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Failed to connect to peer:", error.message); + } + } + + console.log("\nTest completed!"); + } catch (error) { + if (error.error) { + handleRPCError(error); + } else { + console.error("Error during test:", error.message); + } + } +} + +async function testDisconnectPeer() { + console.log("\nStarting peer disconnection test...\n"); + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 30000, + }); + const peerId = "QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo"; + console.log("正在调用 disconnect_peer 方法,节点 ID:", peerId); + const result = await sdk.peer.disconnectPeer(peerId); + console.log("断开链接结果:", result); +} + +async function testListChannels() { + console.log("\n开始测试列出通道...\n"); + + try { + const sdk = new FiberSDK({ + endpoint: "http://127.0.0.1:8227", + timeout: 30000, + }); + + const peerId = "QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo"; + console.log("正在调用 list_channels 方法,节点 ID:", peerId); + + const result = await sdk.listChannels({ + peer_id: peerId, + }); + + console.log("通道列表:", result); + } catch (error) { + console.error("列出通道失败:", error.message); + handleRPCError(error); + } + + console.log("\n测试完成!"); +} + +async function main() { + // 1. First clean up channels in NEGOTIATING_FUNDING state + // await testListChannels(); + + // 2. Then establish network connection + await testConnectPeer(); + + // 3. Disconnect + await testDisconnectPeer(); + + // 4. Finally query channel status + // await testListChannels(); +} + +// Run tests +console.log("开始运行节点连接测试...\n"); + +main() + .then(() => console.log("\n所有测试完成!")) + .catch(console.error); diff --git a/packages/fiber/tsconfig.base.json b/packages/fiber/tsconfig.base.json new file mode 100644 index 00000000..7e5ac952 --- /dev/null +++ b/packages/fiber/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "incremental": true, + "allowJs": true, + "importHelpers": false, + "declaration": true, + "declarationMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/fiber/tsconfig.commonjs.json b/packages/fiber/tsconfig.commonjs.json new file mode 100644 index 00000000..baad27db --- /dev/null +++ b/packages/fiber/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/fiber/tsconfig.json b/packages/fiber/tsconfig.json new file mode 100644 index 00000000..c8645909 --- /dev/null +++ b/packages/fiber/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "node16", + "outDir": "./dist", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + } +} diff --git a/packages/fiber/typedoc.json b/packages/fiber/typedoc.json new file mode 100644 index 00000000..63f8fa3f --- /dev/null +++ b/packages/fiber/typedoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts", "./src/advanced.ts"], + "extends": ["../../typedoc.base.json"], + "name": "@ckb-ccc fiber" +} diff --git a/packages/playground/package.json b/packages/playground/package.json index d5bf9de0..53576390 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -41,4 +41,4 @@ "raw-loader": "^4.0.2", "tailwindcss": "^3.4.1" } -} \ No newline at end of file +} diff --git a/packages/playground/tailwind.config.ts b/packages/playground/tailwind.config.ts index 1af2b12b..2d90dce8 100644 --- a/packages/playground/tailwind.config.ts +++ b/packages/playground/tailwind.config.ts @@ -9,7 +9,7 @@ const config: Config = { safelist: [ { pattern: /./, // Include all Tailwind classes - } + }, ], theme: { extend: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37d82b22..8ba40e30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,7 +223,7 @@ importers: dependencies: '@joyid/ckb': specifier: ^1.1.1 - version: 1.1.1(typescript@5.7.3) + version: 1.1.1(typescript@5.7.3)(zod@3.24.2) '@noble/ciphers': specifier: ^0.5.3 version: 0.5.3 @@ -334,6 +334,9 @@ importers: '@ckb-ccc/connector-react': specifier: workspace:* version: link:../connector-react + '@ckb-ccc/fiber': + specifier: workspace:* + version: link:../fiber '@ckb-ccc/lumos-patches': specifier: workspace:* version: link:../lumos-patches @@ -584,6 +587,76 @@ importers: specifier: ^5.1.3 version: 5.7.3 + packages/fiber: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + axios: + specifier: ^1.7.7 + version: 1.7.9 + devDependencies: + '@eslint/js': + specifier: ^9.1.1 + version: 9.20.0 + '@types/chai': + specifier: ^5.2.0 + version: 5.2.1 + '@types/jest': + specifier: ^29.5.0 + version: 29.5.14 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^22.10.0 + version: 22.13.1 + chai: + specifier: ^5.2.0 + version: 5.2.0 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + dotenv: + specifier: ^16.4.5 + version: 16.4.7 + eslint: + specifier: ^9.1.0 + version: 9.20.0(jiti@1.21.7) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.20.0(jiti@1.21.7)) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.2.3(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.20.0(jiti@1.21.7)))(eslint@9.20.0(jiti@1.21.7))(prettier@3.5.1) + jest: + specifier: ^29.5.0 + version: 29.7.0(@types/node@22.13.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) + mocha: + specifier: ^11.1.0 + version: 11.1.0 + prettier: + specifier: ^3.2.5 + version: 3.5.1 + prettier-plugin-organize-imports: + specifier: ^3.2.4 + version: 3.2.4(prettier@3.5.1)(typescript@5.7.3) + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + ts-jest: + specifier: ^29.1.0 + version: 29.2.5(@babel/core@7.26.8)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.8))(jest@29.7.0(@types/node@22.13.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)))(typescript@5.7.3) + typescript: + specifier: ^5.4.5 + version: 5.7.3 + typescript-eslint: + specifier: ^7.7.0 + version: 7.18.0(eslint@9.20.0(jiti@1.21.7))(typescript@5.7.3) + zod: + specifier: ^3.22.4 + version: 3.24.2 + packages/joy-id: dependencies: '@ckb-ccc/core': @@ -591,10 +664,10 @@ importers: version: link:../core '@joyid/ckb': specifier: ^1.1.1 - version: 1.1.1(typescript@5.7.3) + version: 1.1.1(typescript@5.7.3)(zod@3.24.2) '@joyid/common': specifier: ^0.2.0 - version: 0.2.0(typescript@5.7.3) + version: 0.2.0(typescript@5.7.3)(zod@3.24.2) devDependencies: '@eslint/js': specifier: ^9.1.1 @@ -649,7 +722,7 @@ importers: version: 0.24.0-next.2 '@joyid/ckb': specifier: ^1.1.1 - version: 1.1.1(typescript@5.7.3) + version: 1.1.1(typescript@5.7.3)(zod@3.24.2) devDependencies: '@eslint/js': specifier: ^9.1.1 @@ -2184,12 +2257,18 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/chai@5.2.1': + resolution: {integrity: sha512-iu1JLYmGmITRzUgNiLMZD3WCoFzpYtueuyAgHTXqgwSRAMIlFTnZqG6/xenkpUGRJEzSfklUTI4GNSzks/dc0w==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/deep-freeze-strict@1.1.2': resolution: {integrity: sha512-VvMETBojHvhX4f+ocYTySQlXMZfxKV3Jyb7iCWlWaC+exbedkv6Iv2bZZqI736qXjVguH6IH7bzwMBMfTT+zuQ==} @@ -2256,6 +2335,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -2661,6 +2743,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -2812,6 +2898,9 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + browserify-aes@1.2.0: resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} @@ -2903,6 +2992,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2924,6 +3017,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -3164,6 +3261,10 @@ packages: supports-color: optional: true + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -3172,6 +3273,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-freeze-strict@1.1.1: resolution: {integrity: sha512-QemROZMM2IvhAcCFvahdX2Vbm4S/txeq5rFYU9fh4mQP79WTMW5c/HkQ2ICl1zuzcDZdPZ6zarDxQeQMsVYoNA==} @@ -3242,6 +3347,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3694,6 +3803,10 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} @@ -3901,6 +4014,10 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hex-rgb@4.3.0: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} @@ -4068,6 +4185,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4598,6 +4719,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4748,6 +4872,11 @@ packages: engines: {node: '>=10'} hasBin: true + mocha@11.1.0: + resolution: {integrity: sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + monaco-editor@0.51.0: resolution: {integrity: sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==} @@ -5020,6 +5149,10 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6105,6 +6238,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6182,6 +6318,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} @@ -6201,6 +6341,9 @@ packages: yoga-wasm-web@0.3.3: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -6269,7 +6412,7 @@ snapshots: '@babel/types': 7.26.8 '@types/gensync': 1.0.4 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6427,7 +6570,7 @@ snapshots: '@babel/parser': 7.26.8 '@babel/template': 7.26.8 '@babel/types': 7.26.8 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6705,7 +6848,7 @@ snapshots: '@eslint/config-array@0.19.2': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -6721,7 +6864,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -6735,7 +6878,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -6778,7 +6921,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7184,9 +7327,9 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joyid/ckb@1.1.1(typescript@5.7.3)': + '@joyid/ckb@1.1.1(typescript@5.7.3)(zod@3.24.2)': dependencies: - '@joyid/common': 0.2.0(typescript@5.7.3) + '@joyid/common': 0.2.0(typescript@5.7.3)(zod@3.24.2) '@nervosnetwork/ckb-sdk-utils': 0.109.5 cross-fetch: 4.0.0 uncrypto: 0.1.3 @@ -7195,9 +7338,9 @@ snapshots: - typescript - zod - '@joyid/common@0.2.0(typescript@5.7.3)': + '@joyid/common@0.2.0(typescript@5.7.3)(zod@3.24.2)': dependencies: - abitype: 0.8.7(typescript@5.7.3) + abitype: 0.8.7(typescript@5.7.3)(zod@3.24.2) type-fest: 4.6.0 transitivePeerDependencies: - typescript @@ -7663,14 +7806,20 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.17.17 + '@types/node': 22.13.1 + + '@types/chai@5.2.1': + dependencies: + '@types/deep-eql': 4.0.2 '@types/connect@3.4.38': dependencies: - '@types/node': 20.17.17 + '@types/node': 22.13.1 '@types/cookiejar@2.1.5': {} + '@types/deep-eql@4.0.2': {} + '@types/deep-freeze-strict@1.1.2': {} '@types/eslint-scope@3.7.7': @@ -7687,7 +7836,7 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 20.17.17 + '@types/node': 22.13.1 '@types/qs': 6.9.18 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -7746,6 +7895,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/mocha@10.0.10': {} + '@types/node@12.20.55': {} '@types/node@20.17.17': @@ -7780,12 +7931,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.17.17 + '@types/node': 22.13.1 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.17.17 + '@types/node': 22.13.1 '@types/send': 0.17.4 '@types/stack-utils@2.0.3': {} @@ -7794,7 +7945,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 20.17.17 + '@types/node': 22.13.1 form-data: 4.0.1 '@types/supertest@6.0.2': @@ -7864,7 +8015,7 @@ snapshots: '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -7882,7 +8033,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 optionalDependencies: typescript: 5.7.3 @@ -7895,7 +8046,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 9.20.0(jiti@1.21.7) optionalDependencies: typescript: 5.7.3 @@ -7908,7 +8059,7 @@ snapshots: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 optionalDependencies: typescript: 5.7.3 @@ -7929,7 +8080,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: @@ -7941,7 +8092,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) '@typescript-eslint/utils': 7.18.0(eslint@9.20.0(jiti@1.21.7))(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 9.20.0(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: @@ -7953,7 +8104,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.7.3) '@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: @@ -7969,7 +8120,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -7984,7 +8135,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/visitor-keys': 7.2.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -8129,9 +8280,11 @@ snapshots: '@xtuc/long@4.2.2': {} - abitype@0.8.7(typescript@5.7.3): + abitype@0.8.7(typescript@5.7.3)(zod@3.24.2): dependencies: typescript: 5.7.3 + optionalDependencies: + zod: 3.24.2 abort-controller@3.0.0: dependencies: @@ -8303,6 +8456,8 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} async-function@1.0.0: {} @@ -8512,6 +8667,8 @@ snapshots: brorand@1.1.0: {} + browser-stdout@1.3.1: {} + browserify-aes@1.2.0: dependencies: buffer-xor: 1.0.3 @@ -8612,6 +8769,14 @@ snapshots: ccount@2.0.1: {} + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -8627,6 +8792,8 @@ snapshots: chardet@0.7.0: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -8892,12 +9059,18 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: + debug@4.4.0(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} dedent@1.5.3: {} + deep-eql@5.0.2: {} + deep-freeze-strict@1.1.1: {} deep-is@0.1.4: {} @@ -8954,6 +9127,8 @@ snapshots: diff@4.0.2: {} + diff@5.2.0: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -9158,7 +9333,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -9178,7 +9353,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -9212,7 +9387,7 @@ snapshots: eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) enhanced-resolve: 5.18.1 eslint: 8.57.1 fast-glob: 3.3.3 @@ -9221,14 +9396,14 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) enhanced-resolve: 5.18.1 eslint: 8.57.1 fast-glob: 3.3.3 @@ -9237,7 +9412,18 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.7.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -9252,7 +9438,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -9263,7 +9449,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9281,7 +9467,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -9369,6 +9555,16 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 9.1.0(eslint@9.20.0(jiti@1.21.7)) + eslint-plugin-prettier@5.2.3(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.20.0(jiti@1.21.7)))(eslint@9.20.0(jiti@1.21.7))(prettier@3.5.1): + dependencies: + eslint: 9.20.0(jiti@1.21.7) + prettier: 3.5.1 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 9.1.0(eslint@9.20.0(jiti@1.21.7)) + eslint-plugin-react-hooks@5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -9427,7 +9623,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -9474,7 +9670,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -9712,6 +9908,8 @@ snapshots: flatted: 3.3.2 keyv: 4.5.4 + flat@5.0.2: {} + flatted@3.3.2: {} follow-redirects@1.15.9: {} @@ -9963,6 +10161,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + he@1.2.0: {} + hex-rgb@4.3.0: {} hexoid@2.0.0: {} @@ -10148,6 +10348,8 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@2.1.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.3 @@ -10239,7 +10441,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -10248,7 +10450,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -10953,7 +11155,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.17.17 + '@types/node': 22.13.1 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11154,6 +11356,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.3: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -11293,6 +11497,29 @@ snapshots: mkdirp@1.0.4: {} + mocha@11.1.0: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.4.0(supports-color@8.1.1) + diff: 5.2.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.4.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.1.6 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + monaco-editor@0.51.0: {} mri@1.2.0: {} @@ -11576,6 +11803,8 @@ snapshots: path-type@4.0.0: {} + pathval@2.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -11659,7 +11888,6 @@ snapshots: dependencies: prettier: 3.5.1 typescript: 5.7.3 - optional: true prettier-plugin-tailwindcss@0.5.14(prettier-plugin-organize-imports@3.2.4(prettier@3.4.2)(typescript@5.7.3))(prettier@3.4.2): dependencies: @@ -12261,7 +12489,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.1 formidable: 3.5.2 @@ -12814,6 +13042,8 @@ snapshots: word-wrap@1.2.5: {} + workerpool@6.5.1: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -12869,6 +13099,13 @@ snapshots: yargs-parser@21.1.1: {} + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + yargs@16.2.0: dependencies: cliui: 7.0.4 @@ -12895,4 +13132,6 @@ snapshots: yoga-wasm-web@0.3.3: {} + zod@3.24.2: {} + zwitch@2.0.4: {}