Merge pull request #285 from kinode-dao/tm/unify
Move homepage & appstore UIs to main repo
1
.gitignore
vendored
@ -12,6 +12,7 @@ wit/
|
||||
/home
|
||||
packages/**/pkg/*.wasm
|
||||
packages/**/wit
|
||||
*/**/node_modules
|
||||
.env
|
||||
kinode/src/bootstrapped_processes.rs
|
||||
kinode/packages/**/wasi_snapshot_preview1.wasm
|
||||
|
@ -29,7 +29,7 @@ fn build_and_zip_package(
|
||||
) -> anyhow::Result<(String, String, Vec<u8>)> {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
kit::build::execute(&entry_path, false, false, true, features).await?;
|
||||
kit::build::execute(&entry_path, true, false, true, features).await?;
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
let options = FileOptions::default()
|
||||
|
@ -15,8 +15,8 @@
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
|
||||
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-CPkF34RS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-jnOcECnM.css">
|
||||
<script type="module" crossorigin src="/main:app_store:sys/assets/index-YeOEFbyC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-JESB3UJK.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
18
kinode/packages/app_store/ui/.eslintrc.cjs
Normal file
@ -0,0 +1,18 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}
|
24
kinode/packages/app_store/ui/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
65
kinode/packages/app_store/ui/README.md
Normal file
@ -0,0 +1,65 @@
|
||||
# App Store UI
|
||||
|
||||
This UI template uses the React framework compiled with Vite.
|
||||
It is based on the Vite React Typescript template.
|
||||
|
||||
## Setup
|
||||
|
||||
There are 2 ways to set up this repo:
|
||||
1. Place this repo next to the `pkg` repo of your Kinode project (usually the top level).
|
||||
2. Set `BASE_URL` in `vite.config.ts` to the URL of your Kinode project (i.e. `/chess:chess:sys`) and comment out lines 8 and 9.
|
||||
|
||||
## Development
|
||||
|
||||
Run `npm i`, `npm run tc`, and then `npm start` to start working on the UI.
|
||||
By default, the dev server will proxy requests to `http://localhost:8080`.
|
||||
You can change the proxy target by setting `VITE_NODE_URL` like so:
|
||||
`VITE_NODE_URL=http://localhost:8081 npm start`
|
||||
|
||||
You may see an error:
|
||||
|
||||
```
|
||||
[vite] Pre-transform error: Failed to load url /our.js (resolved id: /our.js). Does the file exist?
|
||||
```
|
||||
|
||||
You can safely ignore this error. The file will be served by the node via the proxy.
|
||||
|
||||
#### public vs assets
|
||||
|
||||
The `public/assets` folder contains files that are referenced in `index.html`, `src/assets` is for asset files that are only referenced in `src` code.
|
||||
|
||||
## Building
|
||||
|
||||
Run `npm run build`, the build will be generated in the `dist` directory.
|
||||
If this repo is next to your Kinode `pkg` directory then you can `npm run build:copy` to build and copy it for installation.
|
||||
|
||||
## About Vite + React
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
22
kinode/packages/app_store/ui/chat_metadata.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
25
kinode/packages/app_store/ui/index.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!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" />
|
||||
<link href='https://fonts.googleapis.com/css?family=Montserrat' rel='stylesheet'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
9718
kinode/packages/app_store/ui/package-lock.json
generated
Normal file
54
kinode/packages/app_store/ui/package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "kit-ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"start": "vite --port 3000",
|
||||
"build": "tsc && vite build",
|
||||
"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",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"tc": "typechain --target ethers-v5 --out-dir src/abis/types/ \"./src/abis/**/*.json\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@ethersproject/hash": "^5.7.0",
|
||||
"@kinode/client-api": "^0.1.0",
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"@web3-react/coinbase-wallet": "^8.2.3",
|
||||
"@web3-react/core": "^8.2.2",
|
||||
"@web3-react/gnosis-safe": "^8.2.4",
|
||||
"@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",
|
||||
"ethers": "^5.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-router-dom": "^6.21.3",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typechain/ethers-v5": "^11.1.1",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"typechain": "^8.3.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
1
kinode/packages/app_store/ui/public/assets/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
381
kinode/packages/app_store/ui/src/App.css
Normal file
@ -0,0 +1,381 @@
|
||||
#root {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
width: 75%;
|
||||
max-height: calc(100vh - 64px);
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.row.around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.col.center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--input-background);
|
||||
border-radius: 0.75em;
|
||||
border: 1px solid var(--orange-medium);
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
button.action-btn {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
button.small {
|
||||
padding: 0.25em 0.5em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Specific */
|
||||
|
||||
.searchbar {
|
||||
height: 2.25em;
|
||||
padding: .5em 1em;
|
||||
border-radius: 16px;
|
||||
flex: 1;
|
||||
background-color: var(--input-background);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.searchbar>input {
|
||||
border: none;
|
||||
height: 1.5em;
|
||||
margin-left: 0.5em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button.connect-wallet {
|
||||
margin: 1em auto 0;
|
||||
}
|
||||
|
||||
.my-pkg-btn {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.my-pkg-btn.selected {
|
||||
background-color: var(--bg-gray-medium);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
cursor: pointer;
|
||||
width: calc(100% - 10.3em);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.app-header:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-header.large:hover {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.app-header.small>img {
|
||||
height: 3em;
|
||||
margin-right: 1em;
|
||||
border-radius: 0.375em;
|
||||
}
|
||||
|
||||
.app-header>img {
|
||||
height: 3em;
|
||||
margin-right: 1em;
|
||||
border-radius: 0.375em;
|
||||
}
|
||||
|
||||
.app-header.large>img {
|
||||
height: 5em;
|
||||
margin-right: 1em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.app-header.large .app-name {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.app-entry {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown>ul {
|
||||
background-color: var(--orange-medium);
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 0.5em;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
border: 1px solid var(--orange-medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dropdown .dropdown-header {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.dropdown .dropdown-list {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: -0.5em;
|
||||
}
|
||||
|
||||
.page-selector {
|
||||
margin: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
.page-selector.selected {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-right: 1em;
|
||||
justify-content: center;
|
||||
width: 2.5em;
|
||||
}
|
||||
|
||||
.app-details {
|
||||
margin-top: 0.5em;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.app-details .title {
|
||||
width: 8em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.app-details .value {
|
||||
margin-bottom: 0.5em;
|
||||
text-align: left;
|
||||
max-width: calc(100% - 8em);
|
||||
}
|
||||
|
||||
.app-details .value.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.app-details .value.permission {
|
||||
background-color: var(--bg-gray-medium);
|
||||
border-radius: 2em;
|
||||
padding: 0.25em 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.app-screenshots {
|
||||
margin-top: 0.5em;
|
||||
overflow-x: scroll;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-screenshots>img {
|
||||
margin-right: 1em;
|
||||
max-height: 10em;
|
||||
max-width: 100%;
|
||||
border-radius: 0.5em;
|
||||
border: 1px solid var(--bg-gray-medium);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: var(--bg-gray-solid);
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.f-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#loading h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
#loader div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 6px;
|
||||
border: 6px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
|
||||
#loader div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
|
||||
#loader div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
|
||||
#loader div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.action-entry {
|
||||
margin-bottom: 0.25em;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.action-entry:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.action-entry:first-child {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.my-apps-list {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
max-height: calc(100vh - 10em);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: calc(100% - 6em);
|
||||
}
|
||||
|
||||
.title>div {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
min-height: 10em;
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.modal-backdrop .close {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
cursor: pointer;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
background-color: var(--dark-background);
|
||||
color: black;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
line-height: 24px;
|
||||
max-width: 500px;
|
||||
min-width: 300px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.modal .modal-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.modal .modal-content {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
form.new {
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
form.metadata {
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form.metadata input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.metadata .row {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
form.metadata .col.label {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
align-items: center;
|
||||
margin: 1em 0;
|
||||
}
|
125
kinode/packages/app_store/ui/src/App.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import { ethers } from "ethers";
|
||||
import { Web3ReactProvider, Web3ReactHooks } from '@web3-react/core';
|
||||
import type { MetaMask } from '@web3-react/metamask'
|
||||
|
||||
import { PackageStore, PackageStore__factory } from "./abis/types";
|
||||
import StorePage from "./pages/StorePage";
|
||||
import MyAppsPage from "./pages/MyAppsPage";
|
||||
import AppPage from "./pages/AppPage";
|
||||
import { MY_APPS_PATH } from "./constants/path";
|
||||
import { ChainId, PACKAGE_STORE_ADDRESSES } from "./constants/chain";
|
||||
import PublishPage from "./pages/PublishPage";
|
||||
import { hooks as metaMaskHooks, metaMask } from './utils/metamask'
|
||||
import "./App.css";
|
||||
|
||||
const connectors: [MetaMask, Web3ReactHooks][] = [
|
||||
[metaMask, metaMaskHooks],
|
||||
]
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
env: {
|
||||
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_SEPOLIA_RPC_URL;
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
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() {
|
||||
const provider = useProvider();
|
||||
const [nodeConnected, setNodeConnected] = useState(true); // eslint-disable-line
|
||||
|
||||
const [packageAbi, setPackageAbi] = useState<PackageStore>(
|
||||
PackageStore__factory.connect(
|
||||
PACKAGE_STORE_ADDRESSES[ChainId.SEPOLIA],
|
||||
new ethers.providers.JsonRpcProvider(RPC_URL)) // TODO: get the RPC URL from the wallet
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
provider?.getNetwork().then(network => {
|
||||
if (network.chainId === ChainId.SEPOLIA) {
|
||||
setPackageAbi(PackageStore__factory.connect(
|
||||
PACKAGE_STORE_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner())
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [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="node-not-connected">
|
||||
<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 (
|
||||
<Web3ReactProvider connectors={connectors}>
|
||||
<Router basename={BASE_URL}>
|
||||
<Routes>
|
||||
<Route path="/" element={<StorePage {...props} />} />
|
||||
<Route path={MY_APPS_PATH} element={<MyAppsPage {...props} />} />
|
||||
<Route path="/app-details/:id" element={<AppPage {...props} />} />
|
||||
<Route path="/publish" element={<PublishPage {...props} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Web3ReactProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
978
kinode/packages/app_store/ui/src/abis/PackageStore.json
Normal file
@ -0,0 +1,978 @@
|
||||
[
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
1270
kinode/packages/app_store/ui/src/abis/types/PackageStore.ts
Normal file
44
kinode/packages/app_store/ui/src/abis/types/common.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/* 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;
|
@ -0,0 +1,999 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/* Autogenerated file. Do not edit manually. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { PackageStore__factory } from "./PackageStore__factory";
|
6
kinode/packages/app_store/ui/src/abis/types/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/* 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";
|
BIN
kinode/packages/app_store/ui/src/assets/background.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 431 B |
1
kinode/packages/app_store/ui/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,3 @@
|
||||
<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>
|
After Width: | Height: | Size: 188 B |
1
kinode/packages/app_store/ui/src/assets/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
221
kinode/packages/app_store/ui/src/components/ActionButton.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
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";
|
||||
|
||||
interface ActionButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
export default function ActionButton({ app, ...props }: ActionButtonProps) {
|
||||
const { updateApp, downloadApp, installApp, getCaps, getMyApp } =
|
||||
useAppsStore();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [mirror, setMirror] = useState(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
const [customMirror, setCustomMirror] = useState("");
|
||||
const [caps, setCaps] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState("");
|
||||
|
||||
const { clean, 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.our.node;
|
||||
return {
|
||||
clean: !installed && !downloaded && !updatable,
|
||||
installed,
|
||||
downloaded,
|
||||
updatable,
|
||||
};
|
||||
}, [app]);
|
||||
|
||||
useEffect(() => {
|
||||
setMirror(app.metadata?.properties?.mirrors?.[0] || "Other");
|
||||
}, [app.metadata?.properties?.mirrors]);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (installed && !updatable) {
|
||||
window.alert("App is installed");
|
||||
} else {
|
||||
if (downloaded) {
|
||||
getCaps(app).then((manifest) => {
|
||||
setCaps(manifest.request_capabilities);
|
||||
});
|
||||
}
|
||||
setShowModal(true);
|
||||
}
|
||||
}, [app, installed, downloaded, updatable, 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 {
|
||||
setLoading(`Downloading ${getAppName(app)}...`);
|
||||
await downloadApp(app, targetMirror);
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then(() => {
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(
|
||||
`Failed to download app from ${targetMirror}, please try a different mirror.`
|
||||
);
|
||||
setLoading("");
|
||||
}
|
||||
}, [mirror, customMirror, app, downloadApp, getMyApp]);
|
||||
|
||||
const install = useCallback(async () => {
|
||||
try {
|
||||
setLoading(`Installing ${getAppName(app)}...`);
|
||||
await installApp(app);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
getMyApp(app)
|
||||
.then((app) => {
|
||||
if (!app.installed) return;
|
||||
setLoading("");
|
||||
setShowModal(false);
|
||||
clearInterval(interval);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to install, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, installApp, getMyApp]);
|
||||
|
||||
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);
|
||||
})
|
||||
.catch(console.log);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
window.alert(`Failed to update, please try again.`);
|
||||
setLoading("");
|
||||
}
|
||||
}, [app, updateApp, getMyApp]);
|
||||
|
||||
const appName = getAppName(app);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={`small action-btn ${props.className || ""}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{installed && updatable
|
||||
? "Update"
|
||||
: installed
|
||||
? "Installed"
|
||||
: downloaded
|
||||
? "Install"
|
||||
: "Download"}
|
||||
</button>
|
||||
<Modal show={showModal} hide={() => setShowModal(false)}>
|
||||
{loading ? (
|
||||
<Loader msg={loading} />
|
||||
) : clean ? (
|
||||
<form className="col" style={{alignItems: "center", gap: "1em"}} onSubmit={download}>
|
||||
<h4>Download '{appName}'</h4>
|
||||
<h5 style={{ margin: 0 }}>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'"
|
||||
style={{ padding: "0.5em", maxWidth: 240, width: "100%" }}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<button type="submit">
|
||||
Download
|
||||
</button>
|
||||
</form>
|
||||
) : downloaded ? (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 style={{ margin: 0 }}>
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
<ul className="col" style={{ alignItems: "flex-start" }}>
|
||||
{caps.map((cap) => (
|
||||
<li key={cap}>{cap}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button type="button" onClick={install}>
|
||||
Approve & Install
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h4>Approve App Permissions</h4>
|
||||
<h5 style={{ margin: 0 }}>
|
||||
{getAppName(app)} needs the following permissions:
|
||||
</h5>
|
||||
{/* <h5>Send Messages:</h5> */}
|
||||
<br />
|
||||
<ul className="col" style={{ alignItems: "flex-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>
|
||||
</>
|
||||
);
|
||||
}
|
23
kinode/packages/app_store/ui/src/components/AppEntry.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import AppHeader from "./AppHeader";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import MoreActions from "./MoreActions";
|
||||
|
||||
interface AppEntryProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
export default function AppEntry({ app, ...props }: AppEntryProps) {
|
||||
return (
|
||||
<div {...props} key={appId(app)} className="app-entry row between">
|
||||
<AppHeader app={app} size="small" />
|
||||
<div className="app-actions row">
|
||||
{!app.state?.caps_approved && <ActionButton app={app} style={{ marginRight: "1em" }} />}
|
||||
<MoreActions app={app} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
39
kinode/packages/app_store/ui/src/components/AppHeader.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface AppHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
app: AppInfo;
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export default function AppHeader({
|
||||
app,
|
||||
size = "medium",
|
||||
...props
|
||||
}: AppHeaderProps) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={`app-header row ${size} ${props.className || ""}`}
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
app.metadata?.image ||
|
||||
"https://png.pngtree.com/png-vector/20190215/ourmid/pngtree-vector-question-mark-icon-png-image_515448.jpg"
|
||||
}
|
||||
alt="app icon"
|
||||
/>
|
||||
<div className="col title">
|
||||
<div className="app-name ellipsis">{app.metadata?.name || appId(app)}</div>
|
||||
{app.metadata?.description && size !== "large" && (
|
||||
<div className="ellipsis">{app.metadata?.description?.slice(0, 100)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
30
kinode/packages/app_store/ui/src/components/Checkbox.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Checkbox({
|
||||
readOnly = false,
|
||||
checked,
|
||||
setChecked,
|
||||
}: {
|
||||
readOnly?: boolean;
|
||||
checked: boolean;
|
||||
setChecked?: (checked: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checked"
|
||||
name="checked"
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked && setChecked(e.target.checked)}
|
||||
autoFocus
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{checked && (
|
||||
<span onClick={() => setChecked && setChecked(false)} className="checkmark">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
16
kinode/packages/app_store/ui/src/components/Dropdown.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FaEllipsisH } from 'react-icons/fa';
|
||||
import { Menu, MenuButton } from '@szhsin/react-menu';
|
||||
|
||||
interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
export default function Dropdown({ ...props }: DropdownProps) {
|
||||
return (
|
||||
<Menu {...props} className={"dropdown " + props.className} menuButton={<MenuButton className="small">
|
||||
<FaEllipsisH style={{ marginBottom: '-0.125em' }} />
|
||||
</MenuButton>}>
|
||||
{props.children}
|
||||
</Menu>
|
||||
)
|
||||
}
|
14
kinode/packages/app_store/ui/src/components/Loader.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
type LoaderProps = {
|
||||
msg: string
|
||||
}
|
||||
|
||||
export default function Loader({ msg }: LoaderProps) {
|
||||
return (
|
||||
<div id="loading" className="flex flex-col text-center">
|
||||
<h4>{msg}</h4>
|
||||
<div id="loader"> <div /> <div /> <div /> <div /> </div>
|
||||
</div>
|
||||
)
|
||||
}
|
293
kinode/packages/app_store/ui/src/components/MetadataForm.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
|
||||
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="col card metadata" style={{ gap: "0.5em" }}>
|
||||
<h4>Fill out metadata</h4>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleFieldChange("name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<label className="metadata-label">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleFieldChange("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<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="col label">
|
||||
<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="col label">
|
||||
<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="col label">
|
||||
<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="col label">
|
||||
<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="col label">
|
||||
<label className="metadata-label">Publisher</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Publisher"
|
||||
value={formData.publisher}
|
||||
onChange={(e) => handleFieldChange("publisher", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col label">
|
||||
<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="col label"
|
||||
style={{
|
||||
gap: "0.5em",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="row"
|
||||
style={{
|
||||
gap: "0.5em",
|
||||
marginTop: 0,
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h5 style={{ margin: 0 }}>Code Hashes</h5>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeHashes([...codeHashes, ["", ""]])}
|
||||
className="small"
|
||||
>
|
||||
Add code hash
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{codeHashes.map(([version, hash], ind, arr) => (
|
||||
<div
|
||||
key={ind + "_code_hash"}
|
||||
className="row"
|
||||
style={{ gap: "0.5em", marginTop: 0, width: "100%" }}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Version"
|
||||
value={version}
|
||||
onChange={(e) =>
|
||||
setCodeHashes((prev) => {
|
||||
const newHashes = [...prev];
|
||||
newHashes[ind][0] = e.target.value;
|
||||
return newHashes;
|
||||
})
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Hash"
|
||||
value={hash}
|
||||
onChange={(e) =>
|
||||
setCodeHashes((prev) => {
|
||||
const newHashes = [...prev];
|
||||
newHashes[ind][1] = e.target.value;
|
||||
return newHashes;
|
||||
})
|
||||
}
|
||||
style={{ flex: 5 }}
|
||||
/>
|
||||
{arr.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCodeHashes((prev) => prev.filter((_, i) => i !== ind))
|
||||
}
|
||||
style={{
|
||||
fontSize: "2em",
|
||||
height: 32,
|
||||
lineHeight: "1em",
|
||||
padding: "0 0.2em",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="row" style={{ gap: "0.5em", margin: "1em 0" }}>
|
||||
<button type="button" onClick={handleSubmit}>
|
||||
Download JSON
|
||||
</button>
|
||||
<button type="button" onClick={handleClearForm}>
|
||||
Clear Form
|
||||
</button>
|
||||
<button type="button" onClick={goBack}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetadataForm;
|
42
kinode/packages/app_store/ui/src/components/Modal.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { MouseEvent } from 'react'
|
||||
import { FaPlus } from 'react-icons/fa'
|
||||
|
||||
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={`modal-backdrop ${show ? 'show' : ''}`} onClick={hide}>
|
||||
<div {...props} className={`col modal ${props.className || ''}`} onClick={dontHide}>
|
||||
{Boolean(title) && <h4 className='modal-title'>{title}</h4>}
|
||||
{!hideClose && (
|
||||
<FaPlus className='close' onClick={hide} />
|
||||
)}
|
||||
<div className='col modal-content' onClick={dontHide}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
82
kinode/packages/app_store/ui/src/components/MoreActions.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MenuItem } from "@szhsin/react-menu";
|
||||
|
||||
import Dropdown from "./Dropdown";
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import { appId } from "../utils/app";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
|
||||
interface MoreActionsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
app: AppInfo;
|
||||
}
|
||||
|
||||
export default function MoreActions({ app }: MoreActionsProps) {
|
||||
const { uninstallApp, setMirroring, setAutoUpdate } = useAppsStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const downloaded = Boolean(app.state);
|
||||
|
||||
if (!downloaded) {
|
||||
if (!app.metadata) return <div style={{ width: 38 }} />;
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
{app.metadata?.description && (
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
>
|
||||
View Details
|
||||
</MenuItem>
|
||||
)}
|
||||
{app.metadata?.external_url && (
|
||||
<MenuItem>
|
||||
<a
|
||||
style={{
|
||||
color: "inherit",
|
||||
whiteSpace: "nowrap",
|
||||
cursor: "pointer",
|
||||
marginTop: "0.25em",
|
||||
}}
|
||||
target="_blank"
|
||||
href={app.metadata?.external_url}
|
||||
>
|
||||
View Site
|
||||
</a>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => navigate(`/app-details/${appId(app)}`)}
|
||||
>
|
||||
View Details
|
||||
</MenuItem>
|
||||
{app.installed && (
|
||||
<>
|
||||
<MenuItem className="action-entry" onClick={() => uninstallApp(app)}>
|
||||
Uninstall
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => setMirroring(app, !app.state?.mirroring)}
|
||||
>
|
||||
{app.state?.mirroring ? "Stop" : "Start"} Mirroring
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="action-entry"
|
||||
onClick={() => setAutoUpdate(app, !app.state?.auto_update)}
|
||||
>
|
||||
{app.state?.auto_update ? "Disable" : "Enable"} Auto Update
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
86
kinode/packages/app_store/ui/src/components/SearchHeader.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
FaArrowLeft,
|
||||
FaDownload,
|
||||
FaRegTimesCircle,
|
||||
FaSearch,
|
||||
FaUpload,
|
||||
} from "react-icons/fa";
|
||||
|
||||
import { MY_APPS_PATH } from "../constants/path";
|
||||
|
||||
interface SearchHeaderProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onBack?: () => void;
|
||||
onlyMyApps?: boolean;
|
||||
hideSearch?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchHeader({
|
||||
value = "",
|
||||
onChange = () => null,
|
||||
onBack,
|
||||
hideSearch = 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;
|
||||
|
||||
return (
|
||||
<div className="search-header row between">
|
||||
{location.pathname !== '/' ? (
|
||||
<button className="back-btn col center" onClick={() => {
|
||||
if (onBack) {
|
||||
onBack()
|
||||
} else {
|
||||
canGoBack ? navigate(-1) : navigate('/')
|
||||
}
|
||||
}}>
|
||||
<FaArrowLeft />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="back-btn col center"
|
||||
onClick={() => navigate("/publish")}
|
||||
>
|
||||
<FaUpload />
|
||||
</button>
|
||||
)}
|
||||
{!hideSearch && (
|
||||
<div className="searchbar row">
|
||||
<FaSearch
|
||||
className="search-icon"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={value}
|
||||
placeholder="Search for apps..."
|
||||
/>
|
||||
{value.length > 0 && (
|
||||
<FaRegTimesCircle
|
||||
className="search-icon"
|
||||
style={{ margin: "0 -0.25em 0 0.25em" }}
|
||||
onClick={() => onChange("")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="row">
|
||||
<button
|
||||
className={`my-pkg-btn row ${isMyAppsPage ? "selected" : ""}`}
|
||||
onClick={() => (isMyAppsPage ? navigate(-1) : navigate(MY_APPS_PATH))}
|
||||
>
|
||||
<FaDownload style={{ marginRight: "0.5em" }} />
|
||||
My Packages
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
16
kinode/packages/app_store/ui/src/constants/chain.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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';
|
||||
|
||||
// Sepolia (for now)
|
||||
export const PACKAGE_STORE_ADDRESSES = {
|
||||
[ChainId.SEPOLIA]: '0x18c39eB547A0060C6034f8bEaFB947D1C16eADF1',
|
||||
// [ChainId.OPTIMISM]: '0x8f6e1c9C5a0fE0A7f9Cf0e9b3aF1A9c4f5c6A9e0',
|
||||
};
|
22
kinode/packages/app_store/ui/src/constants/http.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export enum HTTP_STATUS {
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NO_CONTENT = 204,
|
||||
MOVED_PERMANENTLY = 301,
|
||||
FOUND = 302,
|
||||
SEE_OTHER = 303,
|
||||
NOT_MODIFIED = 304,
|
||||
TEMPORARY_REDIRECT = 307,
|
||||
PERMANENT_REDIRECT = 308,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
PAYLOAD_TOO_LARGE = 413,
|
||||
UNSUPPORTED_MEDIA_TYPE = 415,
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
BAD_GATEWAY = 502,
|
||||
SERVICE_UNAVAILABLE = 503
|
||||
}
|
1
kinode/packages/app_store/ui/src/constants/path.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MY_APPS_PATH = '/my-apps';
|
222
kinode/packages/app_store/ui/src/index.css
Normal file
@ -0,0 +1,222 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--text-light: #FFF5D9;
|
||||
--text-dark: #22211F;
|
||||
--text-orange: #FF7533;
|
||||
--orange-light: #F36822;
|
||||
--orange-medium: #F35422;
|
||||
--orange-burnt: #E25F35;
|
||||
|
||||
--medium-gray: #7E7E7E;
|
||||
--gray-button: rgba(253, 245, 220, 0.25);
|
||||
--dark-background: rgb(130, 59, 28);
|
||||
--input-background: rgba(243, 84, 34, 0.25);
|
||||
/* orange-medium */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: var(--text-light);
|
||||
font-weight: 400;
|
||||
background: url('./assets/background.jpg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
background-color: var(--dark-background);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
a,
|
||||
button,
|
||||
input {
|
||||
font-family: 'Barlow Condensed', sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="checkbox"] {
|
||||
padding: 1em;
|
||||
border: 1px solid var(--orange-medium);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 1em;
|
||||
background-color: var(--input-background);
|
||||
color: var(--text-light);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
padding: 0.25em 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--orange-medium);
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
font-size: 24px;
|
||||
top: -5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
::-moz-placeholder::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
::-ms-input-placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
padding: 0.75em 1em;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--orange-medium);
|
||||
/* border-image: linear-gradient(to right, var(--orange-medium), var(--orange-light)); */
|
||||
border-radius: 8px;
|
||||
background: var(--orange-medium);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 1.125em;
|
||||
transition: all 0.1s;
|
||||
box-shadow: 0 1px 2px var(--orange-light);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
button.alt {
|
||||
background-color: var(--text-light);
|
||||
color: var(--text-dark);
|
||||
border-color: var(--text-light);
|
||||
box-shadow: 0 1px 2px var(--text-light);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: var(--medium-gray);
|
||||
border: 1px solid var(--medium-gray);
|
||||
box-shadow: 0 1px 2px var(--medium-gray);
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.25em 0.5em;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--orange-medium);
|
||||
background-color: var(--input-background);
|
||||
color: var(--text-light);
|
||||
border-radius: 8px;
|
||||
|
||||
/* Use a custom chevron image */
|
||||
background-image: url('./assets/select-chevron.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
/* Adjust the horizontal position to control padding */
|
||||
background-size: 16px;
|
||||
/* Adjust size of the chevron */
|
||||
padding-right: 2em;
|
||||
/* Adjust the padding to make room for the chevron */
|
||||
|
||||
-webkit-appearance: none;
|
||||
/* Removes default styling on WebKit browsers like Safari */
|
||||
-moz-appearance: none;
|
||||
/* Removes default styling on Firefox */
|
||||
appearance: none;
|
||||
/* Standard property, currently not fully supported */
|
||||
}
|
10
kinode/packages/app_store/ui/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
127
kinode/packages/app_store/ui/src/pages/AppPage.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { AppInfo } from "../types/Apps";
|
||||
import useAppsStore from "../store/apps-store";
|
||||
import ActionButton from "../components/ActionButton";
|
||||
import AppHeader from "../components/AppHeader";
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { appId } from "../utils/app";
|
||||
|
||||
interface AppPageProps extends PageProps {}
|
||||
|
||||
export default function AppPage(props: AppPageProps) {
|
||||
// eslint-disable-line
|
||||
const { myApps, listedApps, getListedApp } = useAppsStore();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const [app, setApp] = useState<AppInfo | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const myApp = myApps.local.find((a) => appId(a) === params.id);
|
||||
if (myApp) return setApp(myApp);
|
||||
|
||||
if (params.id) {
|
||||
const app = listedApps.find((a) => appId(a) === params.id);
|
||||
if (app) {
|
||||
setApp(app);
|
||||
} else {
|
||||
getListedApp(params.id)
|
||||
.then((app) => setApp(app))
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
}, [params.id]);
|
||||
|
||||
const goToPublish = useCallback(() => {
|
||||
navigate("/publish", { state: { app } });
|
||||
}, [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];
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<SearchHeader value="" onChange={() => null} hideSearch />
|
||||
<div className="card" style={{ marginTop: "1em" }}>
|
||||
{app ? (
|
||||
<>
|
||||
<div className="row between">
|
||||
<AppHeader app={app} size="large" />
|
||||
<ActionButton app={app} style={{ marginRight: "0.5em" }} />
|
||||
</div>
|
||||
<div className="col" style={{ marginTop: "1em" }}>
|
||||
<div className="app-details row">
|
||||
<div className="title">Description</div>
|
||||
<div className="value">
|
||||
{(app.metadata?.description || "No description given").slice(
|
||||
0,
|
||||
2000
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-details row">
|
||||
<div className="title">Publisher</div>
|
||||
<div className="value underline">{app.publisher}</div>
|
||||
</div>
|
||||
<div className="app-details row">
|
||||
<div className="title">Version</div>
|
||||
<div className="value">{version}</div>
|
||||
</div>
|
||||
<div className="app-details row">
|
||||
<div className="title">Mirrors</div>
|
||||
<div className="col">
|
||||
{(app.metadata?.properties?.mirrors || []).map(
|
||||
(mirror, index) => (
|
||||
<div key={index + mirror} className="value underline">
|
||||
{mirror}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="app-details row">
|
||||
<div className="title">Permissions</div>
|
||||
<div className="col">
|
||||
{app.permissions?.map((permission, index) => (
|
||||
<div key={index + permission} className="value permission">{permission}</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="app-details row">
|
||||
<div className="title">Hash</div>
|
||||
<div className="value" style={{ wordBreak: "break-all" }}>
|
||||
{hash}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-screenshots row">
|
||||
{(app.metadata?.properties?.screenshots || []).map(
|
||||
(screenshot, index) => (
|
||||
<img key={index + screenshot} src={screenshot} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{app.installed && (
|
||||
<button type="button" onClick={goToPublish}>
|
||||
Publish
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h4>App details not found for </h4>
|
||||
<h4>{params.id}</h4>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
80
kinode/packages/app_store/ui/src/pages/MyAppsPage.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
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";
|
||||
|
||||
interface MyAppsPageProps extends PageProps {}
|
||||
|
||||
export default function MyAppsPage(props: MyAppsPageProps) { // 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]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: '100%' }}>
|
||||
<SearchHeader value={searchQuery} onChange={searchMyApps} />
|
||||
<div className="row between page-title">
|
||||
<h4 style={{ marginBottom: "0.5em" }}>My Packages</h4>
|
||||
<button className="row" onClick={() => navigate('/publish')}>
|
||||
<FaUpload style={{ marginRight: "0.5em" }} />
|
||||
Publish Package
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="my-apps-list">
|
||||
<div className="new card col" style={{ gap: "1em" }}>
|
||||
<h4>Downloaded</h4>
|
||||
{(displayedApps.downloaded || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
<h4>Installed</h4>
|
||||
{(displayedApps.installed || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
<h4>Local</h4>
|
||||
{(displayedApps.local || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
<h4>System</h4>
|
||||
{(displayedApps.system || []).map((app) => <AppEntry key={appId(app)} app={app} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
305
kinode/packages/app_store/ui/src/pages/PublishPage.tsx
Normal file
@ -0,0 +1,305 @@
|
||||
import React, { useState, useCallback, FormEvent, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { BigNumber, utils } from "ethers";
|
||||
import { useWeb3React } from "@web3-react/core";
|
||||
|
||||
import SearchHeader from "../components/SearchHeader";
|
||||
import { PageProps } from "../types/Page";
|
||||
import { setChain } from "../utils/chain";
|
||||
import { SEPOLIA_OPT_HEX } from "../constants/chain";
|
||||
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";
|
||||
|
||||
const { useIsActivating } = hooks;
|
||||
|
||||
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 [publisherId, setPublisherId] = useState<string>(
|
||||
window.our?.node || ""
|
||||
); // BytesLike
|
||||
const [metadataUrl, setMetadataUrl] = useState<string>("");
|
||||
const [metadataHash, setMetadataHash] = useState<string>(""); // BytesLike
|
||||
const [isUpdate, setIsUpdate] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const app: AppInfo | undefined = state?.app;
|
||||
if (app) {
|
||||
setPackageName(app.package);
|
||||
setPublisherId(app.publisher);
|
||||
setIsUpdate(true);
|
||||
}
|
||||
}, [state])
|
||||
|
||||
const connectWallet = useCallback(async () => {
|
||||
await metaMask.activate().catch(() => {});
|
||||
|
||||
try {
|
||||
setChain(SEPOLIA_OPT_HEX);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const calculateMetadataHash = useCallback(async () => {
|
||||
if (!metadataUrl) {
|
||||
setMetadataHash("");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const metadataResponse = await fetch(metadataUrl);
|
||||
const metadataText = await metadataResponse.text();
|
||||
JSON.parse(metadataText); // confirm it's valid JSON
|
||||
const metadataHash = utils.keccak256(utils.toUtf8Bytes(metadataText));
|
||||
setMetadataHash(metadataHash);
|
||||
} catch (error) {
|
||||
window.alert(
|
||||
"Error calculating metadata hash. Please ensure the URL is valid and the metadata is in JSON format."
|
||||
);
|
||||
}
|
||||
}, [metadataUrl]);
|
||||
|
||||
const publishPackage = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let metadata = metadataHash;
|
||||
|
||||
try {
|
||||
if (!metadata) {
|
||||
// https://pongo-uploads.s3.us-east-2.amazonaws.com/chat_metadata.json
|
||||
const metadataResponse = await fetch(metadataUrl);
|
||||
await metadataResponse.json(); // confirm it's valid JSON
|
||||
const metadataText = await metadataResponse.text(); // hash as text
|
||||
metadata = utils.keccak256(utils.toUtf8Bytes(metadataText));
|
||||
}
|
||||
|
||||
setLoading("Please confirm the transaction in your wallet");
|
||||
const publisherIdDnsWireFormat = toDNSWireFormat(publisherId);
|
||||
await setChain(SEPOLIA_OPT_HEX);
|
||||
|
||||
// TODO: have a checkbox to show if it's an update of an existing package
|
||||
|
||||
const tx = await (isUpdate
|
||||
? 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("");
|
||||
setPublisherId(window.our?.node || publisherId);
|
||||
setMetadataUrl("");
|
||||
setMetadataHash("");
|
||||
setIsUpdate(false);
|
||||
} catch (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("");
|
||||
}
|
||||
},
|
||||
[
|
||||
packageName,
|
||||
isUpdate,
|
||||
publisherId,
|
||||
metadataUrl,
|
||||
metadataHash,
|
||||
packageAbi,
|
||||
setPublishSuccess,
|
||||
setPackageName,
|
||||
setPublisherId,
|
||||
setMetadataUrl,
|
||||
setMetadataHash,
|
||||
setIsUpdate,
|
||||
]
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<SearchHeader hideSearch onBack={showMetadataForm ? () => setShowMetadataForm(false) : undefined} />
|
||||
<div className="row between page-title">
|
||||
<h4>Publish Package</h4>
|
||||
{Boolean(account) && (
|
||||
<div style={{ textAlign: "right", lineHeight: 1.5 }}>
|
||||
{" "}
|
||||
Connected as{" "}
|
||||
{account?.slice(0, 6) + "..." + account?.slice(account.length - 6)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="col center">
|
||||
<Loader msg={loading} />
|
||||
</div>
|
||||
) : publishSuccess ? (
|
||||
<div className="col center">
|
||||
<h4 style={{ marginBottom: "0.5em" }}>Package Published!</h4>
|
||||
<div style={{ marginBottom: "0.5em" }}>
|
||||
<strong>Package Name:</strong> {publishSuccess.packageName}
|
||||
</div>
|
||||
<div style={{ marginBottom: "0.5em" }}>
|
||||
<strong>Publisher ID:</strong> {publishSuccess.publisherId}
|
||||
</div>
|
||||
<button
|
||||
className={`my-pkg-btn row`}
|
||||
style={{ marginTop: "1em" }}
|
||||
onClick={() => setPublishSuccess(undefined)}
|
||||
>
|
||||
Publish Another Package
|
||||
</button>
|
||||
</div>
|
||||
) : showMetadataForm ? (
|
||||
<MetadataForm {...{packageName, publisherId, app: state?.app}} goBack={() => setShowMetadataForm(false)} />
|
||||
) : !account || !isActive ? (
|
||||
<>
|
||||
<h4 style={{}}>Please connect your wallet to publish a package</h4>
|
||||
<button className={`connect-wallet row`} onClick={connectWallet}>
|
||||
Connect Wallet
|
||||
</button>
|
||||
</>
|
||||
) : isActivating ? (
|
||||
<Loader msg="Approve connection in your wallet" />
|
||||
) : (
|
||||
<form
|
||||
className="new card col"
|
||||
style={{ flex: 1, overflowY: "scroll" }}
|
||||
onSubmit={publishPackage}
|
||||
>
|
||||
<div
|
||||
className="row between"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
padding: "0.5em",
|
||||
margin: "0 0 0 -0.5em",
|
||||
}}
|
||||
onClick={() => setIsUpdate(!isUpdate)}
|
||||
>
|
||||
<Checkbox checked={isUpdate} readOnly />
|
||||
<label htmlFor="update" style={{ cursor: "pointer", marginLeft: 8 }}>
|
||||
Update existing package
|
||||
</label>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="package-name">Package Name</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
id="package-name"
|
||||
type="text"
|
||||
required
|
||||
placeholder="my-package"
|
||||
value={packageName}
|
||||
onChange={(e) => setPackageName(e.target.value)}
|
||||
onBlur={checkIfUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="publisher-id">Publisher ID</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
id="publisher-id"
|
||||
type="text"
|
||||
required
|
||||
value={publisherId}
|
||||
onChange={(e) => setPublisherId(e.target.value)}
|
||||
onBlur={checkIfUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="metadata-url">
|
||||
Metadata URL
|
||||
</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
id="metadata-url"
|
||||
type="text"
|
||||
required
|
||||
value={metadataUrl}
|
||||
onChange={(e) => setMetadataUrl(e.target.value)}
|
||||
onBlur={calculateMetadataHash}
|
||||
placeholder="https://github/my-org/my-repo/metadata.json"
|
||||
/>
|
||||
<div style={{ textAlign: "left", margin: "0.5em 0 0" }}>
|
||||
Metadata is a JSON file that describes your package.
|
||||
<br /> You can{" "}
|
||||
<a onClick={() => setShowMetadataForm(true)} style={{ cursor: "pointer", textDecoration: "underline" }}>
|
||||
fill out a template here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
<div className="col f-width">
|
||||
<label htmlFor="metadata-hash">Metadata Hash</label>
|
||||
<input
|
||||
style={{ minWidth: "80%" }}
|
||||
readOnly
|
||||
id="metadata-hash"
|
||||
type="text"
|
||||
value={metadataHash}
|
||||
onChange={(e) => setMetadataHash(e.target.value)}
|
||||
placeholder="Calculated automatically from metadata URL"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="primary">
|
||||
Publish
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
182
kinode/packages/app_store/ui/src/pages/StorePage.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
|
||||
|
||||
import { AppInfo } 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 { appId } from "../utils/app";
|
||||
|
||||
interface StorePageProps extends PageProps { }
|
||||
|
||||
export default function StorePage(props: StorePageProps) {
|
||||
// eslint-disable-line
|
||||
const { listedApps, getListedApps } = useAppsStore();
|
||||
|
||||
const [resultsSort, setResultsSort] = useState<string>("Recently published");
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [displayedApps, setDisplayedApps] = useState<AppInfo[]>(listedApps);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const pages = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
{ length: Math.ceil(displayedApps.length / 10) },
|
||||
(_, index) => index + 1
|
||||
),
|
||||
[displayedApps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const start = (page - 1) * 10;
|
||||
const end = start + 10;
|
||||
setDisplayedApps(listedApps.slice(start, end));
|
||||
}, [listedApps]);
|
||||
|
||||
// GET on load
|
||||
useEffect(() => {
|
||||
getListedApps()
|
||||
.then((apps) => {
|
||||
setDisplayedApps(Object.values(apps));
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}, []); // eslint-disable-line
|
||||
|
||||
// const pages = useMemo(
|
||||
// () => {
|
||||
// const displayedApps = query ? searchResults : latestApps;
|
||||
|
||||
// return Array.from(
|
||||
// { length: Math.ceil((displayedApps.length - 2) / 10) },
|
||||
// (_, index) => index + 1
|
||||
// )
|
||||
// },
|
||||
// [query, searchResults, latestApps]
|
||||
// );
|
||||
|
||||
// const featuredApps = useMemo(() => latestApps.slice(0, 2), [latestApps]);
|
||||
// const displayedApps = useMemo(
|
||||
// () => {
|
||||
// const displayedApps = query ? searchResults : latestApps.slice(2);
|
||||
// return displayedApps.slice((page - 1) * 10, page * 10)
|
||||
// },
|
||||
// [latestApps, searchResults, page, query]
|
||||
// );
|
||||
|
||||
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 viewDetails = useCallback(
|
||||
// (app: AppInfo) => () => {
|
||||
// navigate(`/app-details/${appId(app)}`);
|
||||
// },
|
||||
// [navigate]
|
||||
// );
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
{/* <div style={{ position: "absolute", top: 4, left: 8 }}>
|
||||
ID: <strong>{window.our?.node}</strong>
|
||||
</div> */}
|
||||
<SearchHeader value={searchQuery} onChange={searchApps} />
|
||||
{/* <h3 style={{ marginBottom: "0.5em" }}>Featured</h3>
|
||||
<div className="featured row">
|
||||
{featuredApps.map((app, i) => (
|
||||
<div
|
||||
key={app.name + app.metadata_hash}
|
||||
className="card col"
|
||||
style={{
|
||||
marginLeft: i === 1 ? "1em" : 0,
|
||||
flex: 1,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={viewDetails(app)}
|
||||
>
|
||||
<AppHeader app={app} />
|
||||
<div style={{ marginTop: "0.25em" }}>
|
||||
{app.metadata?.description || "No description provided."}
|
||||
</div>
|
||||
<ActionButton style={{ marginTop: "0.5em" }} />
|
||||
</div>
|
||||
))}
|
||||
</div> */}
|
||||
<div className="row between page-title">
|
||||
<h4>New</h4>
|
||||
|
||||
<select
|
||||
value={resultsSort}
|
||||
onChange={(e) => {
|
||||
setResultsSort(e.target.value);
|
||||
sortApps(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option>Recently published</option>
|
||||
<option>Most popular</option>
|
||||
<option>Best rating</option>
|
||||
<option>Recently updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="new card col" style={{ flex: 1, overflowY: "auto", gap: "1em" }}>
|
||||
{displayedApps.map((app) => (
|
||||
<AppEntry
|
||||
key={appId(app) + (app.state?.our_version || "")}
|
||||
app={app}
|
||||
/>
|
||||
))}
|
||||
{pages.length > 1 && (
|
||||
<div className="row" style={{ alignSelf: "center" }}>
|
||||
{page !== pages[0] && (
|
||||
<FaChevronLeft onClick={() => setPage(page - 1)} />
|
||||
)}
|
||||
{pages.map((p) => (
|
||||
<div
|
||||
key={`page-${p}`}
|
||||
className={`page-selector ${p === page ? "selected" : ""}`}
|
||||
onClick={() => setPage(p)}
|
||||
>
|
||||
{p}
|
||||
</div>
|
||||
))}
|
||||
{page !== pages[pages.length - 1] && (
|
||||
<FaChevronRight onClick={() => setPage(page + 1)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
204
kinode/packages/app_store/ui/src/store/apps-store.ts
Normal file
@ -0,0 +1,204 @@
|
||||
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>
|
||||
|
||||
// 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 res = await fetch(`${BASE_URL}/apps`)
|
||||
const apps = await res.json() as AppInfo[]
|
||||
|
||||
const myApps = apps.reduce((acc, app) => {
|
||||
const appType = getAppType(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)}`)
|
||||
}
|
||||
},
|
||||
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
|
101
kinode/packages/app_store/ui/src/types/Apps.ts
Normal file
@ -0,0 +1,101 @@
|
||||
export interface MyApps {
|
||||
downloaded: AppInfo[]
|
||||
installed: AppInfo[]
|
||||
local: AppInfo[]
|
||||
system: AppInfo[]
|
||||
}
|
||||
|
||||
export interface AppListing {
|
||||
owner?: string
|
||||
package: string
|
||||
publisher: string
|
||||
metadata_hash: string
|
||||
metadata?: OnchainPackageMetadata
|
||||
installed: boolean
|
||||
state?: PackageState
|
||||
}
|
||||
|
||||
export interface Erc721Properties {
|
||||
package_name: string;
|
||||
publisher: string;
|
||||
current_version: string;
|
||||
mirrors: string[];
|
||||
code_hashes: Record<string, string>;
|
||||
license?: string;
|
||||
screenshots?: string[];
|
||||
wit_version?: [number, number, number];
|
||||
}
|
||||
|
||||
export interface OnchainPackageMetadata {
|
||||
name?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
external_url?: string;
|
||||
animation_url?: string;
|
||||
properties: Erc721Properties;
|
||||
}
|
||||
|
||||
export interface PackageState {
|
||||
mirrored_from: string;
|
||||
our_version: string;
|
||||
installed: boolean;
|
||||
verified: 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 {
|
||||
process_name: string
|
||||
process_wasm_path: string
|
||||
on_exit: string
|
||||
request_networking: boolean
|
||||
request_capabilities: string[]
|
||||
grant_capabilities: string[]
|
||||
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
|
||||
}
|
||||
]
|
7
kinode/packages/app_store/ui/src/types/Page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { ethers } from "ethers";
|
||||
import { PackageStore } from "../abis/types";
|
||||
|
||||
export interface PageProps {
|
||||
provider?: ethers.providers.Web3Provider;
|
||||
packageAbi: PackageStore
|
||||
}
|
24
kinode/packages/app_store/ui/src/utils/app.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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?.capsApproved) {
|
||||
return AppType.Downloaded
|
||||
} else if (!app.metadata) {
|
||||
return AppType.Local
|
||||
} else {
|
||||
return AppType.Installed
|
||||
}
|
||||
}
|
88
kinode/packages/app_store/ui/src/utils/chain.ts
Normal file
@ -0,0 +1,88 @@
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
kinode/packages/app_store/ui/src/utils/dnsWire.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export function toDNSWireFormat(domain: string) {
|
||||
const parts = domain.split('.');
|
||||
const result = new Uint8Array(domain.length + parts.length);
|
||||
let idx = 0;
|
||||
|
||||
for (const part of parts) {
|
||||
const len = part.length;
|
||||
result[idx] = len; // write length byte
|
||||
idx++;
|
||||
for (let j = 0; j < len; j++) {
|
||||
result[idx] = part.charCodeAt(j); // write ASCII bytes of the label
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
// result[idx] = 0; // TODO do you need null byte at the end?
|
||||
|
||||
return `0x${Array.from(result).map(byte => byte.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
4
kinode/packages/app_store/ui/src/utils/metamask.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { initializeConnector } from '@web3-react/core'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
|
||||
export const [metaMask, hooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions }))
|
1
kinode/packages/app_store/ui/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
27
kinode/packages/app_store/ui/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// "strict": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"../src",
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
kinode/packages/app_store/ui/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
68
kinode/packages/app_store/ui/vite.config.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
/*
|
||||
If you are developing a UI outside of a Kinode project,
|
||||
comment out the following 2 lines:
|
||||
*/
|
||||
// import manifest from '../pkg/manifest.json'
|
||||
// import metadata from '../pkg/metadata.json'
|
||||
|
||||
/*
|
||||
IMPORTANT:
|
||||
This must match the process name from pkg/manifest.json + pkg/metadata.json
|
||||
The format is "/" + "process_name:package_name:publisher_node"
|
||||
*/
|
||||
const BASE_URL = `/main:app_store:sys`;
|
||||
// const BASE_URL = `/${manifest[0].process_name}:${metadata.package}:${metadata.publisher}`;
|
||||
|
||||
// This is the proxy URL, it must match the node you are developing against
|
||||
const PROXY_URL = (process.env.VITE_NODE_URL || 'http://127.0.0.1:8080').replace('localhost', '127.0.0.1');
|
||||
|
||||
console.log('process.env.VITE_NODE_URL', process.env.VITE_NODE_URL, PROXY_URL);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: BASE_URL,
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['/our.js']
|
||||
}
|
||||
},
|
||||
server: {
|
||||
open: true,
|
||||
proxy: {
|
||||
'/our': {
|
||||
target: PROXY_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
[`${BASE_URL}/our.js`]: {
|
||||
target: PROXY_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(BASE_URL, ''),
|
||||
},
|
||||
// This route will match all other HTTP requests to the backend
|
||||
[`^${BASE_URL}/(?!(@vite/client|src/.*|node_modules/.*|@react-refresh|$))`]: {
|
||||
target: PROXY_URL,
|
||||
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);
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
}
|
||||
}
|
||||
});
|
5404
kinode/packages/app_store/ui/yarn.lock
Normal file
Before Width: | Height: | Size: 3.5 KiB |
@ -31,7 +31,6 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2em;
|
||||
font-size: 16px;
|
||||
background-color: var(--gray-button);
|
||||
color: var(--text-light);
|
||||
@ -39,7 +38,6 @@
|
||||
background-size: cover;
|
||||
height: calc(100vh - 4em);
|
||||
width: calc(100vw - 4em);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
body,
|
||||
@ -203,6 +201,7 @@
|
||||
#home-page {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
#uq-name {
|
||||
|
@ -1,7 +1,11 @@
|
||||
#![feature(let_chains)]
|
||||
use kinode_process_lib::{
|
||||
await_message, call_init, http::bind_http_static_path, http::HttpServerError, println, Address,
|
||||
Message, ProcessId,
|
||||
await_message, call_init,
|
||||
http::{
|
||||
bind_http_path, bind_http_static_path, send_response, HttpServerError, HttpServerRequest,
|
||||
StatusCode,
|
||||
},
|
||||
println, Address, Message, ProcessId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@ -21,6 +25,14 @@ enum HomepageRequest {
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct HomepageApp {
|
||||
package_name: String,
|
||||
path: String,
|
||||
label: String,
|
||||
base64_icon: String,
|
||||
}
|
||||
|
||||
wit_bindgen::generate!({
|
||||
path: "wit",
|
||||
world: "process",
|
||||
@ -38,7 +50,7 @@ const APP_TEMPLATE: &str = r#"
|
||||
<h6>${label}</h6>
|
||||
</a>"#;
|
||||
|
||||
call_init!(main);
|
||||
call_init!(init);
|
||||
|
||||
/// bind to root path on http_server (we have special dispensation to do so!)
|
||||
fn bind_index(our: &str, apps: &HashMap<ProcessId, String>) {
|
||||
@ -64,9 +76,96 @@ fn bind_index(our: &str, apps: &HashMap<ProcessId, String>) {
|
||||
.expect("failed to bind to /");
|
||||
}
|
||||
|
||||
fn main(our: Address) {
|
||||
let mut apps: HashMap<ProcessId, String> = HashMap::new();
|
||||
// // Copied in from process_lib serve_ui. see https://github.com/kinode-dao/process_lib/blob/main/src/http.rs
|
||||
// fn static_serve_dir(
|
||||
// our: &Address,
|
||||
// directory: &str,
|
||||
// authenticated: bool,
|
||||
// local_only: bool,
|
||||
// paths: Vec<&str>,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// serve_index_html(our, directory, authenticated, local_only, paths)?;
|
||||
|
||||
// let initial_path = format!("{}/pkg/{}", our.package_id(), directory);
|
||||
// println!("initial path: {}", initial_path);
|
||||
|
||||
// let mut queue = VecDeque::new();
|
||||
// queue.push_back(initial_path.clone());
|
||||
|
||||
// while let Some(path) = queue.pop_front() {
|
||||
// let Ok(directory_response) = KiRequest::to(("our", "vfs", "distro", "sys"))
|
||||
// .body(serde_json::to_vec(&VfsRequest {
|
||||
// path,
|
||||
// action: VfsAction::ReadDir,
|
||||
// })?)
|
||||
// .send_and_await_response(5)?
|
||||
// else {
|
||||
// return Err(anyhow::anyhow!(
|
||||
// "serve_ui: no response for path: {}",
|
||||
// initial_path
|
||||
// ));
|
||||
// };
|
||||
|
||||
// let directory_body = serde_json::from_slice::<VfsResponse>(directory_response.body())?;
|
||||
|
||||
// // Determine if it's a file or a directory and handle appropriately
|
||||
// match directory_body {
|
||||
// VfsResponse::ReadDir(directory_info) => {
|
||||
// for entry in directory_info {
|
||||
// match entry.file_type {
|
||||
// // If it's a file, serve it statically
|
||||
// FileType::File => {
|
||||
// KiRequest::to(("our", "vfs", "distro", "sys"))
|
||||
// .body(serde_json::to_vec(&VfsRequest {
|
||||
// path: entry.path.clone(),
|
||||
// action: VfsAction::Read,
|
||||
// })?)
|
||||
// .send_and_await_response(5)??;
|
||||
|
||||
// let Some(blob) = get_blob() else {
|
||||
// return Err(anyhow::anyhow!(
|
||||
// "serve_ui: no blob for {}",
|
||||
// entry.path
|
||||
// ));
|
||||
// };
|
||||
|
||||
// let content_type = get_mime_type(&entry.path);
|
||||
|
||||
// println!("binding {}", entry.path.replace(&initial_path, ""));
|
||||
|
||||
// bind_http_static_path(
|
||||
// entry.path.replace(&initial_path, ""),
|
||||
// authenticated, // Must be authenticated
|
||||
// local_only, // Is not local-only
|
||||
// Some(content_type),
|
||||
// blob.bytes,
|
||||
// )?;
|
||||
// }
|
||||
// FileType::Directory => {
|
||||
// // Push the directory onto the queue
|
||||
// queue.push_back(entry.path);
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {
|
||||
// return Err(anyhow::anyhow!(
|
||||
// "serve_ui: unexpected response for path: {:?}",
|
||||
// directory_body
|
||||
// ))
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
fn init(our: Address) {
|
||||
let mut apps: HashMap<ProcessId, String> = HashMap::new();
|
||||
let mut app_data: HashMap<ProcessId, HomepageApp> = HashMap::new();
|
||||
|
||||
// static_serve_dir(&our, "index.html", true, false, vec!["/"]);
|
||||
bind_index(&our.node, &apps);
|
||||
|
||||
bind_http_static_path(
|
||||
@ -89,6 +188,8 @@ fn main(our: Address) {
|
||||
)
|
||||
.expect("failed to bind to /our.js");
|
||||
|
||||
bind_http_path("/apps", true, true).expect("failed to bind /apps");
|
||||
|
||||
loop {
|
||||
let Ok(ref message) = await_message() else {
|
||||
// we never send requests, so this will never happen
|
||||
@ -108,6 +209,15 @@ fn main(our: Address) {
|
||||
if let Ok(request) = serde_json::from_slice::<HomepageRequest>(message.body()) {
|
||||
match request {
|
||||
HomepageRequest::Add { label, icon, path } => {
|
||||
app_data.insert(
|
||||
message.source().process.clone(),
|
||||
HomepageApp {
|
||||
package_name: message.source().clone().package().to_string(),
|
||||
path: path.clone(),
|
||||
label: label.clone(),
|
||||
base64_icon: icon.clone(),
|
||||
},
|
||||
);
|
||||
apps.insert(
|
||||
message.source().process.clone(),
|
||||
APP_TEMPLATE
|
||||
@ -137,6 +247,36 @@ fn main(our: Address) {
|
||||
bind_index(&our.node, &apps);
|
||||
}
|
||||
}
|
||||
} else if let Ok(request) = serde_json::from_slice::<HttpServerRequest>(message.body())
|
||||
{
|
||||
match request {
|
||||
HttpServerRequest::Http(incoming) => {
|
||||
let path = incoming.bound_path(None);
|
||||
println!("on path: {}", path);
|
||||
if path == "/apps" {
|
||||
send_response(
|
||||
StatusCode::OK,
|
||||
Some(HashMap::from([(
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
)])),
|
||||
app_data
|
||||
.values()
|
||||
.map(|app| serde_json::to_string(app).unwrap())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
send_response(
|
||||
StatusCode::OK,
|
||||
Some(HashMap::new()),
|
||||
"hello".as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@ -90,7 +90,7 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
/* :root {
|
||||
--k-red: #a30101;
|
||||
--k-darkred: #4d0c0c;
|
||||
--k-lightred: #dd0207;
|
||||
@ -173,15 +173,6 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#signup-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 2em;
|
||||
max-width: calc(100vw - 4em);
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
@ -291,7 +282,7 @@
|
||||
font-size: 0.8em;
|
||||
line-height: 1.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
} */
|
||||
</style>
|
||||
</body>
|
||||
|
||||
|
23
kinode/src/register-ui/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
src/abis/types/*
|
1
kinode/src/register-ui/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18.18.0
|
10
kinode/src/register-ui/.sample.env
Normal file
@ -0,0 +1,10 @@
|
||||
# local
|
||||
REACT_APP_INVITE_GET=
|
||||
REACT_APP_BUILD_USER_OP_POST=
|
||||
REACT_APP_BROADCAST_USER_OP_POST=
|
||||
|
||||
REACT_APP_SEPOLIA_RPC_URL=
|
||||
REACT_APP_OPTIMISM_RPC_URL=
|
||||
REACT_APP_MAINNET_RPC_URL=
|
||||
DANGEROUSLY_DISABLE_HOST_CHECK=true # needed for local development
|
||||
|
18
kinode/src/register-ui/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Register
|
||||
This app is compiled and put into the root directory of every Kinode node for login and registration. It handles all on-chain KNS registration flows
|
||||
|
||||
## Development
|
||||
|
||||
1. Run `yarn` to install dependencies
|
||||
1. Run `yarn run tc` to generate ABIs
|
||||
1. Start a kinode locally on port 8080 (default)
|
||||
1. Run `yarn start` to serve the UI at http://localhost:3000 (proxies requests to local kinode)
|
||||
|
||||
If you would like to proxy requests to a kinode that is not at http://localhost:8080, change the `proxy` field in `package.json`.
|
||||
|
||||
## Building
|
||||
|
||||
1. Run `yarn` to install dependencies
|
||||
1. Run `yarn run tc` to generate ABIs
|
||||
1. Run `yarn build` to generate the `./build` folder
|
||||
1. Overwrite `kinode/kinode/src/register-ui/build` with `./build`
|
19
kinode/src/register-ui/add-inline-tags.js
Normal file
@ -0,0 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const indexPath = path.join(__dirname, 'build', 'index.html');
|
||||
|
||||
fs.readFile(indexPath, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let modifiedHtml = data
|
||||
.replace(/<script src="(.*?)"><\/script>/g, '<script src="$1" inline></script>')
|
||||
.replace(/<link href="(.*?)" rel="stylesheet">/g, '<link href="$1" rel="stylesheet" inline>');
|
||||
|
||||
fs.writeFile(indexPath, modifiedHtml, 'utf8', (err) => {
|
||||
if (err) return console.log(err);
|
||||
});
|
||||
});
|
@ -1,15 +1,17 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.a386e724.css",
|
||||
"main.js": "/static/js/main.332b908d.js",
|
||||
"main.css": "/static/css/main.6c087b1c.css",
|
||||
"main.js": "/static/js/main.be5cbd4a.js",
|
||||
"static/media/OpenSans-CondBold.ttf": "/static/media/OpenSans-CondBold.6293057f8484b6c0da03.ttf",
|
||||
"static/media/BarlowCondensed-Black.ttf": "/static/media/BarlowCondensed-Black.3ba02bbdeb04e17f34bf.ttf",
|
||||
"static/media/Futura-Heavy.ttf": "/static/media/Futura-Heavy.af72c25a6945b0f48abb.ttf",
|
||||
"static/media/unknown.png": "/static/media/unknown.880d04d4611a45ab1001.png",
|
||||
"static/media/background.jpg": "/static/media/background.01d2427cfc21fb685016.jpg",
|
||||
"static/media/kinode.svg": "/static/media/kinode.86d0c1a6a4a3ca3be41616b5989d6925.svg",
|
||||
"index.html": "/index.html",
|
||||
"static/media/logo.svg": "/static/media/logo.45dcb752ac5b825f5e3b9299d2210f0a.svg"
|
||||
"static/media/kinode.svg": "/static/media/kinode.6b178bc9164b31d90099844a82d04497.svg"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.a386e724.css",
|
||||
"static/js/main.332b908d.js"
|
||||
"static/css/main.6c087b1c.css",
|
||||
"static/js/main.be5cbd4a.js"
|
||||
]
|
||||
}
|
@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><title>Welcome - Kinode</title><meta charset="utf-8"/><meta http-equiv="pragma" content="no-cache"/><meta http-equiv="cache-control" content="no-cache"/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"><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 defer="defer" src="/static/js/main.332b908d.js"></script><link href="/static/css/main.a386e724.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><title>Welcome - Kinode</title><meta charset="utf-8"/><meta http-equiv="pragma" content="no-cache"/><meta http-equiv="cache-control" content="no-cache"/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"><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 defer="defer" src="/static/js/main.be5cbd4a.js"></script><link href="/static/css/main.6c087b1c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
2
kinode/src/register-ui/build/static/js/main.be5cbd4a.js
Normal file
@ -1,3 +1,9 @@
|
||||
/*!
|
||||
Copyright (c) 2018 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/*!
|
||||
Copyright (c) 2015 Jed Watson.
|
||||
Based on code that is Copyright 2013-2015, Facebook, Inc.
|
||||
@ -25,8 +31,6 @@
|
||||
|
||||
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
@ -88,7 +92,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.10.0
|
||||
* @remix-run/router v1.15.3
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@ -99,7 +103,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.17.0
|
||||
* React Router DOM v6.22.3
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@ -110,7 +114,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.17.0
|
||||
* React Router v6.22.3
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
@ -0,0 +1,4 @@
|
||||
<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>
|
After Width: | Height: | Size: 644 B |
6
kinode/src/register-ui/build_all.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
source ~/.nvm/nvm.sh
|
||||
nvm use
|
||||
npm install
|
||||
npm run tc
|
||||
npm run build
|
22995
kinode/src/register-ui/package-lock.json
generated
Normal file
73
kinode/src/register-ui/package.json
Normal file
@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "register",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:8080",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@ethersproject/hash": "^5.7.0",
|
||||
"@typechain/ethers-v5": "^11.1.1",
|
||||
"@types/node": "^16.18.50",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@web3-react/coinbase-wallet": "^8.2.3",
|
||||
"@web3-react/core": "^8.2.2",
|
||||
"@web3-react/gnosis-safe": "^8.2.4",
|
||||
"@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",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eth-ens-namehash": "^2.0.8",
|
||||
"ethers": "^5.7.2",
|
||||
"idna-uts46-hx": "^2.3.1",
|
||||
"is-valid-domain": "^0.1.6",
|
||||
"jazzicon": "^1.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typechain": "^8.3.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "npm run tc && GENERATE_SOURCEMAP=false react-scripts build",
|
||||
"build:copy": "npm run build",
|
||||
"inline": "node ./add-inline-tags.js && cd build && inline-source ./index.html > ./inline-index.html && cd ..",
|
||||
"build-inline": "npm run build && npm run inline",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"tc": "typechain --target ethers-v5 --out-dir src/abis/types/ \"./src/abis/**/*.json\""
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-modal": "^3.16.2",
|
||||
"inline-source-cli": "^2.0.0"
|
||||
}
|
||||
}
|
23
kinode/src/register-ui/public/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Welcome - Kinode</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<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"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
25
kinode/src/register-ui/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
kinode/src/register-ui/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
252
kinode/src/register-ui/src/App.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Navigate, BrowserRouter as Router, Route, Routes, useParams } from 'react-router-dom';
|
||||
import { hooks } from "./connectors/metamask";
|
||||
import {
|
||||
KNS_REGISTRY_ADDRESSES,
|
||||
DOT_OS_ADDRESSES,
|
||||
ENS_REGISTRY_ADDRESSES,
|
||||
NAMEWRAPPER_ADDRESSES,
|
||||
KNS_ENS_ENTRY_ADDRESSES,
|
||||
KNS_ENS_EXIT_ADDRESSES,
|
||||
} from "./constants/addresses";
|
||||
import { ChainId } from "./constants/chainId";
|
||||
import {
|
||||
KNSRegistryResolver,
|
||||
KNSRegistryResolver__factory,
|
||||
DotOsRegistrar,
|
||||
DotOsRegistrar__factory,
|
||||
KNSEnsEntry,
|
||||
KNSEnsEntry__factory,
|
||||
KNSEnsExit,
|
||||
KNSEnsExit__factory,
|
||||
NameWrapper,
|
||||
NameWrapper__factory,
|
||||
ENSRegistry,
|
||||
ENSRegistry__factory
|
||||
} from "./abis/types";
|
||||
import { ethers } from "ethers";
|
||||
import ConnectWallet from "./components/ConnectWallet";
|
||||
import RegisterEthName from "./pages/RegisterEthName";
|
||||
import RegisterOsName from "./pages/RegisterKnsName";
|
||||
import ClaimOsInvite from "./pages/ClaimKnsInvite";
|
||||
import SetPassword from "./pages/SetPassword";
|
||||
import Login from './pages/Login'
|
||||
import Reset from './pages/ResetKnsName'
|
||||
import KinodeHome from "./pages/KinodeHome"
|
||||
import ResetNode from "./pages/ResetNode";
|
||||
import ImportKeyfile from "./pages/ImportKeyfile";
|
||||
import { UnencryptedIdentity } from "./lib/types";
|
||||
|
||||
const {
|
||||
useProvider,
|
||||
} = hooks;
|
||||
|
||||
function App() {
|
||||
const provider = useProvider();
|
||||
const params = useParams()
|
||||
|
||||
const [pw, setPw] = useState<string>('');
|
||||
const [key, setKey] = useState<string>('');
|
||||
const [keyFileName, setKeyFileName] = useState<string>('');
|
||||
const [reset, setReset] = useState<boolean>(false);
|
||||
const [direct, setDirect] = useState<boolean>(false);
|
||||
const [knsName, setOsName] = useState<string>('');
|
||||
const [appSizeOnLoad, setAppSizeOnLoad] = useState<number>(0);
|
||||
const [networkingKey, setNetworkingKey] = useState<string>('');
|
||||
const [ipAddress, setIpAddress] = useState<number>(0);
|
||||
const [port, setPort] = useState<number>(0);
|
||||
const [routers, setRouters] = useState<string[]>([]);
|
||||
const [nodeChainId, setNodeChainId] = useState('')
|
||||
|
||||
const [navigateToLogin, setNavigateToLogin] = useState<boolean>(false)
|
||||
const [initialVisit, setInitialVisit] = useState<boolean>(!params?.initial)
|
||||
|
||||
const [connectOpen, setConnectOpen] = useState<boolean>(false);
|
||||
const openConnect = () => setConnectOpen(true)
|
||||
const closeConnect = () => setConnectOpen(false)
|
||||
|
||||
const rpcUrl = useMemo(() => provider?.network?.chainId === ChainId.SEPOLIA ? process.env.REACT_APP_SEPOLIA_RPC_URL : process.env.REACT_APP_OPTIMISM_RPC_URL, [provider])
|
||||
|
||||
const [dotOs, setDotOs] = useState<DotOsRegistrar>(
|
||||
DotOsRegistrar__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? DOT_OS_ADDRESSES[ChainId.SEPOLIA] : DOT_OS_ADDRESSES[ChainId.OPTIMISM],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [kns, setKns] = useState<KNSRegistryResolver>(
|
||||
KNSRegistryResolver__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? KNS_REGISTRY_ADDRESSES[ChainId.SEPOLIA] : KNS_REGISTRY_ADDRESSES[ChainId.OPTIMISM],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [knsEnsEntry, setKnsEnsEntry] = useState<KNSEnsEntry>(
|
||||
KNSEnsEntry__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? KNS_ENS_ENTRY_ADDRESSES[ChainId.SEPOLIA] : KNS_ENS_ENTRY_ADDRESSES[ChainId.MAINNET],
|
||||
// set rpc url based on chain id
|
||||
new ethers.providers.JsonRpcProvider(provider?.network?.chainId === ChainId.SEPOLIA ? process.env.REACT_APP_SEPOLIA_RPC_URL : process.env.REACT_APP_MAINNET_RPC_URL))
|
||||
);
|
||||
|
||||
const [knsEnsExit, setKnsEnsExit] = useState<KNSEnsExit>(
|
||||
KNSEnsExit__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? KNS_ENS_EXIT_ADDRESSES[ChainId.SEPOLIA] : KNS_ENS_EXIT_ADDRESSES[ChainId.OPTIMISM],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [nameWrapper, setNameWrapper] = useState<NameWrapper>(
|
||||
NameWrapper__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? NAMEWRAPPER_ADDRESSES[ChainId.SEPOLIA] : NAMEWRAPPER_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
const [ensRegistry, setEnsRegistry] = useState<ENSRegistry>(
|
||||
ENSRegistry__factory.connect(
|
||||
provider?.network?.chainId === ChainId.SEPOLIA ? ENS_REGISTRY_ADDRESSES[ChainId.SEPOLIA] : ENS_REGISTRY_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(rpcUrl))
|
||||
);
|
||||
|
||||
useEffect(() => setAppSizeOnLoad(
|
||||
(window.performance.getEntriesByType('navigation') as any)[0].transferSize
|
||||
), []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const infoResponse = await fetch('/info', { method: 'GET' })
|
||||
|
||||
if (infoResponse.status > 399) {
|
||||
console.log('no info, unbooted')
|
||||
} else {
|
||||
const info: UnencryptedIdentity = await infoResponse.json()
|
||||
|
||||
if (initialVisit) {
|
||||
setOsName(info.name)
|
||||
setRouters(info.allowed_routers)
|
||||
setNavigateToLogin(true)
|
||||
setInitialVisit(false)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.log('no info, unbooted')
|
||||
}
|
||||
|
||||
try {
|
||||
const currentChainResponse = await fetch('/current-chain', { method: 'GET' })
|
||||
|
||||
if (currentChainResponse.status < 400) {
|
||||
const nodeChainId = await currentChainResponse.json()
|
||||
setNodeChainId(nodeChainId.toLowerCase())
|
||||
console.log('Node Chain ID:', nodeChainId)
|
||||
}
|
||||
} catch {
|
||||
console.log('error getting current chain')
|
||||
}
|
||||
})()
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => setNavigateToLogin(false), [initialVisit])
|
||||
|
||||
useEffect(() => {
|
||||
provider?.getNetwork().then(network => {
|
||||
if (network.chainId === ChainId.SEPOLIA) {
|
||||
setDotOs(DotOsRegistrar__factory.connect(
|
||||
DOT_OS_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKns(KNSRegistryResolver__factory.connect(
|
||||
KNS_REGISTRY_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKnsEnsEntry(KNSEnsEntry__factory.connect(
|
||||
KNS_ENS_ENTRY_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKnsEnsExit(KNSEnsExit__factory.connect(
|
||||
KNS_ENS_EXIT_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setNameWrapper(NameWrapper__factory.connect(
|
||||
NAMEWRAPPER_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setEnsRegistry(ENSRegistry__factory.connect(
|
||||
ENS_REGISTRY_ADDRESSES[ChainId.SEPOLIA],
|
||||
provider!.getSigner()
|
||||
))
|
||||
|
||||
} else if (network.chainId === ChainId.OPTIMISM || network.chainId === ChainId.MAINNET) {
|
||||
setDotOs(DotOsRegistrar__factory.connect(
|
||||
DOT_OS_ADDRESSES[ChainId.OPTIMISM],
|
||||
provider!.getSigner())
|
||||
)
|
||||
setKns(KNSRegistryResolver__factory.connect(
|
||||
KNS_REGISTRY_ADDRESSES[ChainId.OPTIMISM],
|
||||
provider!.getSigner())
|
||||
)
|
||||
setKnsEnsExit(KNSEnsExit__factory.connect(
|
||||
KNS_ENS_EXIT_ADDRESSES[ChainId.OPTIMISM],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setKnsEnsEntry(KNSEnsEntry__factory.connect(
|
||||
KNS_ENS_ENTRY_ADDRESSES[ChainId.MAINNET],
|
||||
provider!.getSigner()
|
||||
))
|
||||
setNameWrapper(NameWrapper__factory.connect(
|
||||
NAMEWRAPPER_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(process.env.REACT_APP_MAINNET_RPC_URL)
|
||||
))
|
||||
setEnsRegistry(ENSRegistry__factory.connect(
|
||||
ENS_REGISTRY_ADDRESSES[ChainId.MAINNET],
|
||||
new ethers.providers.JsonRpcProvider(process.env.REACT_APP_MAINNET_RPC_URL)
|
||||
))
|
||||
}
|
||||
})
|
||||
}, [provider])
|
||||
|
||||
const knsEnsEntryNetwork = ChainId.SEPOLIA;
|
||||
const knsEnsExitNetwork = ChainId.SEPOLIA;
|
||||
|
||||
// just pass all the props each time since components won't mind extras
|
||||
const props = {
|
||||
direct, setDirect,
|
||||
key,
|
||||
keyFileName, setKeyFileName,
|
||||
reset, setReset,
|
||||
pw, setPw,
|
||||
knsName, setOsName,
|
||||
dotOs, kns,
|
||||
knsEnsEntryNetwork, knsEnsExitNetwork,
|
||||
knsEnsEntry, knsEnsExit,
|
||||
nameWrapper, ensRegistry,
|
||||
connectOpen, openConnect, closeConnect,
|
||||
provider, appSizeOnLoad,
|
||||
networkingKey, setNetworkingKey,
|
||||
ipAddress, setIpAddress,
|
||||
port, setPort,
|
||||
routers, setRouters,
|
||||
nodeChainId,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectWallet {...props} />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={navigateToLogin
|
||||
? <Navigate to="/login" replace />
|
||||
: <KinodeHome {...props} />
|
||||
} />
|
||||
<Route path="/claim-invite" element={<ClaimOsInvite {...props} />} />
|
||||
<Route path="/register-name" element={<RegisterOsName {...props} />} />
|
||||
<Route path="/register-eth-name" element={<RegisterEthName {...props} />} />
|
||||
<Route path="/set-password" element={<SetPassword {...props} />} />
|
||||
<Route path="/reset" element={<Reset {...props} />} />
|
||||
<Route path="/reset-node" element={<ResetNode {...props} />} />
|
||||
<Route path="/import-keyfile" element={<ImportKeyfile {...props} />} />
|
||||
<Route path="/login" element={<Login {...props} />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
7741
kinode/src/register-ui/src/abis/DotOsRegistrar.json
Normal file
1336
kinode/src/register-ui/src/abis/DotOsRegistrar.ts
Normal file
1
kinode/src/register-ui/src/abis/ENSRegistry.json
Normal file
1
kinode/src/register-ui/src/abis/KNSEnsEntry.json
Normal file
1
kinode/src/register-ui/src/abis/KNSEnsExit.json
Normal file
11870
kinode/src/register-ui/src/abis/KNSRegistryResolver.json
Normal file
1
kinode/src/register-ui/src/abis/NameWrapper.json
Normal file
BIN
kinode/src/register-ui/src/assets/arbitrum.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
kinode/src/register-ui/src/assets/background.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
kinode/src/register-ui/src/assets/eth.png
Normal file
After Width: | Height: | Size: 1.7 KiB |