Merge branch 'develop' into bp/app-api

This commit is contained in:
bitful-pannul 2024-08-15 17:28:44 +03:00
commit 2d5ad486fe
41 changed files with 593 additions and 340 deletions

30
Cargo.lock generated
View File

@ -1294,12 +1294,13 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292"
checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189"
dependencies = [
"jobserver",
"libc",
"shlex",
]
[[package]]
@ -2135,10 +2136,7 @@ dependencies = [
name = "echo"
version = "0.1.0"
dependencies = [
"anyhow",
"kinode_process_lib 0.9.0",
"serde",
"serde_json",
"wit-bindgen",
]
@ -2706,6 +2704,14 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "help"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.9.0",
"wit-bindgen",
]
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -3057,9 +3063,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
@ -3364,7 +3370,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.9.0"
source = "git+https://github.com/kinode-dao/process_lib?branch=develop#4eccb97694578276d0bfc7fadb5cc6ce3ce91991"
source = "git+https://github.com/kinode-dao/process_lib?branch=develop#5c1d8ed36cf10688808c09357ef0e43225396097"
dependencies = [
"alloy",
"alloy-primitives",
@ -5877,15 +5883,15 @@ dependencies = [
[[package]]
name = "tower-layer"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]]
name = "tower-service"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"

View File

@ -22,7 +22,9 @@ members = [
"kinode/packages/kns_indexer/kns_indexer", "kinode/packages/kns_indexer/get_block", "kinode/packages/kns_indexer/state",
"kinode/packages/settings/settings",
"kinode/packages/terminal/terminal",
"kinode/packages/terminal/alias", "kinode/packages/terminal/cat", "kinode/packages/terminal/echo", "kinode/packages/terminal/hi", "kinode/packages/terminal/kfetch", "kinode/packages/terminal/kill", "kinode/packages/terminal/m", "kinode/packages/terminal/top",
"kinode/packages/terminal/alias", "kinode/packages/terminal/cat", "kinode/packages/terminal/echo",
"kinode/packages/terminal/help", "kinode/packages/terminal/hi", "kinode/packages/terminal/kfetch",
"kinode/packages/terminal/kill", "kinode/packages/terminal/m", "kinode/packages/terminal/top",
"kinode/packages/terminal/net_diagnostics", "kinode/packages/terminal/peer", "kinode/packages/terminal/peers",
"kinode/packages/tester/tester",
]

View File

@ -38,6 +38,10 @@ rustup target add wasm32-wasi
rustup target add wasm32-wasi --toolchain nightly
cargo install cargo-wasi
# Install NPM so we can build frontends for "distro" packages.
# https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
# If you want to skip this step, run cargo build with the environment variable SKIP_BUILD_FRONTEND=true
# Build the runtime, along with a number of "distro" Wasm modules.
# The compiled binary will be at `kinode/target/debug/kinode`
# OPTIONAL: --release flag (slower build; faster runtime; binary at `kinode/target/release/kinode`)
@ -139,8 +143,9 @@ A list of the terminal scripts included in this distro:
- Example: `cat /terminal:sys/pkg/scripts.json`
- `echo <text>`: print text to the terminal.
- Example: `echo foo`
- `help <command>`: print the help message for a command. Leave the command blank to print the help message for all commands.
- `hi <name> <string>`: send a text message to another node's command line.
- Example: `hi ben.os hello world`
- Example: `hi mothu.kino hello world`
- `kfetch`: print system information a la neofetch. No arguments.
- `kill <process-id>`: terminate a running process. This will bypass any restart behavioruse judiciously.
- Example: `kill chess:chess:sys`

View File

