diff --git a/.ckb-version b/.ckb-version index 72a8a63..5ee8d4a 100644 --- a/.ckb-version +++ b/.ckb-version @@ -1 +1 @@ -0.41.0 +0.42.0-rc1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 82088b2..f2c2c72 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: macOS | [Tippy.dmg](https://github.com/nervosnetwork/tippy/releases/download/${{ steps.get_tag.outputs.tag }}/Tippy.dmg) | TODO Linux | [tippy-linux-x64.tar.gz](https://github.com/nervosnetwork/tippy/releases/download/${{ steps.get_tag.outputs.tag }}/tippy-linux-x64.tar.gz) | TODO draft: true - prerelease: true + prerelease: false - name: Output upload URL file run: echo "${{ steps.create_release.outputs.upload_url }}" > upload_url.txt - name: Save upload URL file diff --git a/README.md b/README.md index 39b2f9b..9afedce 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,291 @@ brew install gdb --HEAD --build-from-source brew install ttyd ``` +## API + +Tippy exposes a set of RPCs in JSON-RPC 2.0 protocols for controlling a devchain. + +It also proxies API calls to the active running devchain for transparent CKB interactions. + +The URL of Tippy RPC is http://localhost:5000/api. + +### CKB RPCs + +For CKB RPCs, simply call any API method with Tippy API URL. For example: + +``` +echo '{ + "id": 2, + "jsonrpc": "2.0", + "method": "get_tip_block_number", + "params": [] +}' \ +| tr -d '\n' \ +| curl -H 'content-type: application/json' -d @- \ +http://localhost:5000/api +``` + +See [CKB JSON-RPC doc](https://docs.nervos.org/docs/reference/rpc) for more information. + +### Tippy RPCs + +#### Method `create_chain` +* `create_chain({assembler_lock_arg, genesis_issued_cells})` + * `assembler_lock_arg`(optional): Lock arg for block assembler (miner address). + * `genesis_issued_cells`(optional): An array of genesis issued cells. See example for the structure. +* result: `{ id, name }` + +Create a devchain and set it as current active chain. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "create_chain", + "params": [ + { + "assembler_lock_arg": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d8", + "genesis_issued_cells": [ + { + "capacity": "0x5af3107a4000", + "lock": { + "code_hash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "args": "0xf2cb132b2f6849ef8abe57e98cddf713bb8d71cb", + "hash_type": "type" + } + } + ] + } + ] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": { + "id": 4, + "name": "CKB devchain" + } + } + +#### Method `start_chain` + +* `start_chain()` +* result: `"ok"` + +Start the current active chain. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "start_chain", + "params": [] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "ok" + } + +#### Method `stop_chain` + +* `stop_chain()` +* result: `"ok"` + +Stop the current active chain if it's running. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "stop_chain", + "params": [] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "ok" + } + +#### Method `start_miner` + +* `start_miner()` +* result: `"ok"` + +Start the default miner. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "start_miner", + "params": [] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "ok" + } + +#### Method `stop_miner` + +* `stop_miner()` +* result: `"ok"` + +Stop the current running default miner. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "stop_miner", + "params": [] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "ok" + } + +#### Method `mine_blocks` + +* `mine_blocks(number_of_blocks)` +* result: `"Wait for blocks to be mined."` + +Mine `number_of_blocks` blocks at the interval of 1 second. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "mine_blocks", + "params": [3] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "Wait for blocks to be mined." + } + +#### Method `revert_blocks` + +* `revert_blocks(number_of_blocks)` +* result: `"Reverted blocks."` + +Mine `number_of_blocks` blocks at the interval of 1 second. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "revert_blocks", + "params": [3] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "Reverted blocks." + } + +#### Method `ban_transaction` + +* `ban_transaction(tx_hash, type)` + * `tx_hash`: Tx hash of the transaction. + * `type`: Deny type, `propose` or `commit`. +* result: `"Added to denylist."` + +Add a tx to denylist. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "ban_transaction", + "params": ["0x9a0580274e9921e04e139214b58ffc60df1625055ab7806ee635b56d329d7732", "propose"] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "Added to denylist." + } + + +#### Method `unban_transaction` + +* `unban_transaction(tx_hash, type)` + * `tx_hash`: Tx hash of the transaction. + * `type`: Deny type, `propose` or `commit`. +* result: `"Removed from denylist."` + +Remove a tx from denylist. + +**Example** + +Request + + { + "id": "1", + "jsonrpc": "2.0", + "method": "unban_transaction", + "params": ["0x9a0580274e9921e04e139214b58ffc60df1625055ab7806ee635b56d329d7732", "propose"] + } + +Response + + { + "jsonrpc": "2.0", + "id": "1", + "result": "Removed from denylist." + } + ## Contributing 1. Fetch the codebase: `git clone https://github.com/nervosnetwork/tippy.git` diff --git a/src/Tippy.Ctrl/Process/BinariesInfo.cs b/src/Tippy.Ctrl/Process/BinariesInfo.cs index 2130fa2..a77e1f7 100644 --- a/src/Tippy.Ctrl/Process/BinariesInfo.cs +++ b/src/Tippy.Ctrl/Process/BinariesInfo.cs @@ -5,9 +5,8 @@ namespace Tippy.Ctrl.Process { internal class BinariesInfo { - internal string Info { get; private set; } = ""; - internal bool HasDebuggerDeps { get; private set; } = false; - + internal string Info { get; private set; } = ""; + internal bool HasDebuggerDeps { get; private set; } = false; readonly List binaries = new() { "ckb", "ckb-indexer", "ckb-debugger"/*, "ckb-cli"*/ }; internal void Refresh() @@ -41,32 +40,32 @@ internal void Refresh() } if (!OperatingSystem.IsWindows()) - { + { RefreshDebuggerDeps(); } } - void RefreshDebuggerDeps() - { - HasDebuggerDeps = true; - // BUGBUG: .Net won't find commands on M1 Mac which has homebrew location at `/opt/homebrew/bin`. + void RefreshDebuggerDeps() + { + HasDebuggerDeps = true; + // BUGBUG: .Net won't find commands on M1 Mac which has homebrew location at `/opt/homebrew/bin`. foreach (var dep in new List() { "gdb", "ttyd" }) { - try - { - System.Diagnostics.Process process = new(); - process.StartInfo.UseShellExecute = false; - process.StartInfo.FileName = dep; - process.StartInfo.Arguments = "--version"; - process.StartInfo.WorkingDirectory = Core.Environment.GetAppDataFolder(); - process.Start(); - process.WaitForExit(); + try + { + System.Diagnostics.Process process = new(); + process.StartInfo.UseShellExecute = false; + process.StartInfo.FileName = dep; + process.StartInfo.Arguments = "--version"; + process.StartInfo.WorkingDirectory = Core.Environment.GetAppDataFolder(); + process.Start(); + process.WaitForExit(); } catch - { - HasDebuggerDeps = false; + { + HasDebuggerDeps = false; } - } + } } } } diff --git a/src/Tippy.Ctrl/Process/CommandProcess.cs b/src/Tippy.Ctrl/Process/CommandProcess.cs index d4e3c35..549b78d 100644 --- a/src/Tippy.Ctrl/Process/CommandProcess.cs +++ b/src/Tippy.Ctrl/Process/CommandProcess.cs @@ -15,7 +15,7 @@ abstract class CommandProcess internal CommandProcess(ProcessInfo processInfo) => ProcessInfo = processInfo; - abstract protected void Configure(); + protected abstract void Configure(); public bool IsRunning { diff --git a/src/Tippy.Ctrl/Process/Debugger/DebuggerProcess.cs b/src/Tippy.Ctrl/Process/Debugger/DebuggerProcess.cs index 12d9601..c4303f5 100644 --- a/src/Tippy.Ctrl/Process/Debugger/DebuggerProcess.cs +++ b/src/Tippy.Ctrl/Process/Debugger/DebuggerProcess.cs @@ -5,12 +5,12 @@ namespace Tippy.Ctrl.Process.Debugger { internal class DebuggerProcess : CommandProcess { - private string ScriptHash; - private string ScriptGroupType; - private string TxFilePath; - private string IoType; - private int IoIndex; - private string? BinaryPath; + private readonly string ScriptHash; + private readonly string ScriptGroupType; + private readonly string TxFilePath; + private readonly string IoType; + private readonly int IoIndex; + private readonly string? BinaryPath; public DebuggerProcess(ProcessInfo info, string scriptGroupType, string scriptHash, string txFilePath, string ioType, int ioIndex, string? binaryPath = null) : base(info) { diff --git a/src/Tippy.Ctrl/Process/NodeProcess.cs b/src/Tippy.Ctrl/Process/NodeProcess.cs index bbfe45b..0309bff 100644 --- a/src/Tippy.Ctrl/Process/NodeProcess.cs +++ b/src/Tippy.Ctrl/Process/NodeProcess.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -96,15 +95,12 @@ void UpdateConfiguration() string BuildArguments() { - switch (ProcessInfo.Chain) + return ProcessInfo.Chain switch { - case Core.Models.Project.ChainType.Testnet: - return $"init --chain testnet --ba-arg {ProcessInfo.LockArg}"; - case Core.Models.Project.ChainType.Mainnet: - return $"init --chain mainnet --ba-arg {ProcessInfo.LockArg}"; - default: - return $"init --chain dev --ba-arg {ProcessInfo.LockArg} --import-spec -"; - } + Core.Models.Project.ChainType.Testnet => $"init --chain testnet --ba-arg {ProcessInfo.LockArg}", + Core.Models.Project.ChainType.Mainnet => $"init --chain mainnet --ba-arg {ProcessInfo.LockArg}", + _ => $"init --chain dev --ba-arg {ProcessInfo.LockArg} --import-spec -", + }; } string ChainSpec() diff --git a/src/Tippy.Ctrl/ProcessManager.cs b/src/Tippy.Ctrl/ProcessManager.cs index ebc5de1..46159ff 100644 --- a/src/Tippy.Ctrl/ProcessManager.cs +++ b/src/Tippy.Ctrl/ProcessManager.cs @@ -31,7 +31,7 @@ public enum MinerMode Sophisticated, } - static List processGroups = new List(); + static readonly List processGroups = new(); static ProcessGroup? GroupFor(Project project) => processGroups.Find(g => g.ProcessInfo == ProcessInfo.FromProject(project)); diff --git a/src/Tippy/Api/TippyRpc.cs b/src/Tippy/Api/TippyRpc.cs index cd1a1e6..fc42d63 100644 --- a/src/Tippy/Api/TippyRpc.cs +++ b/src/Tippy/Api/TippyRpc.cs @@ -29,7 +29,18 @@ public TippyRpc(TippyDbContext context) readonly TippyDbContext dbContext; - readonly HashSet methods = new() { "create_chain", "start_chain", "stop_chain", "mine_blocks", "revert_blocks" }; + readonly HashSet methods = new() + { + "create_chain", + "start_chain", + "stop_chain", + "start_miner", + "stop_miner", + "mine_blocks", + "revert_blocks", + "ban_transaction", + "unban_transaction", + }; [HttpPost] public ActionResult Index() @@ -94,8 +105,12 @@ internal async Task Handle() "create_chain" => await CreateChain(), "start_chain" => StartChain(), "stop_chain" => StopChain(), + "start_miner" => StartMiner(), + "stop_miner" => StopMiner(), "mine_blocks" => MineBlocks(), "revert_blocks" => RevertBlocks(), + "ban_transaction" => await BanTransaction(), + "unban_transaction" => await UnbanTransaction(), // TODO: other apis _ => "TODO", }; @@ -180,6 +195,40 @@ object StopChain() return "ok"; } + // Start the default miner + object StartMiner() + { + if (project == null) + { + throw new Exception("No active chain. Create a chain first."); + } + + if (!ProcessManager.IsRunning(project)) + { + throw new Exception("Active chain is not running."); + } + + ProcessManager.StartMiner(project, ProcessManager.MinerMode.Default); + return "ok"; + } + + // Stop the running default miner + object StopMiner() + { + if (project == null) + { + throw new Exception("No active chain. Create a chain first."); + } + + if (!ProcessManager.IsRunning(project)) + { + throw new Exception("Active chain is not running."); + } + + ProcessManager.StopMiner(project); + return "ok"; + } + // Mine N blocks at the default 1sec interval. object MineBlocks() { @@ -254,6 +303,74 @@ object RevertBlocks() return "Reverted blocks."; } + + // Add a transaction to denylist + async Task BanTransaction() + { + if (project == null) + { + throw new Exception("No active chain. Create a chain first."); + } + + if (request.Params == null || request.Params.Length != 2) + { + throw new Exception("Must provide params as tx hash and deny type."); + } + + var txHash = request.Params[0].ToString()!; + var type = request.Params[1].ToString()!; + try + { + var item = new DeniedTransaction + { + ProjectId = project.Id, + TxHash = txHash, + DenyType = type == "propose" ? DeniedTransaction.Type.Propose : DeniedTransaction.Type.Commit + }; + dbContext.DeniedTransactions.Add(item); + await dbContext.SaveChangesAsync(); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + return "Added to denylist."; + } + + // Remove a transaction from denylist + async Task UnbanTransaction() + { + if (project == null) + { + throw new Exception("No active chain. Create a chain first."); + } + + if (request.Params == null || request.Params.Length != 2) + { + throw new Exception("Must provide params as tx hash and deny type."); + } + + var txHash = request.Params[0].ToString()!; + var type = request.Params[1].ToString()!; + try + { + var denyType = type == "propose" ? DeniedTransaction.Type.Propose : DeniedTransaction.Type.Commit; + var item = dbContext + .DeniedTransactions + .Where(t => t.TxHash == txHash && t.DenyType == denyType) + .First(); + if (item != null) + { + dbContext.DeniedTransactions.Remove(item); + await dbContext.SaveChangesAsync(); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + return "Removed from denylist."; + } } class RequestObject diff --git a/src/Tippy/Pages/Doc/Api.cshtml b/src/Tippy/Pages/Doc/Api.cshtml deleted file mode 100644 index b6715d2..0000000 --- a/src/Tippy/Pages/Doc/Api.cshtml +++ /dev/null @@ -1,172 +0,0 @@ -@page -@model Tippy.Pages.Doc.ApiModel -@{ ViewData["Title"] = "RPC API"; } - -

RPC API

- - - -
-

- Tippy exposes a set of RPCs in JSON-RPC 2.0 protocols for controlling a devchain. - It also proxies API calls to the active running devchain for transparent CKB interactions. -

- -

- The URL of Tippy RPC is http://localhost:5000/api. -

- -

CKB RPCs

- -

- For CKB RPCs, simply call any API with Tippy API URL. For example: -

- -
echo '{
-    "id": 2,
-    "jsonrpc": "2.0",
-    "method": "get_tip_block_number",
-    "params": []
-}' \
-| tr -d '\n' \
-| curl -H 'content-type: application/json' -d @@- \
-http://localhost:5000/api
- -

