Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 139 additions & 201 deletions kinode/packages/app_store/app_store/src/http_api.rs

Large diffs are not rendered by default.

47 changes: 40 additions & 7 deletions kinode/packages/app_store/app_store/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![feature(let_chains)]
//! App Store:
//! acts as both a local package manager and a protocol to share packages across the network.
//! packages are apps; apps are packages. we use an onchain app listing contract to determine
//! packages are apps; apps are packages. we use the kimap contract to determine
//! 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.
Expand All @@ -22,8 +22,9 @@ use ft_worker_lib::{
spawn_receive_transfer, spawn_transfer, FTWorkerCommand, FTWorkerResult, FileTransferContext,
};
use kinode_process_lib::{
await_message, call_init, eth, get_blob, http, kimap, println, vfs, Address, LazyLoadBlob,
Message, PackageId, Request, Response,
await_message, call_init, eth, get_blob,
http::{self, WsMessageType},
kimap, println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response,
};
use serde::{Deserialize, Serialize};
use state::{AppStoreLogError, PackageState, RequestedPackage, State};
Expand Down Expand Up @@ -53,7 +54,7 @@ pub const APP_SHARE_TIMEOUT: u64 = 120; // 120s
#[cfg(not(feature = "simulation-mode"))]
const KIMAP_ADDRESS: &str = kimap::KIMAP_ADDRESS;
#[cfg(feature = "simulation-mode")]
const KIMAP_ADDRESS: &str = "0x0165878A594ca255338adfa4d48449f69242Eb8F"; // note temp kimap address!
const KIMAP_ADDRESS: &str = "0x0165878A594ca255338adfa4d48449f69242Eb8F";

#[cfg(not(feature = "simulation-mode"))]
const KIMAP_FIRST_BLOCK: u64 = kimap::KIMAP_FIRST_BLOCK;
Expand Down Expand Up @@ -135,11 +136,35 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
let resp = handle_remote_request(state, message.source(), remote_request);
Response::new().body(serde_json::to_vec(&resp)?).send()?;
}
Req::FTWorkerCommand(_) => {
spawn_receive_transfer(&state.our, message.body())?;
}
Req::FTWorkerResult(FTWorkerResult::ReceiveSuccess(name)) => {
handle_receive_download(state, &name)?;
}
Req::FTWorkerCommand(_) => {
spawn_receive_transfer(&state.our, message.body())?;
Req::FTWorkerResult(FTWorkerResult::ProgressUpdate {
file_name,
chunks_received,
total_chunks,
}) => {
// forward progress to UI
let ws_blob = LazyLoadBlob {
mime: Some("application/json".to_string()),
bytes: serde_json::json!({
"kind": "progress",
"data": {
"file_name": file_name,
"chunks_received": chunks_received,
"total_chunks": total_chunks,
}
})
.to_string()
.as_bytes()
.to_vec(),
};
for channel_id in state.ui_ws_channels.iter() {
http::send_ws_push(*channel_id, WsMessageType::Text, ws_blob.clone());
}
}
Req::FTWorkerResult(r) => {
println!("got weird ft_worker result: {r:?}");
Expand Down Expand Up @@ -169,6 +194,10 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
}
if let http::HttpServerRequest::Http(req) = incoming {
http_api::handle_http_request(state, &req)?;
} else if let http::HttpServerRequest::WebSocketOpen { channel_id, .. } = incoming {
state.ui_ws_channels.insert(channel_id);
} else if let http::HttpServerRequest::WebSocketClose { 0: channel_id } = incoming {
state.ui_ws_channels.remove(&channel_id);
}
}
}
Expand Down Expand Up @@ -566,8 +595,12 @@ fn handle_ft_worker_result(ft_worker_result: FTWorkerResult, context: &[u8]) ->
.as_secs_f64(),
);
Ok(())
} else if let FTWorkerResult::Err(e) = ft_worker_result {
Err(anyhow::anyhow!("failed to share app: {e:?}"))
} else {
Err(anyhow::anyhow!("failed to share app"))
Err(anyhow::anyhow!(
"failed to share app: unknown FTWorkerResult {ft_worker_result:?}"
))
}
}

