diff --git a/.env.example b/.env.example index 2c0bdbe3bd..6f48f0e7fc 100644 --- a/.env.example +++ b/.env.example @@ -373,3 +373,15 @@ FUEL_WALLET_PRIVATE_KEY= # Tokenizer Settings TOKENIZER_MODEL= # Specify the tokenizer model to be used. TOKENIZER_TYPE= # Options: tiktoken (for OpenAI models) or auto (AutoTokenizer from Hugging Face for non-OpenAI models). Default: tiktoken. + +# CKB Fiber Configuration +FIBER_ENABLE=true + +FIBER_RPC_URL= # RPC url of your Fiber node, anyone can control the Fiber node through this RPC, so make sure that the RPC url cannot be accessed externally. Default is http://127.0.0.1:8227 +FIBER_RPC_HEADERS= # (Optional) The headers of the Fiber RPC (JSON string). It is used to authenticate the Fiber RPC if needed + +FIBER_DEFAULT_PEER_ID=QmeiL6iR7EvxFM6vkRufj9SbKfiRPHcfofMQfoEFgUJ3Qe # The default peer id of the Fiber node, it is used to connect to the Fiber network, you can get it from https://testnet.explorer.nervos.org/fiber/graph/nodes +FIBER_DEFAULT_PEER_ADDRESS=/ip4/127.0.0.1/tcp/8230/p2p/QmeiL6iR7EvxFM6vkRufj9SbKfiRPHcfofMQfoEFgUJ3Qe # The default peer address of the Fiber node, it is used to connect to the Fiber network, you can get it from https://testnet.explorer.nervos.org/fiber/graph/nodes + +FIBER_CKB_FUNDING_AMOUNT= # (Optional) The default funding amount when open the default channel +FIBER_UDT_FUNDING_AMOUNTS= # (Optional) The default funding udt amounts (JSON string) when open the default channel diff --git a/.gitignore b/.gitignore index 00d0cb4d77..1d4b451d5b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ coverage .eslintcache agent/content +/key/ diff --git a/agent/package.json b/agent/package.json index f4fa0f33e0..e460f1cbd4 100644 --- a/agent/package.json +++ b/agent/package.json @@ -61,6 +61,7 @@ "@elizaos/plugin-fuel": "workspace:*", "@elizaos/plugin-avalanche": "workspace:*", "@elizaos/plugin-web-search": "workspace:*", + "@elizaos/plugin-ckb-fiber": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/agent/src/index.ts b/agent/src/index.ts index 53058cf4ec..732ca54128 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -64,6 +64,7 @@ import { abstractPlugin } from "@elizaos/plugin-abstract"; import { avalanchePlugin } from "@elizaos/plugin-avalanche"; import { webSearchPlugin } from "@elizaos/plugin-web-search"; import { echoChamberPlugin } from "@elizaos/plugin-echochambers"; +import { ckbFiberPlugin } from "@elizaos/plugin-ckb-fiber"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -609,6 +610,9 @@ export async function createAgent( getSecret(character, "ECHOCHAMBERS_API_KEY") ? echoChamberPlugin : null, + getSecret(character, "FIBER_ENABLE") + ? ckbFiberPlugin + : null, ].filter(Boolean), providers: [], actions: [], diff --git a/packages/plugin-ckb-fiber/.env.example b/packages/plugin-ckb-fiber/.env.example new file mode 100644 index 0000000000..af3564caf7 --- /dev/null +++ b/packages/plugin-ckb-fiber/.env.example @@ -0,0 +1,11 @@ +# CKB Fiber Configuration +FIBER_ENABLE=true + +FIBER_RPC_URL= # RPC url of your Fiber node, anyone can control the Fiber node through this RPC, so make sure that the RPC url cannot be accessed externally. Default is http://127.0.0.1:8227 +FIBER_RPC_HEADERS= # (Optional) The headers of the Fiber RPC (JSON string). It is used to authenticate the Fiber RPC if needed + +FIBER_DEFAULT_PEER_ID=QmeiL6iR7EvxFM6vkRufj9SbKfiRPHcfofMQfoEFgUJ3Qe # The default peer id of the Fiber node, it is used to connect to the Fiber network, you can get it from https://testnet.explorer.nervos.org/fiber/graph/nodes +FIBER_DEFAULT_PEER_ADDRESS=/ip4/127.0.0.1/tcp/8230/p2p/QmeiL6iR7EvxFM6vkRufj9SbKfiRPHcfofMQfoEFgUJ3Qe # The default peer address of the Fiber node, it is used to connect to the Fiber network, you can get it from https://testnet.explorer.nervos.org/fiber/graph/nodes + +FIBER_CKB_FUNDING_AMOUNT= # (Optional) The default funding amount when open the default channel +FIBER_UDT_FUNDING_AMOUNTS= # (Optional) The default funding udt amounts (JSON string) when open the default channel diff --git a/packages/plugin-ckb-fiber/.gitignore b/packages/plugin-ckb-fiber/.gitignore new file mode 100644 index 0000000000..485dee64bc --- /dev/null +++ b/packages/plugin-ckb-fiber/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/packages/plugin-ckb-fiber/.npmignore b/packages/plugin-ckb-fiber/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/plugin-ckb-fiber/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-ckb-fiber/README.md b/packages/plugin-ckb-fiber/README.md new file mode 100644 index 0000000000..a6abdc7440 --- /dev/null +++ b/packages/plugin-ckb-fiber/README.md @@ -0,0 +1,133 @@ +# CKB Fiber Plugin +[Fiber](https://github.com/nervosnetwork/fiber) is a type of Lightning Network that enables AI Agents to make stablecoin payments using the Lightning Network. This PR introduces a new plugin: the CKB Fiber Plugin, enabling the Eliza agent to interact with the Fiber Network by managing a Fiber node. We implemented a standalone Fiber Network RPC client and developed a series of actions within the Eliza framework. These actions leverage the RPC client to send RPC requests and facilitate communication with the Fiber node. + +# Configuration +The plugin requires the following environment variables: +``` env +# CKB Fiber Configuration +FIBER_ENABLE=true + +FIBER_RPC_URL= # RPC url of your Fiber node, anyone can control the Fiber node through this RPC, so make sure that the RPC url cannot be accessed externally. Default is http://127.0.0.1:8227 +FIBER_RPC_HEADERS= # (Optional) The headers of the Fiber RPC (JSON string). It is used to authenticate the Fiber RPC if needed + +FIBER_DEFAULT_PEER_ID=QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ # The default peer id of the Fiber node, it is used to connect to the Fiber network, you can get it from https://testnet.explorer.nervos.org/fiber/graph/nodes +FIBER_DEFAULT_PEER_ADDRESS=/ip4/43.198.162.23/tcp/8228/p2p/QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ # The default peer address of the Fiber node, it is used to connect to the Fiber network, you can get it from https://testnet.explorer.nervos.org/fiber/graph/nodes + +FIBER_CKB_FUNDING_AMOUNT= # (Optional) The default funding amount when open the default channel +FIBER_UDT_FUNDING_AMOUNTS= # (Optional) The default funding udt amounts (JSON string) when open the default channel +``` +You can check the default values in `src/constants.ts` + +# Available Actions + +1. **GET_INFO**: Retrieves the current node information from the agent-controlled Fiber node. + +2. **LIST_CHANNELS**: Lists all the opened channels of the agent-controlled Fiber node. + +3. **SEND_PAYMENT**: Given an invoice, payment amount, and asset type (CKB, USDI, or RUSD) in the context, the agent-controlled Fiber node will verify and send the payment. + +4. **GET_PAYMENT**: Given a specific payment hash, retrieves the payment details. + +5. **NEW_INVOICE**: Given an amount and asset type (CKB, USDI, or RUSD), the agent generates a new invoice, enabling others to send funds to the agent-controlled Fiber node. + +# Cautions & Notes +- **Start Your Fiber Node:** Before using the CKB Fiber Plugin, ensure that your Fiber node is up and running. The Fiber node should be connected to the Fiber network and have sufficient channels opened to facilitate payment transactions. Check [nervosnetwork/fiber](https://github.com/nervosnetwork/fiber) for more information. + +- **Network Access Control:** If the Fiber node’s RPC URL is exposed to the external network without proper access control, there could be potential unauthorized access. Ensure the RPC URL is secured or only accessible within the local network to avoid unintended exposure. + +- **Sensitive Actions:** Some actions, such as transfers, involve handling sensitive operations. These actions should be properly secured to prevent users from directly triggering them in inappropriate scenarios (e.g., unauthorized transfers). Ensuring proper access control and validation for these actions is critical. + +- **Channel and Network Connectivity:** If the Fiber node is not connected to the network or doesn't have enough channels opened, SEND_PAYMENT action will fail. This could affect the overall functionality and cause disruptions in the intended operations of the Fiber node and Eliza Agent interactions. + +# Benefits + +### Seamless CKB Integration + +- **Native CKB Support**: Effortlessly integrate CKB blockchain functionalities into your applications. +- **Efficient Transaction Handling**: Streamlined processes for sending and receiving CKB transactions. + +### Robust Financial Operations + +- **Automated Payment Processing**: Facilitate automated payments for services such as API calls, content generation, and more. +- **Multi-Layer Transaction Support**: Handle both on-chain and Layer 2 transactions for enhanced flexibility and scalability. +- **Real-Time Balance Monitoring**: Keep track of wallet balances with up-to-date information and notifications. + +### Flexible Payment Options + +- **Multi-Currency Support**: Accept payments in various cryptocurrencies and fiat currencies with automatic conversion. +- **Customizable Fee Structures**: Adjust transaction fees based on real-time market data and user preferences. +- **Recurring Payments**: Set up recurring billing for subscription-based services, ensuring consistent revenue streams. + +### Enhanced Security + +- **Secure Key Management**: Protect private keys using environment variables and secure storage solutions. +- **Advanced Encryption**: Ensure all sensitive data is encrypted both in transit and at rest to maintain user privacy. + +# Implementation Status + +### Core CKB Functionality + +- Full support for CKB blockchain operations, including transaction creation and validation. +- Integration with CKB's native wallets and APIs for seamless user interactions. + +### Fiber Framework Integration + +- Seamless incorporation with the Fiber web framework, ensuring high performance and scalability. +- Middleware support for efficient request handling and processing within Fiber applications. + +### Payment Processing Features + +- Automated payment workflows implemented, enabling smooth financial transactions between agents. +- Support for both on-chain and Layer 2 transactions to cater to different scalability and cost requirements. + +### USD Denomination Support + +- Enable USD-denominated transactions with automatic cryptocurrency conversion based on real-time exchange rates. +- Integration of real-time price feeds to ensure accurate fee estimation and financial reporting. + +### Upcoming Features + +- **Enhanced Analytics**: Development of advanced dashboards for monitoring transaction metrics and user activity (planned). +- **Extended Protocol Support**: Ongoing work to integrate additional blockchain protocols, expanding the plugin's versatility and applicability. + + + +# Testing +``` bash +cd packages/plugin-ckb-fiber +pnpm test +``` + +# Usage + +## Integration + +``` typescript + +// Assume this is the user's message received by the client +const message: Memory = { + id: stringToUuid( + Date.now() + "-" + runtime.agentId + ), + userId: runtime.agentId, + agentId: runtime.agentId, + content: { + text: "Send me 60 USDI, my invoice is: fibt600000001pp8msdlfq6gqzg7dwfhddw3x46u2xydkgzm2e37kp6dr75yakkemrxjm67xjucccgj7hz46reg9m90gvax25pgfcrerysr67fesg34zzsu895nns8g78ua6x23f3w9xjyfzwht9grq5aa2vwaz0gaaxme6dqxfypk3g02753fc0a6e4e4jx7r982qv282mutcw8zzrx3y992av365sfv2pgpschnwn5wv3lglel8x96adqemcsp9j0l2rfue2rvp9yj60320wdewqj8aln2c3dh04s30nxg0hn0vufhdj8gkcvt5h4h8gfr02k8x6rnyulnlqgt5gqzmhkchn6tcqtgk0zkglgrl0wg8ede99gv204rgsqqjge9mq07u23f7vxfcdzpm57rt72359vp0yad9pkl5ttae44vxd5rzq09m2w8rc0ydryljywvgqj2gq0d", + action: "SEND_PAYMENT", + }, + roomId: runtime.agentId, + embedding: getEmbeddingZeroVector(), + createdAt: Date.now(), +}; + +// Process the message, it will trigger the SEND_PAYMENT action +// callback is a function to handle the response (e.g., send the response back to the user) +runtime.processActions(message, [], state, callback) + +``` diff --git a/packages/plugin-ckb-fiber/eslint.config.mjs b/packages/plugin-ckb-fiber/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-ckb-fiber/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-ckb-fiber/package.json b/packages/plugin-ckb-fiber/package.json new file mode 100644 index 0000000000..4207654124 --- /dev/null +++ b/packages/plugin-ckb-fiber/package.json @@ -0,0 +1,26 @@ +{ + "name": "@elizaos/plugin-ckb-fiber", + "version": "0.1.0", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "@ckb-ccc/core": "^0.1.0-alpha.4" + }, + "devDependencies": { + "tsup": "8.3.5", + "axios": "1.7.8", + "zod": "3.23.8", + "@types/node": "^20.0.0", + "@vitest/coverage-v8": "^0.34.6", + "vitest": "^0.34.6" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache plugin", + + "test": "vitest" + } +} diff --git a/packages/plugin-ckb-fiber/src/actions/closeChannel.ts b/packages/plugin-ckb-fiber/src/actions/closeChannel.ts new file mode 100644 index 0000000000..29b9a97572 --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/closeChannel.ts @@ -0,0 +1,132 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + composeContext, + generateObject, + ModelClass, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import { z } from "zod"; + +const schema = z.object({ + channelId: z.string(), + force: z.boolean().optional(), +}); + +type Content = { + channelId: string; + force?: boolean; +} + +const template = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "channelId": "0x86e89949ffed979c6215fc3c013fb107c31c5b3041244f64d09b5472b60d0fe9", + "force": false +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about closing a channel: +- Channel ID (the unique identifier of the channel to close) +- Force close (whether to force close the channel, default to false) + +Respond with a JSON markdown block containing only the extracted values.` + +export const closeChannel: Action = { + name: "CLOSE_CHANNEL", + similes: ["SHUTDOWN_CHANNEL", "TERMINATE_CHANNEL"], + description: "Close an existing payment channel", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) { + return false; + } + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + + // Initialize or update state + if (!state) { + state = await runtime.composeState(_message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose channel context + const context = composeContext({ state, template }); + + // Generate channel content + const content = (await generateObject({ + runtime, context, modelClass: ModelClass.SMALL, schema + })).object as Content; + + const nodeInfo = await service.rpcClient.getNodeInfo(); + + await service.rpcClient.shutdownChannel({ + channel_id: content.channelId, + close_script: nodeInfo.default_funding_lock_script, + force: content.force + }); + + callback( + { + text: `Successfully initiated channel closure for ${content.channelId}${content.force ? ' (force close)' : ''}. Please wait for the closure to complete.`, + channelId: content.channelId + }, + [] + ); + } catch (error) { + elizaLogger.error("Error closing channel:", error); + callback( + { text: `Failed to close channel, message: ${error.message}` }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Close channel 0x86e89949ffed979c6215fc3c013fb107c31c5b3041244f64d09b5472b60d0fe9", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll help you close the channel. Let me process that for you.", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Force close channel 0x86e89949ffed979c6215fc3c013fb107c31c5b3041244f64d09b5472b60d0fe9", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll help you force close the channel. Let me process that for you.", + }, + }, + ], + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/connectPeer.ts b/packages/plugin-ckb-fiber/src/actions/connectPeer.ts new file mode 100644 index 0000000000..168f8b7c2a --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/connectPeer.ts @@ -0,0 +1,105 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + composeContext, + generateObject, + ModelClass, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import { z } from "zod"; + +const schema = z.object({ + address: z.string(), +}); + +type Content = { + address: string; +} + +const template = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "address": "/ip4/43.198.162.23/tcp/8228/p2p/QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ" +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the peer connection: +- Address (the address of the peer node, e.g., "/ip4/43.198.162.23/tcp/8228/p2p/QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ") + +Respond with a JSON markdown block containing only the extracted values.` + +export const connectPeer: Action = { + name: "CONNECT_PEER", + similes: ["CONNECT_NODE", "PEER_CONNECT"], + description: "Connect to a peer node", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) { + return false; + } + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + + // Initialize or update state + if (!state) { + state = await runtime.composeState(_message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose connection context + const context = composeContext({ state, template }); + + // Generate connection content + const content = (await generateObject({ + runtime, context, modelClass: ModelClass.SMALL, schema + })).object as Content; + + await service.rpcClient.connectPeer({ address: content.address }); + + callback( + { text: `Successfully connected to peer ${content.address}` }, + [] + ); + } catch (error) { + elizaLogger.error("Error connecting to peer:", error); + callback( + { text: `Failed to connect to peer, message: ${error.message}` }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Connect to peer /ip4/43.198.162.23/tcp/8228/p2p/QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll help you connect to the peer node. Let me process that for you.", + }, + }, + ], + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/getInfo.ts b/packages/plugin-ckb-fiber/src/actions/getInfo.ts new file mode 100644 index 0000000000..c52ca443ac --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/getInfo.ts @@ -0,0 +1,61 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import {formatNodeInfo} from "../ckb/fiber/formatter.ts"; + +export const getInfo: Action = { + name: "GET_NODE_INFO", + similes: ["GET_NODE", "GET_INFO", "SHOW_INFO", "SHOW_NODE"], + description: "Get fiber node information", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) + return false + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + const nodeInfo = await service.rpcClient.getNodeInfo(); + const formattedInfo = formatNodeInfo(nodeInfo); + + callback({ text: formattedInfo, action: "LIST_CHANNELS" }, []); + } catch (error) { + elizaLogger.error("Error getting node info:", error); + callback( + { text: "Fail to get node information. Please try again later." }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Get your node information", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Show your node info", + }, + } + ], + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/getPayment.ts b/packages/plugin-ckb-fiber/src/actions/getPayment.ts new file mode 100644 index 0000000000..17f0d26381 --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/getPayment.ts @@ -0,0 +1,154 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + composeContext, + generateObject, + ModelClass, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import { z } from "zod"; +import {formatPayment} from "../ckb/fiber/formatter.ts"; + +const schema = z.object({ + paymentHash: z.string(), +}); + +type Content = { + paymentHash: string; +} + +const template = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "paymentHash": "0xffb18f0bee5b9554dc388f8ec33145751c14e3cf4714ec070c55aa6a6853912f", +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested token transfer: +- Payment Hash + +Respond with a JSON markdown block containing only the extracted values.` + +export const getPayment: Action = { + name: "GET_PAYMENT", + similes: ["GET_PAY_RESULT", "GET_INVOICE_RESULT", "PAYMENT_RESULT"], + description: "Get the payment result", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) + return false + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + + // Initialize or update state + if (!state) { + state = await runtime.composeState(_message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose transfer context + const transferContext = composeContext({ state, template, }); + + // Generate transfer content + const content = (await generateObject({ + runtime, context: transferContext, + modelClass: ModelClass.SMALL, schema + })).object as Content; + + const paymentHash = content.paymentHash; + + const payment = await service.rpcClient.getPayment({ payment_hash: paymentHash }); + + return callback({ text: formatPayment(payment) }, []); + } catch (error) { + elizaLogger.error("Error getting payment:", error); + callback( + { text: `Fail to get payment, message: ${error.message}` }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Send me 60 USDI, my invoice is: fibt600000001pp8msdlfq6gqzg7dwfhddw3x46u2xydkgzm2e37kp6dr75yakkemrxjm67xjucccgj7hz46reg9m90gvax25pgfcrerysr67fesg34zzsu895nns8g78ua6x23f3w9xjyfzwht9grq5aa2vwaz0gaaxme6dqxfypk3g02753fc0a6e4e4jx7r982qv282mutcw8zzrx3y992av365sfv2pgpschnwn5wv3lglel8x96adqemcsp9j0l2rfue2rvp9yj60320wdewqj8aln2c3dh04s30nxg0hn0vufhdj8gkcvt5h4h8gfr02k8x6rnyulnlqgt5gqzmhkchn6tcqtgk0zkglgrl0wg8ede99gv204rgsqqjge9mq07u23f7vxfcdzpm57rt72359vp0yad9pkl5ttae44vxd5rzq09m2w8rc0ydryljywvgqj2gq0d", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm sending...", + action: "SEND_PAYMENT" + } + }, + { + user: "{{agentName}}", + content: { + text: "I've paid the invoice.", + } + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Send 177 CKB to fibt177000000001p53d6ghq0axgfw0pnm6vk7l8tjrkeuqwaknxj0pq9juyuvzkyjr45flh25p0ktwjkswjaurmk0xsemmcq5pc5sztl6p6q99me0rwvyap6wd8m8thl4arfadcv9gteph8ranvt9cyc6ntf2c723khc7t9843ugktdc4htjeredgfacvkl2ljfxvw6njgvn7ww82zf7ly76cqaqnayem5cf07v9jwcqklgrzc25t35rqtm380f4hjzdm4rt5xna7ygclw0l2xcl7vs4pz5z6lwuan3e0lw985thjankl33edg74jt8ncqyadzek", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm sending...", + action: "SEND_PAYMENT" + } + }, + { + user: "{{agentName}}", + content: { + text: "I've paid the invoice.", + } + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Send 9999 CKB to fibt177000000001p53d6ghq0axgfw0pnm6vk7l8tjrkeuqwaknxj0pq9juyuvzkyjr45flh25p0ktwjkswjaurmk0xsemmcq5pc5sztl6p6q99me0rwvyap6wd8m8thl4arfadcv9gteph8ranvt9cyc6ntf2c723khc7t9843ugktdc4htjeredgfacvkl2ljfxvw6njgvn7ww82zf7ly76cqaqnayem5cf07v9jwcqklgrzc25t35rqtm380f4hjzdm4rt5xna7ygclw0l2xcl7vs4pz5z6lwuan3e0lw985thjankl33edg74jt8ncqyadzek", + } + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm sending...", + action: "SEND_PAYMENT" + } + }, + { + user: "{{agentName}}", + content: { + text: "Sorry, I can't pay for this invoice: Invoice amount does not match transfer amount: Invoice amount 177, Transfer amount 9999", + } + } + ] + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/listChannels.ts b/packages/plugin-ckb-fiber/src/actions/listChannels.ts new file mode 100644 index 0000000000..1ae2947caa --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/listChannels.ts @@ -0,0 +1,61 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import {formatChannelList, formatNodeInfo} from "../ckb/fiber/formatter.ts"; + +export const listChannels: Action = { + name: "LIST_CHANNELS", + similes: ["GET_CHANNELS", "SHOW_CHANNELS"], + description: "List the open channels", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) + return false + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + const channels = await service.rpcClient.listChannels() + const formattedInfo = formatChannelList(channels); + + callback({ text: formattedInfo }, []); + } catch (error) { + elizaLogger.error("Error getting channels:", error); + callback( + { text: "Fail to get channels. Please try again later." }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "List the open channels", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Show channels", + }, + } + ], + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/newInvoice.ts b/packages/plugin-ckb-fiber/src/actions/newInvoice.ts new file mode 100644 index 0000000000..2412aca9fb --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/newInvoice.ts @@ -0,0 +1,125 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + composeContext, + generateObject, + ModelClass, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import { z } from "zod"; +import {formatInvoice} from "../ckb/fiber/formatter.ts"; + +const schema = z.object({ + amount: z.number(), + tokenType: z.string(), +}); + +type Content = { + amount: number; + tokenType: string; +} + +const template = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "amount": 1, + "tokenType": "CKB", +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the new invoice: +- Invoice amount (The amount to be received) +- Token type (e.g., "USDI", "CKB", default to "CKB") + +Respond with a JSON markdown block containing only the extracted values.` + +export const newInvoice: Action = { + name: "NEW_INVOICE", + similes: ["CREATE_INVOICE", "RECEIVE_FUND", "REQUEST_PAYMENT"], + description: "Get the payment result", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) + return false + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + + // Initialize or update state + if (!state) { + state = await runtime.composeState(_message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose transfer context + const context = composeContext({ state, template, }); + + // Generate transfer content + const content = (await generateObject({ + runtime, context, modelClass: ModelClass.SMALL, schema + })).object as Content; + + content.tokenType = content.tokenType || "ckb"; + const udtType = content.tokenType.toLowerCase() === "ckb" ? undefined : content.tokenType.toLowerCase(); + + const invoice = await service.newInvoice(content.amount, udtType); + + return callback({ text: formatInvoice(invoice) }, []); + } catch (error) { + elizaLogger.error("Error create invoice:", error); + callback( + { text: `Fail to create invoice, message: ${error.message}` }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Receive 177 CKB", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm creating an invoice to receive 177 CKB...", + action: "NEW_INVOICE" + } + } + ], + [ + { + user: "{{user1}}", + content: { + text: "I want to send you 10 USDI", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm creating an invoice to receive 10 USDI...", + action: "NEW_INVOICE" + } + } + ] + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/openChannel.ts b/packages/plugin-ckb-fiber/src/actions/openChannel.ts new file mode 100644 index 0000000000..b048d04209 --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/openChannel.ts @@ -0,0 +1,133 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + composeContext, + generateObject, + ModelClass, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import { z } from "zod"; + +const schema = z.object({ + peerId: z.string(), + fundingAmount: z.number(), + tokenType: z.string().optional(), +}); + +type Content = { + peerId: string; + fundingAmount: number; + tokenType: string; +} + +const template = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "peerId": "QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ", + "fundingAmount": 1000", + "tokenType": "CKB" +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about opening a channel: +- Peer ID (the ID of the peer node) +- Funding amount (the amount to fund the channel) +- Token type (e.g., "USDI", "CKB", default to "CKB") + +Respond with a JSON markdown block containing only the extracted values.` + +export const openChannel: Action = { + name: "OPEN_CHANNEL", + similes: ["CREATE_CHANNEL", "NEW_CHANNEL"], + description: "Open a new payment channel with a peer", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) { + return false; + } + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + + // Initialize or update state + if (!state) { + state = await runtime.composeState(_message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose channel context + const context = composeContext({ state, template }); + + // Generate channel content + const content = (await generateObject({ + runtime, context, modelClass: ModelClass.SMALL, schema + })).object as Content; + + content.tokenType = content.tokenType || "ckb"; + const udtType = content.tokenType.toLowerCase() === "ckb" ? undefined : content.tokenType.toLowerCase(); + + const tempChannelId = await service.openChannel(content.peerId, content.fundingAmount, true, udtType); + + callback( + { + text: `Successfully opened channel with peer ${content.peerId}. Temporary channel ID is: ${tempChannelId}, please wait for the channel to be accepted and ready. Notice: Once the channel is accepted by the remote node, this temporary channel ID will be disabled automatically.`, + channelId: tempChannelId + }, + [] + ); + } catch (error) { + elizaLogger.error("Error opening channel:", error); + callback( + { text: `Failed to open channel, message: ${error.message}` }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Open a channel with peer QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ with 1000 CKB", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll help you open a channel with the specified peer. Let me process that for you.", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Create a new USDI payment channel with QmSqSsbjkQG6aMBNEkG6hMLKQpMjLW3eaBmUdCghpRWqwJ, funding amount 500", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll help you create a USDI payment channel with the specified peer. Let me process that for you.", + }, + }, + ], + ], +}; diff --git a/packages/plugin-ckb-fiber/src/actions/sendPayment.ts b/packages/plugin-ckb-fiber/src/actions/sendPayment.ts new file mode 100644 index 0000000000..549cf6a1c3 --- /dev/null +++ b/packages/plugin-ckb-fiber/src/actions/sendPayment.ts @@ -0,0 +1,162 @@ +import { + Action, + IAgentRuntime, + Memory, + HandlerCallback, + State, + composeContext, + generateObject, + ModelClass, + elizaLogger, +} from "@elizaos/core"; + +import {CKBFiberService, ServiceTypeCKBFiber} from "../ckb/fiber/service.ts"; +import { z } from "zod"; +import {formatPayment} from "../ckb/fiber/formatter.ts"; + +const schema = z.object({ + invoice: z.string(), + amount: z.string(), + tokenType: z.string(), +}); + +type Content = { + invoice: string; + amount: number; + tokenType: string; +} + +const template = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "invoice": "fibt177000000001p53d6ghq0axgfw0pnm6vk7l8tjrkeuqwaknxj0pq9juyuvzkyjr45flh25p0ktwjkswjaurmk0xsemmcq5pc5sztl6p6q99me0rwvyap6wd8m8thl4arfadcv9gteph8ranvt9cyc6ntf2c723khc7t9843ugktdc4htjeredgfacvkl2ljfxvw6njgvn7ww82zf7ly76cqaqnayem5cf07v9jwcqklgrzc25t35rqtm380f4hjzdm4rt5xna7ygclw0l2xcl7vs4pz5z6lwuan3e0lw985thjankl33edg74jt8ncqyadzek", + "amount": 177, + "tokenType": "CKB", +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested token transfer: +- Invoice +- Amount +- Token type (e.g., "USDI", "CKB", default to "CKB") + +Respond with a JSON markdown block containing only the extracted values.` + +export const sendPayment: Action = { + name: "SEND_PAYMENT", + similes: ["SEND_FUND", "SEND_TOKEN", "SEND_CKB", "SEND_UDT", "PAY_INVOICE"], + description: "Send payment for a invoice", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (!await runtime.getService(ServiceTypeCKBFiber)?.checkNode()) + return false + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + try { + const service = runtime.getService(ServiceTypeCKBFiber); + + // Initialize or update state + if (!state) { + state = await runtime.composeState(_message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose transfer context + const context = composeContext({ state, template, }); + + // Generate transfer content + const content = (await generateObject({ + runtime, context, modelClass: ModelClass.SMALL, schema + })).object as Content; + + content.tokenType = content.tokenType || "ckb"; + const udtType = content.tokenType.toLowerCase() === "ckb" ? undefined : content.tokenType.toLowerCase(); + + const payment = await service.sendPayment(content.invoice, content.amount, udtType); + + return callback({ text: `Payment sent successfully!\n${formatPayment(payment)}` }, []); + } catch (error) { + elizaLogger.error("Error sending payment:", error); + callback( + { text: `Fail to send payment, message: ${error.message}` }, + [] + ); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Send me 60 USDI, my invoice is: fibt600000001pp8msdlfq6gqzg7dwfhddw3x46u2xydkgzm2e37kp6dr75yakkemrxjm67xjucccgj7hz46reg9m90gvax25pgfcrerysr67fesg34zzsu895nns8g78ua6x23f3w9xjyfzwht9grq5aa2vwaz0gaaxme6dqxfypk3g02753fc0a6e4e4jx7r982qv282mutcw8zzrx3y992av365sfv2pgpschnwn5wv3lglel8x96adqemcsp9j0l2rfue2rvp9yj60320wdewqj8aln2c3dh04s30nxg0hn0vufhdj8gkcvt5h4h8gfr02k8x6rnyulnlqgt5gqzmhkchn6tcqtgk0zkglgrl0wg8ede99gv204rgsqqjge9mq07u23f7vxfcdzpm57rt72359vp0yad9pkl5ttae44vxd5rzq09m2w8rc0ydryljywvgqj2gq0d", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm sending...", + action: "SEND_PAYMENT" + } + }, + { + user: "{{agentName}}", + content: { + text: "I've paid the invoice.", + } + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Send 177 CKB to fibt177000000001p53d6ghq0axgfw0pnm6vk7l8tjrkeuqwaknxj0pq9juyuvzkyjr45flh25p0ktwjkswjaurmk0xsemmcq5pc5sztl6p6q99me0rwvyap6wd8m8thl4arfadcv9gteph8ranvt9cyc6ntf2c723khc7t9843ugktdc4htjeredgfacvkl2ljfxvw6njgvn7ww82zf7ly76cqaqnayem5cf07v9jwcqklgrzc25t35rqtm380f4hjzdm4rt5xna7ygclw0l2xcl7vs4pz5z6lwuan3e0lw985thjankl33edg74jt8ncqyadzek", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm sending...", + action: "SEND_PAYMENT" + } + }, + { + user: "{{agentName}}", + content: { + text: "I've paid the invoice.", + } + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Send 9999 CKB to fibt177000000001p53d6ghq0axgfw0pnm6vk7l8tjrkeuqwaknxj0pq9juyuvzkyjr45flh25p0ktwjkswjaurmk0xsemmcq5pc5sztl6p6q99me0rwvyap6wd8m8thl4arfadcv9gteph8ranvt9cyc6ntf2c723khc7t9843ugktdc4htjeredgfacvkl2ljfxvw6njgvn7ww82zf7ly76cqaqnayem5cf07v9jwcqklgrzc25t35rqtm380f4hjzdm4rt5xna7ygclw0l2xcl7vs4pz5z6lwuan3e0lw985thjankl33edg74jt8ncqyadzek", + } + }, + { + user: "{{agentName}}", + content: { + text: "Okay, I'm sending...", + action: "SEND_PAYMENT" + } + }, + { + user: "{{agentName}}", + content: { + text: "Sorry, I can't pay for this invoice: Invoice amount does not match transfer amount: Invoice amount 177, Transfer amount 9999", + } + } + ] + ], +}; diff --git a/packages/plugin-ckb-fiber/src/ckb/ccc-client.ts b/packages/plugin-ckb-fiber/src/ckb/ccc-client.ts new file mode 100644 index 0000000000..176947c76c --- /dev/null +++ b/packages/plugin-ckb-fiber/src/ckb/ccc-client.ts @@ -0,0 +1,31 @@ +import offCKB, { Network } from "./offckb.config"; +import { ccc, CellDepInfoLike, KnownScript, Script } from "@ckb-ccc/core"; + +export const DEVNET_SCRIPTS: Record< + string, + Pick & { cellDeps: CellDepInfoLike[] } +> = { + [KnownScript.Secp256k1Blake160]: + offCKB.systemScripts.secp256k1_blake160_sighash_all!.script, + [KnownScript.Secp256k1Multisig]: + offCKB.systemScripts.secp256k1_blake160_multisig_all!.script, + [KnownScript.AnyoneCanPay]: offCKB.systemScripts.anyone_can_pay!.script, + [KnownScript.OmniLock]: offCKB.systemScripts.omnilock!.script, + [KnownScript.XUdt]: offCKB.systemScripts.xudt!.script, +}; + +export function buildCccClient(network: Network) { + const client = + network === "mainnet" + ? new ccc.ClientPublicMainnet() + : network === "testnet" + ? new ccc.ClientPublicTestnet() + : new ccc.ClientPublicTestnet({ + url: offCKB.rpcUrl, + scripts: DEVNET_SCRIPTS as any, + }); + + return client; +} + +export const cccClient = buildCccClient(offCKB.currentNetwork); diff --git a/packages/plugin-ckb-fiber/src/ckb/fiber/formatter.ts b/packages/plugin-ckb-fiber/src/ckb/fiber/formatter.ts new file mode 100644 index 0000000000..fec91bf7ec --- /dev/null +++ b/packages/plugin-ckb-fiber/src/ckb/fiber/formatter.ts @@ -0,0 +1,187 @@ +import { + ChannelListResponse, + GetNodeInfoResponse, + GetNodeInfoResponseNumberKeys, InvoiceResponse, PaymentResponse, + UdtArgInfoNumberKeys, +} from "./types.ts"; +import {convert} from "./rpcClient.ts"; +import {SupportedUDTs, CKBDecimal} from "../../constants.ts"; +import {cccClient} from "../ccc-client.ts"; +import {ccc} from "@ckb-ccc/core"; +import {toDecimal, udtEq} from "../../utils.ts"; +import {elizaLogger} from "@elizaos/core"; + +export function formatNodeInfo(nodeInfo: GetNodeInfoResponse): string { + nodeInfo = convert(nodeInfo, GetNodeInfoResponseNumberKeys); + nodeInfo.udt_cfg_infos = nodeInfo.udt_cfg_infos.map(udt => convert(udt, UdtArgInfoNumberKeys)); + + const { + version, + commit_hash, + node_name, + node_id, + addresses, + open_channel_auto_accept_min_ckb_funding_amount, + auto_accept_channel_ckb_funding_amount, + tlc_expiry_delta, + default_funding_lock_script, + tlc_min_value, + tlc_max_value, + tlc_fee_proportional_millionths, + channel_count, + pending_channel_count, + peers_count, + udt_cfg_infos, + } = nodeInfo; + + // Format UDT information + const supportedUdtKeys = Object.keys(SupportedUDTs) + + const udtInfos = supportedUdtKeys + .map(udtType => ({ + name: udtType, ...SupportedUDTs[udtType], + config: udt_cfg_infos.find(udt => udtEq(SupportedUDTs[udtType].script, udt.script)) + })) + .filter(udt => !!udt.config) + .map(udt => `- $${udt.name.toUpperCase()}: ${udt.description} + Decimal: ${udt.decimal} + Auto Accept Amount: ${toDecimal(udt.config.auto_accept_amount, udt.name)} ${udt.name.toUpperCase()}`) + .join('\n'); + + const lockScript = { + codeHash: default_funding_lock_script.code_hash, + hashType: default_funding_lock_script.hash_type, + args: default_funding_lock_script.args + } + const script = ccc.Script.from(lockScript) + const address = ccc.Address.fromScript(script, cccClient) + + // Format node status + return `Node Status: +- Version: ${version} +- Name: ${node_name || 'Unnamed Node'} +- Node ID: ${node_id} +- Wallet: ${address.toString()} +- Channels: ${channel_count} (${pending_channel_count} pending) +- Connected Peers: ${peers_count} + +Channel Configuration: +- TLC Min Value: ${toDecimal(tlc_min_value)} CKB +- TLC Max Value: ${toDecimal(tlc_max_value)} CKB +- TLC Fee Rate: ${tlc_fee_proportional_millionths} millionths +- Auto Accept Funding: ${toDecimal(open_channel_auto_accept_min_ckb_funding_amount)} CKB + +Supported UDT Tokens: +${udtInfos}`; +} + +export function formatChannelList(channelList: ChannelListResponse): string { + if (!channelList.channels || channelList.channels.length === 0) { + return 'No channels found.'; + } + + const channels = channelList.channels.map(channel => { + const { + channel_id, + peer_id, + state, + local_balance, + remote_balance, + offered_tlc_balance, + received_tlc_balance, + funding_udt_type_script, + is_public + } = channel; + + // Try to find matching UDT type + let balanceFormat = ''; + + if (funding_udt_type_script) { + const udtType = Object.entries(SupportedUDTs).find(([_, udt]) => + udtEq(udt.script, funding_udt_type_script) + ); + if (udtType) { + const [name] = udtType; + balanceFormat = ` + Local: ${toDecimal(local_balance, name)} ${name} (${toDecimal(offered_tlc_balance, name)} ${name} TLC offered) + Remote: ${toDecimal(remote_balance, name)} ${name} (${toDecimal(received_tlc_balance, name)} ${name} TLC received)`; + } + } else { + // Default to CKB + balanceFormat = ` + Local: ${toDecimal(local_balance)} CKB (${toDecimal(offered_tlc_balance)} CKB TLC offered) + Remote: ${toDecimal(remote_balance)} CKB (${toDecimal(received_tlc_balance)} CKB TLC received)`; + } + + return `- Channel ${channel_id}: + To Peer: ${peer_id} + State: ${state.state_name} [${state?.state_flags?.join?.(', ') || 'null'}] + Public: ${is_public}${balanceFormat}`; + }); + + return `Channels:\n${channels.join('\n\n')}`; +} + +export function formatPayment(payment: PaymentResponse): string { + const { + payment_hash, + status, + created_at, + last_updated_at, + failed_error, + fee + } = payment; + + // Format payment status + const statusInfo = failed_error + ? `Failed: ${failed_error}` + : `Status: ${status}`; + + // Format timestamps + const createdDate = new Date(Number(created_at)).toLocaleString(); + const lastUpdatedDate = new Date(Number(last_updated_at)).toLocaleString(); + + return `Payment Details: +- Payment Hash: ${payment_hash} (Not Tx Hash!) +- Created: ${createdDate} +- Last Updated: ${lastUpdatedDate} +- ${statusInfo} +- Fee: ${toDecimal(fee)} CKB`; +} + +export function formatInvoice(response: InvoiceResponse): string { + const { invoice_address, invoice } = response; + const { amount, data } = invoice; + const { timestamp, payment_hash, attrs } = data; + + // Get udtType + const flattenAttrs = attrs.map((attr) => Object.entries(attr)).flat(); + const udtScriptAttr = flattenAttrs.find(([key]) => key === "UdtScript"); + + let udtType: string = null; + if (udtScriptAttr) { + const script = ccc.Script.fromBytes(udtScriptAttr[1] as string); + const script_ = { + code_hash: script.codeHash, + hash_type: script.hashType, + args: script.args, + }; + const udt = Object.entries(SupportedUDTs).find(([, udt]) => udtEq(script_, udt.script)) + if (udt) udtType = udt[0]; + else udtType = ''; + } + const displayAmount = toDecimal(amount, udtType); + + // Format timestamp + const createdDate = new Date(Number(timestamp)).toLocaleString(); + + // Format status if available + const statusInfo = 'status' in response ? `\n- Status: ${response.status}` : ''; + + return `Invoice Details: +- Invoice: ${invoice_address} (Use this to receive payment) +- Payment Hash: ${payment_hash} +- Created: ${createdDate} +- Token: ${udtType?.toUpperCase() || 'CKB'} +- Amount: ${displayAmount}${statusInfo}`; +} diff --git a/packages/plugin-ckb-fiber/src/ckb/fiber/rpc-document.md b/packages/plugin-ckb-fiber/src/ckb/fiber/rpc-document.md new file mode 100644 index 0000000000..6d6e76552c --- /dev/null +++ b/packages/plugin-ckb-fiber/src/ckb/fiber/rpc-document.md @@ -0,0 +1,714 @@ + +# Fiber Network Node RPC + +The RPC module provides a set of APIs for developers to interact with FNN. Please note that APIs are not stable yet and may change in the future. + +Allowing arbitrary machines to access the JSON-RPC port (using the `rpc.listening_addr` configuration option) is **dangerous and strongly discouraged**. Please strictly limit the access to only trusted machines. + +You may refer to the e2e test cases in the `tests/bruno/e2e` directory for examples of how to use the RPC. + + + +* [RPC Methods](#rpc-methods) + + + * [Module Cch](#module-cch) + * [Method `send_btc`](#cch-send_btc) + * [Method `receive_btc`](#cch-receive_btc) + * [Method `get_receive_btc_order`](#cch-get_receive_btc_order) + * [Module Channel](#module-channel) + * [Method `open_channel`](#channel-open_channel) + * [Method `accept_channel`](#channel-accept_channel) + * [Method `list_channels`](#channel-list_channels) + * [Method `shutdown_channel`](#channel-shutdown_channel) + * [Method `update_channel`](#channel-update_channel) + * [Module Dev](#module-dev) + * [Method `commitment_signed`](#dev-commitment_signed) + * [Method `add_tlc`](#dev-add_tlc) + * [Method `remove_tlc`](#dev-remove_tlc) + * [Method `submit_commitment_transaction`](#dev-submit_commitment_transaction) + * [Module Graph](#module-graph) + * [Method `graph_nodes`](#graph-graph_nodes) + * [Method `graph_channels`](#graph-graph_channels) + * [Module Info](#module-info) + * [Method `node_info`](#info-node_info) + * [Module Invoice](#module-invoice) + * [Method `new_invoice`](#invoice-new_invoice) + * [Method `parse_invoice`](#invoice-parse_invoice) + * [Method `get_invoice`](#invoice-get_invoice) + * [Method `cancel_invoice`](#invoice-cancel_invoice) + * [Module Payment](#module-payment) + * [Method `send_payment`](#payment-send_payment) + * [Method `get_payment`](#payment-get_payment) + * [Module Peer](#module-peer) + * [Method `connect_peer`](#peer-connect_peer) + * [Method `disconnect_peer`](#peer-disconnect_peer) +* [RPC Types](#rpc-types) + + * [Type `Channel`](#type-channel) + * [Type `ChannelInfo`](#type-channelinfo) + * [Type `ChannelState`](#type-channelstate) + * [Type `NodeInfo`](#type-nodeinfo) + * [Type `RemoveTlcReason`](#type-removetlcreason) + * [Type `UdtArgInfo`](#type-udtarginfo) + * [Type `UdtCellDep`](#type-udtcelldep) + * [Type `UdtCfgInfos`](#type-udtcfginfos) + * [Type `UdtScript`](#type-udtscript) +## RPC Modules + + +### Module `Cch` +RPC module for cross chain hub demonstration. + This is the seccond line + + + +#### Method `send_btc` + +Send BTC to a address. + +##### Params + +* `btc_pay_req` - String, Bitcoin payment request string +* `currency` - Currency, Request currency + +##### Returns + +* `timestamp` - u64, Seconds since epoch when the order is created +* `expiry` - u64, Seconds after timestamp that the order expires +* `ckb_final_tlc_expiry_delta` - u64, The minimal expiry in seconds of the final TLC in the CKB network +* `currency` - Currency, Request currency +* `wrapped_btc_type_script` - ckb_jsonrpc_types::Script, Wrapped BTC type script +* `btc_pay_req` - String, Payment request for BTC +* `ckb_pay_req` - String, Payment request for CKB +* `payment_hash` - String, Payment hash for the HTLC for both CKB and BTC. +* `amount_sats` - u128, Amount required to pay in Satoshis, including fee +* `fee_sats` - u128, Fee in Satoshis +* `status` - CchOrderStatus, Order status + + + +#### Method `receive_btc` + +Receive BTC from a payment hash. + +##### Params + +* `payment_hash` - String, Payment hash for the HTLC for both CKB and BTC. +* `channel_id` - Hash256, Channel ID for the CKB payment. +* `amount_sats` - u128, How many satoshis to receive, excluding cross-chain hub fee. +* `final_tlc_expiry` - u64, Expiry set for the HTLC for the CKB payment to the payee. + +##### Returns + +* `timestamp` - u64, Seconds since epoch when the order is created +* `expiry` - u64, Seconds after timestamp that the order expires +* `ckb_final_tlc_expiry_delta` - u64, The minimal expiry in seconds of the final TLC in the CKB network +* `wrapped_btc_type_script` - ckb_jsonrpc_types::Script, Wrapped BTC type script +* `btc_pay_req` - String, Payment request for BTC +* `payment_hash` - String, Payment hash for the HTLC for both CKB and BTC. +* `channel_id` - Hash256, Channel ID for the CKB payment. +* `tlc_id` - `Option`, TLC ID for the CKB payment. +* `amount_sats` - u128, Amount will be received by the payee +* `fee_sats` - u128, Fee in Satoshis +* `status` - CchOrderStatus, Order status + + + +#### Method `get_receive_btc_order` + +Get receive BTC order by payment hash. + +##### Params + +* `payment_hash` - String, Payment hash for the HTLC for both CKB and BTC. + +##### Returns + +* `timestamp` - u64, Seconds since epoch when the order is created +* `expiry` - u64, Seconds after timestamp that the order expires +* `ckb_final_tlc_expiry_delta` - u64, The minimal expiry in seconds of the final TLC in the CKB network +* `wrapped_btc_type_script` - ckb_jsonrpc_types::Script, Wrapped BTC type script +* `btc_pay_req` - String, Payment request for BTC +* `payment_hash` - String, Payment hash for the HTLC for both CKB and BTC. +* `channel_id` - Hash256, Channel ID for the CKB payment. +* `tlc_id` - `Option`, TLC ID for the CKB payment. +* `amount_sats` - u128, Amount will be received by the payee +* `fee_sats` - u128, Fee in Satoshis +* `status` - CchOrderStatus, Order status + + + +### Module `Channel` +RPC module for channel management. + + + +#### Method `open_channel` + +Attempts to open a channel with a peer. + +##### Params + +* `peer_id` - PeerId, The peer ID to open a channel with, the peer must be connected through the [connect_peer](#peer-connect_peer) rpc first. +* `funding_amount` - u128, The amount of CKB or UDT to fund the channel with. +* `public` - `Option`, Whether this is a public channel (will be broadcasted to network, and can be used to forward TLCs), an optional parameter, default value is true. +* `funding_udt_type_script` - `Option