See CKB JSON-RPC doc for more information.

- -

Tippy RPCs

- -

Method create_chain

-
    -
  • create_chain({assembler_lock_arg, genesis_issued_cells})
  • -
  • -
      -
    • assember_lock_arg(optional): Lock arg for block assembler (miner address).
    • -
    • genesis_issued_cells(optional): An array of genesis issued cells. See example for the structure.
    • -
    -
  • -
  • result: { id, name }
  • -
-

Create a devchain and set it as current active chain.

-
Examples
-

Request

-
{
-    "id": "1",
-    "jsonrpc": "2.0",
-    "method": "create_chain",
-    "params": [
-        {
-            "assembler_lock_arg": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d8",
-            "genesis_issued_cells": [
-                {
-                    "capacity": "0x5af3107a4000",
-                    "lock": {
-                        "code_hash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
-                        "args": "0xf2cb132b2f6849ef8abe57e98cddf713bb8d71cb",
-                        "hash_type": "type"
-                    }
-                }
-            ]
-        }
-    ]
-}
-
-

Response

-
{
-    "jsonrpc": "2.0",
-    "id": "1",
-    "result": {
-      "id": 4,
-      "name": "CKB devchain"
-    } 
-}
- -

Method start_chain

-
    -
  • start_chain()
  • -
  • result: "ok"
  • -
