diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1e02be7
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..c4eae26
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+WALLET_CONNECT_PROJECT_ID=b7b3d81af86feb2af54461f26b665ee4
+SQUID_API_URL=https://squid.subsquid.io/subsquid-network-indexer/v/v7/graphql
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..18115e8
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,4 @@
+node_modules
+.yarn
+build
+dist
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..ee0bbf9
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,83 @@
+module.exports = {
+ root: true,
+ env: {
+ browser: true,
+ es2020: true,
+ },
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ plugins: ['@typescript-eslint', 'react', 'prettier'],
+ extends: [
+ 'plugin:import/errors',
+ 'plugin:import/warnings',
+ 'plugin:import/typescript',
+ 'plugin:@typescript-eslint/eslint-recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react/recommended',
+ 'prettier',
+ 'plugin:react-hooks/recommended',
+ ],
+ settings: {
+ react: {
+ version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
+ },
+ 'import/resolver': {
+ node: {
+ extensions: ['.js', '.jsx', '.ts', '.tsx'],
+ },
+ },
+ },
+ rules: {
+ quotes: ['error', 'single', { allowTemplateLiterals: true }],
+ 'prettier/prettier': 'error',
+ 'react/no-unescaped-entities': 'off',
+ 'react/jsx-curly-brace-presence': ['error', { props: "never", children: "never" }],
+ '@typescript-eslint/no-empty-interface': 'off',
+ '@typescript-eslint/no-unused-vars': 'warn',
+ '@typescript-eslint/no-unsafe-declaration-merging': 'off',
+ 'import/order': [
+ 'error',
+ {
+ pathGroups: [
+ {
+ pattern: 'react',
+ group: 'external',
+ position: 'before',
+ },
+ {
+ pattern:
+ '{@{api,apps,components,hooks,contexts,layouts,pages,icons,models,network}/**,@apps}',
+ group: 'internal',
+ position: 'after',
+ },
+ ],
+ 'newlines-between': 'always',
+ alphabetize: {
+ /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */
+ order: 'asc',
+ caseInsensitive: true /* ignore case. Options: [true, false] */,
+ },
+ pathGroupsExcludedImportTypes: ['builtin'],
+ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
+ },
+ ],
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ 'import/no-unresolved': 'off',
+ 'import/namespace': 'off',
+ 'import/no-duplicates': 'off',
+ 'no-console': 'warn',
+ 'guard-for-in': 'off',
+ '@typescript-eslint/no-explicit-any': 'warn',
+ 'react/prop-types': 'off',
+ 'react/display-name': 'off',
+ 'import/default': 'off',
+ 'import/no-named-as-default': 'off',
+ 'import/no-named-as-default-member': 'off',
+ },
+};
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..d25859a
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,96 @@
+name: build
+
+on:
+ workflow_dispatch: {}
+ push:
+ branches: [ main, develop ]
+
+env:
+ PROJECT_ID: ${{ secrets.GCP_PROJECT }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: cancel previous runs
+ uses: styfle/cancel-workflow-action@0.5.0
+ with:
+ access_token: ${{ github.token }}
+
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Install Yarn
+ run: corepack enable
+
+ # Yarn dependencies cannot be cached until yarn is installed
+ # WORKAROUND: https://github.com/actions/setup-node/issues/531
+ - name: Extract cached dependencies
+ uses: actions/setup-node@v4
+ with:
+ cache: yarn
+
+ - name: env
+ id: env
+ run: |
+ echo "::set-output name=tag::$(git rev-parse --short HEAD)"
+ if [ "$REF" = "refs/heads/main" ]; then
+ echo "::set-output name=app_env::prod"
+ echo "::set-output name=wc_project_id::475eff0658d0f3300ca18971418d261b"
+ echo "::set-output name=enable_demo_features::false"
+ echo "::set-output name=squid_api_url::https://squid.subsquid.io/subsquid-network-indexer/v/v7/graphql"
+ echo "::set-output name=app_domain::app.subsquid.io"
+ else
+ echo "::set-output name=app_env::dev"
+ echo "::set-output name=wc_project_id::ec2facac9eaaca7cc0584baadc935c01"
+ echo "::set-output name=enable_demo_features::true"
+ echo "::set-output name=squid_api_url::https://squid.subsquid.io/subsquid-network-indexer/v/v7/graphql"
+ echo "::set-output name=app_domain::app.devsquid.net"
+ fi
+ env:
+ REF: ${{ github.ref }}
+
+ - run: yarn install --immutable
+
+ - run: yarn build
+ env:
+ APP_ENV: ${{ steps.env.outputs.app_env }}
+ APP_VERSION: ${{ steps.env.outputs.tag }}
+ WALLET_CONNECT_PROJECT_ID: ${{ steps.env.outputs.wc_project_id }}
+ ENABLE_DEMO_FEATURES: ${{ steps.env.outputs.enable_demo_features }}
+ NETWORK: ${{ steps.env.outputs.network }}
+ SQUID_API_URL: ${{ steps.env.outputs.squid_api_url }}
+
+
+ # Build and push images to Google Container Registry
+ - name: Build image
+ run: docker build --progress=plain -t "gcr.io/${PROJECT_ID}/subsquid-network-app-${APP_ENV}:${TAG}" -t "gcr.io/${PROJECT_ID}/subsquid-network-app-${APP_ENV}:latest" .
+ env:
+ APP_ENV: ${{ steps.env.outputs.app_env }}
+ TAG: ${{ steps.env.outputs.tag }}
+
+ - id: auth
+ uses: google-github-actions/auth@v0.4.0
+ with:
+ credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}
+
+ - name: Set up Cloud SDK
+ uses: google-github-actions/setup-gcloud@v0.2.1
+
+ # steps for push images to gcr
+ - name: 'gcloud cli --> docker credential helper'
+ run: gcloud auth configure-docker -q
+
+ - name: Push image
+ run: |
+ docker push "gcr.io/${PROJECT_ID}/subsquid-network-app-${APP_ENV}:${TAG}"
+ docker push "gcr.io/${PROJECT_ID}/subsquid-network-app-${APP_ENV}:latest"
+ env:
+ APP_ENV: ${{ steps.env.outputs.app_env }}
+ TAG: ${{ steps.env.outputs.tag }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72a9eb8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,60 @@
+# yarn
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+.git
+
+# dependencies
+node_modules
+.pnp
+.pnp.*
+
+# testing
+coverage
+
+# production
+build
+dist
+storybook
+.cache
+
+# Optional eslint cache
+.eslintcache
+
+# IDE
+.idea
+/.idea
+**/.idea
+.vscode
+
+# envs
+.env
+.env.local
+.env.dev
+.env.prod
+.env.test
+.env.development
+.env.production
+.env.development.local
+.env.production.local
+.env.test.local
+
+# logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# typescript
+*.tsbuildinfo
+
+# misc
+.DS_Store
+*.pem
+secrets.yaml
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..5a182ef
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+yarn lint-staged
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 0000000..c4fd425
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,6 @@
+export default {
+ singleQuote: true,
+ trailingComma: "all",
+ printWidth: 100,
+ arrowParens: "avoid"
+}
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..3186f3f
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1 @@
+nodeLinker: node-modules
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0f0c37e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx
+
+COPY nginx/default.conf /etc/nginx/conf.d/default.conf
+COPY build/ /usr/share/nginx/html/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fce09d9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# Subsquid Cloud UI
+
+## Run
+
+1) Copy `.env.example` to `.env`
+2) ```yarn install```
+3) ```yarn start```
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..3171a45
--- /dev/null
+++ b/index.html
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+ Subsquid Network App
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nginx/default.conf b/nginx/default.conf
new file mode 100644
index 0000000..ae40087
--- /dev/null
+++ b/nginx/default.conf
@@ -0,0 +1,32 @@
+server {
+ listen 80 default_server;
+ server_name localhost;
+
+ access_log /var/log/nginx/host.access.log main;
+
+ # remove header 'Server'
+ server_tokens off;
+
+ # single page application
+ # on request return file or index.html if file not found
+ location = /index.html {
+ expires -1;
+ root /usr/share/nginx/html;
+ try_files /index.html =404;
+ }
+
+ location / {
+ expires 1y; # sets cache-control
+ root /usr/share/nginx/html;
+ try_files $uri /index.html;
+ }
+
+ # redirect server error pages to the static page /50x.html
+ #
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ # Set this location as internal so it cannot be requested externally (will be 404, which returns to /index.html)
+ internal;
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4417ad5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "@subsquid/subsquid-web",
+ "version": "0.0.1",
+ "type": "module",
+ "private": true,
+ "exports": "./src/index.tsx",
+ "scripts": {
+ "analyze": "npx vite-bundle-visualizer",
+ "build": "vite build --outDir build",
+ "lint": "eslint \"src/**/*.{js,ts,tsx}\" --fix",
+ "tsc": "tsc --noEmit",
+ "start": "vite serve --port 3005 --host 127.0.0.1",
+ "preview": "vite preview --outDir build --host 127.0.0.1 --port 3005",
+ "upg": "yarn upgrade-interactive",
+ "prepare": "husky install",
+ "codegen": "graphql-codegen --config src/api/subsquid-network-squid/graphql.config.js"
+ },
+ "lint-staged": {
+ "src/**/*.{js,jsx,ts,tsx}": "eslint --cache --fix"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.0",
+ "@mui/icons-material": "^5.15.14",
+ "@mui/lab": "^5.0.0-alpha.169",
+ "@mui/material": "^5.15.14",
+ "@rainbow-me/rainbowkit": "^1.3.6",
+ "@sentry/react": "^7.108.0",
+ "@tanstack/react-query": "^5.28.9",
+ "@types/ms": "^0.7.34",
+ "axios": "^1.6.8",
+ "base58-universal": "^2.0.0",
+ "bs58": "^5.0.0",
+ "buffer": "^6.0.3",
+ "classnames": "^2.5.1",
+ "country-list": "^2.3.0",
+ "date-fns": "^3.6.0",
+ "decimal.js": "^10.4.3",
+ "ethers": "^6.11.1",
+ "formik": "^2.4.5",
+ "graphql": "^16.8.1",
+ "lodash-es": "^4.17.21",
+ "material-ui-popup-state": "^5.1.0",
+ "notistack": "^3.0.1",
+ "pretty-bytes": "^6.1.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.22.3",
+ "react-scroll": "^1.9.0",
+ "react-syntax-highlighter": "^15.5.0",
+ "recharts": "^2.12.3",
+ "use-element-position": "^1.0.13",
+ "use-local-storage-state": "^19.2.0",
+ "viem": "^1.21.1",
+ "wagmi": "^1.4.12",
+ "yup": "^1.4.0"
+ },
+ "devDependencies": {
+ "@graphql-codegen/add": "^5.0.2",
+ "@graphql-codegen/cli": "^5.0.2",
+ "@graphql-codegen/typescript": "^4.0.6",
+ "@graphql-codegen/typescript-operations": "^4.2.0",
+ "@graphql-codegen/typescript-react-query": "^6.1.0",
+ "@types/jest": "^29.5.12",
+ "@types/lodash-es": "^4.17.12",
+ "@types/pretty-bytes": "^5.2.0",
+ "@types/qs": "^6.9.14",
+ "@types/query-string": "^6.3.0",
+ "@types/react-dom": "^18.2.22",
+ "@types/react-scroll": "^1.8.10",
+ "@types/react-select": "5.0.1",
+ "@types/react-syntax-highlighter": "^15.5.11",
+ "@typescript-eslint/eslint-plugin": "^7.4.0",
+ "@typescript-eslint/parser": "^7.4.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "dotenv": "^16.4.5",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-jest": "^27.9.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-native": "^4.1.0",
+ "eslint-plugin-sort-keys-fix": "^1.1.2",
+ "husky": "^9.0.11",
+ "lint-staged": "^15.2.2",
+ "prettier": "^3.2.5",
+ "typescript": "^5.4.3",
+ "vite": "^5.2.6",
+ "vite-tsconfig-paths": "^4.3.2"
+ },
+ "packageManager": "yarn@4.1.1"
+}
diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png
new file mode 100644
index 0000000..c5fc78c
Binary files /dev/null and b/public/favicon-96x96.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..12a21c7
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000..445c6ef
Binary files /dev/null and b/public/logo.png differ
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..84d10e5
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+
+import { CssBaseline, ThemeProvider } from '@mui/material';
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { QueryClientProvider } from '@tanstack/react-query';
+import { SnackbarProvider } from 'notistack';
+import { BrowserRouter } from 'react-router-dom';
+import { WagmiConfig } from 'wagmi';
+
+import { queryClient } from '@api/client';
+import { Alert } from '@components/Alert';
+import { chains, wagmiConfig } from '@network/config';
+
+import { AppRoutes } from './AppRoutes';
+import { useCreateTheme, useThemeState } from './theme';
+
+function App() {
+ const [themeName] = useThemeState();
+ const theme = useCreateTheme(themeName);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx
new file mode 100644
index 0000000..e9141e9
--- /dev/null
+++ b/src/AppRoutes.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import { Navigate, Route, Routes } from 'react-router-dom';
+
+import { NetworkLayout } from '@layouts/NetworkLayout';
+import { AddNewGateway } from '@pages/GatewayPage/AddNewGateway.tsx';
+import { Gateway } from '@pages/GatewayPage/Gateway.tsx';
+import NetworkDashboardPage from '@pages/NetworkDashboard/NetworkDashboardPage.tsx';
+import { MyAssets } from '@pages/Profile/MyAssets.tsx';
+import { MyDelegations } from '@pages/Profile/MyDelegations.tsx';
+import { MyGateways } from '@pages/Profile/MyGateways.tsx';
+import { MyWorkers } from '@pages/Profile/MyWorkers.tsx';
+import { ProfilePage } from '@pages/Profile/ProfilePage.tsx';
+import { AddNewWorker } from '@pages/WorkersPage/AddNewWorker.tsx';
+import Worker from '@pages/WorkersPage/Worker.tsx';
+import { WorkerEdit } from '@pages/WorkersPage/WorkerEdit.tsx';
+import { WorkersPage } from '@pages/WorkersPage/WorkersPage.tsx';
+
+import { hideLoader } from './index.tsx';
+
+export const AppRoutes = () => {
+ hideLoader(0);
+
+ return (
+
+ } path="/workers">
+ } index />
+ } path=":peerId" />
+
+ } path="/network-dashboard">
+ } />
+
+ } path="/profile">
+ }>
+ } path="assets" />
+ } path="delegations" />
+
+ } path="workers/add" />
+ } path="workers" />
+ } path="workers/:peerId/edit" />
+ } path="workers/:peerId" />
+
+ } path="gateways/add" />
+ } path="gateways" />
+ } path="gateways/:peerId" />
+
+ } index />
+
+
+ } path="*" />
+
+ );
+};
diff --git a/src/api/client.ts b/src/api/client.ts
new file mode 100644
index 0000000..83fd3ca
--- /dev/null
+++ b/src/api/client.ts
@@ -0,0 +1,12 @@
+import { QueryClient } from '@tanstack/react-query';
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ networkMode: 'always',
+ },
+ mutations: {
+ networkMode: 'always',
+ },
+ },
+});
diff --git a/src/api/contracts/claim.ts b/src/api/contracts/claim.ts
new file mode 100644
index 0000000..974323e
--- /dev/null
+++ b/src/api/contracts/claim.ts
@@ -0,0 +1,110 @@
+import { useState } from 'react';
+
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { REWARD_TREASURY_CONTRACT_ABI } from '@api/contracts/reaward-treasury.abi';
+import { errorMessage, TxResult, WriteContractRes } from '@api/contracts/utils';
+import { VESTING_CONTRACT_ABI } from '@api/contracts/vesting.abi';
+import { AccountType, SourceWallet } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount.ts';
+import { useContracts } from '@network/useContracts.ts';
+
+export type ClaimRequest = {
+ wallet: SourceWallet;
+};
+
+function useClaimFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.REWARD_TREASURY,
+ abi: REWARD_TREASURY_CONTRACT_ABI,
+ functionName: 'claimFor',
+ });
+
+ return async ({ wallet }: ClaimRequest): Promise => {
+ try {
+ return {
+ tx: await writeAsync({
+ args: [contracts.REWARD_DISTRIBUTION, wallet.id as `0x${string}`],
+ }),
+ };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+function useClaimFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ wallet }: ClaimRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: REWARD_TREASURY_CONTRACT_ABI,
+ functionName: 'claimFor',
+ args: [contracts.REWARD_DISTRIBUTION, wallet.id as `0x${string}`],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: wallet.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.REWARD_TREASURY, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useClaim() {
+ const client = usePublicClient();
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const claimFromWallet = useClaimFromWallet();
+ const claimFromVestingContract = useClaimFromVestingContract();
+
+ const claim = async ({ wallet }: ClaimRequest): Promise => {
+ setLoading(true);
+
+ const res =
+ wallet.type === AccountType.User
+ ? await claimFromWallet({ wallet })
+ : await claimFromVestingContract({ wallet });
+
+ if (!res.tx) {
+ setLoading(false);
+ setError(res.error);
+
+ return { success: false, failedReason: res.error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: res.tx.hash });
+ setWaitHeight(receipt.blockNumber, []);
+
+ setLoading(false);
+
+ return { success: true };
+ };
+
+ return {
+ claim,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/api/contracts/consts.ts b/src/api/contracts/consts.ts
new file mode 100644
index 0000000..2c39e92
--- /dev/null
+++ b/src/api/contracts/consts.ts
@@ -0,0 +1,2 @@
+export const SQD_TOKEN = 'tSQD';
+export const SQD_DECIMALS = 18;
diff --git a/src/api/contracts/gateway-registration/GatewayMetadata.ts b/src/api/contracts/gateway-registration/GatewayMetadata.ts
new file mode 100644
index 0000000..16777c1
--- /dev/null
+++ b/src/api/contracts/gateway-registration/GatewayMetadata.ts
@@ -0,0 +1,29 @@
+import isEmpty from 'lodash-es/isEmpty';
+import pickBy from 'lodash-es/pickBy';
+
+export interface GetawayMetadata {
+ name: string;
+ email?: string;
+ description?: string;
+ website?: string;
+ endpointUrl?: string;
+}
+
+export function encodeGatewayMetadata(req: GetawayMetadata) {
+ const md = pickBy(
+ {
+ name: req.name,
+ website: req.website,
+ description: req.description,
+ email: req.email,
+ endpointUrl: req.endpointUrl,
+ },
+ Boolean,
+ );
+
+ return !isEmpty(md) ? JSON.stringify(md) : '';
+}
+
+export function decodeGatewayMetadata(req: string) {
+ return JSON.parse(req);
+}
diff --git a/src/api/contracts/gateway-registration/GatewayRegistration.abi.ts b/src/api/contracts/gateway-registration/GatewayRegistration.abi.ts
new file mode 100644
index 0000000..aed2f65
--- /dev/null
+++ b/src/api/contracts/gateway-registration/GatewayRegistration.abi.ts
@@ -0,0 +1,81 @@
+export const GATEWAY_REGISTRATION_CONTRACT_ABI = [
+ {
+ type: 'function',
+ name: 'register',
+ inputs: [
+ {
+ name: 'peerId',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ {
+ name: 'metadata',
+ type: 'string',
+ internalType: 'string',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'setMetadata',
+ inputs: [
+ {
+ name: 'peerId',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ {
+ name: 'metadata',
+ type: 'string',
+ internalType: 'string',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'stake',
+ inputs: [
+ {
+ name: 'amount',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'durationBlocks',
+ type: 'uint128',
+ internalType: 'uint128',
+ },
+ {
+ name: 'withAutoExtension',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'unregister',
+ inputs: [
+ {
+ name: 'peerId',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'unstake',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+] as const;
diff --git a/src/api/contracts/gateway-registration/useRegisterGateway.ts b/src/api/contracts/gateway-registration/useRegisterGateway.ts
new file mode 100644
index 0000000..6fe8e26
--- /dev/null
+++ b/src/api/contracts/gateway-registration/useRegisterGateway.ts
@@ -0,0 +1,115 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { AccountType } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { errorMessage, peerIdToHex, TxResult, WriteContractRes } from '../utils';
+import { VESTING_CONTRACT_ABI } from '../vesting.abi';
+
+import { encodeGatewayMetadata, GetawayMetadata } from './GatewayMetadata';
+import { GATEWAY_REGISTRATION_CONTRACT_ABI } from './GatewayRegistration.abi';
+
+export interface RegisterGatewayRequest extends GetawayMetadata {
+ peerId: string;
+ source: {
+ id: string;
+ type: AccountType;
+ };
+}
+
+function useRegisterGatewayFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.GATEWAY_REGISTRATION,
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'register',
+ });
+
+ return async ({ peerId, ...rest }: RegisterGatewayRequest): Promise => {
+ logger.debug(`registering gateway via worker contract...`);
+
+ try {
+ return { tx: await writeAsync({ args: [peerIdToHex(peerId), encodeGatewayMetadata(rest)] }) };
+ } catch (e: unknown) {
+ return {
+ error: errorMessage(e),
+ };
+ }
+ };
+}
+
+function useRegisterGatewayFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ peerId, source, ...rest }: RegisterGatewayRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'register',
+ args: [peerIdToHex(peerId), encodeGatewayMetadata(rest)], // encodeMetadata(rest)
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: source.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.GATEWAY_REGISTRATION, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useRegisterGateway() {
+ const client = usePublicClient();
+ const { address } = useAccount();
+ const [error, setError] = useState(null);
+ const [isLoading, setLoading] = useState(false);
+
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const registerGatewayFromWallet = useRegisterGatewayFromWallet();
+ const registerGatewayFromVestingContract = useRegisterGatewayFromVestingContract();
+
+ const registerGateway = async (req: RegisterGatewayRequest): Promise => {
+ setLoading(true);
+
+ const { tx, error } =
+ req.source.type === AccountType.User
+ ? await registerGatewayFromWallet(req)
+ : await registerGatewayFromVestingContract(req);
+
+ if (!tx) {
+ logger.debug(`registering gateway failed ${error}`);
+ setLoading(false);
+ setError(error);
+ return { success: false, failedReason: error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: tx.hash });
+ setWaitHeight(receipt.blockNumber, ['myGateways', { address }]);
+ setLoading(false);
+ setError(null);
+
+ return { success: true };
+ };
+
+ return { registerGateway, isLoading, error };
+}
diff --git a/src/api/contracts/gateway-registration/useStakeGateway.ts b/src/api/contracts/gateway-registration/useStakeGateway.ts
new file mode 100644
index 0000000..67d01f0
--- /dev/null
+++ b/src/api/contracts/gateway-registration/useStakeGateway.ts
@@ -0,0 +1,153 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import Decimal from 'decimal.js';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { AccountType, SourceWallet } from '@api/subsquid-network-squid';
+import { BlockchainGateway } from '@api/subsquid-network-squid/gateways-graphql';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { useApproveSqd } from '../sqd';
+import { errorMessage, isApproveRequiredError, toSqd, TxResult, WriteContractRes } from '../utils';
+import { VESTING_CONTRACT_ABI } from '../vesting.abi';
+
+import { GATEWAY_REGISTRATION_CONTRACT_ABI } from './GatewayRegistration.abi';
+
+type StakeGatewayRequest = {
+ gateway: BlockchainGateway;
+ amount: number;
+ durationBlocks: number;
+ autoExtension: boolean;
+ wallet: SourceWallet;
+};
+
+function useStakeFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.GATEWAY_REGISTRATION,
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'stake',
+ });
+
+ const [approveSqd] = useApproveSqd();
+
+ const tryCallContract = async ({
+ autoExtension,
+ amount,
+ durationBlocks,
+ }: StakeGatewayRequest) => {
+ try {
+ return {
+ tx: await writeAsync({
+ args: [toSqd(amount), BigInt(durationBlocks), autoExtension],
+ }),
+ };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+
+ return async (req: StakeGatewayRequest): Promise => {
+ logger.debug(`stake to gateway via worker contract...`);
+
+ const res = await tryCallContract(req);
+ // Try to approve SQD
+ if (isApproveRequiredError(res.error)) {
+ const approveRes = await approveSqd({
+ contractAddress: contracts.GATEWAY_REGISTRATION,
+ amount: new Decimal(toSqd(req.amount).toString()),
+ });
+ if (!approveRes.success) {
+ return { error: approveRes.failedReason };
+ }
+
+ logger.debug(`approved SQD successfully, now trying to register one more time...`);
+
+ return tryCallContract(req);
+ }
+
+ return res;
+ };
+}
+
+function useStakeFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({
+ amount,
+ wallet,
+ autoExtension,
+ durationBlocks,
+ }: StakeGatewayRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'stake',
+ args: [toSqd(amount), BigInt(durationBlocks), autoExtension],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: wallet.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.GATEWAY_REGISTRATION, data, toSqd(amount)],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useStakeGateway() {
+ const client = usePublicClient();
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const stakeFromWallet = useStakeFromWallet();
+ const stakeFromVestingContract = useStakeFromVestingContract();
+
+ const stakeToGateway = async (req: StakeGatewayRequest): Promise => {
+ setLoading(true);
+
+ const res =
+ req.wallet.type === AccountType.User
+ ? await stakeFromWallet(req)
+ : await stakeFromVestingContract(req);
+
+ if (!res.tx) {
+ setLoading(false);
+ setError(res.error);
+
+ return { success: false, failedReason: res.error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: res.tx.hash });
+ setWaitHeight(receipt.blockNumber, []);
+
+ setLoading(false);
+
+ return { success: true };
+ };
+
+ return {
+ stakeToGateway,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/api/contracts/gateway-registration/useUnregisterGateway.ts b/src/api/contracts/gateway-registration/useUnregisterGateway.ts
new file mode 100644
index 0000000..b6dfc94
--- /dev/null
+++ b/src/api/contracts/gateway-registration/useUnregisterGateway.ts
@@ -0,0 +1,111 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { AccountType } from '@api/subsquid-network-squid';
+import { BlockchainGateway } from '@api/subsquid-network-squid/gateways-graphql';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { errorMessage, peerIdToHex, TxResult, WriteContractRes } from '../utils';
+import { VESTING_CONTRACT_ABI } from '../vesting.abi';
+
+import { GATEWAY_REGISTRATION_CONTRACT_ABI } from './GatewayRegistration.abi';
+
+export interface UnregisterGatewayRequest {
+ gateway: BlockchainGateway;
+}
+
+function useUnregisterGatewayFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.GATEWAY_REGISTRATION,
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'unregister',
+ });
+
+ return async ({ gateway }: UnregisterGatewayRequest): Promise => {
+ logger.debug(`unregistering gateway via worker contract...`);
+
+ try {
+ return { tx: await writeAsync({ args: [peerIdToHex(gateway.id)] }) };
+ } catch (e: unknown) {
+ return {
+ error: errorMessage(e),
+ };
+ }
+ };
+}
+
+function useUnregisterGatewayFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ gateway }: UnregisterGatewayRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'unregister',
+ args: [peerIdToHex(gateway.id)],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: gateway.owner.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.GATEWAY_REGISTRATION, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useUnregisterGateway() {
+ const client = usePublicClient();
+ const { address } = useAccount();
+ const [error, setError] = useState(null);
+ const [isLoading, setLoading] = useState(false);
+
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const unregisterGatewayFromWallet = useUnregisterGatewayFromWallet();
+ const unregisterGatewayFromVestingContract = useUnregisterGatewayFromVestingContract();
+
+ const unregisterGateway = async (req: UnregisterGatewayRequest): Promise => {
+ setLoading(true);
+
+ const { tx, error } =
+ req.gateway.owner.type === AccountType.User
+ ? await unregisterGatewayFromWallet(req)
+ : await unregisterGatewayFromVestingContract(req);
+
+ if (!tx) {
+ logger.debug(`unregistering gateway failed ${error}`);
+ setLoading(false);
+ setError(error);
+ return { success: false, failedReason: error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: tx.hash });
+ setWaitHeight(receipt.blockNumber, ['myGateways', { address }]);
+ setLoading(false);
+ setError(null);
+
+ return { success: true };
+ };
+
+ return { unregisterGateway, isLoading, error };
+}
diff --git a/src/api/contracts/gateway-registration/useUnstakeGateway.ts b/src/api/contracts/gateway-registration/useUnstakeGateway.ts
new file mode 100644
index 0000000..8fd69e1
--- /dev/null
+++ b/src/api/contracts/gateway-registration/useUnstakeGateway.ts
@@ -0,0 +1,110 @@
+import { useState } from 'react';
+
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { AccountType } from '@api/subsquid-network-squid';
+import { BlockchainGateway } from '@api/subsquid-network-squid/gateways-graphql';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { errorMessage, TxResult, WriteContractRes } from '../utils';
+import { VESTING_CONTRACT_ABI } from '../vesting.abi';
+
+import { GATEWAY_REGISTRATION_CONTRACT_ABI } from './GatewayRegistration.abi';
+
+type UnstakeGatewayRequest = {
+ gateway: BlockchainGateway;
+};
+
+function useUnstakeFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.GATEWAY_REGISTRATION,
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'unstake',
+ });
+
+ return async (req: UnstakeGatewayRequest): Promise => {
+ try {
+ return {
+ tx: await writeAsync({}),
+ };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+function useUnstakeFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ gateway }: UnstakeGatewayRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: GATEWAY_REGISTRATION_CONTRACT_ABI,
+ functionName: 'unstake',
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: gateway.owner.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.GATEWAY_REGISTRATION, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useUnstakeGateway() {
+ const client = usePublicClient();
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const unstakeFromWallet = useUnstakeFromWallet();
+ const unstakeFromVestingContract = useUnstakeFromVestingContract();
+
+ const unstakeFromGateway = async (req: UnstakeGatewayRequest): Promise => {
+ setLoading(true);
+
+ const res =
+ req.gateway.owner.type === AccountType.User
+ ? await unstakeFromWallet(req)
+ : await unstakeFromVestingContract(req);
+
+ if (!res.tx) {
+ setLoading(false);
+ setError(res.error);
+
+ return { success: false, failedReason: res.error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: res.tx.hash });
+ setWaitHeight(receipt.blockNumber, []);
+
+ setLoading(false);
+
+ return { success: true };
+ };
+
+ return {
+ unstakeFromGateway,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/api/contracts/reaward-treasury.abi.ts b/src/api/contracts/reaward-treasury.abi.ts
new file mode 100644
index 0000000..f51a666
--- /dev/null
+++ b/src/api/contracts/reaward-treasury.abi.ts
@@ -0,0 +1,20 @@
+export const REWARD_TREASURY_CONTRACT_ABI = [
+ {
+ type: 'function',
+ name: 'claimFor',
+ inputs: [
+ {
+ name: 'rewardDistribution',
+ type: 'address',
+ internalType: 'contract IRewardsDistribution',
+ },
+ {
+ name: 'receiver',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+] as const;
diff --git a/src/api/contracts/sqd.ts b/src/api/contracts/sqd.ts
new file mode 100644
index 0000000..dc24a00
--- /dev/null
+++ b/src/api/contracts/sqd.ts
@@ -0,0 +1,51 @@
+import { logger } from '@logger';
+import { WriteContractResult } from '@wagmi/core';
+import Decimal from 'decimal.js';
+import { erc20ABI, useContractWrite, usePublicClient } from 'wagmi';
+
+import { useContracts } from '@network/useContracts.ts';
+
+import { errorMessage, WriteContractRes } from './utils';
+
+export function useApproveSqd() {
+ const client = usePublicClient();
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.SQD,
+ abi: erc20ABI,
+ functionName: 'approve',
+ });
+
+ async function approve({
+ contractAddress,
+ amount,
+ }: {
+ contractAddress: `0x${string}`;
+ amount: Decimal;
+ }): Promise {
+ let tx: WriteContractResult;
+ logger.debug(`approving SQD to ${contracts.WORKER_REGISTRATION}...`);
+ try {
+ tx = await writeAsync({
+ args: [contractAddress, BigInt(amount.toFixed(0))],
+ });
+ } catch (e) {
+ const error = errorMessage(e);
+
+ return { success: false, failedReason: error };
+ }
+
+ if (!tx) {
+ logger.debug(`SQF approve failed with unknown error`);
+ return { success: false, failedReason: 'unknown error' };
+ }
+
+ logger.debug(`waiting confirm of SQD approving tx ${tx.hash}`);
+ await client.waitForTransactionReceipt({ hash: tx.hash });
+ logger.info(`SQD approved, tx ${tx.hash}, completed!`);
+
+ return { success: true };
+ }
+
+ return [approve];
+}
diff --git a/src/api/contracts/staking.abi.ts b/src/api/contracts/staking.abi.ts
new file mode 100644
index 0000000..91346f8
--- /dev/null
+++ b/src/api/contracts/staking.abi.ts
@@ -0,0 +1,55 @@
+export const STAKING_CONTRACT_ABI = [
+ {
+ type: 'function',
+ name: 'deposit',
+ inputs: [
+ {
+ name: 'worker',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'amount',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'withdraw',
+ inputs: [
+ {
+ type: 'uint256',
+ name: 'worker',
+ },
+ {
+ type: 'uint256',
+ name: 'amount',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'claimable',
+ inputs: [
+ {
+ name: 'staker',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+] as const;
diff --git a/src/api/contracts/staking.ts b/src/api/contracts/staking.ts
new file mode 100644
index 0000000..62a1159
--- /dev/null
+++ b/src/api/contracts/staking.ts
@@ -0,0 +1,239 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import Decimal from 'decimal.js';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { useApproveSqd } from '@api/contracts/sqd';
+import {
+ errorMessage,
+ isApproveRequiredError,
+ TxResult,
+ WriteContractRes,
+} from '@api/contracts/utils';
+import { VESTING_CONTRACT_ABI } from '@api/contracts/vesting.abi';
+import { AccountType, BlockchainApiWorker, SourceWallet } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { STAKING_CONTRACT_ABI } from './staking.abi';
+
+type WorkerDepositRequest = {
+ worker: BlockchainApiWorker;
+ amount: bigint;
+ wallet: Pick;
+};
+
+function useDelegateFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.STAKING,
+ abi: STAKING_CONTRACT_ABI,
+ functionName: 'deposit',
+ });
+
+ const [approveSqd] = useApproveSqd();
+
+ const tryCallContract = async ({ worker, amount }: WorkerDepositRequest) => {
+ try {
+ return { tx: await writeAsync({ args: [BigInt(worker.id), amount] }) };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+
+ return async (req: WorkerDepositRequest): Promise => {
+ logger.debug(`deposit to worker via worker contract...`);
+
+ const res = await tryCallContract(req);
+ // Try to approve SQD
+ if (isApproveRequiredError(res.error)) {
+ const approveRes = await approveSqd({
+ contractAddress: contracts.STAKING,
+ amount: new Decimal(req.amount.toString()),
+ });
+ if (!approveRes.success) {
+ return { error: approveRes.failedReason };
+ }
+
+ logger.debug(`approved SQD successfully, now trying to register one more time...`);
+
+ return tryCallContract(req);
+ }
+
+ return res;
+ };
+}
+
+function useDepositFromVestingContract() {
+ const publicClient = usePublicClient();
+ const contracts = useContracts();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ worker, amount, wallet }: WorkerDepositRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: STAKING_CONTRACT_ABI,
+ functionName: 'deposit',
+ args: [BigInt(worker.id), amount],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: wallet.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.STAKING, data, amount],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useWorkerDelegate() {
+ const client = usePublicClient();
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const depositFromWallet = useDelegateFromWallet();
+ const depositFromVestingContract = useDepositFromVestingContract();
+
+ const delegateToWorker = async ({
+ worker,
+ amount,
+ wallet,
+ }: WorkerDepositRequest): Promise => {
+ setLoading(true);
+
+ const res =
+ wallet.type === AccountType.User
+ ? await depositFromWallet({ worker, amount, wallet })
+ : await depositFromVestingContract({ worker, amount, wallet });
+
+ if (!res.tx) {
+ setLoading(false);
+ setError(res.error);
+
+ return { success: false, failedReason: res.error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: res.tx.hash });
+ setWaitHeight(receipt.blockNumber, []);
+
+ setLoading(false);
+
+ return { success: true };
+ };
+
+ return {
+ delegateToWorker,
+ isLoading,
+ error,
+ };
+}
+
+function useUndelegateFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.STAKING,
+ abi: STAKING_CONTRACT_ABI,
+ functionName: 'withdraw',
+ });
+
+ return async ({ worker, amount }: WorkerDepositRequest): Promise => {
+ try {
+ return { tx: await writeAsync({ args: [BigInt(worker.id), amount] }) };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+function useUndelegateFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ worker, amount, wallet }: WorkerDepositRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: STAKING_CONTRACT_ABI,
+ functionName: 'withdraw',
+ args: [BigInt(worker.id), amount],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: wallet.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.STAKING, data, amount],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useWorkerUndelegate() {
+ const client = usePublicClient();
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [isLoading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const undelegateFromWallet = useUndelegateFromWallet();
+ const undelegateFromVestingContract = useUndelegateFromVestingContract();
+
+ const undelegateFromWorker = async ({
+ worker,
+ amount,
+ wallet,
+ }: WorkerDepositRequest): Promise => {
+ setLoading(true);
+
+ const res =
+ wallet.type === AccountType.User
+ ? await undelegateFromWallet({ worker, amount, wallet })
+ : await undelegateFromVestingContract({ worker, amount, wallet });
+
+ if (!res.tx) {
+ setLoading(false);
+ setError(res.error);
+
+ return { success: false, failedReason: res.error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: res.tx.hash });
+ setWaitHeight(receipt.blockNumber, []);
+
+ setLoading(false);
+
+ return { success: true };
+ };
+
+ return {
+ undelegateFromWorker,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/api/contracts/utils.ts b/src/api/contracts/utils.ts
new file mode 100644
index 0000000..5f522eb
--- /dev/null
+++ b/src/api/contracts/utils.ts
@@ -0,0 +1,83 @@
+import { numberWithSpacesFormatter } from '@lib/formatters/formatters.ts';
+import { WriteContractResult } from '@wagmi/core';
+import bs58 from 'bs58';
+import Decimal from 'decimal.js';
+import trimEnd from 'lodash-es/trimEnd';
+import { BaseError as BaseViemError, formatUnits, parseUnits, toHex } from 'viem';
+
+import { SQD_DECIMALS, SQD_TOKEN } from './consts';
+
+export type TxResult = { tx: WriteContractResult; error?: never } | { error: string; tx?: never };
+
+export type WriteContractRes =
+ | { success: true; failedReason?: never }
+ | { success: false; failedReason: string };
+
+const KNOWN_ERRORS: Record = {
+ '0xe450d38c': 'Insufficient balance',
+};
+
+export function errorMessage(e: unknown) {
+ if (e instanceof BaseViemError) {
+ const message = e.shortMessage.split('\n').pop();
+
+ if (message) {
+ if (KNOWN_ERRORS[message]) return KNOWN_ERRORS[message];
+ else if (message.startsWith('0x') && e.metaMessages) return e.metaMessages?.join('\n');
+
+ return message;
+ }
+
+ return e.message;
+ }
+
+ return e instanceof Error ? e.message : e?.toString() || 'unknown error';
+}
+
+export function toSqd(value?: string | bigint | number) {
+ if (!value) return 0n;
+
+ return parseUnits(value.toString(), SQD_DECIMALS);
+}
+
+export function fromSqd(value?: string | bigint | number) {
+ if (!value) return new Decimal(0);
+
+ return new Decimal(formatUnits(BigInt(value), SQD_DECIMALS));
+}
+
+export function humanReadableSqd(value?: string | bigint | number) {
+ if (!value) return '0';
+
+ const v = new Decimal(String(value)).div(10 ** SQD_DECIMALS).toFixed(18);
+
+ return trimEnd(trimEnd(v, '0'), '.');
+}
+
+export function formatSqd(value?: string | Decimal | number, decimals?: number) {
+ if (!value) return `0 ${SQD_TOKEN}`;
+
+ value = typeof value === 'string' ? fromSqd(value) : value;
+
+ const n = Number(value);
+ if (n > 0 && n < 0.01) {
+ return `<0.01 ${SQD_TOKEN}`;
+ }
+
+ return `${numberWithSpacesFormatter(value.toFixed(decimals))} ${SQD_TOKEN}`;
+}
+
+export function isApproveRequiredError(error: unknown) {
+ if (typeof error !== 'string') return;
+
+ const message = error.toLowerCase();
+
+ return (
+ message.includes('insufficient allowance') || // openzeppelin old version
+ message.includes('0xfb8f41b2') // openzeppelin new version
+ );
+}
+
+export function peerIdToHex(peerId: string) {
+ return toHex(bs58.decode(peerId));
+}
diff --git a/src/api/contracts/vesting.abi.ts b/src/api/contracts/vesting.abi.ts
new file mode 100644
index 0000000..6c0c1cc
--- /dev/null
+++ b/src/api/contracts/vesting.abi.ts
@@ -0,0 +1,49 @@
+export const VESTING_CONTRACT_ABI = [
+ {
+ type: 'function',
+ name: 'execute',
+ inputs: [
+ {
+ name: 'to',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'data',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'execute',
+ inputs: [
+ {
+ name: 'to',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'data',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ {
+ name: 'requiredApprove',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ stateMutability: 'nonpayable',
+ },
+] as const;
diff --git a/src/api/contracts/worker-registration/WorkerMetadata.ts b/src/api/contracts/worker-registration/WorkerMetadata.ts
new file mode 100644
index 0000000..5c86c37
--- /dev/null
+++ b/src/api/contracts/worker-registration/WorkerMetadata.ts
@@ -0,0 +1,27 @@
+import { pickBy } from 'lodash-es';
+import isEmpty from 'lodash-es/isEmpty';
+
+export interface WorkerMetadata {
+ name: string;
+ email: string;
+ description: string;
+ website?: string;
+}
+
+export function encodeWorkerMetadata(req: WorkerMetadata) {
+ const md = pickBy(
+ {
+ name: req.name,
+ website: req.website,
+ description: req.description,
+ email: req.email,
+ },
+ Boolean,
+ );
+
+ return !isEmpty(md) ? JSON.stringify(md) : '';
+}
+
+function decodeWorkerMetadata(req: string): WorkerMetadata {
+ return JSON.parse(req);
+}
diff --git a/src/api/contracts/worker-registration/WorkerRegistration.abi.ts b/src/api/contracts/worker-registration/WorkerRegistration.abi.ts
new file mode 100644
index 0000000..8f2c7a8
--- /dev/null
+++ b/src/api/contracts/worker-registration/WorkerRegistration.abi.ts
@@ -0,0 +1,78 @@
+export const WORKER_REGISTRATION_CONTRACT_ABI = [
+ {
+ type: 'function',
+ name: 'bondAmount',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'register',
+ inputs: [
+ {
+ name: 'peerId',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ {
+ name: 'metadata',
+ type: 'string',
+ internalType: 'string',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'updateMetadata',
+ inputs: [
+ {
+ name: 'peerId',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ {
+ name: 'metadata',
+ type: 'string',
+ internalType: 'string',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ inputs: [
+ {
+ internalType: 'bytes',
+ name: 'peerId',
+ type: 'bytes',
+ },
+ ],
+ name: 'deregister',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+
+ {
+ type: 'function',
+ name: 'withdraw',
+ inputs: [
+ {
+ name: 'peerId',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+] as const;
diff --git a/src/api/contracts/worker-registration/useRegisterWorker.ts b/src/api/contracts/worker-registration/useRegisterWorker.ts
new file mode 100644
index 0000000..83f74c3
--- /dev/null
+++ b/src/api/contracts/worker-registration/useRegisterWorker.ts
@@ -0,0 +1,147 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { useApproveSqd } from '@api/contracts/sqd';
+import {
+ errorMessage,
+ isApproveRequiredError,
+ peerIdToHex,
+ TxResult,
+ WriteContractRes,
+} from '@api/contracts/utils';
+import { VESTING_CONTRACT_ABI } from '@api/contracts/vesting.abi';
+import { useNetworkSettings } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { encodeWorkerMetadata, WorkerMetadata } from './WorkerMetadata';
+import { WORKER_REGISTRATION_CONTRACT_ABI } from './WorkerRegistration.abi';
+
+export interface AddWorkerRequest extends WorkerMetadata {
+ peerId: string;
+ vestingContract: string;
+}
+
+function useRegisterFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.WORKER_REGISTRATION,
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'register',
+ });
+ const [approveSqd] = useApproveSqd();
+ const { bondAmount } = useNetworkSettings();
+ const tryCallRegistrationContract = async ({
+ peerId,
+ ...rest
+ }: AddWorkerRequest): Promise => {
+ try {
+ return { tx: await writeAsync({ args: [peerIdToHex(peerId), encodeWorkerMetadata(rest)] }) };
+ } catch (e: unknown) {
+ return {
+ error: errorMessage(e),
+ };
+ }
+ };
+
+ return async (req: AddWorkerRequest): Promise => {
+ logger.debug(`registering worker via worker contract...`);
+
+ const res = await tryCallRegistrationContract(req);
+ // Try to approve SQD
+ if (isApproveRequiredError(res.error)) {
+ const approveRes = await approveSqd({
+ contractAddress: contracts.WORKER_REGISTRATION,
+ amount: bondAmount,
+ });
+ if (!approveRes.success) {
+ return { error: approveRes.failedReason };
+ }
+
+ logger.debug(`approved SQD successfully, now trying to register one more time...`);
+
+ return tryCallRegistrationContract(req);
+ }
+
+ return res;
+ };
+}
+
+function useRegisterWorkerFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ peerId, vestingContract, ...rest }: AddWorkerRequest): Promise => {
+ try {
+ const bond = await publicClient.readContract({
+ address: contracts.WORKER_REGISTRATION,
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'bondAmount',
+ });
+
+ const data = encodeFunctionData({
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'register',
+ args: [peerIdToHex(peerId), encodeWorkerMetadata(rest)],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: vestingContract as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.WORKER_REGISTRATION, data, bond],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useRegisterWorker() {
+ const client = usePublicClient();
+ const { address } = useAccount();
+ const [error, setError] = useState(null);
+ const [isLoading, setLoading] = useState(false);
+
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const registerWorkerContract = useRegisterFromWallet();
+ const registerVestingContract = useRegisterWorkerFromVestingContract();
+
+ const registerWorker = async (req: AddWorkerRequest): Promise => {
+ setLoading(true);
+
+ const { tx, error } = req.vestingContract
+ ? await registerVestingContract(req)
+ : await registerWorkerContract(req);
+
+ if (!tx) {
+ logger.debug(`registering worker failed ${error}`);
+ setLoading(false);
+ setError(error);
+ return { success: false, failedReason: error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: tx.hash });
+ setWaitHeight(receipt.blockNumber, ['myWorkers', { address }]);
+ setLoading(false);
+ setError(null);
+
+ return { success: true };
+ };
+
+ return { registerWorker, isLoading, error };
+}
diff --git a/src/api/contracts/worker-registration/useUnregisterWorker.ts b/src/api/contracts/worker-registration/useUnregisterWorker.ts
new file mode 100644
index 0000000..ca05f9c
--- /dev/null
+++ b/src/api/contracts/worker-registration/useUnregisterWorker.ts
@@ -0,0 +1,118 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { VESTING_CONTRACT_ABI } from '@api/contracts/vesting.abi';
+import { AccountType } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { errorMessage, peerIdToHex, TxResult, WriteContractRes } from '../utils';
+
+import { WORKER_REGISTRATION_CONTRACT_ABI } from './WorkerRegistration.abi';
+
+export interface UnregisterWorkerRequest {
+ peerId: string;
+ source: {
+ id: string;
+ type: AccountType;
+ };
+}
+
+function useUnregisterWorkerFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.WORKER_REGISTRATION,
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'deregister',
+ });
+
+ return async ({ peerId }: { peerId: string }): Promise => {
+ try {
+ return {
+ tx: await writeAsync({
+ args: [peerIdToHex(peerId)],
+ }),
+ };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+function useUnregisterWorkerFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ peerId, source }: UnregisterWorkerRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'deregister',
+ args: [peerIdToHex(peerId)],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: source.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.WORKER_REGISTRATION, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useUnregisterWorker() {
+ const publicClient = usePublicClient();
+ const { address } = useAccount();
+ const [isLoading, setLoading] = useState(false);
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [error, setError] = useState(null);
+
+ const unregisterWorkerFromWallet = useUnregisterWorkerFromWallet();
+ const unregisterWorkerFromVestingContract = useUnregisterWorkerFromVestingContract();
+
+ const unregisterWorker = async (req: UnregisterWorkerRequest): Promise => {
+ setLoading(true);
+
+ const { tx, error } =
+ req.source.type === AccountType.User
+ ? await unregisterWorkerFromWallet(req)
+ : await unregisterWorkerFromVestingContract(req);
+
+ if (!tx) {
+ logger.debug(`update worker failed ${error}`);
+ setLoading(false);
+ setError(error);
+ return { success: false, failedReason: error };
+ }
+
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: tx.hash });
+ setWaitHeight(receipt.blockNumber, ['myWorkers', { address }]);
+ setLoading(false);
+ setError(null);
+
+ return { success: true };
+ };
+
+ return {
+ unregisterWorker,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/api/contracts/worker-registration/useUpdateWorker.ts b/src/api/contracts/worker-registration/useUpdateWorker.ts
new file mode 100644
index 0000000..ee277e6
--- /dev/null
+++ b/src/api/contracts/worker-registration/useUpdateWorker.ts
@@ -0,0 +1,115 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { AccountType } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { errorMessage, peerIdToHex, TxResult, WriteContractRes } from '../utils';
+import { VESTING_CONTRACT_ABI } from '../vesting.abi';
+
+import { encodeWorkerMetadata, WorkerMetadata } from './WorkerMetadata';
+import { WORKER_REGISTRATION_CONTRACT_ABI } from './WorkerRegistration.abi';
+
+export interface UpdateWorkerRequest extends WorkerMetadata {
+ peerId: string;
+ source: {
+ id: string;
+ type: AccountType;
+ };
+}
+
+function useUpdateWorkerFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.WORKER_REGISTRATION,
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'updateMetadata',
+ });
+
+ return async ({ peerId, ...rest }: UpdateWorkerRequest): Promise => {
+ try {
+ return {
+ tx: await writeAsync({
+ args: [peerIdToHex(peerId), encodeWorkerMetadata(rest)],
+ }),
+ };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+function useUpdateWorkerFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ peerId, source, ...rest }: UpdateWorkerRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'updateMetadata',
+ args: [peerIdToHex(peerId), encodeWorkerMetadata(rest)],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: source.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.WORKER_REGISTRATION, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useUpdateWorker() {
+ const client = usePublicClient();
+ const { address } = useAccount();
+ const [error, setError] = useState(null);
+ const [isLoading, setLoading] = useState(false);
+
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const updateWorkerFromWallet = useUpdateWorkerFromWallet();
+ const updateWorkerFromVestingContract = useUpdateWorkerFromVestingContract();
+
+ const updateWorker = async (req: UpdateWorkerRequest): Promise => {
+ setLoading(true);
+
+ const { tx, error } =
+ req.source.type === AccountType.User
+ ? await updateWorkerFromWallet(req)
+ : await updateWorkerFromVestingContract(req);
+
+ if (!tx) {
+ logger.debug(`update worker failed ${error}`);
+ setLoading(false);
+ setError(error);
+ return { success: false, failedReason: error };
+ }
+
+ const receipt = await client.waitForTransactionReceipt({ hash: tx.hash });
+ setWaitHeight(receipt.blockNumber, ['myWorkers', { address }]);
+ setLoading(false);
+ setError(null);
+
+ return { success: true };
+ };
+
+ return { updateWorker, isLoading, error };
+}
diff --git a/src/api/contracts/worker-registration/useWithdrawWorker.ts b/src/api/contracts/worker-registration/useWithdrawWorker.ts
new file mode 100644
index 0000000..4a279ba
--- /dev/null
+++ b/src/api/contracts/worker-registration/useWithdrawWorker.ts
@@ -0,0 +1,110 @@
+import { useState } from 'react';
+
+import { logger } from '@logger';
+import { encodeFunctionData } from 'viem';
+import { useContractWrite, usePublicClient, useWalletClient } from 'wagmi';
+
+import { errorMessage, peerIdToHex, TxResult, WriteContractRes } from '@api/contracts/utils';
+import { VESTING_CONTRACT_ABI } from '@api/contracts/vesting.abi';
+import { UnregisterWorkerRequest } from '@api/contracts/worker-registration/useUnregisterWorker';
+import { AccountType } from '@api/subsquid-network-squid';
+import { useSquidNetworkHeightHooks } from '@hooks/useSquidNetworkHeightHooks.ts';
+import { useAccount } from '@network/useAccount';
+import { useContracts } from '@network/useContracts.ts';
+
+import { WORKER_REGISTRATION_CONTRACT_ABI } from './WorkerRegistration.abi';
+
+function useWithdrawWorkerFromWallet() {
+ const contracts = useContracts();
+ const { writeAsync } = useContractWrite({
+ address: contracts.WORKER_REGISTRATION,
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'withdraw',
+ });
+
+ return async ({ peerId }: { peerId: string }): Promise => {
+ try {
+ return {
+ tx: await writeAsync({
+ args: [peerIdToHex(peerId)],
+ }),
+ };
+ } catch (e) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+function useWithdrawWorkerFromVestingContract() {
+ const contracts = useContracts();
+ const publicClient = usePublicClient();
+ const { data: walletClient } = useWalletClient();
+ const { address: account } = useAccount();
+
+ return async ({ peerId, source }: UnregisterWorkerRequest): Promise => {
+ try {
+ const data = encodeFunctionData({
+ abi: WORKER_REGISTRATION_CONTRACT_ABI,
+ functionName: 'withdraw',
+ args: [peerIdToHex(peerId)],
+ });
+
+ const { request } = await publicClient.simulateContract({
+ account,
+ address: source.id as `0x${string}`,
+ abi: VESTING_CONTRACT_ABI,
+ functionName: 'execute',
+ args: [contracts.WORKER_REGISTRATION, data],
+ });
+
+ const tx = await walletClient?.writeContract(request);
+ if (!tx) {
+ return { error: 'unknown error' };
+ }
+
+ return { tx: { hash: tx } };
+ } catch (e: any) {
+ return { error: errorMessage(e) };
+ }
+ };
+}
+
+export function useWithdrawWorker() {
+ const publicClient = usePublicClient();
+ const { address } = useAccount();
+ const [isLoading, setLoading] = useState(false);
+ const { setWaitHeight } = useSquidNetworkHeightHooks();
+ const [error, setError] = useState(null);
+
+ const withdrawWorkerFromWallet = useWithdrawWorkerFromWallet();
+ const withdrawWorkerFromVestingContract = useWithdrawWorkerFromVestingContract();
+
+ const withdrawWorker = async (req: UnregisterWorkerRequest): Promise => {
+ setLoading(true);
+
+ const { tx, error } =
+ req.source.type === AccountType.User
+ ? await withdrawWorkerFromWallet(req)
+ : await withdrawWorkerFromVestingContract(req);
+
+ if (!tx) {
+ logger.debug(`withdraw worker failed ${error}`);
+ setLoading(false);
+ setError(error);
+ return { success: false, failedReason: error };
+ }
+
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: tx.hash });
+ setWaitHeight(receipt.blockNumber, ['myWorkers', { address }]);
+ setLoading(false);
+ setError(null);
+
+ return { success: true };
+ };
+
+ return {
+ withdrawWorker,
+ isLoading,
+ error,
+ };
+}
diff --git a/src/api/subsquid-network-squid/accounts-graphql.ts b/src/api/subsquid-network-squid/accounts-graphql.ts
new file mode 100644
index 0000000..ba91982
--- /dev/null
+++ b/src/api/subsquid-network-squid/accounts-graphql.ts
@@ -0,0 +1,113 @@
+import { useMemo } from 'react';
+
+import Decimal from 'decimal.js';
+
+import { formatSqd } from '@api/contracts/utils';
+import { useAccount } from '@network/useAccount';
+
+import { SQUID_DATASOURCE } from './datasource';
+import { AccountType, useAccountQuery, useMyAssetsQuery } from './graphql';
+
+export type SourceWallet = {
+ id: string;
+ type: AccountType;
+ balance: string;
+ balanceFormatted: string;
+};
+
+export function useMySources({ enabled }: { enabled?: boolean } = {}) {
+ const { address } = useAccount();
+ const requestEnabled = enabled && !!address;
+ const { data: data, isPending } = useAccountQuery(
+ SQUID_DATASOURCE,
+ {
+ address: address || '',
+ },
+ {
+ enabled: requestEnabled,
+ },
+ );
+
+ const wallet = data?.accountById;
+
+ const res = useMemo((): SourceWallet[] => {
+ return !wallet
+ ? [
+ {
+ type: AccountType.User,
+ id: address as string,
+ balance: '0',
+ balanceFormatted: '0',
+ },
+ ]
+ : [wallet, ...wallet.owned].map(a => ({
+ type: a.type,
+ id: a.id,
+ balance: a.balance as string,
+ balanceFormatted: formatSqd(a.balance),
+ }));
+ }, [address, wallet]);
+
+ const vestingContracts = useMemo(() => {
+ return res.filter(a => a.type === AccountType.Vesting);
+ }, [res]);
+
+ return {
+ sources: res,
+ vestingContracts,
+ isPending,
+ };
+}
+
+export function useMyAssets() {
+ const { address } = useAccount();
+
+ const enabled = !!address;
+ const { data, isLoading } = useMyAssetsQuery(
+ SQUID_DATASOURCE,
+ {
+ address: address || '',
+ },
+ { enabled },
+ );
+
+ const assets = useMemo(() => {
+ const accounts = data?.accounts || [];
+ const delegations = data?.delegations || [];
+ const workers = data?.workers || [];
+
+ let balance = new Decimal(0);
+ let bonded = new Decimal(0);
+ let claimable = new Decimal(0);
+ let delegated = new Decimal(0);
+
+ for (const a of accounts) {
+ balance = balance.add(a.balance);
+
+ for (const o of a.owned) {
+ balance = balance.add(o.balance);
+ }
+ }
+ for (const w of workers) {
+ bonded = bonded.add(w.bond);
+ claimable = claimable.add(w.claimableReward);
+ }
+ for (const d of delegations) {
+ claimable = claimable.add(d.claimableReward);
+ delegated = delegated.add(d.deposit);
+ }
+
+ return {
+ balance: balance.toFixed(0),
+ bonded: bonded.toFixed(0),
+ claimable: claimable.toFixed(0),
+ delegated: delegated.toFixed(0),
+ total: balance.add(bonded).add(claimable).add(delegated).toFixed(0),
+ };
+ }, [data]);
+
+ return {
+ assets,
+ isLoading,
+ };
+}
diff --git a/src/api/subsquid-network-squid/api.graphql b/src/api/subsquid-network-squid/api.graphql
new file mode 100644
index 0000000..b0a6835
--- /dev/null
+++ b/src/api/subsquid-network-squid/api.graphql
@@ -0,0 +1,231 @@
+### COMMON ###
+
+query squidNetworkHeight {
+ squidStatus {
+ height
+ }
+}
+
+query settings {
+ settingsConnection(orderBy: id_ASC) {
+ edges {
+ node {
+ id
+ bondAmount
+ delegationLimitCoefficient
+ }
+ }
+ }
+}
+
+### ACCOUNT ###
+
+query account ($address: String!) {
+ accountById(id: $address) {
+ id
+ type
+ balance
+ owned {
+ id
+ type
+ balance
+ }
+ }
+}
+
+### WORKERS ###
+
+fragment WorkerFragment on Worker {
+ id
+ name
+ email
+ peerId
+ website
+ status
+ createdAt
+ description
+ bond
+ claimableReward
+ claimedReward
+ uptime24Hours
+ uptime90Days
+ totalDelegation
+ delegationCount
+ apr
+ stakerApr
+ online
+ jailed
+ dialOk
+ owner {
+ id
+ }
+ realOwner {
+ id
+ }
+}
+
+fragment WorkerFullFragment on Worker {
+ queries24Hours
+ queries90Days
+ scannedData24Hours
+ scannedData90Days
+ servedData24Hours
+ servedData90Days
+ storedData
+ owner {
+ id
+ type
+ }
+}
+
+query allWorkers {
+ workers(
+ where: {
+ status_eq: ACTIVE
+ }) {
+ ...WorkerFragment
+ }
+}
+
+query workerByPeerId($peerId: String!, $address: String!) {
+ workers(where: {peerId_eq: $peerId}, limit: 1) {
+ ...WorkerFragment
+ ...WorkerFullFragment
+ myDelegations: delegations(where: {realOwner: {id_eq: $address }}) {
+ deposit
+ locked
+ owner {
+ id
+ type
+ balance
+ }
+ }
+ }
+}
+
+query workerDaysUptimeById($id: String!, $from: DateTime!) {
+ workerSnapshotsByDay(workerId: $id, from: $from) {
+ timestamp
+ uptime
+ }
+}
+
+query myWorkers ($address: String!) {
+ workers(orderBy: id_ASC, where: {realOwner: {id_eq: $address}}) {
+ ...WorkerFragment
+ myDelegations: delegations(where: {realOwner: {id_eq: $address }}) {
+ deposit
+ locked
+ owner {
+ id
+ type
+ balance
+ }
+ }
+ }
+}
+
+query myAssets($address: String!) {
+ accounts(where:{id_eq: $address}) {
+ balance
+ owned {
+ balance
+ }
+ }
+ workers(where: {realOwner: {id_eq: $address}}) {
+ bond
+ claimableReward
+ }
+ delegations(where: {realOwner: {id_eq: $address},
+ AND: {
+ OR: [{deposit_gt: 0}, {claimableReward_gt: 0}]
+ }}) {
+ claimableReward
+ deposit
+ }
+}
+
+
+query myDelegations($address: String!) {
+ delegations(where:{realOwner:{id_eq:$address}, deposit_gt: 0}) {
+ claimableReward
+ deposit
+ worker {
+ ...WorkerFragment
+ }
+ owner {
+ id
+ type
+ }
+ }
+}
+
+query myClaimsAvailable($address: String!) {
+ delegations(where:{ realOwner:{id_eq:$address}, claimableReward_gt: 0 }) {
+ claimableReward
+ deposit
+ worker {
+ id
+ name
+ peerId
+ }
+ owner {
+ id
+ type
+ }
+ }
+ workers(where:{ realOwner:{id_eq:$address}, claimableReward_gt: 0 }) {
+ id
+ name
+ peerId
+ claimableReward
+ owner {
+ id
+ type
+ }
+ }
+}
+
+### GATEWAYS ###
+
+fragment GatewayFragment on Gateway {
+ id
+ name
+ status
+ description
+ email
+ endpointUrl
+ website
+ operator {
+ autoExtension
+ stake {
+ amount
+ locked
+ lockStart
+ lockEnd
+ }
+ pendingStake {
+ amount
+ locked
+ lockStart
+ lockEnd
+ }
+ }
+ owner {
+ id
+ type
+ }
+ createdAt
+}
+
+query myGateways($address: String!) {
+ gateways(orderBy: id_ASC, where: {owner: {id_eq: $address}, status_eq: REGISTERED}) {
+ ...GatewayFragment
+ }
+}
+
+query gatewayByPeerId($peerId: String!) {
+ gatewayById(id: $peerId) {
+ ...GatewayFragment
+ }
+}
diff --git a/src/api/subsquid-network-squid/datasource.ts b/src/api/subsquid-network-squid/datasource.ts
new file mode 100644
index 0000000..07749a0
--- /dev/null
+++ b/src/api/subsquid-network-squid/datasource.ts
@@ -0,0 +1,8 @@
+export const SQUID_DATASOURCE: { endpoint: string; fetchParams?: RequestInit } = {
+ endpoint: process.env.SQUID_API_URL || `/graphql`,
+ fetchParams: {
+ headers: {
+ 'Content-type': 'application/json',
+ },
+ },
+};
diff --git a/src/api/subsquid-network-squid/gateways-graphql.ts b/src/api/subsquid-network-squid/gateways-graphql.ts
new file mode 100644
index 0000000..946e275
--- /dev/null
+++ b/src/api/subsquid-network-squid/gateways-graphql.ts
@@ -0,0 +1,82 @@
+import { SQUID_DATASOURCE } from '@api/subsquid-network-squid/datasource';
+import { useAccount } from '@network/useAccount';
+
+import { GatewayFragmentFragment, useGatewayByPeerIdQuery, useMyGatewaysQuery } from './graphql';
+
+// inherit API interface for internal class
+export interface BlockchainGateway extends GatewayFragmentFragment {
+ owner: Exclude;
+}
+
+export class BlockchainGateway {
+ ownedByMe?: boolean;
+ totalStaked: string = '0';
+ pendingStaked: string = '0';
+
+ constructor({ gateway, address }: { gateway: GatewayFragmentFragment; address?: `0x${string}` }) {
+ Object.assign(this, {
+ ...gateway,
+ totalStaked: String(gateway.operator?.stake?.amount || 0),
+ pendingStaked: String(gateway.operator?.pendingStake?.amount || 0),
+ createdAt: new Date(),
+ ownedByMe: gateway?.owner?.id === address,
+ });
+ }
+}
+
+export function useMyGateways() {
+ const { address } = useAccount();
+
+ const enabled = !!address;
+ const { data, isLoading } = useMyGatewaysQuery(
+ SQUID_DATASOURCE,
+ {
+ address: address || '',
+ },
+ {
+ select: res => {
+ return res.gateways.map(
+ gateway =>
+ new BlockchainGateway({
+ gateway,
+ address,
+ }),
+ );
+ },
+ enabled,
+ },
+ );
+
+ return {
+ data: data || [],
+ isLoading: enabled ? isLoading : false,
+ };
+}
+
+export function useGatewayByPeerId(peerId?: string) {
+ const { address } = useAccount();
+ const enabled = !!peerId;
+
+ const { data, isLoading } = useGatewayByPeerIdQuery(
+ SQUID_DATASOURCE,
+ {
+ peerId: peerId || '',
+ },
+ {
+ select: res => {
+ if (!res.gatewayById) return;
+
+ return new BlockchainGateway({
+ gateway: res.gatewayById,
+ address,
+ });
+ },
+ enabled,
+ },
+ );
+
+ return {
+ data,
+ isLoading: enabled ? isLoading : false,
+ };
+}
diff --git a/src/api/subsquid-network-squid/graphql.config.js b/src/api/subsquid-network-squid/graphql.config.js
new file mode 100644
index 0000000..3d36f96
--- /dev/null
+++ b/src/api/subsquid-network-squid/graphql.config.js
@@ -0,0 +1,35 @@
+#!/usr/bin/node
+
+import 'dotenv/config';
+
+export default {
+ overwrite: true,
+ schema:
+ process.env.SQUID_API_URL || ' https://squid.subsquid.io/subsquid-network-indexer/v/v7/graphql',
+ documents: ['src/api/subsquid-network-squid/*.graphql'],
+ hooks: {
+ afterOneFileWrite: ['prettier --write'],
+ },
+ generates: {
+ 'src/api/subsquid-network-squid/graphql.tsx': {
+ plugins: [
+ 'typescript',
+ 'typescript-operations',
+ {
+ 'typescript-react-query': {
+ reactQueryVersion: 5,
+ },
+ },
+ {
+ add: {
+ content: '/* eslint-disable */',
+ },
+ },
+ ],
+ config: {
+ maybeValue: 'T',
+ avoidOptionals: false,
+ },
+ },
+ },
+};
diff --git a/src/api/subsquid-network-squid/graphql.tsx b/src/api/subsquid-network-squid/graphql.tsx
new file mode 100644
index 0000000..b0cbbf7
--- /dev/null
+++ b/src/api/subsquid-network-squid/graphql.tsx
@@ -0,0 +1,5033 @@
+/* eslint-disable */
+import { useQuery, UseQueryOptions } from '@tanstack/react-query';
+export type Maybe = T;
+export type InputMaybe = T;
+export type Exact = { [K in keyof T]: T[K] };
+export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
+export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
+export type MakeEmpty = {
+ [_ in K]?: never;
+};
+export type Incremental =
+ | T
+ | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
+
+function fetcher(
+ endpoint: string,
+ requestInit: RequestInit,
+ query: string,
+ variables?: TVariables,
+) {
+ return async (): Promise => {
+ const res = await fetch(endpoint, {
+ method: 'POST',
+ ...requestInit,
+ body: JSON.stringify({ query, variables }),
+ });
+
+ const json = await res.json();
+
+ if (json.errors) {
+ const { message } = json.errors[0];
+
+ throw new Error(message);
+ }
+
+ return json.data;
+ };
+}
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+ ID: { input: string; output: string };
+ String: { input: string; output: string };
+ Boolean: { input: boolean; output: boolean };
+ Int: { input: number; output: number };
+ Float: { input: number; output: number };
+ BigInt: { input: any; output: any };
+ DateTime: { input: any; output: any };
+};
+
+export type Account = {
+ __typename?: 'Account';
+ balance: Scalars['BigInt']['output'];
+ claimableDelegationCount: Scalars['Int']['output'];
+ claims: Array;
+ delegations: Array;
+ gatewayOperator?: Maybe;
+ gatewayStakes: Array;
+ gateways: Array;
+ id: Scalars['String']['output'];
+ owned: Array;
+ owner?: Maybe;
+ transfers: Array;
+ transfersFrom: Array;
+ transfersTo: Array;
+ type: AccountType;
+ workers: Array;
+};
+
+export type AccountClaimsArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountDelegationsArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountGatewayStakesArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountGatewaysArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountOwnedArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountTransfersArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountTransfersFromArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountTransfersToArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountWorkersArgs = {
+ limit?: InputMaybe;
+ offset?: InputMaybe;
+ orderBy?: InputMaybe>;
+ where?: InputMaybe;
+};
+
+export type AccountEdge = {
+ __typename?: 'AccountEdge';
+ cursor: Scalars['String']['output'];
+ node: Account;
+};
+
+export enum AccountOrderByInput {
+ BalanceAsc = 'balance_ASC',
+ BalanceAscNullsFirst = 'balance_ASC_NULLS_FIRST',
+ BalanceDesc = 'balance_DESC',
+ BalanceDescNullsLast = 'balance_DESC_NULLS_LAST',
+ ClaimableDelegationCountAsc = 'claimableDelegationCount_ASC',
+ ClaimableDelegationCountAscNullsFirst = 'claimableDelegationCount_ASC_NULLS_FIRST',
+ ClaimableDelegationCountDesc = 'claimableDelegationCount_DESC',
+ ClaimableDelegationCountDescNullsLast = 'claimableDelegationCount_DESC_NULLS_LAST',
+ GatewayOperatorAutoExtensionAsc = 'gatewayOperator_autoExtension_ASC',
+ GatewayOperatorAutoExtensionAscNullsFirst = 'gatewayOperator_autoExtension_ASC_NULLS_FIRST',
+ GatewayOperatorAutoExtensionDesc = 'gatewayOperator_autoExtension_DESC',
+ GatewayOperatorAutoExtensionDescNullsLast = 'gatewayOperator_autoExtension_DESC_NULLS_LAST',
+ GatewayOperatorIdAsc = 'gatewayOperator_id_ASC',
+ GatewayOperatorIdAscNullsFirst = 'gatewayOperator_id_ASC_NULLS_FIRST',
+ GatewayOperatorIdDesc = 'gatewayOperator_id_DESC',
+ GatewayOperatorIdDescNullsLast = 'gatewayOperator_id_DESC_NULLS_LAST',
+ IdAsc = 'id_ASC',
+ IdAscNullsFirst = 'id_ASC_NULLS_FIRST',
+ IdDesc = 'id_DESC',
+ IdDescNullsLast = 'id_DESC_NULLS_LAST',
+ OwnerBalanceAsc = 'owner_balance_ASC',
+ OwnerBalanceAscNullsFirst = 'owner_balance_ASC_NULLS_FIRST',
+ OwnerBalanceDesc = 'owner_balance_DESC',
+ OwnerBalanceDescNullsLast = 'owner_balance_DESC_NULLS_LAST',
+ OwnerClaimableDelegationCountAsc = 'owner_claimableDelegationCount_ASC',
+ OwnerClaimableDelegationCountAscNullsFirst = 'owner_claimableDelegationCount_ASC_NULLS_FIRST',
+ OwnerClaimableDelegationCountDesc = 'owner_claimableDelegationCount_DESC',
+ OwnerClaimableDelegationCountDescNullsLast = 'owner_claimableDelegationCount_DESC_NULLS_LAST',
+ OwnerIdAsc = 'owner_id_ASC',
+ OwnerIdAscNullsFirst = 'owner_id_ASC_NULLS_FIRST',
+ OwnerIdDesc = 'owner_id_DESC',
+ OwnerIdDescNullsLast = 'owner_id_DESC_NULLS_LAST',
+ OwnerTypeAsc = 'owner_type_ASC',
+ OwnerTypeAscNullsFirst = 'owner_type_ASC_NULLS_FIRST',
+ OwnerTypeDesc = 'owner_type_DESC',
+ OwnerTypeDescNullsLast = 'owner_type_DESC_NULLS_LAST',
+ TypeAsc = 'type_ASC',
+ TypeAscNullsFirst = 'type_ASC_NULLS_FIRST',
+ TypeDesc = 'type_DESC',
+ TypeDescNullsLast = 'type_DESC_NULLS_LAST',
+}
+
+export type AccountTransfer = {
+ __typename?: 'AccountTransfer';
+ account: Account;
+ direction: TransferDirection;
+ id: Scalars['String']['output'];
+ transfer: Transfer;
+};
+
+export type AccountTransferEdge = {
+ __typename?: 'AccountTransferEdge';
+ cursor: Scalars['String']['output'];
+ node: AccountTransfer;
+};
+
+export enum AccountTransferOrderByInput {
+ AccountBalanceAsc = 'account_balance_ASC',
+ AccountBalanceAscNullsFirst = 'account_balance_ASC_NULLS_FIRST',
+ AccountBalanceDesc = 'account_balance_DESC',
+ AccountBalanceDescNullsLast = 'account_balance_DESC_NULLS_LAST',
+ AccountClaimableDelegationCountAsc = 'account_claimableDelegationCount_ASC',
+ AccountClaimableDelegationCountAscNullsFirst = 'account_claimableDelegationCount_ASC_NULLS_FIRST',
+ AccountClaimableDelegationCountDesc = 'account_claimableDelegationCount_DESC',
+ AccountClaimableDelegationCountDescNullsLast = 'account_claimableDelegationCount_DESC_NULLS_LAST',
+ AccountIdAsc = 'account_id_ASC',
+ AccountIdAscNullsFirst = 'account_id_ASC_NULLS_FIRST',
+ AccountIdDesc = 'account_id_DESC',
+ AccountIdDescNullsLast = 'account_id_DESC_NULLS_LAST',
+ AccountTypeAsc = 'account_type_ASC',
+ AccountTypeAscNullsFirst = 'account_type_ASC_NULLS_FIRST',
+ AccountTypeDesc = 'account_type_DESC',
+ AccountTypeDescNullsLast = 'account_type_DESC_NULLS_LAST',
+ DirectionAsc = 'direction_ASC',
+ DirectionAscNullsFirst = 'direction_ASC_NULLS_FIRST',
+ DirectionDesc = 'direction_DESC',
+ DirectionDescNullsLast = 'direction_DESC_NULLS_LAST',
+ IdAsc = 'id_ASC',
+ IdAscNullsFirst = 'id_ASC_NULLS_FIRST',
+ IdDesc = 'id_DESC',
+ IdDescNullsLast = 'id_DESC_NULLS_LAST',
+ TransferAmountAsc = 'transfer_amount_ASC',
+ TransferAmountAscNullsFirst = 'transfer_amount_ASC_NULLS_FIRST',
+ TransferAmountDesc = 'transfer_amount_DESC',
+ TransferAmountDescNullsLast = 'transfer_amount_DESC_NULLS_LAST',
+ TransferBlockNumberAsc = 'transfer_blockNumber_ASC',
+ TransferBlockNumberAscNullsFirst = 'transfer_blockNumber_ASC_NULLS_FIRST',
+ TransferBlockNumberDesc = 'transfer_blockNumber_DESC',
+ TransferBlockNumberDescNullsLast = 'transfer_blockNumber_DESC_NULLS_LAST',
+ TransferIdAsc = 'transfer_id_ASC',
+ TransferIdAscNullsFirst = 'transfer_id_ASC_NULLS_FIRST',
+ TransferIdDesc = 'transfer_id_DESC',
+ TransferIdDescNullsLast = 'transfer_id_DESC_NULLS_LAST',
+ TransferTimestampAsc = 'transfer_timestamp_ASC',
+ TransferTimestampAscNullsFirst = 'transfer_timestamp_ASC_NULLS_FIRST',
+ TransferTimestampDesc = 'transfer_timestamp_DESC',
+ TransferTimestampDescNullsLast = 'transfer_timestamp_DESC_NULLS_LAST',
+}
+
+export type AccountTransferWhereInput = {
+ AND?: InputMaybe>;
+ OR?: InputMaybe>;
+ account?: InputMaybe;
+ account_isNull?: InputMaybe;
+ direction_eq?: InputMaybe;
+ direction_in?: InputMaybe>;
+ direction_isNull?: InputMaybe;
+ direction_not_eq?: InputMaybe;
+ direction_not_in?: InputMaybe>;
+ id_contains?: InputMaybe;
+ id_containsInsensitive?: InputMaybe;
+ id_endsWith?: InputMaybe;
+ id_eq?: InputMaybe;
+ id_gt?: InputMaybe;
+ id_gte?: InputMaybe;
+ id_in?: InputMaybe>;
+ id_isNull?: InputMaybe;
+ id_lt?: InputMaybe;
+ id_lte?: InputMaybe;
+ id_not_contains?: InputMaybe;
+ id_not_containsInsensitive?: InputMaybe;
+ id_not_endsWith?: InputMaybe;
+ id_not_eq?: InputMaybe;
+ id_not_in?: InputMaybe>;
+ id_not_startsWith?: InputMaybe;
+ id_startsWith?: InputMaybe;
+ transfer?: InputMaybe;
+ transfer_isNull?: InputMaybe;
+};
+
+export type AccountTransfersConnection = {
+ __typename?: 'AccountTransfersConnection';
+ edges: Array;
+ pageInfo: PageInfo;
+ totalCount: Scalars['Int']['output'];
+};
+
+export enum AccountType {
+ User = 'USER',
+ Vesting = 'VESTING',
+}
+
+export type AccountWhereInput = {
+ AND?: InputMaybe>;
+ OR?: InputMaybe>;
+ balance_eq?: InputMaybe;
+ balance_gt?: InputMaybe;
+ balance_gte?: InputMaybe;
+ balance_in?: InputMaybe>;
+ balance_isNull?: InputMaybe;
+ balance_lt?: InputMaybe;
+ balance_lte?: InputMaybe;
+ balance_not_eq?: InputMaybe;
+ balance_not_in?: InputMaybe>;
+ claimableDelegationCount_eq?: InputMaybe;
+ claimableDelegationCount_gt?: InputMaybe;
+ claimableDelegationCount_gte?: InputMaybe;
+ claimableDelegationCount_in?: InputMaybe>;
+ claimableDelegationCount_isNull?: InputMaybe;
+ claimableDelegationCount_lt?: InputMaybe;
+ claimableDelegationCount_lte?: InputMaybe;
+ claimableDelegationCount_not_eq?: InputMaybe;
+ claimableDelegationCount_not_in?: InputMaybe>;
+ claims_every?: InputMaybe;
+ claims_none?: InputMaybe;
+ claims_some?: InputMaybe;
+ delegations_every?: InputMaybe;
+ delegations_none?: InputMaybe;
+ delegations_some?: InputMaybe;
+ gatewayOperator?: InputMaybe;
+ gatewayOperator_isNull?: InputMaybe;
+ gatewayStakes_every?: InputMaybe;
+ gatewayStakes_none?: InputMaybe;
+ gatewayStakes_some?: InputMaybe;
+ gateways_every?: InputMaybe;
+ gateways_none?: InputMaybe;
+ gateways_some?: InputMaybe;
+ id_contains?: InputMaybe;
+ id_containsInsensitive?: InputMaybe;
+ id_endsWith?: InputMaybe;
+ id_eq?: InputMaybe;
+ id_gt?: InputMaybe;
+ id_gte?: InputMaybe;
+ id_in?: InputMaybe>;
+ id_isNull?: InputMaybe;
+ id_lt?: InputMaybe;
+ id_lte?: InputMaybe;
+ id_not_contains?: InputMaybe;
+ id_not_containsInsensitive?: InputMaybe;
+ id_not_endsWith?: InputMaybe;
+ id_not_eq?: InputMaybe;
+ id_not_in?: InputMaybe>;
+ id_not_startsWith?: InputMaybe;
+ id_startsWith?: InputMaybe;
+ owned_every?: InputMaybe;
+ owned_none?: InputMaybe;
+ owned_some?: InputMaybe;
+ owner?: InputMaybe;
+ owner_isNull?: InputMaybe;
+ transfersFrom_every?: InputMaybe;
+ transfersFrom_none?: InputMaybe;
+ transfersFrom_some?: InputMaybe;
+ transfersTo_every?: InputMaybe;
+ transfersTo_none?: InputMaybe;
+ transfersTo_some?: InputMaybe;
+ transfers_every?: InputMaybe;
+ transfers_none?: InputMaybe;
+ transfers_some?: InputMaybe;
+ type_eq?: InputMaybe;
+ type_in?: InputMaybe>;
+ type_isNull?: InputMaybe;
+ type_not_eq?: InputMaybe;
+ type_not_in?: InputMaybe>;
+ workers_every?: InputMaybe;
+ workers_none?: InputMaybe;
+ workers_some?: InputMaybe;
+};
+
+export type AccountsConnection = {
+ __typename?: 'AccountsConnection';
+ edges: Array;
+ pageInfo: PageInfo;
+ totalCount: Scalars['Int']['output'];
+};
+
+export type Block = {
+ __typename?: 'Block';
+ hash: Scalars['String']['output'];
+ height: Scalars['Int']['output'];
+ id: Scalars['String']['output'];
+ l1BlockNumber: Scalars['Int']['output'];
+ timestamp: Scalars['DateTime']['output'];
+};
+
+export type BlockEdge = {
+ __typename?: 'BlockEdge';
+ cursor: Scalars['String']['output'];
+ node: Block;
+};
+
+export enum BlockOrderByInput {
+ HashAsc = 'hash_ASC',
+ HashAscNullsFirst = 'hash_ASC_NULLS_FIRST',
+ HashDesc = 'hash_DESC',
+ HashDescNullsLast = 'hash_DESC_NULLS_LAST',
+ HeightAsc = 'height_ASC',
+ HeightAscNullsFirst = 'height_ASC_NULLS_FIRST',
+ HeightDesc = 'height_DESC',
+ HeightDescNullsLast = 'height_DESC_NULLS_LAST',
+ IdAsc = 'id_ASC',
+ IdAscNullsFirst = 'id_ASC_NULLS_FIRST',
+ IdDesc = 'id_DESC',
+ IdDescNullsLast = 'id_DESC_NULLS_LAST',
+ L1BlockNumberAsc = 'l1BlockNumber_ASC',
+ L1BlockNumberAscNullsFirst = 'l1BlockNumber_ASC_NULLS_FIRST',
+ L1BlockNumberDesc = 'l1BlockNumber_DESC',
+ L1BlockNumberDescNullsLast = 'l1BlockNumber_DESC_NULLS_LAST',
+ TimestampAsc = 'timestamp_ASC',
+ TimestampAscNullsFirst = 'timestamp_ASC_NULLS_FIRST',
+ TimestampDesc = 'timestamp_DESC',
+ TimestampDescNullsLast = 'timestamp_DESC_NULLS_LAST',
+}
+
+export type BlockWhereInput = {
+ AND?: InputMaybe>;
+ OR?: InputMaybe>;
+ hash_contains?: InputMaybe;
+ hash_containsInsensitive?: InputMaybe;
+ hash_endsWith?: InputMaybe;
+ hash_eq?: InputMaybe;
+ hash_gt?: InputMaybe;
+ hash_gte?: InputMaybe;
+ hash_in?: InputMaybe>;
+ hash_isNull?: InputMaybe;
+ hash_lt?: InputMaybe;
+ hash_lte?: InputMaybe;
+ hash_not_contains?: InputMaybe;
+ hash_not_containsInsensitive?: InputMaybe;
+ hash_not_endsWith?: InputMaybe;
+ hash_not_eq?: InputMaybe;
+ hash_not_in?: InputMaybe>;
+ hash_not_startsWith?: InputMaybe;
+ hash_startsWith?: InputMaybe;
+ height_eq?: InputMaybe;
+ height_gt?: InputMaybe;
+ height_gte?: InputMaybe;
+ height_in?: InputMaybe>;
+ height_isNull?: InputMaybe;
+ height_lt?: InputMaybe;
+ height_lte?: InputMaybe;
+ height_not_eq?: InputMaybe;
+ height_not_in?: InputMaybe>;
+ id_contains?: InputMaybe;
+ id_containsInsensitive?: InputMaybe;
+ id_endsWith?: InputMaybe;
+ id_eq?: InputMaybe;
+ id_gt?: InputMaybe;
+ id_gte?: InputMaybe;
+ id_in?: InputMaybe>;
+ id_isNull?: InputMaybe;
+ id_lt?: InputMaybe;
+ id_lte?: InputMaybe;
+ id_not_contains?: InputMaybe;
+ id_not_containsInsensitive?: InputMaybe;
+ id_not_endsWith?: InputMaybe;
+ id_not_eq?: InputMaybe