unify-0.0.1

This commit is contained in:
Tobias Merkle 2024-03-21 15:40:47 -04:00
parent 8c6e86d1db
commit 1ace44c0e3
139 changed files with 76826 additions and 101 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ wit/
packages/**/pkg/*.wasm
packages/**/wit
kinode/packages/**/pkg/ui
*/**/node_modules
.env
kinode/src/bootstrapped_processes.rs
kinode/packages/**/wasi_snapshot_preview1.wasm

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-p7W2iPaq.js"></script>
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-7ftanDAW.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 },
],
},
}

26
kinode/packages/app_store/ui/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
src/abis/types
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,53 @@
{
"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 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",
"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,353 @@
#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 {
padding: 0.125em;
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": []
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,18 @@
<svg width="580" height="72" viewBox="0 0 580 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6_641)">
<path d="M0.824922 1.07031L0.794922 70.0703H14.7949L14.8049 1.07031H0.824922Z" fill="#FFF5D9"/>
<path d="M16.5947 36.8803L41.2547 1.07031H58.2447L33.1647 36.8803L61.2447 70.0703H42.9947L16.5947 36.8803Z" fill="#FFF5D9"/>
<path d="M119.885 1.07031H105.765V70.0703H119.885V1.07031Z" fill="#FFF5D9"/>
<path d="M173.185 1.07031V70.0703H186.775V26.8303L224.045 70.0703H234.825V1.07031H221.325V45.6803L183.445 1.07031H173.185Z" fill="#FFF5D9"/>
<path d="M342.465 8.86C333.025 0.15 321.645 0 318.535 0C315.475 0 303.575 0.22 294.005 9.52C283.845 19.4 283.805 32.24 283.795 35.66C283.785 39.3 283.895 49.03 290.805 57.99C300.855 71.02 316.695 71.31 318.535 71.32C321.375 71.32 334.185 71 343.965 60.66C353.065 51.04 353.265 39.4 353.275 35.66C353.275 32.49 353.305 18.86 342.455 8.86H342.465ZM318.435 58.01C307.095 58.01 297.895 47.95 297.895 35.54C297.895 23.13 307.085 13.07 318.435 13.07C329.785 13.07 338.975 23.13 338.975 35.54C338.975 47.95 329.785 58.01 318.435 58.01Z" fill="#FFF5D9"/>
<path d="M450.495 12.0802C444.975 5.46023 437.135 0.990234 427.955 0.990234C417.555 0.990234 405.295 1.07023 402.295 1.07023V69.9802C405.285 69.9802 417.555 70.0602 427.955 70.0602C445.525 70.0602 458.445 53.4102 459.065 36.8602C459.395 28.0102 456.185 18.9002 450.495 12.0802ZM440.085 49.9502C436.895 53.8702 432.705 56.6902 427.665 57.5602C424.025 58.1902 420.095 57.8302 416.405 57.8302C416.405 50.4002 416.405 42.9802 416.405 35.5502V13.2202C423.795 13.2202 430.525 12.7002 436.605 17.6002C440.275 20.5602 442.925 24.7102 444.165 29.2402C444.525 30.5402 444.765 31.8802 444.875 33.2302C445.395 39.3702 443.995 45.1402 440.085 49.9502Z" fill="#FFF5D9"/>
<path d="M508.135 0.990234V70.0602H552.715V57.9302H522.035V40.4202H547.125V28.0702H521.995V13.3202H552.715V0.990234H508.135Z" fill="#FFF5D9"/>
<path d="M574.835 66.0398H572.745L571.015 63.0698H569.845V66.0398H567.805V57.5498H571.765C572.845 57.5498 573.865 57.9298 574.425 58.9398C575.205 60.3698 574.665 62.3798 573.105 63.0298C573.725 64.1198 574.225 64.9498 574.845 66.0398H574.835ZM570.375 61.0798H570.845C571.335 61.0798 572.365 61.0798 572.365 60.2898C572.365 59.5598 571.335 59.5598 570.845 59.5598H570.375V61.0798Z" fill="#FFF5D9"/>
<path d="M570.964 69.0002C574.913 69.0002 578.114 65.799 578.114 61.8502C578.114 57.9014 574.913 54.7002 570.964 54.7002C567.016 54.7002 563.814 57.9014 563.814 61.8502C563.814 65.799 567.016 69.0002 570.964 69.0002Z" stroke="#FFF5D9" stroke-width="2.2" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_6_641">
<rect width="578.41" height="71.32" fill="white" transform="translate(0.794922)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,10 @@
<svg width="122" height="81" viewBox="0 0 122 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6_651)">
<path d="M89.3665 8.06803L121.5 0.35155L66.5111 0.320312L63.7089 7.69502L0.5 5.7032L54.0253 32.9925L36.1529 80.3203L89.3665 8.06803Z" fill="#FFF5D9"/>
</g>
<defs>
<clipPath id="clip0_6_651">
<rect width="121" height="80" fill="white" transform="translate(0.5 0.320312)"/>
</clipPath>
</defs>
</svg>

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,48 @@
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>
)
// const [isOpen, setIsOpen] = useState(false);
// // const [selectedOption, setSelectedOption] = useState(null);
// const dropdownRef = useRef(null);
// useEffect(() => {
// const handleClickOutside = (event) => {
// if (dropdownRef.current && !dropdownRef?.current?.contains(event.target)) {
// setIsOpen(false);
// }
// };
// document.addEventListener('mousedown', handleClickOutside);
// return () => {
// document.removeEventListener('mousedown', handleClickOutside);
// };
// }, [dropdownRef]);
// const toggleDropdown = () => setIsOpen(!isOpen);
// return (
// <div className="dropdown col" ref={dropdownRef}>
// <div className="dropdown-header row" onClick={toggleDropdown} style={displayStyle}>
// {display || <FaEllipsisH style={{ marginBottom: '-0.125em' }} />}
// </div>
// {isOpen && (
// <div className="dropdown-list col">
// {props.children}
// </div>
// )}
// </div>
// );
}

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="col 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,217 @@
@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;
padding: 2em;
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;
overflow-y: scroll;
}
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 */
}
/* @media (prefers-color-scheme: light) {
:root {
color: var(--bg-black);
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
color: var(--bg-black);
background-color: var(--bg-gray-medium);
}
button:hover {
background-color: var(--bg-gray-light);
}
input:focus-visible {
outline: -webkit-focus-ring-color auto 1px;
}
select {
border: 1px solid var(--bg-gray-solid);
border-radius: 0.25em;
}
input:focus-visible {
outline: var(--text-dark) auto 1px;
}
} */

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: "scroll", 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

