mirror of
https://github.com/uqbar-dao/nectar.git
synced 2025-01-01 21:14:10 +03:00
Merge branch 'release-candidate' into erikdev
This commit is contained in:
commit
bcad1043f2
67
Cargo.lock
generated
67
Cargo.lock
generated
@ -78,7 +78,7 @@ name = "alias"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -983,7 +983,7 @@ dependencies = [
|
||||
"alloy-sol-types",
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1353,7 +1353,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
@ -1516,7 +1516,7 @@ name = "cat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -1580,7 +1580,7 @@ dependencies = [
|
||||
"alloy-sol-types",
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?branch=develop)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1598,7 +1598,7 @@ version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"pleco",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2333,7 +2333,7 @@ name = "download"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -2344,7 +2344,7 @@ name = "downloads"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?branch=develop)",
|
||||
"kinode_process_lib 0.9.1",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2380,7 +2380,7 @@ dependencies = [
|
||||
name = "echo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
@ -2609,7 +2609,7 @@ version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.1",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -2762,7 +2762,7 @@ dependencies = [
|
||||
name = "get_block"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -2825,7 +2825,7 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
name = "globe"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
@ -2952,7 +2952,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
name = "help"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
@ -2981,7 +2981,7 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
|
||||
name = "hi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -3002,7 +3002,7 @@ version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -3330,7 +3330,7 @@ name = "install"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -3506,7 +3506,7 @@ name = "kfetch"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -3518,7 +3518,7 @@ name = "kill"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -3526,7 +3526,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kinode"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"alloy 0.2.1",
|
||||
@ -3547,6 +3547,7 @@ dependencies = [
|
||||
"hex",
|
||||
"hmac",
|
||||
"http 1.1.0",
|
||||
"indexmap",
|
||||
"jwt",
|
||||
"kit 0.6.10",
|
||||
"lazy_static",
|
||||
@ -3635,8 +3636,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kinode_process_lib"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/kinode-dao/process_lib?branch=develop#5c1d8ed36cf10688808c09357ef0e43225396097"
|
||||
version = "0.9.1"
|
||||
source = "git+https://github.com/kinode-dao/process_lib?rev=1c495ad#1c495ad8687dd580aa2aa2222f8e7958679220a6"
|
||||
dependencies = [
|
||||
"alloy 0.1.4",
|
||||
"alloy-primitives",
|
||||
@ -3727,7 +3728,7 @@ dependencies = [
|
||||
"alloy-sol-types",
|
||||
"anyhow",
|
||||
"hex",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -3755,7 +3756,7 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||
|
||||
[[package]]
|
||||
name = "lib"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
dependencies = [
|
||||
"alloy 0.2.1",
|
||||
"kit 0.6.8",
|
||||
@ -3943,7 +3944,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -4104,7 +4105,7 @@ dependencies = [
|
||||
name = "net_diagnostics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"wit-bindgen",
|
||||
@ -4417,7 +4418,7 @@ dependencies = [
|
||||
name = "peer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"wit-bindgen",
|
||||
@ -4427,7 +4428,7 @@ dependencies = [
|
||||
name = "peers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"wit-bindgen",
|
||||
@ -5442,7 +5443,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -5660,7 +5661,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
name = "state"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -5854,7 +5855,7 @@ version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"serde",
|
||||
@ -5868,7 +5869,7 @@ version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"process_macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -6125,7 +6126,7 @@ version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
@ -6442,7 +6443,7 @@ name = "uninstall"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kinode_process_lib 0.9.0 (git+https://github.com/kinode-dao/process_lib?tag=v0.9.0)",
|
||||
"kinode_process_lib 0.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
|
20
Dockerfile
20
Dockerfile
@ -1,18 +1,22 @@
|
||||
FROM debian:12-slim AS downloader
|
||||
FROM --platform=$BUILDPLATFORM alpine AS downloader_start
|
||||
ARG VERSION
|
||||
|
||||
ARG TARGETARCH
|
||||
WORKDIR /tmp/download
|
||||
RUN apk update && apk add unzip wget --no-cache
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install unzip -y
|
||||
|
||||
FROM downloader_start AS downloader_amd64
|
||||
ADD "https://github.com/kinode-dao/kinode/releases/download/${VERSION}/kinode-x86_64-unknown-linux-gnu.zip" kinode-x86_64-unknown-linux-gnu.zip
|
||||
RUN unzip kinode-x86_64-unknown-linux-gnu.zip
|
||||
|
||||
FROM downloader_start AS downloader_arm64
|
||||
ADD "https://github.com/kinode-dao/kinode/releases/download/${VERSION}/kinode-aarch64-unknown-linux-gnu.zip" kinode-aarch64-unknown-linux-gnu.zip
|
||||
RUN unzip kinode-aarch64-unknown-linux-gnu.zip
|
||||
|
||||
FROM downloader_${TARGETARCH} AS downloader
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install openssl -y
|
||||
RUN apt-get update && apt-get install openssl -y
|
||||
|
||||
COPY --from=downloader /tmp/download/kinode /bin/kinode
|
||||
|
||||
@ -20,4 +24,4 @@ ENTRYPOINT [ "/bin/kinode" ]
|
||||
CMD [ "/kinode-home" ]
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 9000
|
||||
EXPOSE 9000
|
||||
|
15
README.md
15
README.md
@ -5,7 +5,6 @@
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
Kinode is a general-purpose sovereign cloud computer, built for crypto.
|
||||
|
||||
This repo contains the core runtime and processes.
|
||||
@ -17,10 +16,10 @@ Then follow the instructions to [install it](https://book.kinode.org/install.htm
|
||||
|
||||
If you have questions, join the [Kinode discord](https://discord.gg/TCgdca5Bjt) and drop us a line in `#dev-support`.
|
||||
|
||||
|
||||
## Setup
|
||||
|
||||
On certain operating systems, you may need to install these dependencies if they are not already present:
|
||||
|
||||
- openssl-sys: https://docs.rs/crate/openssl-sys/0.9.19
|
||||
- libclang 5.0: https://rust-lang.github.io/rust-bindgen/requirements.html
|
||||
|
||||
@ -58,6 +57,7 @@ No security audits of this crate have ever been performed. This software is unde
|
||||
Make sure not to use the same home directory for two nodes at once! You can use any name for the home directory: here we just use `home`. The `--` here separates cargo arguments from binary arguments.
|
||||
|
||||
TODO: document feature flags in `--simulation-mode`
|
||||
|
||||
```bash
|
||||
# OPTIONAL: --release flag
|
||||
cargo +nightly run -p kinode -- home
|
||||
@ -70,6 +70,7 @@ On boot you will be prompted to navigate to `localhost:8080` (or whatever HTTP p
|
||||
By default, a node will use the [hardcoded providers](./kinode/src/eth/default_providers_mainnet.json) for the network it is booted on. A node can use a WebSockets RPC URL directly, or use another Kinode as a relay point. To adjust the providers a node uses, just create and modify the `.eth_providers` file in the node's home folder (set at boot). See the Kinode Book for more docs, and see the [default providers file here](./kinode/src/eth/default_providers_mainnet.json) for a template to create `.eth_providers`.
|
||||
|
||||
You may also add a RPC provider or otherwise modify your configuration by sending messages from the terminal to the `eth:distro:sys` process. You can get one for free at `alchemy.com`. Use this message format to add a provider -- this will make your node's performance better when accessing a blockchain:
|
||||
|
||||
```
|
||||
m our@eth:distro:sys '{"AddProvider": {"chain_id": <SOME_CHAIN_ID>, "trusted": true, "provider": {"RpcUrl": "<WS_RPC_URL>"}}}'
|
||||
```
|
||||
@ -169,9 +170,15 @@ The image includes EXPOSE directives for TCP port `8080` and TCP port `9000`. Po
|
||||
If you are running a direct node, you must map port `9000` to the same port on the host and on your router. Otherwise, your Kinode will not be able to connect to the rest of the network as connection info is written to the chain, and this information is based on the view from inside the Docker container.
|
||||
|
||||
To build a local Docker image, run the following command in this project root.
|
||||
|
||||
```bash
|
||||
# The `VERSION` may be replaced with the tag of a GitHub release
|
||||
docker build -t 0xlynett/kinode . --build-arg VERSION=v0.8.6
|
||||
|
||||
# Build for your system's architecture
|
||||
docker build . -t 0xlynett/kinode --build-arg VERSION=v0.9.1
|
||||
|
||||
# Build a multiarch image
|
||||
docker buildx build . --platform arm64,amd64 --build-arg VERSION=v0.9.1 -t 0xlynett/kinode
|
||||
```
|
||||
|
||||
For example:
|
||||
@ -182,4 +189,4 @@ docker volume create kinode-volume
|
||||
docker run -d -p 8080:8080 -it --name my-kinode \
|
||||
--mount type=volume,source=kinode-volume,destination=/kinode-home \
|
||||
0xlynett/kinode
|
||||
```
|
||||
```
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kinode"
|
||||
authors = ["KinodeDAO"]
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
description = "A general-purpose sovereign cloud computing platform"
|
||||
homepage = "https://kinode.org"
|
||||
@ -58,6 +58,7 @@ generic-array = "0.14.7"
|
||||
hex = "0.4.3"
|
||||
hmac = "0.12"
|
||||
http = "1.1.0"
|
||||
indexmap = "2.4"
|
||||
jwt = "0.16"
|
||||
lib = { path = "../lib" }
|
||||
lazy_static = "1.4.0"
|
||||
|
@ -136,7 +136,8 @@ fn build_and_zip_package(
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
let options = FileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated)
|
||||
.unix_permissions(0o755);
|
||||
.unix_permissions(0o755)
|
||||
.last_modified_time(zip::DateTime::from_date_and_time(2023, 6, 19, 0, 0, 0).unwrap());
|
||||
{
|
||||
let mut zip = zip::ZipWriter::new(&mut writer);
|
||||
|
||||
|
@ -8,9 +8,8 @@ use crate::{
|
||||
|
||||
use kinode_process_lib::{
|
||||
http::{self, server, Method, StatusCode},
|
||||
Address, LazyLoadBlob, PackageId, Request,
|
||||
println, Address, LazyLoadBlob, PackageId, Request, SendError, SendErrorKind,
|
||||
};
|
||||
use kinode_process_lib::{SendError, SendErrorKind};
|
||||
use serde_json::json;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
@ -266,7 +265,7 @@ fn serve_paths(
|
||||
}
|
||||
}
|
||||
// GET detail about a specific app
|
||||
// update a downloaded app: PUT
|
||||
// DELETE uninstall an app
|
||||
"/apps/:id" => {
|
||||
let Ok(package_id) = get_package_id(url_params) else {
|
||||
return Ok((
|
||||
@ -294,6 +293,7 @@ fn serve_paths(
|
||||
Method::DELETE => {
|
||||
// uninstall an app
|
||||
crate::utils::uninstall(state, &package_id)?;
|
||||
println!("successfully uninstalled {:?}", package_id);
|
||||
Ok((
|
||||
StatusCode::NO_CONTENT,
|
||||
None,
|
||||
@ -474,7 +474,10 @@ fn serve_paths(
|
||||
state,
|
||||
&our.node().to_string(),
|
||||
) {
|
||||
Ok(_) => Ok((StatusCode::CREATED, None, vec![])),
|
||||
Ok(_) => {
|
||||
println!("successfully installed package: {:?}", process_package_id);
|
||||
Ok((StatusCode::CREATED, None, vec![]))
|
||||
}
|
||||
Err(e) => Ok((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
None,
|
||||
|
@ -73,8 +73,8 @@ fn init(our: Address) {
|
||||
loop {
|
||||
match await_message() {
|
||||
Err(send_error) => {
|
||||
// TODO handle these based on what they are triggered by
|
||||
println!("got network error: {send_error}");
|
||||
// for now, these are timer callbacks to already finished ft_workers.
|
||||
print_to_terminal(1, &format!("got network error: {send_error}"));
|
||||
}
|
||||
Ok(message) => {
|
||||
if let Err(e) = handle_message(&our, &mut state, &mut http_server, &message) {
|
||||
|
@ -114,11 +114,8 @@ pub fn new_package(
|
||||
|
||||
let download_resp = serde_json::from_slice::<DownloadResponses>(&resp.body())?;
|
||||
|
||||
match download_resp {
|
||||
DownloadResponses::Error(e) => {
|
||||
return Err(anyhow::anyhow!("failed to add download: {:?}", e));
|
||||
}
|
||||
_ => {}
|
||||
if let DownloadResponses::Error(e) = download_resp {
|
||||
return Err(anyhow::anyhow!("failed to add download: {:?}", e));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ alloy-primitives = "0.7.6"
|
||||
alloy-sol-types = "0.7.6"
|
||||
anyhow = "1.0"
|
||||
bincode = "1.3.3"
|
||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", branch = "develop" }
|
||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -38,7 +38,7 @@ const CHAIN_TIMEOUT: u64 = 60; // 60s
|
||||
#[cfg(not(feature = "simulation-mode"))]
|
||||
const KIMAP_ADDRESS: &'static str = kimap::KIMAP_ADDRESS; // optimism
|
||||
#[cfg(feature = "simulation-mode")]
|
||||
const KIMAP_ADDRESS: &str = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658";
|
||||
const KIMAP_ADDRESS: &str = "0xEce71a05B36CA55B895427cD9a440eEF7Cf3669D";
|
||||
|
||||
const DELAY_MS: u64 = 1_000; // 1s
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::kinode::process::downloads::{DownloadRequests, DownloadResponses};
|
||||
use crate::kinode::process::downloads::DownloadRequests;
|
||||
use kinode::process::downloads::LocalDownloadRequest;
|
||||
use kinode_process_lib::{
|
||||
await_next_message_body, call_init, println, Address, Message, PackageId, Request,
|
||||
await_next_message_body, call_init, println, Address, PackageId, Request,
|
||||
};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
@ -37,40 +37,23 @@ fn init(our: Address) {
|
||||
|
||||
let version_hash: String = arg3.to_string();
|
||||
|
||||
let Ok(Ok(Message::Response { body, .. })) =
|
||||
Request::to((our.node(), ("downloads", "app_store", "sys")))
|
||||
.body(
|
||||
serde_json::to_vec(&DownloadRequests::LocalDownload(LocalDownloadRequest {
|
||||
package_id: crate::kinode::process::main::PackageId {
|
||||
package_name: package_id.package_name.clone(),
|
||||
publisher_node: package_id.publisher_node.clone(),
|
||||
},
|
||||
download_from: download_from.clone(),
|
||||
desired_version_hash: version_hash.clone(),
|
||||
}))
|
||||
.expect("Failed to serialize LocalDownloadRequest"),
|
||||
)
|
||||
.send_and_await_response(10)
|
||||
let Ok(_) = Request::to((our.node(), ("downloads", "app_store", "sys")))
|
||||
.body(
|
||||
serde_json::to_vec(&DownloadRequests::LocalDownload(LocalDownloadRequest {
|
||||
package_id: crate::kinode::process::main::PackageId {
|
||||
package_name: package_id.package_name.clone(),
|
||||
publisher_node: package_id.publisher_node.clone(),
|
||||
},
|
||||
download_from: download_from.clone(),
|
||||
desired_version_hash: version_hash.clone(),
|
||||
}))
|
||||
.expect("Failed to serialize LocalDownloadRequest"),
|
||||
)
|
||||
.send()
|
||||
else {
|
||||
println!("download: failed to get a response from app_store..!");
|
||||
println!("download: failed to send request to downloads:app_store!");
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(response) = serde_json::from_slice::<DownloadResponses>(&body) else {
|
||||
println!("download: failed to parse response from app_store..!");
|
||||
return;
|
||||
};
|
||||
|
||||
match response {
|
||||
DownloadResponses::Error(_e) => {
|
||||
println!("download: error");
|
||||
}
|
||||
DownloadResponses::Success => {
|
||||
println!("download: success");
|
||||
}
|
||||
_ => {
|
||||
println!("download: unexpected response from app_store..!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
println!("download: request sent, started download from {download_from}");
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ simulation-mode = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", branch = "develop" }
|
||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", rev = "1c495ad" }
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -14,7 +14,7 @@ use kinode_process_lib::{
|
||||
await_message, call_init, get_blob, get_state,
|
||||
http::client,
|
||||
print_to_terminal, println, set_state,
|
||||
vfs::{self, Directory, File},
|
||||
vfs::{self, Directory},
|
||||
Address, Message, PackageId, ProcessId, Request, Response,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -74,8 +74,9 @@ fn init(our: Address) {
|
||||
.expect("could not create /downloads drive");
|
||||
|
||||
let mut downloads =
|
||||
open_or_create_dir("/app_store:sys/downloads").expect("could not open downloads");
|
||||
let mut tmp = open_or_create_dir("/app_store:sys/downloads/tmp").expect("could not open tmp");
|
||||
vfs::open_dir("/app_store:sys/downloads", true, None).expect("could not open downloads");
|
||||
let mut tmp =
|
||||
vfs::open_dir("/app_store:sys/downloads/tmp", true, None).expect("could not open tmp");
|
||||
|
||||
let mut auto_updates: HashSet<(PackageId, String)> = HashSet::new();
|
||||
|
||||
@ -291,7 +292,7 @@ fn handle_message(
|
||||
downloads.path,
|
||||
add_req.package_id.clone().to_process_lib().to_string()
|
||||
);
|
||||
let _ = open_or_create_dir(&package_dir)?;
|
||||
let _ = vfs::open_dir(&package_dir, true, None)?;
|
||||
|
||||
// Write the zip file
|
||||
let zip_path = format!("{}/{}.zip", package_dir, add_req.version_hash);
|
||||
@ -439,7 +440,7 @@ fn handle_receive_http_download(
|
||||
let bytes = get_blob().ok_or(DownloadError::BlobNotFound)?.bytes;
|
||||
|
||||
let package_dir = format!("{}/{}", "/app_store:sys/downloads", package_id.to_string());
|
||||
let _ = open_or_create_dir(&package_dir).map_err(|_| DownloadError::VfsError)?;
|
||||
let _ = vfs::open_dir(&package_dir, true, None).map_err(|_| DownloadError::VfsError)?;
|
||||
|
||||
let calculated_hash = format!("{:x}", Sha256::digest(&bytes));
|
||||
if calculated_hash != version_hash {
|
||||
@ -519,7 +520,7 @@ fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyh
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let manifest_file = open_or_create_file(&manifest_path)?;
|
||||
let manifest_file = vfs::open_file(&manifest_path, true, None)?;
|
||||
manifest_file.write(contents.as_bytes())?;
|
||||
|
||||
print_to_terminal(1, &format!("Extracted and wrote manifest.json"));
|
||||
@ -540,28 +541,6 @@ fn get_manifest_hash(package_id: PackageId, version_hash: String) -> anyhow::Res
|
||||
Ok(manifest_hash)
|
||||
}
|
||||
|
||||
/// helper function for vfs files, open if exists, if not create
|
||||
fn open_or_create_file(path: &str) -> anyhow::Result<File> {
|
||||
match vfs::open_file(path, false, None) {
|
||||
Ok(file) => Ok(file),
|
||||
Err(_) => match vfs::open_file(path, true, None) {
|
||||
Ok(file) => Ok(file),
|
||||
Err(_) => Err(anyhow::anyhow!("could not create file")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function for vfs directories, open if exists, if not create
|
||||
fn open_or_create_dir(path: &str) -> anyhow::Result<Directory> {
|
||||
match vfs::open_dir(path, true, None) {
|
||||
Ok(dir) => Ok(dir),
|
||||
Err(_) => match vfs::open_dir(path, false, None) {
|
||||
Ok(dir) => Ok(dir),
|
||||
Err(_) => Err(anyhow::anyhow!("could not create dir")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// generate a Keccak-256 hash string (with 0x prefix) of the metadata bytes
|
||||
pub fn keccak_256_hash(bytes: &[u8]) -> String {
|
||||
use sha3::{Digest, Keccak256};
|
||||
|
@ -9,7 +9,7 @@ simulation-mode = []
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bincode = "1.3.3"
|
||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", rev = "1c495ad" }
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
@ -13,12 +13,13 @@ pub fn spawn_send_transfer(
|
||||
to_addr: &Address,
|
||||
) -> anyhow::Result<()> {
|
||||
let transfer_id: u64 = rand::random();
|
||||
let timer_id = ProcessId::new(Some("timer"), "distro", "sys");
|
||||
let Ok(worker_process_id) = spawn(
|
||||
Some(&transfer_id.to_string()),
|
||||
&format!("{}/pkg/ft_worker.wasm", our.package_id()),
|
||||
OnExit::None,
|
||||
our_capabilities(),
|
||||
vec![],
|
||||
vec![timer_id],
|
||||
false,
|
||||
) else {
|
||||
return Err(anyhow::anyhow!("failed to spawn ft_worker!"));
|
||||
@ -48,12 +49,13 @@ pub fn spawn_receive_transfer(
|
||||
timeout: u64,
|
||||
) -> anyhow::Result<Address> {
|
||||
let transfer_id: u64 = rand::random();
|
||||
let timer_id = ProcessId::new(Some("timer"), "distro", "sys");
|
||||
let Ok(worker_process_id) = spawn(
|
||||
Some(&transfer_id.to_string()),
|
||||
&format!("{}/pkg/ft_worker.wasm", our.package_id()),
|
||||
OnExit::None,
|
||||
our_capabilities(),
|
||||
vec![],
|
||||
vec![timer_id],
|
||||
false,
|
||||
) else {
|
||||
return Err(anyhow::anyhow!("failed to spawn ft_worker!"));
|
||||
|
@ -5,7 +5,7 @@ use crate::kinode::process::downloads::{
|
||||
use kinode_process_lib::*;
|
||||
use kinode_process_lib::{
|
||||
print_to_terminal, println, timer,
|
||||
vfs::{open_dir, open_file, Directory, File, SeekFrom},
|
||||
vfs::{File, SeekFrom},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::Read;
|
||||
@ -102,7 +102,7 @@ fn handle_sender(worker: &str, package_id: &PackageId, version_hash: &str) -> an
|
||||
package_id.package_name, package_id.publisher_node, version_hash
|
||||
);
|
||||
|
||||
let mut file = open_file(&filename, false, None)?;
|
||||
let mut file = vfs::open_file(&filename, false, None)?;
|
||||
let size = file.metadata()?.len;
|
||||
let num_chunks = (size as f64 / CHUNK_SIZE as f64).ceil() as u64;
|
||||
|
||||
@ -129,15 +129,23 @@ fn handle_receiver(
|
||||
) -> anyhow::Result<()> {
|
||||
// TODO: write to a temporary location first, then check hash as we go, then rename to final location.
|
||||
|
||||
let package_dir = open_or_create_dir(&format!(
|
||||
"/app_store:sys/downloads/{}:{}/",
|
||||
package_id.package_name,
|
||||
package_id.publisher(),
|
||||
))?;
|
||||
let package_dir = vfs::open_dir(
|
||||
&format!(
|
||||
"/app_store:sys/downloads/{}:{}/",
|
||||
package_id.package_name,
|
||||
package_id.publisher(),
|
||||
),
|
||||
true,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let timer_address = Address::from_str("our@timer:distro:sys")?;
|
||||
|
||||
let mut file = open_or_create_file(&format!("{}{}.zip", &package_dir.path, version_hash))?;
|
||||
let mut file = vfs::open_file(
|
||||
&format!("{}{}.zip", &package_dir.path, version_hash),
|
||||
true,
|
||||
None,
|
||||
)?;
|
||||
let mut size: Option<u64> = None;
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
@ -289,7 +297,7 @@ fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyh
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let manifest_file = open_or_create_file(&manifest_path)?;
|
||||
let manifest_file = vfs::open_file(&manifest_path, true, None)?;
|
||||
manifest_file.write(contents.as_bytes())?;
|
||||
|
||||
print_to_terminal(1, "Extracted and wrote manifest.json");
|
||||
@ -300,28 +308,6 @@ fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyh
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// helper function for vfs files, open if exists, if not create
|
||||
fn open_or_create_file(path: &str) -> anyhow::Result<File> {
|
||||
match open_file(path, false, None) {
|
||||
Ok(file) => Ok(file),
|
||||
Err(_) => match open_file(path, true, None) {
|
||||
Ok(file) => Ok(file),
|
||||
Err(_) => Err(anyhow::anyhow!("could not create file")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function for vfs directories, open if exists, if not create
|
||||
fn open_or_create_dir(path: &str) -> anyhow::Result<Directory> {
|
||||
match open_dir(path, true, None) {
|
||||
Ok(dir) => Ok(dir),
|
||||
Err(_) => match open_dir(path, false, None) {
|
||||
Ok(dir) => Ok(dir),
|
||||
Err(_) => Err(anyhow::anyhow!("could not create dir")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::kinode::process::main::PackageId {
|
||||
pub fn to_process_lib(&self) -> kinode_process_lib::PackageId {
|
||||
kinode_process_lib::PackageId::new(&self.package_name, &self.publisher_node)
|
||||
|
@ -9,6 +9,7 @@
|
||||
"http_server:distro:sys",
|
||||
"main:app_store:sys",
|
||||
"chain:app_store:sys",
|
||||
"terminal:terminal:sys",
|
||||
"vfs:distro:sys",
|
||||
{
|
||||
"process": "vfs:distro:sys",
|
||||
@ -20,6 +21,7 @@
|
||||
"grant_capabilities": [
|
||||
"http_server:distro:sys",
|
||||
"vfs:distro:sys",
|
||||
"terminal:terminal:sys",
|
||||
"http_client:distro:sys"
|
||||
],
|
||||
"public": false
|
||||
@ -99,4 +101,4 @@
|
||||
],
|
||||
"public": false
|
||||
}
|
||||
]
|
||||
]
|
@ -4,10 +4,12 @@
|
||||
"public": false,
|
||||
"request_networking": false,
|
||||
"request_capabilities": [
|
||||
"main:app_store:sys"
|
||||
"main:app_store:sys",
|
||||
"downloads:app_store:sys"
|
||||
],
|
||||
"grant_capabilities": [
|
||||
"main:app_store:sys"
|
||||
"main:app_store:sys",
|
||||
"downloads:app_store:sys"
|
||||
],
|
||||
"wit_version": 0
|
||||
},
|
||||
|
9180
kinode/packages/app_store/ui/package-lock.json
generated
9180
kinode/packages/app_store/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import useAppsStore from "../store";
|
||||
|
||||
interface MirrorSelectorProps {
|
||||
packageId: string | undefined;
|
||||
onMirrorSelect: (mirror: string) => void;
|
||||
onMirrorSelect: (mirror: string, status: boolean | null | 'http') => void;
|
||||
}
|
||||
|
||||
const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSelect }) => {
|
||||
@ -14,36 +14,46 @@ const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSele
|
||||
const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: boolean | null | 'http' }>({});
|
||||
const [availableMirrors, setAvailableMirrors] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMirrors = async () => {
|
||||
if (!packageId) return;
|
||||
const fetchMirrors = useCallback(async () => {
|
||||
if (!packageId) return;
|
||||
|
||||
const appData = await fetchListing(packageId);
|
||||
if (!appData) return;
|
||||
const mirrors = [appData.package_id.publisher_node, ...(appData.metadata?.properties?.mirrors || [])];
|
||||
setAvailableMirrors(mirrors);
|
||||
setSelectedMirror(appData.package_id.publisher_node);
|
||||
const appData = await fetchListing(packageId);
|
||||
if (!appData) return;
|
||||
const mirrors = [appData.package_id.publisher_node, ...(appData.metadata?.properties?.mirrors || [])];
|
||||
setAvailableMirrors(mirrors);
|
||||
setSelectedMirror(appData.package_id.publisher_node);
|
||||
|
||||
mirrors.forEach(mirror => {
|
||||
if (mirror.startsWith('http')) {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: 'http' }));
|
||||
} else {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: null }));
|
||||
checkMirror(mirror)
|
||||
.then(status => setMirrorStatuses(prev => ({ ...prev, [mirror]: status?.is_online ?? false })))
|
||||
.catch(() => setMirrorStatuses(prev => ({ ...prev, [mirror]: false })));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchMirrors();
|
||||
mirrors.forEach(mirror => {
|
||||
if (mirror.startsWith('http')) {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: 'http' }));
|
||||
} else {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: null }));
|
||||
checkMirrorStatus(mirror);
|
||||
}
|
||||
});
|
||||
}, [packageId, fetchListing, checkMirror]);
|
||||
|
||||
useEffect(() => {
|
||||
onMirrorSelect(selectedMirror);
|
||||
}, [selectedMirror, onMirrorSelect]);
|
||||
fetchMirrors();
|
||||
}, [fetchMirrors]);
|
||||
|
||||
const handleMirrorChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const checkMirrorStatus = useCallback(async (mirror: string) => {
|
||||
try {
|
||||
const status = await checkMirror(mirror);
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: status?.is_online ?? false }));
|
||||
} catch {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: false }));
|
||||
}
|
||||
}, [checkMirror]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMirror) {
|
||||
const status = mirrorStatuses[selectedMirror];
|
||||
onMirrorSelect(selectedMirror, status);
|
||||
}
|
||||
}, [selectedMirror, mirrorStatuses, onMirrorSelect]);
|
||||
|
||||
const handleMirrorChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === "custom") {
|
||||
setIsCustomMirrorSelected(true);
|
||||
@ -51,10 +61,15 @@ const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSele
|
||||
setSelectedMirror(value);
|
||||
setIsCustomMirrorSelected(false);
|
||||
setCustomMirror("");
|
||||
if (!value.startsWith('http')) {
|
||||
// Recheck the status when a non-HTTP mirror is selected
|
||||
setMirrorStatuses(prev => ({ ...prev, [value]: null }));
|
||||
await checkMirrorStatus(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetCustomMirror = () => {
|
||||
const handleSetCustomMirror = async () => {
|
||||
if (customMirror) {
|
||||
setSelectedMirror(customMirror);
|
||||
setIsCustomMirrorSelected(false);
|
||||
@ -63,9 +78,7 @@ const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSele
|
||||
if (customMirror.startsWith('http')) {
|
||||
setMirrorStatuses(prev => ({ ...prev, [customMirror]: 'http' }));
|
||||
} else {
|
||||
checkMirror(customMirror)
|
||||
.then(status => setMirrorStatuses(prev => ({ ...prev, [customMirror]: status?.is_online ?? false })))
|
||||
.catch(() => setMirrorStatuses(prev => ({ ...prev, [customMirror]: false })));
|
||||
await checkMirrorStatus(customMirror);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -81,7 +94,7 @@ const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSele
|
||||
<select value={selectedMirror || ""} onChange={handleMirrorChange}>
|
||||
<option value="">Select a mirror</option>
|
||||
{availableMirrors.map((mirror, index) => (
|
||||
<option key={`${mirror}-${index}`} value={mirror} disabled={mirrorStatuses[mirror] === false}>
|
||||
<option key={`${mirror}-${index}`} value={mirror}>
|
||||
{mirror} {getMirrorStatus(mirror, mirrorStatuses[mirror])}
|
||||
</option>
|
||||
))}
|
||||
|
@ -0,0 +1,77 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import useAppsStore from "../store";
|
||||
|
||||
interface PackageSelectorProps {
|
||||
onPackageSelect: (packageName: string, publisherId: string) => void;
|
||||
}
|
||||
|
||||
const PackageSelector: React.FC<PackageSelectorProps> = ({ onPackageSelect }) => {
|
||||
const { installed } = useAppsStore();
|
||||
const [selectedPackage, setSelectedPackage] = useState<string>("");
|
||||
const [customPackage, setCustomPackage] = useState<string>("");
|
||||
const [isCustomPackageSelected, setIsCustomPackageSelected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPackage && selectedPackage !== "custom") {
|
||||
const [packageName, publisherId] = selectedPackage.split(':');
|
||||
onPackageSelect(packageName, publisherId);
|
||||
}
|
||||
}, [selectedPackage, onPackageSelect]);
|
||||
|
||||
const handlePackageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === "custom") {
|
||||
setIsCustomPackageSelected(true);
|
||||
} else {
|
||||
setSelectedPackage(value);
|
||||
setIsCustomPackageSelected(false);
|
||||
setCustomPackage("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetCustomPackage = () => {
|
||||
if (customPackage) {
|
||||
const [packageName, publisherId] = customPackage.split(':');
|
||||
if (packageName && publisherId) {
|
||||
onPackageSelect(packageName, publisherId);
|
||||
setSelectedPackage(customPackage);
|
||||
setIsCustomPackageSelected(false);
|
||||
} else {
|
||||
alert("Please enter the package name and publisher ID in the format 'packageName:publisherId'");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="package-selector">
|
||||
<select value={selectedPackage} onChange={handlePackageChange}>
|
||||
<option value="">Select a package</option>
|
||||
<option value="custom">Custom package</option>
|
||||
{!isCustomPackageSelected && customPackage && (
|
||||
<option value={customPackage}>{customPackage}</option>
|
||||
)}
|
||||
{Object.keys(installed).map((packageFullName) => (
|
||||
<option key={packageFullName} value={packageFullName}>
|
||||
{packageFullName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isCustomPackageSelected && (
|
||||
<div className="custom-package-input">
|
||||
<input
|
||||
type="text"
|
||||
value={customPackage}
|
||||
onChange={(e) => setCustomPackage(e.target.value)}
|
||||
placeholder="Enter as packageName:publisherId"
|
||||
style={{ width: '100%', marginBottom: '10px' }}
|
||||
/>
|
||||
<button onClick={handleSetCustomPackage} disabled={!customPackage}>
|
||||
Set Custom Package
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PackageSelector;
|
@ -1,2 +1,3 @@
|
||||
export { default as Header } from './Header';
|
||||
export { default as MirrorSelector } from './MirrorSelector';
|
||||
export { default as MirrorSelector } from './MirrorSelector';
|
||||
export { default as PackageSelector } from './PackageSelector';
|
@ -224,6 +224,11 @@ td {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--red);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@ -317,35 +322,220 @@ td {
|
||||
|
||||
/* Download Page */
|
||||
.downloads-page {
|
||||
background-color: light-dark(var(--off-white), var(--off-black));
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
background-color: light-dark(var(--white), var(--maroon));
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mirror-selection {
|
||||
max-width: 300px;
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.app-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.launch-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 14px;
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.launch-button:hover {
|
||||
background-color: var(--dark-orange);
|
||||
}
|
||||
|
||||
.version-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.version-selector select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--orange);
|
||||
border-radius: 4px;
|
||||
background-color: light-dark(var(--white), var(--tasteful-dark));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.version-selector select:focus {
|
||||
outline: none;
|
||||
border-color: var(--dark-orange);
|
||||
box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.2);
|
||||
}
|
||||
|
||||
.download-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.download-button,
|
||||
.install-button,
|
||||
.installed-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.download-button:hover:not(:disabled) {
|
||||
background-color: var(--dark-orange);
|
||||
}
|
||||
|
||||
.install-button {
|
||||
background-color: var(--blue);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.install-button:hover {
|
||||
background-color: color-mix(in srgb, var(--blue) 80%, black);
|
||||
}
|
||||
|
||||
.installed-button {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.download-button:disabled,
|
||||
.install-button:disabled,
|
||||
.installed-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.my-downloads {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.my-downloads>button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: transparent;
|
||||
color: var(--orange);
|
||||
border: 2px solid var(--orange);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.my-downloads>button:hover {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.my-downloads table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.my-downloads th,
|
||||
.my-downloads td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--gray);
|
||||
}
|
||||
|
||||
.my-downloads td button {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.app-details {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
.app-details>button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: transparent;
|
||||
color: var(--orange);
|
||||
border: 2px solid var(--orange);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.app-details>button:hover {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.app-details pre {
|
||||
background-color: light-dark(var(--tan), var(--tasteful-dark));
|
||||
border-radius: var(--border-radius);
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cap-approval-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cap-approval-content {
|
||||
background-color: light-dark(var(--white), var(--tasteful-dark));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
background-color: light-dark(var(--tan), var(--off-black));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: light-dark(var(--off-white), var(--off-black));
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.approval-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* My Downloads Page */
|
||||
|
@ -95,7 +95,7 @@ export default function AppPage() {
|
||||
};
|
||||
|
||||
const handleLaunch = () => {
|
||||
navigate(`/${app?.package_id.package_name}:${app?.package_id.package_name}:${app?.package_id.publisher_node}/`);
|
||||
window.location.href = `/${app?.package_id.package_name}:${app?.package_id.package_name}:${app?.package_id.publisher_node}/`;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FaDownload, FaCheck, FaSpinner, FaRocket, FaChevronDown, FaChevronUp, FaTrash } from "react-icons/fa";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { MirrorSelector } from '../components';
|
||||
|
||||
export default function DownloadPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const {
|
||||
listings,
|
||||
@ -20,9 +21,10 @@ export default function DownloadPage() {
|
||||
|
||||
const [showMetadata, setShowMetadata] = useState(false);
|
||||
const [selectedMirror, setSelectedMirror] = useState<string>("");
|
||||
|
||||
const [selectedVersion, setSelectedVersion] = useState<string>("");
|
||||
const [showMyDownloads, setShowMyDownloads] = useState(false);
|
||||
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [selectedVersion, setSelectedVersion] = useState<{ version: string, hash: string } | null>(null);
|
||||
const [manifest, setManifest] = useState<any>(null);
|
||||
|
||||
const app = useMemo(() => listings[id || ""], [listings, id]);
|
||||
@ -31,21 +33,83 @@ export default function DownloadPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
clearAllActiveDownloads();
|
||||
fetchData(id);
|
||||
clearAllActiveDownloads();
|
||||
}
|
||||
}, [id, fetchData, clearAllActiveDownloads]);
|
||||
}, [id, fetchData, clearAllActiveDownloads, installedApp]);
|
||||
|
||||
const handleMirrorSelect = useCallback((mirror: string, status: boolean | null | 'http') => {
|
||||
setSelectedMirror(mirror);
|
||||
setIsMirrorOnline(status === 'http' ? true : status);
|
||||
}, []);
|
||||
|
||||
const sortedVersions = useMemo(() => {
|
||||
if (!app || !app.metadata?.properties?.code_hashes) return [];
|
||||
return app.metadata.properties.code_hashes
|
||||
.map(([version, hash]) => ({ version, hash }))
|
||||
.sort((a, b) => {
|
||||
const vA = a.version.split('.').map(Number);
|
||||
const vB = b.version.split('.').map(Number);
|
||||
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
||||
if (vA[i] > vB[i]) return -1;
|
||||
if (vA[i] < vB[i]) return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [app]);
|
||||
|
||||
useEffect(() => {
|
||||
if (app && !selectedMirror) {
|
||||
setSelectedMirror(app.package_id.publisher_node || "");
|
||||
if (sortedVersions.length > 0 && !selectedVersion) {
|
||||
setSelectedVersion(sortedVersions[0].version);
|
||||
}
|
||||
}, [app, selectedMirror]);
|
||||
}, [sortedVersions, selectedVersion]);
|
||||
|
||||
const handleDownload = useCallback((version: string, hash: string) => {
|
||||
if (!id || !selectedMirror || !app) return;
|
||||
downloadApp(id, hash, selectedMirror);
|
||||
}, [id, selectedMirror, app, downloadApp]);
|
||||
const isDownloaded = useMemo(() => {
|
||||
if (!app || !selectedVersion) return false;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (!versionData) return false;
|
||||
return appDownloads.some(d => d.File && d.File.name === `${versionData.hash}.zip`);
|
||||
}, [app, selectedVersion, sortedVersions, appDownloads]);
|
||||
|
||||
const isDownloading = useMemo(() => {
|
||||
if (!app || !selectedVersion) return false;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (!versionData) return false;
|
||||
// Check for any active download for this app, not just the selected version
|
||||
return Object.keys(activeDownloads).some(key => key.startsWith(`${app.package_id.package_name}:`));
|
||||
}, [app, selectedVersion, sortedVersions, activeDownloads]);
|
||||
|
||||
const downloadProgress = useMemo(() => {
|
||||
if (!isDownloading || !app || !selectedVersion) return null;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (!versionData) return null;
|
||||
// Find the active download for this app
|
||||
const activeDownloadKey = Object.keys(activeDownloads).find(key =>
|
||||
key.startsWith(`${app.package_id.package_name}:`)
|
||||
);
|
||||
if (!activeDownloadKey) return null;
|
||||
const progress = activeDownloads[activeDownloadKey];
|
||||
return progress ? Math.round((progress.downloaded / progress.total) * 100) : 0;
|
||||
}, [isDownloading, app, selectedVersion, sortedVersions, activeDownloads]);
|
||||
|
||||
const isCurrentVersionInstalled = useMemo(() => {
|
||||
if (!app || !selectedVersion || !installedApp) return false;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
return versionData ? installedApp.our_version_hash === versionData.hash : false;
|
||||
}, [app, selectedVersion, installedApp, sortedVersions]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (!id || !selectedMirror || !app || !selectedVersion) return;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
downloadApp(id, versionData.hash, selectedMirror);
|
||||
}
|
||||
}, [id, selectedMirror, app, selectedVersion, sortedVersions, downloadApp]);
|
||||
|
||||
const handleRemoveDownload = useCallback((hash: string) => {
|
||||
if (!id) return;
|
||||
removeDownload(id, hash).then(() => fetchData(id));
|
||||
}, [id, removeDownload, fetchData]);
|
||||
|
||||
const handleInstall = useCallback((version: string, hash: string) => {
|
||||
if (!id || !app) return;
|
||||
@ -54,7 +118,6 @@ export default function DownloadPage() {
|
||||
try {
|
||||
const manifestData = JSON.parse(download.File.manifest);
|
||||
setManifest(manifestData);
|
||||
setSelectedVersion({ version, hash });
|
||||
setShowCapApproval(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse manifest:', error);
|
||||
@ -64,37 +127,27 @@ export default function DownloadPage() {
|
||||
}
|
||||
}, [id, app, appDownloads]);
|
||||
|
||||
const canDownload = useMemo(() => {
|
||||
return selectedMirror && (isMirrorOnline === true || selectedMirror.startsWith('http')) && !isDownloading && !isDownloaded;
|
||||
}, [selectedMirror, isMirrorOnline, isDownloading, isDownloaded]);
|
||||
|
||||
const confirmInstall = useCallback(() => {
|
||||
if (!id || !selectedVersion) return;
|
||||
installApp(id, selectedVersion.hash).then(() => {
|
||||
fetchData(id);
|
||||
setShowCapApproval(false);
|
||||
setManifest(null);
|
||||
});
|
||||
}, [id, selectedVersion, installApp, fetchData]);
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
installApp(id, versionData.hash).then(() => {
|
||||
fetchData(id);
|
||||
setShowCapApproval(false);
|
||||
setManifest(null);
|
||||
});
|
||||
}
|
||||
}, [id, selectedVersion, sortedVersions, installApp, fetchData]);
|
||||
|
||||
const handleRemoveDownload = useCallback((version: string, hash: string) => {
|
||||
if (!id) return;
|
||||
removeDownload(id, hash).then(() => fetchData(id));
|
||||
}, [id, removeDownload, fetchData]);
|
||||
|
||||
const versionList = useMemo(() => {
|
||||
if (!app || !app.metadata?.properties?.code_hashes) return [];
|
||||
|
||||
return app.metadata.properties.code_hashes.map(([version, hash]) => {
|
||||
const download = appDownloads.find(d => d.File && d.File.name === `${hash}.zip`);
|
||||
const downloadKey = `${app.package_id.package_name}:${app.package_id.publisher_node}:${hash}`;
|
||||
const activeDownload = activeDownloads[downloadKey];
|
||||
const isDownloaded = !!download?.File && download.File.size > 0;
|
||||
const isInstalled = installedApp?.our_version_hash === hash;
|
||||
const isDownloading = !!activeDownload && activeDownload.downloaded < activeDownload.total;
|
||||
const progress = isDownloading ? activeDownload : { downloaded: 0, total: 100 };
|
||||
|
||||
console.log(`Version ${version} - isInstalled: ${isInstalled}, installedApp:`, installedApp);
|
||||
|
||||
return { version, hash, isDownloaded, isInstalled, isDownloading, progress };
|
||||
});
|
||||
}, [app, appDownloads, activeDownloads, installedApp]);
|
||||
const handleLaunch = useCallback(() => {
|
||||
if (app) {
|
||||
navigate(`/${app.package_id.package_name}:${app.package_id.package_name}:${app.package_id.publisher_node}/`);
|
||||
}
|
||||
}, [app, navigate]);
|
||||
|
||||
if (!app) {
|
||||
return <div className="downloads-page"><h4>Loading app details...</h4></div>;
|
||||
@ -102,82 +155,108 @@ export default function DownloadPage() {
|
||||
|
||||
return (
|
||||
<div className="downloads-page">
|
||||
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
|
||||
<div className="app-header">
|
||||
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
|
||||
{installedApp && (
|
||||
<button onClick={handleLaunch} className="launch-button">
|
||||
<FaPlay /> Launch
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p>{app.metadata?.description}</p>
|
||||
|
||||
<MirrorSelector packageId={id} onMirrorSelect={setSelectedMirror} />
|
||||
<div className="version-selector">
|
||||
<select
|
||||
value={selectedVersion}
|
||||
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||
>
|
||||
{sortedVersions.map(({ version }, index) => (
|
||||
<option key={version} value={version}>
|
||||
{version} {index === 0 ? "(newest)" : ""}
|
||||
{installedApp && installedApp.our_version_hash === sortedVersions[index].hash ? " (installed)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="version-list">
|
||||
<h3>Available Versions</h3>
|
||||
{versionList.length === 0 ? (
|
||||
<p>No versions available for this app.</p>
|
||||
<div className="download-section">
|
||||
<MirrorSelector
|
||||
packageId={id}
|
||||
onMirrorSelect={handleMirrorSelect}
|
||||
/>
|
||||
{isCurrentVersionInstalled ? (
|
||||
<button className="installed-button" disabled>
|
||||
<FaRocket /> Installed
|
||||
</button>
|
||||
) : isDownloaded ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
handleInstall(versionData.version, versionData.hash);
|
||||
}
|
||||
}}
|
||||
className="install-button"
|
||||
>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!canDownload}
|
||||
className="download-button"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<FaSpinner className="fa-spin" />
|
||||
Downloading... {downloadProgress}%
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload /> Download
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="my-downloads">
|
||||
<button onClick={() => setShowMyDownloads(!showMyDownloads)}>
|
||||
{showMyDownloads ? <FaChevronUp /> : <FaChevronDown />} My Downloads
|
||||
</button>
|
||||
{showMyDownloads && (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versionList.map(({ version, hash, isDownloaded, isInstalled, isDownloading, progress }) => (
|
||||
<tr key={version}>
|
||||
<td>{version}</td>
|
||||
<td>
|
||||
{isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : isDownloading ? 'Downloading' : 'Not downloaded'}
|
||||
</td>
|
||||
<td>
|
||||
{!isDownloaded && !isDownloading && (
|
||||
<button
|
||||
onClick={() => handleDownload(version, hash)}
|
||||
disabled={!selectedMirror}
|
||||
className="download-button"
|
||||
>
|
||||
<FaDownload /> Download
|
||||
{appDownloads.map((download) => {
|
||||
const fileName = download.File?.name;
|
||||
const hash = fileName ? fileName.replace('.zip', '') : '';
|
||||
const versionData = sortedVersions.find(v => v.hash === hash);
|
||||
if (!versionData) return null;
|
||||
return (
|
||||
<tr key={hash}>
|
||||
<td>{versionData.version}</td>
|
||||
<td>
|
||||
<button onClick={() => handleInstall(versionData.version, hash)}>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
)}
|
||||
{isDownloading && (
|
||||
<div className="download-progress">
|
||||
<FaSpinner className="fa-spin" />
|
||||
Downloading... {Math.round((progress.downloaded / progress.total) * 100)}%
|
||||
</div>
|
||||
)}
|
||||
{isDownloaded && !isInstalled && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleInstall(version, hash)}
|
||||
className="install-button"
|
||||
>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemoveDownload(version, hash)}
|
||||
className="delete-button"
|
||||
>
|
||||
<FaTrash /> Delete
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{isInstalled && <FaCheck className="installed" />}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<button onClick={() => handleRemoveDownload(hash)}>
|
||||
<FaTrash /> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-details">
|
||||
<h3>App Details</h3>
|
||||
<button onClick={() => setShowMetadata(!showMetadata)}>
|
||||
{showMetadata ? <FaChevronUp /> : <FaChevronDown />} Metadata
|
||||
</button>
|
||||
{showMetadata && (
|
||||
<pre>{JSON.stringify(app.metadata, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCapApproval && manifest && (
|
||||
<div className="cap-approval-popup">
|
||||
<div className="cap-approval-content">
|
||||
@ -194,6 +273,17 @@ export default function DownloadPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="app-details">
|
||||
<h3>App Details</h3>
|
||||
<button onClick={() => setShowMetadata(!showMetadata)}>
|
||||
{showMetadata ? <FaChevronUp /> : <FaChevronDown />} Metadata
|
||||
</button>
|
||||
{showMetadata && (
|
||||
<pre>{JSON.stringify(app.metadata, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -6,10 +6,13 @@ import { keccak256, toBytes } from 'viem';
|
||||
import { mechAbi, KIMAP, encodeIntoMintCall, encodeMulticalls, kimapAbi, MULTICALL } from "../abis";
|
||||
import { kinohash } from '../utils/kinohash';
|
||||
import useAppsStore from "../store";
|
||||
import { PackageSelector } from "../components";
|
||||
|
||||
const NAME_INVALID = "Package name must contain only valid characters (a-z, 0-9, -, and .)";
|
||||
|
||||
export default function PublishPage() {
|
||||
const { openConnectModal } = useConnectModal();
|
||||
const { ourApps, fetchOurApps } = useAppsStore();
|
||||
const { ourApps, fetchOurApps, installed, downloads } = useAppsStore();
|
||||
const publicClient = usePublicClient();
|
||||
|
||||
const { address, isConnected, isConnecting } = useAccount();
|
||||
@ -24,27 +27,79 @@ export default function PublishPage() {
|
||||
const [metadataUrl, setMetadataUrl] = useState<string>("");
|
||||
const [metadataHash, setMetadataHash] = useState<string>("");
|
||||
|
||||
const [nameValidity, setNameValidity] = useState<string | null>(null);
|
||||
const [metadataError, setMetadataError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOurApps();
|
||||
}, [fetchOurApps]);
|
||||
|
||||
const validatePackageName = useCallback((name: string) => {
|
||||
// Allow lowercase letters, numbers, hyphens, and dots
|
||||
const validNameRegex = /^[a-z0-9.-]+$/;
|
||||
|
||||
if (!validNameRegex.test(name)) {
|
||||
setNameValidity(NAME_INVALID);
|
||||
} else {
|
||||
setNameValidity(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (packageName) {
|
||||
validatePackageName(packageName);
|
||||
} else {
|
||||
setNameValidity(null);
|
||||
}
|
||||
}, [packageName, validatePackageName]);
|
||||
|
||||
|
||||
const calculateMetadataHash = useCallback(async () => {
|
||||
if (!metadataUrl) {
|
||||
setMetadataHash("");
|
||||
setMetadataError("");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const metadataResponse = await fetch(metadataUrl);
|
||||
const metadataText = await metadataResponse.text();
|
||||
JSON.parse(metadataText); // confirm it's valid JSON
|
||||
const metadata = JSON.parse(metadataText);
|
||||
|
||||
// Check if code_hashes exist in metadata and is an object
|
||||
if (metadata.properties && metadata.properties.code_hashes && typeof metadata.properties.code_hashes === 'object') {
|
||||
const codeHashes = metadata.properties.code_hashes;
|
||||
const missingHashes = Object.entries(codeHashes).filter(([version, hash]) =>
|
||||
!downloads[`${packageName}:${publisherId}`]?.some(d => d.File?.name === `${hash}.zip`)
|
||||
);
|
||||
|
||||
if (missingHashes.length > 0) {
|
||||
setMetadataError(`Missing local downloads for mirroring versions: ${missingHashes.map(([version]) => version).join(', ')}`);
|
||||
} else {
|
||||
setMetadataError("");
|
||||
}
|
||||
} else {
|
||||
setMetadataError("The metadata does not contain the required 'code_hashes' property or it's not in the expected format");
|
||||
}
|
||||
|
||||
const metadataHash = keccak256(toBytes(metadataText));
|
||||
setMetadataHash(metadataHash);
|
||||
} catch (error) {
|
||||
alert("Error calculating metadata hash. Please ensure the URL is valid and the metadata is in JSON format.");
|
||||
if (error instanceof SyntaxError) {
|
||||
setMetadataError("The metadata is not valid JSON. Please check the file for syntax errors.");
|
||||
} else if (error instanceof Error) {
|
||||
setMetadataError(`Error processing metadata: ${error.message}`);
|
||||
} else {
|
||||
setMetadataError("An unknown error occurred while processing the metadata.");
|
||||
}
|
||||
setMetadataHash("");
|
||||
}
|
||||
}, [metadataUrl]);
|
||||
}, [metadataUrl, packageName, publisherId, downloads]);
|
||||
|
||||
const handlePackageSelection = (packageName: string, publisherId: string) => {
|
||||
setPackageName(packageName);
|
||||
setPublisherId(publisherId);
|
||||
};
|
||||
|
||||
const publishPackage = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
@ -187,26 +242,11 @@ export default function PublishPage() {
|
||||
) : (
|
||||
<form className="publish-form" onSubmit={publishPackage}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="package-name">Package Name</label>
|
||||
<input
|
||||
id="package-name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="my-package"
|
||||
value={packageName}
|
||||
onChange={(e) => setPackageName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="publisher-id">Publisher ID</label>
|
||||
<input
|
||||
id="publisher-id"
|
||||
type="text"
|
||||
required
|
||||
value={publisherId}
|
||||
onChange={(e) => setPublisherId(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="package-select">Select Package</label>
|
||||
<PackageSelector onPackageSelect={handlePackageSelection} />
|
||||
{nameValidity && <p className="error-message">{nameValidity}</p>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="metadata-url">Metadata URL</label>
|
||||
<input
|
||||
@ -221,6 +261,7 @@ export default function PublishPage() {
|
||||
<p className="help-text">
|
||||
Metadata is a JSON file that describes your package.
|
||||
</p>
|
||||
{metadataError && <p className="error-message">{metadataError}</p>}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="metadata-hash">Metadata Hash</label>
|
||||
@ -232,7 +273,7 @@ export default function PublishPage() {
|
||||
placeholder="Calculated automatically from metadata URL"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" disabled={isConfirming}>
|
||||
<button type="submit" disabled={isConfirming || nameValidity !== null}>
|
||||
{isConfirming ? 'Publishing...' : 'Publish'}
|
||||
</button>
|
||||
</form>
|
||||
|
@ -153,6 +153,8 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> {
|
||||
notes_filter.clone(),
|
||||
&mut pending_notes,
|
||||
);
|
||||
// set a timer tick so any pending logs will be processed
|
||||
timer::set_timer(DELAY_MS, None);
|
||||
println!("done syncing old logs.");
|
||||
|
||||
loop {
|
||||
@ -276,7 +278,10 @@ fn handle_pending_notes(
|
||||
for (note, attempt) in notes.drain(..) {
|
||||
if attempt >= MAX_PENDING_ATTEMPTS {
|
||||
// skip notes that have exceeded max attempts
|
||||
println!("dropping note from block {block} after {attempt} attempts");
|
||||
print_to_terminal(
|
||||
1,
|
||||
&format!("dropping note from block {block} after {attempt} attempts"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = handle_note(state, ¬e) {
|
||||
@ -433,15 +438,20 @@ fn handle_log(
|
||||
if !kimap::valid_note(¬e) {
|
||||
return Err(anyhow::anyhow!("skipping invalid note: {note}"));
|
||||
}
|
||||
if let Some(block_number) = log.block_number {
|
||||
print_to_terminal(
|
||||
1,
|
||||
&format!("adding note to pending_notes for block {block_number}"),
|
||||
);
|
||||
pending_notes
|
||||
.entry(block_number)
|
||||
.or_default()
|
||||
.push((decoded, 0));
|
||||
// handle note: if it precedes parent mint event, add it to pending_notes
|
||||
if let Err(e) = handle_note(state, &decoded) {
|
||||
if let Some(KnsError::NoParentError) = e.downcast_ref::<KnsError>() {
|
||||
if let Some(block_number) = log.block_number {
|
||||
print_to_terminal(
|
||||
1,
|
||||
&format!("adding note to pending_notes for block {block_number}"),
|
||||
);
|
||||
pending_notes
|
||||
.entry(block_number)
|
||||
.or_default()
|
||||
.push((decoded, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_log => {
|
||||
|
@ -4,11 +4,14 @@ use alloy::rpc::client::WsConnect;
|
||||
use alloy::rpc::json_rpc::RpcError;
|
||||
use anyhow::Result;
|
||||
use dashmap::DashMap;
|
||||
use indexmap::IndexMap;
|
||||
use lib::types::core::*;
|
||||
use lib::types::eth::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
use url::Url;
|
||||
|
||||
@ -158,8 +161,15 @@ struct ModuleState {
|
||||
send_to_loop: MessageSender,
|
||||
/// our sender for terminal prints
|
||||
print_tx: PrintSender,
|
||||
/// cache of ETH requests
|
||||
request_cache: RequestCache,
|
||||
}
|
||||
|
||||
type RequestCache = Arc<Mutex<IndexMap<Vec<u8>, (EthResponse, Instant)>>>;
|
||||
|
||||
const DELAY_MS: u64 = 1_000;
|
||||
const MAX_REQUEST_CACHE_LEN: usize = 500;
|
||||
|
||||
/// TODO replace with alloy abstraction
|
||||
fn valid_method(method: &str) -> Option<&'static str> {
|
||||
match method {
|
||||
@ -240,6 +250,7 @@ pub async fn provider(
|
||||
response_channels: Arc::new(DashMap::new()),
|
||||
send_to_loop,
|
||||
print_tx,
|
||||
request_cache: Arc::new(Mutex::new(IndexMap::new())),
|
||||
};
|
||||
|
||||
// convert saved configs into data structure that we will use to route queries
|
||||
@ -598,13 +609,14 @@ async fn handle_eth_action(
|
||||
}
|
||||
}
|
||||
EthAction::Request { .. } => {
|
||||
let (sender, receiver) = tokio::sync::mpsc::channel(1);
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
|
||||
state.response_channels.insert(km.id, sender);
|
||||
let our = state.our.to_string();
|
||||
let send_to_loop = state.send_to_loop.clone();
|
||||
let providers = state.providers.clone();
|
||||
let response_channels = state.response_channels.clone();
|
||||
let print_tx = state.print_tx.clone();
|
||||
let mut request_cache = Arc::clone(&state.request_cache);
|
||||
tokio::spawn(async move {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(timeout),
|
||||
@ -612,26 +624,72 @@ async fn handle_eth_action(
|
||||
&our,
|
||||
km.id,
|
||||
&send_to_loop,
|
||||
eth_action,
|
||||
providers,
|
||||
receiver,
|
||||
ð_action,
|
||||
&providers,
|
||||
&mut receiver,
|
||||
&print_tx,
|
||||
&mut request_cache,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
kernel_message(
|
||||
&our,
|
||||
km.id,
|
||||
km.rsvp.unwrap_or(km.source),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
response,
|
||||
&send_to_loop,
|
||||
)
|
||||
.await;
|
||||
if let EthResponse::Err(EthError::RpcError(_)) = response {
|
||||
// try one more time after 1s delay in case RPC is rate limiting
|
||||
std::thread::sleep(std::time::Duration::from_millis(DELAY_MS));
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(timeout),
|
||||
fulfill_request(
|
||||
&our,
|
||||
km.id,
|
||||
&send_to_loop,
|
||||
ð_action,
|
||||
&providers,
|
||||
&mut receiver,
|
||||
&print_tx,
|
||||
&mut request_cache,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
kernel_message(
|
||||
&our,
|
||||
km.id,
|
||||
km.rsvp.clone().unwrap_or(km.source.clone()),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
response,
|
||||
&send_to_loop,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(_) => {
|
||||
// task timeout
|
||||
error_message(
|
||||
&our,
|
||||
km.id,
|
||||
km.source.clone(),
|
||||
EthError::RpcTimeout,
|
||||
&send_to_loop,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kernel_message(
|
||||
&our,
|
||||
km.id,
|
||||
km.rsvp.unwrap_or(km.source),
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
response,
|
||||
&send_to_loop,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// task timeout
|
||||
@ -650,19 +708,31 @@ async fn fulfill_request(
|
||||
our: &str,
|
||||
km_id: u64,
|
||||
send_to_loop: &MessageSender,
|
||||
eth_action: EthAction,
|
||||
providers: Providers,
|
||||
mut remote_request_receiver: ProcessMessageReceiver,
|
||||
eth_action: &EthAction,
|
||||
providers: &Providers,
|
||||
remote_request_receiver: &mut ProcessMessageReceiver,
|
||||
print_tx: &PrintSender,
|
||||
request_cache: &mut RequestCache,
|
||||
) -> EthResponse {
|
||||
let serialized_action = serde_json::to_vec(eth_action).unwrap();
|
||||
let EthAction::Request {
|
||||
chain_id,
|
||||
ref chain_id,
|
||||
ref method,
|
||||
ref params,
|
||||
} = eth_action
|
||||
else {
|
||||
return EthResponse::Err(EthError::PermissionDenied); // will never hit
|
||||
};
|
||||
{
|
||||
let mut request_cache = request_cache.lock().await;
|
||||
if let Some((cache_hit, time_of_hit)) = request_cache.shift_remove(&serialized_action) {
|
||||
// refresh cache entry (it is most recently accessed) & return it
|
||||
if time_of_hit.elapsed() < Duration::from_millis(DELAY_MS) {
|
||||
request_cache.insert(serialized_action, (cache_hit.clone(), time_of_hit));
|
||||
return cache_hit;
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(method) = valid_method(&method) else {
|
||||
return EthResponse::Err(EthError::InvalidMethod(method.to_string()));
|
||||
};
|
||||
@ -703,7 +773,7 @@ async fn fulfill_request(
|
||||
match pubsub.raw_request(method.into(), params.clone()).await {
|
||||
Ok(value) => {
|
||||
let mut is_replacement_successful = true;
|
||||
providers.entry(chain_id).and_modify(|aps| {
|
||||
providers.entry(chain_id.clone()).and_modify(|aps| {
|
||||
let Some(index) = find_index(
|
||||
&aps.urls.iter().map(|u| u.url.as_str()).collect(),
|
||||
&url_provider.url,
|
||||
@ -724,7 +794,14 @@ async fn fulfill_request(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return EthResponse::Response { value };
|
||||
let response = EthResponse::Response { value };
|
||||
let mut request_cache = request_cache.lock().await;
|
||||
if request_cache.len() >= MAX_REQUEST_CACHE_LEN {
|
||||
// drop 10% oldest cache entries
|
||||
request_cache.drain(0..MAX_REQUEST_CACHE_LEN / 10);
|
||||
}
|
||||
request_cache.insert(serialized_action, (response.clone(), Instant::now()));
|
||||
return response;
|
||||
}
|
||||
Err(rpc_error) => {
|
||||
verbose_print(
|
||||
@ -741,7 +818,7 @@ async fn fulfill_request(
|
||||
}
|
||||
// this provider failed and needs to be reset
|
||||
let mut is_reset_successful = true;
|
||||
providers.entry(chain_id).and_modify(|aps| {
|
||||
providers.entry(chain_id.clone()).and_modify(|aps| {
|
||||
let Some(index) = find_index(
|
||||
&aps.urls.iter().map(|u| u.url.as_str()).collect(),
|
||||
&url_provider.url,
|
||||
@ -787,7 +864,7 @@ async fn fulfill_request(
|
||||
node_provider,
|
||||
eth_action.clone(),
|
||||
send_to_loop,
|
||||
&mut remote_request_receiver,
|
||||
remote_request_receiver,
|
||||
)
|
||||
.await;
|
||||
if let EthResponse::Err(e) = response {
|
||||
@ -1097,37 +1174,48 @@ async fn kernel_message<T: Serialize>(
|
||||
body: T,
|
||||
send_to_loop: &MessageSender,
|
||||
) {
|
||||
let _ = send_to_loop
|
||||
.send(KernelMessage {
|
||||
id: km_id,
|
||||
source: Address {
|
||||
node: our.to_string(),
|
||||
process: ETH_PROCESS_ID.clone(),
|
||||
},
|
||||
target,
|
||||
rsvp,
|
||||
message: if req {
|
||||
Message::Request(Request {
|
||||
let Err(e) = send_to_loop.try_send(KernelMessage {
|
||||
id: km_id,
|
||||
source: Address {
|
||||
node: our.to_string(),
|
||||
process: ETH_PROCESS_ID.clone(),
|
||||
},
|
||||
target,
|
||||
rsvp,
|
||||
message: if req {
|
||||
Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: timeout,
|
||||
body: serde_json::to_vec(&body).unwrap(),
|
||||
metadata: None,
|
||||
capabilities: vec![],
|
||||
})
|
||||
} else {
|
||||
Message::Response((
|
||||
Response {
|
||||
inherit: false,
|
||||
expects_response: timeout,
|
||||
body: serde_json::to_vec(&body).unwrap(),
|
||||
metadata: None,
|
||||
capabilities: vec![],
|
||||
})
|
||||
} else {
|
||||
Message::Response((
|
||||
Response {
|
||||
inherit: false,
|
||||
body: serde_json::to_vec(&body).unwrap(),
|
||||
metadata: None,
|
||||
capabilities: vec![],
|
||||
},
|
||||
None,
|
||||
))
|
||||
},
|
||||
lazy_load_blob: None,
|
||||
})
|
||||
.await;
|
||||
},
|
||||
None,
|
||||
))
|
||||
},
|
||||
lazy_load_blob: None,
|
||||
}) else {
|
||||
// not Err -> send successful; done here
|
||||
return;
|
||||
};
|
||||
// its an Err: handle
|
||||
match e {
|
||||
tokio::sync::mpsc::error::TrySendError::Closed(_) => {
|
||||
panic!("(eth) kernel message sender: receiver closed");
|
||||
}
|
||||
tokio::sync::mpsc::error::TrySendError::Full(_) => {
|
||||
// TODO: implement backpressure
|
||||
panic!("(eth) kernel overloaded with messages: TODO: implement backpressure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_index(vec: &Vec<&str>, item: &str) -> Option<usize> {
|
||||
|
@ -386,24 +386,20 @@ async fn maintain_local_subscription(
|
||||
mut close_receiver: tokio::sync::mpsc::Receiver<bool>,
|
||||
print_tx: &PrintSender,
|
||||
) -> Result<(), EthSubError> {
|
||||
loop {
|
||||
let e = loop {
|
||||
tokio::select! {
|
||||
_ = close_receiver.recv() => {
|
||||
unsubscribe(rx, &chain_id, providers, print_tx).await;
|
||||
//unsubscribe(rx, &chain_id, providers, print_tx).await;
|
||||
return Ok(());
|
||||
},
|
||||
value = rx.recv() => {
|
||||
let Ok(value) = value else {
|
||||
break;
|
||||
let value = match value {
|
||||
Ok(v) => v,
|
||||
Err(e) => break e.to_string(),
|
||||
};
|
||||
let result: SubscriptionResult = match serde_json::from_str(value.get()) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
return Err(EthSubError {
|
||||
id: sub_id,
|
||||
error: e.to_string(),
|
||||
});
|
||||
}
|
||||
Err(e) => break e.to_string(),
|
||||
};
|
||||
kernel_message(
|
||||
our,
|
||||
@ -418,16 +414,16 @@ async fn maintain_local_subscription(
|
||||
.await;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
active_subscriptions
|
||||
.entry(target.clone())
|
||||
.and_modify(|sub_map| {
|
||||
sub_map.remove(&sub_id);
|
||||
});
|
||||
unsubscribe(rx, &chain_id, providers, print_tx).await;
|
||||
//unsubscribe(rx, &chain_id, providers, print_tx).await;
|
||||
Err(EthSubError {
|
||||
id: sub_id,
|
||||
error: format!("subscription ({target}) closed unexpectedly"),
|
||||
error: format!("subscription ({target}) closed unexpectedly {e}"),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -58,38 +58,7 @@ pub async fn mint_local(
|
||||
|
||||
let provider: RootProvider<PubSubFrontend> = ProviderBuilder::default().on_ws(ws).await?;
|
||||
|
||||
// interesting, even if we have a minted name, this does not explicitly fail.
|
||||
// also note, fake.dev.os seems to currently work, need to gate dots from names?
|
||||
let mint_call = mintCall {
|
||||
who: wallet_address,
|
||||
label: Bytes::from(label.as_bytes().to_vec()),
|
||||
initialization: vec![].into(),
|
||||
erc721Data: vec![].into(),
|
||||
implementation: Address::from_str(KINO_ACCOUNT_IMPL).unwrap(),
|
||||
}
|
||||
.abi_encode();
|
||||
|
||||
let nonce = provider.get_transaction_count(wallet_address).await?;
|
||||
|
||||
let tx = TransactionRequest::default()
|
||||
.to(minter)
|
||||
.input(TransactionInput::new(mint_call.into()))
|
||||
.nonce(nonce)
|
||||
.with_chain_id(31337)
|
||||
.with_gas_limit(12_000_00)
|
||||
.with_max_priority_fee_per_gas(200_000_000_000)
|
||||
.with_max_fee_per_gas(300_000_000_000);
|
||||
|
||||
// Build the transaction using the `EthereumSigner` with the provided signer.
|
||||
let tx_envelope = tx.build(&wallet).await?;
|
||||
|
||||
// Encode the transaction using EIP-2718 encoding.
|
||||
let tx_encoded = tx_envelope.encoded_2718();
|
||||
|
||||
// Send the raw transaction and retrieve the transaction receipt.
|
||||
let _tx_hash = provider.send_raw_transaction(&tx_encoded).await?;
|
||||
|
||||
// get tba to set KNS records
|
||||
// get tba to see if name is already registered
|
||||
let namehash: [u8; 32] = keygen::namehash(name);
|
||||
|
||||
let get_call = getCall {
|
||||
@ -149,30 +118,79 @@ pub async fn mint_local(
|
||||
},
|
||||
];
|
||||
|
||||
let is_reset = tba != Address::default();
|
||||
|
||||
let multicall = aggregateCall { calls: multicalls }.abi_encode();
|
||||
|
||||
let execute_call = executeCall {
|
||||
let execute_call: Vec<u8> = executeCall {
|
||||
to: multicall_address,
|
||||
value: U256::from(0), // free mint
|
||||
data: multicall.into(),
|
||||
operation: 1, // ?
|
||||
operation: 1,
|
||||
}
|
||||
.abi_encode();
|
||||
|
||||
let (input_bytes, to) = if is_reset {
|
||||
// name is already registered, multicall reset it
|
||||
(execute_call, tba)
|
||||
} else {
|
||||
// name is not registered, mint it with multicall in initialization param
|
||||
(
|
||||
mintCall {
|
||||
who: wallet_address,
|
||||
label: Bytes::from(label.as_bytes().to_vec()),
|
||||
initialization: execute_call.into(),
|
||||
erc721Data: vec![].into(),
|
||||
implementation: Address::from_str(KINO_ACCOUNT_IMPL).unwrap(),
|
||||
}
|
||||
.abi_encode(),
|
||||
minter,
|
||||
)
|
||||
};
|
||||
|
||||
let nonce = provider.get_transaction_count(wallet_address).await?;
|
||||
|
||||
let tx = TransactionRequest::default()
|
||||
.to(tba)
|
||||
.input(TransactionInput::new(execute_call.into()))
|
||||
.to(to)
|
||||
.input(TransactionInput::new(input_bytes.into()))
|
||||
.nonce(nonce)
|
||||
.with_chain_id(31337)
|
||||
.with_gas_limit(12_000_00)
|
||||
.with_max_priority_fee_per_gas(200_000_000_000)
|
||||
.with_max_fee_per_gas(300_000_000_000);
|
||||
|
||||
// Build the transaction using the `EthereumSigner` with the provided signer.
|
||||
let tx_envelope = tx.build(&wallet).await?;
|
||||
|
||||
// Encode the transaction using EIP-2718 encoding.
|
||||
let tx_encoded = tx_envelope.encoded_2718();
|
||||
let _tx_hash = provider.send_raw_transaction(&tx_encoded).await?;
|
||||
|
||||
// Send the raw transaction and retrieve the transaction receipt.
|
||||
let tx_hash = provider.send_raw_transaction(&tx_encoded).await?;
|
||||
let _receipt = tx_hash.get_receipt().await?;
|
||||
|
||||
// send a small amount of ETH to the zero address
|
||||
// this is a workaround to get anvil to mine a block after our registration tx
|
||||
// instead of doing block-time 1s or similar, which leads to runaway mem-usage.
|
||||
let zero_address = Address::default();
|
||||
let small_amount = U256::from(10); // 10 wei (0.00000001 ETH)
|
||||
|
||||
let nonce = provider.get_transaction_count(wallet_address).await?;
|
||||
|
||||
let small_tx = TransactionRequest::default()
|
||||
.to(zero_address)
|
||||
.value(small_amount)
|
||||
.nonce(nonce)
|
||||
.with_chain_id(31337)
|
||||
.with_gas_limit(21_000)
|
||||
.with_max_priority_fee_per_gas(200_000_000_000)
|
||||
.with_max_fee_per_gas(300_000_000_000);
|
||||
|
||||
let small_tx_envelope = small_tx.build(&wallet).await?;
|
||||
let small_tx_encoded = small_tx_envelope.encoded_2718();
|
||||
|
||||
let small_tx_hash = provider.send_raw_transaction(&small_tx_encoded).await?;
|
||||
let _small_receipt = small_tx_hash.get_receipt().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ mod terminal;
|
||||
mod timer;
|
||||
mod vfs;
|
||||
|
||||
const EVENT_LOOP_CHANNEL_CAPACITY: usize = 10_000;
|
||||
const EVENT_LOOP_CHANNEL_CAPACITY: usize = 100_000;
|
||||
const EVENT_LOOP_DEBUG_CHANNEL_CAPACITY: usize = 50;
|
||||
const TERMINAL_CHANNEL_CAPACITY: usize = 32;
|
||||
const WEBSOCKET_SENDER_CHANNEL_CAPACITY: usize = 32;
|
||||
|
0
kinode/src/register-ui/build.sh
Normal file → Executable file
0
kinode/src/register-ui/build.sh
Normal file → Executable file
@ -6,7 +6,7 @@ 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'
|
||||
import ResetName from './pages/ResetName'
|
||||
import KinodeHome from "./pages/KinodeHome"
|
||||
import ImportKeyfile from "./pages/ImportKeyfile";
|
||||
import { UnencryptedIdentity } from "./lib/types";
|
||||
@ -112,7 +112,7 @@ function App() {
|
||||
<Route path="/commit-os-name" element={<CommitDotOsName {...props} />} />
|
||||
<Route path="/mint-os-name" element={<MintDotOsName {...props} />} />
|
||||
<Route path="/set-password" element={<SetPassword {...props} />} />
|
||||
<Route path="/reset" element={<ResetDotOsName {...props} />} />
|
||||
<Route path="/reset" element={<ResetName {...props} />} />
|
||||
<Route path="/import-keyfile" element={<ImportKeyfile {...props} />} />
|
||||
<Route path="/login" element={<Login {...props} />} />
|
||||
<Route path="/custom-register" element={<MintCustom {...props} />} />
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import isValidDomain from "is-valid-domain";
|
||||
import { toAscii } from "idna-uts46-hx";
|
||||
import { usePublicClient } from 'wagmi'
|
||||
|
||||
@ -13,9 +12,10 @@ export const NAME_INVALID_PUNY = "Unsupported punycode character";
|
||||
export const NAME_NOT_OWNER = "Name already exists and does not belong to this wallet";
|
||||
export const NAME_NOT_REGISTERED = "Name is not registered";
|
||||
|
||||
type ClaimOsNameProps = {
|
||||
type EnterNameProps = {
|
||||
address?: `0x${string}`;
|
||||
name: string;
|
||||
fixedTlz?: string;
|
||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||
nameValidities: string[];
|
||||
setNameValidities: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
@ -28,12 +28,13 @@ function EnterKnsName({
|
||||
address,
|
||||
name,
|
||||
setName,
|
||||
fixedTlz,
|
||||
nameValidities,
|
||||
setNameValidities,
|
||||
triggerNameCheck,
|
||||
setTba,
|
||||
isReset = false,
|
||||
}: ClaimOsNameProps) {
|
||||
}: EnterNameProps) {
|
||||
const client = usePublicClient();
|
||||
const debouncer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
@ -43,90 +44,85 @@ function EnterKnsName({
|
||||
if (debouncer.current) clearTimeout(debouncer.current);
|
||||
|
||||
debouncer.current = setTimeout(async () => {
|
||||
let index: number;
|
||||
let validities: string[] = [];
|
||||
setIsPunyfied('');
|
||||
|
||||
const len = [...name].length;
|
||||
index = validities.indexOf(NAME_LENGTH);
|
||||
if (len < 9 && len !== 0) {
|
||||
if (index === -1) validities.push(NAME_LENGTH);
|
||||
} else if (index !== -1) validities.splice(index, 1);
|
||||
|
||||
let normalized = ''
|
||||
index = validities.indexOf(NAME_INVALID_PUNY);
|
||||
try {
|
||||
normalized = toAscii(name + ".os");
|
||||
if (index !== -1) validities.splice(index, 1);
|
||||
} catch (e) {
|
||||
if (index === -1) validities.push(NAME_INVALID_PUNY);
|
||||
if (/[A-Z]/.test(name)) {
|
||||
validities.push(NAME_URL);
|
||||
setNameValidities(validities);
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized !== (name + ".os")) setIsPunyfied(normalized);
|
||||
let normalized = ''
|
||||
try {
|
||||
normalized = toAscii(fixedTlz ? name + fixedTlz : name);
|
||||
} catch (e) {
|
||||
validities.push(NAME_INVALID_PUNY);
|
||||
}
|
||||
|
||||
// only check if name is valid punycode
|
||||
if (normalized && normalized !== '.os') {
|
||||
index = validities.indexOf(NAME_URL);
|
||||
if (name !== "" && !isValidDomain(normalized)) {
|
||||
if (index === -1) validities.push(NAME_URL);
|
||||
} else if (index !== -1) validities.splice(index, 1);
|
||||
// length check, only for .os
|
||||
if (fixedTlz === '.os') {
|
||||
const len = [...normalized].length - 3;
|
||||
if (len < 9 && len !== 0) {
|
||||
validities.push(NAME_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
index = validities.indexOf(NAME_CLAIMED);
|
||||
if (normalized !== (fixedTlz ? name + fixedTlz : name)) {
|
||||
setIsPunyfied(normalized);
|
||||
}
|
||||
|
||||
// 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: KIMAP,
|
||||
abi: kimapAbi,
|
||||
functionName: "get",
|
||||
args: [namehash]
|
||||
})
|
||||
// only check ownership if name is otherwise valid
|
||||
if (validities.length === 0 && normalized.length > 2) {
|
||||
try {
|
||||
const namehash = kinohash(normalized)
|
||||
|
||||
const tba = data?.[0];
|
||||
if (tba !== undefined) {
|
||||
setTba ? (setTba(tba)) : null;
|
||||
} else {
|
||||
validities.push(NAME_NOT_REGISTERED);
|
||||
}
|
||||
const data = await client?.readContract({
|
||||
address: KIMAP,
|
||||
abi: kimapAbi,
|
||||
functionName: "get",
|
||||
args: [namehash]
|
||||
})
|
||||
|
||||
const owner = data?.[1];
|
||||
const owner_is_zero = owner === "0x0000000000000000000000000000000000000000";
|
||||
|
||||
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);
|
||||
const tba = data?.[0];
|
||||
if (tba !== undefined) {
|
||||
setTba ? (setTba(tba)) : null;
|
||||
} else if (isReset) {
|
||||
validities.push(NAME_NOT_REGISTERED);
|
||||
}
|
||||
|
||||
const owner = data?.[1];
|
||||
const owner_is_zero = owner === "0x0000000000000000000000000000000000000000";
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
setNameValidities(validities);
|
||||
}, 500);
|
||||
}, [name, triggerNameCheck, isReset]);
|
||||
|
||||
const noDotsOrSpaces = (e: any) =>
|
||||
e.target.value.indexOf(".") === -1 && e.target.value.indexOf(" ") === -1 && setName(e.target.value);
|
||||
const noSpaces = (e: any) =>
|
||||
e.target.value.indexOf(" ") === -1 && setName(e.target.value);
|
||||
|
||||
return (
|
||||
<div className="enter-kns-name">
|
||||
<div className="input-wrapper">
|
||||
<input
|
||||
value={name}
|
||||
onChange={noDotsOrSpaces}
|
||||
onChange={noSpaces}
|
||||
type="text"
|
||||
required
|
||||
name="dot-os-name"
|
||||
name="kns-name"
|
||||
placeholder="mynode123"
|
||||
className="kns-input"
|
||||
/>
|
||||
<span className="kns-suffix">.os</span>
|
||||
{fixedTlz && <span className="kns-suffix">{fixedTlz}</span>}
|
||||
</div>
|
||||
{nameValidities.map((x, i) => (
|
||||
<p key={i} className="error-message">{x}</p>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, FormEvent, useCallback } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { toAscii } from "idna-uts46-hx";
|
||||
import EnterKnsName from "../components/EnterKnsName";
|
||||
import Loader from "../components/Loader";
|
||||
import { PageProps } from "../lib/types";
|
||||
@ -51,7 +52,7 @@ function CommitDotOsName({
|
||||
|
||||
useEffect(() => setTriggerNameCheck(!triggerNameCheck), [address])
|
||||
|
||||
const enterOsNameProps = { address, name, setName, nameValidities, setNameValidities, triggerNameCheck }
|
||||
const enterOsNameProps = { address, name, setName, fixedTlz: ".os", nameValidities, setNameValidities, triggerNameCheck }
|
||||
|
||||
useEffect(() => {
|
||||
if (!address) {
|
||||
@ -66,6 +67,7 @@ function CommitDotOsName({
|
||||
openConnectModal?.()
|
||||
return
|
||||
}
|
||||
setName(toAscii(name));
|
||||
console.log("committing to .os name: ", name)
|
||||
const commitSecret = keccak256(stringToHex(name))
|
||||
const commit = keccak256(
|
||||
@ -134,4 +136,4 @@ function CommitDotOsName({
|
||||
);
|
||||
}
|
||||
|
||||
export default CommitDotOsName;
|
||||
export default CommitDotOsName;
|
||||
|
@ -36,7 +36,7 @@ function ResetKnsName({
|
||||
const { data: hash, writeContract, isPending, isError, error } = useWriteContract({
|
||||
mutation: {
|
||||
onSuccess: (data) => {
|
||||
addRecentTransaction({ hash: data, description: `Reset KNS ID: ${name}.os` });
|
||||
addRecentTransaction({ hash: data, description: `Reset KNS ID: ${name}` });
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -46,12 +46,11 @@ function ResetKnsName({
|
||||
});
|
||||
const addRecentTransaction = useAddRecentTransaction();
|
||||
|
||||
const [name, setName] = useState<string>(knsName.slice(0, -3));
|
||||
const [name, setName] = useState<string>(knsName);
|
||||
const [nameValidities, setNameValidities] = useState<string[]>([])
|
||||
const [tba, setTba] = useState<string>("");
|
||||
const [triggerNameCheck, setTriggerNameCheck] = useState<boolean>(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Reset";
|
||||
}, []);
|
||||
@ -75,7 +74,7 @@ function ResetKnsName({
|
||||
return;
|
||||
}
|
||||
|
||||
setKnsName(name + ".os");
|
||||
setKnsName(name);
|
||||
|
||||
try {
|
||||
const data = await generateNetworkingKeys({
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lib"
|
||||
authors = ["KinodeDAO"]
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
description = "A general-purpose sovereign cloud computing platform"
|
||||
homepage = "https://kinode.org"
|
||||
|
@ -1190,7 +1190,20 @@ impl KernelMessage {
|
||||
}
|
||||
|
||||
pub async fn send(self, sender: &MessageSender) {
|
||||
sender.send(self).await.expect("kernel message sender died");
|
||||
let Err(e) = sender.try_send(self) else {
|
||||
// not Err -> send successful; done here
|
||||
return;
|
||||
};
|
||||
// its an Err: handle
|
||||
match e {
|
||||
tokio::sync::mpsc::error::TrySendError::Closed(_) => {
|
||||
panic!("kernel message sender: receiver closed");
|
||||
}
|
||||
tokio::sync::mpsc::error::TrySendError::Full(_) => {
|
||||
// TODO: implement backpressure
|
||||
panic!("kernel overloaded with messages: TODO: implement backpressure");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,14 +53,14 @@ pub struct EthSubError {
|
||||
///
|
||||
/// In the case of an [`EthAction::SubscribeLogs`] request, the response will indicate if
|
||||
/// the subscription was successfully created or not.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum EthResponse {
|
||||
Ok,
|
||||
Response { value: serde_json::Value },
|
||||
Err(EthError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum EthError {
|
||||
/// RPC provider returned an error
|
||||
RpcError(ErrorPayload),
|
||||
|
Loading…
Reference in New Issue
Block a user