Expand Down
24 changes: 20 additions & 4 deletions kinode/packages/app_store/app_store/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub struct MirroringFile {
pub auto_update: bool,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct MirrorCheckFile {
pub node: NodeId,
pub is_online: bool,
pub error: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct RequestedPackage {
pub from: NodeId,
Expand Down Expand Up @@ -109,6 +116,8 @@ pub struct State {
pub requested_packages: HashMap<PackageId, RequestedPackage>,
/// the APIs we have outstanding requests to download (not persisted)
pub requested_apis: HashMap<PackageId, RequestedPackage>,
/// UI websocket connected channel_IDs
pub ui_ws_channels: HashSet<u32>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -144,6 +153,7 @@ impl State {
downloaded_apis: s.downloaded_apis,
requested_packages: HashMap::new(),
requested_apis: HashMap::new(),
ui_ws_channels: HashSet::new(),
}
}

Expand All @@ -158,6 +168,7 @@ impl State {
downloaded_apis: HashSet::new(),
requested_packages: HashMap::new(),
requested_apis: HashMap::new(),
ui_ws_channels: HashSet::new(),
};
state.populate_packages_from_filesystem()?;
Ok(state)
Expand Down Expand Up @@ -202,8 +213,10 @@ impl State {
mirroring: package_state.mirroring,
auto_update: package_state.auto_update,
})?)?;
if utils::extract_api(package_id)? {
self.downloaded_apis.insert(package_id.to_owned());
if let Ok(extracted) = utils::extract_api(package_id) {
if extracted {
self.downloaded_apis.insert(package_id.to_owned());
}
}
listing.state = Some(package_state);
// kinode_process_lib::set_state(&serde_json::to_vec(self)?);
Expand Down Expand Up @@ -345,7 +358,10 @@ impl State {

pub fn uninstall(&mut self, package_id: &PackageId) -> anyhow::Result<()> {
utils::uninstall(package_id)?;
self.packages.remove(package_id);
let Some(listing) = self.packages.get_mut(package_id) else {
return Err(anyhow::anyhow!("package not found"));
};
listing.state = None;
// kinode_process_lib::set_state(&serde_json::to_vec(self)?);
println!("uninstalled {package_id}");
Ok(())
Expand All @@ -364,7 +380,7 @@ impl State {
let block_number: u64 = log.block_number.ok_or(AppStoreLogError::NoBlockNumber)?;

let note: kimap::Note =
kimap::decode_note_log(&log).map_err(AppStoreLogError::DecodeLogError)?;
kimap::decode_note_log(&log).map_err(|_| AppStoreLogError::DecodeLogError)?;

let package_id = note
.parent_path
Expand Down
5 changes: 5 additions & 0 deletions kinode/packages/app_store/ft_worker/src/ft_worker_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ pub enum FTWorkerResult {
SendSuccess,
/// string is name of file. bytes in blob
ReceiveSuccess(String),
ProgressUpdate {
file_name: String,
chunks_received: u64,
total_chunks: u64,
},
Err(TransferError),
}

Expand Down
14 changes: 13 additions & 1 deletion kinode/packages/app_store/ft_worker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fn handle_send(our: &Address, target: &Address, file_name: &str, timeout: u64) -
let file_bytes = blob.bytes;
let mut file_size = file_bytes.len() as u64;
let mut offset: u64 = 0;
let chunk_size: u64 = 1048576; // 1MB, can be changed
let chunk_size: u64 = 262144; // 256KB
let total_chunks = (file_size as f64 / chunk_size as f64).ceil() as u64;
// send a file to another worker
// start by telling target to expect a file,
Expand Down Expand Up @@ -155,6 +155,18 @@ fn handle_receive(
};
chunks_received += 1;
file_bytes.extend(blob.bytes);
// send progress update to parent
Request::to(parent_process.clone())
.body(
serde_json::to_vec(&FTWorkerResult::ProgressUpdate {
file_name: file_name.to_string(),
chunks_received,
total_chunks,
})
.unwrap(),
)
.send()
.unwrap();
if chunks_received == total_chunks {
break;
}
Expand Down
3 changes: 2 additions & 1 deletion kinode/packages/app_store/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom";

import StorePage from "./pages/StorePage";
import AppPage from "./pages/AppPage";
import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
import PublishPage from "./pages/PublishPage";
import Header from "./components/Header";
import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";


const BASE_URL = import.meta.env.BASE_URL;
if (window.our) window.our.process = BASE_URL?.replace("/", "");
Expand Down
14 changes: 7 additions & 7 deletions kinode/packages/app_store/ui/src/abis/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ import { multicallAbi, kinomapAbi, mechAbi, KINOMAP, MULTICALL, KINO_ACCOUNT_IMP
import { encodeFunctionData, encodePacked, stringToHex } from "viem";

export function encodeMulticalls(metadataUri: string, metadataHash: string) {
const metadataUriCall = encodeFunctionData({
const metadataHashCall = encodeFunctionData({
abi: kinomapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~metadata-uri")]),
encodePacked(["bytes"], [stringToHex(metadataUri)]),
encodePacked(["bytes"], [stringToHex("~metadata-hash")]),
encodePacked(["bytes"], [stringToHex(metadataHash)]),
]
})

const metadataHashCall = encodeFunctionData({
const metadataUriCall = encodeFunctionData({
abi: kinomapAbi,
functionName: 'note',
args: [
encodePacked(["bytes"], [stringToHex("~metadata-hash")]),
encodePacked(["bytes"], [stringToHex(metadataHash)]),
encodePacked(["bytes"], [stringToHex("~metadata-uri")]),
encodePacked(["bytes"], [stringToHex(metadataUri)]),
]
})

const calls = [
{ target: KINOMAP, callData: metadataHashCall },
{ target: KINOMAP, callData: metadataUriCall },
{ target: KINOMAP, callData: metadataHashCall }
];

const multicall = encodeFunctionData({
Expand Down
10 changes: 6 additions & 4 deletions kinode/packages/app_store/ui/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { STORE_PATH, PUBLISH_PATH } from '../constants/path';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { FaHome } from "react-icons/fa";

const Header: React.FC = () => {
const location = useLocation();

return (
<header className="app-header">
<div className="header-left">
<nav>
<Link to={STORE_PATH} className={location.pathname === STORE_PATH ? 'active' : ''}>Home</Link>
<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>
</nav>
</div>
Expand Down
Loading