@ -20,7 +20,7 @@ input,
button,
textarea,
select {
font: inherit;
font-family: var(--font-family-main);
}
/* Variables */
@ -38,6 +38,8 @@ select {
--maroon: #4f0000;
--gray: #657b83;
--tasteful-dark: #1f1f1f;
--font-family-main: 'Kode Mono', monospace;
}
/* Typography */
@ -49,10 +51,17 @@ h5,
h6,
p,
label,
li {
li,
span {
font-family: var(--font-family-main);
color: light-dark(var(--off-black), var(--off-white));
}
p,
li {
font-size: 0.8em;
}
h1 {
font-size: 2em;
}
@ -78,6 +87,7 @@ h6 {
}
a {
font-family: var(--font-family-main);
color: light-dark(var(--blue), var(--orange));
text-decoration: none;
}
@ -89,7 +99,6 @@ a:hover {
/* Layout */
body {
font-family: var(--font-family-main, sans-serif);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
background-color: light-dark(var(--tan), var(--tasteful-dark));
@ -162,7 +171,7 @@ button:disabled {
}
button.secondary {
background-color: white;
background-color: light-dark(var(--off-white), var(--off-black));
color: var(--orange);
border: 2px solid var(--orange);
}

View File

@ -1,5 +1,4 @@
use std::{
collections::HashSet,
fs::{self, File},
io::{BufReader, Cursor, Read, Write},
path::{Path, PathBuf},
@ -9,6 +8,13 @@ use flate2::read::GzDecoder;
use tar::Archive;
use zip::write::FileOptions;
macro_rules! p {
($($tokens: tt)*) => {
println!("cargo:warning={}", format!($($tokens)*))
}
}
/// get cargo features to compile packages with
fn get_features() -> String {
let mut features = "".to_string();
for (key, _) in std::env::vars() {
@ -23,25 +29,30 @@ fn get_features() -> String {
features
}
fn output_reruns(dir: &Path, rerun_files: &HashSet<String>) {
/// print `cargo:rerun-if-changed=PATH` for each path of interest
fn output_reruns(dir: &Path) {
// Check files individually
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
// Check if the current file is in our list of interesting files
if filename == "ui" {
continue;
}
if rerun_files.contains(filename) {
// If so, print a `cargo:rerun-if-changed=PATH` line for it
println!("cargo::rerun-if-changed={}", path.display());
continue;
}
}
if path.is_dir() {
// If the entry is a directory not in rerun_files, recursively walk it
output_reruns(&path, rerun_files);
if let Some(dirname) = path.file_name().and_then(|n| n.to_str()) {
if dirname == "ui" || dirname == "target" {
// do not prompt a rerun if only UI/build files have changed
continue;
}
// If the entry is a directory not in rerun_files, recursively walk it
output_reruns(&path);
}
} else {
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
if filename.ends_with(".zip") || filename.ends_with(".wasm") {
// do not prompt a rerun for compiled outputs
continue;
}
// any other changed file within a package subdir prompts a rerun
println!("cargo::rerun-if-changed={}", path.display());
}
}
}
}
@ -64,7 +75,9 @@ fn untar_gz_file(path: &Path, dest: &Path) -> std::io::Result<()> {
Ok(())
}
/// fetch .tar.gz of kinode book for docs app
fn get_kinode_book(packages_dir: &Path) -> anyhow::Result<()> {
p!("fetching kinode book .tar.gz");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let releases = kit::boot_fake_node::fetch_releases("kinode-dao", "kinode-book")
@ -153,7 +166,7 @@ fn build_and_zip_package(
fn main() -> anyhow::Result<()> {
if std::env::var("SKIP_BUILD_SCRIPT").is_ok() {
println!("Skipping build script");
p!("skipping build script");
return Ok(());
}
@ -162,7 +175,7 @@ fn main() -> anyhow::Result<()> {
let packages_dir = pwd.join("packages");
if std::env::var("SKIP_BUILD_FRONTEND").is_ok() {
println!("Skipping build frontend");
p!("skipping frontend builds");
} else {
// build core frontends
let core_frontends = vec![
@ -186,12 +199,7 @@ fn main() -> anyhow::Result<()> {
get_kinode_book(&packages_dir)?;
let rerun_files: HashSet<String> = HashSet::from([
"Cargo.lock".to_string(),
"Cargo.toml".to_string(),
"src".to_string(),
]);
output_reruns(&parent_dir, &rerun_files);
output_reruns(&packages_dir);
let features = get_features();
@ -201,14 +209,14 @@ fn main() -> anyhow::Result<()> {
Ok(e) => e.path(),
Err(_) => return None,
};
let parent_pkg_path = entry_path.join("pkg");
if !parent_pkg_path.exists() {
let child_pkg_path = entry_path.join("pkg");
if !child_pkg_path.exists() {
// don't run on, e.g., `.DS_Store`
return None;
}
Some(build_and_zip_package(
entry_path.clone(),
parent_pkg_path.to_str().unwrap(),
child_pkg_path.to_str().unwrap(),
&features,
))
})

View File

@ -1,9 +1,9 @@
import { multicallAbi, kinomapAbi, mechAbi, KINOMAP, MULTICALL, KINO_ACCOUNT_IMPL } from "./";
import { multicallAbi, kimapAbi, mechAbi, KIMAP, MULTICALL, KINO_ACCOUNT_IMPL } from "./";
import { encodeFunctionData, encodePacked, stringToHex } from "viem";
export function encodeMulticalls(metadataUri: string, metadataHash: string) {
const metadataHashCall = encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~metadata-hash")]),
@ -12,7 +12,7 @@ export function encodeMulticalls(metadataUri: string, metadataHash: string) {
})
const metadataUriCall = encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~metadata-uri")]),
@ -21,8 +21,8 @@ export function encodeMulticalls(metadataUri: string, metadataHash: string) {
})
const calls = [
{ target: KINOMAP, callData: metadataHashCall },
{ target: KINOMAP, callData: metadataUriCall },
{ target: KIMAP, callData: metadataHashCall },
{ target: KIMAP, callData: metadataUriCall },
];
const multicall = encodeFunctionData({
@ -46,7 +46,7 @@ export function encodeIntoMintCall(multicalls: `0x${string}`, our_address: `0x${
});
const mintCall = encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'mint',
args: [
our_address,

View File

@ -2,7 +2,7 @@ import { parseAbi } from "viem";
export { encodeMulticalls, encodeIntoMintCall } from "./helpers";
export const KINOMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658";
export const KIMAP: `0x${string}` = "0xAfA2e57D3cBA08169b416457C14eBA2D6021c4b5";
export const MULTICALL: `0x${string}` = "0xcA11bde05977b3631167028862bE2a173976CA11";
export const KINO_ACCOUNT_IMPL: `0x${string}` = "0x38766C70a4FB2f23137D9251a1aA12b1143fC716";
@ -12,7 +12,7 @@ export const multicallAbi = parseAbi([
`struct Call { address target; bytes callData; }`,
]);
export const kinomapAbi = parseAbi([
export const kimapAbi = parseAbi([
"function mint(address, bytes calldata, bytes calldata, bytes calldata, address) external returns (address tba)",
"function note(bytes calldata,bytes calldata) external returns (bytes32)",
"function get(bytes32 node) external view returns (address tokenBoundAccount, address tokenOwner, bytes memory note)",

View File

@ -3,7 +3,7 @@ import { Link, useLocation } from "react-router-dom";
import { useAccount, useWriteContract, useWaitForTransactionReceipt, usePublicClient } from 'wagmi'
import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit';
import { keccak256, toBytes } from 'viem';
import { mechAbi, KINOMAP, encodeIntoMintCall, encodeMulticalls, kinomapAbi, MULTICALL } from "../abis";
import { mechAbi, KIMAP, encodeIntoMintCall, encodeMulticalls, kimapAbi, MULTICALL } from "../abis";
import { kinohash } from '../utils/kinohash';
import useAppsStore from "../store";
@ -60,8 +60,8 @@ export default function PublishPage() {
try {
// Check if the package already exists and get its TBA
let data = await publicClient.readContract({
abi: kinomapAbi,
address: KINOMAP,
abi: kimapAbi,
address: KIMAP,
functionName: 'get',
args: [kinohash(`${packageName}.${publisherId}`)]
});
@ -73,8 +73,8 @@ export default function PublishPage() {
// If the package doesn't exist, check for the publisher's TBA
if (!currentTBA) {
data = await publicClient.readContract({
abi: kinomapAbi,
address: KINOMAP,
abi: kimapAbi,
address: KIMAP,
functionName: 'get',
args: [kinohash(publisherId)]
});
@ -97,10 +97,10 @@ export default function PublishPage() {
writeContract({
abi: mechAbi,
address: currentTBA || KINOMAP,
address: currentTBA || KIMAP,
functionName: 'execute',
args: [
isUpdate ? MULTICALL : KINOMAP,
isUpdate ? MULTICALL : KIMAP,
BigInt(0),
args,
isUpdate ? 1 : 0
@ -130,8 +130,8 @@ export default function PublishPage() {
}
const data = await publicClient.readContract({
abi: kinomapAbi,
address: KINOMAP,
abi: kimapAbi,
address: KIMAP,
functionName: 'get',
args: [kinohash(`${packageName}.${publisherName}`)]
});
@ -150,7 +150,7 @@ export default function PublishPage() {
address: tba as `0x${string}`,
functionName: 'execute',
args: [
KINOMAP,
KIMAP,
BigInt(0),
multicall,
1

View File

@ -178,7 +178,7 @@ fn init(our: Address) {
let path = incoming.bound_path(None);
match path {
"/apps" => (
server::HttpResponse::new(http::StatusCode::BAD_REQUEST),
server::HttpResponse::new(http::StatusCode::OK),
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(

View File

@ -15,14 +15,14 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className="app-display"
title={isHovered ? (app?.label || app?.package_name) : (!app?.path ? "This app does not serve a UI" : undefined)}
title={app?.label}
style={!app?.path ? { pointerEvents: 'none', textDecoration: 'none !important', filter: 'grayscale(100%)' } : {}}
>
{app?.base64_icon
? <img className="app-icon" src={app.base64_icon} />
: <img className="app-icon" src='/bird-orange.svg' />
}
<h6>{app?.label || app?.package_name}</h6>
{isHovered && !app?.path && <p className="no-ui">This app does not serve a UI</p>}
{app?.path && isHovered && <button className="app-fave-button"
onClick={(e) => {
e.preventDefault()
@ -35,7 +35,7 @@ const AppDisplay: React.FC<AppDisplayProps> = ({ app }) => {
})
}}
>
{app?.favorite ? '★' : '☆'}
<span>{app?.favorite ? '★' : '☆'}</span>
</button>}
</a>
}

View File

@ -1,11 +1,10 @@
import useHomepageStore from "../store/homepageStore"
import { Modal } from "./Modal"
import classNames from "classnames"
import usePersistentStore from "../store/persistentStore"
const WidgetsSettingsModal = () => {
const { apps, setShowWidgetsSettings } = useHomepageStore()
const { widgetSettings, toggleWidgetVisibility, setWidgetSize } = usePersistentStore()
const { widgetSettings, toggleWidgetVisibility } = usePersistentStore()
return <Modal
title='Widget Settings'
@ -25,27 +24,6 @@ const WidgetsSettingsModal = () => {
autoFocus
/></span>
</div>
<div>
<span>Widget size</span>
<div>
<button
className={classNames({
'clear': widgetSettings[app.package_name]?.size === 'large'
})}
onClick={() => setWidgetSize(app.package_name, 'small')}
>
Small
</button>
<button
className={classNames({
'clear': widgetSettings[app.package_name]?.size !== 'large'
})}
onClick={() => setWidgetSize(app.package_name, 'large')}
>
Large
</button>
</div>
</div>
</div>
</div>
);

View File

@ -11,11 +11,12 @@ a {
#homepage {
max-width: 960px;
margin: 0 auto;
margin: 10px auto;
}
header {
width: 100%;
margin-bottom: 30px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@ -46,7 +47,7 @@ header button {
flex-wrap: wrap;
gap: 10px;
justify-content: center;
background-color: #4f000055;
background-color: light-dark(#4f000055, var(--tasteful-dark));
padding: 10px;
border-radius: 10px;
margin: 0 auto;
@ -80,7 +81,16 @@ header button {
right: 0;
padding: 2px;
margin: 0;
font-size: 0.7em;
width: 24px;
height: 24px;
}
.app-fave-button span {
width: 100%;
height: 100%;
text-align: center;
vertical-align: middle;
font-size: 1.1em;
}
.no-ui {
@ -91,6 +101,7 @@ header button {
#widgets {
width: 100%;
margin-top: 30px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@ -143,20 +154,28 @@ footer {
position: fixed;
bottom: 0;
width: 90vw;
margin: 0 auto;
max-height: 100vh;
max-width: 960px;
left: 50%;
transform: translate(-50%, 0);
}
.footer-button {
background-color: var(--blue);
color: light-dark(var(--off-black), var(--off-white));
border-radius: 25px 25px 0px 0px;
}
#all-apps {
border: 1px solid black;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
justify-content: space-evenly;
padding: 10px;
background-color: #4f000085;
border-radius: 5px 5px 0px 0px;
background-color: light-dark(var(--gray), var(--tasteful-dark));
border-radius: 25px 25px 0px 0px;
}
.app-icon {
@ -200,4 +219,8 @@ footer {
display: flex;
flex-direction: row;
gap: 10px;
}
.widget-settings div {
padding: 20px;
}

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import KinodeBird from '../components/KinodeBird'
import useHomepageStore from '../store/homepageStore'
import { FaChevronDown, FaChevronUp, FaScrewdriverWrench } from 'react-icons/fa6'
import { FaChevronDown, FaChevronUp } from 'react-icons/fa6'
import AppsDock from '../components/AppsDock'
import AllApps from '../components/AllApps'
import Widgets from '../components/Widgets'
@ -59,15 +59,15 @@ function Homepage() {
: new Date().getHours() < 18
? 'Good afternoon'
: 'Good evening'}, {our}</h2>
<a href="https://github.com/kinode-dao/kinode/releases" target="_blank">[v{version}]</a>
<button onClick={() => setShowWidgetsSettings(true)}>
<FaScrewdriverWrench />
</button>
<a href="https://github.com/kinode-dao/kinode/releases" target="_blank">[kinode v{version}]</a>
<a href="#" onClick={(e) => { e.preventDefault(); setShowWidgetsSettings(true); }}>
[]
</a>
</header>
<AppsDock />
<Widgets />
<footer>
<button onClick={() => setAllAppsExpanded(!allAppsExpanded)}>
<button className="footer-button" onClick={() => setAllAppsExpanded(!allAppsExpanded)}>
{allAppsExpanded ? <FaChevronDown /> : <FaChevronUp />}
<span>{allAppsExpanded ? 'Collapse' : 'All apps'}</span>
</button>

View File

@ -306,8 +306,8 @@ fn handle_log(our: &Address, state: &mut State, log: &eth::Log) -> anyhow::Resul
let child_hash = decoded.childhash.to_string();
let name = String::from_utf8(decoded.label.to_vec())?;
if !kimap::valid_name(&name, false) {
return Err(anyhow::anyhow!("skipping invalid entry"));
if !kimap::valid_name(&name) {
return Err(anyhow::anyhow!("skipping invalid name: {name}"));
}
let full_name = match get_parent_name(&state.names, &parent_hash) {
@ -334,6 +334,10 @@ fn handle_log(our: &Address, state: &mut State, log: &eth::Log) -> anyhow::Resul
let note = String::from_utf8(decoded.label.to_vec())?;
let node_hash = decoded.parenthash.to_string();
if !kimap::valid_note(&note) {
return Err(anyhow::anyhow!("skipping invalid note: {note}"));
}
let Some(node_name) = get_parent_name(&state.names, &node_hash) else {
return Err(anyhow::anyhow!("parent node for note not found"));
};

View File

@ -60,6 +60,7 @@
article#node-info {
grid-area: node-info;
word-wrap: break-word;
display: flex;
flex-direction: column;
justify-content: space-around;
@ -162,9 +163,7 @@
<p id="net-key"></p>
<p id="ip-ports"></p>
<p id="routers"></p>
<button>reset networking key (TODO)</button>
<button>adjust networking info (TODO)</button>
<button id="shutdown">shut down node</button>
<button id="shutdown">shut down node(!)</button>
</article>
<article id="pings">

View File

@ -7,10 +7,7 @@ edition = "2021"
simulation-mode = []
[dependencies]
anyhow = "1.0"
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", branch = "develop" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.24.0"
[lib]

View File

@ -0,0 +1,17 @@
[package]
name = "help"
version = "0.1.0"
edition = "2021"
[features]
simulation-mode = []
[dependencies]
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", branch = "develop" }
wit-bindgen = "0.24.0"
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "kinode:process"

View File

@ -0,0 +1,50 @@
use kinode_process_lib::{script, Address};
wit_bindgen::generate!({
path: "target/wit",
world: "process-v0",
});
const HELP_MESSAGES: [[&str; 2]; 11] = [
["alias", "\n\x1b[1malias\x1b[0m <shorthand> <process_id>: create an alias for a script.\n - Example: \x1b[1malias get_block get_block:kns_indexer:sys\x1b[0m\n - note: all of these listed commands are just default aliases for terminal scripts."],
["cat", "\n\x1b[1mcat\x1b[0m <vfs-file-path>: print the contents of a file in the terminal.\n - Example: \x1b[1mcat /terminal:sys/pkg/scripts.json\x1b[0m"],
["echo", "\n\x1b[1mecho\x1b[0m <text>: print text to the terminal.\n - Example: \x1b[1mecho foo\x1b[0m"],
["hi", "\n\x1b[1mhi\x1b[0m <name> <string>: send a text message to another node's command line.\n - Example: \x1b[1mhi mothu.kino hello world\x1b[0m"],
["kfetch", "\n\x1b[1mkfetch\x1b[0m: print system information a la neofetch. No arguments."],
["kill", "\n\x1b[1mkill\x1b[0m <process-id>: terminate a running process. This will bypass any restart behavioruse judiciously.\n - Example: \x1b[1mkill chess:chess:sys\x1b[0m"],
["m", "\n\x1b[1mm\x1b[0m <address> '<json>': send an inter-process message. <address> is formatted as <node>@<process_id>. <process_id> is formatted as <process_name>:<package_name>:<publisher_node>. JSON containing spaces must be wrapped in single-quotes (\x1b[1m''\x1b[0m).\n - Example: \x1b[1mm our@eth:distro:sys \"SetPublic\" -a 5\x1b[0m\n - the '-a' flag is used to expect a response with a given timeout\n - \x1b[1mour\x1b[0m will always be interpolated by the system as your node's name"],
["net_diagnostics", "\n\x1b[1mnet_diagnostics\x1b[0m: print some useful networking diagnostic data."],
["peer", "\n\x1b[1mpeer\x1b[0m <name>: print the peer's PKI info, if it exists."],
["peers", "\n\x1b[1mpeers\x1b[0m: print the peers the node currently hold connections with."],
["top", "\n\x1b[1mtop\x1b[0m <process_id>: display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes.\n - Example: \x1b[1mtop net:distro:sys\x1b[0m\n - Example: \x1b[1mtop\x1b[0m"],
];
script!(init);
fn init(_our: Address, args: String) -> String {
// if args is empty, print the entire help message.
// if args contains the name of a command, print the help message for that command.
// otherwise, print an error message.
if args.is_empty() {
let mut help_message = String::from(
"\n====================\n\
Kinode Terminal Help\n\
====================\n",
);
for [_, message] in HELP_MESSAGES.iter() {
help_message.push_str(message);
help_message.push_str("\n");
}
help_message.push_str(
"For more help, look to the documentation at book.kinode.org.\n\
============================================================\n",
);
return help_message;
} else if let Some(message) = HELP_MESSAGES.iter().find(|[cmd, _]| cmd == &args) {
return message[1].to_string();
} else {
return format!("No help found for command \x1b[1m{args}\x1b[0m");
}
}

View File

@ -33,6 +33,14 @@
"grant_capabilities": [],
"wit_version": 0
},
"help.wasm": {
"root": false,
"public": false,
"request_networking": false,
"request_capabilities": [],
"grant_capabilities": [],
"wit_version": 0
},
"hi.wasm": {
"root": false,
"public": false,

View File

@ -71,6 +71,10 @@ impl TerminalState {
"echo".to_string(),
ProcessId::new(Some("echo"), "terminal", "sys"),
),
(
"help".to_string(),
ProcessId::new(Some("help"), "terminal", "sys"),
),
(
"hi".to_string(),
ProcessId::new(Some("hi"), "terminal", "sys"),
@ -110,14 +114,23 @@ impl TerminalState {
call_init!(init);
fn init(our: Address) {
let mut state: TerminalState = match get_typed_state(|bytes| bincode::deserialize(bytes)) {
Some(s) => s,
None => {
let state = TerminalState::new(our);
set_state(&bincode::serialize(&state).unwrap());
state
}
};
let mut state: TerminalState =
match get_typed_state(|bytes| bincode::deserialize::<TerminalState>(bytes)) {
Some(mut s) => {
// **add** the pre-installed scripts to the terminal state
// in case new ones have been added or if user has deleted aliases
let default_state = TerminalState::new(our);
for (alias, process) in default_state.aliases {
s.aliases.insert(alias, process);
}
s
}
None => {
let state = TerminalState::new(our);
set_state(&bincode::serialize(&state).unwrap());
state
}
};
loop {
let message = match await_message() {

View File

@ -143,7 +143,7 @@
oninput="document.getElementById('password-err').style.display = 'none';" value="" class="self-stretch mb-2">
<div id="password-err" class="login-row flex mb-2" style="display: none;"> Incorrect Password </div>
<div class="flex flex-col leading-6 self-stretch mb-2">
<button> Login </button>
<button id="login-button" disabled> Login </button>
<div class="flex flex-col mt-2 text-sm leading-6" id="fake-or-not"></div>
</div>
</form>
@ -156,20 +156,27 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script>
if ('${fake}' === 'true') {
document.getElementById("fake-or-not").innerHTML = "Fake node -- any password will work!";
} else {
document.getElementById("fake-or-not").innerHTML = "Restart your node to change networking settings.";
}
let isInitialized = false;
const firstPathItem = window.location.pathname.split('/')[1];
const expectedSecureSubdomain = generateSecureSubdomain(firstPathItem);
const maybeSecureSubdomain = window.location.host.split('.')[0];
const isSecureSubdomain = expectedSecureSubdomain === maybeSecureSubdomain;
if (isSecureSubdomain) {
document.getElementById("node-and-domain").innerText = "${node}: authenticate for secure subdomain app " + firstPathItem;
} else {
document.getElementById("node-and-domain").innerText = "${node} ";
function initializeLoginForm() {
if ('${fake}' === 'true') {
document.getElementById("fake-or-not").innerHTML = "Fake node -- any password will work!";
} else {
document.getElementById("fake-or-not").innerHTML = "Restart your node to change networking settings.";
}
const firstPathItem = window.location.pathname.split('/')[1];
const expectedSecureSubdomain = generateSecureSubdomain(firstPathItem);
const maybeSecureSubdomain = window.location.host.split('.')[0];
const isSecureSubdomain = expectedSecureSubdomain === maybeSecureSubdomain;
if (isSecureSubdomain) {
document.getElementById("node-and-domain").innerText = "${node}: authenticate for secure subdomain app " + firstPathItem;
} else {
document.getElementById("node-and-domain").innerText = "${node} ";
}
document.getElementById("login-button").disabled = false;
isInitialized = true;
}
async function login(password) {
@ -197,7 +204,6 @@
document.getElementById("password").value = "";
document.getElementById("password-err").style.display = "flex";
document.getElementById("password").focus();
return;
}
}
@ -213,12 +219,15 @@
}
document.addEventListener("DOMContentLoaded", () => {
initializeLoginForm();
const form = document.getElementById("login-form");
form.addEventListener("submit", (e) => {
e.preventDefault();
e.stopPropagation();
const password = document.getElementById("password").value;
login(password);
if (isInitialized) {
const password = document.getElementById("password").value;
login(password);
}
});
});
</script>

View File

@ -17,7 +17,7 @@ type DiskKey = [u8; CREDENTIAL_LEN];
pub const CREDENTIAL_LEN: usize = ring::digest::SHA256_OUTPUT_LEN;
pub const ITERATIONS: u32 = 1_000_000;
pub static PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256; // TODO maybe look into Argon2
pub static PBKDF2_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
pub fn encode_keyfile(
password_hash: String,

View File

@ -11,6 +11,7 @@
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<link href="https://fonts.googleapis.com/css2?family=Kode+Mono:wght@700&display=swap" rel="stylesheet">
</head>
<body>

View File

@ -3,6 +3,7 @@ import { Navigate, BrowserRouter as Router, Route, Routes, useParams } from 'rea
import CommitDotOsName from "./pages/CommitDotOsName";
import MintDotOsName from "./pages/MintDotOsName";
import MintCustom from "./pages/MintCustom";
import SetPassword from "./pages/SetPassword";
import Login from './pages/Login'
import ResetDotOsName from './pages/ResetDotOsName'
@ -19,7 +20,7 @@ function App() {
const [keyFileName, setKeyFileName] = useState<string>('');
const [reset, setReset] = useState<boolean>(false);
const [direct, setDirect] = useState<boolean>(false);
const [knsName, setOsName] = useState<string>('');
const [knsName, setKnsName] = useState<string>('');
const [appSizeOnLoad, setAppSizeOnLoad] = useState<number>(0);
const [networkingKey, setNetworkingKey] = useState<string>('');
const [ipAddress, setIpAddress] = useState<number>(0);
@ -50,7 +51,7 @@ function App() {
const info: UnencryptedIdentity = await infoResponse.json()
if (initialVisit) {
setOsName(info.name)
setKnsName(info.name)
setRouters(info.allowed_routers)
setNavigateToLogin(true)
setInitialVisit(false)
@ -87,7 +88,7 @@ function App() {
keyFileName, setKeyFileName,
reset, setReset,
pw, setPw,
knsName, setOsName,
knsName, setKnsName,
connectOpen, openConnect, closeConnect,
networkingKey, setNetworkingKey,
ipAddress, setIpAddress,
@ -114,6 +115,7 @@ function App() {
<Route path="/reset" element={<ResetDotOsName {...props} />} />
<Route path="/import-keyfile" element={<ImportKeyfile {...props} />} />
<Route path="/login" element={<Login {...props} />} />
<Route path="/custom-register" element={<MintCustom {...props} />} />
</Routes>
</main>
</Router>

View File

@ -2,7 +2,7 @@
import { NetworkingInfo } from "../lib/types";
import { kinohash } from "../utils/kinohash";
import { ipToBytes, portToBytes } from "../utils/kns_encoding";
import { multicallAbi, kinomapAbi, mechAbi, KINOMAP, MULTICALL } from "./";
import { multicallAbi, kimapAbi, mechAbi, KIMAP, MULTICALL } from "./";
import { encodeFunctionData, encodePacked, stringToHex, bytesToHex } from "viem";
// Function to encode router names into keccak256 hashes
@ -58,7 +58,7 @@ export const generateNetworkingKeys = async ({
console.log("networking_key: ", networking_key);
const netkeycall = encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~net-key")]),
@ -68,7 +68,7 @@ export const generateNetworkingKeys = async ({
const ws_port_call =
encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~ws-port")]),
@ -78,7 +78,7 @@ export const generateNetworkingKeys = async ({
const tcp_port_call =
encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~tcp-port")]),
@ -88,7 +88,7 @@ export const generateNetworkingKeys = async ({
const ip_address_call =
encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~ip")]),
@ -100,7 +100,7 @@ export const generateNetworkingKeys = async ({
const router_call =
encodeFunctionData({
abi: kinomapAbi,
abi: kimapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~routers")]),
@ -111,13 +111,13 @@ export const generateNetworkingKeys = async ({
});
const calls = direct ? [
{ target: KINOMAP, callData: netkeycall },
{ target: KINOMAP, callData: ws_port_call },
{ target: KINOMAP, callData: tcp_port_call },
{ target: KINOMAP, callData: ip_address_call },
{ target: KIMAP, callData: netkeycall },
{ target: KIMAP, callData: ws_port_call },
{ target: KIMAP, callData: tcp_port_call },
{ target: KIMAP, callData: ip_address_call },
] : [
{ target: KINOMAP, callData: netkeycall },
{ target: KINOMAP, callData: router_call },
{ target: KIMAP, callData: netkeycall },
{ target: KIMAP, callData: router_call },
];
const multicalls = encodeFunctionData({
@ -143,7 +143,7 @@ export const generateNetworkingKeys = async ({
// to mint a subname of your own, you would do something like this.
// const mintCall = encodeFunctionData({
// abi: kinomapAbi,
// abi: kimapAbi,
// functionName: 'mint',
// args: [
// our_address,

View File

@ -3,7 +3,7 @@ import { parseAbi } from "viem";
export { generateNetworkingKeys } from "./helpers";
// move to constants? // also for anvil/optimism
export const KINOMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658";
export const KIMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658";
export const MULTICALL: `0x${string}` = "0xcA11bde05977b3631167028862bE2a173976CA11";
export const KINO_ACCOUNT_IMPL: `0x${string}` = "0x38766C70a4FB2f23137D9251a1aA12b1143fC716";
export const DOTOS: `0x${string}` = "0x9BD054E4c7753791FA0C138b9713319F62ed235D";
@ -13,7 +13,7 @@ export const multicallAbi = parseAbi([
`struct Call { address target; bytes callData; }`,
]);
export const kinomapAbi = parseAbi([
export const kimapAbi = parseAbi([
"function mint(address, bytes calldata, bytes calldata, bytes calldata, address) external returns (address tba)",
"function note(bytes calldata,bytes calldata) external returns (bytes32)",
"function get(bytes32 node) external view returns (address tokenBoundAccount, address tokenOwner, bytes memory note)",
@ -24,28 +24,11 @@ export const mechAbi = parseAbi([
"function token() external view returns (uint256,address,uint256)"
])
export const dotOsAbi = [
{
type: 'function',
name: 'commit',
stateMutability: 'nonpayable',
inputs: [
{ name: '_commit', type: 'bytes32' },
],
outputs: [],
},
{
type: 'function',
name: 'mint',
stateMutability: 'nonpayable',
inputs: [
{ name: 'who', type: 'address' },
{ name: 'name', type: 'bytes' },
{ name: 'initialization', type: 'bytes' },
{ name: 'erc721Data', type: 'bytes' },
{ name: 'implementation', type: 'address' },
{ name: 'secret', type: 'bytes32' },
],
outputs: [{ type: 'address' }],
},
] as const
export const dotOsAbi = parseAbi([
"function commit(bytes32 _commit) external",
"function mint(address who, bytes calldata name, bytes calldata initialization, bytes calldata erc721Data, address implementation, bytes32 secret) external returns (address)"
]);
export const customAbi = parseAbi([
"function mint(address who, bytes calldata name, bytes calldata initialization, bytes calldata erc721Data, address implementation) external returns (address)"
]);

View File

@ -1,3 +1,5 @@
import { Tooltip } from "./Tooltip";
export const DirectTooltip: React.FC = () => <Tooltip text={`A direct node publishes its own networking information on-chain: IP, port, so on. An indirect node relies on the service of routers, which are themselves direct nodes. Only register a direct node if you know what youre doing and have a public, static IP address.`} />
export const DirectTooltip: React.FC = () => <Tooltip text={`A direct node publishes its own networking information on-chain: IP, port, so on. An indirect node relies on the service of routers, which are themselves direct nodes. Only register a direct node if you know what you're doing and have a public, static IP address.`}>
<span></span>
</Tooltip>

View File

@ -1,9 +1,9 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import isValidDomain from "is-valid-domain";
import { toAscii } from "idna-uts46-hx";
import { usePublicClient } from 'wagmi'
import { KINOMAP, kinomapAbi } from '../abis'
import { KIMAP, kimapAbi } from '../abis'
import { kinohash } from "../utils/kinohash";
export const NAME_URL = "Name must contain only valid characters (a-z, 0-9, and -)";
@ -14,33 +14,38 @@ export const NAME_NOT_OWNER = "Name already exists and does not belong to this w
export const NAME_NOT_REGISTERED = "Name is not registered";
type ClaimOsNameProps = {
address?: `0x${string}`;
name: string;
setName: React.Dispatch<React.SetStateAction<string>>;
nameValidities: string[];
setNameValidities: React.Dispatch<React.SetStateAction<string[]>>;
triggerNameCheck: boolean;
setTba?: React.Dispatch<React.SetStateAction<string>>;
isReset?: boolean;
};
function EnterKnsName({
address,
name,
setName,
nameValidities,
setNameValidities,
triggerNameCheck,
setTba,
isReset = false,
}: ClaimOsNameProps) {
const client = usePublicClient();
const debouncer = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (isReset) return;
const [isPunyfied, setIsPunyfied] = useState('');
useEffect(() => {
if (debouncer.current) clearTimeout(debouncer.current);
debouncer.current = setTimeout(async () => {
let index: number;
let validities = [...nameValidities];
let validities: string[] = [];
setIsPunyfied('');
const len = [...name].length;
index = validities.indexOf(NAME_LENGTH);
@ -57,6 +62,8 @@ function EnterKnsName({
if (index === -1) validities.push(NAME_INVALID_PUNY);
}
if (normalized !== (name + ".os")) setIsPunyfied(normalized);
// only check if name is valid punycode
if (normalized && normalized !== '.os') {
index = validities.indexOf(NAME_URL);
@ -66,46 +73,57 @@ function EnterKnsName({
index = validities.indexOf(NAME_CLAIMED);
// only check if name is valid and long enough
if (validities.length === 0 || index !== -1 && normalized.length > 2) {
try {
const namehash = kinohash(normalized)
// maybe separate into helper function for readability?
// also note picking the right chain ID & address!
const data = await client?.readContract({
address: KINOMAP,
abi: kinomapAbi,
address: KIMAP,
abi: kimapAbi,
functionName: "get",
args: [namehash]
})
const tba = data?.[0];
if (tba !== undefined) {
setTba ? (setTba(tba)) : null;
} else {
validities.push(NAME_NOT_REGISTERED);
}
const owner = data?.[1];
const owner_is_zero = owner === "0x0000000000000000000000000000000000000000";
if (!owner_is_zero && index === -1) validities.push(NAME_CLAIMED);
if (!owner_is_zero && !isReset) validities.push(NAME_CLAIMED);
if (!owner_is_zero && isReset && address && owner !== address) validities.push(NAME_NOT_OWNER);
if (isReset && owner_is_zero) validities.push(NAME_NOT_REGISTERED);
} catch (e) {
console.error({ e })
if (index !== -1) validities.splice(index, 1);
}
}
}
setNameValidities(validities);
}, 100);
}, 500);
}, [name, triggerNameCheck, isReset]);
const noDots = (e: any) =>
e.target.value.indexOf(".") === -1 && setName(e.target.value);
const noDotsOrSpaces = (e: any) =>
e.target.value.indexOf(".") === -1 && e.target.value.indexOf(" ") === -1 && setName(e.target.value);
return (
<div className="enter-kns-name">
<div className="input-wrapper">
<input
value={name}
onChange={noDots}
onChange={noDotsOrSpaces}
type="text"
required
name="dot-os-name"
placeholder="e.g. myname"
placeholder="mynode123"
className="kns-input"
/>
<span className="kns-suffix">.os</span>
@ -113,6 +131,7 @@ function EnterKnsName({
{nameValidities.map((x, i) => (
<p key={i} className="error-message">{x}</p>
))}
{isPunyfied !== '' && <p className="puny-warning">special characters will be converted to punycode: {isPunyfied}</p>}
</div>
);
}

View File

@ -29,6 +29,7 @@
}
.tooltip-text {
font-size: 0.8em;
visibility: hidden;
width: 200px;
background-color: #555;
@ -127,13 +128,14 @@
.kns-input {
flex-grow: 1;
padding: 0.5rem;
font-size: 1.2em;
border: 1px solid var(--gray);
border-radius: 4px 0 0 4px;
}
.kns-suffix {
padding: 0.5rem;
background-color: var(--gray);
background-color: var(--blue);
border: 1px solid var(--tasteful-dark);
border-left: none;
border-radius: 0 4px 4px 0;
@ -148,6 +150,7 @@
.error-message {
color: var(--ansi-red);
margin-top: 0.5rem;
overflow-wrap: break-word;
}
.direct-checkbox {
@ -215,6 +218,7 @@
.checkbox-label {
margin-left: 10px;
font-size: 0.9em;
}
.file-input-label {
@ -235,4 +239,8 @@
.file-input-label:hover .button {
background-color: var(--dark-orange);
color: var(--off-white);
}
button.secondary {
width: 100%;
}

View File

@ -13,7 +13,7 @@ export interface PageProps {
direct: boolean,
setDirect: React.Dispatch<React.SetStateAction<boolean>>,
knsName: string,
setOsName: React.Dispatch<React.SetStateAction<string>>,
setKnsName: React.Dispatch<React.SetStateAction<string>>,
key: string,
keyFileName: string,
setKeyFileName: React.Dispatch<React.SetStateAction<string>>,

View File

@ -17,7 +17,7 @@ interface RegisterOsNameProps extends PageProps { }
function CommitDotOsName({
direct,
setDirect,
setOsName,
setKnsName,
setNetworkingKey,
setIpAddress,
setWsPort,
@ -51,7 +51,13 @@ function CommitDotOsName({
useEffect(() => setTriggerNameCheck(!triggerNameCheck), [address])
const enterOsNameProps = { name, setName, nameValidities, setNameValidities, triggerNameCheck }
const enterOsNameProps = { address, name, setName, nameValidities, setNameValidities, triggerNameCheck }
useEffect(() => {
if (!address) {
openConnectModal?.();
}
}, [address, openConnectModal]);
let handleCommit = useCallback(async (e: FormEvent) => {
e.preventDefault()
@ -80,23 +86,23 @@ function CommitDotOsName({
useEffect(() => {
if (isConfirmed) {
setOsName(`${name}.os`);
setKnsName(`${name}.os`);
navigate("/mint-os-name");
}
}, [isConfirmed, address, name, setOsName, navigate]);
}, [isConfirmed, address, name, setKnsName, navigate]);
return (
<div className="container fade-in">
<div className="section">
{Boolean(address) && (
{
<form className="form" onSubmit={handleCommit}>
{isPending || isConfirming ? (
<Loader msg={isConfirming ? 'Pre-committing to chosen ID...' : 'Please confirm the transaction in your wallet'} />
<Loader msg={isConfirming ? 'Pre-committing to chosen name...' : 'Please confirm the transaction in your wallet'} />
) : (
<>
<h3 className="form-label">
<Tooltip text="Kinodes need an onchain node identity in order to communicate with other nodes in the network.">
Choose a name for your Kinode
Choose a name for your node
</Tooltip>
</h3>
<EnterKnsName {...enterOsNameProps} />
@ -107,21 +113,22 @@ function CommitDotOsName({
type="submit"
className="button"
>
Register .os name
Register name
</button>
<p>This will confirm availability of the name and reserve it, then on the next screen you will be prompted to mint.</p>
<Link to="/reset" className="button secondary">
Already have a dot-os-name?
Already have a node?
</Link>
</div>
</>
)}
{isError && (
<p className="error-message">
Error: {error?.message || 'There was an error registering your dot-os-name, please try again.'}
Error: {error?.message || 'There was an error registering your name, please try again.'}
</p>
)}
</form>
)}
}
</div>
</div>
);

View File

@ -62,7 +62,7 @@ function ImportKeyfile({
credentials: 'include',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
keyfile: Buffer.from(localKey).toString('base64'),
keyfile: Buffer.from(localKey).toString('utf8'),
password_hash: hashed_password,
}),
});
@ -118,13 +118,13 @@ function ImportKeyfile({
required
minLength={6}
name="password"
placeholder="Min 6 characters"
placeholder=""
value={pw}
onChange={(e) => setPw(e.target.value)}
/>
{pwErr && <p className="error-message">{pwErr}</p>}
{pwDebounced && !pwVet && 6 <= pw.length && (
<p className="error-message">Password is incorrect</p>
<p className="error-message">Password is incorrect!</p>
)}
</div>
@ -132,7 +132,7 @@ function ImportKeyfile({
{keyErrs.map((x, i) => (
<p key={i} className="error-message">{x}</p>
))}
<button type="submit" className="button">Import Keyfile</button>
<button type="submit" className="button">Boot Node</button>
</div>
<p className="text-sm mt-2">
Please note: if the original node was booted as a direct node

View File

@ -12,7 +12,7 @@ function KinodeHome({ knsName }: OsHomeProps) {
const resetRedir = () => navigate('/reset')
const importKeyfileRedir = () => navigate('/import-keyfile')
const loginRedir = () => navigate('/login')
const customRegisterRedir = () => navigate('/custom-register')
const previouslyBooted = Boolean(knsName)
useEffect(() => {
@ -27,25 +27,28 @@ function KinodeHome({ knsName }: OsHomeProps) {
{previouslyBooted ? (
<div className="text-center">
<h2 className="mb-2">Welcome back!</h2>
<button onClick={loginRedir} className="button">Login</button>
<button onClick={loginRedir} className="button">Log in</button>
</div>
) : (
<>
<h2 className="text-center mb-2">Welcome to Kinode</h2>
<h4 className="text-center mb-2">New here? Register a username to get started</h4>
<h4 className="text-center mb-2">New here? Register a name to get started</h4>
<div className="button-group">
<button onClick={registerRedir} className="button">
Register Kinode Name
Register .os Name
</button>
</div>
<h4 className="text-center mt-2 mb-2">Other options</h4>
<div className="button-group">
<button onClick={resetRedir} className="button secondary">
Reset Kinode Name
</button>
<button onClick={importKeyfileRedir} className="button secondary">
Import Keyfile
</button>
<button onClick={resetRedir} className="button secondary">
Reset Existing Name
</button>
<button onClick={customRegisterRedir} className="button secondary">
Register Non-.os Name (Advanced)
</button>
</div>
</>
)}

View File

@ -3,6 +3,7 @@ import { PageProps, UnencryptedIdentity } from "../lib/types";
import Loader from "../components/Loader";
import { useNavigate } from "react-router-dom";
import { sha256, toBytes } from "viem";
import { Tooltip } from "../components/Tooltip";
interface LoginProps extends PageProps { }
@ -13,7 +14,7 @@ function Login({
routers,
setRouters,
knsName,
setOsName,
setKnsName,
}: LoginProps) {
const navigate = useNavigate();
@ -29,7 +30,7 @@ function Login({
res.json()
)) as UnencryptedIdentity;
setRouters(infoData.allowed_routers);
setOsName(infoData.name);
setKnsName(infoData.name);
} catch { }
})();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
@ -89,8 +90,9 @@ function Login({
>
<div className="form-group">
<div className="form-header">
<h3>{knsName}</h3>
<span>({isDirect ? "direct" : "indirect"} node)</span>
<Tooltip text={`(${isDirect ? "direct" : "indirect"} node)`}>
<h3>{knsName}</h3>
</Tooltip>
</div>
<input
type="password"
@ -113,14 +115,14 @@ function Login({
</div>
)}
<button type="submit">Login</button>
<button type="submit">Log in</button>
<div className="additional-options">
<button
className="clear"
className="secondary"
onClick={() => navigate('/reset')}
>
Reset Node & Networking Info
Reset Password & Networking Info
</button>
</div>
</form>

View File

@ -0,0 +1,154 @@
import { useState, useEffect, FormEvent, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import Loader from "../components/Loader";
import { PageProps } from "../lib/types";
import DirectCheckbox from "../components/DirectCheckbox";
import { useAccount, useWaitForTransactionReceipt, useSendTransaction } from "wagmi";
import { useConnectModal, useAddRecentTransaction } from "@rainbow-me/rainbowkit"
import { customAbi, generateNetworkingKeys, KINO_ACCOUNT_IMPL } from "../abis";
import { encodePacked, encodeFunctionData, stringToHex } from "viem";
interface MintCustomNameProps extends PageProps { }
function MintCustom({
direct,
setDirect,
knsName,
setKnsName,
setNetworkingKey,
setIpAddress,
setWsPort,
setTcpPort,
setRouters,
}: MintCustomNameProps) {
let { address } = useAccount();
let navigate = useNavigate();
let { openConnectModal } = useConnectModal();
const { data: hash, sendTransaction, isPending, isError, error } = useSendTransaction({
mutation: {
onSuccess: (data) => {
addRecentTransaction({ hash: data, description: `Mint ${knsName}` });
}
}
});
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({
hash,
});
const addRecentTransaction = useAddRecentTransaction();
const [triggerNameCheck, setTriggerNameCheck] = useState<boolean>(false)
useEffect(() => {
document.title = "Mint"
}, [])
useEffect(() => setTriggerNameCheck(!triggerNameCheck), [address])
useEffect(() => {
if (!address) {
openConnectModal?.();
}
}, [address, openConnectModal]);
let handleMint = useCallback(async (e: FormEvent) => {
e.preventDefault()
e.stopPropagation()
const formData = new FormData(e.target as HTMLFormElement)
if (!address) {
openConnectModal?.()
return
}
const initCall = await generateNetworkingKeys({
direct,
our_address: address,
label: knsName,
setNetworkingKey,
setIpAddress,
setWsPort,
setTcpPort,
setRouters,
reset: false,
});
setKnsName(formData.get('full-kns-name') as string)
const name = formData.get('name') as string
console.log("full kns name", formData.get('full-kns-name'))
console.log("name", name)
const data = encodeFunctionData({
abi: customAbi,
functionName: 'mint',
args: [
address,
encodePacked(["bytes"], [stringToHex(name)]),
initCall,
"0x",
KINO_ACCOUNT_IMPL,
],
})
// use data to write to contract -- do NOT use writeContract
// writeContract will NOT generate the correct selector for some reason
// probably THEIR bug.. no abi works
try {
sendTransaction({
to: formData.get('tba') as `0x${string}`,
data: data,
gas: 1000000n,
})
} catch (error) {
console.error('Failed to send transaction:', error)
}
}, [direct, address, sendTransaction, setNetworkingKey, setIpAddress, setWsPort, setTcpPort, setRouters, openConnectModal])
useEffect(() => {
if (isConfirmed) {
navigate("/set-password");
}
}, [isConfirmed, address, navigate]);
return (
<div className="container fade-in">
<div className="section">
{
<form className="form" onSubmit={handleMint}>
{isPending || isConfirming ? (
<Loader msg={isConfirming ? 'Minting name...' : 'Please confirm the transaction in your wallet'} />
) : (
<>
<p className="form-label">
Register a name on a different top-level zone -- this will likely fail if that zone's requirements are not met
</p>
<input type="text" name="name" placeholder="Enter kimap name" />
<input type="text" name="full-kns-name" placeholder="Enter full KNS name" />
<input type="text" name="tba" placeholder="Enter TBA to mint under" />
<DirectCheckbox {...{ direct, setDirect }} />
<div className="button-group">
<button type="submit" className="button">
Mint custom name
</button>
</div>
</>
)}
{isError && (
<p className="error-message">
Error: {error?.message || 'There was an error minting your name, please try again.'}
</p>
)}
</form>
}
</div>
</div>
);
}
export default MintCustom;

View File

@ -44,6 +44,12 @@ function MintDotOsName({
useEffect(() => setTriggerNameCheck(!triggerNameCheck), [address])
useEffect(() => {
if (!address) {
openConnectModal?.();
}
}, [address, openConnectModal]);
let handleMint = useCallback(async (e: FormEvent) => {
e.preventDefault()
e.stopPropagation()
@ -107,26 +113,26 @@ function MintDotOsName({
return (
<div className="container fade-in">
<div className="section">
{Boolean(address) && (
{
<form className="form" onSubmit={handleMint}>
{isPending || isConfirming ? (
<Loader msg={isConfirming ? 'Minting .os name...' : 'Please confirm the transaction in your wallet'} />
<Loader msg={isConfirming ? 'Minting name...' : 'Please confirm the transaction in your wallet'} />
) : (
<>
<div className="button-group">
<button type="submit" className="button">
Mint pre-committed .os name
Mint {knsName}
</button>
</div>
</>
)}
{isError && (
<p className="error-message">
Error: {error?.message || 'There was an error minting your dot-os-name, please try again.'}
Error: {error?.message || 'There was an error minting your name, please try again.'}
</p>
)}
</form>
)}
}
</div>
</div>
);

View File

@ -2,24 +2,18 @@ import {
FormEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useNavigate } from "react-router-dom";
import { toAscii } from "idna-uts46-hx";
import isValidDomain from "is-valid-domain";
import Loader from "../components/Loader";
import { PageProps } from "../lib/types";
import { KINOMAP, MULTICALL, generateNetworkingKeys, kinomapAbi, mechAbi } from "../abis";
import { MULTICALL, generateNetworkingKeys, mechAbi } from "../abis";
import { Tooltip } from "../components/Tooltip";
import DirectCheckbox from "../components/DirectCheckbox";
import EnterKnsName from "../components/EnterKnsName";
import { useAccount, usePublicClient, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { useAccount, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { useConnectModal, useAddRecentTransaction } from "@rainbow-me/rainbowkit";
import { kinohash } from "../utils/kinohash";
import { NAME_URL, NAME_INVALID_PUNY, NAME_NOT_OWNER, NAME_NOT_REGISTERED } from "../components/EnterKnsName";
interface ResetProps extends PageProps { }
@ -28,7 +22,6 @@ function ResetKnsName({
setDirect,
setReset,
knsName,
setOsName,
setNetworkingKey,
setIpAddress,
setWsPort,
@ -37,7 +30,6 @@ function ResetKnsName({
}: ResetProps) {
const { address } = useAccount();
const navigate = useNavigate();
const client = usePublicClient();
const { openConnectModal } = useConnectModal();
const { data: hash, writeContract, isPending, isError, error } = useWriteContract({
@ -54,7 +46,6 @@ function ResetKnsName({
const addRecentTransaction = useAddRecentTransaction();
const [name, setName] = useState<string>(knsName.slice(0, -3));
const [nameVets, setNameVets] = useState<string[]>([]);
const [nameValidities, setNameValidities] = useState<string[]>([])
const [tba, setTba] = useState<string>("");
const [triggerNameCheck, setTriggerNameCheck] = useState<boolean>(false);
@ -67,80 +58,11 @@ function ResetKnsName({
// so inputs will validate once wallet is connected
useEffect(() => setTriggerNameCheck(!triggerNameCheck), [address]); // eslint-disable-line react-hooks/exhaustive-deps
// TODO: separate this whole namechecking thing into helper function
// boolean to branch whether to check for occupied or to match against our_address.
const nameDebouncer = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (nameDebouncer.current) clearTimeout(nameDebouncer.current);
nameDebouncer.current = setTimeout(async () => {
setNameVets([]);
if (name === "") return;
let index: number;
let vets = [...nameVets];
let normalized: string;
index = vets.indexOf(NAME_INVALID_PUNY);
try {
normalized = toAscii(name + ".os");
if (index !== -1) vets.splice(index, 1);
} catch (e) {
if (index === -1) vets.push(NAME_INVALID_PUNY);
}
// only check if name is valid punycode
if (normalized! !== undefined) {
index = vets.indexOf(NAME_URL);
if (name !== "" && !isValidDomain(normalized)) {
if (index === -1) vets.push(NAME_URL);
} else if (index !== -1) vets.splice(index, 1);
try {
const namehash = kinohash(normalized)
console.log('normalized', normalized)
console.log('namehash', namehash)
// maybe separate into helper function for readability?
// also note picking the right chain ID & address!
const data = await client?.readContract({
address: KINOMAP,
abi: kinomapAbi,
functionName: "get",
args: [namehash]
})
const tba = data?.[0];
const owner = data?.[1];
console.log('GOT data', data)
console.log('GOT tba', tba)
index = vets.indexOf(NAME_NOT_OWNER);
if (owner === address && index !== -1) vets.splice(index, 1);
else if (index === -1 && owner !== address)
vets.push(NAME_NOT_OWNER);
index = vets.indexOf(NAME_NOT_REGISTERED);
if (index !== -1) vets.splice(index, 1);
if (tba !== undefined) {
setTba(tba);
}
} catch (e) {
index = vets.indexOf(NAME_NOT_REGISTERED);
if (index === -1) vets.push(NAME_NOT_REGISTERED);
}
if (nameVets.length === 0) setOsName(normalized);
}
setNameVets(vets);
}, 500);
}, [name, triggerNameCheck]); // eslint-disable-line react-hooks/exhaustive-deps
if (!address) {
openConnectModal?.();
}
}, [address, openConnectModal]);
const handleResetRecords = useCallback(
async (e: FormEvent) => {
@ -152,8 +74,6 @@ function ResetKnsName({
return;
}
try {
const data = await generateNetworkingKeys({
direct,
@ -167,10 +87,6 @@ function ResetKnsName({
reset: true,
});
console.log('data', data)
console.log('tba', tba)
writeContract({
address: tba as `0x${string}`,
abi: mechAbi,
@ -202,19 +118,22 @@ function ResetKnsName({
return (
<div className="container fade-in">
<div className="section">
{Boolean(address) && (
{
<form className="form" onSubmit={handleResetRecords}>
{isPending || isConfirming ? (
<Loader msg={isConfirming ? "Resetting Networking Information..." : "Please confirm the transaction in your wallet"} />
) : (
<>
<h3 className="form-label">
<Tooltip text="Kinodes use a .os name in order to identify themselves to other nodes in the network.">
Specify the node ID to reset
<Tooltip text="Kinodes use an onchain username in order to identify themselves to other nodes in the network.">
Node ID to reset:
</Tooltip>
</h3>
<EnterKnsName {...{ name, setName, nameVets, triggerNameCheck, nameValidities, setNameValidities, isReset: true }} />
<EnterKnsName {...{ address, name, setName, triggerNameCheck, nameValidities, setNameValidities, setTba, isReset: true }} />
<DirectCheckbox {...{ direct, setDirect }} />
<p>
A reset will not delete any data. It only updates the networking information that your node publishes onchain.
</p>
<button
type="submit"
className="button mt-2"
@ -230,7 +149,7 @@ function ResetKnsName({
</p>
)}
</form>
)}
}
</div>
</div>
);

View File

@ -4,7 +4,7 @@ import { downloadKeyfile } from "../utils/download-keyfile";
import { Tooltip } from "../components/Tooltip";
import { sha256, toBytes } from "viem";
import { useSignTypedData, useAccount, useChainId } from 'wagmi'
import { KINOMAP } from "../abis";
import { KIMAP } from "../abis";
type SetPasswordProps = {
direct: boolean;
@ -61,7 +61,7 @@ function SetPassword({
name: "Kimap",
version: "1",
chainId: chainId,
verifyingContract: KINOMAP,
verifyingContract: KIMAP,
},
types: {
Boot: [
@ -111,7 +111,6 @@ function SetPassword({
res.status < 300 &&
Number(res.headers.get("content-length")) !== appSizeOnLoad
) {
console.log("WE GOOD, ROUTING")
clearInterval(interval);
window.location.replace("/");
}
@ -132,8 +131,8 @@ function SetPassword({
) : (
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<Tooltip text="This password will be used to log in if you restart your node or switch browsers.">
<label className="form-label" htmlFor="password">New Password</label>
<Tooltip text="This password will be used to log in when you restart your node or switch browsers.">
<label className="form-label" htmlFor="password">Set Password</label>
</Tooltip>
<input
type="password"
@ -141,7 +140,7 @@ function SetPassword({
required
minLength={6}
name="password"
placeholder="Min 6 characters"
placeholder="6 characters minimum"
value={pw}
onChange={(e) => setPw(e.target.value)}
autoFocus
@ -155,7 +154,7 @@ function SetPassword({
required
minLength={6}
name="confirm-password"
placeholder="Min 6 characters"
placeholder="6 characters minimum"
value={pw2}
onChange={(e) => setPw2(e.target.value)}
/>

View File

@ -105,6 +105,7 @@ pub async fn register(
.or(warp::path("reset"))
.or(warp::path("import-keyfile"))
.or(warp::path("set-password"))
.or(warp::path("custom-register"))
.and(warp::get())
.map(move |_| warp::reply::html(include_str!("register-ui/build/index.html")));
@ -347,7 +348,6 @@ async fn handle_boot(
// this call can fail if the indexer has not caught up to the transaction
// that just got confirmed on our frontend. for this reason, we retry
// the call a few times before giving up.
// todo remove?
let mut attempts = 0;
let mut get_result = Err(());
@ -443,6 +443,7 @@ async fn handle_import_keyfile(
sender: Arc<RegistrationSender>,
provider: Arc<RootProvider<PubSubFrontend>>,
) -> Result<impl Reply, Rejection> {
println!("received base64 keyfile: {}\r", info.keyfile);
// if keyfile was not present in node and is present from user upload
let encoded_keyfile = match base64_standard.decode(info.keyfile) {
Ok(k) => k,
@ -455,6 +456,10 @@ async fn handle_import_keyfile(
}
};
println!(
"received keyfile: {}\r",
String::from_utf8_lossy(&encoded_keyfile)
);
let (decoded_keyfile, mut our) =
match keygen::decode_keyfile(&encoded_keyfile, &info.password_hash) {
Ok(k) => {

View File

@ -132,7 +132,10 @@ pub async fn terminal(
// the kernel will try and print all events by default so that booting with
// verbosity mode 3 guarantees all events from boot are shown.
if verbose_mode != 3 {
let _ = debug_event_loop.send(DebugCommand::ToggleEventLoop).await;
debug_event_loop
.send(DebugCommand::ToggleEventLoop)
.await
.expect("failed to toggle full event loop off");
}
// only create event stream if not in detached mode
@ -336,8 +339,21 @@ async fn handle_event(
match verbose_mode {
0 => *verbose_mode = 1,
1 => *verbose_mode = 2,
2 => *verbose_mode = 3,
_ => *verbose_mode = 0,
2 => {
*verbose_mode = 3;
debug_event_loop
.send(DebugCommand::ToggleEventLoop)
.await
.expect("failed to toggle ON full event loop");
}
3 => {
*verbose_mode = 0;
debug_event_loop
.send(DebugCommand::ToggleEventLoop)
.await
.expect("failed to toggle OFF full event loop");
}
_ => unreachable!(),
}
Printout::new(
0,
@ -347,15 +363,13 @@ async fn handle_event(
0 => "off",
1 => "debug",
2 => "super-debug",
_ => "full event loop",
3 => "full event loop",
_ => unreachable!(),
}
),
)
.send(&print_tx)
.await;
if *verbose_mode == 3 {
let _ = debug_event_loop.send(DebugCommand::ToggleEventLoop).await;
}
}
//
// CTRL+J: toggle debug mode -- makes system-level event loop step-through

View File

@ -1299,8 +1299,9 @@ impl Printout {
}
}
/// Fire the printout to the terminal without checking for success.
pub async fn send(self, sender: &PrintSender) {
sender.send(self).await.expect("print sender died");
let _ = sender.send(self).await;
}
}