mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 08:32:23 +03:00
Merge branch 'main' into main
This commit is contained in:
commit
b74b90694f
22
.github/workflows/build_release.yml
vendored
Normal file
22
.github/workflows/build_release.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
name: rust tagged release in main CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v[0-9].[0-9]+.[0-9]+']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build and deploy kinode
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_PROD_API_HOST }}
|
||||||
|
username: ${{ secrets.SSH_PROD_USER }}
|
||||||
|
key: ${{ secrets.SSH_PROD_API_ED25519KEY }}
|
||||||
|
port: ${{ secrets.SSH_PROD_PORT }}
|
||||||
|
command_timeout: 60m
|
||||||
|
script: |
|
||||||
|
curl -X PUT http://localhost:8000/monitor/build-kinode
|
7
.github/workflows/release_candidate.yml
vendored
7
.github/workflows/release_candidate.yml
vendored
@ -13,11 +13,10 @@ jobs:
|
|||||||
- name: build and deploy kinode
|
- name: build and deploy kinode
|
||||||
uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.SSH_HOST }}
|
host: ${{ secrets.SSH_API_HOST }}
|
||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_ED25519KEY }}
|
key: ${{ secrets.SSH_API_ED25519KEY }}
|
||||||
port: ${{ secrets.SSH_PORT }}
|
port: ${{ secrets.SSH_PORT }}
|
||||||
command_timeout: 60m
|
command_timeout: 60m
|
||||||
script: |
|
script: |
|
||||||
cd ~
|
curl -X PUT http://localhost:8000/monitor/build-kinode
|
||||||
./build-kinode.sh
|
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -15,3 +15,9 @@ wit/
|
|||||||
.env
|
.env
|
||||||
kinode/src/bootstrapped_processes.rs
|
kinode/src/bootstrapped_processes.rs
|
||||||
kinode/packages/**/wasi_snapshot_preview1.wasm
|
kinode/packages/**/wasi_snapshot_preview1.wasm
|
||||||
|
|
||||||
|
kinode/packages/app_store/pkg/ui/*
|
||||||
|
kinode/packages/homepage/pkg/ui/*
|
||||||
|
kinode/src/register-ui/build/
|
||||||
|
kinode/src/register-ui/dist/
|
||||||
|
kinode/packages/docs/pkg/ui
|
||||||
|
2104
Cargo.lock
generated
2104
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kinode_lib"
|
name = "kinode_lib"
|
||||||
authors = ["KinodeDAO"]
|
authors = ["KinodeDAO"]
|
||||||
version = "0.8.0"
|
version = "0.9.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A general-purpose sovereign cloud computing platform"
|
description = "A general-purpose sovereign cloud computing platform"
|
||||||
homepage = "https://kinode.org"
|
homepage = "https://kinode.org"
|
||||||
@ -15,15 +15,17 @@ lib = { path = "lib" }
|
|||||||
members = [
|
members = [
|
||||||
"lib", "kinode",
|
"lib", "kinode",
|
||||||
"kinode/packages/app_store/app_store", "kinode/packages/app_store/ft_worker",
|
"kinode/packages/app_store/app_store", "kinode/packages/app_store/ft_worker",
|
||||||
"kinode/packages/app_store/download", "kinode/packages/app_store/install", "kinode/packages/app_store/uninstall",
|
"kinode/packages/app_store/download", "kinode/packages/app_store/install", "kinode/packages/app_store/uninstall", "kinode/packages/app_store/downloads", "kinode/packages/app_store/chain",
|
||||||
"kinode/packages/chess/chess",
|
"kinode/packages/chess/chess",
|
||||||
"kinode/packages/homepage/homepage",
|
"kinode/packages/homepage/homepage",
|
||||||
"kinode/packages/kino_updates/widget",
|
"kinode/packages/kino_updates/blog", "kinode/packages/kino_updates/globe",
|
||||||
"kinode/packages/kns_indexer/kns_indexer", "kinode/packages/kns_indexer/get_block", "kinode/packages/kns_indexer/state",
|
"kinode/packages/kns_indexer/kns_indexer", "kinode/packages/kns_indexer/get_block", "kinode/packages/kns_indexer/state",
|
||||||
"kinode/packages/settings/settings",
|
"kinode/packages/settings/settings",
|
||||||
"kinode/packages/terminal/terminal",
|
"kinode/packages/terminal/terminal",
|
||||||
"kinode/packages/terminal/alias", "kinode/packages/terminal/cat", "kinode/packages/terminal/echo", "kinode/packages/terminal/hi", "kinode/packages/terminal/kfetch", "kinode/packages/terminal/kill", "kinode/packages/terminal/m", "kinode/packages/terminal/top",
|
"kinode/packages/terminal/alias", "kinode/packages/terminal/cat", "kinode/packages/terminal/echo",
|
||||||
"kinode/packages/terminal/namehash_to_name", "kinode/packages/terminal/net_diagnostics", "kinode/packages/terminal/peer", "kinode/packages/terminal/peers",
|
"kinode/packages/terminal/help", "kinode/packages/terminal/hi", "kinode/packages/terminal/kfetch",
|
||||||
|
"kinode/packages/terminal/kill", "kinode/packages/terminal/m", "kinode/packages/terminal/top",
|
||||||
|
"kinode/packages/terminal/net_diagnostics", "kinode/packages/terminal/peer", "kinode/packages/terminal/peers",
|
||||||
"kinode/packages/tester/tester",
|
"kinode/packages/tester/tester",
|
||||||
]
|
]
|
||||||
default-members = ["lib"]
|
default-members = ["lib"]
|
||||||
|
2
LICENSE
2
LICENSE
@ -175,7 +175,7 @@
|
|||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
Copyright 2024 Unzentrum DAO
|
Copyright 2024 Sybil Technologies AG
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
43
README.md
43
README.md
@ -37,7 +37,11 @@ rustup target add wasm32-wasi
|
|||||||
rustup target add wasm32-wasi --toolchain nightly
|
rustup target add wasm32-wasi --toolchain nightly
|
||||||
cargo install cargo-wasi
|
cargo install cargo-wasi
|
||||||
|
|
||||||
# Build the runtime, along with a number of "distro" WASM modules.
|
# Install NPM so we can build frontends for "distro" packages.
|
||||||
|
# https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||||
|
# If you want to skip this step, run cargo build with the environment variable SKIP_BUILD_FRONTEND=true
|
||||||
|
|
||||||
|
# Build the runtime, along with a number of "distro" Wasm modules.
|
||||||
# The compiled binary will be at `kinode/target/debug/kinode`
|
# The compiled binary will be at `kinode/target/debug/kinode`
|
||||||
# OPTIONAL: --release flag (slower build; faster runtime; binary at `kinode/target/release/kinode`)
|
# OPTIONAL: --release flag (slower build; faster runtime; binary at `kinode/target/release/kinode`)
|
||||||
|
|
||||||
@ -121,22 +125,41 @@ The `sys` publisher is not a real node ID, but it's also not a special case valu
|
|||||||
- UpArrow/DownArrow or CTRL+P/CTRL+N to move up and down through command history
|
- UpArrow/DownArrow or CTRL+P/CTRL+N to move up and down through command history
|
||||||
- CTRL+R to search history, CTRL+R again to toggle through search results, CTRL+G to cancel search
|
- CTRL+R to search history, CTRL+R again to toggle through search results, CTRL+G to cancel search
|
||||||
|
|
||||||
|
### Built-in terminal scripts
|
||||||
|
|
||||||
|
The terminal package contains a number of built-in scripts.
|
||||||
|
Users may also call scripts from other packages in the terminal by entering the (full) ID of the script process followed by any arguments.
|
||||||
|
In order to call a script with shorthand, a user may apply an *alias* using the terminal `alias` script, like so:
|
||||||
|
```
|
||||||
|
alias <shorthand> <full_name>
|
||||||
|
```
|
||||||
|
Subsequent use of the shorthand will then be interpolated as the process ID.
|
||||||
|
|
||||||
|
A list of the terminal scripts included in this distro:
|
||||||
|
|
||||||
|
- `alias <shorthand> <process_id>`: create an alias for a script.
|
||||||
|
- Example: `alias get_block get_block:kns_indexer:sys`
|
||||||
|
- note: all of these listed commands are just default aliases for terminal scripts.
|
||||||
|
- `cat <vfs-file-path>`: print the contents of a file in the terminal.
|
||||||
|
- Example: `cat /terminal:sys/pkg/scripts.json`
|
||||||
|
- `echo <text>`: print text to the terminal.
|
||||||
|
- Example: `echo foo`
|
||||||
|
- `help <command>`: print the help message for a command. Leave the command blank to print the help message for all commands.
|
||||||
|
- `hi <name> <string>`: send a text message to another node's command line.
|
||||||
|
- Example: `hi mothu.kino hello world`
|
||||||
|
- `kfetch`: print system information a la neofetch. No arguments.
|
||||||
|
- `kill <process-id>`: terminate a running process. This will bypass any restart behavior–use judiciously.
|
||||||
|
- Example: `kill chess:chess:sys`
|
||||||
- `m <address> '<json>'`: send an inter-process message. <address> is formatted as <node>@<process_id>. <process_id> is formatted as <process_name>:<package_name>:<publisher_node>. JSON containing spaces must be wrapped in single-quotes (`''`).
|
- `m <address> '<json>'`: send an inter-process message. <address> is formatted as <node>@<process_id>. <process_id> is formatted as <process_name>:<package_name>:<publisher_node>. JSON containing spaces must be wrapped in single-quotes (`''`).
|
||||||
- Example: `m our@eth:distro:sys "SetPublic" -a 5`
|
- Example: `m our@eth:distro:sys "SetPublic" -a 5`
|
||||||
- the '-a' flag is used to expect a response with a given timeout
|
- the '-a' flag is used to expect a response with a given timeout
|
||||||
- `our` will always be interpolated by the system as your node's name
|
- `our` will always be interpolated by the system as your node's name
|
||||||
- `hi <name> <string>`: send a text message to another node's command line.
|
- `net_diagnostics`: print some useful networking diagnostic data.
|
||||||
- Example: `hi ben.os hello world`
|
- `peer <name>`: print the peer's PKI info, if it exists.
|
||||||
|
- `peers`: print the peers the node currently hold connections with.
|
||||||
- `top <process_id>`: display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes.
|
- `top <process_id>`: display kernel debugging info about a process. Leave the process ID blank to display info about all processes and get the total number of running processes.
|
||||||
- Example: `top net:distro:sys`
|
- Example: `top net:distro:sys`
|
||||||
- Example: `top`
|
- Example: `top`
|
||||||
- `cat <vfs-file-path>`: print the contents of a file in the terminal
|
|
||||||
- Example: `cat /terminal:sys/pkg/scripts.json`
|
|
||||||
- `echo <text>`: print `text` to the terminal
|
|
||||||
- Example: `echo foo`
|
|
||||||
- `net_diagnostics`: print some useful networking diagnostic data
|
|
||||||
- `peers`: print the peers the node currently hold connections with
|
|
||||||
- `peer <name>`: print the peer's PKI info, if it exists
|
|
||||||
|
|
||||||
## Running as a Docker container
|
## Running as a Docker container
|
||||||
|
|
||||||
|
182
css/kinode.css
Normal file
182
css/kinode.css
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/* CSS Reset and Base Styles */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
picture,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-family: var(--font-family-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variables */
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--orange: #FF4F00;
|
||||||
|
--dark-orange: #cc4100;
|
||||||
|
--blue: #2B88D9;
|
||||||
|
--off-white: #fdfdfd;
|
||||||
|
--white: #ffffff;
|
||||||
|
--off-black: #0C090A;
|
||||||
|
--black: #000000;
|
||||||
|
--tan: #fdf6e3;
|
||||||
|
--ansi-red: #dc322f;
|
||||||
|
--maroon: #4f0000;
|
||||||
|
--gray: #657b83;
|
||||||
|
--tasteful-dark: #1f1f1f;
|
||||||
|
|
||||||
|
--font-family-main: 'Kode Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
label,
|
||||||
|
li,
|
||||||
|
span {
|
||||||
|
font-family: var(--font-family-main);
|
||||||
|
color: light-dark(var(--off-black), var(--off-white));
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
li {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-family: var(--font-family-main);
|
||||||
|
color: light-dark(var(--blue), var(--orange));
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: light-dark(var(--orange), var(--dark-orange));
|
||||||
|
text-decoration: underline wavy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
body {
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
background-color: light-dark(var(--tan), var(--tasteful-dark));
|
||||||
|
background-image: radial-gradient(circle at -1% -47%, #4700002b 7%, transparent 58.05%), radial-gradient(circle at 81% 210%, #d6430550 17%, transparent 77.05%);
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
section {
|
||||||
|
background-color: light-dark(var(--white), var(--maroon));
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
section:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid var(--orange);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--dark-orange);
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: var(--orange);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--dark-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background-color: light-dark(var(--off-white), var(--off-black));
|
||||||
|
color: var(--orange);
|
||||||
|
border: 2px solid var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:hover {
|
||||||
|
background-color: var(--orange);
|
||||||
|
color: white;
|
||||||
|
}
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 644 B |
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kinode"
|
name = "kinode"
|
||||||
authors = ["KinodeDAO"]
|
authors = ["KinodeDAO"]
|
||||||
version = "0.8.0"
|
version = "0.9.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A general-purpose sovereign cloud computing platform"
|
description = "A general-purpose sovereign cloud computing platform"
|
||||||
homepage = "https://kinode.org"
|
homepage = "https://kinode.org"
|
||||||
@ -14,9 +14,9 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.71"
|
anyhow = "1.0.71"
|
||||||
kit = { git = "https://github.com/kinode-dao/kit", rev = "d319c5b" }
|
flate2 = "1.0"
|
||||||
rayon = "1.8.1"
|
kit = { git = "https://github.com/kinode-dao/kit", tag = "v0.6.10" }
|
||||||
sha2 = "0.10"
|
tar = "0.4"
|
||||||
tokio = "1.28"
|
tokio = "1.28"
|
||||||
walkdir = "2.4"
|
walkdir = "2.4"
|
||||||
zip = "0.6"
|
zip = "0.6"
|
||||||
@ -26,7 +26,7 @@ simulation-mode = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes-gcm = "0.10.3"
|
aes-gcm = "0.10.3"
|
||||||
alloy = { git = "https://github.com/alloy-rs/alloy", rev = "05f8162", features = [
|
alloy = { git = "https://github.com/kinode-dao/alloy.git", rev = "e672f3e", features = [
|
||||||
"consensus",
|
"consensus",
|
||||||
"contract",
|
"contract",
|
||||||
"json-rpc",
|
"json-rpc",
|
||||||
@ -34,46 +34,35 @@ alloy = { git = "https://github.com/alloy-rs/alloy", rev = "05f8162", features =
|
|||||||
"provider-ws",
|
"provider-ws",
|
||||||
"providers",
|
"providers",
|
||||||
"pubsub",
|
"pubsub",
|
||||||
"rpc-client-ws",
|
"rpc",
|
||||||
"rpc-client",
|
"rpc-client",
|
||||||
"rpc-types-eth",
|
"rpc-client-ws",
|
||||||
"rpc-types",
|
"rpc-types",
|
||||||
"signer-wallet",
|
"rpc-types-eth",
|
||||||
"signers",
|
"signers",
|
||||||
|
"signer-local",
|
||||||
] }
|
] }
|
||||||
|
alloy-primitives = "0.7.6"
|
||||||
alloy-primitives = "0.7.5"
|
alloy-sol-macro = "0.7.6"
|
||||||
alloy-sol-macro = "0.7.5"
|
alloy-sol-types = "0.7.6"
|
||||||
alloy-sol-types = "0.7.5"
|
|
||||||
anyhow = "1.0.71"
|
anyhow = "1.0.71"
|
||||||
async-trait = "0.1.71"
|
async-trait = "0.1.71"
|
||||||
base64 = "0.22.0"
|
base64 = "0.22.0"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
blake3 = "1.4.1"
|
|
||||||
bytes = "1.4.0"
|
|
||||||
chacha20poly1305 = "0.10.1"
|
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
crossterm = { version = "0.27.0", features = ["event-stream", "bracketed-paste"] }
|
crossterm = { version = "0.27.0", features = ["event-stream", "bracketed-paste"] }
|
||||||
curve25519-dalek = "^4.1.2"
|
|
||||||
dashmap = "5.5.3"
|
dashmap = "5.5.3"
|
||||||
digest = "0.10"
|
|
||||||
elliptic-curve = { version = "0.13.8", features = ["ecdh"] }
|
|
||||||
flate2 = "1.0"
|
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
generic-array = "1.0.0"
|
generic-array = "0.14.7"
|
||||||
getrandom = "0.2.10"
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hkdf = "0.12.3"
|
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
http = "1.1.0"
|
http = "1.1.0"
|
||||||
jwt = "0.16"
|
jwt = "0.16"
|
||||||
lib = { path = "../lib" }
|
lib = { path = "../lib" }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4.20"
|
|
||||||
nohash-hasher = "0.2.0"
|
nohash-hasher = "0.2.0"
|
||||||
num-traits = "0.2"
|
open = "5.1.4"
|
||||||
open = "5.0.0"
|
|
||||||
public-ip = "0.2.2"
|
public-ip = "0.2.2"
|
||||||
rand = "0.8.4"
|
rand = "0.8.4"
|
||||||
reqwest = "0.12.4"
|
reqwest = "0.12.4"
|
||||||
@ -84,8 +73,7 @@ route-recognizer = "0.3.1"
|
|||||||
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
sha2 = "0.10.8"
|
||||||
sha2 = "0.10"
|
|
||||||
sha3 = "0.10.8"
|
sha3 = "0.10.8"
|
||||||
# snow = { version = "0.9.5", features = ["ring-resolver"] }
|
# snow = { version = "0.9.5", features = ["ring-resolver"] }
|
||||||
# unfortunately need to use forked version for async use and in-place encryption
|
# unfortunately need to use forked version for async use and in-place encryption
|
||||||
@ -96,7 +84,6 @@ thiserror = "1.0"
|
|||||||
tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "signal", "sync"] }
|
tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "signal", "sync"] }
|
||||||
tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] }
|
||||||
url = "2.4.1"
|
url = "2.4.1"
|
||||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
|
||||||
warp = "0.3.5"
|
warp = "0.3.5"
|
||||||
wasi-common = "19.0.1"
|
wasi-common = "19.0.1"
|
||||||
wasmtime = "19.0.1"
|
wasmtime = "19.0.1"
|
||||||
|
175
kinode/build.rs
175
kinode/build.rs
@ -1,12 +1,20 @@
|
|||||||
use rayon::prelude::*;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{Cursor, Read, Write},
|
io::{BufReader, Cursor, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use tar::Archive;
|
||||||
use zip::write::FileOptions;
|
use zip::write::FileOptions;
|
||||||
|
|
||||||
|
macro_rules! p {
|
||||||
|
($($tokens: tt)*) => {
|
||||||
|
println!("cargo:warning={}", format!($($tokens)*))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get cargo features to compile packages with
|
||||||
fn get_features() -> String {
|
fn get_features() -> String {
|
||||||
let mut features = "".to_string();
|
let mut features = "".to_string();
|
||||||
for (key, _) in std::env::vars() {
|
for (key, _) in std::env::vars() {
|
||||||
@ -16,40 +24,88 @@ fn get_features() -> String {
|
|||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.replace("_", "-");
|
.replace("_", "-");
|
||||||
features.push_str(&feature);
|
features.push_str(&feature);
|
||||||
//println!("cargo:rustc-cfg=feature=\"{}\"", feature);
|
|
||||||
//println!("- {}", feature);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
features
|
features
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_reruns(dir: &Path, rerun_files: &HashSet<String>) {
|
/// print `cargo:rerun-if-changed=PATH` for each path of interest
|
||||||
if rerun_files.contains(dir.to_str().unwrap()) {
|
fn output_reruns(dir: &Path) {
|
||||||
// Output for all files in the directory if the directory itself is specified in rerun_files
|
|
||||||
if let Ok(entries) = fs::read_dir(dir) {
|
|
||||||
for entry in entries.filter_map(|e| e.ok()) {
|
|
||||||
let path = entry.path();
|
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check files individually
|
// Check files individually
|
||||||
if let Ok(entries) = fs::read_dir(dir) {
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
for entry in entries.filter_map(|e| e.ok()) {
|
for entry in entries.filter_map(|e| e.ok()) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
// If the entry is a directory, recursively walk it
|
if let Some(dirname) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
output_reruns(&path, rerun_files);
|
if dirname == "ui" || dirname == "target" {
|
||||||
} else if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
// do not prompt a rerun if only UI/build files have changed
|
||||||
// Check if the current file is in our list of interesting files
|
continue;
|
||||||
if rerun_files.contains(filename) {
|
}
|
||||||
// If so, print a `cargo:rerun-if-changed=PATH` line for it
|
// If the entry is a directory not in rerun_files, recursively walk it
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
output_reruns(&path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if filename.ends_with(".zip") || filename.ends_with(".wasm") {
|
||||||
|
// do not prompt a rerun for compiled outputs
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// any other changed file within a package subdir prompts a rerun
|
||||||
|
println!("cargo::rerun-if-changed={}", path.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn untar_gz_file(path: &Path, dest: &Path) -> std::io::Result<()> {
|
||||||
|
// Open the .tar.gz file
|
||||||
|
let tar_gz = File::open(path)?;
|
||||||
|
let tar_gz_reader = BufReader::new(tar_gz);
|
||||||
|
|
||||||
|
// Decode the gzip layer
|
||||||
|
let tar = GzDecoder::new(tar_gz_reader);
|
||||||
|
|
||||||
|
// Create a new archive from the tar file
|
||||||
|
let mut archive = Archive::new(tar);
|
||||||
|
|
||||||
|
// Unpack the archive into the specified destination directory
|
||||||
|
archive.unpack(dest)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch .tar.gz of kinode book for docs app
|
||||||
|
fn get_kinode_book(packages_dir: &Path) -> anyhow::Result<()> {
|
||||||
|
p!("fetching kinode book .tar.gz");
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
rt.block_on(async {
|
||||||
|
let releases = kit::boot_fake_node::fetch_releases("kinode-dao", "kinode-book")
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
|
||||||
|
if releases.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("couldn't retrieve kinode-book releases"));
|
||||||
|
}
|
||||||
|
let release = &releases[0];
|
||||||
|
if release.assets.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"most recent kinode-book release has no assets"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let release_url = format!(
|
||||||
|
"https://github.com/kinode-dao/kinode-book/releases/download/{}/{}",
|
||||||
|
release.tag_name, release.assets[0].name,
|
||||||
|
);
|
||||||
|
let book_dir = packages_dir.join("docs").join("pkg").join("ui");
|
||||||
|
fs::create_dir_all(&book_dir)?;
|
||||||
|
let book_tar_path = book_dir.join("book.tar.gz");
|
||||||
|
kit::build::download_file(&release_url, &book_tar_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
|
||||||
|
untar_gz_file(&book_tar_path, &book_dir)?;
|
||||||
|
fs::remove_file(book_tar_path)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_and_zip_package(
|
fn build_and_zip_package(
|
||||||
@ -59,13 +115,27 @@ fn build_and_zip_package(
|
|||||||
) -> anyhow::Result<(String, String, Vec<u8>)> {
|
) -> anyhow::Result<(String, String, Vec<u8>)> {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
kit::build::execute(&entry_path, true, false, true, features, None, None) // TODO
|
kit::build::execute(
|
||||||
|
&entry_path,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
features,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
|
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
|
||||||
|
|
||||||
let mut writer = Cursor::new(Vec::new());
|
let mut writer = Cursor::new(Vec::new());
|
||||||
let options = FileOptions::default()
|
let options = FileOptions::default()
|
||||||
.compression_method(zip::CompressionMethod::Stored)
|
.compression_method(zip::CompressionMethod::Deflated)
|
||||||
.unix_permissions(0o755);
|
.unix_permissions(0o755);
|
||||||
{
|
{
|
||||||
let mut zip = zip::ZipWriter::new(&mut writer);
|
let mut zip = zip::ZipWriter::new(&mut writer);
|
||||||
@ -96,7 +166,7 @@ fn build_and_zip_package(
|
|||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
if std::env::var("SKIP_BUILD_SCRIPT").is_ok() {
|
if std::env::var("SKIP_BUILD_SCRIPT").is_ok() {
|
||||||
println!("Skipping build script");
|
p!("skipping build script");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,30 +174,49 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let parent_dir = pwd.parent().unwrap();
|
let parent_dir = pwd.parent().unwrap();
|
||||||
let packages_dir = pwd.join("packages");
|
let packages_dir = pwd.join("packages");
|
||||||
|
|
||||||
let entries: Vec<_> = fs::read_dir(packages_dir)?
|
if std::env::var("SKIP_BUILD_FRONTEND").is_ok() {
|
||||||
.map(|entry| entry.unwrap().path())
|
p!("skipping frontend builds");
|
||||||
.collect();
|
} else {
|
||||||
|
// build core frontends
|
||||||
|
let core_frontends = vec![
|
||||||
|
"src/register-ui",
|
||||||
|
"packages/app_store/ui",
|
||||||
|
"packages/homepage/ui",
|
||||||
|
// chess when brought in
|
||||||
|
];
|
||||||
|
|
||||||
let rerun_files: HashSet<String> = HashSet::from([
|
// for each frontend, execute build.sh
|
||||||
"Cargo.lock".to_string(),
|
for frontend in core_frontends {
|
||||||
"Cargo.toml".to_string(),
|
let status = std::process::Command::new("sh")
|
||||||
"src/".to_string(),
|
.current_dir(pwd.join(frontend))
|
||||||
]);
|
.arg("./build.sh")
|
||||||
output_reruns(&parent_dir, &rerun_files);
|
.status()?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to build frontend: {}", frontend));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_kinode_book(&packages_dir)?;
|
||||||
|
|
||||||
|
output_reruns(&packages_dir);
|
||||||
|
|
||||||
let features = get_features();
|
let features = get_features();
|
||||||
|
|
||||||
let results: Vec<anyhow::Result<(String, String, Vec<u8>)>> = entries
|
let results: Vec<anyhow::Result<(String, String, Vec<u8>)>> = fs::read_dir(&packages_dir)?
|
||||||
.par_iter()
|
.filter_map(|entry| {
|
||||||
.filter_map(|entry_path| {
|
let entry_path = match entry {
|
||||||
let parent_pkg_path = entry_path.join("pkg");
|
Ok(e) => e.path(),
|
||||||
if !parent_pkg_path.exists() {
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
let child_pkg_path = entry_path.join("pkg");
|
||||||
|
if !child_pkg_path.exists() {
|
||||||
// don't run on, e.g., `.DS_Store`
|
// don't run on, e.g., `.DS_Store`
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(build_and_zip_package(
|
Some(build_and_zip_package(
|
||||||
entry_path.clone(),
|
entry_path.clone(),
|
||||||
parent_pkg_path.to_str().unwrap(),
|
child_pkg_path.to_str().unwrap(),
|
||||||
&features,
|
&features,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
@ -160,7 +249,11 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeln!(bootstrapped_processes, "];")?;
|
writeln!(bootstrapped_processes, "];")?;
|
||||||
let bootstrapped_processes_path = pwd.join("src/bootstrapped_processes.rs");
|
let target_dir = pwd.join("../target");
|
||||||
|
if !target_dir.exists() {
|
||||||
|
fs::create_dir_all(&target_dir)?;
|
||||||
|
}
|
||||||
|
let bootstrapped_processes_path = target_dir.join("bootstrapped_processes.rs");
|
||||||
fs::write(&bootstrapped_processes_path, bootstrapped_processes)?;
|
fs::write(&bootstrapped_processes_path, bootstrapped_processes)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,9 +1,157 @@
|
|||||||
interface main {
|
interface downloads {
|
||||||
//
|
//
|
||||||
// app store API as presented by main:app_store:sys-v0
|
// download API as presented by download:app_store:sys-v0
|
||||||
//
|
//
|
||||||
|
|
||||||
use standard.{package-id};
|
use standard.{package-id};
|
||||||
|
use chain.{onchain-metadata};
|
||||||
|
|
||||||
|
variant download-requests {
|
||||||
|
// remote only
|
||||||
|
remote-download(remote-download-request),
|
||||||
|
chunk(chunk-request),
|
||||||
|
progress(progress-update),
|
||||||
|
size(size-update),
|
||||||
|
// local only
|
||||||
|
local-download(local-download-request),
|
||||||
|
auto-update(auto-update-request),
|
||||||
|
download-complete(download-complete-request),
|
||||||
|
get-files(option<package-id>),
|
||||||
|
remove-file(remove-file-request),
|
||||||
|
add-download(add-download-request),
|
||||||
|
start-mirroring(package-id),
|
||||||
|
stop-mirroring(package-id),
|
||||||
|
}
|
||||||
|
|
||||||
|
variant download-responses {
|
||||||
|
success,
|
||||||
|
error(download-error),
|
||||||
|
get-files(list<entry>),
|
||||||
|
}
|
||||||
|
|
||||||
|
record local-download-request {
|
||||||
|
package-id: package-id,
|
||||||
|
download-from: string,
|
||||||
|
desired-version-hash: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
record auto-update-request {
|
||||||
|
package-id: package-id,
|
||||||
|
metadata: onchain-metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
record remote-download-request {
|
||||||
|
package-id: package-id,
|
||||||
|
worker-address: string,
|
||||||
|
desired-version-hash: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
variant download-error {
|
||||||
|
no-package,
|
||||||
|
not-mirroring,
|
||||||
|
hash-mismatch(hash-mismatch),
|
||||||
|
file-not-found,
|
||||||
|
worker-spawn-failed,
|
||||||
|
http-client-error,
|
||||||
|
blob-not-found,
|
||||||
|
vfs-error,
|
||||||
|
}
|
||||||
|
|
||||||
|
record download-complete-request {
|
||||||
|
package-id: package-id,
|
||||||
|
version-hash: string,
|
||||||
|
error: option<download-error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
record hash-mismatch {
|
||||||
|
desired: string,
|
||||||
|
actual: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
record chunk-request {
|
||||||
|
package-id: package-id,
|
||||||
|
version-hash: string,
|
||||||
|
offset: u64,
|
||||||
|
length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
variant entry {
|
||||||
|
file(file-entry),
|
||||||
|
dir(dir-entry),
|
||||||
|
}
|
||||||
|
|
||||||
|
record file-entry {
|
||||||
|
name: string,
|
||||||
|
size: u64,
|
||||||
|
manifest: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
record dir-entry {
|
||||||
|
name: string,
|
||||||
|
mirroring: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
record remove-file-request {
|
||||||
|
package-id: package-id,
|
||||||
|
version-hash: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// part of new-package-request local-only flow.
|
||||||
|
record add-download-request {
|
||||||
|
package-id: package-id,
|
||||||
|
version-hash: string,
|
||||||
|
mirror: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
record progress-update {
|
||||||
|
package-id: package-id,
|
||||||
|
version-hash: string,
|
||||||
|
downloaded: u64,
|
||||||
|
total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
record size-update {
|
||||||
|
package-id: package-id,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface chain {
|
||||||
|
//
|
||||||
|
// on-chain API as presented by chain:app_store:sys-v0
|
||||||
|
//
|
||||||
|
|
||||||
|
use standard.{package-id};
|
||||||
|
|
||||||
|
variant chain-requests {
|
||||||
|
get-app(package-id),
|
||||||
|
get-apps,
|
||||||
|
get-our-apps,
|
||||||
|
start-auto-update(package-id),
|
||||||
|
stop-auto-update(package-id),
|
||||||
|
}
|
||||||
|
|
||||||
|
variant chain-responses {
|
||||||
|
get-app(option<onchain-app>),
|
||||||
|
get-apps(list<onchain-app>),
|
||||||
|
get-our-apps(list<onchain-app>),
|
||||||
|
auto-update-started,
|
||||||
|
auto-update-stopped,
|
||||||
|
error(chain-error),
|
||||||
|
}
|
||||||
|
|
||||||
|
variant chain-error {
|
||||||
|
no-package,
|
||||||
|
}
|
||||||
|
|
||||||
|
record onchain-app {
|
||||||
|
package-id: package-id,
|
||||||
|
tba: string,
|
||||||
|
metadata-uri: string,
|
||||||
|
metadata-hash: string,
|
||||||
|
metadata: option<onchain-metadata>,
|
||||||
|
auto-update: bool,
|
||||||
|
}
|
||||||
|
|
||||||
record onchain-metadata {
|
record onchain-metadata {
|
||||||
name: option<string>,
|
name: option<string>,
|
||||||
@ -25,84 +173,55 @@ interface main {
|
|||||||
wit-version: option<u32>,
|
wit-version: option<u32>,
|
||||||
dependencies: option<list<string>>,
|
dependencies: option<list<string>>,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface main {
|
||||||
|
//
|
||||||
|
// app store API as presented by main:app_store:sys-v0
|
||||||
|
//
|
||||||
|
|
||||||
|
use standard.{package-id};
|
||||||
|
use chain.{onchain-metadata, chain-error};
|
||||||
|
use downloads.{download-error};
|
||||||
|
|
||||||
variant request {
|
variant request {
|
||||||
remote(remote-request),
|
|
||||||
local(local-request),
|
local(local-request),
|
||||||
}
|
}
|
||||||
|
|
||||||
variant response {
|
variant response {
|
||||||
remote(remote-response),
|
|
||||||
local(local-response),
|
local(local-response),
|
||||||
}
|
chain-error(chain-error),
|
||||||
|
download-error(download-error),
|
||||||
variant remote-request {
|
|
||||||
download(remote-download-request),
|
|
||||||
}
|
|
||||||
|
|
||||||
record remote-download-request {
|
|
||||||
package-id: package-id,
|
|
||||||
desired-version-hash: option<string>,
|
|
||||||
}
|
|
||||||
|
|
||||||
variant remote-response {
|
|
||||||
download-approved,
|
|
||||||
download-denied(reason),
|
|
||||||
}
|
|
||||||
|
|
||||||
variant reason {
|
|
||||||
no-package,
|
|
||||||
not-mirroring,
|
|
||||||
hash-mismatch(hash-mismatch),
|
|
||||||
file-not-found,
|
|
||||||
worker-spawn-failed
|
|
||||||
}
|
|
||||||
|
|
||||||
record hash-mismatch {
|
|
||||||
requested: string,
|
|
||||||
have: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variant local-request {
|
variant local-request {
|
||||||
new-package(new-package-request),
|
new-package(new-package-request),
|
||||||
download(download-request),
|
install(install-package-request),
|
||||||
install(package-id),
|
|
||||||
uninstall(package-id),
|
uninstall(package-id),
|
||||||
start-mirroring(package-id),
|
|
||||||
stop-mirroring(package-id),
|
|
||||||
start-auto-update(package-id),
|
|
||||||
stop-auto-update(package-id),
|
|
||||||
rebuild-index,
|
|
||||||
apis,
|
apis,
|
||||||
get-api(package-id),
|
get-api(package-id),
|
||||||
}
|
}
|
||||||
|
|
||||||
record new-package-request {
|
|
||||||
package-id: package-id,
|
|
||||||
metadata: onchain-metadata,
|
|
||||||
mirror: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
record download-request {
|
|
||||||
package-id: package-id,
|
|
||||||
download-from: string,
|
|
||||||
mirror: bool,
|
|
||||||
auto-update: bool,
|
|
||||||
desired-version-hash: option<string>,
|
|
||||||
}
|
|
||||||
|
|
||||||
variant local-response {
|
variant local-response {
|
||||||
new-package-response(new-package-response),
|
new-package-response(new-package-response),
|
||||||
download-response(download-response),
|
|
||||||
install-response(install-response),
|
install-response(install-response),
|
||||||
uninstall-response(uninstall-response),
|
uninstall-response(uninstall-response),
|
||||||
mirror-response(mirror-response),
|
|
||||||
auto-update-response(auto-update-response),
|
|
||||||
rebuild-index-response(rebuild-index-response),
|
|
||||||
apis-response(apis-response),
|
apis-response(apis-response),
|
||||||
get-api-response(get-api-response),
|
get-api-response(get-api-response),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
record new-package-request {
|
||||||
|
package-id: package-id,
|
||||||
|
mirror: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
record install-package-request {
|
||||||
|
package-id: package-id,
|
||||||
|
metadata: option<onchain-metadata>, // if None == local sideload package.
|
||||||
|
version-hash: string,
|
||||||
|
}
|
||||||
|
|
||||||
enum new-package-response {
|
enum new-package-response {
|
||||||
success,
|
success,
|
||||||
no-blob,
|
no-blob,
|
||||||
@ -110,37 +229,14 @@ interface main {
|
|||||||
already-exists,
|
already-exists,
|
||||||
}
|
}
|
||||||
|
|
||||||
variant download-response {
|
|
||||||
started,
|
|
||||||
bad-response,
|
|
||||||
denied(reason),
|
|
||||||
already-exists,
|
|
||||||
already-downloading,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum install-response {
|
enum install-response {
|
||||||
success,
|
success,
|
||||||
failure, // TODO
|
failure,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum uninstall-response {
|
enum uninstall-response {
|
||||||
success,
|
success,
|
||||||
failure, // TODO
|
failure,
|
||||||
}
|
|
||||||
|
|
||||||
enum mirror-response {
|
|
||||||
success,
|
|
||||||
failure, // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
enum auto-update-response {
|
|
||||||
success,
|
|
||||||
failure, // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
enum rebuild-index-response {
|
|
||||||
success,
|
|
||||||
failure, // TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
record apis-response {
|
record apis-response {
|
||||||
@ -150,11 +246,13 @@ interface main {
|
|||||||
// the API itself will be in response blob if success!
|
// the API itself will be in response blob if success!
|
||||||
enum get-api-response {
|
enum get-api-response {
|
||||||
success,
|
success,
|
||||||
failure, // TODO
|
failure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
world app-store-sys-v0 {
|
world app-store-sys-v0 {
|
||||||
import main;
|
import main;
|
||||||
|
import downloads;
|
||||||
|
import chain;
|
||||||
include process-v0;
|
include process-v0;
|
||||||
}
|
}
|
@ -7,11 +7,11 @@ edition = "2021"
|
|||||||
simulation-mode = []
|
simulation-mode = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alloy-primitives = "0.7.0"
|
alloy-primitives = "0.7.6"
|
||||||
alloy-sol-types = "0.7.0"
|
alloy-sol-types = "0.7.6"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.8.0" }
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -1,79 +1,133 @@
|
|||||||
use crate::state::{PackageListing, PackageState, State};
|
use crate::{
|
||||||
use crate::DownloadResponse;
|
kinode::process::chain::{ChainRequests, ChainResponses},
|
||||||
use kinode_process_lib::{
|
kinode::process::downloads::{
|
||||||
http::{
|
DownloadRequests, DownloadResponses, LocalDownloadRequest, RemoveFileRequest,
|
||||||
bind_http_path, bind_ws_path, send_response, serve_ui, IncomingHttpRequest, Method,
|
|
||||||
StatusCode,
|
|
||||||
},
|
},
|
||||||
Address, NodeId, PackageId, Request,
|
state::{MirrorCheck, PackageState, State},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use kinode_process_lib::{
|
||||||
|
http::{self, server, Method, StatusCode},
|
||||||
|
Address, LazyLoadBlob, PackageId, Request,
|
||||||
|
};
|
||||||
|
use kinode_process_lib::{SendError, SendErrorKind};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
const ICON: &str = include_str!("icon");
|
const ICON: &str = include_str!("icon");
|
||||||
|
|
||||||
/// Bind static and dynamic HTTP paths for the app store,
|
/// Bind static and dynamic HTTP paths for the app store,
|
||||||
/// bind to our WS updates path, and add icon and widget to homepage.
|
/// bind to our WS updates path, and add icon and widget to homepage.
|
||||||
pub fn init_frontend(our: &Address) {
|
pub fn init_frontend(our: &Address, http_server: &mut server::HttpServer) {
|
||||||
|
let config = server::HttpBindingConfig::default();
|
||||||
|
|
||||||
for path in [
|
for path in [
|
||||||
"/apps",
|
"/apps", // all on-chain apps
|
||||||
"/apps/listed",
|
"/downloads", // all downloads
|
||||||
"/apps/:id",
|
"/installed", // all installed apps
|
||||||
"/apps/listed/:id",
|
"/ourapps", // all apps we've published
|
||||||
"/apps/:id/caps",
|
"/apps/:id", // detail about an on-chain app
|
||||||
"/apps/:id/mirror",
|
"/downloads/:id", // local downloads for an app
|
||||||
"/apps/:id/auto-update",
|
"/installed/:id", // detail about an installed app
|
||||||
"/apps/rebuild-index",
|
// actions
|
||||||
|
"/apps/:id/download", // download a listed app
|
||||||
|
"/apps/:id/install", // install a downloaded app
|
||||||
|
"/downloads/:id/mirror", // start mirroring a version of a downloaded app
|
||||||
|
"/downloads/:id/remove", // remove a downloaded app
|
||||||
|
"/apps/:id/auto-update", // set auto-updating a version of a downloaded app
|
||||||
|
"/mirrorcheck/:node", // check if a node/mirror is online/offline
|
||||||
] {
|
] {
|
||||||
bind_http_path(path, true, false).expect("failed to bind http path");
|
http_server
|
||||||
|
.bind_http_path(path, config.clone())
|
||||||
|
.expect("failed to bind http path");
|
||||||
}
|
}
|
||||||
serve_ui(
|
http_server
|
||||||
|
.serve_ui(
|
||||||
&our,
|
&our,
|
||||||
"ui",
|
"ui",
|
||||||
true,
|
vec!["/", "/app/:id", "/publish", "/download/:id", "my-downloads"],
|
||||||
false,
|
config.clone(),
|
||||||
vec!["/", "/my-apps", "/app-details/:id", "/publish"],
|
|
||||||
)
|
)
|
||||||
.expect("failed to serve static UI");
|
.expect("failed to serve static UI");
|
||||||
|
|
||||||
bind_ws_path("/", true, true).expect("failed to bind ws path");
|
http_server
|
||||||
|
.bind_ws_path("/", server::WsBindingConfig::default())
|
||||||
|
.expect("failed to bind ws path");
|
||||||
|
|
||||||
// add ourselves to the homepage
|
// add ourselves to the homepage
|
||||||
Request::to(("our", "homepage", "homepage", "sys"))
|
kinode_process_lib::homepage::add_to_homepage(
|
||||||
.body(
|
"App Store",
|
||||||
serde_json::json!({
|
Some(ICON),
|
||||||
"Add": {
|
Some("/"),
|
||||||
"label": "App Store",
|
Some(&make_widget()),
|
||||||
"icon": ICON,
|
);
|
||||||
"path": "/",
|
|
||||||
"widget": make_widget()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string()
|
|
||||||
.as_bytes()
|
|
||||||
.to_vec(),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_widget() -> String {
|
fn make_widget() -> String {
|
||||||
return r#"<html>
|
return r#"<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="/kinode.css">
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Kode Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#latest-apps {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: stretch;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: sans-serif;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-image {
|
.app-image {
|
||||||
background-size: cover;
|
border-radius: 0.75rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
height: 92px;
|
||||||
|
width: 92px;
|
||||||
|
max-width: 33%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-info {
|
.app-info {
|
||||||
max-width: 67%
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-info h2 {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 500px) {
|
@media screen and (min-width: 500px) {
|
||||||
@ -83,41 +137,30 @@ fn make_widget() -> String {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="text-white overflow-hidden">
|
<body>
|
||||||
<div
|
<div id="latest-apps"></div>
|
||||||
id="latest-apps"
|
|
||||||
class="flex flex-wrap p-2 gap-2 items-center backdrop-brightness-125 rounded-xl shadow-lg h-screen w-screen overflow-y-auto"
|
|
||||||
style="
|
|
||||||
scrollbar-color: transparent transparent;
|
|
||||||
scrollbar-width: none;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
fetch('/main:app_store:sys/apps/listed', { credentials: 'include' })
|
fetch('/main:app_store:sys/apps', { credentials: 'include' })
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
const container = document.getElementById('latest-apps');
|
const container = document.getElementById('latest-apps');
|
||||||
data.forEach(app => {
|
data.forEach(app => {
|
||||||
if (app.metadata) {
|
if (app.metadata) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.className = 'app p-2 grow flex items-stretch rounded-lg shadow bg-white/10 hover:bg-white/20 font-sans cursor-pointer';
|
a.className = 'app';
|
||||||
a.href = `/main:app_store:sys/app-details/${app.package}:${app.publisher}`
|
a.href = `/main:app_store:sys/app/${app.package_id.package_name}:${app.package_id.publisher_node}`
|
||||||
a.target = '_blank';
|
a.target = '_blank';
|
||||||
a.rel = 'noopener noreferrer';
|
a.rel = 'noopener noreferrer';
|
||||||
const iconLetter = app.metadata_hash.replace('0x', '')[0].toUpperCase();
|
const iconLetter = app.metadata_hash.replace('0x', '')[0].toUpperCase();
|
||||||
a.innerHTML = `<div
|
a.innerHTML = `<div
|
||||||
class="app-image rounded mr-2 grow"
|
class="app-image"
|
||||||
style="
|
style="
|
||||||
background-image: url('${app.metadata.image || `/icons/${iconLetter}`}');
|
background-image: url('${app.metadata.image || `/bird-orange.svg`}');
|
||||||
height: 92px;
|
|
||||||
width: 92px;
|
|
||||||
max-width: 33%;
|
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
<div class="app-info flex flex-col grow">
|
<div class="app-info">
|
||||||
<h2 class="font-bold">${app.metadata.name}</h2>
|
<h2>${app.metadata.name}</h2>
|
||||||
<p>${app.metadata.description}</p>
|
<p>${app.metadata.description}</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
container.appendChild(a);
|
container.appendChild(a);
|
||||||
@ -133,38 +176,45 @@ fn make_widget() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Actions supported over HTTP:
|
/// Actions supported over HTTP:
|
||||||
/// - get all downloaded apps: GET /apps
|
/// - get all apps: GET /apps
|
||||||
/// - get all listed apps: GET /apps/listed
|
/// - get all downloaded apps: GET /downloads
|
||||||
/// - get some subset of listed apps, via search or filter: ?
|
/// - get all installed apps: GET /installed
|
||||||
/// - get detail about a specific downloaded app: GET /apps/:id
|
/// - get all apps we've published: GET /ourapps
|
||||||
/// - get capabilities for a specific downloaded app: GET /apps/:id/caps
|
/// - get detail about a specific app: GET /apps/:id
|
||||||
/// - get detail about a specific listed app: GET /apps/listed/:id
|
/// - get detail about a specific apps downloads: GET /downloads/:id
|
||||||
///
|
/// - remove a downloaded app: POST /downloads/:id/remove
|
||||||
/// - download a listed app: POST /apps/listed/:id
|
|
||||||
/// - install a downloaded app: POST /apps/:id
|
/// - get online/offline mirrors for a listed app: GET /mirrorcheck/:node
|
||||||
|
/// - download a listed app: POST /apps/:id/download
|
||||||
|
/// - install a downloaded app: POST /apps/:id/install
|
||||||
/// - uninstall/delete a downloaded app: DELETE /apps/:id
|
/// - uninstall/delete a downloaded app: DELETE /apps/:id
|
||||||
/// - update a downloaded app: PUT /apps/:id
|
|
||||||
/// - approve capabilities for a downloaded app: POST /apps/:id/caps
|
|
||||||
/// - start mirroring a downloaded app: PUT /apps/:id/mirror
|
/// - start mirroring a downloaded app: PUT /apps/:id/mirror
|
||||||
/// - stop mirroring a downloaded app: DELETE /apps/:id/mirror
|
/// - stop mirroring a downloaded app: DELETE /apps/:id/mirror
|
||||||
/// - start auto-updating a downloaded app: PUT /apps/:id/auto-update
|
/// - start auto-updating a downloaded app: PUT /apps/:id/auto-update
|
||||||
/// - stop auto-updating a downloaded app: DELETE /apps/:id/auto-update
|
/// - stop auto-updating a downloaded app: DELETE /apps/:id/auto-update
|
||||||
///
|
///
|
||||||
/// - RebuildIndex: POST /apps/rebuild-index
|
/// - RebuildIndex: POST /apps/rebuild-index // TODO, this could be just terminal I think?
|
||||||
pub fn handle_http_request(state: &mut State, req: &IncomingHttpRequest) -> anyhow::Result<()> {
|
pub fn handle_http_request(
|
||||||
match serve_paths(state, req) {
|
our: &Address,
|
||||||
Ok((status_code, _headers, body)) => send_response(
|
state: &mut State,
|
||||||
status_code,
|
req: &server::IncomingHttpRequest,
|
||||||
Some(HashMap::from([(
|
) -> (server::HttpResponse, Option<LazyLoadBlob>) {
|
||||||
String::from("Content-Type"),
|
match serve_paths(our, state, req) {
|
||||||
String::from("application/json"),
|
Ok((status_code, _headers, body)) => (
|
||||||
)])),
|
server::HttpResponse::new(status_code).header("Content-Type", "application/json"),
|
||||||
body,
|
Some(LazyLoadBlob {
|
||||||
|
mime: None,
|
||||||
|
bytes: body,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Err(e) => (
|
||||||
|
server::HttpResponse::new(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
Some(LazyLoadBlob {
|
||||||
|
mime: None,
|
||||||
|
bytes: serde_json::to_vec(&json!({"error": e.to_string()})).unwrap(),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
Err(_e) => send_response(StatusCode::INTERNAL_SERVER_ERROR, None, vec![]),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<PackageId> {
|
fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<PackageId> {
|
||||||
@ -176,100 +226,47 @@ fn get_package_id(url_params: &HashMap<String, String>) -> anyhow::Result<Packag
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_package_info(
|
fn gen_package_info(id: &PackageId, state: &PackageState) -> serde_json::Value {
|
||||||
id: &PackageId,
|
// installed package info
|
||||||
listing: Option<&PackageListing>,
|
|
||||||
state: Option<&PackageState>,
|
|
||||||
) -> serde_json::Value {
|
|
||||||
json!({
|
json!({
|
||||||
"owner": match &listing {
|
"package_id": {
|
||||||
Some(listing) => Some(&listing.owner),
|
"package_name": id.package(),
|
||||||
None => None,
|
"publisher_node": id.publisher(),
|
||||||
},
|
},
|
||||||
"package": id.package().to_string(),
|
"our_version_hash": state.our_version_hash,
|
||||||
"publisher": id.publisher(),
|
"publisher": id.publisher(),
|
||||||
"installed": match &state {
|
"our_version_hash": state.our_version_hash,
|
||||||
Some(state) => state.installed,
|
|
||||||
None => false,
|
|
||||||
},
|
|
||||||
"metadata_hash": match &listing {
|
|
||||||
Some(listing) => Some(&listing.metadata_hash),
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
"metadata": match &listing {
|
|
||||||
Some(listing) => Some(&listing.metadata),
|
|
||||||
None => match state {
|
|
||||||
Some(state) => Some(&state.metadata),
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"state": match &state {
|
|
||||||
Some(state) => json!({
|
|
||||||
"mirrored_from": state.mirrored_from,
|
|
||||||
"our_version": state.our_version,
|
|
||||||
"caps_approved": state.caps_approved,
|
|
||||||
"mirroring": state.mirroring,
|
|
||||||
"auto_update": state.auto_update,
|
|
||||||
"verified": state.verified,
|
"verified": state.verified,
|
||||||
}),
|
"caps_approved": state.caps_approved,
|
||||||
None => json!(null),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serve_paths(
|
fn serve_paths(
|
||||||
|
our: &Address,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
req: &IncomingHttpRequest,
|
req: &server::IncomingHttpRequest,
|
||||||
) -> anyhow::Result<(StatusCode, Option<HashMap<String, String>>, Vec<u8>)> {
|
) -> anyhow::Result<(http::StatusCode, Option<HashMap<String, String>>, Vec<u8>)> {
|
||||||
let method = req.method()?;
|
let method = req.method()?;
|
||||||
|
|
||||||
let bound_path: &str = req.bound_path(Some(&state.our.process.to_string()));
|
let bound_path: &str = req.bound_path(Some(&our.process.to_string()));
|
||||||
let url_params = req.url_params();
|
let url_params = req.url_params();
|
||||||
|
|
||||||
match bound_path {
|
match bound_path {
|
||||||
// GET all downloaded apps
|
// GET all apps
|
||||||
"/apps" => {
|
"/apps" => {
|
||||||
if method != Method::GET {
|
let resp = Request::to(("our", "chain", "app_store", "sys"))
|
||||||
return Ok((
|
.body(serde_json::to_vec(&ChainRequests::GetApps)?)
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
.send_and_await_response(5)??;
|
||||||
None,
|
let msg = serde_json::from_slice::<ChainResponses>(resp.body())?;
|
||||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
match msg {
|
||||||
));
|
ChainResponses::GetApps(apps) => {
|
||||||
|
Ok((StatusCode::OK, None, serde_json::to_vec(&apps)?))
|
||||||
}
|
}
|
||||||
let all: Vec<serde_json::Value> = state
|
_ => Err(anyhow::anyhow!("Invalid response from chain: {:?}", msg)),
|
||||||
.downloaded_packages
|
|
||||||
.iter()
|
|
||||||
.map(|(package_id, package_state)| {
|
|
||||||
let listing = state.get_listing(package_id);
|
|
||||||
gen_package_info(package_id, listing, Some(package_state))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?));
|
|
||||||
}
|
}
|
||||||
// GET all listed apps
|
|
||||||
"/apps/listed" => {
|
|
||||||
if method != Method::GET {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
|
||||||
None,
|
|
||||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
let all: Vec<serde_json::Value> = state
|
// GET detail about a specific app
|
||||||
.listed_packages
|
|
||||||
.iter()
|
|
||||||
.map(|(_hash, listing)| {
|
|
||||||
let package_id = PackageId::new(&listing.name, &listing.publisher);
|
|
||||||
let state = state.downloaded_packages.get(&package_id);
|
|
||||||
gen_package_info(&package_id, Some(listing), state)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?));
|
|
||||||
}
|
|
||||||
// GET detail about a specific downloaded app
|
|
||||||
// install a downloaded app: POST
|
|
||||||
// update a downloaded app: PUT
|
// update a downloaded app: PUT
|
||||||
// uninstall/delete a downloaded app: DELETE
|
|
||||||
"/apps/:id" => {
|
"/apps/:id" => {
|
||||||
let Ok(package_id) = get_package_id(url_params) else {
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
return Ok((
|
return Ok((
|
||||||
@ -281,64 +278,22 @@ fn serve_paths(
|
|||||||
|
|
||||||
match method {
|
match method {
|
||||||
Method::GET => {
|
Method::GET => {
|
||||||
let Some(pkg) = state.downloaded_packages.get(&package_id) else {
|
let package_id =
|
||||||
return Ok((
|
crate::kinode::process::main::PackageId::from_process_lib(package_id);
|
||||||
StatusCode::NOT_FOUND,
|
let resp = Request::to(("our", "chain", "app_store", "sys"))
|
||||||
None,
|
.body(serde_json::to_vec(&ChainRequests::GetApp(package_id))?)
|
||||||
format!("App not found: {package_id}").into_bytes(),
|
.send_and_await_response(5)??;
|
||||||
));
|
let msg = serde_json::from_slice::<ChainResponses>(resp.body())?;
|
||||||
};
|
match msg {
|
||||||
let listing = state.get_listing(&package_id);
|
ChainResponses::GetApp(app) => {
|
||||||
Ok((
|
Ok((StatusCode::OK, None, serde_json::to_vec(&app)?))
|
||||||
StatusCode::OK,
|
|
||||||
None,
|
|
||||||
gen_package_info(&package_id, listing, Some(pkg))
|
|
||||||
.to_string()
|
|
||||||
.into_bytes(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
Method::POST => {
|
_ => Err(anyhow::anyhow!("Invalid response from chain: {:?}", msg)),
|
||||||
// install an app
|
|
||||||
crate::handle_install(state, &package_id)?;
|
|
||||||
Ok((StatusCode::CREATED, None, format!("Installed").into_bytes()))
|
|
||||||
}
|
|
||||||
Method::PUT => {
|
|
||||||
// update an app
|
|
||||||
let _pkg_listing: &PackageListing = state
|
|
||||||
.get_listing(&package_id)
|
|
||||||
.ok_or(anyhow::anyhow!("No package"))?;
|
|
||||||
let pkg_state: &PackageState = state
|
|
||||||
.downloaded_packages
|
|
||||||
.get(&package_id)
|
|
||||||
.ok_or(anyhow::anyhow!("No package"))?;
|
|
||||||
let download_from = pkg_state
|
|
||||||
.mirrored_from
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(anyhow::anyhow!("No mirror for package {package_id}"))?
|
|
||||||
.to_string();
|
|
||||||
match crate::start_download(
|
|
||||||
state,
|
|
||||||
package_id,
|
|
||||||
download_from,
|
|
||||||
pkg_state.mirroring,
|
|
||||||
pkg_state.auto_update,
|
|
||||||
None,
|
|
||||||
) {
|
|
||||||
DownloadResponse::Started => Ok((
|
|
||||||
StatusCode::CREATED,
|
|
||||||
None,
|
|
||||||
format!("Downloading").into_bytes(),
|
|
||||||
)),
|
|
||||||
_ => Ok((
|
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
|
||||||
None,
|
|
||||||
format!("Failed to download").into_bytes(),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Method::DELETE => {
|
Method::DELETE => {
|
||||||
// uninstall an app
|
// uninstall an app
|
||||||
state.uninstall(&package_id)?;
|
crate::utils::uninstall(state, &package_id)?;
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::NO_CONTENT,
|
StatusCode::NO_CONTENT,
|
||||||
None,
|
None,
|
||||||
@ -352,9 +307,144 @@ fn serve_paths(
|
|||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GET detail about a specific listed app
|
"/downloads" => {
|
||||||
// download a listed app: POST
|
// get all local downloads!
|
||||||
"/apps/listed/:id" => {
|
let resp = Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::GetFiles(None))?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
|
||||||
|
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
DownloadResponses::GetFiles(files) => {
|
||||||
|
Ok((StatusCode::OK, None, serde_json::to_vec(&files)?))
|
||||||
|
}
|
||||||
|
DownloadResponses::Error(e) => {
|
||||||
|
Err(anyhow::anyhow!("Error from downloads: {:?}", e))
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Invalid response from downloads: {:?}",
|
||||||
|
msg
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"/downloads/:id" => {
|
||||||
|
// get all local downloads!
|
||||||
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
None,
|
||||||
|
format!("Missing id").into_bytes(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let package_id = crate::kinode::process::main::PackageId::from_process_lib(package_id);
|
||||||
|
let resp = Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::GetFiles(Some(
|
||||||
|
package_id,
|
||||||
|
)))?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
|
||||||
|
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
DownloadResponses::GetFiles(files) => {
|
||||||
|
Ok((StatusCode::OK, None, serde_json::to_vec(&files)?))
|
||||||
|
}
|
||||||
|
DownloadResponses::Error(e) => {
|
||||||
|
Err(anyhow::anyhow!("Error from downloads: {:?}", e))
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Invalid response from downloads: {:?}",
|
||||||
|
msg
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"/installed" => {
|
||||||
|
let all: Vec<serde_json::Value> = state
|
||||||
|
.packages
|
||||||
|
.iter()
|
||||||
|
.map(|(package_id, listing)| gen_package_info(package_id, listing))
|
||||||
|
.collect();
|
||||||
|
return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?));
|
||||||
|
}
|
||||||
|
"/installed/:id" => {
|
||||||
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
None,
|
||||||
|
format!("Missing id").into_bytes(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let specific_package_info = state
|
||||||
|
.packages
|
||||||
|
.get(&package_id)
|
||||||
|
.map(|listing| gen_package_info(&package_id, listing))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::Error::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
format!("Package with id {} not found", package_id),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
return Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
None,
|
||||||
|
serde_json::to_vec(&specific_package_info)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
"/ourapps" => {
|
||||||
|
let resp = Request::to(("our", "chain", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&ChainRequests::GetOurApps)?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
let msg = serde_json::from_slice::<ChainResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
ChainResponses::GetOurApps(apps) => {
|
||||||
|
Ok((StatusCode::OK, None, serde_json::to_vec(&apps)?))
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!("Invalid response from chain: {:?}", msg)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// POST /apps/:id/download
|
||||||
|
// download a listed app from a mirror
|
||||||
|
"/apps/:id/download" => {
|
||||||
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
None,
|
||||||
|
format!("Missing id").into_bytes(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
// from POST body, look for download_from field and use that as the mirror
|
||||||
|
let body = crate::get_blob()
|
||||||
|
.ok_or(anyhow::anyhow!("missing blob"))?
|
||||||
|
.bytes;
|
||||||
|
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||||
|
let download_from = body_json
|
||||||
|
.get("download_from")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No download_from specified!"))?;
|
||||||
|
let version_hash = body_json
|
||||||
|
.get("version_hash")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No version_hash specified!"))?;
|
||||||
|
|
||||||
|
let download_request = DownloadRequests::LocalDownload(LocalDownloadRequest {
|
||||||
|
package_id: crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
|
download_from: download_from.clone(),
|
||||||
|
desired_version_hash: version_hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&download_request)?)
|
||||||
|
.send()?;
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
None,
|
||||||
|
serde_json::to_vec(&DownloadResponses::Success)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
// POST /apps/:id/install
|
||||||
|
// install a downloaded app
|
||||||
|
"/apps/:id/install" => {
|
||||||
let Ok(package_id) = get_package_id(url_params) else {
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@ -363,124 +453,38 @@ fn serve_paths(
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
match method {
|
|
||||||
Method::GET => {
|
|
||||||
let Some(listing) = state.get_listing(&package_id) else {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
None,
|
|
||||||
format!("App not found: {package_id}").into_bytes(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let downloaded = state.downloaded_packages.get(&package_id);
|
|
||||||
Ok((
|
|
||||||
StatusCode::OK,
|
|
||||||
None,
|
|
||||||
gen_package_info(&package_id, Some(listing), downloaded)
|
|
||||||
.to_string()
|
|
||||||
.into_bytes(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Method::POST => {
|
|
||||||
// download an app
|
|
||||||
let pkg_listing: &PackageListing = state
|
|
||||||
.get_listing(&package_id)
|
|
||||||
.ok_or(anyhow::anyhow!("No package"))?;
|
|
||||||
// from POST body, look for download_from field and use that as the mirror
|
|
||||||
let body = crate::get_blob()
|
let body = crate::get_blob()
|
||||||
.ok_or(anyhow::anyhow!("missing blob"))?
|
.ok_or(anyhow::anyhow!("missing blob"))?
|
||||||
.bytes;
|
.bytes;
|
||||||
let body_json: serde_json::Value =
|
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||||
serde_json::from_slice(&body).unwrap_or_default();
|
|
||||||
let mirrors: &Vec<NodeId> = pkg_listing
|
let version_hash = body_json
|
||||||
.metadata
|
.get("version_hash")
|
||||||
.as_ref()
|
.and_then(|v| v.as_str())
|
||||||
.expect("Package does not have metadata")
|
.map(|s| s.to_string())
|
||||||
.properties
|
.ok_or_else(|| anyhow::anyhow!("No version_hash specified!"))?;
|
||||||
.mirrors
|
|
||||||
.as_ref();
|
let process_package_id =
|
||||||
let download_from = body_json
|
crate::kinode::process::main::PackageId::from_process_lib(package_id);
|
||||||
.get("download_from")
|
|
||||||
.unwrap_or(&json!(mirrors
|
match crate::utils::install(
|
||||||
.first()
|
&process_package_id,
|
||||||
.ok_or(anyhow::anyhow!("No mirrors for package {package_id}"))?))
|
|
||||||
.as_str()
|
|
||||||
.ok_or(anyhow::anyhow!("download_from not a string"))?
|
|
||||||
.to_string();
|
|
||||||
// TODO select on FE? or after download but before install?
|
|
||||||
let mirror = false;
|
|
||||||
let auto_update = false;
|
|
||||||
let desired_version_hash = None;
|
|
||||||
match crate::start_download(
|
|
||||||
state,
|
|
||||||
package_id,
|
|
||||||
download_from,
|
|
||||||
mirror,
|
|
||||||
auto_update,
|
|
||||||
desired_version_hash,
|
|
||||||
) {
|
|
||||||
DownloadResponse::Started => Ok((
|
|
||||||
StatusCode::CREATED,
|
|
||||||
None,
|
None,
|
||||||
format!("Downloading").into_bytes(),
|
&version_hash,
|
||||||
)),
|
state,
|
||||||
other => Ok((
|
&our.node().to_string(),
|
||||||
|
) {
|
||||||
|
Ok(_) => Ok((StatusCode::CREATED, None, vec![])),
|
||||||
|
Err(e) => Ok((
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
None,
|
None,
|
||||||
format!("Failed to download: {other:?}").into_bytes(),
|
e.to_string().into_bytes(),
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Ok((
|
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
|
||||||
None,
|
|
||||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// GET caps for a specific downloaded app
|
|
||||||
// approve capabilities for a downloaded app: POST
|
|
||||||
"/apps/:id/caps" => {
|
|
||||||
let Ok(package_id) = get_package_id(url_params) else {
|
|
||||||
return Ok((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
None,
|
|
||||||
format!("Missing id").into_bytes(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
match method {
|
|
||||||
// return the capabilities for that app
|
|
||||||
Method::GET => Ok(match crate::utils::fetch_package_manifest(&package_id) {
|
|
||||||
Ok(manifest) => (StatusCode::OK, None, serde_json::to_vec(&manifest)?),
|
|
||||||
Err(_) => (
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
None,
|
|
||||||
format!("App manifest not found: {package_id}").into_bytes(),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
// approve the capabilities for that app
|
|
||||||
Method::POST => Ok(
|
|
||||||
match state.update_downloaded_package(&package_id, |pkg| {
|
|
||||||
pkg.caps_approved = true;
|
|
||||||
}) {
|
|
||||||
true => (StatusCode::OK, None, vec![]),
|
|
||||||
false => (
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
None,
|
|
||||||
format!("App not found: {package_id}").into_bytes(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_ => Ok((
|
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
|
||||||
None,
|
|
||||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// start mirroring a downloaded app: PUT
|
// start mirroring a downloaded app: PUT
|
||||||
// stop mirroring a downloaded app: DELETE
|
// stop mirroring a downloaded app: DELETE
|
||||||
"/apps/:id/mirror" => {
|
"/downloads/:id/mirror" => {
|
||||||
let Ok(package_id) = get_package_id(url_params) else {
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
@ -488,17 +492,48 @@ fn serve_paths(
|
|||||||
format!("Missing id").into_bytes(),
|
format!("Missing id").into_bytes(),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
let downloads = Address::from_str("our@downloads:app_store:sys")?;
|
||||||
|
|
||||||
match method {
|
match method {
|
||||||
// start mirroring an app
|
// start mirroring an app
|
||||||
Method::PUT => {
|
Method::PUT => {
|
||||||
state.start_mirroring(&package_id);
|
let resp = Request::new()
|
||||||
Ok((StatusCode::OK, None, vec![]))
|
.target(downloads)
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::StartMirroring(
|
||||||
|
crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
|
))?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])),
|
||||||
|
DownloadResponses::Error(e) => {
|
||||||
|
Err(anyhow::anyhow!("Error starting mirroring: {:?}", e))
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Invalid response from downloads: {:?}",
|
||||||
|
msg
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// stop mirroring an app
|
// stop mirroring an app
|
||||||
Method::DELETE => {
|
Method::DELETE => {
|
||||||
state.stop_mirroring(&package_id);
|
let resp = Request::new()
|
||||||
Ok((StatusCode::OK, None, vec![]))
|
.target(downloads)
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::StopMirroring(
|
||||||
|
crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
|
))?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])),
|
||||||
|
DownloadResponses::Error(e) => {
|
||||||
|
Err(anyhow::anyhow!("Error stopping mirroring: {:?}", e))
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Invalid response from downloads: {:?}",
|
||||||
|
msg
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => Ok((
|
_ => Ok((
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
StatusCode::METHOD_NOT_ALLOWED,
|
||||||
@ -507,46 +542,126 @@ fn serve_paths(
|
|||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// remove a downloaded app: POST
|
||||||
|
"/downloads/:id/remove" => {
|
||||||
|
let Ok(package_id) = get_package_id(url_params) else {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
None,
|
||||||
|
format!("Missing id").into_bytes(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let body = crate::get_blob()
|
||||||
|
.ok_or(anyhow::anyhow!("missing blob"))?
|
||||||
|
.bytes;
|
||||||
|
let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||||
|
let version_hash = body_json
|
||||||
|
.get("version_hash")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No version_hash specified!"))?;
|
||||||
|
let download_request = DownloadRequests::RemoveFile(RemoveFileRequest {
|
||||||
|
package_id: crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
|
version_hash: version_hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&download_request)?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])),
|
||||||
|
DownloadResponses::Error(e) => Err(anyhow::anyhow!("Error removing file: {:?}", e)),
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Invalid response from downloads: {:?}",
|
||||||
|
msg
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
// start auto-updating a downloaded app: PUT
|
// start auto-updating a downloaded app: PUT
|
||||||
// stop auto-updating a downloaded app: DELETE
|
// stop auto-updating a downloaded app: DELETE
|
||||||
"/apps/:id/auto-update" => {
|
"/apps/:id/auto-update" => {
|
||||||
let Ok(package_id) = get_package_id(url_params) else {
|
let package_id = get_package_id(url_params)?;
|
||||||
return Ok((
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
None,
|
|
||||||
format!("Missing id").into_bytes(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
match method {
|
let chain_request = match method {
|
||||||
// start auto-updating an app
|
Method::PUT => ChainRequests::StartAutoUpdate(
|
||||||
Method::PUT => {
|
crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
state.start_auto_update(&package_id);
|
),
|
||||||
Ok((StatusCode::OK, None, vec![]))
|
Method::DELETE => ChainRequests::StopAutoUpdate(
|
||||||
}
|
crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
// stop auto-updating an app
|
),
|
||||||
Method::DELETE => {
|
_ => {
|
||||||
state.stop_auto_update(&package_id);
|
return Ok((
|
||||||
Ok((StatusCode::OK, None, vec![]))
|
|
||||||
}
|
|
||||||
_ => Ok((
|
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
StatusCode::METHOD_NOT_ALLOWED,
|
||||||
None,
|
None,
|
||||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = Request::to(("our", "chain", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&chain_request)?)
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
|
||||||
|
let msg = serde_json::from_slice::<ChainResponses>(resp.body())?;
|
||||||
|
match msg {
|
||||||
|
ChainResponses::AutoUpdateStarted
|
||||||
|
| ChainResponses::AutoUpdateStopped
|
||||||
|
| ChainResponses::Error(_) => Ok((StatusCode::OK, None, serde_json::to_vec(&msg)?)),
|
||||||
|
_ => Ok((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
None,
|
||||||
|
format!("Invalid response from chain: {:?}", msg).into_bytes(),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// RebuildIndex: POST
|
// GET online/offline mirrors for a listed app
|
||||||
"/apps/rebuild-index" => {
|
"/mirrorcheck/:node" => {
|
||||||
if method != Method::POST {
|
if method != Method::GET {
|
||||||
return Ok((
|
return Ok((
|
||||||
StatusCode::METHOD_NOT_ALLOWED,
|
StatusCode::METHOD_NOT_ALLOWED,
|
||||||
None,
|
None,
|
||||||
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
format!("Invalid method {method} for {bound_path}").into_bytes(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
crate::rebuild_index(state);
|
let Some(node) = url_params.get("node") else {
|
||||||
Ok((StatusCode::OK, None, vec![]))
|
return Ok((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
None,
|
||||||
|
format!("Missing node").into_bytes(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if let Err(SendError { kind, .. }) = Request::to((node, "net", "distro", "sys"))
|
||||||
|
.body(b"checking your mirror status...")
|
||||||
|
.send_and_await_response(3)
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
match kind {
|
||||||
|
SendErrorKind::Timeout => {
|
||||||
|
let check_reponse = MirrorCheck {
|
||||||
|
node: node.to_string(),
|
||||||
|
is_online: false,
|
||||||
|
error: Some(format!("node {} timed out", node).to_string()),
|
||||||
|
};
|
||||||
|
return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?));
|
||||||
|
}
|
||||||
|
SendErrorKind::Offline => {
|
||||||
|
let check_reponse = MirrorCheck {
|
||||||
|
node: node.to_string(),
|
||||||
|
is_online: false,
|
||||||
|
error: Some(format!("node {} is offline", node).to_string()),
|
||||||
|
};
|
||||||
|
return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let check_reponse = MirrorCheck {
|
||||||
|
node: node.to_string(),
|
||||||
|
is_online: true,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => Ok((
|
_ => Ok((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
//! App Store:
|
//! main:app_store:
|
||||||
//! acts as both a local package manager and a protocol to share packages across the network.
|
//! acts as a manager for installed apps, and coordinator for http requests.
|
||||||
//! packages are apps; apps are packages. we use an onchain app listing contract to determine
|
//!
|
||||||
|
//! the chain:app_store process takes care of on-chain indexing, while
|
||||||
|
//! the downloads:app_store process takes care of sharing and versioning.
|
||||||
|
//!
|
||||||
|
//! packages are apps; apps are packages. chain:app_store uses the kimap contract to determine
|
||||||
//! what apps are available to download and what node(s) to download them from.
|
//! what apps are available to download and what node(s) to download them from.
|
||||||
//!
|
//!
|
||||||
//! once we know that list, we can request a package from a node and download it locally.
|
//! once we know that list, we can request a package from a node and download it locally.
|
||||||
@ -12,22 +16,19 @@
|
|||||||
//! - given permissions (necessary to complete install)
|
//! - given permissions (necessary to complete install)
|
||||||
//! - uninstalled + deleted
|
//! - uninstalled + deleted
|
||||||
//! - set to automatically update if a new version is available
|
//! - set to automatically update if a new version is available
|
||||||
use crate::kinode::process::main::{
|
use crate::kinode::process::downloads::{
|
||||||
ApisResponse, AutoUpdateResponse, DownloadRequest, DownloadResponse, GetApiResponse,
|
DownloadCompleteRequest, DownloadResponses, ProgressUpdate,
|
||||||
HashMismatch, InstallResponse, LocalRequest, LocalResponse, MirrorResponse, NewPackageRequest,
|
|
||||||
NewPackageResponse, Reason, RebuildIndexResponse, RemoteDownloadRequest, RemoteRequest,
|
|
||||||
RemoteResponse, UninstallResponse,
|
|
||||||
};
|
};
|
||||||
use ft_worker_lib::{
|
use crate::kinode::process::main::{
|
||||||
spawn_receive_transfer, spawn_transfer, FTWorkerCommand, FTWorkerResult, FileTransferContext,
|
ApisResponse, GetApiResponse, InstallPackageRequest, InstallResponse, LocalRequest,
|
||||||
|
LocalResponse, NewPackageRequest, NewPackageResponse, UninstallResponse,
|
||||||
};
|
};
|
||||||
use kinode_process_lib::{
|
use kinode_process_lib::{
|
||||||
await_message, call_init, eth, get_blob, http, println, vfs, Address, LazyLoadBlob, Message,
|
await_message, call_init, get_blob, http, print_to_terminal, println, vfs, Address,
|
||||||
NodeId, PackageId, Request, Response,
|
LazyLoadBlob, Message, PackageId, Response,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use state::{AppStoreLogError, PackageState, RequestedPackage, State};
|
use state::State;
|
||||||
use utils::{fetch_and_subscribe_logs, fetch_state, subscribe_to_logs};
|
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
path: "target/wit",
|
path: "target/wit",
|
||||||
@ -36,35 +37,11 @@ wit_bindgen::generate!({
|
|||||||
additional_derives: [serde::Deserialize, serde::Serialize],
|
additional_derives: [serde::Deserialize, serde::Serialize],
|
||||||
});
|
});
|
||||||
|
|
||||||
mod ft_worker_lib;
|
|
||||||
mod http_api;
|
mod http_api;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
#[cfg(not(feature = "simulation-mode"))]
|
const VFS_TIMEOUT: u64 = 10;
|
||||||
const CHAIN_ID: u64 = 10; // optimism
|
|
||||||
#[cfg(feature = "simulation-mode")]
|
|
||||||
const CHAIN_ID: u64 = 31337; // local
|
|
||||||
|
|
||||||
const CHAIN_TIMEOUT: u64 = 60; // 60s
|
|
||||||
pub const VFS_TIMEOUT: u64 = 5; // 5s
|
|
||||||
pub const APP_SHARE_TIMEOUT: u64 = 120; // 120s
|
|
||||||
|
|
||||||
#[cfg(not(feature = "simulation-mode"))]
|
|
||||||
const CONTRACT_ADDRESS: &str = "0x52185B6a6017E6f079B994452F234f7C2533787B"; // optimism
|
|
||||||
#[cfg(feature = "simulation-mode")]
|
|
||||||
const CONTRACT_ADDRESS: &str = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"; // local
|
|
||||||
|
|
||||||
#[cfg(not(feature = "simulation-mode"))]
|
|
||||||
const CONTRACT_FIRST_BLOCK: u64 = 118_590_088;
|
|
||||||
#[cfg(feature = "simulation-mode")]
|
|
||||||
const CONTRACT_FIRST_BLOCK: u64 = 1;
|
|
||||||
|
|
||||||
const EVENTS: [&str; 3] = [
|
|
||||||
"AppRegistered(uint256,string,bytes,string,bytes32)",
|
|
||||||
"AppMetadataUpdated(uint256,string,bytes32)",
|
|
||||||
"Transfer(address,address,uint256)",
|
|
||||||
];
|
|
||||||
|
|
||||||
// internal types
|
// internal types
|
||||||
|
|
||||||
@ -72,35 +49,26 @@ const EVENTS: [&str; 3] = [
|
|||||||
#[serde(untagged)] // untagged as a meta-type for all incoming requests
|
#[serde(untagged)] // untagged as a meta-type for all incoming requests
|
||||||
pub enum Req {
|
pub enum Req {
|
||||||
LocalRequest(LocalRequest),
|
LocalRequest(LocalRequest),
|
||||||
RemoteRequest(RemoteRequest),
|
Progress(ProgressUpdate),
|
||||||
FTWorkerCommand(FTWorkerCommand),
|
DownloadComplete(DownloadCompleteRequest),
|
||||||
FTWorkerResult(FTWorkerResult),
|
Http(http::server::HttpServerRequest),
|
||||||
Eth(eth::EthSubResult),
|
|
||||||
Http(http::HttpServerRequest),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(untagged)] // untagged as a meta-type for all incoming responses
|
#[serde(untagged)] // untagged as a meta-type for all incoming responses
|
||||||
pub enum Resp {
|
pub enum Resp {
|
||||||
LocalResponse(LocalResponse),
|
LocalResponse(LocalResponse),
|
||||||
RemoteResponse(RemoteResponse),
|
Download(DownloadResponses),
|
||||||
FTWorkerResult(FTWorkerResult),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
call_init!(init);
|
call_init!(init);
|
||||||
fn init(our: Address) {
|
fn init(our: Address) {
|
||||||
println!("started");
|
println!("started");
|
||||||
|
|
||||||
http_api::init_frontend(&our);
|
let mut http_server = http::server::HttpServer::new(5);
|
||||||
|
http_api::init_frontend(&our, &mut http_server);
|
||||||
|
|
||||||
println!("indexing on contract address {}", CONTRACT_ADDRESS);
|
let mut state = State::load().expect("state loading failed");
|
||||||
|
|
||||||
// create new provider with request-timeout of 60s
|
|
||||||
// can change, log requests can take quite a long time.
|
|
||||||
let eth_provider = eth::Provider::new(CHAIN_ID, CHAIN_TIMEOUT);
|
|
||||||
|
|
||||||
let mut state = fetch_state(our, eth_provider);
|
|
||||||
fetch_and_subscribe_logs(&mut state);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match await_message() {
|
match await_message() {
|
||||||
@ -109,7 +77,7 @@ fn init(our: Address) {
|
|||||||
println!("got network error: {send_error}");
|
println!("got network error: {send_error}");
|
||||||
}
|
}
|
||||||
Ok(message) => {
|
Ok(message) => {
|
||||||
if let Err(e) = handle_message(&mut state, &message) {
|
if let Err(e) = handle_message(&our, &mut state, &mut http_server, &message) {
|
||||||
println!("error handling message: {:?}", e);
|
println!("error handling message: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,14 +89,19 @@ fn init(our: Address) {
|
|||||||
/// function defined for each kind of message. check whether the source
|
/// function defined for each kind of message. check whether the source
|
||||||
/// of the message is allowed to send that kind of message to us.
|
/// of the message is allowed to send that kind of message to us.
|
||||||
/// finally, fire a response if expected from a request.
|
/// finally, fire a response if expected from a request.
|
||||||
fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
|
fn handle_message(
|
||||||
|
our: &Address,
|
||||||
|
state: &mut State,
|
||||||
|
http_server: &mut http::server::HttpServer,
|
||||||
|
message: &Message,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
if message.is_request() {
|
if message.is_request() {
|
||||||
match serde_json::from_slice::<Req>(message.body())? {
|
match serde_json::from_slice::<Req>(message.body())? {
|
||||||
Req::LocalRequest(local_request) => {
|
Req::LocalRequest(local_request) => {
|
||||||
if !message.is_local(&state.our) {
|
if !message.is_local(our) {
|
||||||
return Err(anyhow::anyhow!("local request from non-local node"));
|
return Err(anyhow::anyhow!("request from non-local node"));
|
||||||
}
|
}
|
||||||
let (body, blob) = handle_local_request(state, local_request);
|
let (body, blob) = handle_local_request(our, state, local_request);
|
||||||
let response = Response::new().body(serde_json::to_vec(&body)?);
|
let response = Response::new().body(serde_json::to_vec(&body)?);
|
||||||
if let Some(blob) = blob {
|
if let Some(blob) = blob {
|
||||||
response.blob(blob).send()?;
|
response.blob(blob).send()?;
|
||||||
@ -136,117 +109,122 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
|
|||||||
response.send()?;
|
response.send()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Req::RemoteRequest(remote_request) => {
|
Req::Http(server_request) => {
|
||||||
let resp = handle_remote_request(state, message.source(), remote_request);
|
if !message.is_local(&our) || message.source().process != "http_server:distro:sys" {
|
||||||
Response::new().body(serde_json::to_vec(&resp)?).send()?;
|
|
||||||
}
|
|
||||||
Req::FTWorkerResult(FTWorkerResult::ReceiveSuccess(name)) => {
|
|
||||||
handle_receive_download(state, &name)?;
|
|
||||||
}
|
|
||||||
Req::FTWorkerCommand(_) => {
|
|
||||||
spawn_receive_transfer(&state.our, message.body())?;
|
|
||||||
}
|
|
||||||
Req::FTWorkerResult(r) => {
|
|
||||||
println!("got weird ft_worker result: {r:?}");
|
|
||||||
}
|
|
||||||
Req::Eth(eth_result) => {
|
|
||||||
if !message.is_local(&state.our) || message.source().process != "eth:distro:sys" {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"eth sub event from weird addr: {}",
|
|
||||||
message.source()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if let Ok(eth::EthSub { result, .. }) = eth_result {
|
|
||||||
handle_eth_sub_event(state, result)?;
|
|
||||||
} else {
|
|
||||||
println!("got eth subscription error");
|
|
||||||
// attempt to resubscribe
|
|
||||||
subscribe_to_logs(&state.provider, utils::app_store_filter(state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Req::Http(incoming) => {
|
|
||||||
if !message.is_local(&state.our)
|
|
||||||
|| message.source().process != "http_server:distro:sys"
|
|
||||||
{
|
|
||||||
return Err(anyhow::anyhow!("http_server from non-local node"));
|
return Err(anyhow::anyhow!("http_server from non-local node"));
|
||||||
}
|
}
|
||||||
if let http::HttpServerRequest::Http(req) = incoming {
|
http_server.handle_request(
|
||||||
http_api::handle_http_request(state, &req)?;
|
server_request,
|
||||||
|
|incoming| http_api::handle_http_request(our, state, &incoming),
|
||||||
|
|_channel_id, _message_type, _blob| {
|
||||||
|
// not expecting any websocket messages from FE currently
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Req::Progress(progress) => {
|
||||||
|
if !message.is_local(&our) {
|
||||||
|
return Err(anyhow::anyhow!("http_server from non-local node"));
|
||||||
|
}
|
||||||
|
http_server.ws_push_all_channels(
|
||||||
|
"/",
|
||||||
|
http::server::WsMessageType::Text,
|
||||||
|
LazyLoadBlob {
|
||||||
|
mime: Some("application/json".to_string()),
|
||||||
|
bytes: serde_json::json!({
|
||||||
|
"kind": "progress",
|
||||||
|
"data": {
|
||||||
|
"package_id": progress.package_id,
|
||||||
|
"version_hash": progress.version_hash,
|
||||||
|
"downloaded": progress.downloaded,
|
||||||
|
"total": progress.total,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Req::DownloadComplete(req) => {
|
||||||
|
if !message.is_local(&our) {
|
||||||
|
return Err(anyhow::anyhow!("download complete from non-local node"));
|
||||||
|
}
|
||||||
|
|
||||||
|
http_server.ws_push_all_channels(
|
||||||
|
"/",
|
||||||
|
http::server::WsMessageType::Text,
|
||||||
|
LazyLoadBlob {
|
||||||
|
mime: Some("application/json".to_string()),
|
||||||
|
bytes: serde_json::json!({
|
||||||
|
"kind": "complete",
|
||||||
|
"data": {
|
||||||
|
"package_id": req.package_id,
|
||||||
|
"version_hash": req.version_hash,
|
||||||
|
"error": req.error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// auto_install case:
|
||||||
|
// the downloads process has given us the new package manifest's
|
||||||
|
// capability hashes, and the old package's capability hashes.
|
||||||
|
// we can use these to determine if the new package has the same
|
||||||
|
// capabilities as the old one, and if so, auto-install it.
|
||||||
|
if let Some(context) = message.context() {
|
||||||
|
let manifest_hash = String::from_utf8(context.to_vec())?;
|
||||||
|
if let Some(package) =
|
||||||
|
state.packages.get(&req.package_id.clone().to_process_lib())
|
||||||
|
{
|
||||||
|
if package.manifest_hash == Some(manifest_hash) {
|
||||||
|
print_to_terminal(1, "auto_install:main, manifest_hash match");
|
||||||
|
if let Err(e) = utils::install(
|
||||||
|
&req.package_id,
|
||||||
|
None,
|
||||||
|
&req.version_hash,
|
||||||
|
state,
|
||||||
|
&our.node,
|
||||||
|
) {
|
||||||
|
print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!("error auto_installing package: {e}"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"auto_installed update for package: {:?}",
|
||||||
|
&req.package_id.to_process_lib()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print_to_terminal(1, "auto_install:main, manifest_hash do not match");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// the only kind of response we care to handle here!
|
match serde_json::from_slice::<Resp>(message.body())? {
|
||||||
handle_ft_worker_result(message.body(), message.context().unwrap_or(&vec![]))?;
|
Resp::LocalResponse(_) => {
|
||||||
|
// don't need to handle these at the moment
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// fielding requests to download packages and APIs from us
|
/// fielding requests to download packages and APIs from us
|
||||||
fn handle_remote_request(state: &mut State, source: &Address, request: RemoteRequest) -> Resp {
|
|
||||||
let (package_id, desired_version_hash) = match &request {
|
|
||||||
RemoteRequest::Download(RemoteDownloadRequest {
|
|
||||||
package_id,
|
|
||||||
desired_version_hash,
|
|
||||||
}) => (package_id, desired_version_hash),
|
|
||||||
};
|
|
||||||
|
|
||||||
let package_id = package_id.to_owned().to_process_lib();
|
|
||||||
let Some(package_state) = state.get_downloaded_package(&package_id) else {
|
|
||||||
return Resp::RemoteResponse(RemoteResponse::DownloadDenied(Reason::NoPackage));
|
|
||||||
};
|
|
||||||
if !package_state.mirroring {
|
|
||||||
return Resp::RemoteResponse(RemoteResponse::DownloadDenied(Reason::NotMirroring));
|
|
||||||
}
|
|
||||||
if let Some(hash) = desired_version_hash.clone() {
|
|
||||||
if package_state.our_version != hash {
|
|
||||||
return Resp::RemoteResponse(RemoteResponse::DownloadDenied(Reason::HashMismatch(
|
|
||||||
HashMismatch {
|
|
||||||
requested: hash,
|
|
||||||
have: package_state.our_version,
|
|
||||||
},
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_name = match &request {
|
|
||||||
RemoteRequest::Download(_) => {
|
|
||||||
// the file name of the zipped app
|
|
||||||
format!("/{}.zip", package_id)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// get the .zip from VFS and attach as blob to response
|
|
||||||
let Ok(Ok(_)) = Request::to(("our", "vfs", "distro", "sys"))
|
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: format!("/{}/pkg{}", package_id, file_name),
|
|
||||||
action: vfs::VfsAction::Read,
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)
|
|
||||||
else {
|
|
||||||
return Resp::RemoteResponse(RemoteResponse::DownloadDenied(Reason::FileNotFound));
|
|
||||||
};
|
|
||||||
// transfer will *inherit* the blob bytes we receive from VFS
|
|
||||||
match spawn_transfer(&state.our, &file_name, None, APP_SHARE_TIMEOUT, &source) {
|
|
||||||
Ok(()) => Resp::RemoteResponse(RemoteResponse::DownloadApproved),
|
|
||||||
Err(_e) => Resp::RemoteResponse(RemoteResponse::DownloadDenied(Reason::WorkerSpawnFailed)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// only `our.node` can call this
|
/// only `our.node` can call this
|
||||||
fn handle_local_request(
|
fn handle_local_request(
|
||||||
|
our: &Address,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
request: LocalRequest,
|
request: LocalRequest,
|
||||||
) -> (LocalResponse, Option<LazyLoadBlob>) {
|
) -> (LocalResponse, Option<LazyLoadBlob>) {
|
||||||
match request {
|
match request {
|
||||||
LocalRequest::NewPackage(NewPackageRequest {
|
LocalRequest::NewPackage(NewPackageRequest { package_id, mirror }) => {
|
||||||
package_id,
|
|
||||||
metadata,
|
|
||||||
mirror,
|
|
||||||
}) => {
|
|
||||||
let Some(blob) = get_blob() else {
|
let Some(blob) = get_blob() else {
|
||||||
return (
|
return (
|
||||||
LocalResponse::NewPackageResponse(NewPackageResponse::NoBlob),
|
LocalResponse::NewPackageResponse(NewPackageResponse::NoBlob),
|
||||||
@ -254,39 +232,26 @@ fn handle_local_request(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
match utils::new_package(
|
match utils::new_package(package_id, mirror, blob.bytes) {
|
||||||
&package_id.to_process_lib(),
|
|
||||||
state,
|
|
||||||
metadata.to_erc721_metadata(),
|
|
||||||
mirror,
|
|
||||||
blob.bytes,
|
|
||||||
) {
|
|
||||||
Ok(()) => LocalResponse::NewPackageResponse(NewPackageResponse::Success),
|
Ok(()) => LocalResponse::NewPackageResponse(NewPackageResponse::Success),
|
||||||
Err(_) => LocalResponse::NewPackageResponse(NewPackageResponse::InstallFailed),
|
Err(_) => LocalResponse::NewPackageResponse(NewPackageResponse::InstallFailed),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LocalRequest::Download(DownloadRequest {
|
LocalRequest::Install(InstallPackageRequest {
|
||||||
package_id,
|
package_id,
|
||||||
download_from,
|
metadata,
|
||||||
mirror,
|
version_hash,
|
||||||
auto_update,
|
|
||||||
desired_version_hash,
|
|
||||||
}) => (
|
}) => (
|
||||||
LocalResponse::DownloadResponse(start_download(
|
match utils::install(&package_id, metadata, &version_hash, state, &our.node) {
|
||||||
state,
|
Ok(()) => {
|
||||||
package_id.to_process_lib(),
|
println!(
|
||||||
download_from,
|
"successfully installed package: {:?}",
|
||||||
mirror,
|
&package_id.to_process_lib()
|
||||||
auto_update,
|
);
|
||||||
desired_version_hash,
|
LocalResponse::InstallResponse(InstallResponse::Success)
|
||||||
)),
|
}
|
||||||
None,
|
|
||||||
),
|
|
||||||
LocalRequest::Install(package_id) => (
|
|
||||||
match handle_install(state, &package_id.to_process_lib()) {
|
|
||||||
Ok(()) => LocalResponse::InstallResponse(InstallResponse::Success),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("error installing package: {e}");
|
println!("error installing package: {e}");
|
||||||
LocalResponse::InstallResponse(InstallResponse::Failure)
|
LocalResponse::InstallResponse(InstallResponse::Failure)
|
||||||
@ -295,59 +260,34 @@ fn handle_local_request(
|
|||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
LocalRequest::Uninstall(package_id) => (
|
LocalRequest::Uninstall(package_id) => (
|
||||||
match state.uninstall(&package_id.to_process_lib()) {
|
match utils::uninstall(state, &package_id.clone().to_process_lib()) {
|
||||||
Ok(()) => LocalResponse::UninstallResponse(UninstallResponse::Success),
|
Ok(()) => {
|
||||||
Err(_) => LocalResponse::UninstallResponse(UninstallResponse::Failure),
|
println!(
|
||||||
|
"successfully uninstalled package: {:?}",
|
||||||
|
&package_id.to_process_lib()
|
||||||
|
);
|
||||||
|
LocalResponse::UninstallResponse(UninstallResponse::Success)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"error uninstalling package: {:?}: {e}",
|
||||||
|
&package_id.to_process_lib()
|
||||||
|
);
|
||||||
|
LocalResponse::UninstallResponse(UninstallResponse::Failure)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
LocalRequest::StartMirroring(package_id) => (
|
|
||||||
match state.start_mirroring(&package_id.to_process_lib()) {
|
|
||||||
true => LocalResponse::MirrorResponse(MirrorResponse::Success),
|
|
||||||
false => LocalResponse::MirrorResponse(MirrorResponse::Failure),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
LocalRequest::StopMirroring(package_id) => (
|
|
||||||
match state.stop_mirroring(&package_id.to_process_lib()) {
|
|
||||||
true => LocalResponse::MirrorResponse(MirrorResponse::Success),
|
|
||||||
false => LocalResponse::MirrorResponse(MirrorResponse::Failure),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
LocalRequest::StartAutoUpdate(package_id) => (
|
|
||||||
match state.start_auto_update(&package_id.to_process_lib()) {
|
|
||||||
true => LocalResponse::AutoUpdateResponse(AutoUpdateResponse::Success),
|
|
||||||
false => LocalResponse::AutoUpdateResponse(AutoUpdateResponse::Failure),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
LocalRequest::StopAutoUpdate(package_id) => (
|
|
||||||
match state.stop_auto_update(&package_id.to_process_lib()) {
|
|
||||||
true => LocalResponse::AutoUpdateResponse(AutoUpdateResponse::Success),
|
|
||||||
false => LocalResponse::AutoUpdateResponse(AutoUpdateResponse::Failure),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
LocalRequest::RebuildIndex => (rebuild_index(state), None),
|
|
||||||
LocalRequest::Apis => (list_apis(state), None),
|
LocalRequest::Apis => (list_apis(state), None),
|
||||||
LocalRequest::GetApi(package_id) => get_api(state, &package_id.to_process_lib()),
|
LocalRequest::GetApi(package_id) => get_api(state, &package_id.to_process_lib()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_api(state: &mut State, package_id: &PackageId) -> (LocalResponse, Option<LazyLoadBlob>) {
|
pub fn get_api(state: &mut State, package_id: &PackageId) -> (LocalResponse, Option<LazyLoadBlob>) {
|
||||||
if !state.downloaded_apis.contains(package_id) {
|
if !state.installed_apis.contains(package_id) {
|
||||||
return (LocalResponse::GetApiResponse(GetApiResponse::Failure), None);
|
return (LocalResponse::GetApiResponse(GetApiResponse::Failure), None);
|
||||||
}
|
}
|
||||||
let Ok(Ok(_)) = Request::new()
|
let Ok(Ok(_)) = utils::vfs_request(format!("/{package_id}/pkg/api.zip"), vfs::VfsAction::Read)
|
||||||
.target(("our", "vfs", "distro", "sys"))
|
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: format!("/{package_id}/pkg/api.zip"),
|
|
||||||
action: vfs::VfsAction::Read,
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)
|
.send_and_await_response(VFS_TIMEOUT)
|
||||||
else {
|
else {
|
||||||
return (LocalResponse::GetApiResponse(GetApiResponse::Failure), None);
|
return (LocalResponse::GetApiResponse(GetApiResponse::Failure), None);
|
||||||
@ -367,226 +307,10 @@ pub fn get_api(state: &mut State, package_id: &PackageId) -> (LocalResponse, Opt
|
|||||||
pub fn list_apis(state: &mut State) -> LocalResponse {
|
pub fn list_apis(state: &mut State) -> LocalResponse {
|
||||||
LocalResponse::ApisResponse(ApisResponse {
|
LocalResponse::ApisResponse(ApisResponse {
|
||||||
apis: state
|
apis: state
|
||||||
.downloaded_apis
|
.installed_apis
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| crate::kinode::process::main::PackageId::from_process_lib(id))
|
.map(|id| crate::kinode::process::main::PackageId::from_process_lib(id))
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebuild_index(state: &mut State) -> LocalResponse {
|
|
||||||
// kill our old subscription and build a new one.
|
|
||||||
let _ = state.provider.unsubscribe(1);
|
|
||||||
|
|
||||||
let eth_provider = eth::Provider::new(CHAIN_ID, CHAIN_TIMEOUT);
|
|
||||||
*state = State::new(
|
|
||||||
state.our.clone(),
|
|
||||||
eth_provider,
|
|
||||||
state.contract_address.clone(),
|
|
||||||
)
|
|
||||||
.expect("state creation failed");
|
|
||||||
|
|
||||||
fetch_and_subscribe_logs(state);
|
|
||||||
LocalResponse::RebuildIndexResponse(RebuildIndexResponse::Success)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_download(
|
|
||||||
state: &mut State,
|
|
||||||
package_id: PackageId,
|
|
||||||
from: NodeId,
|
|
||||||
mirror: bool,
|
|
||||||
auto_update: bool,
|
|
||||||
desired_version_hash: Option<String>,
|
|
||||||
) -> DownloadResponse {
|
|
||||||
let download_request = RemoteDownloadRequest {
|
|
||||||
package_id: crate::kinode::process::main::PackageId::from_process_lib(package_id.clone()),
|
|
||||||
desired_version_hash: desired_version_hash.clone(),
|
|
||||||
};
|
|
||||||
if let Ok(Ok(Message::Response { body, .. })) =
|
|
||||||
Request::to((from.as_str(), state.our.process.clone()))
|
|
||||||
.body(serde_json::to_vec(&RemoteRequest::Download(download_request)).unwrap())
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)
|
|
||||||
{
|
|
||||||
if let Ok(Resp::RemoteResponse(RemoteResponse::DownloadApproved)) =
|
|
||||||
serde_json::from_slice::<Resp>(&body)
|
|
||||||
{
|
|
||||||
let requested = RequestedPackage {
|
|
||||||
from,
|
|
||||||
mirror,
|
|
||||||
auto_update,
|
|
||||||
desired_version_hash,
|
|
||||||
};
|
|
||||||
state.requested_packages.insert(package_id, requested);
|
|
||||||
return DownloadResponse::Started;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DownloadResponse::BadResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_receive_download(state: &mut State, package_name: &str) -> anyhow::Result<()> {
|
|
||||||
// remove leading / and .zip from file name to get package ID
|
|
||||||
let package_name = package_name
|
|
||||||
.trim_start_matches("/")
|
|
||||||
.trim_end_matches(".zip");
|
|
||||||
let Ok(package_id) = package_name.parse::<PackageId>() else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"bad package ID from download: {package_name}"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
handle_receive_download_package(state, &package_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_receive_download_package(
|
|
||||||
state: &mut State,
|
|
||||||
package_id: &PackageId,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
println!("successfully received {}", package_id);
|
|
||||||
// only save the package if we actually requested it
|
|
||||||
let Some(requested_package) = state.requested_packages.remove(package_id) else {
|
|
||||||
return Err(anyhow::anyhow!("received unrequested package--rejecting!"));
|
|
||||||
};
|
|
||||||
let Some(blob) = get_blob() else {
|
|
||||||
return Err(anyhow::anyhow!("received download but found no blob"));
|
|
||||||
};
|
|
||||||
// check the version hash for this download against requested!
|
|
||||||
let download_hash = utils::generate_version_hash(&blob.bytes);
|
|
||||||
let (verified, metadata) = match requested_package.desired_version_hash {
|
|
||||||
Some(hash) => {
|
|
||||||
let Some(package_listing) = state.get_listing(package_id) else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"downloaded package cannot be found in manager--rejecting download!"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let Some(metadata) = &package_listing.metadata else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"downloaded package has no metadata to check validity against!"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
if download_hash != hash {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"downloaded package is not desired version--rejecting download! \
|
|
||||||
download hash: {download_hash}, desired hash: {hash}"
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
(true, Some(metadata.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => match state.get_listing(package_id) {
|
|
||||||
None => {
|
|
||||||
println!("downloaded package cannot be found onchain, proceeding with unverified download");
|
|
||||||
(true, None)
|
|
||||||
}
|
|
||||||
Some(package_listing) => {
|
|
||||||
if let Some(metadata) = &package_listing.metadata {
|
|
||||||
let latest_hash = metadata
|
|
||||||
.properties
|
|
||||||
.code_hashes
|
|
||||||
.get(&metadata.properties.current_version);
|
|
||||||
if Some(&download_hash) != latest_hash {
|
|
||||||
println!(
|
|
||||||
"downloaded package is not latest version \
|
|
||||||
download hash: {download_hash}, latest hash: {latest_hash:?} \
|
|
||||||
proceeding with unverified download"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(true, Some(metadata.clone()))
|
|
||||||
} else {
|
|
||||||
println!("downloaded package has no metadata to check validity against, proceeding with unverified download");
|
|
||||||
(true, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let old_manifest_hash = match state.downloaded_packages.get(package_id) {
|
|
||||||
Some(package_state) => package_state
|
|
||||||
.manifest_hash
|
|
||||||
.clone()
|
|
||||||
.unwrap_or("OLD".to_string()),
|
|
||||||
_ => "OLD".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.add_downloaded_package(
|
|
||||||
package_id,
|
|
||||||
PackageState {
|
|
||||||
mirrored_from: Some(requested_package.from),
|
|
||||||
our_version: download_hash,
|
|
||||||
installed: false,
|
|
||||||
verified,
|
|
||||||
caps_approved: false,
|
|
||||||
manifest_hash: None, // generated in the add fn
|
|
||||||
mirroring: requested_package.mirror,
|
|
||||||
auto_update: requested_package.auto_update,
|
|
||||||
metadata,
|
|
||||||
},
|
|
||||||
Some(blob.bytes),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let new_manifest_hash = match state.downloaded_packages.get(package_id) {
|
|
||||||
Some(package_state) => package_state
|
|
||||||
.manifest_hash
|
|
||||||
.clone()
|
|
||||||
.unwrap_or("NEW".to_string()),
|
|
||||||
_ => "NEW".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// lastly, if auto_update is true, AND the manifest has NOT changed,
|
|
||||||
// trigger install!
|
|
||||||
if requested_package.auto_update && old_manifest_hash == new_manifest_hash {
|
|
||||||
handle_install(state, package_id)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_ft_worker_result(body: &[u8], context: &[u8]) -> anyhow::Result<()> {
|
|
||||||
if let Ok(Resp::FTWorkerResult(ft_worker_result)) = serde_json::from_slice::<Resp>(body) {
|
|
||||||
let context = serde_json::from_slice::<FileTransferContext>(context)?;
|
|
||||||
if let FTWorkerResult::SendSuccess = ft_worker_result {
|
|
||||||
println!(
|
|
||||||
"successfully shared {} in {:.4}s",
|
|
||||||
context.file_name,
|
|
||||||
std::time::SystemTime::now()
|
|
||||||
.duration_since(context.start_time)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs_f64(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("failed to share app"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_eth_sub_event(
|
|
||||||
state: &mut State,
|
|
||||||
event: eth::SubscriptionResult,
|
|
||||||
) -> Result<(), AppStoreLogError> {
|
|
||||||
let eth::SubscriptionResult::Log(log) = event else {
|
|
||||||
return Err(AppStoreLogError::DecodeLogError);
|
|
||||||
};
|
|
||||||
state.ingest_contract_event(*log, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the steps to take an existing package on disk and install/start it
|
|
||||||
/// make sure you have reviewed and approved caps in manifest before calling this
|
|
||||||
pub fn handle_install(state: &mut State, package_id: &PackageId) -> anyhow::Result<()> {
|
|
||||||
// wit version will default to the latest if not specified
|
|
||||||
let metadata = state
|
|
||||||
.get_downloaded_package(package_id)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("package not found in manager"))?
|
|
||||||
.metadata;
|
|
||||||
|
|
||||||
let wit_version = match metadata {
|
|
||||||
Some(metadata) => metadata.properties.wit_version,
|
|
||||||
None => Some(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
utils::install(package_id, &state.our.node, wit_version)?;
|
|
||||||
|
|
||||||
// finally set the package as installed
|
|
||||||
state.update_downloaded_package(package_id, |package_state| {
|
|
||||||
package_state.installed = true;
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -1,41 +1,17 @@
|
|||||||
use crate::VFS_TIMEOUT;
|
use crate::{utils, VFS_TIMEOUT};
|
||||||
use crate::{utils, DownloadRequest, LocalRequest};
|
use kinode_process_lib::{kimap, vfs, PackageId};
|
||||||
use alloy_sol_types::{sol, SolEvent};
|
|
||||||
use kinode_process_lib::kernel_types::Erc721Metadata;
|
|
||||||
use kinode_process_lib::{
|
|
||||||
eth, kernel_types as kt, net, println, vfs, Address, Message, NodeId, PackageId, Request,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
sol! {
|
|
||||||
event AppRegistered(
|
|
||||||
uint256 indexed package,
|
|
||||||
string packageName,
|
|
||||||
bytes publisherName,
|
|
||||||
string metadataUrl,
|
|
||||||
bytes32 metadataHash
|
|
||||||
);
|
|
||||||
event AppMetadataUpdated(
|
|
||||||
uint256 indexed package,
|
|
||||||
string metadataUrl,
|
|
||||||
bytes32 metadataHash
|
|
||||||
);
|
|
||||||
event Transfer(
|
|
||||||
address indexed from,
|
|
||||||
address indexed to,
|
|
||||||
uint256 indexed tokenId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// app store types
|
// main:app_store types
|
||||||
//
|
//
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum AppStoreLogError {
|
pub enum AppStoreLogError {
|
||||||
NoBlockNumber,
|
NoBlockNumber,
|
||||||
DecodeLogError,
|
GetNameError,
|
||||||
|
DecodeLogError(kimap::DecodeLogError),
|
||||||
PackageHashMismatch,
|
PackageHashMismatch,
|
||||||
InvalidPublisherName,
|
InvalidPublisherName,
|
||||||
MetadataNotFound,
|
MetadataNotFound,
|
||||||
@ -47,7 +23,8 @@ impl std::fmt::Display for AppStoreLogError {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
AppStoreLogError::NoBlockNumber => write!(f, "log with no block number"),
|
AppStoreLogError::NoBlockNumber => write!(f, "log with no block number"),
|
||||||
AppStoreLogError::DecodeLogError => write!(f, "error decoding log data"),
|
AppStoreLogError::GetNameError => write!(f, "no corresponding name for namehash found"),
|
||||||
|
AppStoreLogError::DecodeLogError(e) => write!(f, "error decoding log data: {e:?}"),
|
||||||
AppStoreLogError::PackageHashMismatch => write!(f, "mismatched package hash"),
|
AppStoreLogError::PackageHashMismatch => write!(f, "mismatched package hash"),
|
||||||
AppStoreLogError::InvalidPublisherName => write!(f, "invalid publisher name"),
|
AppStoreLogError::InvalidPublisherName => write!(f, "invalid publisher name"),
|
||||||
AppStoreLogError::MetadataNotFound => write!(f, "metadata not found"),
|
AppStoreLogError::MetadataNotFound => write!(f, "metadata not found"),
|
||||||
@ -59,245 +36,71 @@ impl std::fmt::Display for AppStoreLogError {
|
|||||||
|
|
||||||
impl std::error::Error for AppStoreLogError {}
|
impl std::error::Error for AppStoreLogError {}
|
||||||
|
|
||||||
pub type PackageHash = String;
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MirrorCheck {
|
||||||
/// listing information derived from metadata hash in listing event
|
pub node: String,
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
pub is_online: bool,
|
||||||
pub struct PackageListing {
|
pub error: Option<String>,
|
||||||
pub owner: String, // eth address
|
|
||||||
pub name: String,
|
|
||||||
pub publisher: NodeId,
|
|
||||||
pub metadata_url: String,
|
|
||||||
pub metadata_hash: String,
|
|
||||||
pub metadata: Option<kt::Erc721Metadata>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct RequestedPackage {
|
|
||||||
pub from: NodeId,
|
|
||||||
pub mirror: bool,
|
|
||||||
pub auto_update: bool,
|
|
||||||
// if none, we're requesting the latest version onchain
|
|
||||||
pub desired_version_hash: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// state of an individual package we have downloaded
|
/// state of an individual package we have downloaded
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct PackageState {
|
pub struct PackageState {
|
||||||
/// the node we last downloaded the package from
|
/// the version of the package we have installed
|
||||||
/// this is "us" if we don't know the source (usually cause it's a local install)
|
pub our_version_hash: String,
|
||||||
pub mirrored_from: Option<NodeId>,
|
|
||||||
/// the version of the package we have downloaded
|
|
||||||
pub our_version: String,
|
|
||||||
pub installed: bool,
|
|
||||||
pub verified: bool,
|
pub verified: bool,
|
||||||
pub caps_approved: bool,
|
pub caps_approved: bool,
|
||||||
/// the hash of the manifest file, which is used to determine whether package
|
/// the hash of the manifest, which is used to determine whether package
|
||||||
/// capabilities have changed. if they have changed, auto-install must fail
|
/// capabilities have changed. if they have changed, auto-install must fail
|
||||||
/// and the user must approve the new capabilities.
|
/// and the user must approve the new capabilities.
|
||||||
pub manifest_hash: Option<String>,
|
pub manifest_hash: Option<String>,
|
||||||
/// are we serving this package to others?
|
|
||||||
pub mirroring: bool,
|
|
||||||
/// if we get a listing data update, will we try to download it?
|
|
||||||
pub auto_update: bool,
|
|
||||||
pub metadata: Option<kt::Erc721Metadata>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// this process's saved state
|
/// this process's saved state
|
||||||
pub struct State {
|
pub struct State {
|
||||||
/// our address, grabbed from init()
|
/// packages we have installed
|
||||||
pub our: Address,
|
pub packages: HashMap<PackageId, PackageState>,
|
||||||
/// the eth provider we are using -- not persisted
|
|
||||||
pub provider: eth::Provider,
|
|
||||||
/// the address of the contract we are using to read package listings
|
|
||||||
pub contract_address: String,
|
|
||||||
/// the last block at which we saved the state of the listings to disk.
|
|
||||||
/// when we boot, we can read logs starting from this block and
|
|
||||||
/// rebuild latest state.
|
|
||||||
pub last_saved_block: u64,
|
|
||||||
pub package_hashes: HashMap<PackageId, PackageHash>,
|
|
||||||
/// we keep the full state of the package manager here, calculated from
|
|
||||||
/// the listings contract logs. in the future, we'll offload this and
|
|
||||||
/// only track a certain number of packages...
|
|
||||||
pub listed_packages: HashMap<PackageHash, PackageListing>,
|
|
||||||
/// we keep the full state of the packages we have downloaded here.
|
|
||||||
/// in order to keep this synchronized with our filesystem, we will
|
|
||||||
/// ingest apps on disk if we have to rebuild our state. this is also
|
|
||||||
/// updated every time we download, create, or uninstall a package.
|
|
||||||
pub downloaded_packages: HashMap<PackageId, PackageState>,
|
|
||||||
/// the APIs we have
|
/// the APIs we have
|
||||||
pub downloaded_apis: HashSet<PackageId>,
|
pub installed_apis: HashSet<PackageId>,
|
||||||
/// the packages we have outstanding requests to download (not persisted)
|
|
||||||
pub requested_packages: HashMap<PackageId, RequestedPackage>,
|
|
||||||
/// the APIs we have outstanding requests to download (not persisted)
|
|
||||||
pub requested_apis: HashMap<PackageId, RequestedPackage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct SerializedState {
|
|
||||||
pub contract_address: String,
|
|
||||||
pub last_saved_block: u64,
|
|
||||||
pub package_hashes: HashMap<PackageId, PackageHash>,
|
|
||||||
pub listed_packages: HashMap<PackageHash, PackageListing>,
|
|
||||||
pub downloaded_packages: HashMap<PackageId, PackageState>,
|
|
||||||
pub downloaded_apis: HashSet<PackageId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for State {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
use serde::ser::SerializeStruct;
|
|
||||||
let mut state = serializer.serialize_struct("State", 6)?;
|
|
||||||
state.serialize_field("contract_address", &self.contract_address)?;
|
|
||||||
state.serialize_field("last_saved_block", &self.last_saved_block)?;
|
|
||||||
state.serialize_field("package_hashes", &self.package_hashes)?;
|
|
||||||
state.serialize_field("listed_packages", &self.listed_packages)?;
|
|
||||||
state.serialize_field("downloaded_packages", &self.downloaded_packages)?;
|
|
||||||
state.serialize_field("downloaded_apis", &self.downloaded_apis)?;
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn from_serialized(our: Address, provider: eth::Provider, s: SerializedState) -> Self {
|
/// To load state, we populate the downloaded_packages map
|
||||||
State {
|
|
||||||
our,
|
|
||||||
provider,
|
|
||||||
contract_address: s.contract_address,
|
|
||||||
last_saved_block: s.last_saved_block,
|
|
||||||
package_hashes: s.package_hashes,
|
|
||||||
listed_packages: s.listed_packages,
|
|
||||||
downloaded_packages: s.downloaded_packages,
|
|
||||||
downloaded_apis: s.downloaded_apis,
|
|
||||||
requested_packages: HashMap::new(),
|
|
||||||
requested_apis: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// To create a new state, we populate the downloaded_packages map
|
|
||||||
/// with all packages parseable from our filesystem.
|
/// with all packages parseable from our filesystem.
|
||||||
pub fn new(
|
pub fn load() -> anyhow::Result<Self> {
|
||||||
our: Address,
|
|
||||||
provider: eth::Provider,
|
|
||||||
contract_address: String,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let mut state = State {
|
let mut state = State {
|
||||||
our,
|
packages: HashMap::new(),
|
||||||
provider,
|
installed_apis: HashSet::new(),
|
||||||
contract_address,
|
|
||||||
last_saved_block: crate::CONTRACT_FIRST_BLOCK,
|
|
||||||
package_hashes: HashMap::new(),
|
|
||||||
listed_packages: HashMap::new(),
|
|
||||||
downloaded_packages: HashMap::new(),
|
|
||||||
downloaded_apis: HashSet::new(),
|
|
||||||
requested_packages: HashMap::new(),
|
|
||||||
requested_apis: HashMap::new(),
|
|
||||||
};
|
};
|
||||||
state.populate_packages_from_filesystem()?;
|
state.populate_packages_from_filesystem()?;
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_listing(&self, package_id: &PackageId) -> Option<&PackageListing> {
|
|
||||||
self.listed_packages
|
|
||||||
.get(self.package_hashes.get(package_id)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_listing_with_hash_mut(
|
|
||||||
&mut self,
|
|
||||||
package_hash: &PackageHash,
|
|
||||||
) -> Option<&mut PackageListing> {
|
|
||||||
self.listed_packages.get_mut(package_hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_downloaded_package(&self, package_id: &PackageId) -> Option<PackageState> {
|
|
||||||
self.downloaded_packages.get(package_id).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_downloaded_package(
|
|
||||||
&mut self,
|
|
||||||
package_id: &PackageId,
|
|
||||||
mut package_state: PackageState,
|
|
||||||
package_bytes: Option<Vec<u8>>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if let Some(package_bytes) = package_bytes {
|
|
||||||
let manifest_hash = utils::create_package_drive(package_id, package_bytes)?;
|
|
||||||
package_state.manifest_hash = Some(manifest_hash);
|
|
||||||
}
|
|
||||||
if utils::extract_api(package_id)? {
|
|
||||||
self.downloaded_apis.insert(package_id.to_owned());
|
|
||||||
}
|
|
||||||
self.downloaded_packages
|
|
||||||
.insert(package_id.to_owned(), package_state);
|
|
||||||
kinode_process_lib::set_state(&serde_json::to_vec(self)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns True if the package was found and updated, False otherwise
|
|
||||||
pub fn update_downloaded_package(
|
|
||||||
&mut self,
|
|
||||||
package_id: &PackageId,
|
|
||||||
fn_: impl FnOnce(&mut PackageState),
|
|
||||||
) -> bool {
|
|
||||||
let res = self
|
|
||||||
.downloaded_packages
|
|
||||||
.get_mut(package_id)
|
|
||||||
.map(|package_state| {
|
|
||||||
fn_(package_state);
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
kinode_process_lib::set_state(&serde_json::to_vec(self).unwrap());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_mirroring(&mut self, package_id: &PackageId) -> bool {
|
|
||||||
self.update_downloaded_package(package_id, |package_state| {
|
|
||||||
package_state.mirroring = true;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop_mirroring(&mut self, package_id: &PackageId) -> bool {
|
|
||||||
self.update_downloaded_package(package_id, |package_state| {
|
|
||||||
package_state.mirroring = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_auto_update(&mut self, package_id: &PackageId) -> bool {
|
|
||||||
self.update_downloaded_package(package_id, |package_state| {
|
|
||||||
package_state.auto_update = true;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop_auto_update(&mut self, package_id: &PackageId) -> bool {
|
|
||||||
self.update_downloaded_package(package_id, |package_state| {
|
|
||||||
package_state.auto_update = false;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// saves state
|
/// saves state
|
||||||
pub fn populate_packages_from_filesystem(&mut self) -> anyhow::Result<()> {
|
pub fn populate_packages_from_filesystem(&mut self) -> anyhow::Result<()> {
|
||||||
let Message::Response { body, .. } = Request::to(("our", "vfs", "distro", "sys"))
|
// call VFS and ask for all directories in our root drive
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
// (we have root VFS capability so this is allowed)
|
||||||
path: "/".to_string(),
|
// we will interpret any that are package dirs and ingest them
|
||||||
action: vfs::VfsAction::ReadDir,
|
let vfs::VfsResponse::ReadDir(entries) = serde_json::from_slice::<vfs::VfsResponse>(
|
||||||
})?)
|
utils::vfs_request("/", vfs::VfsAction::ReadDir)
|
||||||
.send_and_await_response(VFS_TIMEOUT)??
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
|
.body(),
|
||||||
|
)?
|
||||||
else {
|
else {
|
||||||
return Err(anyhow::anyhow!("vfs: bad response"));
|
return Err(anyhow::anyhow!("vfs: unexpected response to ReadDir"));
|
||||||
};
|
|
||||||
let response = serde_json::from_slice::<vfs::VfsResponse>(&body)?;
|
|
||||||
let vfs::VfsResponse::ReadDir(entries) = response else {
|
|
||||||
return Err(anyhow::anyhow!("vfs: unexpected response: {:?}", response));
|
|
||||||
};
|
};
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
|
// ignore non-dirs
|
||||||
|
if entry.file_type != vfs::FileType::Directory {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// ignore non-package dirs
|
// ignore non-package dirs
|
||||||
let Ok(package_id) = entry.path.parse::<PackageId>() else {
|
let Ok(package_id) = entry.path.parse::<PackageId>() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if entry.file_type == vfs::FileType::Directory {
|
// grab package .zip if it exists
|
||||||
let zip_file = vfs::File {
|
let zip_file = vfs::File {
|
||||||
path: format!("/{}/pkg/{}.zip", package_id, package_id),
|
path: format!("/{package_id}/pkg/{package_id}.zip"),
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
};
|
};
|
||||||
let Ok(zip_file_bytes) = zip_file.read() else {
|
let Ok(zip_file_bytes) = zip_file.read() else {
|
||||||
@ -305,281 +108,30 @@ impl State {
|
|||||||
};
|
};
|
||||||
// generate entry from this data
|
// generate entry from this data
|
||||||
// for the version hash, take the SHA-256 hash of the zip file
|
// for the version hash, take the SHA-256 hash of the zip file
|
||||||
let our_version = utils::generate_version_hash(&zip_file_bytes);
|
let our_version_hash = utils::sha_256_hash(&zip_file_bytes);
|
||||||
let manifest_file = vfs::File {
|
let manifest_file = vfs::File {
|
||||||
path: format!("/{}/pkg/manifest.json", package_id),
|
path: format!("/{package_id}/pkg/manifest.json"),
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
};
|
};
|
||||||
let manifest_bytes = manifest_file.read()?;
|
let manifest_bytes = manifest_file.read()?;
|
||||||
// the user will need to turn mirroring and auto-update back on if they
|
let manifest_hash = utils::keccak_256_hash(&manifest_bytes);
|
||||||
// have to reset the state of their app store for some reason. the apps
|
self.packages.insert(
|
||||||
// themselves will remain on disk unless explicitly deleted.
|
package_id.clone(),
|
||||||
self.add_downloaded_package(
|
|
||||||
&package_id,
|
|
||||||
PackageState {
|
PackageState {
|
||||||
mirrored_from: None,
|
our_version_hash,
|
||||||
our_version,
|
|
||||||
installed: true,
|
|
||||||
verified: true, // implicitly verified (TODO re-evaluate)
|
verified: true, // implicitly verified (TODO re-evaluate)
|
||||||
caps_approved: false, // must re-approve if you want to do something
|
caps_approved: false, // must re-approve if you want to do something ??
|
||||||
manifest_hash: Some(utils::generate_metadata_hash(&manifest_bytes)),
|
manifest_hash: Some(manifest_hash),
|
||||||
mirroring: false,
|
|
||||||
auto_update: false,
|
|
||||||
metadata: None,
|
|
||||||
},
|
},
|
||||||
None,
|
);
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Ok(Ok(_)) = Request::new()
|
if let Ok(Ok(_)) =
|
||||||
.target(("our", "vfs", "distro", "sys"))
|
utils::vfs_request(format!("/{package_id}/pkg/api"), vfs::VfsAction::Metadata)
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: format!("/{package_id}/pkg/api"),
|
|
||||||
action: vfs::VfsAction::Metadata,
|
|
||||||
})
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)
|
.send_and_await_response(VFS_TIMEOUT)
|
||||||
{
|
{
|
||||||
self.downloaded_apis.insert(package_id.to_owned());
|
self.installed_apis.insert(package_id);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uninstall(&mut self, package_id: &PackageId) -> anyhow::Result<()> {
|
|
||||||
utils::uninstall(package_id)?;
|
|
||||||
self.downloaded_packages.remove(package_id);
|
|
||||||
kinode_process_lib::set_state(&serde_json::to_vec(self)?);
|
|
||||||
println!("uninstalled {package_id}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// saves state
|
|
||||||
///
|
|
||||||
/// only saves the onchain data in our package listings --
|
|
||||||
/// in order to fetch metadata and trigger auto-update for all packages,
|
|
||||||
/// call [`State::update_listings`], or call this with `true` as the third argument.
|
|
||||||
pub fn ingest_contract_event(
|
|
||||||
&mut self,
|
|
||||||
log: eth::Log,
|
|
||||||
update_listings: bool,
|
|
||||||
) -> Result<(), AppStoreLogError> {
|
|
||||||
let block_number: u64 = log.block_number.ok_or(AppStoreLogError::NoBlockNumber)?;
|
|
||||||
|
|
||||||
match log.topics()[0] {
|
|
||||||
AppRegistered::SIGNATURE_HASH => {
|
|
||||||
let app = AppRegistered::decode_log_data(log.data(), false)
|
|
||||||
.map_err(|_| AppStoreLogError::DecodeLogError)?;
|
|
||||||
let package_name = app.packageName;
|
|
||||||
let publisher_dnswire = app.publisherName;
|
|
||||||
let metadata_url = app.metadataUrl;
|
|
||||||
let metadata_hash = app.metadataHash;
|
|
||||||
|
|
||||||
let package_hash = log.topics()[1].to_string();
|
|
||||||
let metadata_hash = metadata_hash.to_string();
|
|
||||||
|
|
||||||
kinode_process_lib::print_to_terminal(
|
|
||||||
1,
|
|
||||||
&format!("new package {package_name} registered onchain"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if utils::generate_package_hash(&package_name, &publisher_dnswire) != package_hash {
|
|
||||||
return Err(AppStoreLogError::PackageHashMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(publisher_name) = net::dnswire_decode(&publisher_dnswire) else {
|
|
||||||
return Err(AppStoreLogError::InvalidPublisherName);
|
|
||||||
};
|
|
||||||
|
|
||||||
let metadata = if update_listings {
|
|
||||||
let metadata =
|
|
||||||
utils::fetch_metadata_from_url(&metadata_url, &metadata_hash, 5)?;
|
|
||||||
if metadata.properties.publisher != publisher_name {
|
|
||||||
return Err(AppStoreLogError::PublisherNameMismatch);
|
|
||||||
}
|
|
||||||
Some(metadata)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
self.package_hashes.insert(
|
|
||||||
PackageId::new(&package_name, &publisher_name),
|
|
||||||
package_hash.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
match self.listed_packages.entry(package_hash) {
|
|
||||||
std::collections::hash_map::Entry::Occupied(mut listing) => {
|
|
||||||
let listing = listing.get_mut();
|
|
||||||
listing.name = package_name;
|
|
||||||
listing.publisher = publisher_name;
|
|
||||||
listing.metadata_url = metadata_url;
|
|
||||||
listing.metadata_hash = metadata_hash;
|
|
||||||
listing.metadata = metadata;
|
|
||||||
}
|
|
||||||
std::collections::hash_map::Entry::Vacant(listing) => {
|
|
||||||
listing.insert(PackageListing {
|
|
||||||
owner: "".to_string(),
|
|
||||||
name: package_name,
|
|
||||||
publisher: publisher_name,
|
|
||||||
metadata_url,
|
|
||||||
metadata_hash,
|
|
||||||
metadata,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
AppMetadataUpdated::SIGNATURE_HASH => {
|
|
||||||
let upd = AppMetadataUpdated::decode_log_data(log.data(), false)
|
|
||||||
.map_err(|_| AppStoreLogError::DecodeLogError)?;
|
|
||||||
let metadata_url = upd.metadataUrl;
|
|
||||||
let metadata_hash = upd.metadataHash;
|
|
||||||
|
|
||||||
let package_hash = log.topics()[1].to_string();
|
|
||||||
let metadata_hash = metadata_hash.to_string();
|
|
||||||
|
|
||||||
let Some(current_listing) =
|
|
||||||
self.get_listing_with_hash_mut(&package_hash.to_string())
|
|
||||||
else {
|
|
||||||
// package not found, so we can't update it
|
|
||||||
// this will never happen if we're ingesting logs in order
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let metadata = if update_listings {
|
|
||||||
Some(utils::fetch_metadata_from_url(
|
|
||||||
&metadata_url,
|
|
||||||
&metadata_hash,
|
|
||||||
5,
|
|
||||||
)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
current_listing.metadata_url = metadata_url;
|
|
||||||
current_listing.metadata_hash = metadata_hash;
|
|
||||||
|
|
||||||
if update_listings {
|
|
||||||
current_listing.metadata = metadata.clone();
|
|
||||||
let package_id =
|
|
||||||
PackageId::new(¤t_listing.name, ¤t_listing.publisher);
|
|
||||||
if let Some(package_state) = self.downloaded_packages.get(&package_id) {
|
|
||||||
auto_update(&self.our, package_id, &metadata.unwrap(), &package_state);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current_listing.metadata = metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Transfer::SIGNATURE_HASH => {
|
|
||||||
let from = alloy_primitives::Address::from_word(log.topics()[1]);
|
|
||||||
let to = alloy_primitives::Address::from_word(log.topics()[2]);
|
|
||||||
let package_hash = log.topics()[3].to_string();
|
|
||||||
|
|
||||||
if from == alloy_primitives::Address::ZERO {
|
|
||||||
// this is a new package, set the owner
|
|
||||||
match self.listed_packages.entry(package_hash) {
|
|
||||||
std::collections::hash_map::Entry::Occupied(mut listing) => {
|
|
||||||
let listing = listing.get_mut();
|
|
||||||
listing.owner = to.to_string();
|
|
||||||
}
|
|
||||||
std::collections::hash_map::Entry::Vacant(listing) => {
|
|
||||||
listing.insert(PackageListing {
|
|
||||||
owner: to.to_string(),
|
|
||||||
name: "".to_string(),
|
|
||||||
publisher: "".to_string(),
|
|
||||||
metadata_url: "".to_string(),
|
|
||||||
metadata_hash: "".to_string(),
|
|
||||||
metadata: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if to == alloy_primitives::Address::ZERO {
|
|
||||||
// this is a package deletion
|
|
||||||
if let Some(old) = self.listed_packages.remove(&package_hash) {
|
|
||||||
self.package_hashes
|
|
||||||
.remove(&PackageId::new(&old.name, &old.publisher));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let Some(listing) = self.get_listing_with_hash_mut(&package_hash) else {
|
|
||||||
// package not found, so we can't update it
|
|
||||||
// this will never happen if we're ingesting logs in order
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
listing.owner = to.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.last_saved_block = block_number;
|
|
||||||
if update_listings {
|
|
||||||
kinode_process_lib::set_state(&serde_json::to_vec(self).unwrap());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// iterate through all package listings and try to fetch metadata.
|
|
||||||
/// this is done after ingesting a bunch of logs to remove fetches
|
|
||||||
/// of stale metadata.
|
|
||||||
pub fn update_listings(&mut self) {
|
|
||||||
for (_package_hash, listing) in self.listed_packages.iter_mut() {
|
|
||||||
if listing.metadata.is_none() {
|
|
||||||
if let Ok(metadata) =
|
|
||||||
utils::fetch_metadata_from_url(&listing.metadata_url, &listing.metadata_hash, 5)
|
|
||||||
{
|
|
||||||
let package_id = PackageId::new(&listing.name, &listing.publisher);
|
|
||||||
if let Some(package_state) = self.downloaded_packages.get(&package_id) {
|
|
||||||
auto_update(&self.our, package_id, &metadata, &package_state);
|
|
||||||
}
|
|
||||||
listing.metadata = Some(metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
kinode_process_lib::set_state(&serde_json::to_vec(self).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// if we have this app installed, and we have auto_update set to true,
|
|
||||||
/// we should try to download new version from the mirrored_from node
|
|
||||||
/// and install it if successful.
|
|
||||||
fn auto_update(
|
|
||||||
our: &Address,
|
|
||||||
package_id: PackageId,
|
|
||||||
metadata: &Erc721Metadata,
|
|
||||||
package_state: &PackageState,
|
|
||||||
) {
|
|
||||||
if package_state.auto_update {
|
|
||||||
let latest_version_hash = metadata
|
|
||||||
.properties
|
|
||||||
.code_hashes
|
|
||||||
.get(&metadata.properties.current_version);
|
|
||||||
if let Some(mirrored_from) = &package_state.mirrored_from
|
|
||||||
&& Some(&package_state.our_version) != latest_version_hash
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
"auto-updating package {package_id} from {} to {} using mirror {mirrored_from}",
|
|
||||||
metadata
|
|
||||||
.properties
|
|
||||||
.code_hashes
|
|
||||||
.get(&package_state.our_version)
|
|
||||||
.unwrap_or(&package_state.our_version),
|
|
||||||
metadata.properties.current_version,
|
|
||||||
);
|
|
||||||
Request::to(our)
|
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&LocalRequest::Download(DownloadRequest {
|
|
||||||
package_id: crate::kinode::process::main::PackageId::from_process_lib(
|
|
||||||
package_id,
|
|
||||||
),
|
|
||||||
download_from: mirrored_from.clone(),
|
|
||||||
mirror: package_state.mirroring,
|
|
||||||
auto_update: package_state.auto_update,
|
|
||||||
desired_version_hash: None,
|
|
||||||
}))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
use {
|
use {
|
||||||
crate::kinode::process::main::OnchainMetadata,
|
crate::{
|
||||||
crate::state::{AppStoreLogError, PackageState, SerializedState, State},
|
kinode::process::{
|
||||||
crate::{CONTRACT_ADDRESS, EVENTS, VFS_TIMEOUT},
|
chain::{ChainRequests, ChainResponses, OnchainMetadata},
|
||||||
kinode_process_lib::{
|
downloads::{AddDownloadRequest, DownloadRequests, DownloadResponses},
|
||||||
eth, get_blob, get_state, http, kernel_types as kt, println, vfs, Address, LazyLoadBlob,
|
|
||||||
PackageId, ProcessId, Request,
|
|
||||||
},
|
},
|
||||||
std::collections::HashSet,
|
state::{PackageState, State},
|
||||||
std::str::FromStr,
|
VFS_TIMEOUT,
|
||||||
|
},
|
||||||
|
kinode_process_lib::{
|
||||||
|
get_blob, kernel_types as kt, println, vfs, Address, LazyLoadBlob, PackageId, ProcessId,
|
||||||
|
Request,
|
||||||
|
},
|
||||||
|
std::collections::{HashMap, HashSet},
|
||||||
};
|
};
|
||||||
|
|
||||||
// quite annoyingly, we must convert from our gen'd version of PackageId
|
// quite annoyingly, we must convert from our gen'd version of PackageId
|
||||||
@ -28,157 +32,31 @@ impl crate::kinode::process::main::PackageId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// less annoying but still bad
|
/// generate a Keccak-256 hash string (with 0x prefix) of the metadata bytes
|
||||||
impl OnchainMetadata {
|
pub fn keccak_256_hash(bytes: &[u8]) -> String {
|
||||||
pub fn to_erc721_metadata(self) -> kt::Erc721Metadata {
|
|
||||||
use kt::Erc721Properties;
|
|
||||||
kt::Erc721Metadata {
|
|
||||||
name: self.name,
|
|
||||||
description: self.description,
|
|
||||||
image: self.image,
|
|
||||||
external_url: self.external_url,
|
|
||||||
animation_url: self.animation_url,
|
|
||||||
properties: Erc721Properties {
|
|
||||||
package_name: self.properties.package_name,
|
|
||||||
publisher: self.properties.publisher,
|
|
||||||
current_version: self.properties.current_version,
|
|
||||||
mirrors: self.properties.mirrors,
|
|
||||||
code_hashes: self.properties.code_hashes.into_iter().collect(),
|
|
||||||
license: self.properties.license,
|
|
||||||
screenshots: self.properties.screenshots,
|
|
||||||
wit_version: self.properties.wit_version,
|
|
||||||
dependencies: self.properties.dependencies,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// fetch state from disk or create a new one if that fails
|
|
||||||
pub fn fetch_state(our: Address, provider: eth::Provider) -> State {
|
|
||||||
if let Some(state_bytes) = get_state() {
|
|
||||||
match serde_json::from_slice::<SerializedState>(&state_bytes) {
|
|
||||||
Ok(state) => {
|
|
||||||
if state.contract_address == CONTRACT_ADDRESS {
|
|
||||||
return State::from_serialized(our, provider, state);
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
"state contract address mismatch! expected {}, got {}",
|
|
||||||
CONTRACT_ADDRESS, state.contract_address
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => println!("failed to deserialize saved state: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::new(our, provider, CONTRACT_ADDRESS.to_string()).expect("state creation failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app_store_filter(state: &State) -> eth::Filter {
|
|
||||||
eth::Filter::new()
|
|
||||||
.address(eth::Address::from_str(&state.contract_address).unwrap())
|
|
||||||
.from_block(state.last_saved_block)
|
|
||||||
.events(EVENTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// create a filter to fetch app store event logs from chain and subscribe to new events
|
|
||||||
pub fn fetch_and_subscribe_logs(state: &mut State) {
|
|
||||||
let filter = app_store_filter(state);
|
|
||||||
// get past logs, subscribe to new ones.
|
|
||||||
for log in fetch_logs(&state.provider, &filter) {
|
|
||||||
if let Err(e) = state.ingest_contract_event(log, false) {
|
|
||||||
println!("error ingesting log: {e:?}");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
state.update_listings();
|
|
||||||
subscribe_to_logs(&state.provider, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// subscribe to logs from the chain with a given filter
|
|
||||||
pub fn subscribe_to_logs(eth_provider: ð::Provider, filter: eth::Filter) {
|
|
||||||
loop {
|
|
||||||
match eth_provider.subscribe(1, filter.clone()) {
|
|
||||||
Ok(()) => break,
|
|
||||||
Err(_) => {
|
|
||||||
println!("failed to subscribe to chain! trying again in 5s...");
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("subscribed to logs successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// fetch logs from the chain with a given filter
|
|
||||||
fn fetch_logs(eth_provider: ð::Provider, filter: ð::Filter) -> Vec<eth::Log> {
|
|
||||||
loop {
|
|
||||||
match eth_provider.get_logs(filter) {
|
|
||||||
Ok(res) => return res,
|
|
||||||
Err(_) => {
|
|
||||||
println!("failed to fetch logs! trying again in 5s...");
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// fetch metadata from url and verify it matches metadata_hash
|
|
||||||
pub fn fetch_metadata_from_url(
|
|
||||||
metadata_url: &str,
|
|
||||||
metadata_hash: &str,
|
|
||||||
timeout: u64,
|
|
||||||
) -> Result<kt::Erc721Metadata, AppStoreLogError> {
|
|
||||||
if let Ok(url) = url::Url::parse(metadata_url) {
|
|
||||||
if let Ok(_) =
|
|
||||||
http::send_request_await_response(http::Method::GET, url, None, timeout, vec![])
|
|
||||||
{
|
|
||||||
if let Some(body) = get_blob() {
|
|
||||||
let hash = generate_metadata_hash(&body.bytes);
|
|
||||||
if &hash == metadata_hash {
|
|
||||||
return Ok(serde_json::from_slice::<kt::Erc721Metadata>(&body.bytes)
|
|
||||||
.map_err(|_| AppStoreLogError::MetadataNotFound)?);
|
|
||||||
} else {
|
|
||||||
return Err(AppStoreLogError::MetadataHashMismatch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(AppStoreLogError::MetadataNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generate a Keccak-256 hash of the metadata bytes
|
|
||||||
pub fn generate_metadata_hash(metadata: &[u8]) -> String {
|
|
||||||
use sha3::{Digest, Keccak256};
|
use sha3::{Digest, Keccak256};
|
||||||
let mut hasher = Keccak256::new();
|
let mut hasher = Keccak256::new();
|
||||||
hasher.update(metadata);
|
hasher.update(bytes);
|
||||||
format!("0x{:x}", hasher.finalize())
|
format!("0x{:x}", hasher.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// generate a Keccak-256 hash of the package name and publisher (match onchain)
|
|
||||||
pub fn generate_package_hash(name: &str, publisher_dnswire: &[u8]) -> String {
|
|
||||||
use sha3::{Digest, Keccak256};
|
|
||||||
let mut hasher = Keccak256::new();
|
|
||||||
hasher.update([name.as_bytes(), publisher_dnswire].concat());
|
|
||||||
let hash = hasher.finalize();
|
|
||||||
format!("0x{:x}", hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// generate a SHA-256 hash of the zip bytes to act as a version hash
|
/// generate a SHA-256 hash of the zip bytes to act as a version hash
|
||||||
pub fn generate_version_hash(zip_bytes: &[u8]) -> String {
|
pub fn sha_256_hash(bytes: &[u8]) -> String {
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(zip_bytes);
|
hasher.update(bytes);
|
||||||
format!("{:x}", hasher.finalize())
|
format!("{:x}", hasher.finalize())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// note: this can only be called in the install process,
|
||||||
|
/// manifest.json for an arbitrary download can be found with GetFiles
|
||||||
pub fn fetch_package_manifest(
|
pub fn fetch_package_manifest(
|
||||||
package_id: &PackageId,
|
package_id: &PackageId,
|
||||||
) -> anyhow::Result<Vec<kt::PackageManifestEntry>> {
|
) -> anyhow::Result<Vec<kt::PackageManifestEntry>> {
|
||||||
Request::to(("our", "vfs", "distro", "sys"))
|
vfs_request(
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
format!("/{package_id}/pkg/manifest.json"),
|
||||||
path: format!("/{package_id}/pkg/manifest.json"),
|
vfs::VfsAction::Read,
|
||||||
action: vfs::VfsAction::Read,
|
)
|
||||||
})?)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??;
|
||||||
let Some(blob) = get_blob() else {
|
let Some(blob) = get_blob() else {
|
||||||
return Err(anyhow::anyhow!("no blob"));
|
return Err(anyhow::anyhow!("no blob"));
|
||||||
@ -188,52 +66,66 @@ pub fn fetch_package_manifest(
|
|||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fetch_package_metadata(
|
||||||
|
package_id: &crate::kinode::process::main::PackageId,
|
||||||
|
) -> anyhow::Result<OnchainMetadata> {
|
||||||
|
let resp = Request::to(("our", "chain", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&ChainRequests::GetApp(package_id.clone())).unwrap())
|
||||||
|
.send_and_await_response(5)??;
|
||||||
|
|
||||||
|
let resp = serde_json::from_slice::<ChainResponses>(&resp.body())?;
|
||||||
|
let app = match resp {
|
||||||
|
ChainResponses::GetApp(Some(app)) => app,
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"No app data found in response from chain:app_store:sys"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let metadata = match app.metadata {
|
||||||
|
Some(metadata) => metadata,
|
||||||
|
None => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"No metadata found in response from chain:app_store:sys"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_package(
|
pub fn new_package(
|
||||||
package_id: &PackageId,
|
package_id: crate::kinode::process::main::PackageId,
|
||||||
state: &mut State,
|
|
||||||
metadata: kt::Erc721Metadata,
|
|
||||||
mirror: bool,
|
mirror: bool,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// set the version hash for this new local package
|
// set the version hash for this new local package
|
||||||
let our_version = generate_version_hash(&bytes);
|
let version_hash = sha_256_hash(&bytes);
|
||||||
|
|
||||||
let package_state = PackageState {
|
let resp = Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
mirrored_from: Some(state.our.node.clone()),
|
.body(serde_json::to_vec(&DownloadRequests::AddDownload(
|
||||||
our_version,
|
AddDownloadRequest {
|
||||||
installed: false,
|
package_id: package_id.clone(),
|
||||||
verified: true, // side loaded apps are implicitly verified because there is no "source" to verify against
|
version_hash: version_hash.clone(),
|
||||||
caps_approved: true, // TODO see if we want to auto-approve local installs
|
mirror,
|
||||||
manifest_hash: None, // generated in the add fn
|
},
|
||||||
mirroring: mirror,
|
))?)
|
||||||
auto_update: false, // can't auto-update a local package
|
.blob_bytes(bytes)
|
||||||
metadata: Some(metadata),
|
.send_and_await_response(5)??;
|
||||||
};
|
|
||||||
let Ok(()) = state.add_downloaded_package(&package_id, package_state, Some(bytes)) else {
|
|
||||||
return Err(anyhow::anyhow!("failed to add package"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let drive_path = format!("/{package_id}/pkg");
|
let download_resp = serde_json::from_slice::<DownloadResponses>(&resp.body())?;
|
||||||
let result = Request::new()
|
|
||||||
.target(("our", "vfs", "distro", "sys"))
|
match download_resp {
|
||||||
.body(
|
DownloadResponses::Error(e) => {
|
||||||
serde_json::to_vec(&vfs::VfsRequest {
|
return Err(anyhow::anyhow!("failed to add download: {:?}", e));
|
||||||
path: format!("{}/api", drive_path),
|
}
|
||||||
action: vfs::VfsAction::Metadata,
|
_ => {}
|
||||||
})
|
}
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT);
|
|
||||||
if let Ok(Ok(_)) = result {
|
|
||||||
state.downloaded_apis.insert(package_id.to_owned());
|
|
||||||
};
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// create a new package drive in VFS and add the package zip to it.
|
/// create a new package drive in VFS and add the package zip to it.
|
||||||
/// if an `api.zip` is present, unzip and stow in `/api`.
|
/// if an `api.zip` is present, unzip and stow in `/api`.
|
||||||
/// returns a string representing the manifest hash of the package
|
/// returns a string representing the manfifest hash.
|
||||||
/// and a bool returning whether or not an api was found and unzipped.
|
|
||||||
pub fn create_package_drive(
|
pub fn create_package_drive(
|
||||||
package_id: &PackageId,
|
package_id: &PackageId,
|
||||||
package_bytes: Vec<u8>,
|
package_bytes: Vec<u8>,
|
||||||
@ -246,44 +138,32 @@ pub fn create_package_drive(
|
|||||||
|
|
||||||
// create a new drive for this package in VFS
|
// create a new drive for this package in VFS
|
||||||
// this is possible because we have root access
|
// this is possible because we have root access
|
||||||
Request::to(("our", "vfs", "distro", "sys"))
|
vfs_request(drive_name.clone(), vfs::VfsAction::CreateDrive)
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: drive_name.clone(),
|
|
||||||
action: vfs::VfsAction::CreateDrive,
|
|
||||||
})?)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??;
|
||||||
|
|
||||||
// DELETE the /pkg folder in the package drive
|
// DELETE the /pkg folder in the package drive
|
||||||
// in order to replace with the fresh one
|
// in order to replace with the fresh one
|
||||||
Request::to(("our", "vfs", "distro", "sys"))
|
vfs_request(drive_name.clone(), vfs::VfsAction::RemoveDirAll)
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: drive_name.clone(),
|
|
||||||
action: vfs::VfsAction::RemoveDirAll,
|
|
||||||
})?)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??;
|
||||||
|
|
||||||
// convert the zip to a new package drive
|
// convert the zip to a new package drive
|
||||||
let response = Request::to(("our", "vfs", "distro", "sys"))
|
let vfs::VfsResponse::Ok = serde_json::from_slice::<vfs::VfsResponse>(
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
vfs_request(drive_name.clone(), vfs::VfsAction::AddZip)
|
||||||
path: drive_name.clone(),
|
|
||||||
action: vfs::VfsAction::AddZip,
|
|
||||||
})?)
|
|
||||||
.blob(blob.clone())
|
.blob(blob.clone())
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
let vfs::VfsResponse::Ok = serde_json::from_slice::<vfs::VfsResponse>(response.body())? else {
|
.body(),
|
||||||
|
)?
|
||||||
|
else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"cannot add NewPackage: do not have capability to access vfs"
|
"cannot add NewPackage: do not have capability to access vfs"
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// be careful, this is technically a duplicate.. but..
|
||||||
// save the zip file itself in VFS for sharing with other nodes
|
// save the zip file itself in VFS for sharing with other nodes
|
||||||
// call it <package_id>.zip
|
// call it <package_id>.zip
|
||||||
let zip_path = format!("{}/{}.zip", drive_name, package_id);
|
let zip_path = format!("{}/{}.zip", drive_name, package_id);
|
||||||
Request::to(("our", "vfs", "distro", "sys"))
|
vfs_request(zip_path, vfs::VfsAction::Write)
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: zip_path,
|
|
||||||
action: vfs::VfsAction::Write,
|
|
||||||
})?)
|
|
||||||
.blob(blob)
|
.blob(blob)
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??;
|
||||||
|
|
||||||
@ -292,35 +172,34 @@ pub fn create_package_drive(
|
|||||||
timeout: VFS_TIMEOUT,
|
timeout: VFS_TIMEOUT,
|
||||||
};
|
};
|
||||||
let manifest_bytes = manifest_file.read()?;
|
let manifest_bytes = manifest_file.read()?;
|
||||||
Ok(generate_metadata_hash(&manifest_bytes))
|
let manifest_hash = keccak_256_hash(&manifest_bytes);
|
||||||
|
|
||||||
|
Ok(manifest_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_api(package_id: &PackageId) -> anyhow::Result<bool> {
|
pub fn extract_api(package_id: &PackageId) -> anyhow::Result<bool> {
|
||||||
// get `pkg/api.zip` if it exists
|
// get `pkg/api.zip` if it exists
|
||||||
let api_response = Request::to(("our", "vfs", "distro", "sys"))
|
if let vfs::VfsResponse::Read = serde_json::from_slice(
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
vfs_request(format!("/{package_id}/pkg/api.zip"), vfs::VfsAction::Read)
|
||||||
path: format!("/{package_id}/pkg/api.zip"),
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
action: vfs::VfsAction::Read,
|
.body(),
|
||||||
})?)
|
)? {
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
|
||||||
if let Ok(vfs::VfsResponse::Read) = serde_json::from_slice(api_response.body()) {
|
|
||||||
// unzip api.zip into /api
|
// unzip api.zip into /api
|
||||||
// blob inherited from Read request
|
// blob inherited from Read request
|
||||||
let response = Request::to(("our", "vfs", "distro", "sys"))
|
if let vfs::VfsResponse::Ok = serde_json::from_slice(
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
vfs_request(format!("/{package_id}/pkg/api"), vfs::VfsAction::AddZip)
|
||||||
path: format!("/{package_id}/pkg/api"),
|
|
||||||
action: vfs::VfsAction::AddZip,
|
|
||||||
})?)
|
|
||||||
.inherit(true)
|
.inherit(true)
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
if let Ok(vfs::VfsResponse::Ok) = serde_json::from_slice(response.body()) {
|
.body(),
|
||||||
|
)? {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// given a package id, interact with VFS and kernel to get manifest,
|
/// given a `PackageId`, interact with VFS and kernel to get {package_hash}.zip,
|
||||||
|
/// unzip the manifest and pkg,
|
||||||
/// grant the capabilities in manifest, then initialize and start
|
/// grant the capabilities in manifest, then initialize and start
|
||||||
/// the processes in manifest.
|
/// the processes in manifest.
|
||||||
///
|
///
|
||||||
@ -329,13 +208,49 @@ pub fn extract_api(package_id: &PackageId) -> anyhow::Result<bool> {
|
|||||||
/// note also that each capability will only be granted if we, the process
|
/// note also that each capability will only be granted if we, the process
|
||||||
/// using this function, own that capability ourselves.
|
/// using this function, own that capability ourselves.
|
||||||
pub fn install(
|
pub fn install(
|
||||||
package_id: &PackageId,
|
package_id: &crate::kinode::process::main::PackageId,
|
||||||
|
metadata: Option<OnchainMetadata>,
|
||||||
|
version_hash: &str,
|
||||||
|
state: &mut State,
|
||||||
our_node: &str,
|
our_node: &str,
|
||||||
wit_version: Option<u32>,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let process_package_id = package_id.clone().to_process_lib();
|
||||||
|
let file = vfs::open_file(
|
||||||
|
&format!("/app_store:sys/downloads/{process_package_id}/{version_hash}.zip"),
|
||||||
|
false,
|
||||||
|
Some(VFS_TIMEOUT),
|
||||||
|
)?;
|
||||||
|
let bytes = file.read()?;
|
||||||
|
let manifest_hash = create_package_drive(&process_package_id, bytes)?;
|
||||||
|
|
||||||
|
let package_state = PackageState {
|
||||||
|
our_version_hash: version_hash.to_string(),
|
||||||
|
verified: true, // sideloaded apps are implicitly verified because there is no "source" to verify against
|
||||||
|
caps_approved: true, // TODO see if we want to auto-approve local installs
|
||||||
|
manifest_hash: Some(manifest_hash),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(extracted) = extract_api(&process_package_id) {
|
||||||
|
if extracted {
|
||||||
|
state.installed_apis.insert(process_package_id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
.packages
|
||||||
|
.insert(process_package_id.clone(), package_state);
|
||||||
|
|
||||||
// get the package manifest
|
// get the package manifest
|
||||||
let drive_path = format!("/{package_id}/pkg");
|
let drive_path = format!("/{process_package_id}/pkg");
|
||||||
let manifest = fetch_package_manifest(package_id)?;
|
let manifest = fetch_package_manifest(&process_package_id)?;
|
||||||
|
// get wit version from metadata if local or chain if remote.
|
||||||
|
let metadata = if let Some(metadata) = metadata {
|
||||||
|
metadata
|
||||||
|
} else {
|
||||||
|
fetch_package_metadata(&package_id)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let wit_version = metadata.properties.wit_version;
|
||||||
|
|
||||||
// first, for each process in manifest, initialize it
|
// first, for each process in manifest, initialize it
|
||||||
// then, once all have been initialized, grant them requested caps
|
// then, once all have been initialized, grant them requested caps
|
||||||
@ -347,90 +262,43 @@ pub fn install(
|
|||||||
format!("/{}", entry.process_wasm_path)
|
format!("/{}", entry.process_wasm_path)
|
||||||
};
|
};
|
||||||
let wasm_path = format!("{}{}", drive_path, wasm_path);
|
let wasm_path = format!("{}{}", drive_path, wasm_path);
|
||||||
let process_id = format!("{}:{}", entry.process_name, package_id);
|
|
||||||
let Ok(parsed_new_process_id) = process_id.parse::<ProcessId>() else {
|
let process_id = format!("{}:{}", entry.process_name, process_package_id);
|
||||||
|
let Ok(process_id) = process_id.parse::<ProcessId>() else {
|
||||||
return Err(anyhow::anyhow!("invalid process id!"));
|
return Err(anyhow::anyhow!("invalid process id!"));
|
||||||
};
|
};
|
||||||
// kill process if it already exists
|
// kill process if it already exists
|
||||||
Request::to(("our", "kernel", "distro", "sys"))
|
kernel_request(kt::KernelCommand::KillProcess(process_id.clone())).send()?;
|
||||||
.body(serde_json::to_vec(&kt::KernelCommand::KillProcess(
|
|
||||||
parsed_new_process_id.clone(),
|
|
||||||
))?)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if let Ok(vfs::VfsResponse::Err(_)) = serde_json::from_slice(
|
// read wasm file from VFS, bytes of which will be stored in blob
|
||||||
Request::to(("our", "vfs", "distro", "sys"))
|
if let Ok(vfs::VfsResponse::Err(e)) = serde_json::from_slice(
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
vfs_request(&wasm_path, vfs::VfsAction::Read)
|
||||||
path: wasm_path.clone(),
|
|
||||||
action: vfs::VfsAction::Read,
|
|
||||||
})?)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)??
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
.body(),
|
.body(),
|
||||||
) {
|
) {
|
||||||
return Err(anyhow::anyhow!("failed to read process file"));
|
return Err(anyhow::anyhow!("failed to read process file: {e}"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// use inherited blob to initialize process in kernel
|
||||||
let Ok(kt::KernelResponse::InitializedProcess) = serde_json::from_slice(
|
let Ok(kt::KernelResponse::InitializedProcess) = serde_json::from_slice(
|
||||||
Request::new()
|
kernel_request(kt::KernelCommand::InitializeProcess {
|
||||||
.target(("our", "kernel", "distro", "sys"))
|
id: process_id.clone(),
|
||||||
.body(serde_json::to_vec(&kt::KernelCommand::InitializeProcess {
|
|
||||||
id: parsed_new_process_id.clone(),
|
|
||||||
wasm_bytes_handle: wasm_path,
|
wasm_bytes_handle: wasm_path,
|
||||||
wit_version,
|
wit_version,
|
||||||
on_exit: entry.on_exit.clone(),
|
on_exit: entry.on_exit.clone(),
|
||||||
initial_capabilities: HashSet::new(),
|
initial_capabilities: HashSet::new(),
|
||||||
public: entry.public,
|
public: entry.public,
|
||||||
})?)
|
})
|
||||||
.inherit(true)
|
.inherit(true)
|
||||||
.send_and_await_response(VFS_TIMEOUT)??
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
.body(),
|
.body(),
|
||||||
) else {
|
) else {
|
||||||
return Err(anyhow::anyhow!("failed to initialize process"));
|
return Err(anyhow::anyhow!("failed to initialize process"));
|
||||||
};
|
};
|
||||||
// build initial caps
|
|
||||||
let mut requested_capabilities: Vec<kt::Capability> = vec![];
|
// build initial caps from manifest
|
||||||
for value in &entry.request_capabilities {
|
let mut requested_capabilities: Vec<kt::Capability> =
|
||||||
match value {
|
parse_capabilities(our_node, &entry.request_capabilities);
|
||||||
serde_json::Value::String(process_name) => {
|
|
||||||
if let Ok(parsed_process_id) = process_name.parse::<ProcessId>() {
|
|
||||||
requested_capabilities.push(kt::Capability {
|
|
||||||
issuer: Address {
|
|
||||||
node: our_node.to_string(),
|
|
||||||
process: parsed_process_id.clone(),
|
|
||||||
},
|
|
||||||
params: "\"messaging\"".into(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
println!("{process_id} manifest requested invalid cap: {value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::Value::Object(map) => {
|
|
||||||
if let Some(process_name) = map.get("process") {
|
|
||||||
if let Ok(parsed_process_id) = process_name
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.parse::<ProcessId>()
|
|
||||||
{
|
|
||||||
if let Some(params) = map.get("params") {
|
|
||||||
requested_capabilities.push(kt::Capability {
|
|
||||||
issuer: Address {
|
|
||||||
node: our_node.to_string(),
|
|
||||||
process: parsed_process_id.clone(),
|
|
||||||
},
|
|
||||||
params: params.to_string(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
println!("{process_id} manifest requested invalid cap: {value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val => {
|
|
||||||
println!("{process_id} manifest requested invalid cap: {val}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.request_networking {
|
if entry.request_networking {
|
||||||
requested_capabilities.push(kt::Capability {
|
requested_capabilities.push(kt::Capability {
|
||||||
@ -457,12 +325,10 @@ pub fn install(
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Request::new()
|
kernel_request(kt::KernelCommand::GrantCapabilities {
|
||||||
.target(("our", "kernel", "distro", "sys"))
|
target: process_id.clone(),
|
||||||
.body(serde_json::to_vec(&kt::KernelCommand::GrantCapabilities {
|
|
||||||
target: parsed_new_process_id.clone(),
|
|
||||||
capabilities: requested_capabilities,
|
capabilities: requested_capabilities,
|
||||||
})?)
|
})
|
||||||
.send()?;
|
.send()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,28 +336,26 @@ pub fn install(
|
|||||||
// this is done after initialization so that processes within a package
|
// this is done after initialization so that processes within a package
|
||||||
// can grant capabilities to one another in the manifest.
|
// can grant capabilities to one another in the manifest.
|
||||||
for entry in &manifest {
|
for entry in &manifest {
|
||||||
let process_id = format!("{}:{}", entry.process_name, package_id);
|
let process_id = ProcessId::new(
|
||||||
let Ok(parsed_new_process_id) = process_id.parse::<ProcessId>() else {
|
Some(&entry.process_name),
|
||||||
return Err(anyhow::anyhow!("invalid process id!"));
|
process_package_id.package(),
|
||||||
};
|
process_package_id.publisher(),
|
||||||
|
);
|
||||||
|
|
||||||
for value in &entry.grant_capabilities {
|
for value in &entry.grant_capabilities {
|
||||||
match value {
|
match value {
|
||||||
serde_json::Value::String(process_name) => {
|
serde_json::Value::String(process_name) => {
|
||||||
if let Ok(parsed_process_id) = process_name.parse::<ProcessId>() {
|
if let Ok(parsed_process_id) = process_name.parse::<ProcessId>() {
|
||||||
Request::to(("our", "kernel", "distro", "sys"))
|
kernel_request(kt::KernelCommand::GrantCapabilities {
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&kt::KernelCommand::GrantCapabilities {
|
|
||||||
target: parsed_process_id,
|
target: parsed_process_id,
|
||||||
capabilities: vec![kt::Capability {
|
capabilities: vec![kt::Capability {
|
||||||
issuer: Address {
|
issuer: Address {
|
||||||
node: our_node.to_string(),
|
node: our_node.to_string(),
|
||||||
process: parsed_new_process_id.clone(),
|
process: process_id.clone(),
|
||||||
},
|
},
|
||||||
params: "\"messaging\"".into(),
|
params: "\"messaging\"".into(),
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.send()?;
|
.send()?;
|
||||||
} else {
|
} else {
|
||||||
println!("{process_id} manifest tried to grant invalid cap: {value}");
|
println!("{process_id} manifest tried to grant invalid cap: {value}");
|
||||||
@ -505,19 +369,16 @@ pub fn install(
|
|||||||
.parse::<ProcessId>()
|
.parse::<ProcessId>()
|
||||||
{
|
{
|
||||||
if let Some(params) = map.get("params") {
|
if let Some(params) = map.get("params") {
|
||||||
Request::to(("our", "kernel", "distro", "sys"))
|
kernel_request(kt::KernelCommand::GrantCapabilities {
|
||||||
.body(serde_json::to_vec(
|
|
||||||
&kt::KernelCommand::GrantCapabilities {
|
|
||||||
target: parsed_process_id,
|
target: parsed_process_id,
|
||||||
capabilities: vec![kt::Capability {
|
capabilities: vec![kt::Capability {
|
||||||
issuer: Address {
|
issuer: Address {
|
||||||
node: our_node.to_string(),
|
node: our_node.to_string(),
|
||||||
process: parsed_new_process_id.clone(),
|
process: process_id.clone(),
|
||||||
},
|
},
|
||||||
params: params.to_string(),
|
params: params.to_string(),
|
||||||
}],
|
}],
|
||||||
},
|
})
|
||||||
)?)
|
|
||||||
.send()?;
|
.send()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -533,10 +394,7 @@ pub fn install(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Ok(kt::KernelResponse::StartedProcess) = serde_json::from_slice(
|
let Ok(kt::KernelResponse::StartedProcess) = serde_json::from_slice(
|
||||||
Request::to(("our", "kernel", "distro", "sys"))
|
kernel_request(kt::KernelCommand::RunProcess(process_id))
|
||||||
.body(serde_json::to_vec(&kt::KernelCommand::RunProcess(
|
|
||||||
parsed_new_process_id,
|
|
||||||
))?)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)??
|
.send_and_await_response(VFS_TIMEOUT)??
|
||||||
.body(),
|
.body(),
|
||||||
) else {
|
) else {
|
||||||
@ -546,41 +404,124 @@ pub fn install(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uninstall(package_id: &PackageId) -> anyhow::Result<()> {
|
/// given a `PackageId`, read its manifest, kill all processes declared in it,
|
||||||
|
/// then remove its drive in the virtual filesystem.
|
||||||
|
pub fn uninstall(state: &mut State, package_id: &PackageId) -> anyhow::Result<()> {
|
||||||
|
if !state.packages.contains_key(package_id) {
|
||||||
|
return Err(anyhow::anyhow!("package not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// the drive corresponding to the package we will be removing
|
||||||
let drive_path = format!("/{package_id}/pkg");
|
let drive_path = format!("/{package_id}/pkg");
|
||||||
Request::new()
|
|
||||||
.target(("our", "vfs", "distro", "sys"))
|
// get manifest.json from drive
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
vfs_request(
|
||||||
path: format!("{}/manifest.json", drive_path),
|
format!("{}/manifest.json", drive_path),
|
||||||
action: vfs::VfsAction::Read,
|
vfs::VfsAction::Read,
|
||||||
})?)
|
)
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??;
|
||||||
let Some(blob) = get_blob() else {
|
let Some(blob) = get_blob() else {
|
||||||
return Err(anyhow::anyhow!("no blob"));
|
return Err(anyhow::anyhow!(
|
||||||
|
"couldn't find manifest.json for uninstall!"
|
||||||
|
));
|
||||||
};
|
};
|
||||||
let manifest = String::from_utf8(blob.bytes)?;
|
let manifest = serde_json::from_slice::<Vec<kt::PackageManifestEntry>>(&blob.bytes)?;
|
||||||
let manifest = serde_json::from_str::<Vec<kt::PackageManifestEntry>>(&manifest)?;
|
|
||||||
// reading from the package manifest, kill every process
|
// reading from the package manifest, kill every process named
|
||||||
for entry in &manifest {
|
for entry in &manifest {
|
||||||
let process_id = format!("{}:{}", entry.process_name, package_id);
|
kernel_request(kt::KernelCommand::KillProcess(ProcessId::new(
|
||||||
let Ok(parsed_new_process_id) = process_id.parse::<ProcessId>() else {
|
Some(&entry.process_name),
|
||||||
continue;
|
package_id.package(),
|
||||||
};
|
package_id.publisher(),
|
||||||
Request::new()
|
)))
|
||||||
.target(("our", "kernel", "distro", "sys"))
|
|
||||||
.body(serde_json::to_vec(&kt::KernelCommand::KillProcess(
|
|
||||||
parsed_new_process_id,
|
|
||||||
))?)
|
|
||||||
.send()?;
|
.send()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// then, delete the drive
|
// then, delete the drive
|
||||||
Request::new()
|
vfs_request(drive_path, vfs::VfsAction::RemoveDirAll)
|
||||||
.target(("our", "vfs", "distro", "sys"))
|
|
||||||
.body(serde_json::to_vec(&vfs::VfsRequest {
|
|
||||||
path: drive_path,
|
|
||||||
action: vfs::VfsAction::RemoveDirAll,
|
|
||||||
})?)
|
|
||||||
.send_and_await_response(VFS_TIMEOUT)??;
|
.send_and_await_response(VFS_TIMEOUT)??;
|
||||||
|
|
||||||
|
// Remove the package from the state
|
||||||
|
state.packages.remove(package_id);
|
||||||
|
|
||||||
|
// If this package had an API, remove it from installed_apis
|
||||||
|
state.installed_apis.remove(package_id);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _extract_caps_hashes(manifest_bytes: &[u8]) -> anyhow::Result<HashMap<String, String>> {
|
||||||
|
let manifest = serde_json::from_slice::<Vec<kt::PackageManifestEntry>>(manifest_bytes)?;
|
||||||
|
let mut caps_hashes = HashMap::new();
|
||||||
|
for process in &manifest {
|
||||||
|
let caps_bytes = serde_json::to_vec(&process.request_capabilities)?;
|
||||||
|
let caps_hash = keccak_256_hash(&caps_bytes);
|
||||||
|
caps_hashes.insert(process.process_name.clone(), caps_hash);
|
||||||
|
}
|
||||||
|
Ok(caps_hashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_capabilities(our_node: &str, caps: &Vec<serde_json::Value>) -> Vec<kt::Capability> {
|
||||||
|
let mut requested_capabilities: Vec<kt::Capability> = vec![];
|
||||||
|
for value in caps {
|
||||||
|
match value {
|
||||||
|
serde_json::Value::String(process_name) => {
|
||||||
|
if let Ok(parsed_process_id) = process_name.parse::<ProcessId>() {
|
||||||
|
requested_capabilities.push(kt::Capability {
|
||||||
|
issuer: Address {
|
||||||
|
node: our_node.to_string(),
|
||||||
|
process: parsed_process_id.clone(),
|
||||||
|
},
|
||||||
|
params: "\"messaging\"".into(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
println!("manifest requested invalid cap: {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::Value::Object(map) => {
|
||||||
|
if let Some(process_name) = map.get("process") {
|
||||||
|
if let Ok(parsed_process_id) = process_name
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.parse::<ProcessId>()
|
||||||
|
{
|
||||||
|
if let Some(params) = map.get("params") {
|
||||||
|
requested_capabilities.push(kt::Capability {
|
||||||
|
issuer: Address {
|
||||||
|
node: our_node.to_string(),
|
||||||
|
process: parsed_process_id.clone(),
|
||||||
|
},
|
||||||
|
params: params.to_string(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
println!("manifest requested invalid cap: {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val => {
|
||||||
|
println!("manifest requested invalid cap: {val}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requested_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kernel_request(command: kt::KernelCommand) -> Request {
|
||||||
|
Request::to(("our", "kernel", "distro", "sys"))
|
||||||
|
.body(serde_json::to_vec(&command).expect("failed to serialize KernelCommand"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vfs_request<T>(path: T, action: vfs::VfsAction) -> Request
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
Request::to(("our", "vfs", "distro", "sys")).body(
|
||||||
|
serde_json::to_vec(&vfs::VfsRequest {
|
||||||
|
path: path.into(),
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
.expect("failed to serialize VfsRequest"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
29
kinode/packages/app_store/chain/Cargo.toml
Normal file
29
kinode/packages/app_store/chain/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "chain"
|
||||||
|
version = "0.5.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
simulation-mode = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
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" }
|
||||||
|
rand = "0.8"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
sha3 = "0.10.8"
|
||||||
|
url = "2.4.1"
|
||||||
|
urlencoding = "2.1.0"
|
||||||
|
wit-bindgen = "0.24.0"
|
||||||
|
zip = { version = "1.1.1", default-features = false }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[package.metadata.component]
|
||||||
|
package = "kinode:process"
|
497
kinode/packages/app_store/chain/src/lib.rs
Normal file
497
kinode/packages/app_store/chain/src/lib.rs
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
#![feature(let_chains)]
|
||||||
|
//! chain:app_store:sys
|
||||||
|
//! manages indexing relevant packages and their versions from the kimap.
|
||||||
|
//! keeps eth subscriptions open, keeps data updated.
|
||||||
|
//!
|
||||||
|
use crate::kinode::process::chain::{
|
||||||
|
ChainError, ChainRequests, OnchainApp, OnchainMetadata, OnchainProperties,
|
||||||
|
};
|
||||||
|
use crate::kinode::process::downloads::{AutoUpdateRequest, DownloadRequests};
|
||||||
|
use alloy_primitives::keccak256;
|
||||||
|
use alloy_sol_types::SolEvent;
|
||||||
|
use kinode::process::chain::ChainResponses;
|
||||||
|
use kinode_process_lib::{
|
||||||
|
await_message, call_init, eth, get_blob, get_state, http, kernel_types as kt, kimap,
|
||||||
|
print_to_terminal, println, timer, Address, Message, PackageId, Request, Response,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
path: "target/wit",
|
||||||
|
generate_unused_types: true,
|
||||||
|
world: "app-store-sys-v0",
|
||||||
|
additional_derives: [serde::Deserialize, serde::Serialize],
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(feature = "simulation-mode"))]
|
||||||
|
const CHAIN_ID: u64 = kimap::KIMAP_CHAIN_ID;
|
||||||
|
#[cfg(feature = "simulation-mode")]
|
||||||
|
const CHAIN_ID: u64 = 31337; // local
|
||||||
|
|
||||||
|
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 DELAY_MS: u64 = 1_000; // 1s
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct State {
|
||||||
|
/// the kimap helper we are using
|
||||||
|
pub kimap: kimap::Kimap,
|
||||||
|
/// the last block at which we saved the state of the listings to disk.
|
||||||
|
/// when we boot, we can read logs starting from this block and
|
||||||
|
/// rebuild latest state.
|
||||||
|
pub last_saved_block: u64,
|
||||||
|
/// onchain listings
|
||||||
|
pub listings: HashMap<PackageId, PackageListing>,
|
||||||
|
/// set of packages that we have published
|
||||||
|
pub published: HashSet<PackageId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// listing information derived from metadata hash in listing event
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct PackageListing {
|
||||||
|
pub tba: eth::Address,
|
||||||
|
pub metadata_uri: String,
|
||||||
|
pub metadata_hash: String,
|
||||||
|
// should this even be optional?
|
||||||
|
// relegate to only valid apps maybe?
|
||||||
|
pub metadata: Option<kt::Erc721Metadata>,
|
||||||
|
pub auto_update: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)] // untagged as a meta-type for all incoming requests
|
||||||
|
pub enum Req {
|
||||||
|
Eth(eth::EthSubResult),
|
||||||
|
Request(ChainRequests),
|
||||||
|
}
|
||||||
|
|
||||||
|
call_init!(init);
|
||||||
|
fn init(our: Address) {
|
||||||
|
println!(
|
||||||
|
"chain started, indexing on contract address {}",
|
||||||
|
KIMAP_ADDRESS
|
||||||
|
);
|
||||||
|
// create new provider with request-timeout of 60s
|
||||||
|
// can change, log requests can take quite a long time.
|
||||||
|
let eth_provider: eth::Provider = eth::Provider::new(CHAIN_ID, CHAIN_TIMEOUT);
|
||||||
|
|
||||||
|
let mut state = fetch_state(eth_provider);
|
||||||
|
fetch_and_subscribe_logs(&our, &mut state);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match await_message() {
|
||||||
|
Err(send_error) => {
|
||||||
|
print_to_terminal(1, &format!("got network error: {send_error}"));
|
||||||
|
}
|
||||||
|
Ok(message) => {
|
||||||
|
if let Err(e) = handle_message(&our, &mut state, &message) {
|
||||||
|
print_to_terminal(1, &format!("error handling message: {:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(our: &Address, state: &mut State, message: &Message) -> anyhow::Result<()> {
|
||||||
|
if !message.is_request() {
|
||||||
|
if message.is_local(&our) && message.source().process == "timer:distro:sys" {
|
||||||
|
// handling of ETH RPC subscriptions delayed by DELAY_MS
|
||||||
|
// to allow kns to have a chance to process block: handle now
|
||||||
|
let Some(context) = message.context() else {
|
||||||
|
return Err(anyhow::anyhow!("foo"));
|
||||||
|
};
|
||||||
|
let log = serde_json::from_slice(context)?;
|
||||||
|
handle_eth_log(our, state, log)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let req: Req = serde_json::from_slice(message.body())?;
|
||||||
|
match req {
|
||||||
|
Req::Eth(eth_result) => {
|
||||||
|
if !message.is_local(our) || message.source().process != "eth:distro:sys" {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"eth sub event from unexpected address: {}",
|
||||||
|
message.source()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(eth::EthSub { result, .. }) = eth_result {
|
||||||
|
if let eth::SubscriptionResult::Log(ref log) = result {
|
||||||
|
// delay handling of ETH RPC subscriptions by DELAY_MS
|
||||||
|
// to allow kns to have a chance to process block
|
||||||
|
timer::set_timer(DELAY_MS, Some(serde_json::to_vec(log)?));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// attempt to resubscribe
|
||||||
|
state
|
||||||
|
.kimap
|
||||||
|
.provider
|
||||||
|
.subscribe_loop(1, app_store_filter(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Req::Request(chains) => {
|
||||||
|
handle_local_request(state, chains)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_local_request(state: &mut State, req: ChainRequests) -> anyhow::Result<()> {
|
||||||
|
match req {
|
||||||
|
ChainRequests::GetApp(package_id) => {
|
||||||
|
let onchain_app = state
|
||||||
|
.listings
|
||||||
|
.get(&package_id.clone().to_process_lib())
|
||||||
|
.map(|app| OnchainApp {
|
||||||
|
package_id: package_id,
|
||||||
|
tba: app.tba.to_string(),
|
||||||
|
metadata_uri: app.metadata_uri.clone(),
|
||||||
|
metadata_hash: app.metadata_hash.clone(),
|
||||||
|
metadata: app.metadata.as_ref().map(|m| m.clone().into()),
|
||||||
|
auto_update: app.auto_update,
|
||||||
|
});
|
||||||
|
let response = ChainResponses::GetApp(onchain_app);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&response)?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
ChainRequests::GetApps => {
|
||||||
|
let apps: Vec<OnchainApp> = state
|
||||||
|
.listings
|
||||||
|
.iter()
|
||||||
|
.map(|(id, listing)| listing.to_onchain_app(id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let response = ChainResponses::GetApps(apps);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&response)?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
ChainRequests::GetOurApps => {
|
||||||
|
let apps: Vec<OnchainApp> = state
|
||||||
|
.published
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
state
|
||||||
|
.listings
|
||||||
|
.get(id)
|
||||||
|
.map(|listing| listing.to_onchain_app(id))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let response = ChainResponses::GetOurApps(apps);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&response)?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
ChainRequests::StartAutoUpdate(package_id) => {
|
||||||
|
if let Some(listing) = state.listings.get_mut(&package_id.to_process_lib()) {
|
||||||
|
listing.auto_update = true;
|
||||||
|
let response = ChainResponses::AutoUpdateStarted;
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&response)?)
|
||||||
|
.send()?;
|
||||||
|
} else {
|
||||||
|
let error_response = ChainResponses::Error(ChainError::NoPackage);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&error_response)?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChainRequests::StopAutoUpdate(package_id) => {
|
||||||
|
if let Some(listing) = state.listings.get_mut(&package_id.to_process_lib()) {
|
||||||
|
listing.auto_update = false;
|
||||||
|
let response = ChainResponses::AutoUpdateStopped;
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&response)?)
|
||||||
|
.send()?;
|
||||||
|
} else {
|
||||||
|
let error_response = ChainResponses::Error(ChainError::NoPackage);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&error_response)?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_eth_log(our: &Address, state: &mut State, log: eth::Log) -> anyhow::Result<()> {
|
||||||
|
let block_number: u64 = log.block_number.ok_or(anyhow::anyhow!("blocknumbaerror"))?;
|
||||||
|
let note: kimap::Note =
|
||||||
|
kimap::decode_note_log(&log).map_err(|e| anyhow::anyhow!("decodelogerror: {e:?}"))?;
|
||||||
|
|
||||||
|
let package_id = note
|
||||||
|
.parent_path
|
||||||
|
.split_once('.')
|
||||||
|
.ok_or(anyhow::anyhow!("invalid publisher name"))
|
||||||
|
.and_then(|(package, publisher)| {
|
||||||
|
if package.is_empty() || publisher.is_empty() {
|
||||||
|
Err(anyhow::anyhow!("invalid publisher name"))
|
||||||
|
} else {
|
||||||
|
Ok(PackageId::new(&package, &publisher))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// the app store exclusively looks for ~metadata-uri postings: if one is
|
||||||
|
// observed, we then *query* for ~metadata-hash to verify the content
|
||||||
|
// at the URI.
|
||||||
|
|
||||||
|
let metadata_uri = String::from_utf8_lossy(¬e.data).to_string();
|
||||||
|
let is_our_package = &package_id.publisher() == &our.node();
|
||||||
|
|
||||||
|
let (tba, metadata_hash) = {
|
||||||
|
// generate ~metadata-hash full-path
|
||||||
|
let hash_note = format!("~metadata-hash.{}", note.parent_path);
|
||||||
|
|
||||||
|
// owner can change which we don't track (yet?) so don't save, need to get when desired
|
||||||
|
let (tba, _owner, data) = match state.kimap.get(&hash_note) {
|
||||||
|
Ok(gr) => Ok(gr),
|
||||||
|
Err(e) => match e {
|
||||||
|
eth::EthError::RpcError(_) => {
|
||||||
|
// retry on RpcError after DELAY_MS sleep
|
||||||
|
// sleep here rather than with, e.g., a message to
|
||||||
|
// `timer:distro:sys` so that events are processed in
|
||||||
|
// order of receipt
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(DELAY_MS));
|
||||||
|
state.kimap.get(&hash_note)
|
||||||
|
}
|
||||||
|
_ => Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.map_err(|e| {
|
||||||
|
println!("Couldn't find {hash_note}: {e:?}");
|
||||||
|
anyhow::anyhow!("metadata hash mismatch")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match data {
|
||||||
|
None => {
|
||||||
|
// if ~metadata-uri is also empty, this is an unpublish action!
|
||||||
|
if metadata_uri.is_empty() {
|
||||||
|
state.published.remove(&package_id);
|
||||||
|
state.listings.remove(&package_id);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return Err(anyhow::anyhow!("metadata hash not found"));
|
||||||
|
}
|
||||||
|
Some(hash_note) => (tba, String::from_utf8_lossy(&hash_note).to_string()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch metadata from the URI (currently only handling HTTP(S) URLs!)
|
||||||
|
// assert that the metadata hash matches the fetched data
|
||||||
|
let metadata = fetch_metadata_from_url(&metadata_uri, &metadata_hash, 30)?;
|
||||||
|
|
||||||
|
match state.listings.entry(package_id.clone()) {
|
||||||
|
std::collections::hash_map::Entry::Occupied(mut listing) => {
|
||||||
|
let listing = listing.get_mut();
|
||||||
|
listing.metadata_uri = metadata_uri;
|
||||||
|
listing.tba = tba;
|
||||||
|
listing.metadata_hash = metadata_hash;
|
||||||
|
listing.metadata = Some(metadata.clone());
|
||||||
|
}
|
||||||
|
std::collections::hash_map::Entry::Vacant(listing) => {
|
||||||
|
listing.insert(PackageListing {
|
||||||
|
tba,
|
||||||
|
metadata_uri,
|
||||||
|
metadata_hash,
|
||||||
|
metadata: Some(metadata.clone()),
|
||||||
|
auto_update: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_our_package {
|
||||||
|
state.published.insert(package_id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
state.last_saved_block = block_number;
|
||||||
|
|
||||||
|
// if auto_update is enabled, send a message to downloads to kick off the update.
|
||||||
|
if let Some(listing) = state.listings.get(&package_id) {
|
||||||
|
if listing.auto_update {
|
||||||
|
print_to_terminal(1, &format!("kicking off auto-update for: {}", package_id));
|
||||||
|
let request = DownloadRequests::AutoUpdate(AutoUpdateRequest {
|
||||||
|
package_id: crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||||
|
metadata: metadata.into(),
|
||||||
|
});
|
||||||
|
Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&request)?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create the filter used for app store getLogs and subscription.
|
||||||
|
/// the app store exclusively looks for ~metadata-uri postings: if one is
|
||||||
|
/// observed, we then *query* for ~metadata-hash to verify the content
|
||||||
|
/// at the URI.
|
||||||
|
///
|
||||||
|
/// this means that ~metadata-hash should be *posted before or at the same time* as ~metadata-uri!
|
||||||
|
pub fn app_store_filter(state: &State) -> eth::Filter {
|
||||||
|
let notes = vec![keccak256("~metadata-uri")];
|
||||||
|
|
||||||
|
eth::Filter::new()
|
||||||
|
.address(*state.kimap.address())
|
||||||
|
.events([kimap::contract::Note::SIGNATURE])
|
||||||
|
.topic3(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create a filter to fetch app store event logs from chain and subscribe to new events
|
||||||
|
pub fn fetch_and_subscribe_logs(our: &Address, state: &mut State) {
|
||||||
|
let filter = app_store_filter(state);
|
||||||
|
// get past logs, subscribe to new ones.
|
||||||
|
// subscribe first so we don't miss any logs
|
||||||
|
println!("subscribing...");
|
||||||
|
state.kimap.provider.subscribe_loop(1, filter.clone());
|
||||||
|
for log in fetch_logs(
|
||||||
|
&state.kimap.provider,
|
||||||
|
&filter.from_block(state.last_saved_block),
|
||||||
|
) {
|
||||||
|
if let Err(e) = handle_eth_log(our, state, log) {
|
||||||
|
print_to_terminal(1, &format!("error ingesting log: {e}"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch logs from the chain with a given filter
|
||||||
|
fn fetch_logs(eth_provider: ð::Provider, filter: ð::Filter) -> Vec<eth::Log> {
|
||||||
|
loop {
|
||||||
|
match eth_provider.get_logs(filter) {
|
||||||
|
Ok(res) => return res,
|
||||||
|
Err(_) => {
|
||||||
|
println!("failed to fetch logs! trying again in 5s...");
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch metadata from url and verify it matches metadata_hash
|
||||||
|
pub fn fetch_metadata_from_url(
|
||||||
|
metadata_url: &str,
|
||||||
|
metadata_hash: &str,
|
||||||
|
timeout: u64,
|
||||||
|
) -> Result<kt::Erc721Metadata, anyhow::Error> {
|
||||||
|
if let Ok(url) = url::Url::parse(metadata_url) {
|
||||||
|
if let Ok(_) =
|
||||||
|
http::client::send_request_await_response(http::Method::GET, url, None, timeout, vec![])
|
||||||
|
{
|
||||||
|
if let Some(body) = get_blob() {
|
||||||
|
let hash = keccak_256_hash(&body.bytes);
|
||||||
|
if &hash == metadata_hash {
|
||||||
|
return Ok(serde_json::from_slice::<kt::Erc721Metadata>(&body.bytes)
|
||||||
|
.map_err(|_| anyhow::anyhow!("metadata not found"))?);
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("metadata hash mismatch"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("metadata not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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};
|
||||||
|
let mut hasher = Keccak256::new();
|
||||||
|
hasher.update(bytes);
|
||||||
|
format!("0x{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch state from disk or create a new one if that fails
|
||||||
|
pub fn fetch_state(provider: eth::Provider) -> State {
|
||||||
|
if let Some(state_bytes) = get_state() {
|
||||||
|
match serde_json::from_slice::<State>(&state_bytes) {
|
||||||
|
Ok(state) => {
|
||||||
|
if state.kimap.address().to_string() == KIMAP_ADDRESS {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"state contract address mismatch. rebuilding state! expected {}, got {}",
|
||||||
|
KIMAP_ADDRESS,
|
||||||
|
state.kimap.address().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => println!("failed to deserialize saved state, rebuilding: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State {
|
||||||
|
kimap: kimap::Kimap::new(provider, eth::Address::from_str(KIMAP_ADDRESS).unwrap()),
|
||||||
|
last_saved_block: 0,
|
||||||
|
listings: HashMap::new(),
|
||||||
|
published: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quite annoyingly, we must convert from our gen'd version of PackageId
|
||||||
|
// to the process_lib's gen'd version. this is in order to access custom
|
||||||
|
// Impls that we want to use
|
||||||
|
impl crate::kinode::process::main::PackageId {
|
||||||
|
pub fn to_process_lib(self) -> PackageId {
|
||||||
|
PackageId {
|
||||||
|
package_name: self.package_name,
|
||||||
|
publisher_node: self.publisher_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_process_lib(package_id: PackageId) -> Self {
|
||||||
|
Self {
|
||||||
|
package_name: package_id.package_name,
|
||||||
|
publisher_node: package_id.publisher_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageListing {
|
||||||
|
pub fn to_onchain_app(&self, package_id: &PackageId) -> OnchainApp {
|
||||||
|
OnchainApp {
|
||||||
|
package_id: crate::kinode::process::main::PackageId::from_process_lib(
|
||||||
|
package_id.clone(),
|
||||||
|
),
|
||||||
|
tba: self.tba.to_string(),
|
||||||
|
metadata_uri: self.metadata_uri.clone(),
|
||||||
|
metadata_hash: self.metadata_hash.clone(),
|
||||||
|
metadata: self.metadata.as_ref().map(|m| m.clone().into()),
|
||||||
|
auto_update: self.auto_update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<kt::Erc721Metadata> for OnchainMetadata {
|
||||||
|
fn from(erc: kt::Erc721Metadata) -> Self {
|
||||||
|
OnchainMetadata {
|
||||||
|
name: erc.name,
|
||||||
|
description: erc.description,
|
||||||
|
image: erc.image,
|
||||||
|
external_url: erc.external_url,
|
||||||
|
animation_url: erc.animation_url,
|
||||||
|
properties: OnchainProperties {
|
||||||
|
package_name: erc.properties.package_name,
|
||||||
|
publisher: erc.properties.publisher,
|
||||||
|
current_version: erc.properties.current_version,
|
||||||
|
mirrors: erc.properties.mirrors,
|
||||||
|
code_hashes: erc.properties.code_hashes.into_iter().collect(),
|
||||||
|
license: erc.properties.license,
|
||||||
|
screenshots: erc.properties.screenshots,
|
||||||
|
wit_version: erc.properties.wit_version,
|
||||||
|
dependencies: erc.properties.dependencies,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ simulation-mode = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.8.0" }
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
wit-bindgen = "0.24.0"
|
wit-bindgen = "0.24.0"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::kinode::process::main::{DownloadResponse, LocalRequest, LocalResponse};
|
use crate::kinode::process::downloads::{DownloadRequests, DownloadResponses};
|
||||||
use kinode::process::main::DownloadRequest;
|
use kinode::process::downloads::LocalDownloadRequest;
|
||||||
use kinode_process_lib::{
|
use kinode_process_lib::{
|
||||||
await_next_message_body, call_init, println, Address, Message, NodeId, PackageId, Request,
|
await_next_message_body, call_init, println, Address, Message, PackageId, Request,
|
||||||
};
|
};
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
@ -19,14 +19,15 @@ fn init(our: Address) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let args = String::from_utf8(body).unwrap_or_default();
|
let args = String::from_utf8(body).unwrap_or_default();
|
||||||
|
let parts: Vec<&str> = args.split_whitespace().collect();
|
||||||
let Some((arg1, arg2)) = args.split_once(" ") else {
|
if parts.len() != 3 {
|
||||||
println!("download: 2 arguments required, the node id to download from and the package id of the app");
|
println!("download: 3 arguments required, the node id to download from, the package id of the app, and the version hash");
|
||||||
println!("example: download my-friend.os app:publisher.os");
|
println!("example: download my-friend.os app:publisher.os f5d374ab50e66888a7c2332b22d0f909f2e3115040725cfab98dcae488916990");
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
let (arg1, arg2, arg3) = (parts[0], parts[1], parts[2]);
|
||||||
|
|
||||||
let download_from: NodeId = arg1.to_string();
|
let download_from: String = arg1.to_string();
|
||||||
|
|
||||||
let Ok(package_id) = arg2.parse::<PackageId>() else {
|
let Ok(package_id) = arg2.parse::<PackageId>() else {
|
||||||
println!("download: invalid package id, make sure to include package name and publisher");
|
println!("download: invalid package id, make sure to include package name and publisher");
|
||||||
@ -34,38 +35,38 @@ fn init(our: Address) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let version_hash: String = arg3.to_string();
|
||||||
|
|
||||||
let Ok(Ok(Message::Response { body, .. })) =
|
let Ok(Ok(Message::Response { body, .. })) =
|
||||||
Request::to((our.node(), ("main", "app_store", "sys")))
|
Request::to((our.node(), ("downloads", "app_store", "sys")))
|
||||||
.body(
|
.body(
|
||||||
serde_json::to_vec(&LocalRequest::Download(DownloadRequest {
|
serde_json::to_vec(&DownloadRequests::LocalDownload(LocalDownloadRequest {
|
||||||
package_id: crate::kinode::process::main::PackageId {
|
package_id: crate::kinode::process::main::PackageId {
|
||||||
package_name: package_id.package_name.clone(),
|
package_name: package_id.package_name.clone(),
|
||||||
publisher_node: package_id.publisher_node.clone(),
|
publisher_node: package_id.publisher_node.clone(),
|
||||||
},
|
},
|
||||||
download_from: download_from.clone(),
|
download_from: download_from.clone(),
|
||||||
mirror: true,
|
desired_version_hash: version_hash.clone(),
|
||||||
auto_update: true,
|
|
||||||
desired_version_hash: None,
|
|
||||||
}))
|
}))
|
||||||
.unwrap(),
|
.expect("Failed to serialize LocalDownloadRequest"),
|
||||||
)
|
)
|
||||||
.send_and_await_response(5)
|
.send_and_await_response(10)
|
||||||
else {
|
else {
|
||||||
println!("download: failed to get a response from app_store..!");
|
println!("download: failed to get a response from app_store..!");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(response) = serde_json::from_slice::<LocalResponse>(&body) else {
|
let Ok(response) = serde_json::from_slice::<DownloadResponses>(&body) else {
|
||||||
println!("download: failed to parse response from app_store..!");
|
println!("download: failed to parse response from app_store..!");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
LocalResponse::DownloadResponse(DownloadResponse::Started) => {
|
DownloadResponses::Error(_e) => {
|
||||||
println!("started downloading package {package_id} from {download_from}");
|
println!("download: error");
|
||||||
}
|
}
|
||||||
LocalResponse::DownloadResponse(_) => {
|
DownloadResponses::Success => {
|
||||||
println!("failed to download package {package_id} from {download_from}");
|
println!("download: success");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("download: unexpected response from app_store..!");
|
println!("download: unexpected response from app_store..!");
|
||||||
|
26
kinode/packages/app_store/downloads/Cargo.toml
Normal file
26
kinode/packages/app_store/downloads/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "downloads"
|
||||||
|
version = "0.5.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
simulation-mode = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", branch = "develop" }
|
||||||
|
rand = "0.8"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
sha3 = "0.10.8"
|
||||||
|
url = "2.4.1"
|
||||||
|
urlencoding = "2.1.0"
|
||||||
|
wit-bindgen = "0.24.0"
|
||||||
|
zip = { version = "1.1.4", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[package.metadata.component]
|
||||||
|
package = "kinode:process"
|
589
kinode/packages/app_store/downloads/src/lib.rs
Normal file
589
kinode/packages/app_store/downloads/src/lib.rs
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
#![feature(let_chains)]
|
||||||
|
//! downloads:app_store:sys
|
||||||
|
//! manages downloading and sharing of versioned packages.
|
||||||
|
//!
|
||||||
|
use crate::kinode::process::downloads::{
|
||||||
|
AutoUpdateRequest, DirEntry, DownloadCompleteRequest, DownloadError, DownloadRequests,
|
||||||
|
DownloadResponses, Entry, FileEntry, HashMismatch, LocalDownloadRequest, RemoteDownloadRequest,
|
||||||
|
RemoveFileRequest,
|
||||||
|
};
|
||||||
|
use std::{collections::HashSet, io::Read, str::FromStr};
|
||||||
|
|
||||||
|
use ft_worker_lib::{spawn_receive_transfer, spawn_send_transfer};
|
||||||
|
use kinode_process_lib::{
|
||||||
|
await_message, call_init, get_blob, get_state,
|
||||||
|
http::client,
|
||||||
|
print_to_terminal, println, set_state,
|
||||||
|
vfs::{self, Directory, File},
|
||||||
|
Address, Message, PackageId, ProcessId, Request, Response,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
path: "target/wit",
|
||||||
|
generate_unused_types: true,
|
||||||
|
world: "app-store-sys-v0",
|
||||||
|
additional_derives: [serde::Deserialize, serde::Serialize],
|
||||||
|
});
|
||||||
|
|
||||||
|
mod ft_worker_lib;
|
||||||
|
|
||||||
|
pub const VFS_TIMEOUT: u64 = 5; // 5s
|
||||||
|
pub const APP_SHARE_TIMEOUT: u64 = 120; // 120s
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)] // untagged as a meta-type for all incoming responses
|
||||||
|
pub enum Resp {
|
||||||
|
Download(DownloadResponses),
|
||||||
|
HttpClient(Result<client::HttpClientResponse, client::HttpClientError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct State {
|
||||||
|
// persisted metadata about which packages we are mirroring
|
||||||
|
mirroring: HashSet<PackageId>,
|
||||||
|
// note, pending auto_updates are not persisted.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn load() -> Self {
|
||||||
|
match get_state() {
|
||||||
|
Some(blob) => match serde_json::from_slice::<State>(&blob) {
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(_) => State {
|
||||||
|
mirroring: HashSet::new(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => State {
|
||||||
|
mirroring: HashSet::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
call_init!(init);
|
||||||
|
fn init(our: Address) {
|
||||||
|
println!("downloads: started");
|
||||||
|
|
||||||
|
// mirroring metadata is separate from vfs downloads state.
|
||||||
|
let mut state = State::load();
|
||||||
|
|
||||||
|
// /app_store:sys/downloads/
|
||||||
|
vfs::create_drive(our.package_id(), "downloads", None)
|
||||||
|
.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");
|
||||||
|
|
||||||
|
let mut auto_updates: HashSet<(PackageId, String)> = HashSet::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match await_message() {
|
||||||
|
Err(send_error) => {
|
||||||
|
print_to_terminal(1, &format!("got network error: {send_error}"));
|
||||||
|
}
|
||||||
|
Ok(message) => {
|
||||||
|
if let Err(e) = handle_message(
|
||||||
|
&our,
|
||||||
|
&mut state,
|
||||||
|
&message,
|
||||||
|
&mut downloads,
|
||||||
|
&mut tmp,
|
||||||
|
&mut auto_updates,
|
||||||
|
) {
|
||||||
|
print_to_terminal(1, &format!("error handling message: {:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// message router: parse into our Req and Resp types, then pass to
|
||||||
|
/// function defined for each kind of message. check whether the source
|
||||||
|
/// of the message is allowed to send that kind of message to us.
|
||||||
|
/// finally, fire a response if expected from a request.
|
||||||
|
fn handle_message(
|
||||||
|
our: &Address,
|
||||||
|
state: &mut State,
|
||||||
|
message: &Message,
|
||||||
|
downloads: &mut Directory,
|
||||||
|
_tmp: &mut Directory,
|
||||||
|
auto_updates: &mut HashSet<(PackageId, String)>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if message.is_request() {
|
||||||
|
match serde_json::from_slice::<DownloadRequests>(message.body())? {
|
||||||
|
DownloadRequests::LocalDownload(download_request) => {
|
||||||
|
// we want to download a package.
|
||||||
|
if !message.is_local(our) {
|
||||||
|
return Err(anyhow::anyhow!("not local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let LocalDownloadRequest {
|
||||||
|
package_id,
|
||||||
|
download_from,
|
||||||
|
desired_version_hash,
|
||||||
|
} = download_request.clone();
|
||||||
|
|
||||||
|
if download_from.starts_with("http") {
|
||||||
|
// use http_client to GET it
|
||||||
|
Request::to(("our", "http_client", "distro", "sys"))
|
||||||
|
.body(
|
||||||
|
serde_json::to_vec(&client::HttpClientAction::Http(
|
||||||
|
client::OutgoingHttpRequest {
|
||||||
|
method: "GET".to_string(),
|
||||||
|
version: None,
|
||||||
|
url: download_from.clone(),
|
||||||
|
headers: std::collections::HashMap::new(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.context(serde_json::to_vec(&download_request)?)
|
||||||
|
.expects_response(60)
|
||||||
|
.send()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// go download from the node or url
|
||||||
|
// spawn a worker, and send a downlaod to the node.
|
||||||
|
let our_worker = spawn_receive_transfer(
|
||||||
|
our,
|
||||||
|
&package_id,
|
||||||
|
&desired_version_hash,
|
||||||
|
&download_from,
|
||||||
|
APP_SHARE_TIMEOUT,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Request::to((&download_from, "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::RemoteDownload(
|
||||||
|
RemoteDownloadRequest {
|
||||||
|
package_id,
|
||||||
|
desired_version_hash,
|
||||||
|
worker_address: our_worker.to_string(),
|
||||||
|
},
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::RemoteDownload(download_request) => {
|
||||||
|
// this is a node requesting a download from us.
|
||||||
|
// check if we are mirroring. we should maybe implement some back and forth here.
|
||||||
|
// small handshake for started? but we do not really want to wait for that in this loop..
|
||||||
|
// might be okay. implement.
|
||||||
|
let RemoteDownloadRequest {
|
||||||
|
package_id,
|
||||||
|
desired_version_hash,
|
||||||
|
worker_address,
|
||||||
|
} = download_request;
|
||||||
|
|
||||||
|
let target_worker = Address::from_str(&worker_address)?;
|
||||||
|
let _ = spawn_send_transfer(
|
||||||
|
our,
|
||||||
|
&package_id,
|
||||||
|
&desired_version_hash,
|
||||||
|
APP_SHARE_TIMEOUT,
|
||||||
|
&target_worker,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
DownloadRequests::Progress(progress) => {
|
||||||
|
// forward progress to main:app_store:sys,
|
||||||
|
// pushed to UI via websockets
|
||||||
|
let _ = Request::to(("our", "main", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&progress)?)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
DownloadRequests::DownloadComplete(req) => {
|
||||||
|
if !message.is_local(our) {
|
||||||
|
return Err(anyhow::anyhow!("got non local download complete"));
|
||||||
|
}
|
||||||
|
// if we have a pending auto_install, forward that context to the main process.
|
||||||
|
// it will check if the caps_hashes match (no change in capabilities), and auto_install if it does.
|
||||||
|
|
||||||
|
let context = if auto_updates.remove(&(
|
||||||
|
req.package_id.clone().to_process_lib(),
|
||||||
|
req.version_hash.clone(),
|
||||||
|
)) {
|
||||||
|
match get_manifest_hash(
|
||||||
|
req.package_id.clone().to_process_lib(),
|
||||||
|
req.version_hash.clone(),
|
||||||
|
) {
|
||||||
|
Ok(manifest_hash) => Some(manifest_hash.as_bytes().to_vec()),
|
||||||
|
Err(e) => {
|
||||||
|
print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!("auto_update: error getting manifest hash: {:?}", e),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// pushed to UI via websockets
|
||||||
|
let mut request = Request::to(("our", "main", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&req)?);
|
||||||
|
|
||||||
|
if let Some(ctx) = context {
|
||||||
|
request = request.context(ctx);
|
||||||
|
}
|
||||||
|
request.send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::GetFiles(maybe_id) => {
|
||||||
|
// if not local, throw to the boonies.
|
||||||
|
// note, can also implement a discovery protocol here in the future
|
||||||
|
if !message.is_local(our) {
|
||||||
|
return Err(anyhow::anyhow!("got non local get_files"));
|
||||||
|
}
|
||||||
|
let files = match maybe_id {
|
||||||
|
Some(id) => {
|
||||||
|
let package_path =
|
||||||
|
format!("{}/{}", downloads.path, id.to_process_lib().to_string());
|
||||||
|
let dir = vfs::open_dir(&package_path, false, None)?;
|
||||||
|
let dir = dir.read()?;
|
||||||
|
format_entries(dir, state)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let dir = downloads.read()?;
|
||||||
|
format_entries(dir, state)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = DownloadResponses::GetFiles(files);
|
||||||
|
|
||||||
|
Response::new().body(serde_json::to_string(&resp)?).send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::RemoveFile(remove_req) => {
|
||||||
|
if !message.is_local(our) {
|
||||||
|
return Err(anyhow::anyhow!("not local"));
|
||||||
|
}
|
||||||
|
let RemoveFileRequest {
|
||||||
|
package_id,
|
||||||
|
version_hash,
|
||||||
|
} = remove_req;
|
||||||
|
let package_dir = format!(
|
||||||
|
"{}/{}",
|
||||||
|
downloads.path,
|
||||||
|
package_id.to_process_lib().to_string()
|
||||||
|
);
|
||||||
|
let zip_path = format!("{}/{}.zip", package_dir, version_hash);
|
||||||
|
let _ = vfs::remove_file(&zip_path, None);
|
||||||
|
let manifest_path = format!("{}/{}.json", package_dir, version_hash);
|
||||||
|
let _ = vfs::remove_file(&manifest_path, None);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&Resp::Download(
|
||||||
|
DownloadResponses::Success,
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::AddDownload(add_req) => {
|
||||||
|
if !message.is_local(our) {
|
||||||
|
return Err(anyhow::anyhow!("not local"));
|
||||||
|
}
|
||||||
|
let Some(blob) = get_blob() else {
|
||||||
|
return Err(anyhow::anyhow!("could not get blob"));
|
||||||
|
};
|
||||||
|
let bytes = blob.bytes;
|
||||||
|
|
||||||
|
let package_dir = format!(
|
||||||
|
"{}/{}",
|
||||||
|
downloads.path,
|
||||||
|
add_req.package_id.clone().to_process_lib().to_string()
|
||||||
|
);
|
||||||
|
let _ = open_or_create_dir(&package_dir)?;
|
||||||
|
|
||||||
|
// Write the zip file
|
||||||
|
let zip_path = format!("{}/{}.zip", package_dir, add_req.version_hash);
|
||||||
|
let file = vfs::create_file(&zip_path, None)?;
|
||||||
|
file.write(bytes.as_slice())?;
|
||||||
|
|
||||||
|
// Extract and write the manifest
|
||||||
|
let manifest_path = format!("{}/{}.json", package_dir, add_req.version_hash);
|
||||||
|
extract_and_write_manifest(&bytes, &manifest_path)?;
|
||||||
|
|
||||||
|
// add mirrors if applicable and save:
|
||||||
|
if add_req.mirror {
|
||||||
|
state.mirroring.insert(add_req.package_id.to_process_lib());
|
||||||
|
set_state(&serde_json::to_vec(&state)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&Resp::Download(
|
||||||
|
DownloadResponses::Success,
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::StartMirroring(package_id) => {
|
||||||
|
let package_id = package_id.to_process_lib();
|
||||||
|
state.mirroring.insert(package_id);
|
||||||
|
set_state(&serde_json::to_vec(&state)?);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&Resp::Download(
|
||||||
|
DownloadResponses::Success,
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::StopMirroring(package_id) => {
|
||||||
|
let package_id = package_id.to_process_lib();
|
||||||
|
state.mirroring.remove(&package_id);
|
||||||
|
set_state(&serde_json::to_vec(&state)?);
|
||||||
|
Response::new()
|
||||||
|
.body(serde_json::to_vec(&Resp::Download(
|
||||||
|
DownloadResponses::Success,
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
DownloadRequests::AutoUpdate(auto_update_request) => {
|
||||||
|
if !message.is_local(&our)
|
||||||
|
&& message.source().process != ProcessId::new(Some("chain"), "app_store", "sys")
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"got auto-update from non local chain source"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let AutoUpdateRequest {
|
||||||
|
package_id,
|
||||||
|
metadata,
|
||||||
|
} = auto_update_request.clone();
|
||||||
|
let process_lib_package_id = package_id.clone().to_process_lib();
|
||||||
|
|
||||||
|
// default auto_update to publisher. TODO: more config here.
|
||||||
|
let download_from = metadata.properties.publisher;
|
||||||
|
let current_version = metadata.properties.current_version;
|
||||||
|
let code_hashes = metadata.properties.code_hashes;
|
||||||
|
|
||||||
|
let version_hash = code_hashes
|
||||||
|
.iter()
|
||||||
|
.find(|(version, _)| version == ¤t_version)
|
||||||
|
.map(|(_, hash)| hash.clone())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("auto_update: error for package_id: {}, current_version: {}, no matching hash found", process_lib_package_id.to_string(), current_version))?;
|
||||||
|
|
||||||
|
let download_request = LocalDownloadRequest {
|
||||||
|
package_id,
|
||||||
|
download_from,
|
||||||
|
desired_version_hash: version_hash.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// kick off local download to ourselves.
|
||||||
|
Request::to(("our", "downloads", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::LocalDownload(
|
||||||
|
download_request,
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
auto_updates.insert((process_lib_package_id, version_hash));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match serde_json::from_slice::<Resp>(message.body())? {
|
||||||
|
Resp::Download(download_response) => {
|
||||||
|
// these are handled in line.
|
||||||
|
print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!("got a weird download response: {:?}", download_response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Resp::HttpClient(resp) => {
|
||||||
|
let Some(context) = message.context() else {
|
||||||
|
return Err(anyhow::anyhow!("http_client response without context"));
|
||||||
|
};
|
||||||
|
let download_request = serde_json::from_slice::<LocalDownloadRequest>(context)?;
|
||||||
|
if let Ok(client::HttpClientResponse::Http(client::HttpResponse {
|
||||||
|
status, ..
|
||||||
|
})) = resp
|
||||||
|
{
|
||||||
|
if status == 200 {
|
||||||
|
if let Err(e) = handle_receive_http_download(&download_request) {
|
||||||
|
print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!("error handling http_client response: {:?}", e),
|
||||||
|
);
|
||||||
|
Request::to(("our", "main", "app_store", "sys"))
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::DownloadComplete(
|
||||||
|
DownloadCompleteRequest {
|
||||||
|
package_id: download_request.package_id.clone(),
|
||||||
|
version_hash: download_request.desired_version_hash.clone(),
|
||||||
|
error: Some(e),
|
||||||
|
},
|
||||||
|
))?)
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("got http_client error: {resp:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_receive_http_download(
|
||||||
|
download_request: &LocalDownloadRequest,
|
||||||
|
) -> anyhow::Result<(), DownloadError> {
|
||||||
|
let package_id = download_request.package_id.clone().to_process_lib();
|
||||||
|
let version_hash = download_request.desired_version_hash.clone();
|
||||||
|
|
||||||
|
print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!(
|
||||||
|
"Received HTTP download for: {}, with version hash: {}",
|
||||||
|
package_id.to_string(),
|
||||||
|
version_hash
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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 calculated_hash = format!("{:x}", Sha256::digest(&bytes));
|
||||||
|
if calculated_hash != version_hash {
|
||||||
|
return Err(DownloadError::HashMismatch(HashMismatch {
|
||||||
|
desired: version_hash,
|
||||||
|
actual: calculated_hash,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the zip file
|
||||||
|
let zip_path = format!("{}/{}.zip", package_dir, version_hash);
|
||||||
|
let file = vfs::create_file(&zip_path, None).map_err(|_| DownloadError::VfsError)?;
|
||||||
|
file.write(bytes.as_slice())
|
||||||
|
.map_err(|_| DownloadError::VfsError)?;
|
||||||
|
|
||||||
|
// Write the manifest file
|
||||||
|
// Extract and write the manifest
|
||||||
|
let manifest_path = format!("{}/{}.json", package_dir, version_hash);
|
||||||
|
extract_and_write_manifest(&bytes, &manifest_path).map_err(|_| DownloadError::VfsError)?;
|
||||||
|
|
||||||
|
Request::to(("our", "main", "app_store", "sys"))
|
||||||
|
.body(
|
||||||
|
serde_json::to_vec(&DownloadCompleteRequest {
|
||||||
|
package_id: download_request.package_id.clone(),
|
||||||
|
version_hash,
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_entries(entries: Vec<vfs::DirEntry>, state: &State) -> Vec<Entry> {
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let name = entry.path.split('/').last()?.to_string();
|
||||||
|
let is_file = entry.file_type == vfs::FileType::File;
|
||||||
|
|
||||||
|
if is_file && name.ends_with(".zip") {
|
||||||
|
let size = vfs::metadata(&entry.path, None)
|
||||||
|
.map(|meta| meta.len)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let json_path = entry.path.replace(".zip", ".json");
|
||||||
|
let manifest = vfs::open_file(&json_path, false, None)
|
||||||
|
.and_then(|file| file.read_to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Some(Entry::File(FileEntry {
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
manifest,
|
||||||
|
}))
|
||||||
|
} else if !is_file {
|
||||||
|
let mirroring = state.mirroring.iter().any(|pid| {
|
||||||
|
pid.package_name == name
|
||||||
|
|| format!("{}:{}", pid.package_name, pid.publisher_node) == name
|
||||||
|
});
|
||||||
|
Some(Entry::Dir(DirEntry { name, mirroring }))
|
||||||
|
} else {
|
||||||
|
None // Skip non-zip files
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyhow::Result<()> {
|
||||||
|
let reader = std::io::Cursor::new(file_contents);
|
||||||
|
let mut archive = zip::ZipArchive::new(reader)?;
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
if file.name() == "manifest.json" {
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
let manifest_file = open_or_create_file(&manifest_path)?;
|
||||||
|
manifest_file.write(contents.as_bytes())?;
|
||||||
|
|
||||||
|
print_to_terminal(1, &format!("Extracted and wrote manifest.json"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_manifest_hash(package_id: PackageId, version_hash: String) -> anyhow::Result<String> {
|
||||||
|
let package_dir = format!("{}/{}", "/app_store:sys/downloads", package_id.to_string());
|
||||||
|
let manifest_path = format!("{}/{}.json", package_dir, version_hash);
|
||||||
|
let manifest_file = vfs::open_file(&manifest_path, false, None)?;
|
||||||
|
|
||||||
|
let manifest_bytes = manifest_file.read()?;
|
||||||
|
let manifest_hash = keccak_256_hash(&manifest_bytes);
|
||||||
|
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};
|
||||||
|
let mut hasher = Keccak256::new();
|
||||||
|
hasher.update(bytes);
|
||||||
|
format!("0x{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// quite annoyingly, we must convert from our gen'd version of PackageId
|
||||||
|
// to the process_lib's gen'd version. this is in order to access custom
|
||||||
|
// Impls that we want to use
|
||||||
|
impl crate::kinode::process::main::PackageId {
|
||||||
|
pub fn to_process_lib(self) -> PackageId {
|
||||||
|
PackageId {
|
||||||
|
package_name: self.package_name,
|
||||||
|
publisher_node: self.publisher_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_process_lib(package_id: PackageId) -> Self {
|
||||||
|
Self {
|
||||||
|
package_name: package_id.package_name,
|
||||||
|
publisher_node: package_id.publisher_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,11 +9,13 @@ simulation-mode = []
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.8.0" }
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
sha2 = "0.10.8"
|
||||||
wit-bindgen = "0.24.0"
|
wit-bindgen = "0.24.0"
|
||||||
|
zip = { version = "1.1.4", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
@ -1,135 +1,76 @@
|
|||||||
|
use crate::kinode::process::downloads::{
|
||||||
|
DownloadRequests, LocalDownloadRequest, PackageId, RemoteDownloadRequest,
|
||||||
|
};
|
||||||
|
|
||||||
use kinode_process_lib::*;
|
use kinode_process_lib::*;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct FileTransferContext {
|
|
||||||
pub file_name: String,
|
|
||||||
pub file_size: Option<u64>,
|
|
||||||
pub start_time: std::time::SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sent as first Request to a newly spawned worker
|
|
||||||
/// the Receive command will be sent out to target
|
|
||||||
/// in order to prompt them to spawn a worker
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum FTWorkerCommand {
|
|
||||||
/// make sure to attach file itself as blob
|
|
||||||
Send {
|
|
||||||
target: Address,
|
|
||||||
file_name: String,
|
|
||||||
timeout: u64,
|
|
||||||
},
|
|
||||||
Receive {
|
|
||||||
transfer_id: u64,
|
|
||||||
file_name: String,
|
|
||||||
file_size: u64,
|
|
||||||
total_chunks: u64,
|
|
||||||
timeout: u64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// sent as Response by worker to its parent
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum FTWorkerResult {
|
|
||||||
SendSuccess,
|
|
||||||
/// string is name of file. bytes in blob
|
|
||||||
ReceiveSuccess(String),
|
|
||||||
Err(TransferError),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// the possible errors that can be returned to the parent inside `FTWorkerResult`
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum TransferError {
|
|
||||||
TargetOffline,
|
|
||||||
TargetTimeout,
|
|
||||||
TargetRejected,
|
|
||||||
SourceFailed,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper function to spawn a worker and initialize a file transfer.
|
|
||||||
/// The outcome will be sent as an [`FTWorkerResult`] to the caller process.
|
|
||||||
///
|
|
||||||
/// if `file_bytes` is None, expects to inherit blob!
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn spawn_transfer(
|
pub fn spawn_send_transfer(
|
||||||
our: &Address,
|
our: &Address,
|
||||||
file_name: &str,
|
package_id: &PackageId,
|
||||||
file_bytes: Option<Vec<u8>>,
|
version_hash: &str,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
to_addr: &Address,
|
to_addr: &Address,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let transfer_id: u64 = rand::random();
|
let transfer_id: u64 = rand::random();
|
||||||
// spawn a worker and tell it to send the file
|
|
||||||
let Ok(worker_process_id) = spawn(
|
let Ok(worker_process_id) = spawn(
|
||||||
Some(&transfer_id.to_string()),
|
Some(&transfer_id.to_string()),
|
||||||
&format!("{}/pkg/ft_worker.wasm", our.package_id()),
|
&format!("{}/pkg/ft_worker.wasm", our.package_id()),
|
||||||
OnExit::None, // can set message-on-panic here
|
OnExit::None,
|
||||||
our_capabilities(),
|
our_capabilities(),
|
||||||
vec![],
|
vec![],
|
||||||
false, // not public
|
false,
|
||||||
) else {
|
) else {
|
||||||
return Err(anyhow::anyhow!("failed to spawn ft_worker!"));
|
return Err(anyhow::anyhow!("failed to spawn ft_worker!"));
|
||||||
};
|
};
|
||||||
// tell the worker what to do
|
|
||||||
let blob_or_inherit = match file_bytes {
|
let req = Request::new()
|
||||||
Some(bytes) => Some(LazyLoadBlob { mime: None, bytes }),
|
.target((&our.node, worker_process_id))
|
||||||
None => None,
|
.expects_response(timeout + 1)
|
||||||
};
|
|
||||||
let mut req = Request::new()
|
|
||||||
.target((our.node.as_ref(), worker_process_id))
|
|
||||||
.inherit(!blob_or_inherit.is_some())
|
|
||||||
.expects_response(timeout + 1) // don't call with 2^64 lol
|
|
||||||
.body(
|
.body(
|
||||||
serde_json::to_vec(&FTWorkerCommand::Send {
|
serde_json::to_vec(&DownloadRequests::RemoteDownload(RemoteDownloadRequest {
|
||||||
target: to_addr.clone(),
|
package_id: package_id.clone(),
|
||||||
file_name: file_name.into(),
|
desired_version_hash: version_hash.to_string(),
|
||||||
timeout,
|
worker_address: to_addr.to_string(),
|
||||||
})
|
}))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
);
|
||||||
.context(
|
req.send()?;
|
||||||
serde_json::to_vec(&FileTransferContext {
|
Ok(())
|
||||||
file_name: file_name.into(),
|
}
|
||||||
file_size: match &blob_or_inherit {
|
|
||||||
Some(p) => Some(p.bytes.len() as u64),
|
#[allow(dead_code)]
|
||||||
None => None, // TODO
|
pub fn spawn_receive_transfer(
|
||||||
},
|
our: &Address,
|
||||||
start_time: std::time::SystemTime::now(),
|
package_id: &PackageId,
|
||||||
})
|
version_hash: &str,
|
||||||
|
from_node: &str,
|
||||||
|
timeout: u64,
|
||||||
|
) -> anyhow::Result<Address> {
|
||||||
|
let transfer_id: u64 = rand::random();
|
||||||
|
let Ok(worker_process_id) = spawn(
|
||||||
|
Some(&transfer_id.to_string()),
|
||||||
|
&format!("{}/pkg/ft_worker.wasm", our.package_id()),
|
||||||
|
OnExit::None,
|
||||||
|
our_capabilities(),
|
||||||
|
vec![],
|
||||||
|
false,
|
||||||
|
) else {
|
||||||
|
return Err(anyhow::anyhow!("failed to spawn ft_worker!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = Request::new()
|
||||||
|
.target((&our.node, worker_process_id.clone()))
|
||||||
|
.expects_response(timeout + 1)
|
||||||
|
.body(
|
||||||
|
serde_json::to_vec(&DownloadRequests::LocalDownload(LocalDownloadRequest {
|
||||||
|
package_id: package_id.clone(),
|
||||||
|
desired_version_hash: version_hash.to_string(),
|
||||||
|
download_from: from_node.to_string(),
|
||||||
|
}))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(blob) = blob_or_inherit {
|
req.send()?;
|
||||||
req = req.blob(blob);
|
Ok(Address::new(&our.node, worker_process_id))
|
||||||
}
|
|
||||||
req.send()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper function to allow a process to easily handle an incoming transfer
|
|
||||||
/// from an ft_worker. Call this when you get the initial [`FTWorkerCommand::Receive`]
|
|
||||||
/// and let it do the rest. The outcome will be sent as an [`FTWorkerResult`] inside
|
|
||||||
/// a Response to the caller.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn spawn_receive_transfer(our: &Address, body: &[u8]) -> anyhow::Result<()> {
|
|
||||||
let Ok(FTWorkerCommand::Receive { transfer_id, .. }) = serde_json::from_slice(body) else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"spawn_receive_transfer: got malformed request"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let Ok(worker_process_id) = spawn(
|
|
||||||
Some(&transfer_id.to_string()),
|
|
||||||
&format!("{}/pkg/ft_worker.wasm", our.package_id()),
|
|
||||||
OnExit::None, // can set message-on-panic here
|
|
||||||
our_capabilities(),
|
|
||||||
vec![],
|
|
||||||
false, // not public
|
|
||||||
) else {
|
|
||||||
return Err(anyhow::anyhow!("failed to spawn ft_worker!"));
|
|
||||||
};
|
|
||||||
// forward receive command to worker
|
|
||||||
Request::new()
|
|
||||||
.target((our.node.as_ref(), worker_process_id))
|
|
||||||
.inherit(true)
|
|
||||||
.body(body)
|
|
||||||
.send()
|
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,26 @@
|
|||||||
use kinode_process_lib::println;
|
use crate::kinode::process::downloads::{
|
||||||
|
ChunkRequest, DownloadCompleteRequest, DownloadError, DownloadRequests, HashMismatch,
|
||||||
|
LocalDownloadRequest, ProgressUpdate, RemoteDownloadRequest, SizeUpdate,
|
||||||
|
};
|
||||||
use kinode_process_lib::*;
|
use kinode_process_lib::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use kinode_process_lib::{
|
||||||
|
print_to_terminal, println, timer,
|
||||||
|
vfs::{open_dir, open_file, Directory, File, SeekFrom},
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::io::Read;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
mod ft_worker_lib;
|
pub mod ft_worker_lib;
|
||||||
use ft_worker_lib::*;
|
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
path: "target/wit",
|
path: "target/wit",
|
||||||
world: "process-v0",
|
generate_unused_types: true,
|
||||||
|
world: "app-store-sys-v0",
|
||||||
|
additional_derives: [serde::Deserialize, serde::Serialize],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// internal worker protocol
|
const CHUNK_SIZE: u64 = 262144; // 256KB
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum FTWorkerProtocol {
|
|
||||||
Ready,
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
call_init!(init);
|
call_init!(init);
|
||||||
fn init(our: Address) {
|
fn init(our: Address) {
|
||||||
@ -28,150 +33,321 @@ fn init(our: Address) {
|
|||||||
panic!("ft_worker: got bad init message");
|
panic!("ft_worker: got bad init message");
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = serde_json::from_slice::<FTWorkerCommand>(&body)
|
if parent_process.node() != our.node() {
|
||||||
.expect("ft_worker: got unparseable init message");
|
panic!("ft_worker: got bad init message source");
|
||||||
|
}
|
||||||
|
|
||||||
let Some(result) = (match command {
|
// killswitch timer, 2 minutes. sender or receiver gets killed/cleaned up.
|
||||||
FTWorkerCommand::Send {
|
timer::set_timer(120000, None);
|
||||||
target,
|
|
||||||
file_name,
|
let start = std::time::Instant::now();
|
||||||
timeout,
|
|
||||||
} => Some(handle_send(&our, &target, &file_name, timeout)),
|
let req: DownloadRequests =
|
||||||
FTWorkerCommand::Receive {
|
serde_json::from_slice(&body).expect("ft_worker: got unparseable init message");
|
||||||
file_name,
|
|
||||||
total_chunks,
|
match req {
|
||||||
timeout,
|
DownloadRequests::LocalDownload(local_request) => {
|
||||||
|
let LocalDownloadRequest {
|
||||||
|
package_id,
|
||||||
|
desired_version_hash,
|
||||||
..
|
..
|
||||||
} => handle_receive(parent_process, &file_name, total_chunks, timeout),
|
} = local_request;
|
||||||
}) else {
|
match handle_receiver(
|
||||||
return;
|
&parent_process,
|
||||||
};
|
&package_id.to_process_lib(),
|
||||||
|
&desired_version_hash,
|
||||||
|
) {
|
||||||
|
Ok(_) => print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!(
|
||||||
|
"ft_worker: receive downloaded package in {}ms",
|
||||||
|
start.elapsed().as_millis()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Err(e) => print_to_terminal(1, &format!("ft_worker: receive error: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DownloadRequests::RemoteDownload(remote_request) => {
|
||||||
|
let RemoteDownloadRequest {
|
||||||
|
package_id,
|
||||||
|
desired_version_hash,
|
||||||
|
worker_address,
|
||||||
|
} = remote_request;
|
||||||
|
|
||||||
Response::new()
|
match handle_sender(
|
||||||
.body(serde_json::to_vec(&result).unwrap())
|
&worker_address,
|
||||||
.send()
|
&package_id.to_process_lib(),
|
||||||
.unwrap();
|
&desired_version_hash,
|
||||||
|
) {
|
||||||
// job is done
|
Ok(_) => print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!(
|
||||||
|
"ft_worker: sent package to {} in {}ms",
|
||||||
|
worker_address,
|
||||||
|
start.elapsed().as_millis()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Err(e) => print_to_terminal(1, &format!("ft_worker: send error: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => println!("ft_worker: got unexpected message"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_send(our: &Address, target: &Address, file_name: &str, timeout: u64) -> FTWorkerResult {
|
fn handle_sender(worker: &str, package_id: &PackageId, version_hash: &str) -> anyhow::Result<()> {
|
||||||
let transfer_id: u64 = our.process().parse().unwrap();
|
let target_worker = Address::from_str(worker)?;
|
||||||
let Some(blob) = get_blob() else {
|
|
||||||
println!("ft_worker: wasn't given blob!");
|
let filename = format!(
|
||||||
return FTWorkerResult::Err(TransferError::SourceFailed);
|
"/app_store:sys/downloads/{}:{}/{}.zip",
|
||||||
};
|
package_id.package_name, package_id.publisher_node, version_hash
|
||||||
let file_bytes = blob.bytes;
|
);
|
||||||
let mut file_size = file_bytes.len() as u64;
|
|
||||||
let mut offset: u64 = 0;
|
let mut file = open_file(&filename, false, None)?;
|
||||||
let chunk_size: u64 = 1048576; // 1MB, can be changed
|
let size = file.metadata()?.len;
|
||||||
let total_chunks = (file_size as f64 / chunk_size as f64).ceil() as u64;
|
let num_chunks = (size as f64 / CHUNK_SIZE as f64).ceil() as u64;
|
||||||
// send a file to another worker
|
|
||||||
// start by telling target to expect a file,
|
Request::new()
|
||||||
// then upon reciving affirmative response,
|
.body(serde_json::to_vec(&DownloadRequests::Size(SizeUpdate {
|
||||||
// send contents in chunks and wait for
|
package_id: package_id.clone().into(),
|
||||||
// acknowledgement.
|
size,
|
||||||
let Ok(Ok(response)) = Request::to(target.clone())
|
}))?)
|
||||||
.body(
|
.target(target_worker.clone())
|
||||||
serde_json::to_vec(&FTWorkerCommand::Receive {
|
.send()?;
|
||||||
transfer_id,
|
file.seek(SeekFrom::Start(0))?;
|
||||||
file_name: file_name.to_string(),
|
|
||||||
file_size,
|
for i in 0..num_chunks {
|
||||||
total_chunks,
|
send_chunk(&mut file, i, size, &target_worker, package_id, version_hash)?;
|
||||||
timeout,
|
}
|
||||||
})
|
|
||||||
.unwrap(),
|
Ok(())
|
||||||
)
|
}
|
||||||
.send_and_await_response(timeout)
|
|
||||||
else {
|
fn handle_receiver(
|
||||||
return FTWorkerResult::Err(TransferError::TargetOffline);
|
parent_process: &Address,
|
||||||
};
|
package_id: &PackageId,
|
||||||
let opp_worker = response.source();
|
version_hash: &str,
|
||||||
let Ok(FTWorkerProtocol::Ready) = serde_json::from_slice(&response.body()) else {
|
) -> anyhow::Result<()> {
|
||||||
return FTWorkerResult::Err(TransferError::TargetRejected);
|
// TODO: write to a temporary location first, then check hash as we go, then rename to final location.
|
||||||
};
|
|
||||||
// send file in chunks
|
let package_dir = open_or_create_dir(&format!(
|
||||||
|
"/app_store:sys/downloads/{}:{}/",
|
||||||
|
package_id.package_name,
|
||||||
|
package_id.publisher(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
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 size: Option<u64> = None;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if file_size < chunk_size {
|
let message = await_message()?;
|
||||||
// this is the last chunk, so we should expect a Finished response
|
if *message.source() == timer_address {
|
||||||
let _ = Request::to(opp_worker.clone())
|
return Ok(());
|
||||||
.body(vec![])
|
|
||||||
.blob(LazyLoadBlob {
|
|
||||||
mime: None,
|
|
||||||
bytes: file_bytes[offset as usize..offset as usize + file_size as usize]
|
|
||||||
.to_vec(),
|
|
||||||
})
|
|
||||||
.expects_response(timeout)
|
|
||||||
.send();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
let _ = Request::to(opp_worker.clone())
|
let Message::Request { body, .. } = message else {
|
||||||
.body(vec![])
|
return Err(anyhow::anyhow!("ft_worker: got bad message"));
|
||||||
.blob(LazyLoadBlob {
|
|
||||||
mime: None,
|
|
||||||
bytes: file_bytes[offset as usize..offset as usize + chunk_size as usize].to_vec(),
|
|
||||||
})
|
|
||||||
.send();
|
|
||||||
file_size -= chunk_size;
|
|
||||||
offset += chunk_size;
|
|
||||||
}
|
|
||||||
// now wait for Finished response
|
|
||||||
let Ok(Message::Response { body, .. }) = await_message() else {
|
|
||||||
return FTWorkerResult::Err(TransferError::TargetRejected);
|
|
||||||
};
|
};
|
||||||
let Ok(FTWorkerProtocol::Finished) = serde_json::from_slice(&body) else {
|
|
||||||
return FTWorkerResult::Err(TransferError::TargetRejected);
|
let req: DownloadRequests = serde_json::from_slice(&body)?;
|
||||||
|
|
||||||
|
match req {
|
||||||
|
DownloadRequests::Chunk(chunk) => {
|
||||||
|
handle_chunk(&mut file, &chunk, parent_process, &mut size, &mut hasher)?;
|
||||||
|
if let Some(s) = size {
|
||||||
|
if chunk.offset + chunk.length >= s {
|
||||||
|
let recieved_hash = format!("{:x}", hasher.finalize());
|
||||||
|
|
||||||
|
if recieved_hash != version_hash {
|
||||||
|
print_to_terminal(
|
||||||
|
1,
|
||||||
|
&format!(
|
||||||
|
"ft_worker: {} hash mismatch: desired: {} != actual: {}",
|
||||||
|
package_id.to_string(),
|
||||||
|
version_hash,
|
||||||
|
recieved_hash
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let req = DownloadCompleteRequest {
|
||||||
|
package_id: package_id.clone().into(),
|
||||||
|
version_hash: version_hash.to_string(),
|
||||||
|
error: Some(DownloadError::HashMismatch(HashMismatch {
|
||||||
|
desired: version_hash.to_string(),
|
||||||
|
actual: recieved_hash,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
// return success to parent
|
Request::new()
|
||||||
return FTWorkerResult::SendSuccess;
|
.body(serde_json::to_vec(&DownloadRequests::DownloadComplete(
|
||||||
|
req,
|
||||||
|
))?)
|
||||||
|
.target(parent_process.clone())
|
||||||
|
.send()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_receive(
|
let manifest_filename =
|
||||||
parent_process: Address,
|
format!("{}{}.json", package_dir.path, version_hash);
|
||||||
file_name: &str,
|
|
||||||
total_chunks: u64,
|
let contents = file.read()?;
|
||||||
timeout: u64,
|
extract_and_write_manifest(&contents, &manifest_filename)?;
|
||||||
) -> Option<FTWorkerResult> {
|
|
||||||
// send Ready response to counterparty
|
Request::new()
|
||||||
Response::new()
|
.body(serde_json::to_vec(&DownloadRequests::DownloadComplete(
|
||||||
.body(serde_json::to_vec(&FTWorkerProtocol::Ready).unwrap())
|
DownloadCompleteRequest {
|
||||||
.send()
|
package_id: package_id.clone().into(),
|
||||||
.unwrap();
|
version_hash: version_hash.to_string(),
|
||||||
// receive a file from a worker, then send it to parent
|
error: None,
|
||||||
// all messages will be chunks of file. when we receive the
|
},
|
||||||
// last chunk, send a Finished message to sender and Success to parent.
|
))?)
|
||||||
let mut file_bytes = Vec::new();
|
.target(parent_process.clone())
|
||||||
let mut chunks_received = 0;
|
.send()?;
|
||||||
let start_time = std::time::Instant::now();
|
return Ok(());
|
||||||
loop {
|
|
||||||
let Ok(Message::Request { .. }) = await_message() else {
|
|
||||||
return Some(FTWorkerResult::Err(TransferError::SourceFailed));
|
|
||||||
};
|
|
||||||
if start_time.elapsed().as_secs() > timeout {
|
|
||||||
return Some(FTWorkerResult::Err(TransferError::SourceFailed));
|
|
||||||
}
|
}
|
||||||
let Some(blob) = get_blob() else {
|
}
|
||||||
return Some(FTWorkerResult::Err(TransferError::SourceFailed));
|
}
|
||||||
|
DownloadRequests::Size(update) => {
|
||||||
|
size = Some(update.size);
|
||||||
|
}
|
||||||
|
_ => println!("ft_worker: got unexpected message"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_chunk(
|
||||||
|
file: &mut File,
|
||||||
|
chunk_index: u64,
|
||||||
|
total_size: u64,
|
||||||
|
target: &Address,
|
||||||
|
package_id: &PackageId,
|
||||||
|
version_hash: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let offset = chunk_index * CHUNK_SIZE;
|
||||||
|
let length = CHUNK_SIZE.min(total_size - offset);
|
||||||
|
|
||||||
|
let mut buffer = vec![0; length as usize];
|
||||||
|
// this extra seek might be unnecessary. fix multireads per process in vfs
|
||||||
|
file.seek(SeekFrom::Start(offset))?;
|
||||||
|
file.read_at(&mut buffer)?;
|
||||||
|
|
||||||
|
Request::new()
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::Chunk(
|
||||||
|
ChunkRequest {
|
||||||
|
package_id: package_id.clone().into(),
|
||||||
|
version_hash: version_hash.to_string(),
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
},
|
||||||
|
))?)
|
||||||
|
.target(target.clone())
|
||||||
|
.blob_bytes(buffer)
|
||||||
|
.send()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_chunk(
|
||||||
|
file: &mut File,
|
||||||
|
chunk: &ChunkRequest,
|
||||||
|
parent: &Address,
|
||||||
|
size: &mut Option<u64>,
|
||||||
|
hasher: &mut Sha256,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let bytes = if let Some(blob) = get_blob() {
|
||||||
|
blob.bytes
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("ft_worker: got no blob"));
|
||||||
};
|
};
|
||||||
chunks_received += 1;
|
|
||||||
file_bytes.extend(blob.bytes);
|
file.write_all(&bytes)?;
|
||||||
if chunks_received == total_chunks {
|
hasher.update(&bytes);
|
||||||
|
|
||||||
|
if let Some(total_size) = size {
|
||||||
|
// let progress = ((chunk.offset + chunk.length) as f64 / *total_size as f64 * 100.0) as u64;
|
||||||
|
|
||||||
|
Request::new()
|
||||||
|
.body(serde_json::to_vec(&DownloadRequests::Progress(
|
||||||
|
ProgressUpdate {
|
||||||
|
package_id: chunk.package_id.clone(),
|
||||||
|
downloaded: chunk.offset + chunk.length,
|
||||||
|
total: *total_size,
|
||||||
|
version_hash: chunk.version_hash.clone(),
|
||||||
|
},
|
||||||
|
))?)
|
||||||
|
.target(parent.clone())
|
||||||
|
.send()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyhow::Result<()> {
|
||||||
|
let reader = std::io::Cursor::new(file_contents);
|
||||||
|
let mut archive = zip::ZipArchive::new(reader)?;
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
if file.name() == "manifest.json" {
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
let manifest_file = open_or_create_file(&manifest_path)?;
|
||||||
|
manifest_file.write(contents.as_bytes())?;
|
||||||
|
|
||||||
|
print_to_terminal(1, "Extracted and wrote manifest.json");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// send Finished message to sender
|
|
||||||
Response::new()
|
Ok(())
|
||||||
.body(serde_json::to_vec(&FTWorkerProtocol::Finished).unwrap())
|
}
|
||||||
.send()
|
|
||||||
.unwrap();
|
/// helper function for vfs files, open if exists, if not create
|
||||||
// send Success message to parent
|
fn open_or_create_file(path: &str) -> anyhow::Result<File> {
|
||||||
Request::to(parent_process)
|
match open_file(path, false, None) {
|
||||||
.body(serde_json::to_vec(&FTWorkerResult::ReceiveSuccess(file_name.to_string())).unwrap())
|
Ok(file) => Ok(file),
|
||||||
.blob(LazyLoadBlob {
|
Err(_) => match open_file(path, true, None) {
|
||||||
mime: None,
|
Ok(file) => Ok(file),
|
||||||
bytes: file_bytes,
|
Err(_) => Err(anyhow::anyhow!("could not create file")),
|
||||||
})
|
},
|
||||||
.send()
|
}
|
||||||
.unwrap();
|
}
|
||||||
None
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_process_lib(package_id: &kinode_process_lib::PackageId) -> Self {
|
||||||
|
Self {
|
||||||
|
package_name: package_id.package_name.clone(),
|
||||||
|
publisher_node: package_id.publisher_node.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion from wit PackageId to process_lib's PackageId
|
||||||
|
impl From<crate::kinode::process::downloads::PackageId> for kinode_process_lib::PackageId {
|
||||||
|
fn from(package_id: crate::kinode::process::downloads::PackageId) -> Self {
|
||||||
|
kinode_process_lib::PackageId::new(&package_id.package_name, &package_id.publisher_node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion from process_lib's PackageId to wit PackageId
|
||||||
|
impl From<kinode_process_lib::PackageId> for crate::kinode::process::downloads::PackageId {
|
||||||
|
fn from(package_id: kinode_process_lib::PackageId) -> Self {
|
||||||
|
Self {
|
||||||
|
package_name: package_id.package_name,
|
||||||
|
publisher_node: package_id.publisher_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ simulation-mode = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.8.0" }
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
wit-bindgen = "0.24.0"
|
wit-bindgen = "0.24.0"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use crate::kinode::process::main::{InstallResponse, LocalRequest, LocalResponse};
|
use crate::kinode::process::main::{
|
||||||
|
InstallPackageRequest, InstallResponse, LocalRequest, LocalResponse,
|
||||||
|
};
|
||||||
use kinode_process_lib::{
|
use kinode_process_lib::{
|
||||||
await_next_message_body, call_init, println, Address, Message, PackageId, Request,
|
await_next_message_body, call_init, println, Address, Message, PackageId, Request,
|
||||||
};
|
};
|
||||||
@ -18,28 +20,35 @@ fn init(our: Address) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let arg = String::from_utf8(body).unwrap_or_default();
|
let arg = String::from_utf8(body).unwrap_or_default();
|
||||||
|
let args: Vec<&str> = arg.split_whitespace().collect();
|
||||||
|
|
||||||
if arg.is_empty() {
|
if args.len() != 2 {
|
||||||
println!("install: 1 argument required, the package id of the app");
|
println!(
|
||||||
println!("example: install app:publisher.os");
|
"install: 2 arguments required, the package id of the app and desired version_hash"
|
||||||
|
);
|
||||||
|
println!("example: install app:publisher.os f5d374ab50e66888a7c2332b22d0f909f2e3115040725cfab98dcae488916990");
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
let Ok(package_id) = arg.parse::<PackageId>() else {
|
let Ok(package_id) = args[0].parse::<PackageId>() else {
|
||||||
println!("install: invalid package id, make sure to include package name and publisher");
|
println!("install: invalid package id, make sure to include package name and publisher");
|
||||||
println!("example: app_name:publisher_name");
|
println!("example: app_name:publisher_name");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let version_hash = args[1].to_string();
|
||||||
|
|
||||||
let Ok(Ok(Message::Response { body, .. })) =
|
let Ok(Ok(Message::Response { body, .. })) =
|
||||||
Request::to((our.node(), ("main", "app_store", "sys")))
|
Request::to((our.node(), ("main", "app_store", "sys")))
|
||||||
.body(
|
.body(
|
||||||
serde_json::to_vec(&LocalRequest::Install(
|
serde_json::to_vec(&LocalRequest::Install(InstallPackageRequest {
|
||||||
crate::kinode::process::main::PackageId {
|
package_id: crate::kinode::process::main::PackageId {
|
||||||
package_name: package_id.package_name.clone(),
|
package_name: package_id.package_name.clone(),
|
||||||
publisher_node: package_id.publisher_node.clone(),
|
publisher_node: package_id.publisher_node.clone(),
|
||||||
},
|
},
|
||||||
))
|
version_hash,
|
||||||
|
metadata: None,
|
||||||
|
}))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.send_and_await_response(5)
|
.send_and_await_response(5)
|
||||||
|
@ -1,4 +1,59 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"process_name": "downloads",
|
||||||
|
"process_wasm_path": "/downloads.wasm",
|
||||||
|
"on_exit": "Restart",
|
||||||
|
"request_networking": true,
|
||||||
|
"request_capabilities": [
|
||||||
|
"http_client:distro:sys",
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"main:app_store:sys",
|
||||||
|
"chain:app_store:sys",
|
||||||
|
"vfs:distro:sys",
|
||||||
|
{
|
||||||
|
"process": "vfs:distro:sys",
|
||||||
|
"params": {
|
||||||
|
"root": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grant_capabilities": [
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"vfs:distro:sys",
|
||||||
|
"http_client:distro:sys"
|
||||||
|
],
|
||||||
|
"public": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"process_name": "chain",
|
||||||
|
"process_wasm_path": "/chain.wasm",
|
||||||
|
"on_exit": "Restart",
|
||||||
|
"request_networking": true,
|
||||||
|
"request_capabilities": [
|
||||||
|
"main:app_store:sys",
|
||||||
|
"downloads:app_store:sys",
|
||||||
|
"vfs:distro:sys",
|
||||||
|
"kns_indexer:kns_indexer:sys",
|
||||||
|
"eth:distro:sys",
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"http_client:distro:sys",
|
||||||
|
{
|
||||||
|
"process": "vfs:distro:sys",
|
||||||
|
"params": {
|
||||||
|
"root": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grant_capabilities": [
|
||||||
|
"http_server:distro:sys",
|
||||||
|
"kns_indexer:kns_indexer:sys",
|
||||||
|
"vfs:distro:sys",
|
||||||
|
"http_client:distro:sys",
|
||||||
|
"eth:distro:sys",
|
||||||
|
"timer:distro:sys"
|
||||||
|
],
|
||||||
|
"public": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"process_name": "main",
|
"process_name": "main",
|
||||||
"process_wasm_path": "/app_store.wasm",
|
"process_wasm_path": "/app_store.wasm",
|
||||||
@ -11,6 +66,8 @@
|
|||||||
"http_server:distro:sys",
|
"http_server:distro:sys",
|
||||||
"http_client:distro:sys",
|
"http_client:distro:sys",
|
||||||
"net:distro:sys",
|
"net:distro:sys",
|
||||||
|
"downloads:app_store:sys",
|
||||||
|
"chain:app_store:sys",
|
||||||
"vfs:distro:sys",
|
"vfs:distro:sys",
|
||||||
"kernel:distro:sys",
|
"kernel:distro:sys",
|
||||||
"eth:distro:sys",
|
"eth:distro:sys",
|
||||||
@ -33,6 +90,7 @@
|
|||||||
],
|
],
|
||||||
"grant_capabilities": [
|
"grant_capabilities": [
|
||||||
"eth:distro:sys",
|
"eth:distro:sys",
|
||||||
|
"net:distro:sys",
|
||||||
"http_client:distro:sys",
|
"http_client:distro:sys",
|
||||||
"http_server:distro:sys",
|
"http_server:distro:sys",
|
||||||
"kns_indexer:kns_indexer:sys",
|
"kns_indexer:kns_indexer:sys",
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,25 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<!-- This sets window.our.node -->
|
|
||||||
<script src="/our.js"></script>
|
|
||||||
|
|
||||||
<title>Package Store</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="cache-control" content="no-cache" />
|
|
||||||
<link rel="icon"
|
|
||||||
href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzc5IiBoZWlnaHQ9IjUxNCIgdmlld0JveD0iMCAwIDc3OSA1MTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8c3R5bGU+CiAgICAgICAgQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewogICAgICAgICAgICBzdmcgeyBmaWxsOiB3aGl0ZTsgfQogICAgICAgIH0KICAgICAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBsaWdodCkgewogICAgICAgICAgICBzdmcgeyBmaWxsOiBibGFjazsgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8cGF0aCBkPSJNNzUzLjA5MiA1LjkxOTMyQzc1Ni41NTcgNS4wOTk3NiA3NTUuOTYyIC0wLjAwMDEyMjA3IDc1Mi40MDEgLTAuMDAwMTIyMDdINDI2LjAwMUM0MjQuNzU1IC0wLjAwMDEyMjA3IDQyMy42MzkgMC43NzAyNyA0MjMuMTk3IDEuOTM1MzVMMjM2Ljk2OCA0OTIuNkMyMzUuNzI5IDQ5NS44NjUgMjQwLjEyMyA0OTguMjU1IDI0Mi4xOTEgNDk1LjQ0MUw1NjkuMzU3IDUwLjExMzJDNTY5Ljc3OCA0OS41MzkyIDU3MC4zOTEgNDkuMTMzOSA1NzEuMDg0IDQ4Ljk3TDc1My4wOTIgNS45MTkzMloiLz4KICAgIDxwYXRoIGQ9Ik0xMS45NjY1IDQwLjIyODhDOS4xMDk0OSAzOC43NzcgMTAuMjEzNSAzNC40NTgzIDEzLjQxNjcgMzQuNTU1N0w0MDQuMjczIDQ2LjQzNjdDNDA2LjMzNCA0Ni40OTkzIDQwNy43MTkgNDguNTc0OSA0MDYuOTg2IDUwLjUwMjNMMzQ3LjQzOCAyMDYuOTgxQzM0Ni44MDQgMjA4LjY0NyAzNDQuODY1IDIwOS4zOTYgMzQzLjI3NSAyMDguNTg4TDExLjk2NjUgNDAuMjI4OFoiLz4KPC9zdmc+Cg==">
|
|
||||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
|
||||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-I5kjLT9f.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-fGthT1qI.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
@ -1 +1 @@
|
|||||||
npm run build:copy && cd ~/kinode && cargo +nightly build -p kinode && cd kinode/packages/app_store/ui
|
npm install && npm run build:copy
|
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Chat Template",
|
|
||||||
"subtitle": "The chat template from kit",
|
|
||||||
"description": "The kit chat template is the default app when starting a new kit project. This app is the basic version of that, packaged for the app store.",
|
|
||||||
"image": "https://st4.depositphotos.com/7662228/30134/v/450/depositphotos_301343880-stock-illustration-best-chat-speech-bubble-icon.jpg",
|
|
||||||
"version": "0.1.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"website": "https://kinode.org",
|
|
||||||
"screenshots": [
|
|
||||||
"https://pongo-uploads.s3.us-east-2.amazonaws.com/Screenshot+2024-01-30+at+10.01.46+PM.png",
|
|
||||||
"https://pongo-uploads.s3.us-east-2.amazonaws.com/Screenshot+2024-01-30+at+10.01.52+PM.png"
|
|
||||||
],
|
|
||||||
"mirrors": [
|
|
||||||
"odinsbadeye.os"
|
|
||||||
],
|
|
||||||
"versions": [
|
|
||||||
"a2c584bf63a730efdc79ec0a3c93bc97eba4e8745c633e3abe090b4f7e270e92",
|
|
||||||
"c13f7ae39fa7f652164cfc1db305cd864cc1dc5f33827a2d74f7dde70ef36662",
|
|
||||||
"09d24205d8e1f3634448e881db200b88ad691bbdaabbccb885b225147ba4a93e",
|
|
||||||
"733be24324802a35944a73f355595f781de65d9d6e393bdabe879edcb77dfb62"
|
|
||||||
]
|
|
||||||
}
|
|
@ -5,10 +5,11 @@
|
|||||||
<!-- This sets window.our.node -->
|
<!-- This sets window.our.node -->
|
||||||
<script src="/our.js"></script>
|
<script src="/our.js"></script>
|
||||||
|
|
||||||
<title>Package Store</title>
|
<title>App Store</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="pragma" content="no-cache" />
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
<meta http-equiv="cache-control" content="no-cache" />
|
<meta http-equiv="cache-control" content="no-cache" />
|
||||||
|
<link rel="stylesheet" href="/kinode.css">
|
||||||
<link rel="icon"
|
<link rel="icon"
|
||||||
href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzc5IiBoZWlnaHQ9IjUxNCIgdmlld0JveD0iMCAwIDc3OSA1MTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8c3R5bGU+CiAgICAgICAgQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewogICAgICAgICAgICBzdmcgeyBmaWxsOiB3aGl0ZTsgfQogICAgICAgIH0KICAgICAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBsaWdodCkgewogICAgICAgICAgICBzdmcgeyBmaWxsOiBibGFjazsgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8cGF0aCBkPSJNNzUzLjA5MiA1LjkxOTMyQzc1Ni41NTcgNS4wOTk3NiA3NTUuOTYyIC0wLjAwMDEyMjA3IDc1Mi40MDEgLTAuMDAwMTIyMDdINDI2LjAwMUM0MjQuNzU1IC0wLjAwMDEyMjA3IDQyMy42MzkgMC43NzAyNyA0MjMuMTk3IDEuOTM1MzVMMjM2Ljk2OCA0OTIuNkMyMzUuNzI5IDQ5NS44NjUgMjQwLjEyMyA0OTguMjU1IDI0Mi4xOTEgNDk1LjQ0MUw1NjkuMzU3IDUwLjExMzJDNTY5Ljc3OCA0OS41MzkyIDU3MC4zOTEgNDkuMTMzOSA1NzEuMDg0IDQ4Ljk3TDc1My4wOTIgNS45MTkzMloiLz4KICAgIDxwYXRoIGQ9Ik0xMS45NjY1IDQwLjIyODhDOS4xMDk0OSAzOC43NzcgMTAuMjEzNSAzNC40NTgzIDEzLjQxNjcgMzQuNTU1N0w0MDQuMjczIDQ2LjQzNjdDNDA2LjMzNCA0Ni40OTkzIDQwNy43MTkgNDguNTc0OSA0MDYuOTg2IDUwLjUwMjNMMzQ3LjQzOCAyMDYuOTgxQzM0Ni44MDQgMjA4LjY0NyAzNDQuODY1IDIwOS4zOTYgMzQzLjI3NSAyMDguNTg4TDExLjk2NjUgNDAuMjI4OFoiLz4KPC9zdmc+Cg==">
|
href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzc5IiBoZWlnaHQ9IjUxNCIgdmlld0JveD0iMCAwIDc3OSA1MTQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8c3R5bGU+CiAgICAgICAgQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewogICAgICAgICAgICBzdmcgeyBmaWxsOiB3aGl0ZTsgfQogICAgICAgIH0KICAgICAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBsaWdodCkgewogICAgICAgICAgICBzdmcgeyBmaWxsOiBibGFjazsgfQogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8cGF0aCBkPSJNNzUzLjA5MiA1LjkxOTMyQzc1Ni41NTcgNS4wOTk3NiA3NTUuOTYyIC0wLjAwMDEyMjA3IDc1Mi40MDEgLTAuMDAwMTIyMDdINDI2LjAwMUM0MjQuNzU1IC0wLjAwMDEyMjA3IDQyMy42MzkgMC43NzAyNyA0MjMuMTk3IDEuOTM1MzVMMjM2Ljk2OCA0OTIuNkMyMzUuNzI5IDQ5NS44NjUgMjQwLjEyMyA0OTguMjU1IDI0Mi4xOTEgNDk1LjQ0MUw1NjkuMzU3IDUwLjExMzJDNTY5Ljc3OCA0OS41MzkyIDU3MC4zOTEgNDkuMTMzOSA1NzEuMDg0IDQ4Ljk3TDc1My4wOTIgNS45MTkzMloiLz4KICAgIDxwYXRoIGQ9Ik0xMS45NjY1IDQwLjIyODhDOS4xMDk0OSAzOC43NzcgMTAuMjEzNSAzNC40NTgzIDEzLjQxNjcgMzQuNTU1N0w0MDQuMjczIDQ2LjQzNjdDNDA2LjMzNCA0Ni40OTkzIDQwNy43MTkgNDguNTc0OSA0MDYuOTg2IDUwLjUwMjNMMzQ3LjQzOCAyMDYuOTgxQzM0Ni44MDQgMjA4LjY0NyAzNDQuODY1IDIwOS4zOTYgMzQzLjI3NSAyMDguNTg4TDExLjk2NjUgNDAuMjI4OFoiLz4KPC9zdmc+Cg==">
|
||||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
14597
kinode/packages/app_store/ui/package-lock.json
generated
14597
kinode/packages/app_store/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,38 +8,27 @@
|
|||||||
"start": "vite --port 3000",
|
"start": "vite --port 3000",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"copy": "mkdir -p ../pkg/ui && rm -rf ../pkg/ui/* && cp -r dist/* ../pkg/ui/",
|
"copy": "mkdir -p ../pkg/ui && rm -rf ../pkg/ui/* && cp -r dist/* ../pkg/ui/",
|
||||||
"build:copy": "npm run tc && npm run build && npm run copy",
|
"build:copy": "npm run build && npm run copy",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"tc": "typechain --target ethers-v5 --out-dir src/abis/types/ \"./src/abis/**/*.json\""
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hash": "^5.7.0",
|
|
||||||
"@kinode/client-api": "^0.1.0",
|
"@kinode/client-api": "^0.1.0",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
|
"@rainbow-me/rainbowkit": "^2.1.2",
|
||||||
"@szhsin/react-menu": "^4.1.0",
|
"@szhsin/react-menu": "^4.1.0",
|
||||||
"@web3-react/coinbase-wallet": "^8.2.3",
|
"@tanstack/react-query": "^5.45.1",
|
||||||
"@web3-react/core": "^8.2.2",
|
"idna-uts46-hx": "^6.0.4",
|
||||||
"@web3-react/gnosis-safe": "^8.2.4",
|
"js-sha3": "^0.9.3",
|
||||||
"@web3-react/injected-connector": "^6.0.7",
|
|
||||||
"@web3-react/metamask": "^8.2.3",
|
|
||||||
"@web3-react/network": "^8.2.3",
|
|
||||||
"@web3-react/types": "^8.2.2",
|
|
||||||
"@web3-react/walletconnect": "^8.2.3",
|
|
||||||
"@web3-react/walletconnect-connector": "^6.2.13",
|
|
||||||
"@web3-react/walletconnect-v2": "^8.5.1",
|
|
||||||
"classnames": "^2.5.1",
|
|
||||||
"ethers": "^5.7.2",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.0.1",
|
||||||
"react-router-dom": "^6.21.3",
|
"react-router-dom": "^6.21.3",
|
||||||
"tailwindcss": "^3.4.3",
|
"viem": "^2.15.1",
|
||||||
"unocss": "^0.59.0-beta.1",
|
"wagmi": "^2.10.3",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typechain/ethers-v5": "^11.1.1",
|
|
||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
@ -52,6 +41,7 @@
|
|||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"typechain": "^8.3.1",
|
"typechain": "^8.3.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.0.8"
|
"vite": "^5.0.8",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +0,0 @@
|
|||||||
<svg width="779" height="514" viewBox="0 0 779 514" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M753.092 5.91932C756.557 5.09976 755.962 -0.00012207 752.401 -0.00012207H426.001C424.755 -0.00012207 423.639 0.77027 423.197 1.93535L236.968 492.6C235.729 495.865 240.123 498.255 242.191 495.441L569.357 50.1132C569.778 49.5392 570.391 49.1339 571.084 48.97L753.092 5.91932Z" fill="#FFF5D9"/>
|
|
||||||
<path d="M11.9665 40.2288C9.10949 38.777 10.2135 34.4583 13.4167 34.5557L404.273 46.4367C406.334 46.4993 407.719 48.5749 406.986 50.5023L347.438 206.981C346.804 208.647 344.865 209.396 343.275 208.588L11.9665 40.2288Z" fill="#FFF5D9"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 644 B |
@ -1,126 +1,33 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||||
import { Web3ReactProvider, Web3ReactHooks } from '@web3-react/core';
|
|
||||||
import type { MetaMask } from '@web3-react/metamask'
|
|
||||||
|
|
||||||
import { PackageStore, PackageStore__factory } from "./abis/types";
|
import Header from "./components/Header";
|
||||||
|
import { APP_DETAILS_PATH, DOWNLOAD_PATH, MY_DOWNLOADS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
||||||
|
|
||||||
import StorePage from "./pages/StorePage";
|
import StorePage from "./pages/StorePage";
|
||||||
import MyAppsPage from "./pages/MyAppsPage";
|
|
||||||
import AppPage from "./pages/AppPage";
|
import AppPage from "./pages/AppPage";
|
||||||
import { APP_DETAILS_PATH, MY_APPS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
import DownloadPage from "./pages/DownloadPage";
|
||||||
import { ChainId, PACKAGE_STORE_ADDRESSES } from "./constants/chain";
|
|
||||||
import PublishPage from "./pages/PublishPage";
|
import PublishPage from "./pages/PublishPage";
|
||||||
import { hooks as metaMaskHooks, metaMask } from './utils/metamask'
|
import MyDownloadsPage from "./pages/MyDownloadsPage";
|
||||||
|
|
||||||
const connectors: [MetaMask, Web3ReactHooks][] = [
|
|
||||||
[metaMask, metaMaskHooks],
|
|
||||||
]
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface ImportMeta {
|
|
||||||
env: {
|
|
||||||
VITE_OPTIMISM_RPC_URL: string;
|
|
||||||
VITE_SEPOLIA_RPC_URL: string;
|
|
||||||
BASE_URL: string;
|
|
||||||
VITE_NODE_URL?: string;
|
|
||||||
DEV: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
interface Window {
|
|
||||||
our: {
|
|
||||||
node: string;
|
|
||||||
process: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
useProvider,
|
|
||||||
} = metaMaskHooks;
|
|
||||||
|
|
||||||
const RPC_URL = import.meta.env.VITE_OPTIMISM_RPC_URL;
|
|
||||||
const BASE_URL = import.meta.env.BASE_URL;
|
const BASE_URL = import.meta.env.BASE_URL;
|
||||||
if (window.our) window.our.process = BASE_URL?.replace("/", "");
|
if (window.our) window.our.process = BASE_URL?.replace("/", "");
|
||||||
|
|
||||||
const PROXY_TARGET = `${import.meta.env.VITE_NODE_URL || "http://localhost:8080"
|
|
||||||
}${BASE_URL}`;
|
|
||||||
|
|
||||||
// This env also has BASE_URL which should match the process + package name
|
|
||||||
const WEBSOCKET_URL = import.meta.env.DEV // eslint-disable-line
|
|
||||||
? `${PROXY_TARGET.replace("http", "ws")}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const provider = useProvider();
|
|
||||||
const [nodeConnected, setNodeConnected] = useState(true); // eslint-disable-line
|
|
||||||
|
|
||||||
const [packageAbi, setPackageAbi] = useState<PackageStore | undefined>(undefined);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!provider) return;
|
|
||||||
|
|
||||||
const updatePackageAbi = async () => {
|
|
||||||
const network = await provider.getNetwork();
|
|
||||||
if (network.chainId === ChainId.OPTIMISM) {
|
|
||||||
setPackageAbi(PackageStore__factory.connect(
|
|
||||||
PACKAGE_STORE_ADDRESSES[ChainId.OPTIMISM],
|
|
||||||
provider.getSigner())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updatePackageAbi();
|
|
||||||
|
|
||||||
}, [provider])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// if (window.our?.node && window.our?.process) {
|
|
||||||
// const api = new KinodeClientApi({
|
|
||||||
// uri: WEBSOCKET_URL,
|
|
||||||
// nodeId: window.our.node,
|
|
||||||
// processId: window.our.process,
|
|
||||||
// onOpen: (_event, _api) => {
|
|
||||||
// console.log("Connected to Kinode");
|
|
||||||
// // api.send({ data: "Hello World" });
|
|
||||||
// },
|
|
||||||
// onMessage: (json, _api) => {
|
|
||||||
// console.log('UNEXPECTED WEBSOCKET MESSAGE', json)
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// setApi(api);
|
|
||||||
// } else {
|
|
||||||
// setNodeConnected(false);
|
|
||||||
// }
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!nodeConnected) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col c">
|
|
||||||
<h2 style={{ color: "red" }}>Node not connected</h2>
|
|
||||||
<h4>
|
|
||||||
You need to start a node at {PROXY_TARGET} before you can use this UI
|
|
||||||
in development.
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = { provider, packageAbi };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col c h-screen w-screen max-h-screen max-w-screen overflow-x-hidden special-appstore-background">
|
<div>
|
||||||
<Web3ReactProvider connectors={connectors}>
|
|
||||||
<Router basename={BASE_URL}>
|
<Router basename={BASE_URL}>
|
||||||
|
<Header />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={STORE_PATH} element={<StorePage />} />
|
<Route path={STORE_PATH} element={<StorePage />} />
|
||||||
<Route path={MY_APPS_PATH} element={<MyAppsPage />} />
|
<Route path={MY_DOWNLOADS_PATH} element={<MyDownloadsPage />} />
|
||||||
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
|
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />
|
||||||
<Route path={PUBLISH_PATH} element={<PublishPage {...props} />} />
|
<Route path={PUBLISH_PATH} element={<PublishPage />} />
|
||||||
|
<Route path={`${DOWNLOAD_PATH}/:id`} element={<DownloadPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</Web3ReactProvider>
|
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,978 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "UPGRADE_INTERFACE_VERSION",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "approve",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "apps",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherKnsNodeId",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "balanceOf",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "contractURI",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "getApproved",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "getInitializedVersion",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "uint64",
|
|
||||||
"internalType": "uint64"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "getPackageId",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherName",
|
|
||||||
"type": "bytes",
|
|
||||||
"internalType": "bytes"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "pure"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "getPackageInfo",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "package",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "tuple",
|
|
||||||
"internalType": "struct IKinodeAppStore.PackageInfo",
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherKnsNodeId",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "getPackageInfo",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherName",
|
|
||||||
"type": "bytes",
|
|
||||||
"internalType": "bytes"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "tuple",
|
|
||||||
"internalType": "struct IKinodeAppStore.PackageInfo",
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherKnsNodeId",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "initialize",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "_knsResolver",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "contract KNSRegistryResolver"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "isApprovedForAll",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "operator",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "bool",
|
|
||||||
"internalType": "bool"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "knsResolver",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "contract KNSRegistryResolver"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "name",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "owner",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "ownerOf",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "proxiableUUID",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "registerApp",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherName",
|
|
||||||
"type": "bytes",
|
|
||||||
"internalType": "bytes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "renounceOwnership",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "safeTransferFrom",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "safeTransferFrom",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data",
|
|
||||||
"type": "bytes",
|
|
||||||
"internalType": "bytes"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "setApprovalForAll",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "operator",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "approved",
|
|
||||||
"type": "bool",
|
|
||||||
"internalType": "bool"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "supportsInterface",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "interfaceId",
|
|
||||||
"type": "bytes4",
|
|
||||||
"internalType": "bytes4"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "bool",
|
|
||||||
"internalType": "bool"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "symbol",
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "tokenURI",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"stateMutability": "view"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "transferFrom",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "transferOwnership",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "newOwner",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "unlistPacakge",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "package",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "updateContractURI",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "uri",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "updateMetadata",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "package",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "nonpayable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"name": "upgradeToAndCall",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "newImplementation",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "data",
|
|
||||||
"type": "bytes",
|
|
||||||
"internalType": "bytes"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"stateMutability": "payable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "AppMetadataUpdated",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "package",
|
|
||||||
"type": "uint256",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "AppRegistered",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "package",
|
|
||||||
"type": "uint256",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "packageName",
|
|
||||||
"type": "string",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publisherName",
|
|
||||||
"type": "bytes",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "bytes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataUrl",
|
|
||||||
"type": "string",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "metadataHash",
|
|
||||||
"type": "bytes32",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "Approval",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "approved",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "ApprovalForAll",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "operator",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "approved",
|
|
||||||
"type": "bool",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "bool"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "Initialized",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "version",
|
|
||||||
"type": "uint64",
|
|
||||||
"indexed": false,
|
|
||||||
"internalType": "uint64"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "OwnershipTransferred",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "previousOwner",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "newOwner",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "Transfer",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "event",
|
|
||||||
"name": "Upgraded",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "implementation",
|
|
||||||
"type": "address",
|
|
||||||
"indexed": true,
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"anonymous": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "AddressEmptyCode",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "target",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC1967InvalidImplementation",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "implementation",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC1967NonPayable",
|
|
||||||
"inputs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721IncorrectOwner",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "sender",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721InsufficientApproval",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "operator",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721InvalidApprover",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "approver",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721InvalidOperator",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "operator",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721InvalidOwner",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721InvalidReceiver",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "receiver",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721InvalidSender",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "sender",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "ERC721NonexistentToken",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "tokenId",
|
|
||||||
"type": "uint256",
|
|
||||||
"internalType": "uint256"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "FailedInnerCall",
|
|
||||||
"inputs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "InvalidInitialization",
|
|
||||||
"inputs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "NotInitializing",
|
|
||||||
"inputs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "OwnableInvalidOwner",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "owner",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "OwnableUnauthorizedAccount",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "account",
|
|
||||||
"type": "address",
|
|
||||||
"internalType": "address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "UUPSUnauthorizedCallContext",
|
|
||||||
"inputs": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "UUPSUnsupportedProxiableUUID",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "slot",
|
|
||||||
"type": "bytes32",
|
|
||||||
"internalType": "bytes32"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"name": "Unauthorized",
|
|
||||||
"inputs": []
|
|
||||||
}
|
|
||||||
]
|
|
60
kinode/packages/app_store/ui/src/abis/helpers.ts
Normal file
60
kinode/packages/app_store/ui/src/abis/helpers.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { multicallAbi, kimapAbi, mechAbi, KIMAP, MULTICALL, KINO_ACCOUNT_IMPL } from "./";
|
||||||
|
import { encodeFunctionData, encodePacked, stringToHex } from "viem";
|
||||||
|
|
||||||
|
export function encodeMulticalls(metadataUri: string, metadataHash: string) {
|
||||||
|
const metadataHashCall = encodeFunctionData({
|
||||||
|
abi: kimapAbi,
|
||||||
|
functionName: 'note',
|
||||||
|
args: [
|
||||||
|
encodePacked(["bytes"], [stringToHex("~metadata-hash")]),
|
||||||
|
encodePacked(["bytes"], [stringToHex(metadataHash)]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const metadataUriCall = encodeFunctionData({
|
||||||
|
abi: kimapAbi,
|
||||||
|
functionName: 'note',
|
||||||
|
args: [
|
||||||
|
encodePacked(["bytes"], [stringToHex("~metadata-uri")]),
|
||||||
|
encodePacked(["bytes"], [stringToHex(metadataUri)]),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const calls = [
|
||||||
|
{ target: KIMAP, callData: metadataHashCall },
|
||||||
|
{ target: KIMAP, callData: metadataUriCall },
|
||||||
|
];
|
||||||
|
|
||||||
|
const multicall = encodeFunctionData({
|
||||||
|
abi: multicallAbi,
|
||||||
|
functionName: 'aggregate',
|
||||||
|
args: [calls]
|
||||||
|
});
|
||||||
|
return multicall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeIntoMintCall(multicalls: `0x${string}`, our_address: `0x${string}`, app_name: string) {
|
||||||
|
const initCall = encodeFunctionData({
|
||||||
|
abi: mechAbi,
|
||||||
|
functionName: 'execute',
|
||||||
|
args: [
|
||||||
|
MULTICALL,
|
||||||
|
BigInt(0),
|
||||||
|
multicalls,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const mintCall = encodeFunctionData({
|
||||||
|
abi: kimapAbi,
|
||||||
|
functionName: 'mint',
|
||||||
|
args: [
|
||||||
|
our_address,
|
||||||
|
encodePacked(["bytes"], [stringToHex(app_name)]),
|
||||||
|
initCall,
|
||||||
|
"0x",
|
||||||
|
KINO_ACCOUNT_IMPL,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
return mintCall;
|
||||||
|
}
|
24
kinode/packages/app_store/ui/src/abis/index.ts
Normal file
24
kinode/packages/app_store/ui/src/abis/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { parseAbi } from "viem";
|
||||||
|
|
||||||
|
export { encodeMulticalls, encodeIntoMintCall } from "./helpers";
|
||||||
|
|
||||||
|
export const KIMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658";
|
||||||
|
export const MULTICALL: `0x${string}` = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
||||||
|
export const KINO_ACCOUNT_IMPL: `0x${string}` = "0x38766C70a4FB2f23137D9251a1aA12b1143fC716";
|
||||||
|
|
||||||
|
|
||||||
|
export const multicallAbi = parseAbi([
|
||||||
|
`function aggregate(Call[] calls) external payable returns (uint256 blockNumber, bytes[] returnData)`,
|
||||||
|
`struct Call { address target; bytes callData; }`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const kimapAbi = parseAbi([
|
||||||
|
"function mint(address, bytes calldata, bytes calldata, bytes calldata, address) external returns (address tba)",
|
||||||
|
"function note(bytes calldata,bytes calldata) external returns (bytes32)",
|
||||||
|
"function get(bytes32 node) external view returns (address tokenBoundAccount, address tokenOwner, bytes memory note)",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const mechAbi = parseAbi([
|
||||||
|
"function execute(address to, uint256 value, bytes calldata data, uint8 operation) returns (bytes memory returnData)",
|
||||||
|
"function token() external view returns (uint256,address,uint256)"
|
||||||
|
])
|
File diff suppressed because it is too large
Load Diff
@ -1,44 +0,0 @@
|
|||||||
/* Autogenerated file. Do not edit manually. */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
import type { Listener } from "@ethersproject/providers";
|
|
||||||
import type { Event, EventFilter } from "ethers";
|
|
||||||
|
|
||||||
export interface TypedEvent<
|
|
||||||
TArgsArray extends Array<any> = any,
|
|
||||||
TArgsObject = any
|
|
||||||
> extends Event {
|
|
||||||
args: TArgsArray & TArgsObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TypedEventFilter<_TEvent extends TypedEvent>
|
|
||||||
extends EventFilter {}
|
|
||||||
|
|
||||||
export interface TypedListener<TEvent extends TypedEvent> {
|
|
||||||
(...listenerArg: [...__TypechainArgsArray<TEvent>, TEvent]): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type __TypechainArgsArray<T> = T extends TypedEvent<infer U> ? U : never;
|
|
||||||
|
|
||||||
export interface OnEvent<TRes> {
|
|
||||||
<TEvent extends TypedEvent>(
|
|
||||||
eventFilter: TypedEventFilter<TEvent>,
|
|
||||||
listener: TypedListener<TEvent>
|
|
||||||
): TRes;
|
|
||||||
(eventName: string, listener: Listener): TRes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MinEthersFactory<C, ARGS> = {
|
|
||||||
deploy(...a: ARGS[]): Promise<C>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetContractTypeFromFactory<F> = F extends MinEthersFactory<
|
|
||||||
infer C,
|
|
||||||
any
|
|
||||||
>
|
|
||||||
? C
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export type GetARGsTypeFromFactory<F> = F extends MinEthersFactory<any, any>
|
|
||||||
? Parameters<F["deploy"]>
|
|
||||||
: never;
|
|
@ -1,999 +0,0 @@
|
|||||||
/* Autogenerated file. Do not edit manually. */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import { Contract, Signer, utils } from "ethers";
|
|
||||||
import type { Provider } from "@ethersproject/providers";
|
|
||||||
import type { PackageStore, PackageStoreInterface } from "../PackageStore";
|
|
||||||
|
|
||||||
const _abi = [
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "UPGRADE_INTERFACE_VERSION",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "approve",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "to",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "apps",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherKnsNodeId",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "balanceOf",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "contractURI",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "getApproved",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "getInitializedVersion",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "uint64",
|
|
||||||
internalType: "uint64",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "getPackageId",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherName",
|
|
||||||
type: "bytes",
|
|
||||||
internalType: "bytes",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "pure",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "getPackageInfo",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "package",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "tuple",
|
|
||||||
internalType: "struct IKinodeAppStore.PackageInfo",
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherKnsNodeId",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "getPackageInfo",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherName",
|
|
||||||
type: "bytes",
|
|
||||||
internalType: "bytes",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "tuple",
|
|
||||||
internalType: "struct IKinodeAppStore.PackageInfo",
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherKnsNodeId",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "initialize",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "_knsResolver",
|
|
||||||
type: "address",
|
|
||||||
internalType: "contract KNSRegistryResolver",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "isApprovedForAll",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "operator",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "bool",
|
|
||||||
internalType: "bool",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "knsResolver",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "address",
|
|
||||||
internalType: "contract KNSRegistryResolver",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "name",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "owner",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "ownerOf",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "proxiableUUID",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "registerApp",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherName",
|
|
||||||
type: "bytes",
|
|
||||||
internalType: "bytes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "renounceOwnership",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "safeTransferFrom",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "from",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "to",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "safeTransferFrom",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "from",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "to",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "data",
|
|
||||||
type: "bytes",
|
|
||||||
internalType: "bytes",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "setApprovalForAll",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "operator",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "approved",
|
|
||||||
type: "bool",
|
|
||||||
internalType: "bool",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "supportsInterface",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "interfaceId",
|
|
||||||
type: "bytes4",
|
|
||||||
internalType: "bytes4",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "bool",
|
|
||||||
internalType: "bool",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "symbol",
|
|
||||||
inputs: [],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "tokenURI",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stateMutability: "view",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "transferFrom",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "from",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "to",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "transferOwnership",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "newOwner",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "unlistPacakge",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "package",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "updateContractURI",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "uri",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "updateMetadata",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "package",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "nonpayable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "function",
|
|
||||||
name: "upgradeToAndCall",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "newImplementation",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "data",
|
|
||||||
type: "bytes",
|
|
||||||
internalType: "bytes",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [],
|
|
||||||
stateMutability: "payable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "AppMetadataUpdated",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "package",
|
|
||||||
type: "uint256",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "AppRegistered",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "package",
|
|
||||||
type: "uint256",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "packageName",
|
|
||||||
type: "string",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "publisherName",
|
|
||||||
type: "bytes",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "bytes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataUrl",
|
|
||||||
type: "string",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "string",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "metadataHash",
|
|
||||||
type: "bytes32",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "Approval",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "approved",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "ApprovalForAll",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "operator",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "approved",
|
|
||||||
type: "bool",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "bool",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "Initialized",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "version",
|
|
||||||
type: "uint64",
|
|
||||||
indexed: false,
|
|
||||||
internalType: "uint64",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "OwnershipTransferred",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "previousOwner",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "newOwner",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "Transfer",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "from",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "to",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "event",
|
|
||||||
name: "Upgraded",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "implementation",
|
|
||||||
type: "address",
|
|
||||||
indexed: true,
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
anonymous: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "AddressEmptyCode",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "target",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC1967InvalidImplementation",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "implementation",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC1967NonPayable",
|
|
||||||
inputs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721IncorrectOwner",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "sender",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721InsufficientApproval",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "operator",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721InvalidApprover",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "approver",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721InvalidOperator",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "operator",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721InvalidOwner",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721InvalidReceiver",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "receiver",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721InvalidSender",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "sender",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "ERC721NonexistentToken",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "tokenId",
|
|
||||||
type: "uint256",
|
|
||||||
internalType: "uint256",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "FailedInnerCall",
|
|
||||||
inputs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "InvalidInitialization",
|
|
||||||
inputs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "NotInitializing",
|
|
||||||
inputs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "OwnableInvalidOwner",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "owner",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "OwnableUnauthorizedAccount",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "account",
|
|
||||||
type: "address",
|
|
||||||
internalType: "address",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "UUPSUnauthorizedCallContext",
|
|
||||||
inputs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "UUPSUnsupportedProxiableUUID",
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
name: "slot",
|
|
||||||
type: "bytes32",
|
|
||||||
internalType: "bytes32",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "error",
|
|
||||||
name: "Unauthorized",
|
|
||||||
inputs: [],
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export class PackageStore__factory {
|
|
||||||
static readonly abi = _abi;
|
|
||||||
static createInterface(): PackageStoreInterface {
|
|
||||||
return new utils.Interface(_abi) as PackageStoreInterface;
|
|
||||||
}
|
|
||||||
static connect(
|
|
||||||
address: string,
|
|
||||||
signerOrProvider: Signer | Provider
|
|
||||||
): PackageStore {
|
|
||||||
return new Contract(address, _abi, signerOrProvider) as PackageStore;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
/* Autogenerated file. Do not edit manually. */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
export { PackageStore__factory } from "./PackageStore__factory";
|
|
@ -1,6 +0,0 @@
|
|||||||
/* Autogenerated file. Do not edit manually. */
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
export type { PackageStore } from "./PackageStore";
|
|
||||||
export * as factories from "./factories";
|
|
||||||
export { PackageStore__factory } from "./factories/PackageStore__factory";
|
|
@ -1,10 +0,0 @@
|
|||||||
<svg width="122" height="81" viewBox="0 0 122 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_6_651)">
|
|
||||||
<path d="M89.3665 8.06803L121.5 0.35155L66.5111 0.320312L63.7089 7.69502L0.5 5.7032L54.0253 32.9925L36.1529 80.3203L89.3665 8.06803Z" fill="#FFF5D9"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_6_651">
|
|
||||||
<rect width="121" height="80" fill="white" transform="translate(0.5 0.320312)"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 431 B |
@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#FFF5D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M6 9l6 6 6-6"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 188 B |
@ -1,65 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import UpdateButton from "./UpdateButton";
|
|
||||||
import DownloadButton from "./DownloadButton";
|
|
||||||
import InstallButton from "./InstallButton";
|
|
||||||
import LaunchButton from "./LaunchButton";
|
|
||||||
import { FaCheck } from "react-icons/fa6";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface ActionButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
isIcon?: boolean;
|
|
||||||
permitMultiButton?: boolean;
|
|
||||||
launchPath?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ActionButton({ app, launchPath = '', isIcon = false, permitMultiButton = false, ...props }: ActionButtonProps) {
|
|
||||||
const { installed, downloaded, updatable } = useMemo(() => {
|
|
||||||
const versions = Object.entries(app?.metadata?.properties?.code_hashes || {});
|
|
||||||
const latestHash = (versions.find(([v]) => v === app.metadata?.properties?.current_version) || [])[1];
|
|
||||||
|
|
||||||
const installed = app.installed;
|
|
||||||
const downloaded = Boolean(app.state);
|
|
||||||
|
|
||||||
const updatable =
|
|
||||||
Boolean(app.state?.our_version && latestHash) &&
|
|
||||||
app.state?.our_version !== latestHash &&
|
|
||||||
app.publisher !== (window as any).our.node;
|
|
||||||
return {
|
|
||||||
installed,
|
|
||||||
downloaded,
|
|
||||||
updatable,
|
|
||||||
};
|
|
||||||
}, [app]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* if it's got a UI and it's updatable, show both buttons if we have space (launch will otherwise push out update) */}
|
|
||||||
{permitMultiButton && installed && updatable && launchPath && <UpdateButton app={app} {...props} isIcon={isIcon} />}
|
|
||||||
{(installed && launchPath)
|
|
||||||
? <LaunchButton app={app} {...props} isIcon={isIcon} launchPath={launchPath} />
|
|
||||||
: (installed && updatable)
|
|
||||||
? <UpdateButton app={app} {...props} isIcon={isIcon} />
|
|
||||||
: !downloaded
|
|
||||||
? <DownloadButton app={app} {...props} isIcon={isIcon} />
|
|
||||||
: !installed
|
|
||||||
? <InstallButton app={app} {...props} isIcon={isIcon} />
|
|
||||||
: isIcon
|
|
||||||
? <button
|
|
||||||
className="pointer-events none icon clear absolute top-0 right-0"
|
|
||||||
>
|
|
||||||
<FaCheck />
|
|
||||||
</button>
|
|
||||||
: <></>
|
|
||||||
// <button
|
|
||||||
// onClick={() => { }}
|
|
||||||
// {...props as any}
|
|
||||||
// className={classNames("clear pointer-events-none", props.className)}
|
|
||||||
// >
|
|
||||||
// Installed
|
|
||||||
// </button>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
import AppHeader from "./AppHeader";
|
|
||||||
import ActionButton from "./ActionButton";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import { appId } from "../utils/app";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { APP_DETAILS_PATH } from "../constants/path";
|
|
||||||
import MoreActions from "./MoreActions";
|
|
||||||
|
|
||||||
interface AppEntryProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
overrideImageSize?: "small" | "medium" | "large";
|
|
||||||
showMoreActions?: boolean;
|
|
||||||
launchPath?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppEntry({ app, size = "medium", overrideImageSize, showMoreActions, launchPath, ...props }: AppEntryProps) {
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
key={appId(app)}
|
|
||||||
className={classNames("flex justify-between rounded-lg hover:bg-white/10 card cursor-pointer", props.className, {
|
|
||||||
'flex-wrap gap-2': isMobile,
|
|
||||||
'flex-col relative': size !== 'large'
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
if (!showMoreActions) {
|
|
||||||
navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppHeader app={app} size={size} overrideImageSize={overrideImageSize} />
|
|
||||||
<div className={classNames("flex items-center", {
|
|
||||||
'absolute': size !== 'large',
|
|
||||||
'top-2 right-2': size !== 'large' && showMoreActions,
|
|
||||||
'top-0 right-0': size !== 'large' && !showMoreActions,
|
|
||||||
'ml-auto': size === 'large' && isMobile,
|
|
||||||
'min-w-1/5': size === 'large'
|
|
||||||
})}>
|
|
||||||
<ActionButton
|
|
||||||
app={app}
|
|
||||||
launchPath={launchPath}
|
|
||||||
isIcon={!showMoreActions && size !== 'large'}
|
|
||||||
className={classNames({
|
|
||||||
'bg-orange text-lg': size === 'large',
|
|
||||||
'mr-2': showMoreActions,
|
|
||||||
'w-full': size === 'large'
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{showMoreActions && <MoreActions app={app} className="self-stretch" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import { appId } from "../utils/app";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import ColorDot from "./ColorDot";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
import AppIconPlaceholder from './AppIconPlaceholder'
|
|
||||||
|
|
||||||
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
size?: "small" | "medium" | "large";
|
|
||||||
overrideImageSize?: "small" | "medium" | "large"
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppHeader({
|
|
||||||
app,
|
|
||||||
size = "medium",
|
|
||||||
overrideImageSize,
|
|
||||||
...props
|
|
||||||
}: AppHeaderProps) {
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
const appName = <div
|
|
||||||
className={classNames({
|
|
||||||
'text-3xl font-[OpenSans]': !isMobile && size === 'large',
|
|
||||||
'text-xl': !isMobile && size !== 'large',
|
|
||||||
'text-lg': isMobile
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{app.metadata?.name || appId(app)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
const imageSize = overrideImageSize || size
|
|
||||||
|
|
||||||
return <div
|
|
||||||
{...props}
|
|
||||||
className={classNames('flex w-full justify-content-start', size, props.className, {
|
|
||||||
'flex-col': size === 'small',
|
|
||||||
'gap-2': isMobile,
|
|
||||||
'gap-4': !isMobile,
|
|
||||||
'gap-6': !isMobile && size === 'large'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{size === 'small' && appName}
|
|
||||||
{app.metadata?.image
|
|
||||||
? <img
|
|
||||||
src={app.metadata.image}
|
|
||||||
alt="app icon"
|
|
||||||
className={classNames('object-cover', {
|
|
||||||
'rounded': !imageSize,
|
|
||||||
'rounded-md': imageSize === 'small',
|
|
||||||
'rounded-lg': imageSize === 'medium',
|
|
||||||
'rounded-2xl': imageSize === 'large',
|
|
||||||
'h-32': imageSize === 'large' || imageSize === 'small',
|
|
||||||
'h-20': imageSize === 'medium',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
: <AppIconPlaceholder
|
|
||||||
text={app.metadata_hash || app.state?.our_version?.toString() || ''}
|
|
||||||
size={imageSize}
|
|
||||||
/>}
|
|
||||||
<div className={classNames("flex flex-col", {
|
|
||||||
'gap-2': isMobile,
|
|
||||||
'gap-4 max-w-3/4': isMobile && size !== 'small'
|
|
||||||
})}>
|
|
||||||
{size !== 'small' && appName}
|
|
||||||
{app.metadata?.description && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis'
|
|
||||||
}}
|
|
||||||
className={classNames({
|
|
||||||
'text-2xl': size === 'large'
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{app.metadata.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { isMobileCheck } from '../utils/dimensions';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
const AppIconPlaceholder: React.FC<{ text: string, className?: string, size: 'small' | 'medium' | 'large' }> = ({ text, className, size }) => {
|
|
||||||
const index = text.split('').pop()?.toUpperCase() || '0'
|
|
||||||
const derivedFilename = `/icons/${index}`
|
|
||||||
|
|
||||||
if (!derivedFilename) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
return <img
|
|
||||||
src={derivedFilename}
|
|
||||||
className={classNames('m-0 align-self-center rounded-full', {
|
|
||||||
'h-32 w-32': !isMobile && size === 'large',
|
|
||||||
'h-18 w-18': !isMobile && size === 'medium',
|
|
||||||
'h-12 w-12': isMobile || size === 'small',
|
|
||||||
}, className)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppIconPlaceholder
|
|
@ -1,32 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { FaCheck } from "react-icons/fa6";
|
|
||||||
|
|
||||||
export default function Checkbox({
|
|
||||||
readOnly = false,
|
|
||||||
checked,
|
|
||||||
setChecked,
|
|
||||||
}: {
|
|
||||||
readOnly?: boolean;
|
|
||||||
checked: boolean;
|
|
||||||
setChecked?: (checked: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="checked"
|
|
||||||
name="checked"
|
|
||||||
checked={checked}
|
|
||||||
onChange={(e) => setChecked && setChecked(e.target.checked)}
|
|
||||||
autoFocus
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
|
||||||
{checked && (
|
|
||||||
<FaCheck
|
|
||||||
className="absolute left-1 top-1 cursor-pointer"
|
|
||||||
onClick={() => setChecked && setChecked(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import classNames from 'classnames'
|
|
||||||
import React from 'react'
|
|
||||||
import { hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../utils/colors'
|
|
||||||
import { isMobileCheck } from '../utils/dimensions'
|
|
||||||
|
|
||||||
interface ColorDotProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
||||||
num: string,
|
|
||||||
dotSize?: 'small' | 'medium' | 'large'
|
|
||||||
}
|
|
||||||
|
|
||||||
const ColorDot: React.FC<ColorDotProps> = ({
|
|
||||||
num,
|
|
||||||
dotSize,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
num = num ? num : '';
|
|
||||||
|
|
||||||
while (num.length < 6) {
|
|
||||||
num = '0' + num
|
|
||||||
}
|
|
||||||
|
|
||||||
const leftHsl = rgbToHsl(hexToRgb(num.slice(0, 6)))
|
|
||||||
const rightHsl = rgbToHsl(hexToRgb(num.length > 6 ? num.slice(num.length - 6) : num))
|
|
||||||
leftHsl.s = rightHsl.s = 1
|
|
||||||
const leftColor = rgbToHex(hslToRgb(leftHsl))
|
|
||||||
const rightColor = rgbToHex(hslToRgb(rightHsl))
|
|
||||||
|
|
||||||
const angle = (parseInt(num, 16) % 360) || -45
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...props} className={classNames('flex', props.className)}>
|
|
||||||
<div
|
|
||||||
className={classNames('m-0 align-self-center border rounded-full outline-black', {
|
|
||||||
'h-32 w-32': !isMobile && dotSize === 'large',
|
|
||||||
'h-18 w-18': !isMobile && dotSize === 'medium',
|
|
||||||
'h-12 w-12': isMobile || dotSize === 'small',
|
|
||||||
'border-4': !isMobile,
|
|
||||||
'border-2': isMobile,
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
borderTopColor: leftColor,
|
|
||||||
borderRightColor: rightColor,
|
|
||||||
borderBottomColor: rightColor,
|
|
||||||
borderLeftColor: leftColor,
|
|
||||||
background: `linear-gradient(${angle}deg, ${leftColor} 0 50%, ${rightColor} 50% 100%)`,
|
|
||||||
filter: 'saturate(0.25)',
|
|
||||||
opacity: '0.75'
|
|
||||||
}} />
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ColorDot
|
|
@ -1,122 +0,0 @@
|
|||||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import Modal from "./Modal";
|
|
||||||
import { getAppName } from "../utils/app";
|
|
||||||
import Loader from "./Loader";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { FaDownload } from "react-icons/fa6";
|
|
||||||
|
|
||||||
interface DownloadButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
isIcon?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DownloadButton({ app, isIcon = false, ...props }: DownloadButtonProps) {
|
|
||||||
const { downloadApp, getCaps, getMyApp, getMyApps } =
|
|
||||||
useAppsStore();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [mirror, setMirror] = useState(app.metadata?.properties?.mirrors?.[0] || "Other");
|
|
||||||
const [customMirror, setCustomMirror] = useState("");
|
|
||||||
const [downloading, setDownloading] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMirror(app.metadata?.properties?.mirrors?.[0] || "Other");
|
|
||||||
}, [app.metadata?.properties?.mirrors]);
|
|
||||||
|
|
||||||
const onClick = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowModal(true);
|
|
||||||
}, [app, setShowModal, getCaps]);
|
|
||||||
|
|
||||||
const download = useCallback(async (e: FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const targetMirror = mirror === "Other" ? customMirror : mirror;
|
|
||||||
|
|
||||||
if (!targetMirror) {
|
|
||||||
window.alert("Please select a mirror");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setDownloading(`Downloading ${getAppName(app)}...`);
|
|
||||||
await downloadApp(app, targetMirror);
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
getMyApp(app)
|
|
||||||
.then(() => {
|
|
||||||
setDownloading("");
|
|
||||||
setShowModal(false);
|
|
||||||
clearInterval(interval);
|
|
||||||
getMyApps();
|
|
||||||
})
|
|
||||||
.catch(console.log);
|
|
||||||
}, 2000);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
window.alert(
|
|
||||||
`Failed to download app from ${targetMirror}, please try a different mirror.`
|
|
||||||
);
|
|
||||||
setDownloading("");
|
|
||||||
}
|
|
||||||
}, [mirror, customMirror, app, downloadApp, getMyApp]);
|
|
||||||
|
|
||||||
const appName = getAppName(app);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
{...props}
|
|
||||||
type="button"
|
|
||||||
className={classNames("text-sm self-start", props.className, {
|
|
||||||
'icon clear': isIcon,
|
|
||||||
'black': !isIcon,
|
|
||||||
})}
|
|
||||||
disabled={!!downloading}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{isIcon
|
|
||||||
? <FaDownload />
|
|
||||||
: downloading
|
|
||||||
? 'Downloading...'
|
|
||||||
: 'Download'}
|
|
||||||
</button>
|
|
||||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
|
||||||
{downloading ? (
|
|
||||||
<div className="flex-col-center gap-4">
|
|
||||||
<Loader msg={downloading} />
|
|
||||||
<div className="text-center">
|
|
||||||
App is downloading in the background. You can safely close this window.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form className="flex flex-col items-center gap-2" onSubmit={download}>
|
|
||||||
<h4>Download '{appName}'</h4>
|
|
||||||
<h5>Select Mirror</h5>
|
|
||||||
<select value={mirror} onChange={(e) => setMirror(e.target.value)}>
|
|
||||||
{((app.metadata?.properties?.mirrors || []).concat(["Other"])).map((m) => (
|
|
||||||
<option key={m} value={m}>
|
|
||||||
{m}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{mirror === "Other" && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={customMirror}
|
|
||||||
onChange={(e) => setCustomMirror(e.target.value)}
|
|
||||||
placeholder="Mirror, i.e. 'template.os'"
|
|
||||||
className="p-1 max-w-[240px] w-full"
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button type="submit">
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FaEllipsisH } from 'react-icons/fa';
|
|
||||||
import { Menu, MenuButton } from '@szhsin/react-menu';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dropdown({ ...props }: DropdownProps) {
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
{...props}
|
|
||||||
unmountOnClose={true}
|
|
||||||
className={classNames("relative", props.className)}
|
|
||||||
direction='left'
|
|
||||||
menuButton={<MenuButton className="small">
|
|
||||||
<FaEllipsisH className='-mb-1' />
|
|
||||||
</MenuButton>}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Menu>
|
|
||||||
)
|
|
||||||
}
|
|
27
kinode/packages/app_store/ui/src/components/Header.tsx
Normal file
27
kinode/packages/app_store/ui/src/components/Header.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { STORE_PATH, PUBLISH_PATH, MY_DOWNLOADS_PATH } from '../constants/path';
|
||||||
|
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
||||||
|
import { FaHome } from "react-icons/fa";
|
||||||
|
|
||||||
|
const Header: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<header className="app-header">
|
||||||
|
<div className="header-left">
|
||||||
|
<nav>
|
||||||
|
<button onClick={() => window.location.href = '/'}>
|
||||||
|
<FaHome />
|
||||||
|
</button>
|
||||||
|
<Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Apps</Link>
|
||||||
|
<Link to={PUBLISH_PATH} className={location.pathname === PUBLISH_PATH ? 'active' : ''}>Publish</Link>
|
||||||
|
<Link to={MY_DOWNLOADS_PATH} className={location.pathname === MY_DOWNLOADS_PATH ? 'active' : ''}>My Downloads</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="header-right">
|
||||||
|
<ConnectButton />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
@ -1,19 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React from "react"
|
|
||||||
import { FaHome } from "react-icons/fa"
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
|
|
||||||
const HomeButton: React.FC = () => {
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
return <button
|
|
||||||
className={classNames("clear absolute p-2", {
|
|
||||||
'top-2 left-2': isMobile,
|
|
||||||
'top-8 left-8': !isMobile
|
|
||||||
})}
|
|
||||||
onClick={() => window.location.href = '/'}
|
|
||||||
>
|
|
||||||
<FaHome size={24} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HomeButton;
|
|
@ -1,97 +0,0 @@
|
|||||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import Modal from "./Modal";
|
|
||||||
import { getAppName } from "../utils/app";
|
|
||||||
import Loader from "./Loader";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { FaI } from "react-icons/fa6";
|
|
||||||
|
|
||||||
interface InstallButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
isIcon?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InstallButton({ app, isIcon = false, ...props }: InstallButtonProps) {
|
|
||||||
const { installApp, getCaps, getMyApp, getMyApps } =
|
|
||||||
useAppsStore();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [caps, setCaps] = useState<string[]>([]);
|
|
||||||
const [installing, setInstalling] = useState("");
|
|
||||||
|
|
||||||
const onClick = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
getCaps(app).then((manifest) => {
|
|
||||||
setCaps(manifest.request_capabilities);
|
|
||||||
});
|
|
||||||
setShowModal(true);
|
|
||||||
}, [app, setShowModal, getCaps]);
|
|
||||||
|
|
||||||
const install = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setInstalling(`Installing ${getAppName(app)}...`);
|
|
||||||
await installApp(app);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
getMyApp(app)
|
|
||||||
.then((app) => {
|
|
||||||
if (!app.installed) return;
|
|
||||||
setInstalling("");
|
|
||||||
setShowModal(false);
|
|
||||||
clearInterval(interval);
|
|
||||||
getMyApps();
|
|
||||||
})
|
|
||||||
.catch(console.log);
|
|
||||||
}, 2000);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
window.alert(`Failed to install, please try again.`);
|
|
||||||
setInstalling("");
|
|
||||||
}
|
|
||||||
}, [app, installApp, getMyApp]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
{...props}
|
|
||||||
type="button"
|
|
||||||
className={classNames("text-sm self-start", props.className, {
|
|
||||||
'icon clear': isIcon
|
|
||||||
})}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={!!installing}
|
|
||||||
>
|
|
||||||
{isIcon
|
|
||||||
? <FaI />
|
|
||||||
: installing
|
|
||||||
? 'Installing...'
|
|
||||||
: "Install"}
|
|
||||||
</button>
|
|
||||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
|
||||||
{installing ? (
|
|
||||||
<div className="flex-col-center gap-4">
|
|
||||||
<Loader msg={installing} />
|
|
||||||
<div className="text-center">
|
|
||||||
App is installing in the background. You can safely close this window.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex-col-center gap-2">
|
|
||||||
<h4>Approve App Permissions</h4>
|
|
||||||
<h5 className="m-0">
|
|
||||||
{getAppName(app)} needs the following permissions:
|
|
||||||
</h5>
|
|
||||||
<ul className="flex flex-col items-start">
|
|
||||||
{caps.map((cap) => (
|
|
||||||
<li key={cap}>{cap}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<button type="button" onClick={install}>
|
|
||||||
Approve & Install
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import jazzicon from '@metamask/jazzicon';
|
|
||||||
|
|
||||||
interface JazziconProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
address: string;
|
|
||||||
diameter?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Jazzicon: React.FC<JazziconProps> = ({ address, diameter = 40, ...props }) => {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (address && ref.current) {
|
|
||||||
const seed = parseInt(address.slice(2, 10), 16); // Derive a seed from Ethereum address
|
|
||||||
const icon = jazzicon(diameter, seed);
|
|
||||||
|
|
||||||
// Clear the current icon
|
|
||||||
ref.current.innerHTML = '';
|
|
||||||
// Append the new icon
|
|
||||||
ref.current.appendChild(icon);
|
|
||||||
}
|
|
||||||
}, [address, diameter]);
|
|
||||||
|
|
||||||
return <div {...props} ref={ref} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Jazzicon;
|
|
@ -1,34 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { FaPlay } from "react-icons/fa6";
|
|
||||||
|
|
||||||
interface LaunchButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
launchPath: string;
|
|
||||||
isIcon?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LaunchButton({ app, launchPath, isIcon = false, ...props }: LaunchButtonProps) {
|
|
||||||
const onLaunch = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
window.location.href = `/${launchPath.replace('/', '')}`
|
|
||||||
return;
|
|
||||||
}, [app, launchPath]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
{...props}
|
|
||||||
type="button"
|
|
||||||
className={classNames("text-sm self-start", props.className, {
|
|
||||||
'icon clear': isIcon,
|
|
||||||
'alt': !isIcon
|
|
||||||
})}
|
|
||||||
onClick={onLaunch}
|
|
||||||
>
|
|
||||||
{isIcon ? <FaPlay /> : "Launch"}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { FaCircleNotch } from 'react-icons/fa6'
|
|
||||||
|
|
||||||
type LoaderProps = {
|
|
||||||
msg: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Loader({ msg }: LoaderProps) {
|
|
||||||
return (
|
|
||||||
<div id="loading" className="flex-col-center text-center gap-4">
|
|
||||||
<h4>{msg}</h4>
|
|
||||||
<FaCircleNotch className="animate-spin rounded-full h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,279 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import { FaX } from "react-icons/fa6";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
app?: AppInfo;
|
|
||||||
packageName: string;
|
|
||||||
publisherId: string;
|
|
||||||
goBack: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VALID_VERSION_REGEX = /^\d+\.\d+\.\d+$/;
|
|
||||||
|
|
||||||
const MetadataForm = ({ app, packageName, publisherId, goBack }: Props) => {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: app?.metadata?.name || "",
|
|
||||||
description: app?.metadata?.description || "",
|
|
||||||
image: app?.metadata?.image || "",
|
|
||||||
external_url: app?.metadata?.external_url || "",
|
|
||||||
animation_url: app?.metadata?.animation_url || "",
|
|
||||||
// properties, which can come from the app itself
|
|
||||||
package_name: packageName,
|
|
||||||
current_version: "",
|
|
||||||
publisher: publisherId,
|
|
||||||
mirrors: [publisherId],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [codeHashes, setCodeHashes] = useState<[string, string][]>(
|
|
||||||
Object.entries(app?.metadata?.properties?.code_hashes || {}).concat([
|
|
||||||
["", app?.state?.our_version || ""],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldChange = (field, value) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[field]: value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleFieldChange("package_name", packageName);
|
|
||||||
}, [packageName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleFieldChange("publisher", publisherId);
|
|
||||||
}, [publisherId]);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
|
||||||
const code_hashes = codeHashes.reduce((acc, [version, hash]) => {
|
|
||||||
acc[version] = hash;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
if (!VALID_VERSION_REGEX.test(formData.current_version)) {
|
|
||||||
window.alert("Current version must be in the format x.y.z");
|
|
||||||
return;
|
|
||||||
} else if (!code_hashes[formData.current_version]) {
|
|
||||||
window.alert(
|
|
||||||
`Code hashes must include current version (${formData.current_version})`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} else if (
|
|
||||||
!Object.keys(code_hashes).reduce(
|
|
||||||
(valid, version) => valid && VALID_VERSION_REGEX.test(version),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
window.alert("Code hashes must be a JSON object with valid version keys");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonData = JSON.stringify({
|
|
||||||
name: formData.name,
|
|
||||||
description: formData.description,
|
|
||||||
image: formData.image,
|
|
||||||
external_url: formData.external_url,
|
|
||||||
animation_url: formData.animation_url,
|
|
||||||
properties: {
|
|
||||||
package_name: formData.package_name,
|
|
||||||
current_version: formData.current_version,
|
|
||||||
publisher: formData.publisher,
|
|
||||||
mirrors: formData.mirrors,
|
|
||||||
code_hashes,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const blob = new Blob([jsonData], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download =
|
|
||||||
formData.package_name + "_" + formData.publisher + "_metadata.json";
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, [formData, codeHashes]);
|
|
||||||
|
|
||||||
const handleClearForm = () => {
|
|
||||||
setFormData({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
image: "",
|
|
||||||
external_url: "",
|
|
||||||
animation_url: "",
|
|
||||||
|
|
||||||
package_name: "",
|
|
||||||
current_version: "",
|
|
||||||
publisher: "",
|
|
||||||
mirrors: [],
|
|
||||||
});
|
|
||||||
setCodeHashes([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="flex flex-col card mt-2 gap-2">
|
|
||||||
<h4>Fill out metadata</h4>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => handleFieldChange("name", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Description</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) => handleFieldChange("description", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Image URL</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Image URL"
|
|
||||||
value={formData.image}
|
|
||||||
onChange={(e) => handleFieldChange("image", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">External URL</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="External URL"
|
|
||||||
value={formData.external_url}
|
|
||||||
onChange={(e) => handleFieldChange("external_url", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Animation URL</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Animation URL"
|
|
||||||
value={formData.animation_url}
|
|
||||||
onChange={(e) => handleFieldChange("animation_url", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Package Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Package Name"
|
|
||||||
value={formData.package_name}
|
|
||||||
onChange={(e) => handleFieldChange("package_name", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Current Version</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Current Version"
|
|
||||||
value={formData.current_version}
|
|
||||||
onChange={(e) => handleFieldChange("current_version", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Publisher</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Publisher"
|
|
||||||
value={formData.publisher}
|
|
||||||
onChange={(e) => handleFieldChange("publisher", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-3/4">
|
|
||||||
<label className="metadata-label">Mirrors (separated by commas)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Mirrors (separated by commas)"
|
|
||||||
value={formData.mirrors.join(",")}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleFieldChange(
|
|
||||||
"mirrors",
|
|
||||||
e.target.value.split(",").map((m) => m.trim())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex flex-col w-3/4 gap-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex gap-2 mt-0 justify-between w-full"
|
|
||||||
>
|
|
||||||
<h5 className="m-0">Code Hashes</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setCodeHashes([...codeHashes, ["", ""]])}
|
|
||||||
className="clear"
|
|
||||||
>
|
|
||||||
Add code hash
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{codeHashes.map(([version, hash], ind, arr) => (
|
|
||||||
<div
|
|
||||||
key={ind + "_code_hash"}
|
|
||||||
className="flex gap-2 mt-0 w-full"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Version"
|
|
||||||
value={version}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCodeHashes((prev) => {
|
|
||||||
const newHashes = [...prev];
|
|
||||||
newHashes[ind][0] = e.target.value;
|
|
||||||
return newHashes;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Hash"
|
|
||||||
value={hash}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCodeHashes((prev) => {
|
|
||||||
const newHashes = [...prev];
|
|
||||||
newHashes[ind][1] = e.target.value;
|
|
||||||
return newHashes;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="flex-5"
|
|
||||||
/>
|
|
||||||
{arr.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setCodeHashes((prev) => prev.filter((_, i) => i !== ind))
|
|
||||||
}
|
|
||||||
className="icon"
|
|
||||||
>
|
|
||||||
<FaX />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 my-4">
|
|
||||||
<button type="button" onClick={handleSubmit} className="alt">
|
|
||||||
Download JSON
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={handleClearForm} className="clear">
|
|
||||||
Clear Form
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={goBack}>
|
|
||||||
Done
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MetadataForm;
|
|
107
kinode/packages/app_store/ui/src/components/MirrorSelector.tsx
Normal file
107
kinode/packages/app_store/ui/src/components/MirrorSelector.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import useAppsStore from "../store";
|
||||||
|
|
||||||
|
interface MirrorSelectorProps {
|
||||||
|
packageId: string | undefined;
|
||||||
|
onMirrorSelect: (mirror: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSelect }) => {
|
||||||
|
const { fetchListing, checkMirror } = useAppsStore();
|
||||||
|
const [selectedMirror, setSelectedMirror] = useState<string>("");
|
||||||
|
const [customMirror, setCustomMirror] = useState<string>("");
|
||||||
|
const [isCustomMirrorSelected, setIsCustomMirrorSelected] = useState(false);
|
||||||
|
const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: boolean | null | 'http' }>({});
|
||||||
|
const [availableMirrors, setAvailableMirrors] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMirrors = 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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}, [packageId, fetchListing, checkMirror]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onMirrorSelect(selectedMirror);
|
||||||
|
}, [selectedMirror, onMirrorSelect]);
|
||||||
|
|
||||||
|
const handleMirrorChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "custom") {
|
||||||
|
setIsCustomMirrorSelected(true);
|
||||||
|
} else {
|
||||||
|
setSelectedMirror(value);
|
||||||
|
setIsCustomMirrorSelected(false);
|
||||||
|
setCustomMirror("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetCustomMirror = () => {
|
||||||
|
if (customMirror) {
|
||||||
|
setSelectedMirror(customMirror);
|
||||||
|
setIsCustomMirrorSelected(false);
|
||||||
|
setAvailableMirrors(prev => [...prev, customMirror]);
|
||||||
|
|
||||||
|
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 })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMirrorStatus = (mirror: string, status: boolean | null | 'http') => {
|
||||||
|
if (status === 'http') return '(HTTP)';
|
||||||
|
if (status === null) return '(checking)';
|
||||||
|
return status ? '(online)' : '(offline)';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mirror-selector">
|
||||||
|
<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}>
|
||||||
|
{mirror} {getMirrorStatus(mirror, mirrorStatuses[mirror])}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value="custom">Custom mirror</option>
|
||||||
|
</select>
|
||||||
|
{isCustomMirrorSelected && (
|
||||||
|
<div className="custom-mirror-input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customMirror}
|
||||||
|
onChange={(e) => setCustomMirror(e.target.value)}
|
||||||
|
placeholder="Enter custom mirror URL"
|
||||||
|
/>
|
||||||
|
<button onClick={handleSetCustomMirror} disabled={!customMirror}>
|
||||||
|
Set Custom Mirror
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MirrorSelector;
|
@ -1,60 +0,0 @@
|
|||||||
import classNames from 'classnames'
|
|
||||||
import React, { MouseEvent } from 'react'
|
|
||||||
import { FaX } from 'react-icons/fa6'
|
|
||||||
|
|
||||||
export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
show: boolean
|
|
||||||
hide: () => void
|
|
||||||
hideClose?: boolean
|
|
||||||
children: React.ReactNode,
|
|
||||||
title?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({
|
|
||||||
show,
|
|
||||||
hide,
|
|
||||||
hideClose = false,
|
|
||||||
title,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const dontHide = (e: MouseEvent) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(`bg-black/25 backdrop-blur-lg fixed top-0 bottom-0 left-0 right-0 flex flex-col c z-30 min-h-[10em] min-w-[30em]`,
|
|
||||||
{ show }
|
|
||||||
)}
|
|
||||||
onClick={hide}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={`flex flex-col relative bg-black/90 rounded-lg py-6 px-12 ${props.className || ''}`}
|
|
||||||
onClick={dontHide}
|
|
||||||
>
|
|
||||||
{Boolean(title) && <h4 className='mt-0 mb-2'>{title}</h4>}
|
|
||||||
{!hideClose && (
|
|
||||||
<button
|
|
||||||
className='icon absolute top-1 right-1'
|
|
||||||
onClick={hide}
|
|
||||||
>
|
|
||||||
<FaX />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className='flex flex-col items-center w-full'
|
|
||||||
onClick={dontHide}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Modal
|
|
@ -1,83 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import Dropdown from "./Dropdown";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import { appId } from "../utils/app";
|
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import { APP_DETAILS_PATH } from "../constants/path";
|
|
||||||
|
|
||||||
interface MoreActionsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MoreActions({ app, className }: MoreActionsProps) {
|
|
||||||
const { uninstallApp, setMirroring, setAutoUpdate } = useAppsStore();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const downloaded = Boolean(app.state);
|
|
||||||
|
|
||||||
if (!downloaded) {
|
|
||||||
if (!app.metadata) return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown className={className}>
|
|
||||||
<div className="flex flex-col backdrop-blur-lg bg-black/10 p-2 rounded-lg relative z-10">
|
|
||||||
{app.metadata?.description && (
|
|
||||||
<button
|
|
||||||
className="my-1 whitespace-nowrap clear"
|
|
||||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
|
||||||
>
|
|
||||||
View Details
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{app.metadata?.external_url && (
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href={app.metadata?.external_url}
|
|
||||||
className="mb-1 whitespace-nowrap button clear"
|
|
||||||
>
|
|
||||||
View Site
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown className={className}>
|
|
||||||
<div className="flex flex-col p-2 rounded-lg backdrop-blur-lg relative z-10">
|
|
||||||
<button
|
|
||||||
className="my-1 whitespace-nowrap clear"
|
|
||||||
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
|
|
||||||
>
|
|
||||||
View Details
|
|
||||||
</button>
|
|
||||||
{app.installed && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="mb-1 whitespace-nowrap clear"
|
|
||||||
onClick={() => uninstallApp(app)}
|
|
||||||
>
|
|
||||||
Uninstall
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="mb-1 whitespace-nowrap clear"
|
|
||||||
onClick={() => setMirroring(app, !app.state?.mirroring)}
|
|
||||||
>
|
|
||||||
{app.state?.mirroring ? "Stop" : "Start"} Mirroring
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="mb-1 whitespace-nowrap clear"
|
|
||||||
onClick={() => setAutoUpdate(app, !app.state?.auto_update)}
|
|
||||||
>
|
|
||||||
{app.state?.auto_update ? "Disable" : "Enable"} Auto Update
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
FaArrowLeft,
|
|
||||||
FaDownload,
|
|
||||||
FaMagnifyingGlass,
|
|
||||||
FaUpload,
|
|
||||||
} from "react-icons/fa6";
|
|
||||||
|
|
||||||
import { MY_APPS_PATH, PUBLISH_PATH } from "../constants/path";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
import HomeButton from "./HomeButton";
|
|
||||||
import { FaHome } from "react-icons/fa";
|
|
||||||
|
|
||||||
interface SearchHeaderProps {
|
|
||||||
value?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
onBack?: () => void;
|
|
||||||
onlyMyApps?: boolean;
|
|
||||||
hideSearch?: boolean;
|
|
||||||
hidePublish?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SearchHeader({
|
|
||||||
value = "",
|
|
||||||
onChange = () => null,
|
|
||||||
onBack,
|
|
||||||
hideSearch = false,
|
|
||||||
hidePublish = false,
|
|
||||||
}: SearchHeaderProps) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const canGoBack = location.key !== "default";
|
|
||||||
const isMyAppsPage = location.pathname === MY_APPS_PATH;
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames("flex justify-between", {
|
|
||||||
"gap-4": isMobile,
|
|
||||||
"gap-8": !isMobile
|
|
||||||
})}>
|
|
||||||
{location.pathname !== '/'
|
|
||||||
? <button
|
|
||||||
className="flex flex-col c icon icon-orange"
|
|
||||||
onClick={() => {
|
|
||||||
if (onBack) {
|
|
||||||
onBack()
|
|
||||||
} else {
|
|
||||||
canGoBack ? navigate(-1) : navigate('/')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaArrowLeft />
|
|
||||||
</button>
|
|
||||||
: isMobile
|
|
||||||
? <button
|
|
||||||
className={classNames("icon icon-orange", {
|
|
||||||
})}
|
|
||||||
onClick={() => window.location.href = '/'}
|
|
||||||
>
|
|
||||||
<FaHome />
|
|
||||||
</button>
|
|
||||||
: <></>}
|
|
||||||
{!hidePublish && <button
|
|
||||||
className="flex flex-col c icon icon-orange"
|
|
||||||
onClick={() => navigate(PUBLISH_PATH)}
|
|
||||||
>
|
|
||||||
<FaUpload />
|
|
||||||
</button>}
|
|
||||||
{!hideSearch && (
|
|
||||||
<div className="flex flex-1 rounded-md relative">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={(event) => onChange(event.target.value)}
|
|
||||||
value={value}
|
|
||||||
placeholder="Search for apps..."
|
|
||||||
className="w-full self-stretch grow"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={classNames("icon border-0 absolute top-1/2 -translate-y-1/2", {
|
|
||||||
'right-2': isMobile,
|
|
||||||
'right-4': !isMobile
|
|
||||||
})}
|
|
||||||
type="button"
|
|
||||||
onClick={() => inputRef.current?.focus()}
|
|
||||||
>
|
|
||||||
<FaMagnifyingGlass />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className={classNames("flex c", {
|
|
||||||
"gap-4": isMobile,
|
|
||||||
"gap-8 basis-1/5": !isMobile
|
|
||||||
})}
|
|
||||||
onClick={() => (isMyAppsPage ? navigate(-1) : navigate(MY_APPS_PATH))}
|
|
||||||
>
|
|
||||||
{!isMobile && <span>My Apps</span>}
|
|
||||||
<FaDownload />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import { React, useState } from "react"
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import { FaQuestion, FaX } from 'react-icons/fa6'
|
|
||||||
|
|
||||||
interface TooltipProps {
|
|
||||||
text: string
|
|
||||||
button?: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
position?: "top" | "bottom" | "left" | "right"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tooltip: React.FC<TooltipProps> = ({ text, button, className, position }) => {
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false)
|
|
||||||
return <div className={classNames("flex place-items-center place-content-center text-sm relative cursor-pointer shrink", className)}>
|
|
||||||
<div onClick={() => setShowTooltip(!showTooltip)}>
|
|
||||||
{button || <button
|
|
||||||
className="icon ml-4"
|
|
||||||
type='button'
|
|
||||||
>
|
|
||||||
<FaQuestion />
|
|
||||||
</button>}
|
|
||||||
</div>
|
|
||||||
<div className={classNames('absolute rounded bg-black p-2 min-w-[200px] z-10',
|
|
||||||
{
|
|
||||||
"hidden": !showTooltip,
|
|
||||||
"top-8": position === "top" || !position,
|
|
||||||
"bottom-8": position === "bottom",
|
|
||||||
"right-8": position === "left",
|
|
||||||
"left-8": position === "right",
|
|
||||||
})}>
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
<button className={classNames("absolute bg-black icon right-0 top-0", {
|
|
||||||
"hidden": !showTooltip,
|
|
||||||
})} onClick={() => setShowTooltip(false)}>
|
|
||||||
<FaX />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import Modal from "./Modal";
|
|
||||||
import { getAppName } from "../utils/app";
|
|
||||||
import Loader from "./Loader";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { FaU } from "react-icons/fa6";
|
|
||||||
|
|
||||||
interface UpdateButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
|
||||||
app: AppInfo;
|
|
||||||
isIcon?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdateButton({ app, isIcon = false, ...props }: UpdateButtonProps) {
|
|
||||||
const { updateApp, getCaps, getMyApp, getMyApps } =
|
|
||||||
useAppsStore();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [caps, setCaps] = useState<string[]>([]);
|
|
||||||
const [loading, setLoading] = useState("");
|
|
||||||
|
|
||||||
|
|
||||||
const onClick = useCallback(async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
getCaps(app).then((manifest) => {
|
|
||||||
setCaps(manifest.request_capabilities);
|
|
||||||
});
|
|
||||||
setShowModal(true);
|
|
||||||
}, [app, setShowModal, getCaps]);
|
|
||||||
|
|
||||||
const update = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(`Updating ${getAppName(app)}...`);
|
|
||||||
await updateApp(app);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
getMyApp(app)
|
|
||||||
.then((app) => {
|
|
||||||
if (!app.installed) return;
|
|
||||||
setLoading("");
|
|
||||||
setShowModal(false);
|
|
||||||
clearInterval(interval);
|
|
||||||
getMyApps();
|
|
||||||
})
|
|
||||||
.catch(console.log);
|
|
||||||
}, 2000);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
window.alert(`Failed to update, please try again.`);
|
|
||||||
setLoading("");
|
|
||||||
}
|
|
||||||
}, [app, updateApp, getMyApp]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
{...props}
|
|
||||||
type="button"
|
|
||||||
className={classNames("text-sm self-start", props.className, {
|
|
||||||
'icon clear': isIcon
|
|
||||||
})}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{isIcon ? <FaU /> : 'Update'}
|
|
||||||
</button>
|
|
||||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
|
||||||
{loading ? (
|
|
||||||
<Loader msg={loading} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h4>Approve App Permissions</h4>
|
|
||||||
<h5 className="m-0">
|
|
||||||
{getAppName(app)} needs the following permissions:
|
|
||||||
</h5>
|
|
||||||
{/* <h5>Send Messages:</h5> */}
|
|
||||||
<br />
|
|
||||||
<ul className="flex flex-col items-start">
|
|
||||||
{caps.map((cap) => (
|
|
||||||
<li key={cap}>{cap}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{/* <h5>Receive Messages:</h5>
|
|
||||||
<ul>
|
|
||||||
{caps.map((cap) => (
|
|
||||||
<li key={cap}>{cap}</li>
|
|
||||||
))}
|
|
||||||
</ul> */}
|
|
||||||
<button type="button" onClick={update}>
|
|
||||||
Approve & Update
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
2
kinode/packages/app_store/ui/src/components/index.ts
Normal file
2
kinode/packages/app_store/ui/src/components/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as Header } from './Header';
|
||||||
|
export { default as MirrorSelector } from './MirrorSelector';
|
@ -1,18 +0,0 @@
|
|||||||
export enum ChainId {
|
|
||||||
SEPOLIA = 11155111,
|
|
||||||
OPTIMISM = 10,
|
|
||||||
OPTIMISM_GOERLI = 420,
|
|
||||||
LOCAL = 1337,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SEPOLIA_OPT_HEX = '0xaa36a7';
|
|
||||||
export const OPTIMISM_OPT_HEX = '0xa';
|
|
||||||
export const SEPOLIA_OPT_INT = '11155111';
|
|
||||||
export const OPTIMISM_OPT_INT = '10';
|
|
||||||
|
|
||||||
// Optimism (for now)
|
|
||||||
export const PACKAGE_STORE_ADDRESSES = {
|
|
||||||
[ChainId.OPTIMISM]: '0x52185B6a6017E6f079B994452F234f7C2533787B',
|
|
||||||
// [ChainId.SEPOLIA]: '0x18c39eB547A0060C6034f8bEaFB947D1C16eADF1',
|
|
||||||
|
|
||||||
};
|
|
@ -1,4 +1,5 @@
|
|||||||
export const MY_APPS_PATH = '/my-apps';
|
|
||||||
export const STORE_PATH = '/';
|
export const STORE_PATH = '/';
|
||||||
export const PUBLISH_PATH = '/publish';
|
export const PUBLISH_PATH = '/publish';
|
||||||
export const APP_DETAILS_PATH = '/app-details';
|
export const APP_DETAILS_PATH = '/app';
|
||||||
|
export const DOWNLOAD_PATH = '/download';
|
||||||
|
export const MY_DOWNLOADS_PATH = '/my-downloads';
|
||||||
|
25
kinode/packages/app_store/ui/src/declarations.d.ts
vendored
Normal file
25
kinode/packages/app_store/ui/src/declarations.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
declare module 'idna-uts46-hx' {
|
||||||
|
export function toAscii(domain: string, options?: object): string;
|
||||||
|
export function toUnicode(domain: string, options?: object): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@ensdomains/eth-ens-namehash' {
|
||||||
|
export function hash(name: string): string;
|
||||||
|
export function normalize(name: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface ImportMeta {
|
||||||
|
env: {
|
||||||
|
VITE_OPTIMISM_RPC_URL: string;
|
||||||
|
VITE_SEPOLIA_RPC_URL: string;
|
||||||
|
BASE_URL: string;
|
||||||
|
VITE_NODE_URL?: string;
|
||||||
|
DEV: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
declare interface Window {
|
||||||
|
our: {
|
||||||
|
node: string;
|
||||||
|
process: string;
|
||||||
|
};
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -1,12 +1,44 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import '@unocss/reset/tailwind.css'
|
import '@rainbow-me/rainbowkit/styles.css';
|
||||||
import 'uno.css'
|
|
||||||
|
import {
|
||||||
|
getDefaultConfig,
|
||||||
|
RainbowKitProvider,
|
||||||
|
} from '@rainbow-me/rainbowkit';
|
||||||
|
import { WagmiProvider, http } from 'wagmi';
|
||||||
|
import {
|
||||||
|
optimism,
|
||||||
|
} from 'wagmi/chains';
|
||||||
|
import {
|
||||||
|
QueryClientProvider,
|
||||||
|
QueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
const config = getDefaultConfig({
|
||||||
|
appName: 'Kinode App Store',
|
||||||
|
projectId: 'KINODE_APP_STORE',
|
||||||
|
chains: [optimism],
|
||||||
|
ssr: false,
|
||||||
|
transports: {
|
||||||
|
[optimism.id]: http(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<WagmiProvider config={config}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RainbowKitProvider showRecentTransactions={true}>
|
||||||
<App />
|
<App />
|
||||||
|
</RainbowKitProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</WagmiProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
@ -1,178 +1,182 @@
|
|||||||
import React, { useState, useEffect, useMemo, useCallback, ReactElement } from "react";
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { FaDownload, FaCheck, FaTimes, FaPlay, FaSpinner, FaTrash, FaSync } from "react-icons/fa";
|
||||||
import { AppInfo } from "../types/Apps";
|
import useAppsStore from "../store";
|
||||||
import useAppsStore from "../store/apps-store";
|
import { AppListing, PackageState } from "../types/Apps";
|
||||||
import ActionButton from "../components/ActionButton";
|
import { compareVersions } from "../utils/compareVersions";
|
||||||
import AppHeader from "../components/AppHeader";
|
|
||||||
import SearchHeader from "../components/SearchHeader";
|
|
||||||
import { PageProps } from "../types/Page";
|
|
||||||
import { appId } from "../utils/app";
|
|
||||||
import { PUBLISH_PATH } from "../constants/path";
|
|
||||||
import HomeButton from "../components/HomeButton";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
import { FaGlobe, FaPeopleGroup, FaStar } from "react-icons/fa6";
|
|
||||||
|
|
||||||
interface AppPageProps extends PageProps { }
|
|
||||||
|
|
||||||
export default function AppPage() {
|
export default function AppPage() {
|
||||||
// eslint-disable-line
|
const { id } = useParams();
|
||||||
const { myApps, listedApps, getListedApp } = useAppsStore();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams();
|
const { fetchListing, fetchInstalledApp, uninstallApp, setAutoUpdate } = useAppsStore();
|
||||||
const [app, setApp] = useState<AppInfo | undefined>(undefined);
|
const [app, setApp] = useState<AppListing | null>(null);
|
||||||
const [launchPath, setLaunchPath] = useState('');
|
const [installedApp, setInstalledApp] = useState<PackageState | null>(null);
|
||||||
|
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
||||||
|
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isUninstalling, setIsUninstalling] = useState(false);
|
||||||
|
const [isTogglingAutoUpdate, setIsTogglingAutoUpdate] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [appData, installedAppData] = await Promise.all([
|
||||||
|
fetchListing(id),
|
||||||
|
fetchInstalledApp(id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setApp(appData);
|
||||||
|
setInstalledApp(installedAppData);
|
||||||
|
|
||||||
|
if (appData?.metadata?.properties?.code_hashes) {
|
||||||
|
const versions = appData.metadata.properties.code_hashes;
|
||||||
|
if (versions.length > 0) {
|
||||||
|
const latestVer = versions.reduce((latest, current) =>
|
||||||
|
compareVersions(current[0], latest[0]) > 0 ? current : latest
|
||||||
|
)[0];
|
||||||
|
setLatestVersion(latestVer);
|
||||||
|
|
||||||
|
if (installedAppData) {
|
||||||
|
const installedVersion = versions.find(([_, hash]) => hash === installedAppData.our_version_hash);
|
||||||
|
if (installedVersion) {
|
||||||
|
setCurrentVersion(installedVersion[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError("Failed to load app details. Please try again.");
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, fetchListing, fetchInstalledApp]);
|
||||||
|
|
||||||
|
const handleUninstall = async () => {
|
||||||
|
if (!app) return;
|
||||||
|
setIsUninstalling(true);
|
||||||
|
try {
|
||||||
|
await uninstallApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
|
||||||
|
await loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Uninstallation failed:', error);
|
||||||
|
setError(`Uninstallation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setIsUninstalling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAutoUpdate = async () => {
|
||||||
|
if (!app || !latestVersion) return;
|
||||||
|
setIsTogglingAutoUpdate(true);
|
||||||
|
try {
|
||||||
|
const newAutoUpdateState = !app.auto_update;
|
||||||
|
await setAutoUpdate(`${app.package_id.package_name}:${app.package_id.publisher_node}`, latestVersion, newAutoUpdateState);
|
||||||
|
await loadData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle auto-update:', error);
|
||||||
|
setError(`Failed to toggle auto-update: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setIsTogglingAutoUpdate(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const myApp = myApps.local.find((a) => appId(a) === params.id);
|
loadData();
|
||||||
if (myApp) return setApp(myApp);
|
}, [loadData]);
|
||||||
|
|
||||||
if (params.id) {
|
const handleDownload = () => {
|
||||||
const app = listedApps.find((a) => appId(a) === params.id);
|
navigate(`/download/${id}`);
|
||||||
if (app) {
|
};
|
||||||
setApp(app);
|
|
||||||
} else {
|
const handleLaunch = () => {
|
||||||
getListedApp(params.id)
|
navigate(`/${app?.package_id.package_name}:${app?.package_id.package_name}:${app?.package_id.publisher_node}/`);
|
||||||
.then((app) => setApp(app))
|
};
|
||||||
.catch(console.error);
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="app-page"><h4>Loading app details...</h4></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="app-page"><h4>{error}</h4></div>;
|
||||||
}
|
}
|
||||||
}, [params.id, myApps, listedApps]);
|
|
||||||
|
|
||||||
const goToPublish = useCallback(() => {
|
if (!app) {
|
||||||
navigate(PUBLISH_PATH, { state: { app } });
|
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
|
||||||
}, [app, navigate]);
|
|
||||||
|
|
||||||
const version = useMemo(
|
|
||||||
() => app?.metadata?.properties?.current_version || "Unknown",
|
|
||||||
[app]
|
|
||||||
);
|
|
||||||
const versions = Object.entries(app?.metadata?.properties?.code_hashes || {});
|
|
||||||
const hash =
|
|
||||||
app?.state?.our_version ||
|
|
||||||
(versions[(versions.length || 1) - 1] || ["", ""])[1];
|
|
||||||
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
const appDetails: Array<{ top: ReactElement, middle: ReactElement, bottom: ReactElement }> = [
|
|
||||||
// {
|
|
||||||
// top: <div className={classNames({ 'text-sm': isMobile })}>0 ratings</div>,
|
|
||||||
// middle: <span className="text-2xl">5.0</span>,
|
|
||||||
// bottom: <div className={classNames("flex-center gap-1", {
|
|
||||||
// 'text-sm': isMobile
|
|
||||||
// })}>
|
|
||||||
// <FaStar />
|
|
||||||
// <FaStar />
|
|
||||||
// <FaStar />
|
|
||||||
// <FaStar />
|
|
||||||
// <FaStar />
|
|
||||||
// </div>
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
top: <div className={classNames({ 'text-sm': isMobile })}>Developer</div>,
|
|
||||||
middle: <FaPeopleGroup size={36} />,
|
|
||||||
bottom: <div className={classNames({ 'text-sm': isMobile })}>
|
|
||||||
{app?.publisher}
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
top: <div className={classNames({ 'text-sm': isMobile })}>Version</div>,
|
|
||||||
middle: <span className="text-2xl">{version}</span>,
|
|
||||||
bottom: <div className={classNames({ 'text-xs': isMobile })}>
|
|
||||||
{hash.slice(0, 5)}...{hash.slice(-5)}
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
top: <div className={classNames({ 'text-sm': isMobile })}>Mirrors</div>,
|
|
||||||
middle: <FaGlobe size={36} />,
|
|
||||||
bottom: <div className={classNames({ 'text-sm': isMobile })}>
|
|
||||||
{app?.metadata?.properties?.mirrors?.length || 0}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/apps').then(data => data.json())
|
|
||||||
.then((data: Array<{ package_name: string, path: string }>) => {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
const homepageAppData = data.find(otherApp => app?.package === otherApp.package_name)
|
|
||||||
if (homepageAppData) {
|
|
||||||
setLaunchPath(homepageAppData.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [app])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("flex flex-col w-full p-2",
|
<section className="app-page">
|
||||||
{
|
<div className="app-header">
|
||||||
'gap-4 max-w-screen': isMobile,
|
{app.metadata?.image && (
|
||||||
'gap-8 max-w-[900px]': !isMobile,
|
<img src={app.metadata.image} alt={app.metadata?.name || app.package_id.package_name} className="app-icon" />
|
||||||
})}
|
|
||||||
>
|
|
||||||
{!isMobile && <HomeButton />}
|
|
||||||
<SearchHeader
|
|
||||||
value=""
|
|
||||||
onChange={() => null}
|
|
||||||
hideSearch
|
|
||||||
hidePublish
|
|
||||||
/>
|
|
||||||
<div className={classNames("flex-col-center card !rounded-3xl", {
|
|
||||||
'p-12 gap-4 grow overflow-y-auto': isMobile,
|
|
||||||
'p-24 gap-8': !isMobile,
|
|
||||||
})}>
|
|
||||||
{app ? <>
|
|
||||||
<AppHeader app={app} size={isMobile ? "medium" : "large"} />
|
|
||||||
<div className="w-5/6 h-0 border border-orange" />
|
|
||||||
<div className={classNames("flex items-start text-xl", {
|
|
||||||
'gap-4 flex-wrap': isMobile,
|
|
||||||
'gap-8': !isMobile,
|
|
||||||
})}>
|
|
||||||
{appDetails.map((detail, index) => <>
|
|
||||||
<div
|
|
||||||
className={classNames("flex-col-center gap-2 justify-between self-stretch", {
|
|
||||||
'rounded-lg bg-white/10 p-1 min-w-1/4 grow': isMobile,
|
|
||||||
'opacity-50': !isMobile,
|
|
||||||
})}
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
{detail.top}
|
|
||||||
{detail.middle}
|
|
||||||
{detail.bottom}
|
|
||||||
</div>
|
|
||||||
{!isMobile && index !== appDetails.length - 1 && <div className="h-3/4 w-0 border border-orange self-center" />}
|
|
||||||
</>)}
|
|
||||||
</div>
|
|
||||||
{Array.isArray(app.metadata?.properties?.screenshots)
|
|
||||||
&& app.metadata?.properties.screenshots.length > 0
|
|
||||||
&& <div className="flex flex-wrap overflow-x-auto max-w-full">
|
|
||||||
{app.metadata.properties.screenshots.map(
|
|
||||||
(screenshot, index) => (
|
|
||||||
<img key={index + screenshot} src={screenshot} className="mr-2 max-h-20 max-w-full rounded border border-black" />
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</div>}
|
<div className="app-title">
|
||||||
<div className={classNames("flex-center gap-2", {
|
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
|
||||||
'flex-col': isMobile,
|
<p className="app-id">{`${app.package_id.package_name}.${app.package_id.publisher_node}`}</p>
|
||||||
})}>
|
|
||||||
<ActionButton
|
|
||||||
app={app}
|
|
||||||
launchPath={launchPath}
|
|
||||||
className={classNames("self-center bg-orange text-lg px-12")}
|
|
||||||
permitMultiButton
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{app.installed && app.state?.mirroring && (
|
</div>
|
||||||
<button type="button" onClick={goToPublish}>
|
|
||||||
Publish
|
<div className="app-description">{app.metadata?.description || "No description available"}</div>
|
||||||
|
|
||||||
|
<div className="app-info">
|
||||||
|
<ul className="detail-list">
|
||||||
|
<li>
|
||||||
|
<span>Installed:</span>
|
||||||
|
<span className="status-icon">{installedApp ? <FaCheck className="installed" /> : <FaTimes className="not-installed" />}</span>
|
||||||
|
</li>
|
||||||
|
{currentVersion && (
|
||||||
|
<li><span>Current Version:</span> <span>{currentVersion}</span></li>
|
||||||
|
)}
|
||||||
|
{latestVersion && (
|
||||||
|
<li><span>Latest Version:</span> <span>{latestVersion}</span></li>
|
||||||
|
)}
|
||||||
|
<li><span>Publisher:</span> <span>{app.package_id.publisher_node}</span></li>
|
||||||
|
<li><span>License:</span> <span>{app.metadata?.properties?.license || "Not specified"}</span></li>
|
||||||
|
<li>
|
||||||
|
<span>Auto Update:</span>
|
||||||
|
<span className="status-icon">
|
||||||
|
{app.auto_update ? <FaCheck className="installed" /> : <FaTimes className="not-installed" />}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="app-actions">
|
||||||
|
{installedApp && (
|
||||||
|
<>
|
||||||
|
<button onClick={handleLaunch} className="primary">
|
||||||
|
<FaPlay /> Launch
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={handleUninstall} className="secondary" disabled={isUninstalling}>
|
||||||
|
{isUninstalling ? <FaSpinner className="fa-spin" /> : <FaTrash />} Uninstall
|
||||||
|
</button>
|
||||||
|
<button onClick={handleToggleAutoUpdate} className="secondary" disabled={isTogglingAutoUpdate}>
|
||||||
|
{isTogglingAutoUpdate ? <FaSpinner className="fa-spin" /> : <FaSync />}
|
||||||
|
{app.auto_update ? " Disable" : " Enable"} Auto Update
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</> : <>
|
<button onClick={handleDownload} className="primary">
|
||||||
<h4>App details not found for </h4>
|
<FaDownload /> Download
|
||||||
<h4>{params.id}</h4>
|
</button>
|
||||||
</>}
|
</div>
|
||||||
|
|
||||||
|
{app.metadata?.properties?.screenshots && (
|
||||||
|
<div className="app-screenshots">
|
||||||
|
<h3>Screenshots</h3>
|
||||||
|
<div className="screenshot-container">
|
||||||
|
{app.metadata.properties.screenshots.map((screenshot, index) => (
|
||||||
|
<img key={index} src={screenshot} alt={`Screenshot ${index + 1}`} className="app-screenshot" />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
199
kinode/packages/app_store/ui/src/pages/DownloadPage.tsx
Normal file
199
kinode/packages/app_store/ui/src/pages/DownloadPage.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
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 useAppsStore from "../store";
|
||||||
|
import { MirrorSelector } from '../components';
|
||||||
|
|
||||||
|
export default function DownloadPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const {
|
||||||
|
listings,
|
||||||
|
downloads,
|
||||||
|
installed,
|
||||||
|
activeDownloads,
|
||||||
|
fetchData,
|
||||||
|
downloadApp,
|
||||||
|
installApp,
|
||||||
|
removeDownload,
|
||||||
|
clearAllActiveDownloads,
|
||||||
|
} = useAppsStore();
|
||||||
|
|
||||||
|
const [showMetadata, setShowMetadata] = useState(false);
|
||||||
|
const [selectedMirror, setSelectedMirror] = useState<string>("");
|
||||||
|
|
||||||
|
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]);
|
||||||
|
const appDownloads = useMemo(() => downloads[id || ""] || [], [downloads, id]);
|
||||||
|
const installedApp = useMemo(() => installed[id || ""], [installed, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
clearAllActiveDownloads();
|
||||||
|
fetchData(id);
|
||||||
|
}
|
||||||
|
}, [id, fetchData, clearAllActiveDownloads]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (app && !selectedMirror) {
|
||||||
|
setSelectedMirror(app.package_id.publisher_node || "");
|
||||||
|
}
|
||||||
|
}, [app, selectedMirror]);
|
||||||
|
|
||||||
|
const handleDownload = useCallback((version: string, hash: string) => {
|
||||||
|
if (!id || !selectedMirror || !app) return;
|
||||||
|
downloadApp(id, hash, selectedMirror);
|
||||||
|
}, [id, selectedMirror, app, downloadApp]);
|
||||||
|
|
||||||
|
const handleInstall = useCallback((version: string, hash: string) => {
|
||||||
|
if (!id || !app) return;
|
||||||
|
const download = appDownloads.find(d => d.File && d.File.name === `${hash}.zip`);
|
||||||
|
if (download?.File?.manifest) {
|
||||||
|
try {
|
||||||
|
const manifestData = JSON.parse(download.File.manifest);
|
||||||
|
setManifest(manifestData);
|
||||||
|
setSelectedVersion({ version, hash });
|
||||||
|
setShowCapApproval(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse manifest:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Manifest not found for the selected version');
|
||||||
|
}
|
||||||
|
}, [id, app, appDownloads]);
|
||||||
|
|
||||||
|
const confirmInstall = useCallback(() => {
|
||||||
|
if (!id || !selectedVersion) return;
|
||||||
|
installApp(id, selectedVersion.hash).then(() => {
|
||||||
|
fetchData(id);
|
||||||
|
setShowCapApproval(false);
|
||||||
|
setManifest(null);
|
||||||
|
});
|
||||||
|
}, [id, selectedVersion, 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]);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return <div className="downloads-page"><h4>Loading app details...</h4></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="downloads-page">
|
||||||
|
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
|
||||||
|
<p>{app.metadata?.description}</p>
|
||||||
|
|
||||||
|
<MirrorSelector packageId={id} onMirrorSelect={setSelectedMirror} />
|
||||||
|
|
||||||
|
<div className="version-list">
|
||||||
|
<h3>Available Versions</h3>
|
||||||
|
{versionList.length === 0 ? (
|
||||||
|
<p>No versions available for this app.</p>
|
||||||
|
) : (
|
||||||
|
<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
|
||||||
|
</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>
|
||||||
|
))}
|
||||||
|
</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">
|
||||||
|
<h3>Approve Capabilities</h3>
|
||||||
|
<pre className="json-display">
|
||||||
|
{JSON.stringify(manifest[0]?.request_capabilities || [], null, 2)}
|
||||||
|
</pre>
|
||||||
|
<div className="approval-buttons">
|
||||||
|
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
||||||
|
<button onClick={confirmInstall}>
|
||||||
|
Approve and Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,112 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import { FaUpload } from "react-icons/fa";
|
|
||||||
|
|
||||||
import { AppInfo, MyApps } from "../types/Apps";
|
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import AppEntry from "../components/AppEntry";
|
|
||||||
import SearchHeader from "../components/SearchHeader";
|
|
||||||
import { PageProps } from "../types/Page";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { appId } from "../utils/app";
|
|
||||||
import { PUBLISH_PATH } from "../constants/path";
|
|
||||||
import HomeButton from "../components/HomeButton";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export default function MyAppsPage() { // eslint-disable-line
|
|
||||||
const { myApps, getMyApps, } = useAppsStore()
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
||||||
const [displayedApps, setDisplayedApps] = useState<MyApps>(myApps);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getMyApps()
|
|
||||||
.then(setDisplayedApps)
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}, []); // eslint-disable-line
|
|
||||||
|
|
||||||
const searchMyApps = useCallback((query: string) => {
|
|
||||||
setSearchQuery(query);
|
|
||||||
const filteredApps = Object.keys(myApps).reduce((acc, key) => {
|
|
||||||
acc[key] = myApps[key].filter((app) => {
|
|
||||||
return app.package.toLowerCase().includes(query.toLowerCase())
|
|
||||||
|| app.metadata?.description?.toLowerCase().includes(query.toLowerCase())
|
|
||||||
|| app.metadata?.description?.toLowerCase().includes(query.toLowerCase());
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {
|
|
||||||
downloaded: [] as AppInfo[],
|
|
||||||
installed: [] as AppInfo[],
|
|
||||||
local: [] as AppInfo[],
|
|
||||||
system: [] as AppInfo[],
|
|
||||||
} as MyApps)
|
|
||||||
|
|
||||||
setDisplayedApps(filteredApps);
|
|
||||||
}, [myApps]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchQuery) {
|
|
||||||
searchMyApps(searchQuery);
|
|
||||||
} else {
|
|
||||||
setDisplayedApps(myApps);
|
|
||||||
}
|
|
||||||
}, [myApps]);
|
|
||||||
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
console.log({ myApps })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames("flex flex-col w-full h-screen p-2",
|
|
||||||
{
|
|
||||||
'gap-4 max-w-screen': isMobile,
|
|
||||||
'gap-8 max-w-[900px]': !isMobile,
|
|
||||||
})}>
|
|
||||||
<HomeButton />
|
|
||||||
<SearchHeader value={searchQuery} onChange={searchMyApps} />
|
|
||||||
<div className="flex justify-between items-center mt-2">
|
|
||||||
<h3>My Packages</h3>
|
|
||||||
<button onClick={() => navigate(PUBLISH_PATH)}>
|
|
||||||
<FaUpload className="mr-2" />
|
|
||||||
Publish Package
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classNames("flex flex-col card gap-2 mt-2",
|
|
||||||
{
|
|
||||||
'max-h-[80vh] overflow-y-scroll overflow-x-visible': !isMobile,
|
|
||||||
})}
|
|
||||||
style={{
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
scrollbarColor: '#FFF5D9 transparent',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{displayedApps.downloaded.length > 0 && <h4>Downloaded</h4>}
|
|
||||||
{(displayedApps.downloaded || []).map((app) => <AppEntry
|
|
||||||
key={appId(app)}
|
|
||||||
app={app}
|
|
||||||
showMoreActions
|
|
||||||
/>)}
|
|
||||||
{displayedApps.installed.length > 0 && <h4>Installed</h4>}
|
|
||||||
{(displayedApps.installed || []).map((app) => <AppEntry
|
|
||||||
key={appId(app)}
|
|
||||||
app={app}
|
|
||||||
showMoreActions
|
|
||||||
/>)}
|
|
||||||
{displayedApps.local.length > 0 && <h4>Local</h4>}
|
|
||||||
{(displayedApps.local || []).map((app) => <AppEntry
|
|
||||||
key={appId(app)}
|
|
||||||
app={app}
|
|
||||||
showMoreActions
|
|
||||||
/>)}
|
|
||||||
{displayedApps.system.length > 0 && <h4>System</h4>}
|
|
||||||
{(displayedApps.system || []).map((app) => <AppEntry
|
|
||||||
key={appId(app)}
|
|
||||||
app={app}
|
|
||||||
showMoreActions
|
|
||||||
/>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
207
kinode/packages/app_store/ui/src/pages/MyDownloadsPage.tsx
Normal file
207
kinode/packages/app_store/ui/src/pages/MyDownloadsPage.tsx
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck, FaTrash } from "react-icons/fa";
|
||||||
|
import useAppsStore from "../store";
|
||||||
|
import { DownloadItem, PackageManifest, PackageState } from "../types/Apps";
|
||||||
|
|
||||||
|
export default function MyDownloadsPage() {
|
||||||
|
const { fetchDownloads, fetchDownloadsForApp, startMirroring, stopMirroring, installApp, removeDownload, fetchInstalled, installed } = useAppsStore();
|
||||||
|
const [currentPath, setCurrentPath] = useState<string[]>([]);
|
||||||
|
const [items, setItems] = useState<DownloadItem[]>([]);
|
||||||
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||||
|
const [manifest, setManifest] = useState<PackageManifest | null>(null);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<DownloadItem | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadItems();
|
||||||
|
fetchInstalled();
|
||||||
|
}, [currentPath]);
|
||||||
|
|
||||||
|
const loadItems = async () => {
|
||||||
|
try {
|
||||||
|
let downloads: DownloadItem[];
|
||||||
|
if (currentPath.length === 0) {
|
||||||
|
downloads = await fetchDownloads();
|
||||||
|
} else {
|
||||||
|
downloads = await fetchDownloadsForApp(currentPath.join(':'));
|
||||||
|
}
|
||||||
|
setItems(downloads);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading items:", error);
|
||||||
|
setError(`Error loading items: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToItem = (item: DownloadItem) => {
|
||||||
|
if (item.Dir) {
|
||||||
|
setCurrentPath([...currentPath, item.Dir.name]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateUp = () => {
|
||||||
|
setCurrentPath(currentPath.slice(0, -1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMirroring = async (item: DownloadItem) => {
|
||||||
|
if (item.Dir) {
|
||||||
|
const packageId = [...currentPath, item.Dir.name].join(':');
|
||||||
|
try {
|
||||||
|
if (item.Dir.mirroring) {
|
||||||
|
await stopMirroring(packageId);
|
||||||
|
} else {
|
||||||
|
await startMirroring(packageId);
|
||||||
|
}
|
||||||
|
await loadItems();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error toggling mirroring:", error);
|
||||||
|
setError(`Error toggling mirroring: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstall = async (item: DownloadItem) => {
|
||||||
|
if (item.File) {
|
||||||
|
setSelectedItem(item);
|
||||||
|
try {
|
||||||
|
const manifestData = JSON.parse(item.File.manifest);
|
||||||
|
setManifest(manifestData);
|
||||||
|
setShowCapApproval(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse manifest:', error);
|
||||||
|
setError(`Failed to parse manifest: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmInstall = async () => {
|
||||||
|
if (!selectedItem?.File) return;
|
||||||
|
setIsInstalling(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const fileName = selectedItem.File.name;
|
||||||
|
const parts = fileName.split(':');
|
||||||
|
const versionHash = parts.pop()?.replace('.zip', '');
|
||||||
|
|
||||||
|
if (!versionHash) throw new Error('Invalid file name format');
|
||||||
|
|
||||||
|
// Construct packageId by combining currentPath and remaining parts of the filename
|
||||||
|
const packageId = [...currentPath, ...parts].join(':');
|
||||||
|
|
||||||
|
await installApp(packageId, versionHash);
|
||||||
|
await fetchInstalled();
|
||||||
|
setShowCapApproval(false);
|
||||||
|
await loadItems();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Installation failed:', error);
|
||||||
|
setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setIsInstalling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveDownload = async (item: DownloadItem) => {
|
||||||
|
if (item.File) {
|
||||||
|
try {
|
||||||
|
const packageId = currentPath.join(':');
|
||||||
|
const versionHash = item.File.name.replace('.zip', '');
|
||||||
|
await removeDownload(packageId, versionHash);
|
||||||
|
await loadItems();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove download:', error);
|
||||||
|
setError(`Failed to remove download: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAppInstalled = (name: string): boolean => {
|
||||||
|
const packageName = name.replace('.zip', '');
|
||||||
|
return Object.values(installed).some(app => app.package_id.package_name === packageName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="downloads-page">
|
||||||
|
<h2>Downloads</h2>
|
||||||
|
<div className="file-explorer">
|
||||||
|
<div className="path-navigation">
|
||||||
|
{currentPath.length > 0 && (
|
||||||
|
<button onClick={navigateUp} className="navigate-up">
|
||||||
|
<FaChevronLeft /> Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span className="current-path">/{currentPath.join('/')}</span>
|
||||||
|
</div>
|
||||||
|
<table className="downloads-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Mirroring</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const isFile = !!item.File;
|
||||||
|
const name = isFile ? item.File!.name : item.Dir!.name;
|
||||||
|
const isInstalled = isFile && isAppInstalled(name);
|
||||||
|
return (
|
||||||
|
<tr key={index} onClick={() => navigateToItem(item)} className={isFile ? 'file' : 'directory'}>
|
||||||
|
<td>
|
||||||
|
{isFile ? <FaFile /> : <FaFolder />} {name}
|
||||||
|
</td>
|
||||||
|
<td>{isFile ? 'File' : 'Directory'}</td>
|
||||||
|
<td>{isFile ? `${(item.File!.size / 1024).toFixed(2)} KB` : '-'}</td>
|
||||||
|
<td>{!isFile && (item.Dir!.mirroring ? 'Yes' : 'No')}</td>
|
||||||
|
<td>
|
||||||
|
{!isFile && (
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); toggleMirroring(item); }}>
|
||||||
|
<FaSync /> {item.Dir!.mirroring ? 'Stop' : 'Start'} Mirroring
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isFile && !isInstalled && (
|
||||||
|
<>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); handleInstall(item); }}>
|
||||||
|
<FaRocket /> Install
|
||||||
|
</button>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); handleRemoveDownload(item); }}>
|
||||||
|
<FaTrash /> Delete
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isFile && isInstalled && (
|
||||||
|
<FaCheck className="installed" />
|
||||||
|
)} </td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error-message">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showCapApproval && manifest && (
|
||||||
|
<div className="cap-approval-popup">
|
||||||
|
<div className="cap-approval-content">
|
||||||
|
<h3>Approve Capabilities</h3>
|
||||||
|
<pre className="json-display">
|
||||||
|
{JSON.stringify(manifest[0]?.request_capabilities || [], null, 2)}
|
||||||
|
</pre>
|
||||||
|
<div className="approval-buttons">
|
||||||
|
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
||||||
|
<button onClick={confirmInstall} disabled={isInstalling}>
|
||||||
|
{isInstalling ? <FaSpinner className="fa-spin" /> : 'Approve and Install'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,94 +1,48 @@
|
|||||||
import React, { useState, useCallback, FormEvent, useEffect } from "react";
|
import React, { useState, useCallback, FormEvent, useEffect } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { BigNumber, utils } from "ethers";
|
import { useAccount, useWriteContract, useWaitForTransactionReceipt, usePublicClient } from 'wagmi'
|
||||||
import { useWeb3React } from "@web3-react/core";
|
import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit';
|
||||||
|
import { keccak256, toBytes } from 'viem';
|
||||||
|
import { mechAbi, KIMAP, encodeIntoMintCall, encodeMulticalls, kimapAbi, MULTICALL } from "../abis";
|
||||||
|
import { kinohash } from '../utils/kinohash';
|
||||||
|
import useAppsStore from "../store";
|
||||||
|
|
||||||
import SearchHeader from "../components/SearchHeader";
|
export default function PublishPage() {
|
||||||
import { PageProps } from "../types/Page";
|
const { openConnectModal } = useConnectModal();
|
||||||
import { setChain } from "../utils/chain";
|
const { ourApps, fetchOurApps } = useAppsStore();
|
||||||
import { OPTIMISM_OPT_HEX } from "../constants/chain";
|
const publicClient = usePublicClient();
|
||||||
import { hooks, metaMask } from "../utils/metamask";
|
|
||||||
import Loader from "../components/Loader";
|
|
||||||
import { toDNSWireFormat } from "../utils/dnsWire";
|
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import MetadataForm from "../components/MetadataForm";
|
|
||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
import Checkbox from "../components/Checkbox";
|
|
||||||
import Jazzicon from "../components/Jazzicon";
|
|
||||||
import { Tooltip } from "../components/Tooltip";
|
|
||||||
import HomeButton from "../components/HomeButton";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
|
|
||||||
const { useIsActivating } = hooks;
|
const { address, isConnected, isConnecting } = useAccount();
|
||||||
|
const { data: hash, writeContract, error } = useWriteContract();
|
||||||
|
const { isLoading: isConfirming, isSuccess: isConfirmed } =
|
||||||
|
useWaitForTransactionReceipt({
|
||||||
|
hash,
|
||||||
|
});
|
||||||
|
|
||||||
interface PublishPageProps extends PageProps { }
|
|
||||||
|
|
||||||
export default function PublishPage({
|
|
||||||
provider,
|
|
||||||
packageAbi,
|
|
||||||
}: PublishPageProps) {
|
|
||||||
// get state from router
|
|
||||||
const { state } = useLocation();
|
|
||||||
const { listedApps } = useAppsStore();
|
|
||||||
// TODO: figure out how to handle provider
|
|
||||||
const { account, isActive } = useWeb3React();
|
|
||||||
const isActivating = useIsActivating();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState("");
|
|
||||||
const [publishSuccess, setPublishSuccess] = useState<
|
|
||||||
{ packageName: string; publisherId: string } | undefined
|
|
||||||
>();
|
|
||||||
const [showMetadataForm, setShowMetadataForm] = useState<boolean>(false);
|
|
||||||
const [packageName, setPackageName] = useState<string>("");
|
const [packageName, setPackageName] = useState<string>("");
|
||||||
const [publisherId, setPublisherId] = useState<string>(
|
const [publisherId, setPublisherId] = useState<string>(window.our?.node || "");
|
||||||
window.our?.node || ""
|
|
||||||
); // BytesLike
|
|
||||||
const [metadataUrl, setMetadataUrl] = useState<string>("");
|
const [metadataUrl, setMetadataUrl] = useState<string>("");
|
||||||
const [metadataHash, setMetadataHash] = useState<string>(""); // BytesLike
|
const [metadataHash, setMetadataHash] = useState<string>("");
|
||||||
const [isUpdate, setIsUpdate] = useState<boolean>(false);
|
|
||||||
const [myPublishedApps, setMyPublishedApps] = useState<AppInfo[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const app: AppInfo | undefined = state?.app;
|
fetchOurApps();
|
||||||
if (app) {
|
}, [fetchOurApps]);
|
||||||
setPackageName(app.package);
|
|
||||||
setPublisherId(app.publisher);
|
|
||||||
setIsUpdate(true);
|
|
||||||
}
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMyPublishedApps(
|
|
||||||
listedApps.filter((app) => app.owner?.toLowerCase() === account?.toLowerCase())
|
|
||||||
);
|
|
||||||
}, [listedApps, account])
|
|
||||||
|
|
||||||
const connectWallet = useCallback(async () => {
|
|
||||||
await metaMask.activate().catch(() => { });
|
|
||||||
|
|
||||||
try {
|
|
||||||
setChain(OPTIMISM_OPT_HEX);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const calculateMetadataHash = useCallback(async () => {
|
const calculateMetadataHash = useCallback(async () => {
|
||||||
if (!metadataUrl) {
|
if (!metadataUrl) {
|
||||||
setMetadataHash("");
|
setMetadataHash("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadataResponse = await fetch(metadataUrl);
|
const metadataResponse = await fetch(metadataUrl);
|
||||||
const metadataText = await metadataResponse.text();
|
const metadataText = await metadataResponse.text();
|
||||||
JSON.parse(metadataText); // confirm it's valid JSON
|
JSON.parse(metadataText); // confirm it's valid JSON
|
||||||
const metadataHash = utils.keccak256(utils.toUtf8Bytes(metadataText));
|
const metadataHash = keccak256(toBytes(metadataText));
|
||||||
setMetadataHash(metadataHash);
|
setMetadataHash(metadataHash);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(
|
alert("Error calculating metadata hash. Please ensure the URL is valid and the metadata is in JSON format.");
|
||||||
"Error calculating metadata hash. Please ensure the URL is valid and the metadata is in JSON format."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [metadataUrl]);
|
}, [metadataUrl]);
|
||||||
|
|
||||||
@ -97,189 +51,142 @@ export default function PublishPage({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
let metadata = metadataHash;
|
if (!publicClient || !address) {
|
||||||
|
openConnectModal?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if the package already exists and get its TBA
|
||||||
|
console.log('packageName, publisherId: ', packageName, publisherId)
|
||||||
|
let data = await publicClient.readContract({
|
||||||
|
abi: kimapAbi,
|
||||||
|
address: KIMAP,
|
||||||
|
functionName: 'get',
|
||||||
|
args: [kinohash(`${packageName}.${publisherId}`)]
|
||||||
|
});
|
||||||
|
|
||||||
|
let [tba, owner, _data] = data as [string, string, string];
|
||||||
|
let isUpdate = Boolean(tba && tba !== '0x' && owner === address);
|
||||||
|
let currentTBA = isUpdate ? tba as `0x${string}` : null;
|
||||||
|
console.log('currenttba, isupdate: ', currentTBA, isUpdate)
|
||||||
|
// If the package doesn't exist, check for the publisher's TBA
|
||||||
|
if (!currentTBA) {
|
||||||
|
data = await publicClient.readContract({
|
||||||
|
abi: kimapAbi,
|
||||||
|
address: KIMAP,
|
||||||
|
functionName: 'get',
|
||||||
|
args: [kinohash(publisherId)]
|
||||||
|
});
|
||||||
|
|
||||||
|
[tba, owner, _data] = data as [string, string, string];
|
||||||
|
isUpdate = false; // It's a new package, but we might have a publisher TBA
|
||||||
|
currentTBA = (tba && tba !== '0x') ? tba as `0x${string}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = metadataHash;
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
// https://pongo-uploads.s3.us-east-2.amazonaws.com/chat_metadata.json
|
|
||||||
const metadataResponse = await fetch(metadataUrl);
|
const metadataResponse = await fetch(metadataUrl);
|
||||||
await metadataResponse.json(); // confirm it's valid JSON
|
await metadataResponse.json(); // confirm it's valid JSON
|
||||||
const metadataText = await metadataResponse.text(); // hash as text
|
const metadataText = await metadataResponse.text(); // hash as text
|
||||||
metadata = utils.keccak256(utils.toUtf8Bytes(metadataText));
|
metadata = keccak256(toBytes(metadataText));
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading("Please confirm the transaction in your wallet");
|
const multicall = encodeMulticalls(metadataUrl, metadata);
|
||||||
const publisherIdDnsWireFormat = toDNSWireFormat(publisherId);
|
const args = isUpdate ? multicall : encodeIntoMintCall(multicall, address, packageName);
|
||||||
await setChain(OPTIMISM_OPT_HEX);
|
|
||||||
|
|
||||||
// TODO: have a checkbox to show if it's an update of an existing package
|
writeContract({
|
||||||
|
abi: mechAbi,
|
||||||
|
address: currentTBA || KIMAP,
|
||||||
|
functionName: 'execute',
|
||||||
|
args: [
|
||||||
|
isUpdate ? MULTICALL : KIMAP,
|
||||||
|
BigInt(0),
|
||||||
|
args,
|
||||||
|
isUpdate ? 1 : 0
|
||||||
|
],
|
||||||
|
gas: BigInt(1000000),
|
||||||
|
});
|
||||||
|
|
||||||
const tx = await (isUpdate
|
// Reset form fields
|
||||||
? packageAbi?.updateMetadata(
|
|
||||||
BigNumber.from(
|
|
||||||
utils.solidityKeccak256(
|
|
||||||
["string", "bytes"],
|
|
||||||
[packageName, publisherIdDnsWireFormat]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
metadataUrl,
|
|
||||||
metadata
|
|
||||||
)
|
|
||||||
: packageAbi?.registerApp(
|
|
||||||
packageName,
|
|
||||||
publisherIdDnsWireFormat,
|
|
||||||
metadataUrl,
|
|
||||||
metadata
|
|
||||||
));
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
setLoading("Publishing package...");
|
|
||||||
await tx?.wait();
|
|
||||||
setPublishSuccess({ packageName, publisherId });
|
|
||||||
setPackageName("");
|
setPackageName("");
|
||||||
setPublisherId(window.our?.node || publisherId);
|
setPublisherId(window.our?.node || "");
|
||||||
setMetadataUrl("");
|
setMetadataUrl("");
|
||||||
setMetadataHash("");
|
setMetadataHash("");
|
||||||
setIsUpdate(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
window.alert(
|
|
||||||
"Error publishing package. Please ensure the package name and publisher ID are valid, and the metadata is in JSON format."
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading("");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[publicClient, openConnectModal, packageName, publisherId, address, metadataUrl, metadataHash, writeContract]
|
||||||
packageName,
|
|
||||||
isUpdate,
|
|
||||||
publisherId,
|
|
||||||
metadataUrl,
|
|
||||||
metadataHash,
|
|
||||||
packageAbi,
|
|
||||||
setPublishSuccess,
|
|
||||||
setPackageName,
|
|
||||||
setPublisherId,
|
|
||||||
setMetadataUrl,
|
|
||||||
setMetadataHash,
|
|
||||||
setIsUpdate,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const unpublishPackage = useCallback(
|
const unpublishPackage = useCallback(
|
||||||
async (packageName: string, publisherName: string) => {
|
async (packageName: string, publisherName: string) => {
|
||||||
try {
|
try {
|
||||||
await setChain(OPTIMISM_OPT_HEX);
|
if (!publicClient) {
|
||||||
|
openConnectModal?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tx = await
|
const data = await publicClient.readContract({
|
||||||
packageAbi?.unlistPacakge(
|
abi: kimapAbi,
|
||||||
utils.keccak256(utils.solidityPack(
|
address: KIMAP,
|
||||||
["string", "bytes"],
|
functionName: 'get',
|
||||||
[packageName, toDNSWireFormat(publisherName)]
|
args: [kinohash(`${packageName}.${publisherName}`)]
|
||||||
))
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
const [tba, _owner, _data] = data as [string, string, string];
|
||||||
|
|
||||||
|
if (!tba || tba === '0x') {
|
||||||
|
console.error("No TBA found for this package");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const multicall = encodeMulticalls("", "");
|
||||||
|
|
||||||
|
writeContract({
|
||||||
|
abi: mechAbi,
|
||||||
|
address: tba as `0x${string}`,
|
||||||
|
functionName: 'execute',
|
||||||
|
args: [
|
||||||
|
MULTICALL,
|
||||||
|
BigInt(0),
|
||||||
|
multicall,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
gas: BigInt(1000000),
|
||||||
|
});
|
||||||
|
|
||||||
setLoading("Unlisting package...");
|
|
||||||
await tx?.wait();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
window.alert(
|
|
||||||
"Error unlisting package"
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading("");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[packageAbi, setLoading]
|
[publicClient, openConnectModal, writeContract]
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkIfUpdate = useCallback(async () => {
|
|
||||||
if (isUpdate) return;
|
|
||||||
|
|
||||||
if (
|
|
||||||
packageName &&
|
|
||||||
publisherId &&
|
|
||||||
listedApps.find(
|
|
||||||
(app) => app.package === packageName && app.publisher === publisherId
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setIsUpdate(true);
|
|
||||||
}
|
|
||||||
}, [listedApps, packageName, publisherId, isUpdate, setIsUpdate]);
|
|
||||||
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("w-full flex flex-col gap-2", {
|
<div className="publish-page">
|
||||||
'max-w-[900px]': !isMobile,
|
<h1>Publish Package</h1>
|
||||||
'p-2 h-screen w-screen': isMobile
|
{Boolean(address) && (
|
||||||
})}>
|
<div className="publisher-info">
|
||||||
{!isMobile && <HomeButton />}
|
|
||||||
<SearchHeader
|
|
||||||
hideSearch
|
|
||||||
hidePublish
|
|
||||||
onBack={showMetadataForm ? () => setShowMetadataForm(false) : undefined}
|
|
||||||
/>
|
|
||||||
<div className="flex-center justify-between">
|
|
||||||
<h4>Publish Package</h4>
|
|
||||||
{Boolean(account) && <div className="card flex-center">
|
|
||||||
<span>Publishing as:</span>
|
<span>Publishing as:</span>
|
||||||
<Jazzicon address={account!} className="mx-2" />
|
<span className="address">{address?.slice(0, 4)}...{address?.slice(-4)}</span>
|
||||||
<span className="font-mono">{account?.slice(0, 4)}...{account?.slice(-4)}</span>
|
|
||||||
</div>}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{isConfirming ? (
|
||||||
<div className="flex-col-center">
|
<div className="message info">Publishing package...</div>
|
||||||
<Loader msg={loading} />
|
) : !address || !isConnected ? (
|
||||||
</div>
|
|
||||||
) : publishSuccess ? (
|
|
||||||
<div className="flex-col-center gap-2">
|
|
||||||
<h4>Package Published!</h4>
|
|
||||||
<div>
|
|
||||||
<strong>Package Name:</strong> {publishSuccess.packageName}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Publisher ID:</strong> {publishSuccess.publisherId}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={`flex ml-2`}
|
|
||||||
onClick={() => setPublishSuccess(undefined)}
|
|
||||||
>
|
|
||||||
Publish Another Package
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : showMetadataForm ? (
|
|
||||||
<MetadataForm {...{ packageName, publisherId, app: state?.app }} goBack={() => setShowMetadataForm(false)} />
|
|
||||||
) : !account || !isActive ? (
|
|
||||||
<>
|
<>
|
||||||
<h4>Please connect your wallet {isMobile && <br />} to publish a package</h4>
|
<h4>Please connect your wallet to publish a package</h4>
|
||||||
<button className={`connect-wallet row`} onClick={connectWallet}>
|
<ConnectButton />
|
||||||
Connect Wallet
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
) : isActivating ? (
|
) : isConnecting ? (
|
||||||
<Loader msg="Approve connection in your wallet" />
|
<div className="message info">Approve connection in your wallet</div>
|
||||||
) : (
|
) : (
|
||||||
<form
|
<form className="publish-form" onSubmit={publishPackage}>
|
||||||
className="flex flex-col flex-1 overflow-y-auto gap-2"
|
<div className="form-group">
|
||||||
onSubmit={publishPackage}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex cursor-pointer p-2 -mb-2"
|
|
||||||
onClick={() => setIsUpdate(!isUpdate)}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={isUpdate} readOnly
|
|
||||||
/>
|
|
||||||
<label htmlFor="update" className="cursor-pointer ml-4">
|
|
||||||
Update existing package
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="package-name">Package Name</label>
|
<label htmlFor="package-name">Package Name</label>
|
||||||
<input
|
<input
|
||||||
id="package-name"
|
id="package-name"
|
||||||
@ -288,10 +195,9 @@ export default function PublishPage({
|
|||||||
placeholder="my-package"
|
placeholder="my-package"
|
||||||
value={packageName}
|
value={packageName}
|
||||||
onChange={(e) => setPackageName(e.target.value)}
|
onChange={(e) => setPackageName(e.target.value)}
|
||||||
onBlur={checkIfUpdate}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="form-group">
|
||||||
<label htmlFor="publisher-id">Publisher ID</label>
|
<label htmlFor="publisher-id">Publisher ID</label>
|
||||||
<input
|
<input
|
||||||
id="publisher-id"
|
id="publisher-id"
|
||||||
@ -299,13 +205,10 @@ export default function PublishPage({
|
|||||||
required
|
required
|
||||||
value={publisherId}
|
value={publisherId}
|
||||||
onChange={(e) => setPublisherId(e.target.value)}
|
onChange={(e) => setPublisherId(e.target.value)}
|
||||||
onBlur={checkIfUpdate}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="form-group">
|
||||||
<label htmlFor="metadata-url">
|
<label htmlFor="metadata-url">Metadata URL</label>
|
||||||
Metadata URL
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
id="metadata-url"
|
id="metadata-url"
|
||||||
type="text"
|
type="text"
|
||||||
@ -315,54 +218,55 @@ export default function PublishPage({
|
|||||||
onBlur={calculateMetadataHash}
|
onBlur={calculateMetadataHash}
|
||||||
placeholder="https://github/my-org/my-repo/metadata.json"
|
placeholder="https://github/my-org/my-repo/metadata.json"
|
||||||
/>
|
/>
|
||||||
<div>
|
<p className="help-text">
|
||||||
Metadata is a JSON file that describes your package.
|
Metadata is a JSON file that describes your package.
|
||||||
<br /> You can{" "}
|
</p>
|
||||||
<a onClick={() => setShowMetadataForm(true)}
|
|
||||||
className="underline cursor-pointer"
|
|
||||||
>
|
|
||||||
fill out a template here
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="form-group">
|
||||||
<div className="flex flex-col">
|
|
||||||
<label htmlFor="metadata-hash">Metadata Hash</label>
|
<label htmlFor="metadata-hash">Metadata Hash</label>
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
id="metadata-hash"
|
id="metadata-hash"
|
||||||
type="text"
|
type="text"
|
||||||
value={metadataHash}
|
value={metadataHash}
|
||||||
onChange={(e) => setMetadataHash(e.target.value)}
|
|
||||||
placeholder="Calculated automatically from metadata URL"
|
placeholder="Calculated automatically from metadata URL"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">
|
<button type="submit" disabled={isConfirming}>
|
||||||
Publish
|
{isConfirming ? 'Publishing...' : 'Publish'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col">
|
{isConfirmed && (
|
||||||
<h4>Packages You Own</h4>
|
<div className="message success">
|
||||||
{myPublishedApps.length > 0 ? (
|
Package published successfully!
|
||||||
<div className="flex flex-col">
|
|
||||||
{myPublishedApps.map((app) => (
|
|
||||||
<div key={`${app.package}${app.publisher}`} className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Jazzicon address={app.publisher} className="mr-2" />
|
|
||||||
<span>{app.package}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button className="flex items-center" onClick={() => unpublishPackage(app.package, app.publisher)}>
|
)}
|
||||||
<span>Unpublish</span>
|
{error && (
|
||||||
|
<div className="message error">
|
||||||
|
Error: {error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="my-packages">
|
||||||
|
<h2>Packages You Own</h2>
|
||||||
|
{Object.keys(ourApps).length > 0 ? (
|
||||||
|
<ul>
|
||||||
|
{Object.values(ourApps).map((app) => (
|
||||||
|
<li key={`${app.package_id.package_name}:${app.package_id.publisher_node}`}>
|
||||||
|
<Link to={`/app/${app.package_id.package_name}:${app.package_id.publisher_node}`} className="app-name">
|
||||||
|
{app.metadata?.name || app.package_id.package_name}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button onClick={() => unpublishPackage(app.package_id.package_name, app.package_id.publisher_node)}>
|
||||||
|
Unpublish
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center">
|
<p>No packages published</p>
|
||||||
<span>No packages published</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,239 +1,87 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
|
import useAppsStore from "../store";
|
||||||
|
import { AppListing } from "../types/Apps";
|
||||||
import { AppInfo } from "../types/Apps";
|
import { Link } from "react-router-dom";
|
||||||
import useAppsStore from "../store/apps-store";
|
|
||||||
import AppEntry from "../components/AppEntry";
|
|
||||||
import SearchHeader from "../components/SearchHeader";
|
|
||||||
import { PageProps } from "../types/Page";
|
|
||||||
import { appId } from "../utils/app";
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { FaArrowRotateRight } from "react-icons/fa6";
|
|
||||||
import { isMobileCheck } from "../utils/dimensions";
|
|
||||||
import HomeButton from "../components/HomeButton";
|
|
||||||
|
|
||||||
interface StorePageProps extends PageProps { }
|
|
||||||
|
|
||||||
export default function StorePage() {
|
export default function StorePage() {
|
||||||
// eslint-disable-line
|
const { listings, fetchListings } = useAppsStore();
|
||||||
const { listedApps, getListedApps, rebuildIndex } = useAppsStore();
|
|
||||||
|
|
||||||
const [resultsSort, setResultsSort] = useState<string>("Recently published");
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [displayedApps, setDisplayedApps] = useState<AppInfo[]>(listedApps);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [tags, setTags] = useState<string[]>([])
|
|
||||||
const [launchPaths, setLaunchPaths] = useState<{ [package_name: string]: string }>({})
|
|
||||||
|
|
||||||
const pages = useMemo(
|
|
||||||
() =>
|
|
||||||
Array.from(
|
|
||||||
{
|
|
||||||
length: Math.ceil(listedApps.length / 10)
|
|
||||||
},
|
|
||||||
(_, index) => index + 1
|
|
||||||
),
|
|
||||||
[listedApps]
|
|
||||||
);
|
|
||||||
|
|
||||||
const featuredPackageNames = ['dartfrog', 'kcal', 'memedeck', 'filter'];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const start = (page - 1) * 10;
|
fetchListings();
|
||||||
const end = start + 10;
|
}, [fetchListings]);
|
||||||
setDisplayedApps(listedApps.slice(start, end));
|
|
||||||
}, [listedApps, page]);
|
|
||||||
|
|
||||||
// GET on load
|
// extensive temp null handling due to weird prod bug
|
||||||
useEffect(() => {
|
const filteredApps = React.useMemo(() => {
|
||||||
getListedApps()
|
if (!listings) return [];
|
||||||
.then((apps) => {
|
return Object.values(listings).filter((app) => {
|
||||||
setDisplayedApps(Object.values(apps));
|
if (!app || !app.package_id) return false;
|
||||||
let _tags: string[] = [];
|
const nameMatch = app.package_id.package_name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
for (const app of Object.values(apps)) {
|
const descMatch = app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase()) || false;
|
||||||
_tags = _tags.concat((app.metadata as any || {}).tags || [])
|
return nameMatch || descMatch;
|
||||||
}
|
});
|
||||||
if (_tags.length === 0) {
|
}, [listings, searchQuery]);
|
||||||
_tags = ['App', 'Tags', 'Coming', 'Soon', 'tm'];
|
|
||||||
}
|
|
||||||
setTags(Array.from(new Set(_tags)))
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error));
|
|
||||||
}, []); // eslint-disable-line
|
|
||||||
|
|
||||||
const sortApps = useCallback(async (sort: string) => {
|
|
||||||
switch (sort) {
|
|
||||||
case "Recently published":
|
|
||||||
break;
|
|
||||||
case "Most popular":
|
|
||||||
break;
|
|
||||||
case "Best rating":
|
|
||||||
break;
|
|
||||||
case "Recently updated":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const searchApps = useCallback(
|
|
||||||
(query: string) => {
|
|
||||||
setSearchQuery(query);
|
|
||||||
const filteredApps = listedApps.filter(
|
|
||||||
(app) => {
|
|
||||||
return (
|
|
||||||
app.package.toLowerCase().includes(query.toLowerCase()) ||
|
|
||||||
app.metadata?.description
|
|
||||||
?.toLowerCase()
|
|
||||||
.includes(query.toLowerCase()) ||
|
|
||||||
app.metadata?.description
|
|
||||||
?.toLowerCase()
|
|
||||||
.includes(query.toLowerCase())
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[listedApps]
|
|
||||||
);
|
|
||||||
setDisplayedApps(filteredApps);
|
|
||||||
},
|
|
||||||
[listedApps]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tryRebuildIndex = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
await rebuildIndex();
|
|
||||||
alert("Index rebuilt successfully.");
|
|
||||||
await getListedApps();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}, [rebuildIndex]);
|
|
||||||
|
|
||||||
const isMobile = isMobileCheck()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/apps').then(data => data.json())
|
|
||||||
.then((data: Array<{ package_name: string, path: string }>) => {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
listedApps.forEach(app => {
|
|
||||||
const homepageAppData = data.find(otherApp => app.package === otherApp.package_name)
|
|
||||||
if (homepageAppData) {
|
|
||||||
setLaunchPaths({
|
|
||||||
...launchPaths,
|
|
||||||
[app.package]: homepageAppData.path
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [listedApps])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("flex flex-col w-full max-h-screen p-2", {
|
<div className="store-page">
|
||||||
'gap-4 max-w-screen': isMobile,
|
<div className="store-header">
|
||||||
'gap-6 max-w-[900px]': !isMobile
|
<input
|
||||||
})}>
|
type="text"
|
||||||
{!isMobile && <HomeButton />}
|
placeholder="Search apps..."
|
||||||
<SearchHeader value={searchQuery} onChange={searchApps} />
|
value={searchQuery}
|
||||||
<div className={classNames("flex items-center self-stretch justify-between", {
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
'gap-4 flex-wrap': isMobile,
|
|
||||||
'gap-8 grow': !isMobile
|
|
||||||
})}>
|
|
||||||
<button
|
|
||||||
className="flex flex-col c icon icon-orange"
|
|
||||||
onClick={tryRebuildIndex}
|
|
||||||
title="Rebuild index"
|
|
||||||
>
|
|
||||||
<FaArrowRotateRight />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{tags.slice(0, isMobile ? 3 : 6).map(tag => (
|
|
||||||
<button
|
|
||||||
key={tag}
|
|
||||||
className="clear flex c rounded-full !bg-white/10 !hover:bg-white/25"
|
|
||||||
onClick={() => {
|
|
||||||
console.log('clicked tag', tag)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={resultsSort}
|
|
||||||
onChange={(e) => {
|
|
||||||
setResultsSort(e.target.value);
|
|
||||||
sortApps(e.target.value);
|
|
||||||
}}
|
|
||||||
className={classNames('hidden', {
|
|
||||||
'basis-1/5': !isMobile
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<option>Recently published</option>
|
|
||||||
<option>Most popular</option>
|
|
||||||
<option>Best rating</option>
|
|
||||||
<option>Recently updated</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{!searchQuery && <div className={classNames("flex flex-col", {
|
|
||||||
'gap-4': !isMobile,
|
|
||||||
'grow overflow-y-auto gap-2 items-center px-2': isMobile
|
|
||||||
})}>
|
|
||||||
<h2>Featured Apps</h2>
|
|
||||||
<div className={classNames("flex gap-2", {
|
|
||||||
'flex-col': isMobile
|
|
||||||
})}>
|
|
||||||
{listedApps.filter(app => {
|
|
||||||
return featuredPackageNames.indexOf(app.package) !== -1
|
|
||||||
}).map((app) => (
|
|
||||||
<AppEntry
|
|
||||||
key={appId(app) + (app.state?.our_version || "")}
|
|
||||||
size={'medium'}
|
|
||||||
app={app}
|
|
||||||
launchPath={launchPaths[app.package]}
|
|
||||||
className={classNames("grow", {
|
|
||||||
'w-1/4': !isMobile,
|
|
||||||
'w-full': isMobile
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
<div className="app-list">
|
||||||
<h2>{searchQuery ? 'Search Results' : 'All Apps'}</h2>
|
{!listings ? (
|
||||||
<div className={classNames("flex flex-col grow overflow-y-auto", {
|
<p>Loading...</p>
|
||||||
'gap-2': isMobile,
|
) : filteredApps.length === 0 ? (
|
||||||
'gap-4': !isMobile,
|
<p>No apps available.</p>
|
||||||
})}>
|
) : (
|
||||||
{displayedApps
|
<table>
|
||||||
.filter(app => searchQuery ? true : featuredPackageNames.indexOf(app.package) === -1)
|
<thead>
|
||||||
.map(app => <AppEntry
|
<tr>
|
||||||
key={appId(app) + (app.state?.our_version || "")}
|
<th></th>
|
||||||
size='large'
|
<th>Name</th>
|
||||||
app={app}
|
<th>Description</th>
|
||||||
className="self-stretch"
|
<th>Publisher</th>
|
||||||
overrideImageSize="medium"
|
</tr>
|
||||||
/>)}
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
{pages.length > 1 && <div className="flex flex-wrap self-center gap-2">
|
{filteredApps.map((app) => (
|
||||||
<button
|
<AppRow key={`${app.package_id?.package_name}:${app.package_id?.publisher_node}`} app={app} />
|
||||||
className="icon"
|
|
||||||
onClick={() => page !== pages[0] && setPage(page - 1)}
|
|
||||||
>
|
|
||||||
<FaChevronLeft />
|
|
||||||
</button>
|
|
||||||
{pages.map((p) => (
|
|
||||||
<button
|
|
||||||
key={`page-${p}`}
|
|
||||||
className={classNames('icon', { "!bg-white/10": p === page })}
|
|
||||||
onClick={() => setPage(p)}
|
|
||||||
>
|
|
||||||
{p}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
<button
|
</tbody>
|
||||||
className="icon"
|
</table>
|
||||||
onClick={() => page !== pages[pages.length - 1] && setPage(page + 1)}
|
)}
|
||||||
>
|
</div>
|
||||||
<FaChevronRight />
|
|
||||||
</button>
|
|
||||||
</div>}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AppRow: React.FC<{ app: AppListing }> = ({ app }) => {
|
||||||
|
if (!app || !app.package_id) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="app-row">
|
||||||
|
<td>
|
||||||
|
{app.metadata?.image && (
|
||||||
|
<img
|
||||||
|
src={app.metadata.image}
|
||||||
|
alt={`${app.metadata?.name || app.package_id.package_name} icon`}
|
||||||
|
className="app-icon"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Link to={`/app/${app.package_id.package_name}:${app.package_id.publisher_node}`} className="app-name">
|
||||||
|
{app.metadata?.name || app.package_id.package_name}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>{app.metadata?.description || "No description available"}</td>
|
||||||
|
<td>{app.package_id.publisher_node}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
@ -1,217 +0,0 @@
|
|||||||
import { create } from 'zustand'
|
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
|
||||||
import { MyApps, AppInfo, PackageManifest } from '../types/Apps'
|
|
||||||
import { HTTP_STATUS } from '../constants/http';
|
|
||||||
import { appId, getAppType } from '../utils/app';
|
|
||||||
|
|
||||||
const BASE_URL = (import.meta as any).env.BASE_URL; // eslint-disable-line
|
|
||||||
|
|
||||||
const isApp = (a1: AppInfo, a2: AppInfo) => a1.package === a2.package && a1.publisher === a2.publisher
|
|
||||||
|
|
||||||
export interface AppsStore {
|
|
||||||
myApps: MyApps
|
|
||||||
listedApps: AppInfo[]
|
|
||||||
searchResults: AppInfo[]
|
|
||||||
query: string
|
|
||||||
|
|
||||||
getMyApps: () => Promise<MyApps>
|
|
||||||
getListedApps: () => Promise<AppInfo[]>
|
|
||||||
getMyApp: (app: AppInfo) => Promise<AppInfo>
|
|
||||||
installApp: (app: AppInfo) => Promise<void>
|
|
||||||
updateApp: (app: AppInfo) => Promise<void>
|
|
||||||
uninstallApp: (app: AppInfo) => Promise<void>
|
|
||||||
getListedApp: (packageName: string) => Promise<AppInfo>
|
|
||||||
downloadApp: (app: AppInfo, download_from: string) => Promise<void>
|
|
||||||
getCaps: (app: AppInfo) => Promise<PackageManifest>
|
|
||||||
approveCaps: (app: AppInfo) => Promise<void>
|
|
||||||
setMirroring: (info: AppInfo, mirroring: boolean) => Promise<void>
|
|
||||||
setAutoUpdate: (app: AppInfo, autoUpdate: boolean) => Promise<void>
|
|
||||||
rebuildIndex: () => Promise<void>
|
|
||||||
|
|
||||||
// searchApps: (query: string, onlyMyApps?: boolean) => Promise<AppInfo[]>
|
|
||||||
|
|
||||||
get: () => AppsStore
|
|
||||||
set: (partial: AppsStore | Partial<AppsStore>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const useAppsStore = create<AppsStore>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
myApps: {
|
|
||||||
downloaded: [] as AppInfo[],
|
|
||||||
installed: [] as AppInfo[],
|
|
||||||
local: [] as AppInfo[],
|
|
||||||
system: [] as AppInfo[],
|
|
||||||
},
|
|
||||||
listedApps: [] as AppInfo[],
|
|
||||||
searchResults: [] as AppInfo[],
|
|
||||||
query: '',
|
|
||||||
getMyApps: async () => {
|
|
||||||
const listedApps = await get().getListedApps()
|
|
||||||
const res = await fetch(`${BASE_URL}/apps`)
|
|
||||||
const apps = await res.json() as AppInfo[]
|
|
||||||
|
|
||||||
const myApps = apps.reduce((acc, app) => {
|
|
||||||
const appType = getAppType(app)
|
|
||||||
|
|
||||||
if (listedApps.find(lapp => lapp.metadata_hash === app.metadata_hash)) {
|
|
||||||
console.log({ listedappmatch: app })
|
|
||||||
}
|
|
||||||
acc[appType].push(app)
|
|
||||||
return acc
|
|
||||||
}, {
|
|
||||||
downloaded: [],
|
|
||||||
installed: [],
|
|
||||||
local: [],
|
|
||||||
system: [],
|
|
||||||
} as MyApps)
|
|
||||||
|
|
||||||
set(() => ({ myApps }))
|
|
||||||
return myApps
|
|
||||||
},
|
|
||||||
getListedApps: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/listed`)
|
|
||||||
const apps = await res.json() as AppInfo[]
|
|
||||||
set({ listedApps: apps })
|
|
||||||
return apps
|
|
||||||
},
|
|
||||||
getMyApp: async (info: AppInfo) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}`)
|
|
||||||
const app = await res.json() as AppInfo
|
|
||||||
const appType = getAppType(app)
|
|
||||||
const myApps = get().myApps
|
|
||||||
myApps[appType] = myApps[appType].map((a) => isApp(a, app) ? app : a)
|
|
||||||
const listedApps = [...get().listedApps].map((a) => isApp(a, app) ? app : a)
|
|
||||||
set({ myApps, listedApps })
|
|
||||||
return app
|
|
||||||
},
|
|
||||||
installApp: async (info: AppInfo) => {
|
|
||||||
const approveRes = await fetch(`${BASE_URL}/apps/${appId(info)}/caps`, {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
if (approveRes.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error(`Failed to approve caps for app: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const installRes = await fetch(`${BASE_URL}/apps/${appId(info)}`, {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
if (installRes.status !== HTTP_STATUS.CREATED) {
|
|
||||||
throw new Error(`Failed to install app: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateApp: async (app: AppInfo) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, {
|
|
||||||
method: 'PUT'
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.NO_CONTENT) {
|
|
||||||
throw new Error(`Failed to update app: ${appId(app)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: get the app from the server instead of updating locally
|
|
||||||
},
|
|
||||||
uninstallApp: async (app: AppInfo) => {
|
|
||||||
if (!confirm(`Are you sure you want to remove ${appId(app)}?`)) return
|
|
||||||
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.NO_CONTENT) {
|
|
||||||
throw new Error(`Failed to remove app: ${appId(app)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const myApps = { ...get().myApps }
|
|
||||||
const appType = getAppType(app)
|
|
||||||
myApps[appType] = myApps[appType].filter((a) => !isApp(a, app))
|
|
||||||
const listedApps = get().listedApps.map((a) => isApp(a, app) ? { ...a, state: undefined, installed: false } : a)
|
|
||||||
set({ myApps, listedApps })
|
|
||||||
},
|
|
||||||
getListedApp: async (packageName: string) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/listed/${packageName}`)
|
|
||||||
if (res.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error(`Failed to get app: ${packageName}`)
|
|
||||||
}
|
|
||||||
const app = await res.json() as AppInfo
|
|
||||||
return app
|
|
||||||
},
|
|
||||||
downloadApp: async (info: AppInfo, download_from: string) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/listed/${appId(info)}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ download_from }),
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.CREATED) {
|
|
||||||
throw new Error(`Failed to get app: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCaps: async (info: AppInfo) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/caps`)
|
|
||||||
if (res.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error(`Failed to get app: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const caps = await res.json() as PackageManifest[]
|
|
||||||
return caps[0]
|
|
||||||
},
|
|
||||||
approveCaps: async (info: AppInfo) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/caps`, {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error(`Failed to get app: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rebuildIndex: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/rebuild-index`, {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error('Failed to rebuild index')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setMirroring: async (info: AppInfo, mirroring: boolean) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/mirror`, {
|
|
||||||
method: mirroring ? 'PUT' : 'DELETE',
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error(`Failed to start mirror: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
get().getMyApp(info)
|
|
||||||
},
|
|
||||||
setAutoUpdate: async (info: AppInfo, autoUpdate: boolean) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/apps/${appId(info)}/auto-update`, {
|
|
||||||
method: autoUpdate ? 'PUT' : 'DELETE',
|
|
||||||
})
|
|
||||||
if (res.status !== HTTP_STATUS.OK) {
|
|
||||||
throw new Error(`Failed to change auto update: ${appId(info)}`)
|
|
||||||
}
|
|
||||||
get().getMyApp(info)
|
|
||||||
},
|
|
||||||
|
|
||||||
// searchApps: async (query: string, onlyMyApps = true) => {
|
|
||||||
// if (onlyMyApps) {
|
|
||||||
// const searchResults = get().myApps.filter((app) =>
|
|
||||||
// app.name.toLowerCase().includes(query.toLowerCase())
|
|
||||||
// || app.publisher.toLowerCase().includes(query.toLowerCase())
|
|
||||||
// || app.metadata?.name?.toLowerCase()?.includes(query.toLowerCase())
|
|
||||||
// )
|
|
||||||
// set(() => ({ searchResults }))
|
|
||||||
// return searchResults
|
|
||||||
// } else {
|
|
||||||
// const res = await fetch(`${BASE_URL}/apps/search/${encodeURIComponent(query)}`)
|
|
||||||
// const searchResults = await res.json() as AppInfo[]
|
|
||||||
// set(() => ({ searchResults }))
|
|
||||||
// return searchResults
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'app_store', // unique name
|
|
||||||
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export default useAppsStore
|
|
379
kinode/packages/app_store/ui/src/store/index.ts
Normal file
379
kinode/packages/app_store/ui/src/store/index.ts
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
import { PackageState, AppListing, MirrorCheckFile, PackageManifest, DownloadItem } from '../types/Apps'
|
||||||
|
import { HTTP_STATUS } from '../constants/http'
|
||||||
|
import KinodeClientApi from "@kinode/client-api"
|
||||||
|
import { WEBSOCKET_URL } from '../utils/ws'
|
||||||
|
|
||||||
|
const BASE_URL = '/main:app_store:sys'
|
||||||
|
|
||||||
|
interface AppsStore {
|
||||||
|
listings: Record<string, AppListing>
|
||||||
|
installed: Record<string, PackageState>
|
||||||
|
downloads: Record<string, DownloadItem[]>
|
||||||
|
ourApps: AppListing[]
|
||||||
|
ws: KinodeClientApi
|
||||||
|
activeDownloads: Record<string, { downloaded: number, total: number }>
|
||||||
|
|
||||||
|
fetchData: (id: string) => Promise<void>
|
||||||
|
fetchListings: () => Promise<void>
|
||||||
|
fetchListing: (id: string) => Promise<AppListing | null>
|
||||||
|
fetchInstalled: () => Promise<void>
|
||||||
|
fetchInstalledApp: (id: string) => Promise<PackageState | null>
|
||||||
|
fetchDownloads: () => Promise<DownloadItem[]>
|
||||||
|
fetchOurApps: () => Promise<void>
|
||||||
|
fetchDownloadsForApp: (id: string) => Promise<DownloadItem[]>
|
||||||
|
checkMirror: (node: string) => Promise<MirrorCheckFile | null>
|
||||||
|
|
||||||
|
installApp: (id: string, version_hash: string) => Promise<void>
|
||||||
|
uninstallApp: (id: string) => Promise<void>
|
||||||
|
downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void>
|
||||||
|
removeDownload: (packageId: string, versionHash: string) => Promise<void>
|
||||||
|
getCaps: (id: string) => Promise<PackageManifest | null>
|
||||||
|
approveCaps: (id: string) => Promise<void>
|
||||||
|
startMirroring: (id: string) => Promise<void>
|
||||||
|
stopMirroring: (id: string) => Promise<void>
|
||||||
|
setAutoUpdate: (id: string, version_hash: string, autoUpdate: boolean) => Promise<void>
|
||||||
|
|
||||||
|
setActiveDownload: (appId: string, downloaded: number, total: number) => void
|
||||||
|
clearActiveDownload: (appId: string) => void
|
||||||
|
clearAllActiveDownloads: () => void;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAppsStore = create<AppsStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get): AppsStore => ({
|
||||||
|
listings: {},
|
||||||
|
installed: {},
|
||||||
|
downloads: {},
|
||||||
|
ourApps: [],
|
||||||
|
activeDownloads: {},
|
||||||
|
|
||||||
|
fetchData: async (id: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
const [listing, downloads, installedApp] = await Promise.all([
|
||||||
|
get().fetchListing(id),
|
||||||
|
get().fetchDownloadsForApp(id),
|
||||||
|
get().fetchInstalledApp(id)
|
||||||
|
]);
|
||||||
|
set((state) => ({
|
||||||
|
listings: listing ? { ...state.listings, [id]: listing } : state.listings,
|
||||||
|
downloads: { ...state.downloads, [id]: downloads },
|
||||||
|
installed: installedApp ? { ...state.installed, [id]: installedApp } : state.installed
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching app data:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchListings: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const data: AppListing[] = await res.json();
|
||||||
|
const listingsMap = data.reduce((acc, listing) => {
|
||||||
|
acc[`${listing.package_id.package_name}:${listing.package_id.publisher_node}`] = listing;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, AppListing>);
|
||||||
|
set({ listings: listingsMap });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching listings:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchListing: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const listing: AppListing = await res.json();
|
||||||
|
set((state) => ({
|
||||||
|
listings: { ...state.listings, [id]: listing }
|
||||||
|
}));
|
||||||
|
return listing;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching listing:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchInstalled: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/installed`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const data: PackageState[] = await res.json();
|
||||||
|
const installedMap = data.reduce((acc, pkg) => {
|
||||||
|
acc[`${pkg.package_id.package_name}:${pkg.package_id.publisher_node}`] = pkg;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, PackageState>);
|
||||||
|
set({ installed: installedMap });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching installed apps:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchInstalledApp: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/installed/${id}`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const installedApp: PackageState = await res.json();
|
||||||
|
set((state) => ({
|
||||||
|
installed: { ...state.installed, [id]: installedApp }
|
||||||
|
}));
|
||||||
|
return installedApp;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching installed app:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchDownloads: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/downloads`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const downloads: DownloadItem[] = await res.json();
|
||||||
|
set({ downloads: { root: downloads } });
|
||||||
|
return downloads;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching downloads:", error);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchOurApps: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/ourapps`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const data: AppListing[] = await res.json();
|
||||||
|
set({ ourApps: data });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching our apps:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchDownloadsForApp: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/downloads/${id}`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
const downloads: DownloadItem[] = await res.json();
|
||||||
|
set((state) => ({
|
||||||
|
downloads: { ...state.downloads, [id]: downloads }
|
||||||
|
}));
|
||||||
|
return downloads;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching downloads for app:", error);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
checkMirror: async (node: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
return await res.json() as MirrorCheckFile;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking mirror:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
installApp: async (id: string, version_hash: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}/install`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ version_hash })
|
||||||
|
});
|
||||||
|
if (res.status === HTTP_STATUS.CREATED) {
|
||||||
|
await get().fetchInstalled();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error installing app:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstallApp: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}`, { method: 'DELETE' });
|
||||||
|
if (res.status === HTTP_STATUS.NO_CONTENT) {
|
||||||
|
await get().fetchInstalled();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uninstalling app:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadApp: async (id: string, version_hash: string, downloadFrom: string) => {
|
||||||
|
const [package_name, publisher_node] = id.split(':');
|
||||||
|
const appId = `${id}:${version_hash}`;
|
||||||
|
set((state) => ({
|
||||||
|
activeDownloads: {
|
||||||
|
...state.activeDownloads,
|
||||||
|
[appId]: { downloaded: 0, total: 100 }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}/download`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
package_id: { package_name, publisher_node },
|
||||||
|
version_hash,
|
||||||
|
download_from: downloadFrom,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (res.status !== HTTP_STATUS.OK) {
|
||||||
|
get().clearActiveDownload(appId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading app:", error);
|
||||||
|
get().clearActiveDownload(appId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAllActiveDownloads: () => set({ activeDownloads: {} }),
|
||||||
|
|
||||||
|
removeDownload: async (packageId: string, versionHash: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BASE_URL}/downloads/${packageId}/remove`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ version_hash: versionHash }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
await get().fetchDownloadsForApp(packageId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove download:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCaps: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}/caps`);
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
return await res.json() as PackageManifest;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting caps:", error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
approveCaps: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}/caps`, { method: 'POST' });
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
await get().fetchListing(id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error approving caps:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startMirroring: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/downloads/${id}/mirror`, {
|
||||||
|
method: 'PUT'
|
||||||
|
});
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
await get().fetchDownloadsForApp(id.split(':').slice(0, -1).join(':'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error starting mirroring:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopMirroring: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/downloads/${id}/mirror`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
await get().fetchDownloadsForApp(id.split(':').slice(0, -1).join(':'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error stopping mirroring:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setAutoUpdate: async (id: string, version_hash: string, autoUpdate: boolean) => {
|
||||||
|
try {
|
||||||
|
const method = autoUpdate ? 'PUT' : 'DELETE';
|
||||||
|
const res = await fetch(`${BASE_URL}/apps/${id}/auto-update`, {
|
||||||
|
method,
|
||||||
|
body: JSON.stringify({ version_hash })
|
||||||
|
});
|
||||||
|
if (res.status === HTTP_STATUS.OK) {
|
||||||
|
await get().fetchListing(id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting auto-update:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setActiveDownload: (appId, downloaded, total) => {
|
||||||
|
set((state) => ({
|
||||||
|
activeDownloads: {
|
||||||
|
...state.activeDownloads,
|
||||||
|
[appId]: { downloaded, total }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
clearActiveDownload: (appId) => {
|
||||||
|
set((state) => {
|
||||||
|
const { [appId]: _, ...rest } = state.activeDownloads;
|
||||||
|
return { activeDownloads: rest };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
ws: new KinodeClientApi({
|
||||||
|
uri: WEBSOCKET_URL,
|
||||||
|
nodeId: (window as any).our?.node,
|
||||||
|
processId: "main:app_store:sys",
|
||||||
|
onMessage: (message) => {
|
||||||
|
console.log('WebSocket message received', message);
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
if (data.kind === 'progress') {
|
||||||
|
const { package_id, version_hash, downloaded, total } = data.data;
|
||||||
|
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
||||||
|
get().setActiveDownload(appId, downloaded, total);
|
||||||
|
} else if (data.kind === 'complete') {
|
||||||
|
const { package_id, version_hash } = data.data;
|
||||||
|
const appId = `${package_id.package_name}:${package_id.publisher_node}:${version_hash}`;
|
||||||
|
get().clearActiveDownload(appId);
|
||||||
|
get().fetchData(`${package_id.package_name}:${package_id.publisher_node}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpen: (_e) => {
|
||||||
|
console.log('WebSocket connection opened');
|
||||||
|
},
|
||||||
|
onClose: (_e) => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'app_store',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default useAppsStore
|
@ -1,18 +1,37 @@
|
|||||||
export interface MyApps {
|
export interface PackageId {
|
||||||
downloaded: AppInfo[]
|
package_name: string;
|
||||||
installed: AppInfo[]
|
publisher_node: string;
|
||||||
local: AppInfo[]
|
|
||||||
system: AppInfo[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppListing {
|
export interface AppListing {
|
||||||
owner?: string
|
package_id: PackageId
|
||||||
package: string
|
tba: string
|
||||||
publisher: string
|
metadata_uri: string
|
||||||
metadata_hash: string
|
metadata_hash: string
|
||||||
metadata?: OnchainPackageMetadata
|
metadata?: OnchainPackageMetadata
|
||||||
installed: boolean
|
auto_update: boolean
|
||||||
state?: PackageState
|
}
|
||||||
|
|
||||||
|
export type DownloadItem = {
|
||||||
|
Dir?: DirItem;
|
||||||
|
File?: FileItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DirItem {
|
||||||
|
name: string;
|
||||||
|
mirroring: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileItem {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
manifest: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MirrorCheckFile {
|
||||||
|
node: string;
|
||||||
|
is_online: boolean;
|
||||||
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Erc721Properties {
|
export interface Erc721Properties {
|
||||||
@ -20,7 +39,7 @@ export interface Erc721Properties {
|
|||||||
publisher: string;
|
publisher: string;
|
||||||
current_version: string;
|
current_version: string;
|
||||||
mirrors: string[];
|
mirrors: string[];
|
||||||
code_hashes: Record<string, string>;
|
code_hashes: [string, string][];
|
||||||
license?: string;
|
license?: string;
|
||||||
screenshots?: string[];
|
screenshots?: string[];
|
||||||
wit_version?: [number, number, number];
|
wit_version?: [number, number, number];
|
||||||
@ -36,19 +55,10 @@ export interface OnchainPackageMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PackageState {
|
export interface PackageState {
|
||||||
mirrored_from: string;
|
package_id: PackageId;
|
||||||
our_version: string;
|
our_version_hash: string;
|
||||||
installed: boolean;
|
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
caps_approved: boolean;
|
caps_approved: boolean;
|
||||||
manifest_hash?: string;
|
|
||||||
mirroring: boolean;
|
|
||||||
auto_update: boolean;
|
|
||||||
// source_zip?: Uint8Array, // bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppInfo extends AppListing {
|
|
||||||
permissions?: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PackageManifest {
|
export interface PackageManifest {
|
||||||
@ -56,46 +66,7 @@ export interface PackageManifest {
|
|||||||
process_wasm_path: string
|
process_wasm_path: string
|
||||||
on_exit: string
|
on_exit: string
|
||||||
request_networking: boolean
|
request_networking: boolean
|
||||||
request_capabilities: string[]
|
request_capabilities: any[]
|
||||||
grant_capabilities: string[]
|
grant_capabilities: any[]
|
||||||
public: boolean
|
public: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"installed": false,
|
|
||||||
"metadata": null,
|
|
||||||
"metadata_hash": "0xf244e4e227494c6a0716597f0c405284eb53f7916427d48ceb03a24ed5b52b5d",
|
|
||||||
"owner": "0x7Bf904E36715B650Fb1F99113cb4A2B2FfE22392",
|
|
||||||
"package": "sdapi",
|
|
||||||
"publisher": "mothu-et-doria.os",
|
|
||||||
"state": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"installed": false,
|
|
||||||
|
|
||||||
"metadata_hash": "0xe43f616b39f2511f2c3c29c801a0993de5a74ab1fc4382ff7c68aad50f0242f3",
|
|
||||||
"owner": "0xDe12193c037F768fDC0Db0B77B7E70de723b95E7",
|
|
||||||
"package": "chat",
|
|
||||||
"publisher": "mythicengineer.os",
|
|
||||||
"state": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"installed": false,
|
|
||||||
"metadata": null,
|
|
||||||
"metadata_hash": "0x4385b4b9ddddcc25ce99d6ae1542b1362c0e7f41abf1385cd9eda4d39ced6e39",
|
|
||||||
"owner": "0x7213aa2A6581b37506C035b387b4Bf2Fb93E2f88",
|
|
||||||
"package": "chat_template",
|
|
||||||
"publisher": "odinsbadeye.os",
|
|
||||||
"state": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"installed": false,
|
|
||||||
"metadata": null,
|
|
||||||
"metadata_hash": "0x0f4c02462407d88fb43a0e24df7e36b7be4a09f2fc27bb690e5b76c8d21088ef",
|
|
||||||
"owner": "0x958946dEcCfe3546fE7F3f98eb07c100E472F09D",
|
|
||||||
"package": "kino_files",
|
|
||||||
"publisher": "gloriainexcelsisdeo.os",
|
|
||||||
"state": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { ethers } from "ethers";
|
|
||||||
import { PackageStore } from "../abis/types";
|
|
||||||
|
|
||||||
export interface PageProps {
|
|
||||||
provider?: ethers.providers.Web3Provider;
|
|
||||||
packageAbi?: PackageStore
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { AppInfo } from "../types/Apps";
|
|
||||||
|
|
||||||
export const appId = (app: AppInfo) => `${app.package}:${app.publisher}`
|
|
||||||
|
|
||||||
export const getAppName = (app: AppInfo) => app.metadata?.name || appId(app)
|
|
||||||
|
|
||||||
export enum AppType {
|
|
||||||
Downloaded = 'downloaded',
|
|
||||||
Installed = 'installed',
|
|
||||||
Local = 'local',
|
|
||||||
System = 'system',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAppType = (app: AppInfo) => {
|
|
||||||
if (app.publisher === 'sys') {
|
|
||||||
return AppType.System
|
|
||||||
} else if (app.state?.our_version && !app.state?.caps_approved) {
|
|
||||||
return AppType.Downloaded
|
|
||||||
} else if (!app.metadata) {
|
|
||||||
return AppType.Local
|
|
||||||
} else {
|
|
||||||
return AppType.Installed
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
import { SEPOLIA_OPT_HEX, OPTIMISM_OPT_HEX } from "../constants/chain";
|
|
||||||
const CHAIN_NOT_FOUND = "4902"
|
|
||||||
|
|
||||||
export interface Chain {
|
|
||||||
chainId: string, // Replace with the correct chainId for Sepolia
|
|
||||||
chainName: string,
|
|
||||||
nativeCurrency: {
|
|
||||||
name: string,
|
|
||||||
symbol: string,
|
|
||||||
decimals: number
|
|
||||||
},
|
|
||||||
rpcUrls: string[],
|
|
||||||
blockExplorerUrls: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHAIN_DETAILS: { [key: string]: Chain } = {
|
|
||||||
[SEPOLIA_OPT_HEX]: {
|
|
||||||
chainId: SEPOLIA_OPT_HEX,
|
|
||||||
chainName: 'Sepolia',
|
|
||||||
nativeCurrency: {
|
|
||||||
name: 'Ether',
|
|
||||||
symbol: 'ETH',
|
|
||||||
decimals: 18
|
|
||||||
},
|
|
||||||
rpcUrls: ['https://rpc.sepolia.org'],
|
|
||||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
|
||||||
},
|
|
||||||
[OPTIMISM_OPT_HEX]: {
|
|
||||||
chainId: OPTIMISM_OPT_HEX,
|
|
||||||
chainName: 'Optimism',
|
|
||||||
nativeCurrency: {
|
|
||||||
name: 'Ether',
|
|
||||||
symbol: 'ETH',
|
|
||||||
decimals: 18
|
|
||||||
},
|
|
||||||
rpcUrls: ['https://mainnet.optimism.io'],
|
|
||||||
blockExplorerUrls: ['https://optimistic.etherscan.io']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getNetworkName = (networkId: string) => {
|
|
||||||
switch (networkId) {
|
|
||||||
case '1':
|
|
||||||
case '0x1':
|
|
||||||
return 'Ethereum'; // Ethereum Mainnet
|
|
||||||
case '10':
|
|
||||||
case 'a':
|
|
||||||
case '0xa':
|
|
||||||
return 'Optimism'; // Optimism
|
|
||||||
case '42161':
|
|
||||||
return 'Arbitrum'; // Arbitrum One
|
|
||||||
case '11155111':
|
|
||||||
case 'aa36a7':
|
|
||||||
case '0xaa36a7':
|
|
||||||
return 'Sepolia'; // Sepolia Testnet
|
|
||||||
default:
|
|
||||||
return 'Unknown';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setChain = async (chainId: string) => {
|
|
||||||
let networkId = await (window.ethereum as any)?.request({ method: 'net_version' }).catch(() => '1') // eslint-disable-line
|
|
||||||
networkId = '0x' + (typeof networkId === 'string' ? networkId.replace(/^0x/, '') : networkId.toString(16))
|
|
||||||
|
|
||||||
if (!CHAIN_DETAILS[chainId]) {
|
|
||||||
console.error(`Invalid chain ID: ${chainId}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chainId !== networkId) {
|
|
||||||
try {
|
|
||||||
await (window.ethereum as any)?.request({ // eslint-disable-line
|
|
||||||
method: "wallet_switchEthereumChain",
|
|
||||||
params: [{ chainId }]
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (String(err).includes(CHAIN_NOT_FOUND)) {
|
|
||||||
await (window.ethereum as any)?.request({ // eslint-disable-line
|
|
||||||
method: 'wallet_addEthereumChain',
|
|
||||||
params: [CHAIN_DETAILS[chainId]]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
window.alert(`You must enable the ${getNetworkName(chainId)} network in your wallet.`)
|
|
||||||
throw new Error(`User cancelled connection to ${chainId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
12
kinode/packages/app_store/ui/src/utils/compareVersions.ts
Normal file
12
kinode/packages/app_store/ui/src/utils/compareVersions.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Helper function to compare version strings
|
||||||
|
export const compareVersions = (v1: string, v2: string) => {
|
||||||
|
const parts1 = v1.split('.').map(Number);
|
||||||
|
const parts2 = v2.split('.').map(Number);
|
||||||
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||||
|
const part1 = parts1[i] || 0;
|
||||||
|
const part2 = parts2[i] || 0;
|
||||||
|
if (part1 > part2) return 1;
|
||||||
|
if (part1 < part2) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
@ -1 +0,0 @@
|
|||||||
export const isMobileCheck = () => window.innerWidth <= 600
|
|
21
kinode/packages/app_store/ui/src/utils/kinohash.ts
Normal file
21
kinode/packages/app_store/ui/src/utils/kinohash.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import sha3 from 'js-sha3';
|
||||||
|
import { toUnicode } from 'idna-uts46-hx';
|
||||||
|
|
||||||
|
export const kinohash = (inputName: string): `0x${string}` =>
|
||||||
|
('0x' + normalize(inputName)
|
||||||
|
.split('.')
|
||||||
|
.reverse()
|
||||||
|
.reduce(reducer, '00'.repeat(32))) as `0x${string}`;
|
||||||
|
|
||||||
|
const reducer = (node: string, label: string): string =>
|
||||||
|
sha3.keccak_256(Buffer.from(node + sha3.keccak_256(label), 'hex'));
|
||||||
|
|
||||||
|
export const normalize = (name: string): string => {
|
||||||
|
const tilde = name.startsWith('~');
|
||||||
|
const clean = tilde ? name.slice(1) : name;
|
||||||
|
const normalized = clean ? unicode(clean) : clean;
|
||||||
|
return tilde ? '~' + normalized : normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unicode = (name: string): string =>
|
||||||
|
toUnicode(name, { useStd3ASCII: true, transitional: false })
|
@ -1,4 +0,0 @@
|
|||||||
import { initializeConnector } from '@web3-react/core'
|
|
||||||
import { MetaMask } from '@web3-react/metamask'
|
|
||||||
|
|
||||||
export const [metaMask, hooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions }))
|
|
11
kinode/packages/app_store/ui/src/utils/ws.ts
Normal file
11
kinode/packages/app_store/ui/src/utils/ws.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// TODO: remove as much as possible of this..
|
||||||
|
const BASE_URL = "/main:app_store:sys/";
|
||||||
|
|
||||||
|
if (window.our) window.our.process = BASE_URL?.replace("/", "");
|
||||||
|
|
||||||
|
export const PROXY_TARGET = `${(import.meta.env.VITE_NODE_URL || `http://localhost:8080`)}${BASE_URL}`;
|
||||||
|
|
||||||
|
// This env also has BASE_URL which should match the process + package name
|
||||||
|
export const WEBSOCKET_URL = import.meta.env.DEV
|
||||||
|
? `${PROXY_TARGET.replace('http', 'ws')}`
|
||||||
|
: undefined;
|
@ -1,17 +1,13 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import UnoCSS from 'unocss/vite'
|
|
||||||
import { transformerDirectives } from 'unocss'
|
|
||||||
import presetIcons from '@unocss/preset-icons'
|
|
||||||
import presetUno from '@unocss/preset-uno'
|
|
||||||
import presetWind from '@unocss/preset-wind'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
If you are developing a UI outside of a Kinode project,
|
If you are developing a UI outside of a Kinode project,
|
||||||
comment out the following 2 lines:
|
comment out the following 2 lines:
|
||||||
*/
|
*/
|
||||||
// import manifest from '../pkg/manifest.json'
|
import manifest from '../pkg/manifest.json'
|
||||||
// import metadata from '../pkg/metadata.json'
|
import metadata from '../metadata.json'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
@ -27,35 +23,10 @@ console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL);
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
UnoCSS({
|
nodePolyfills({
|
||||||
presets: [presetUno(), presetWind(), presetIcons()],
|
globals: {
|
||||||
shortcuts: [
|
Buffer: true,
|
||||||
{
|
}
|
||||||
'flex-center': 'flex justify-center items-center',
|
|
||||||
'flex-col-center': 'flex flex-col justify-center items-center',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
rules: [
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
colors: {
|
|
||||||
'white': '#FFF5D9',
|
|
||||||
'black': '#22211F',
|
|
||||||
'orange': '#F35422',
|
|
||||||
'transparent': 'transparent',
|
|
||||||
'gray': '#7E7E7E',
|
|
||||||
},
|
|
||||||
font: {
|
|
||||||
'sans': ['Barlow', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
|
||||||
'serif': ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
|
|
||||||
'mono': ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
|
|
||||||
'heading': ['OpenSans', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
|
||||||
'display': ['Futura', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transformers: [
|
|
||||||
transformerDirectives()
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
react(),
|
react(),
|
||||||
],
|
],
|
||||||
@ -68,37 +39,30 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/our': {
|
[`^${BASE_URL}/our.js`]: {
|
||||||
target: PROXY_URL,
|
target: PROXY_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => {
|
||||||
|
console.log('Proxying jsrequest:', path);
|
||||||
|
return '/our.js';
|
||||||
},
|
},
|
||||||
[`${BASE_URL}/our.js`]: {
|
},
|
||||||
|
[`^${BASE_URL}/kinode.css`]: {
|
||||||
target: PROXY_URL,
|
target: PROXY_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(BASE_URL, ''),
|
rewrite: (path) => {
|
||||||
|
console.log('Proxying csrequest:', path);
|
||||||
|
return '/kinode.css';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// This route will match all other HTTP requests to the backend
|
// This route will match all other HTTP requests to the backend
|
||||||
[`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|__uno.css|$))`]: {
|
[`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: {
|
||||||
target: PROXY_URL,
|
target: PROXY_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
// '/example': {
|
|
||||||
// target: PROXY_URL,
|
},
|
||||||
// changeOrigin: true,
|
|
||||||
// rewrite: (path) => path.replace(BASE_URL, ''),
|
|
||||||
// // This is only for debugging purposes
|
},
|
||||||
// configure: (proxy, _options) => {
|
|
||||||
// proxy.on('error', (err, _req, _res) => {
|
|
||||||
// console.log('proxy error', err);
|
|
||||||
// });
|
|
||||||
// proxy.on('proxyReq', (proxyReq, req, _res) => {
|
|
||||||
// console.log('Sending Request to the Target:', req.method, req.url);
|
|
||||||
// });
|
|
||||||
// proxy.on('proxyRes', (proxyRes, req, _res) => {
|
|
||||||
// console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ simulation-mode = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.8.0" }
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
wit-bindgen = "0.24.0"
|
wit-bindgen = "0.24.0"
|
||||||
|
5
kinode/packages/chess/chess/Cargo.lock
generated
5
kinode/packages/chess/chess/Cargo.lock
generated
@ -222,13 +222,8 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kinode_process_lib"
|
name = "kinode_process_lib"
|
||||||
<<<<<<< HEAD:modules/chess/chess/Cargo.lock
|
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
source = "git+https://github.com/kinode-dao/process_lib?tag=v0.5.9-alpha#c1ac7227951fbd8cabf6568704f0ce11e8558c8a"
|
source = "git+https://github.com/kinode-dao/process_lib?tag=v0.5.9-alpha#c1ac7227951fbd8cabf6568704f0ce11e8558c8a"
|
||||||
=======
|
|
||||||
version = "0.5.6"
|
|
||||||
source = "git+https://github.com/kinode-dao/process_lib?rev=fccb6a0#fccb6a0c07ebda3e385bff7f76e4984b741f01c7"
|
|
||||||
>>>>>>> develop:kinode/packages/chess/chess/Cargo.lock
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
@ -8,13 +8,11 @@ simulation-mode = []
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
base64 = "0.22.0"
|
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.8.0" }
|
kinode_process_lib = { git = "https://github.com/kinode-dao/process_lib", tag = "v0.9.0" }
|
||||||
pleco = "0.5"
|
pleco = "0.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
url = "*"
|
|
||||||
wit-bindgen = "0.24.0"
|
wit-bindgen = "0.24.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user