25
kinode/packages/homepage/ui/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.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,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
2. Run `yarn run tc` to generate ABIs
3. Start a kinode locally on port 8080 (default)
3. 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
2. Run `yarn run tc` to generate ABIs
3. Run `yarn build` to generate the `./build` folder
4. 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

@ -0,0 +1,6 @@
#!/bin/bash
source ~/.nvm/nvm.sh
nvm use
npm install
npm run tc
npm run build

21429
kinode/packages/homepage/ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
{
"name": "register",
"version": "0.1.0",
"private": true,
"proxy": "http://127.0.0.1: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",
"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-modal": "^3.16.1",
"react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"typechain": "^8.3.1",
"typescript": "^4.9.5"
},
"scripts": {
"start": "react-scripts start",
"build": "GENERATE_SOURCEMAP=false react-scripts build",
"build:copy": "npm run build && npm run copy",
"copy": "mkdir -p ../../../src/register-ui/build && rm -rf ../../../src/register-ui/build/* && cp -r build/* ../../../src/register-ui/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,256 @@
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 OsHome 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 />
: <OsHome {...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

View File

@ -0,0 +1,18 @@
<svg width="580" height="72" viewBox="0 0 580 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6_641)">
<path d="M0.824922 1.07031L0.794922 70.0703H14.7949L14.8049 1.07031H0.824922Z" fill="#FFF5D9"/>
<path d="M16.5947 36.8803L41.2547 1.07031H58.2447L33.1647 36.8803L61.2447 70.0703H42.9947L16.5947 36.8803Z" fill="#FFF5D9"/>
<path d="M119.885 1.07031H105.765V70.0703H119.885V1.07031Z" fill="#FFF5D9"/>
<path d="M173.185 1.07031V70.0703H186.775V26.8303L224.045 70.0703H234.825V1.07031H221.325V45.6803L183.445 1.07031H173.185Z" fill="#FFF5D9"/>
<path d="M342.465 8.86C333.025 0.15 321.645 0 318.535 0C315.475 0 303.575 0.22 294.005 9.52C283.845 19.4 283.805 32.24 283.795 35.66C283.785 39.3 283.895 49.03 290.805 57.99C300.855 71.02 316.695 71.31 318.535 71.32C321.375 71.32 334.185 71 343.965 60.66C353.065 51.04 353.265 39.4 353.275 35.66C353.275 32.49 353.305 18.86 342.455 8.86H342.465ZM318.435 58.01C307.095 58.01 297.895 47.95 297.895 35.54C297.895 23.13 307.085 13.07 318.435 13.07C329.785 13.07 338.975 23.13 338.975 35.54C338.975 47.95 329.785 58.01 318.435 58.01Z" fill="#FFF5D9"/>
<path d="M450.495 12.0802C444.975 5.46023 437.135 0.990234 427.955 0.990234C417.555 0.990234 405.295 1.07023 402.295 1.07023V69.9802C405.285 69.9802 417.555 70.0602 427.955 70.0602C445.525 70.0602 458.445 53.4102 459.065 36.8602C459.395 28.0102 456.185 18.9002 450.495 12.0802ZM440.085 49.9502C436.895 53.8702 432.705 56.6902 427.665 57.5602C424.025 58.1902 420.095 57.8302 416.405 57.8302C416.405 50.4002 416.405 42.9802 416.405 35.5502V13.2202C423.795 13.2202 430.525 12.7002 436.605 17.6002C440.275 20.5602 442.925 24.7102 444.165 29.2402C444.525 30.5402 444.765 31.8802 444.875 33.2302C445.395 39.3702 443.995 45.1402 440.085 49.9502Z" fill="#FFF5D9"/>
<path d="M508.135 0.990234V70.0602H552.715V57.9302H522.035V40.4202H547.125V28.0702H521.995V13.3202H552.715V0.990234H508.135Z" fill="#FFF5D9"/>
<path d="M574.835 66.0398H572.745L571.015 63.0698H569.845V66.0398H567.805V57.5498H571.765C572.845 57.5498 573.865 57.9298 574.425 58.9398C575.205 60.3698 574.665 62.3798 573.105 63.0298C573.725 64.1198 574.225 64.9498 574.845 66.0398H574.835ZM570.375 61.0798H570.845C571.335 61.0798 572.365 61.0798 572.365 60.2898C572.365 59.5598 571.335 59.5598 570.845 59.5598H570.375V61.0798Z" fill="#FFF5D9"/>
<path d="M570.964 69.0002C574.913 69.0002 578.114 65.799 578.114 61.8502C578.114 57.9014 574.913 54.7002 570.964 54.7002C567.016 54.7002 563.814 57.9014 563.814 61.8502C563.814 65.799 567.016 69.0002 570.964 69.0002Z" stroke="#FFF5D9" stroke-width="2.2" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_6_641">
<rect width="578.41" height="71.32" fill="white" transform="translate(0.794922)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,10 @@
<svg width="122" height="81" viewBox="0 0 122 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6_651)">
<path d="M89.3665 8.06803L121.5 0.35155L66.5111 0.320312L63.7089 7.69502L0.5 5.7032L54.0253 32.9925L36.1529 80.3203L89.3665 8.06803Z" fill="#FFF5D9"/>
</g>
<defs>
<clipPath id="clip0_6_651">
<rect width="121" height="80" fill="white" transform="translate(0.5 0.320312)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,100 @@
import { useCallback } from 'react';
import ethLogo from '../assets/eth.png';
import sepoliaLogo from '../assets/sepolia.png';
import optimismLogo from '../assets/optimism.png';
import arbitrumLogo from '../assets/arbitrum.png';
import unknownLogo from '../assets/unknown.png';
import Jazzicon from "./Jazzicon";
import { hooks } from "../connectors/metamask";
import { KNS_REGISTRY_ADDRESSES } from '../constants/addresses';
const { useChainId } = hooks;
interface ChainInfoProps {
account: string;
networkName: string;
changeConnectedAccount: () => void;
changeToNodeChain: () => void;
}
function ChainInfo({
account,
networkName,
changeConnectedAccount,
changeToNodeChain,
}: ChainInfoProps) {
const chainId = useChainId();
const formatAddress = (address: string) => {
return `${address.substring(0, 6)}...${address.substring(
address.length - 4
)}`;
};
const generateNetworkIcon = (networkName: string) => {
switch (networkName) {
case "Ethereum":
return <img className="network-icon" src={ethLogo} alt={networkName} />;
case "Optimism":
return (
<img className="network-icon" src={optimismLogo} alt={networkName} />
);
case "Arbitrum":
return (
<img className="network-icon" src={arbitrumLogo} alt={networkName} />
);
case "Sepolia":
return (
<img
className="network-icon"
src={sepoliaLogo}
alt={networkName}
style={{ filter: "grayscale(100%)" }}
/>
);
default:
return (
<img
className="network-icon"
src={unknownLogo}
alt={networkName}
style={{ filter: "grayscale(100%)" }}
/>
);
}
};
const showKnsAddress = useCallback(() => {
window.alert(`The KNS Contract Address is: ${KNS_REGISTRY_ADDRESSES[chainId || ''] || 'unavailable on ' + networkName}`)
}, [chainId, networkName])
return (
<div style={{ display: "flex", gap: 10, maxWidth: 500 }}>
{/* TODO: prompt to change address */}
<button
onClick={changeConnectedAccount}
className="chain-button monospace"
>
<Jazzicon
address={account || ""}
diameter={24}
style={{ marginRight: "0.5em" }}
/>{" "}
{formatAddress(account || "")}
</button>
<button
onClick={changeToNodeChain}
className="chain-button"
style={{ maxWidth: "27%" }}
>
{generateNetworkIcon(networkName)} {networkName}
</button>
{/* TODO: show KNS contract ID in modal */}
<button onClick={showKnsAddress} className="chain-button" style={{ maxWidth: "27%" }}>
KNS Contract
</button>
</div>
);
}
export default ChainInfo;

View File

@ -0,0 +1,63 @@
import { useCallback } from 'react';
import { hooks, metaMask } from "../connectors/metamask";
import Modal from "react-modal"
import { SEPOLIA_OPT_HEX, SEPOLIA_OPT_INT } from '../constants/chainId';
const {
useChainId,
useIsActivating,
} = hooks;
type ConnectWalletProps = {
connectOpen: boolean,
closeConnect: () => void
}
export default function ConnectWallet({ connectOpen, closeConnect }: ConnectWalletProps) {
const isActivating = useIsActivating();
const connect = useCallback(async () => {
closeConnect()
await metaMask.activate().catch(() => { })
try {
const networkId = String(await (window.ethereum as any)?.request({ method: 'net_version' }).catch(() => '0x1'))
if (networkId !== SEPOLIA_OPT_HEX && networkId !== SEPOLIA_OPT_INT) {
const SEPOLIA_DETAILS = {
chainId: '0xaa36a7',
chainName: 'Sepolia Test Network',
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://sepolia-infura.brave.com/'], // Replace with Sepolia's RPC URL
blockExplorerUrls: ['https://sepolia.etherscan.io'] // Replace with Sepolia's block explorer URL
};
await (window.ethereum as any)?.request({
method: 'wallet_addEthereumChain',
params: [SEPOLIA_DETAILS]
})
}
} catch (err) {
console.error('FAILED TO ADD SEPOLIA:', err)
}
}, [closeConnect]);
return (
<Modal
isOpen={connectOpen}
onRequestClose={closeConnect}
className="connect-modal"
overlayClassName="overlay-modal"
>
<div className="connect-modal-content">
<button onClick={connect} disabled={isActivating} >
Connect to Wallet
</button>
</div>
</Modal>
);
}

View File

@ -0,0 +1,38 @@
interface Props {
direct: boolean;
setDirect: (direct: boolean) => void;
}
export default function DirectCheckbox({ direct, setDirect }: Props) {
return (
<div className="row">
<div style={{ position: "relative" }}>
<input
type="checkbox"
id="direct"
name="direct"
checked={direct}
onChange={(e) => setDirect(e.target.checked)}
autoFocus
/>
{direct && (
<span onClick={() => setDirect(false)} className="checkmark">
&#10003;
</span>
)}
</div>
<label htmlFor="direct" className="direct-node-message">
Register as a direct node. If you are unsure leave unchecked.
</label>
<div className="tooltip-container">
<div className="tooltip-button">&#8505;</div>
<div className="tooltip-content">
A direct node publishes its own networking information on-chain: IP,
port, so on. An indirect node relies on the service of routers, which
are themselves direct nodes. Only register a direct node if you know
what youre doing and have a public, static IP address.
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,119 @@
import React, { useEffect, useRef } from "react";
import { hooks } from "../connectors/metamask";
import { NameWrapper, ENSRegistry } from "../abis/types";
import isValidDomain from 'is-valid-domain'
import { hash } from 'eth-ens-namehash'
import { toAscii } from 'idna-uts46-hx'
global.Buffer = global.Buffer || require('buffer').Buffer;
const {
useChainId,
useProvider,
useAccount,
} = hooks;
type ClaimOsNameProps = {
name: string,
setName: React.Dispatch<React.SetStateAction<string>>
nameValidities: string[],
setNameValidities: React.Dispatch<React.SetStateAction<string[]>>,
nameWrapper: NameWrapper,
ensRegistry: ENSRegistry,
triggerNameCheck: boolean
}
function EnterEthName({
name,
setName,
nameValidities,
setNameValidities,
nameWrapper,
ensRegistry,
triggerNameCheck
}: ClaimOsNameProps) {
const userAddress = useAccount()
console.log("userAddress", userAddress)
const NAME_URL = "Name must be a valid URL without subdomains (A-Z, a-z, 0-9, and punycode)"
const NAME_NOT_OWNED = "Name is not owned by your wallet"
const NAME_INVALID_PUNY = "Unsupported punycode character"
const debouncer = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (debouncer.current)
clearTimeout(debouncer.current);
debouncer.current = setTimeout(async () => {
if (name.length == 0) return
let index: number
let validities = [...nameValidities]
let normalized: string
index = validities.indexOf(NAME_INVALID_PUNY)
try {
normalized = toAscii(name + ".eth")
if (index != -1) validities.splice(index, 1)
} catch (e) {
if (index == -1) validities.push(NAME_INVALID_PUNY)
}
// only check if name is valid punycode
if (normalized! !== undefined) {
index = validities.indexOf(NAME_URL)
if (name != "" && !isValidDomain(normalized)) {
if (index == -1) validities.push(NAME_URL)
} else if (index != -1) validities.splice(index, 1)
index = validities.indexOf(NAME_NOT_OWNED)
if (validities.length == 0 || index != -1) {
let owner = await ensRegistry.owner(hash(normalized))
if (owner == nameWrapper.address)
owner = await nameWrapper.ownerOf(hash(normalized))
if (owner != userAddress) {
if (index == -1) validities.push(NAME_NOT_OWNED)
} else {
validities.splice(index, 1)
}
}
}
setNameValidities(validities)
}, 500)
}, [name, triggerNameCheck])
const noDots = (e: any) => e.target.value.indexOf('.') == -1
&& setName(e.target.value)
return (
<div className="col" style={{ width: '100%' }}>
<div className="row" style={{ width: '100%' }}>
<input
value={name}
onChange={noDots}
type="text"
required
name="dot-os-name"
placeholder="e.g. myname"
/>
<div className="os">.eth</div>
</div>
{nameValidities.map((x, i) => <div key={i}><br /><span className="name-validity">{x}</span></div>)}
</div>
)
}
export default EnterEthName;

View File

@ -0,0 +1,111 @@
import React, { useEffect, useRef } from "react";
import { hooks } from "../connectors/metamask";
import { DotOsRegistrar } from "../abis/types";
import isValidDomain from 'is-valid-domain'
import { hash } from 'eth-ens-namehash'
import { toAscii } from 'idna-uts46-hx'
global.Buffer = global.Buffer || require('buffer').Buffer;
const {
useChainId,
useProvider,
} = hooks;
type ClaimOsNameProps = {
name: string,
setName: React.Dispatch<React.SetStateAction<string>>
nameValidities: string[],
setNameValidities: React.Dispatch<React.SetStateAction<string[]>>,
dotOs: DotOsRegistrar,
triggerNameCheck: boolean
}
function EnterOsName({
name,
setName,
nameValidities,
setNameValidities,
dotOs,
triggerNameCheck
}: ClaimOsNameProps) {
const NAME_URL = "Name must be a valid URL without subdomains (A-Z, a-z, 0-9, and punycode)"
const NAME_LENGTH = "Name must be 9 characters or more"
const NAME_CLAIMED = "Name is already claimed"
const NAME_INVALID_PUNY = "Unsupported punycode character"
const debouncer = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
if (debouncer.current)
clearTimeout(debouncer.current);
debouncer.current = setTimeout(async () => {
let index: number
let validities = [...nameValidities]
const len = [...name].length
index = validities.indexOf(NAME_LENGTH)
if (len < 9 && len != 0) {
if (index == -1) validities.push(NAME_LENGTH)
} else if (index != -1) validities.splice(index, 1)
let normalized: string
index = validities.indexOf(NAME_INVALID_PUNY)
try {
normalized = toAscii(name + ".os")
if (index != -1) validities.splice(index, 1)
} catch (e) {
if (index == -1) validities.push(NAME_INVALID_PUNY)
}
// only check if name is valid punycode
if (normalized! !== undefined) {
index = validities.indexOf(NAME_URL)
if (name != "" && !isValidDomain(normalized)) {
if (index == -1) validities.push(NAME_URL)
} else if (index != -1) validities.splice(index, 1)
index = validities.indexOf(NAME_CLAIMED)
if (validities.length == 0 || index != -1) {
try {
await dotOs.ownerOf(hash(normalized))
if (index == -1) validities.push(NAME_CLAIMED)
} catch (e) {
if (index != -1) validities.splice(index, 1)
}
}
}
setNameValidities(validities)
}, 500)
}, [name, triggerNameCheck])
const noDots = (e: any) => e.target.value.indexOf('.') == -1
&& setName(e.target.value)
return (
<div className="col" style={{ width: '100%' }}>
<div className="row" style={{ width: '100%' }}>
<input
value={name}
onChange={noDots}
type="text"
required
name="dot-os-name"
placeholder="e.g. myname"
/>
<div className="os">.os</div>
</div>
{nameValidities.map((x, i) => <div key={i}><br /><span className="name-validity">{x}</span></div>)}
</div>
)
}
export default EnterOsName;