-

Start the current active chain.

-
Examples
-

Request

-
{
-    "id": "1",
-    "jsonrpc": "2.0",
-    "method": "start_chain",
-    "params": []
-}
-

Response

-
{
-    "jsonrpc": "2.0",
-    "id": "1",
-    "result": "ok"
-}
- -

Method stop_chain

-
    -
  • stop_chain()
  • -
  • result: "ok"
  • -
-

Stop the current active chain if it's running.

-
Examples
-

Request

-
{
-    "id": "1",
-    "jsonrpc": "2.0",
-    "method": "stop_chain",
-    "params": []
-}
-

Response

-
{
-    "jsonrpc": "2.0",
-    "id": "1",
-    "result": "ok"
-}
- -

Method mine_blocks

-
    -
  • mine_blocks(number_of_blocks)
  • -
  • result: "Wait for blocks to be mined."
  • -
-

Mine number_of_blocks blocks at the interval of 1 second.

-
Examples
-

Request

-
{
-    "id": "1",
-    "jsonrpc": "2.0",
-    "method": "mine_blocks",
-    "params": [3]
-}
-

Response

-
{
-    "jsonrpc": "2.0",
-    "id": "1",
-    "result": "Wait for blocks to be mined."
-}
- -

Method revert_blocks

-
    -
  • revert_blocks(number_of_blocks)
  • -
  • result: "Reverted blocks."
  • -
