Merge pull request #285 from kinode-dao/tm/unify

Move homepage & appstore UIs to main repo
This commit is contained in:
0x70b1a5 2024-04-02 11:10:59 -04:00 committed by GitHub
commit 929d9e4b5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
145 changed files with 82709 additions and 38 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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>

View 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
View 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?

View 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

View 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"
]
}

View 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>

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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

View 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;
}

View 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;

View 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": []
}
]

File diff suppressed because it is too large Load Diff

View 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;

View File

@ -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;
}
}

View File

@ -0,0 +1,4 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
export { PackageStore__factory } from "./PackageStore__factory";

View 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";

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View 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

View File

@ -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

View 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

View 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>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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">
&#10003;
</span>
)}
</div>
);
}

View 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>
)
}

View 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>
)
}

View 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",
}}
>
&times;
</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;

View 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

View 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>
);
}

View 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>
);
}

View 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',
};

View 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
}

View File

@ -0,0 +1 @@
export const MY_APPS_PATH = '/my-apps';

View 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 */
}

View 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>,
)

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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

View 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
}
]

View File

@ -0,0 +1,7 @@
import { ethers } from "ethers";
import { PackageStore } from "../abis/types";
export interface PageProps {
provider?: ethers.providers.Web3Provider;
packageAbi: PackageStore
}

View 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
}
}

View 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}`)
}
}
}
}

View 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('')}`;
}

View 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 }))

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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);
// });
// },
// },
}
}
});

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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 {

View File

@ -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(),
);
}
_ => {}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -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
View 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/*

View File

@ -0,0 +1 @@
v18.18.0

View 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

View 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`

View 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);
});
});

View File

@ -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"
]
}

View File

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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.
*

View File

@ -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

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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>

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Some files were not shown because too many files have changed in this diff Show More