View File

@ -0,0 +1,27 @@
import React, { useEffect, useRef } from 'react';
import jazzicon from 'jazzicon';
interface JazziconProps extends React.HTMLAttributes<HTMLDivElement> {
address: string;
diameter?: number;
}
const Jazzicon: React.FC<JazziconProps> = ({ address, diameter = 40, ...props }) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (address && ref.current) {
const seed = parseInt(address.slice(2, 10), 16); // Derive a seed from Ethereum address
const icon = jazzicon(diameter, seed);
// Clear the current icon
ref.current.innerHTML = '';
// Append the new icon
ref.current.appendChild(icon);
}
}, [address, diameter]);
return <div {...props} ref={ref} />;
};
export default Jazzicon;

View File

@ -0,0 +1,176 @@
import { useWeb3React } from "@web3-react/core";
import { hooks, metaMask } from "../connectors/metamask";
import { ReactNode, useCallback, useEffect, useState } from "react";
import Loader from "./Loader";
import { getNetworkName, setChain } from "../utils/chain";
import ChainInfo from "./ChainInfo";
import { OPTIMISM_OPT_HEX, SEPOLIA_OPT_HEX } from "../constants/chainId";
import sepoliaLogo from "../assets/sepolia.png";
import optimismLogo from "../assets/optimism.png";
const { useIsActivating, useChainId } = hooks;
type OsHeaderProps = {
header: ReactNode;
nameLogo?: boolean;
nodeChainId: string;
openConnect: () => void;
closeConnect: () => void;
hideConnect?: boolean;
};
function OsHeader({
header,
openConnect,
nameLogo = false,
closeConnect,
nodeChainId,
hideConnect = false,
}: OsHeaderProps) {
const { account, isActive } = useWeb3React();
const isActivating = useIsActivating();
const chainId = useChainId();
const [networkName, setNetworkName] = useState("");
useEffect(() => {
setNetworkName(getNetworkName((chainId || 1).toString()));
}, [chainId]);
const connectWallet = useCallback(async () => {
closeConnect();
await metaMask.activate().catch(() => {});
try {
setChain(nodeChainId);
} catch (error) {
console.error(error);
}
}, [closeConnect, nodeChainId]);
const changeToNodeChain = useCallback(async () => {
// If correct ndetwork is set, just say that
if (chainId) {
const hexChainId = "0x" + chainId.toString(16);
if (hexChainId === nodeChainId) {
return alert(
`You are already connected to ${getNetworkName(chainId.toString())}`
);
}
try {
setChain(nodeChainId);
} catch (error) {
console.error(error);
}
}
}, [chainId, nodeChainId]);
const changeConnectedAccount = useCallback(async () => {
alert("You can change your connected account in your wallet.");
}, []);
// <div style={{ textAlign: 'center', lineHeight: 1.5 }}> Connected as {account?.slice(0,6) + '...' + account?.slice(account.length - 6)}</div>
return (
<>
<div id="signup-form-header" className="col">
{(nodeChainId === SEPOLIA_OPT_HEX ||
nodeChainId === OPTIMISM_OPT_HEX) && (
<div
className="tooltip-container"
style={{ position: "absolute", top: 32, right: 32 }}
>
<div className="tooltip-button chain">
{nodeChainId === SEPOLIA_OPT_HEX ? (
<img alt="sepolia" src={sepoliaLogo} className="sepolia" />
) : nodeChainId === OPTIMISM_OPT_HEX ? (
<img alt="optimism" src={optimismLogo} />
) : null}
</div>
<div className="tooltip-content left">
{nodeChainId === SEPOLIA_OPT_HEX ? (
<div
style={{
textAlign: "center",
lineHeight: "1.5em",
maxWidth: 450,
}}
>
Your Kinode is currently pointed at Sepolia. To point at
Optimism, boot without the "--testnet" flag.
</div>
) : nodeChainId === OPTIMISM_OPT_HEX ? (
<div
style={{
textAlign: "center",
lineHeight: "1.5em",
maxWidth: 450,
}}
>
Your Kinode is currently pointed at Optimism. To point at
Sepolia, boot with the "--testnet" flag.
</div>
) : null}
</div>
</div>
)}
<div className="col" style={{ gap: 16, marginBottom: 32 }}>
{header}
</div>
{!hideConnect && (
<div
style={{
minWidth: "50vw",
width: 400,
justifyContent: "center",
display: "flex",
}}
>
{isActive && account ? (
<ChainInfo
account={account}
networkName={networkName}
changeToNodeChain={changeToNodeChain}
changeConnectedAccount={changeConnectedAccount}
/>
) : (
<div className="col" style={{ gap: 32, marginTop: 16 }}>
<h5 style={{ textAlign: "center", lineHeight: "1.5em" }}>
You must connect to a browser wallet to continue
</h5>
{/* <div style={{ textAlign: 'center', lineHeight: '1.5em' }}>We recommend <a href="https://metamask.io/download.html" target="_blank" rel="noreferrer">MetaMask</a></div> */}
{isActivating ? (
<Loader msg="Approve connection in your wallet" />
) : (
<button onClick={connectWallet}> Connect Wallet </button>
)}
{nodeChainId === SEPOLIA_OPT_HEX && (
<h5
style={{
textAlign: "center",
lineHeight: "1.5em",
maxWidth: 450,
}}
>
Kinode is currently on the Sepolia Testnet, if you need
testnet ETH, you can get some from the{" "}
<a
href="https://sepoliafaucet.com/"
target="_blank"
rel="noreferrer"
>
Sepolia Faucet
</a>
</h5>
)}
</div>
)}
</div>
)}
</div>
</>
);
}
export default OsHeader;