-

Mine number_of_blocks blocks at the interval of 1 second.

-
Examples
-

Request

-
{
-    "id": "1",
-    "jsonrpc": "2.0",
-    "method": "revert_blocks",
-    "params": [3]
-}
-

Response

-
{
-    "jsonrpc": "2.0",
-    "id": "1",
-    "result": "Reverted blocks."
-}
- -
diff --git a/src/Tippy/Pages/Doc/Api.cshtml.cs b/src/Tippy/Pages/Doc/Api.cshtml.cs deleted file mode 100644 index 09ad9e6..0000000 --- a/src/Tippy/Pages/Doc/Api.cshtml.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Tippy.Pages.Doc -{ - public class ApiModel : PageModelBase - { - public ApiModel(Tippy.Core.Data.TippyDbContext context) : base(context) - { - } - - public void OnGet() - { - } - } -} diff --git a/src/Tippy/Pages/Home/Index.cshtml b/src/Tippy/Pages/Home/Index.cshtml index 4560be0..bec25af 100644 --- a/src/Tippy/Pages/Home/Index.cshtml +++ b/src/Tippy/Pages/Home/Index.cshtml @@ -59,7 +59,7 @@
@Model.ActiveProject.Chain.ToString().ToLower()
CKB RPC
http://localhost:@Model.ActiveProject.NodeRpcPort
-
Tippy RPC (document)
+
Tippy RPC (document)
http://localhost:5000/api
diff --git a/src/Tippy/Pages/Home/_EpochInfo.cshtml b/src/Tippy/Pages/Home/_EpochInfo.cshtml index cfe4011..3d2f428 100644 --- a/src/Tippy/Pages/Home/_EpochInfo.cshtml +++ b/src/Tippy/Pages/Home/_EpochInfo.cshtml @@ -3,12 +3,12 @@
Epoch
-
@Hex.HexToUInt64(Model.EpochView!.Number)
+
@Hex.HexToUInt64(Model.EpochView?.Number ?? "0x0")
Block Index
- @{ var current = Model.TipBlockNumber - Hex.HexToUInt64(Model.EpochView!.StartNumber) + 1; } + @{ var current = Model.TipBlockNumber - Hex.HexToUInt64(Model.EpochView?.StartNumber ?? "0x0") + 1; }
@current
Length
-
@Hex.HexToUInt64(Model.EpochView!.Length)
+
@Hex.HexToUInt64(Model.EpochView?.Length ?? "0x0")
Compact Target
-
@Hex.HexToUInt32(Model.EpochView!.CompactTarget)
+
@Hex.HexToUInt32(Model.EpochView?.CompactTarget ?? "0x0")
diff --git a/src/Tippy/Tippy.csproj b/src/Tippy/Tippy.csproj index 773a9a6..6d1aae1 100644 --- a/src/Tippy/Tippy.csproj +++ b/src/Tippy/Tippy.csproj @@ -8,7 +8,7 @@ false embedded 6e0dc08b-01f2-47f6-8249-66ed1b18f469 - 0.2.1 + 0.2.2 https://github.com/nervosnetwork/tippy https://github.com/nervosnetwork/tippy Copyright (c) Nervos Foundation diff --git a/tools/osx/Info.plist b/tools/osx/Info.plist index ec3f194..61582eb 100644 --- a/tools/osx/Info.plist +++ b/tools/osx/Info.plist @@ -9,9 +9,9 @@ CFBundleIconFile Tippy.icns CFBundleVersion - 0.2.1 + 0.2.2 CFBundleShortVersionString - 0.2.1 + 0.2.2 CFBundleExecutable main CFBundleInfoDictionaryVersion