View File

@ -0,0 +1,12 @@
type LoaderProps = {
msg: string
}
export default function Loader({ msg } : LoaderProps) {
return (
<div id="loading" className="col">
<h3>{msg}</h3>
<div id="loader"> <div/> <div/> <div/> <div/> </div>
</div>
)
}

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,34 @@
import { ChainId } from './chainId'
type AddressMap = { [chainId: string]: string }
export const KNS_REGISTRY_ADDRESSES: AddressMap = {
[ChainId.SEPOLIA]: '0x3807fBD692Aa5c96F1D8D7c59a1346a885F40B1C',
[ChainId.OPTIMISM]: '0xca5b5811c0C40aAB3295f932b1B5112Eb7bb4bD6',
}
export const DOT_OS_ADDRESSES: AddressMap = {
[ChainId.SEPOLIA]: '0xC5a939923E0B336642024b479502E039338bEd00',
[ChainId.OPTIMISM]: '0x66929F55Ea1E38591f9430E5013C92cdC01F6cAd',
}
export const NAMEWRAPPER_ADDRESSES: AddressMap = {
[ChainId.SEPOLIA]: '0x0635513f179D50A207757E05759CbD106d7dFcE8',
[ChainId.MAINNET]: '0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401',
}
export const ENS_REGISTRY_ADDRESSES: AddressMap = {
[ChainId.SEPOLIA]: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
[ChainId.MAINNET]: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
}
export const KNS_ENS_ENTRY_ADDRESSES: AddressMap = {
[ChainId.SEPOLIA]: '0xD4583DFd73B382B7e3230aa29Be774C1843FB7d2',
[ChainId.GOERLI]: '0xD4583DFd73B382B7e3230aa29Be774C1843FB7d2',
[ChainId.MAINNET]: '0xa1F47fBBa93574DB4a049C1c5bA03471A21EE01D',
}
export const KNS_ENS_EXIT_ADDRESSES: AddressMap = {
[ChainId.SEPOLIA]: '0x528bA1BA3186d8CABD2c4E8758a98fAf64eD8Af0',
[ChainId.OPTIMISM]: '0x0b35664aB5950cE92bce7222be165BB575D9b7c5',
}

View File

@ -0,0 +1,13 @@
export enum ChainId {
LOCAL = 1337,
MAINNET = 1,
SEPOLIA = 11155111,
OPTIMISM = 10,
OPTIMISM_GOERLI = 420,
GOERLI = 5,
}
export const SEPOLIA_OPT_HEX = '0xaa36a7';
export const OPTIMISM_OPT_HEX = '0xa';
export const MAINNET_OPT_HEX = '0x1';
export const SEPOLIA_OPT_INT = '11155111';

View File

@ -0,0 +1,2 @@
export const KEY_WRONG_NET_KEY = "Keyfile does not match public key";
export const KEY_WRONG_IP = "IP Address does not match records";

View File

@ -0,0 +1,7 @@
declare module 'eth-ens-namehash' {
export function hash(name: string): string;
export function normalize(name: string): string;
}
declare module 'idna-uts46-hx' {
export function toAscii(domain: string, options?: object): string;
}

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