added web-app and landing

This commit is contained in:
johba 2025-09-23 14:18:04 +02:00
parent af031877a5
commit 769fa105b8
198 changed files with 22132 additions and 10 deletions

56
web-app/.gitignore vendored Normal file
View file

@ -0,0 +1,56 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependency directories
node_modules
.pnpm-store
# Build output
/dist
/dist-ssr
storybook-static
coverage
# Cache & temp directories
.cache
.tmp
.temp
.vite
.vite-inspect
.eslintcache
.stylelintcache
# OS metadata
.DS_Store
Thumbs.db
# Local env files
.env
.env.*.local
.env.local
*.local
# Cypress artifacts
/cypress/videos/
/cypress/screenshots/
/cypress/downloads/
# TypeScript
*.tsbuildinfo
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.iml
*.sw?

33
web-app/README.md Normal file
View file

@ -0,0 +1,33 @@
# harb staking
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
web-app/env.d.ts vendored Normal file
View file

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

13
web-app/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KrAIken Staking Interface</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

8711
web-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

44
web-app/package.json Normal file
View file

@ -0,0 +1,44 @@
{
"name": "harb-staking",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"subtree": "git subtree push --prefix dist origin gh-pages"
},
"dependencies": {
"@tanstack/vue-query": "^5.64.2",
"@vue/test-utils": "^2.4.6",
"@wagmi/vue": "^0.1.11",
"axios": "^1.7.9",
"chart.js": "^4.4.7",
"chartjs-plugin-zoom": "^2.2.0",
"element-plus": "^2.9.3",
"harb-lib": "^0.2.0",
"sass": "^1.83.4",
"viem": "^2.22.13",
"vitest": "^3.0.4",
"vue": "^3.5.13",
"vue-router": "^4.2.5",
"vue-tippy": "^6.6.0",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@iconify/vue": "^4.3.0",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"gh-pages": "^6.1.1",
"npm-run-all2": "^7.0.2",
"typescript": "~5.7.3",
"vite": "^6.0.11",
"vite-plugin-vue-devtools": "^7.7.0",
"vue-tsc": "^2.2.0"
}
}

BIN
web-app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,13 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13571_129878)">
<rect width="40" height="40" fill="#0052FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3312 0H31.6672C36.2704 0 40 4.0128 40 8.9632V31.0368C40 35.9872 36.2704 40 31.6688 40H8.3312C3.7296 40 0 35.9872 0 31.0368V8.9632C0 4.0128 3.7296 0 8.3312 0Z" fill="#0052FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9989 5.79443C27.8453 5.79443 34.2053 12.1544 34.2053 20.0008C34.2053 27.8472 27.8453 34.2072 19.9989 34.2072C12.1525 34.2072 5.79254 27.8472 5.79254 20.0008C5.79254 12.1544 12.1525 5.79443 19.9989 5.79443Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5005 15.459H23.4973C24.0733 15.459 24.5389 15.9614 24.5389 16.579V23.419C24.5389 24.0382 24.0717 24.539 23.4973 24.539H16.5005C15.9245 24.539 15.4589 24.0366 15.4589 23.419V16.579C15.4589 15.9614 15.9261 15.459 16.5005 15.459Z" fill="#0052FF"/>
</g>
<defs>
<clipPath id="clip0_13571_129878">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="#3396FF"/>
<path d="M20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.9543 0 0 8.9543 0 20C0 31.0457 8.9543 40 20 40Z" fill="#3396FF"/>
<path d="M12.2327 14.8836C16.5225 10.7003 23.4779 10.7003 27.7677 14.8836L28.284 15.3871C28.4985 15.5962 28.4985 15.9353 28.284 16.1445L26.5179 17.8668C26.4106 17.9713 26.2368 17.9713 26.1295 17.8668L25.419 17.1739C22.4263 14.2555 17.5741 14.2555 14.5813 17.1739L13.8204 17.9159C13.7132 18.0205 13.5393 18.0205 13.4321 17.9159L11.666 16.1936C11.4514 15.9845 11.4514 15.6453 11.666 15.4362L12.2327 14.8836ZM31.4203 18.4454L32.9922 19.9782C33.2067 20.1874 33.2067 20.5265 32.9922 20.7356L25.9045 27.6473C25.69 27.8565 25.3422 27.8565 25.1277 27.6473L20.0973 22.742C20.0437 22.6896 19.9568 22.6896 19.9031 22.742L14.8728 27.6473C14.6583 27.8565 14.3106 27.8565 14.096 27.6473L7.00816 20.7355C6.79367 20.5264 6.79367 20.1873 7.00816 19.9782L8.58004 18.4453C8.79454 18.2362 9.1423 18.2362 9.35679 18.4453L14.3873 23.3508C14.4409 23.4031 14.5278 23.4031 14.5814 23.3508L19.6117 18.4453C19.8262 18.2361 20.1739 18.2361 20.3885 18.4453L25.4189 23.3508C25.4726 23.4031 25.5595 23.4031 25.6131 23.3508L30.6436 18.4454C30.858 18.2362 31.2058 18.2362 31.4203 18.4454Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

38
web-app/src/App.vue Normal file
View file

@ -0,0 +1,38 @@
<template>
<component :is="layoutComponent">
<router-view />
</component>
</template>
<script setup lang="ts">
import { RouterView, useRoute, useRouter } from "vue-router";
import type { LayoutName } from '@/types/router';
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import NavbarLayout from '@/layouts/NavbarLayout.vue';
const layouts = {
DefaultLayout,
NavbarLayout
};
import { computed, defineAsyncComponent, provide, ref } from "vue";
const route = useRoute();
const router = useRouter();
const layoutComponent = computed(() => {
const layoutName: LayoutName = route.meta.layout ?? 'DefaultLayout';
return layouts[layoutName]; // Jetzt kennt TypeScript den Typ!
});
</script>
<style lang="sass">
footer
margin-top: auto
</style>

View file

@ -0,0 +1,5 @@
<svg width="68" height="24" viewBox="0 0 68 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.4508 11.9999L15.1008 4.64991C14.8508 4.39991 14.7298 4.10824 14.7378 3.77491C14.7458 3.44157 14.8751 3.14991 15.1258 2.89991C15.3764 2.64991 15.6681 2.52491 16.0008 2.52491C16.3334 2.52491 16.6251 2.64991 16.8758 2.89991L24.5758 10.5749C24.7758 10.7749 24.9258 10.9999 25.0258 11.2499C25.1258 11.4999 25.1758 11.7499 25.1758 11.9999C25.1758 12.2499 25.1258 12.4999 25.0258 12.7499C24.9258 12.9999 24.7758 13.2249 24.5758 13.4249L16.8758 21.1249C16.6258 21.3749 16.3298 21.4959 15.9878 21.4879C15.6458 21.4799 15.3501 21.3506 15.1008 21.0999C14.8514 20.8492 14.7264 20.5576 14.7258 20.2249C14.7251 19.8922 14.8501 19.6006 15.1008 19.3499L22.4508 11.9999Z" fill="#D6D6D6"/>
<path d="M36.4508 11.9999L29.1008 4.64991C28.8508 4.39991 28.7298 4.10824 28.7378 3.77491C28.7458 3.44157 28.8751 3.14991 29.1258 2.89991C29.3764 2.64991 29.6681 2.52491 30.0008 2.52491C30.3334 2.52491 30.6251 2.64991 30.8758 2.89991L38.5758 10.5749C38.7758 10.7749 38.9258 10.9999 39.0258 11.2499C39.1258 11.4999 39.1758 11.7499 39.1758 11.9999C39.1758 12.2499 39.1258 12.4999 39.0258 12.7499C38.9258 12.9999 38.7758 13.2249 38.5758 13.4249L30.8758 21.1249C30.6258 21.3749 30.3298 21.4959 29.9878 21.4879C29.6458 21.4799 29.3501 21.3506 29.1008 21.0999C28.8514 20.8492 28.7264 20.5576 28.7258 20.2249C28.7251 19.8922 28.8501 19.6006 29.1008 19.3499L36.4508 11.9999Z" fill="#D6D6D6"/>
<path d="M50.4508 11.9999L43.1008 4.64991C42.8508 4.39991 42.7298 4.10824 42.7378 3.77491C42.7458 3.44157 42.8751 3.14991 43.1258 2.89991C43.3764 2.64991 43.6681 2.52491 44.0008 2.52491C44.3334 2.52491 44.6251 2.64991 44.8758 2.89991L52.5758 10.5749C52.7758 10.7749 52.9258 10.9999 53.0258 11.2499C53.1258 11.4999 53.1758 11.7499 53.1758 11.9999C53.1758 12.2499 53.1258 12.4999 53.0258 12.7499C52.9258 12.9999 52.7758 13.2249 52.5758 13.4249L44.8758 21.1249C44.6258 21.3749 44.3298 21.4959 43.9878 21.4879C43.6458 21.4799 43.3501 21.3506 43.1008 21.0999C42.8514 20.8492 42.7264 20.5576 42.7258 20.2249C42.7251 19.8922 42.8501 19.6006 43.1008 19.3499L50.4508 11.9999Z" fill="#D6D6D6"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,13 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13571_129878)">
<rect width="40" height="40" fill="#0052FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3312 0H31.6672C36.2704 0 40 4.0128 40 8.9632V31.0368C40 35.9872 36.2704 40 31.6688 40H8.3312C3.7296 40 0 35.9872 0 31.0368V8.9632C0 4.0128 3.7296 0 8.3312 0Z" fill="#0052FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9989 5.79443C27.8453 5.79443 34.2053 12.1544 34.2053 20.0008C34.2053 27.8472 27.8453 34.2072 19.9989 34.2072C12.1525 34.2072 5.79254 27.8472 5.79254 20.0008C5.79254 12.1544 12.1525 5.79443 19.9989 5.79443Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5005 15.459H23.4973C24.0733 15.459 24.5389 15.9614 24.5389 16.579V23.419C24.5389 24.0382 24.0717 24.539 23.4973 24.539H16.5005C15.9245 24.539 15.4589 24.0366 15.4589 23.419V16.579C15.4589 15.9614 15.9261 15.459 16.5005 15.459Z" fill="#0052FF"/>
</g>
<defs>
<clipPath id="clip0_13571_129878">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="#3396FF"/>
<path d="M20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.9543 0 0 8.9543 0 20C0 31.0457 8.9543 40 20 40Z" fill="#3396FF"/>
<path d="M12.2327 14.8836C16.5225 10.7003 23.4779 10.7003 27.7677 14.8836L28.284 15.3871C28.4985 15.5962 28.4985 15.9353 28.284 16.1445L26.5179 17.8668C26.4106 17.9713 26.2368 17.9713 26.1295 17.8668L25.419 17.1739C22.4263 14.2555 17.5741 14.2555 14.5813 17.1739L13.8204 17.9159C13.7132 18.0205 13.5393 18.0205 13.4321 17.9159L11.666 16.1936C11.4514 15.9845 11.4514 15.6453 11.666 15.4362L12.2327 14.8836ZM31.4203 18.4454L32.9922 19.9782C33.2067 20.1874 33.2067 20.5265 32.9922 20.7356L25.9045 27.6473C25.69 27.8565 25.3422 27.8565 25.1277 27.6473L20.0973 22.742C20.0437 22.6896 19.9568 22.6896 19.9031 22.742L14.8728 27.6473C14.6583 27.8565 14.3106 27.8565 14.096 27.6473L7.00816 20.7355C6.79367 20.5264 6.79367 20.1873 7.00816 19.9782L8.58004 18.4453C8.79454 18.2362 9.1423 18.2362 9.35679 18.4453L14.3873 23.3508C14.4409 23.4031 14.5278 23.4031 14.5814 23.3508L19.6117 18.4453C19.8262 18.2361 20.1739 18.2361 20.3885 18.4453L25.4189 23.3508C25.4726 23.4031 25.5595 23.4031 25.6131 23.3508L30.6436 18.4454C30.858 18.2362 31.2058 18.2362 31.4203 18.4454Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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.3 MiB

BIN
web-app/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -0,0 +1,20 @@
<svg width="49" height="50" viewBox="0 0 49 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3277_37470)">
<path d="M49 25C49 38.531 38.031 49.5 24.5 49.5C10.969 49.5 0 38.531 0 25C0 11.469 10.969 0.5 24.5 0.5C38.031 0.5 49 11.469 49 25Z" fill="url(#paint0_linear_3277_37470)"/>
<path d="M24.4216 18.4727L46.6369 50.0385H2.20625L24.4216 18.4727Z" fill="#1A54F4"/>
<path d="M27.0679 26.1869L29.2195 26.7659L29.7988 26.1869L24.3369 18.4121L23.5921 19.4046L9.02702 40.1649L8.44773 40.992H10.1856L13.0821 38.5934L15.5648 38.2626L19.206 33.7962L18.7923 31.9766L20.7784 26.9313L24.0059 24.0364L27.0679 26.1869Z" fill="white"/>
<path d="M30.1086 10.4087C29.8314 10.6748 29.5708 10.9456 29.3146 11.2119C28.0608 12.5151 26.9116 13.7097 24.4234 13.7097C22.8617 13.7097 21.8976 13.2834 21.1067 12.6834C20.6977 12.3731 20.3259 12.0093 19.9298 11.6019C19.8504 11.5202 19.7696 11.4364 19.6873 11.351C19.3941 11.0466 19.0818 10.7225 18.7439 10.406C19.0782 10.091 19.3802 9.77848 19.6701 9.47847C20.9318 8.173 21.9643 7.10465 24.4234 7.10465C26.8967 7.10465 27.9868 8.23505 29.2452 9.54003C29.5197 9.82458 29.8021 10.1174 30.1086 10.4087Z" stroke="white" stroke-width="1.22884"/>
<circle cx="24.3315" cy="10.2992" r="2.87241" stroke="white" stroke-width="1.01379"/>
<path d="M27.0342 8.27148H28.1474L29.3997 10.468L28.1474 12.6646H27.0342V8.27148Z" fill="white"/>
<path d="M21.6279 12.666L20.5147 12.666L19.2624 10.4695L20.5147 8.27291L21.6279 8.27291L21.6279 12.666Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_3277_37470" x1="24.331" y1="49.5" x2="24.331" y2="2.18965" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#1A54F4"/>
</linearGradient>
<clipPath id="clip0_3277_37470">
<rect y="0.5" width="49" height="49" rx="24.5" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,8 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_2610_39819" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="28">
<rect x="28" y="27.999" width="27.9984" height="27.9984" transform="rotate(-180 28 27.999)" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_2610_39819)">
<path d="M14.0006 3.50019L14.0006 5.8334L5.83438 5.8334L5.83437 22.1658L14.0006 22.1658L14.0006 24.499L5.83437 24.499C5.19274 24.499 4.64347 24.2706 4.18655 23.8136C3.72963 23.3567 3.50117 22.8074 3.50117 22.1658L3.50117 5.8334C3.50117 5.19176 3.72963 4.64249 4.18655 4.18557C4.64347 3.72865 5.19274 3.50019 5.83438 3.50019L14.0006 3.50019ZM16.3338 8.1666L17.9379 9.85817L14.963 12.833L24.5 12.833L24.5 15.1662L14.963 15.1662L17.9379 18.141L16.3338 19.8326L10.5008 13.9996L16.3338 8.1666Z" fill="#F9F9FA"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View file

@ -0,0 +1,305 @@
@font-face
font-family: "DM Sans"
src: url("/fonts/DMSans-variable.ttf")
font-style: normal
@font-face
font-family: "DM Sans semi-bold"
src: url("/fonts/DMSans-SemiBold.ttf")
font-weight: 600
font-style: normal
@font-face
font-family: "Crypto Scam"
src: url("/fonts/Crypto-Scam-Regular.ttf")
font-weight: 400
font-style: normal
@font-face
font-family: "Play"
src: url("/fonts/Play-Regular.ttf")
font-weight: 400
font-style: normal
@font-face
font-family: "Play-Bold"
src: url("/fonts/Play-Bold.ttf")
@font-face
font-family: "Audiowide"
src: url("/fonts/Audiowide-Regular.ttf") format('truetype')
:root, [data-theme="default"]
font-family: "DM Sans", sans-serif
color: var(--color-midnight-black)
font-optical-sizing: auto
font-size: 14px
line-height: 20px
letter-spacing: 0.17px
// global
--color-primary: #7550AE
--color-secondary: #07111B
--color-tertiary: #141414
--color-input: #202020
--color-white: #FFF
--color-primary--hovered: var(--color-white-hovered)
--color-primary-light: #FFF
--color-white-hovered: #cccccc
--color-black-hovered: #333333
--color-grey: #858585
--color-quatenary: #FFF
--color-steel-blue: #4682B4
--color-based-blue: #7550AE
--color-based-blue--hovered: #5f4884
--color-bright-white: #FFFFFF
--color-bright-white-hovered: #e6e6e6
--color-very-light-grey: #F0F0F0
--color-light-grey: #D6D6D6
--color-border-main: #A1A3A7
--color-midnight-black: #0F0F0F
--color-midnight-black-hovered: #272727
--color-font: var(--color-white)
--color-bg: white
--color-black: black
--color-blood: #8B0000
--color-white: white
--color-text-primary: #121312
--color-text-primary-hovered: #292b29
--color-contrast-lower: hsl(0, 0%, 95%)
--color-contrast-low: hsl(240, 1%, 83%)
--color-contrast-medium: hsl(240, 1%, 48%)
--color-contrast-high: hsl(240, 4%, 20%)
--color-contrast-higher: black
--color-disabled: #A1A3A7
--color-bg-disabled: #D6D6D6
--border-radius: 16px
//buttons
--btn-color: var(--color-primary)
//todo btn-color hover
--btn-bg-color: var(--color-black)
--btn-padding: 16px 32px
--btn-dense-padding: 8px 16px
--font-size-number-big: 24px
--font-size-number-small: 14px
--font-size-alert-title: 16px
--font-size-navigation: 18px
--font-size-button-small: 13px
--font-size-button-medium: 14px
--font-size-button-large: 20px
--color-button-bg: var(--color-based-blue)
--color-button-font: var(--color-white)
--color-button-bg--outlined: var(--color-white)
--color-button-font--outlined: var(--color-based-blue)
--color-button-border--outlined: var(--color-based-blue)
--color-button-bg--hovered: var(--color-based-blue)
--color-button-font--hovered: var(--color-based-blue)
--color-button-bg--active: var(--color-based-blue)
--color-button-font--active: var(--color-based-blue)
--color-button-bg--light: var(--color-white)
--color-button-font--light: var(--color-secondary)
--color-button-border--light: var(--color-based-blue)
//card
--color-card-font: var(--color-font)
--color-card-bg: var(--color-tertiary)
--color-card-box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.20)
--card-border-color: var(--color-border-main)
--card-border-radius: var(--border-radius)
--card-border-width: 1px
--card-border-style: solid
--color-tab-bg: var(--color-primary-light)
--color-tab-font--active: var(--color-based-blue)
--color-tab-border--active: var(--color-based-blue)
--color-navbar-bg: var(--color-secondary)
--color-navbar-font: var(--font-color)
--color-navbar-border: var(--color-black)
--color-font-main: var(--color-font)
--color-social-font: var(--color-black)
--color-social-border: var(--color-black)
--color-collapse-font: var(--color-black)
--color-collapse-bg: var(--color-white)
--color-collapse-border: var(--color-black)
--color-output-bg: var(--color-very-light-grey)
--color-expand-icon: var(--color-black)
--color-icon: var(--color-black)
--chart-button-background-color: var(--color-based-blue)
--chart-button-background-color--hovered: var(--color-based-blue--hovered)
// --color-tab-bg--active:
// --color-tab-font--hovered:
// --color-tab-bg--hovered:
// --color-tab-font:
// --color-tab-bg:
//fonts
--font-h1: 52px
--font-h2: 44px
--font-h3: 32px
--font-h4: 27px
--font-h5: 24px
--font-h6: 20px
--font-subheader1: 16px
--font-subheader2: 14px
--font-body0: 18px
--font-body1: 16px
--font-body2: 14px
--font-caption: 12px
--font-overline: 11px
body
color: var(--color-font-main)
h1,h2,h3,h4,h5, h6
text-align: center
h1
font-size: var(--font-h2)
margin: 24px 0
letter-spacing: -0.5px
font-weight: 700
line-height: 120%
@media (min-width: 768px)
font-size: var(--font-h1)
h2
font-size: var(--font-h2)
letter-spacing: -0.5px
font-weight: 700
line-height: 120%
h3
font-size: var(--font-h3)
h4
font-size: var(--font-h4)
h5
font-size: var(--font-h5)
text-transform: none
font-weight: 700
line-height: 133.4%
h6
font-size: var(--font-h6)
.subheader1
font-size: var(--font-subheader1)
font-weight: bold
letter-spacing: 0.15px
text-decoration: underline
.subheader2
font-size: var(--font-subheader2)
font-weight: bold
letter-spacing: 0.1px
.number-big
font-size: var(--font-size-number-big)
letter-spacing: 4.8px
.number-small
font-size: var(--font-size-number-small)
letter-spacing: 20%
.navigation-font
font-weight: 500
font-size: var(--font-size-navigation)
.button-font
font-weight: 500
font-size: var(--font-size-navigation)
.body2
font-size: var(--font-body2)
font-weight: 400
letter-spacing: 0.17px
.caption
font-size: var(--font-caption)
letter-spacing: 1px
[data-theme="dark"]
--color-primary: black
--color-primary--hovered: var(--color-black-hovered)
--color-secondary: black
--color-steel-blue: #4682B4
--color-based-blue: #1A54F4
--color-bg: black
--color-font: white
--color-black: white
--color-dark-grey: #1F1F1F
--color-contrast-lower: hsl(0, 0%, 95%)
--color-contrast-low: hsl(240, 1%, 83%)
--color-contrast-medium: hsl(240, 1%, 48%)
--color-contrast-high: hsl(240, 4%, 20%)
--color-contrast-higher: white
--color-disabled: #A1A3A7
--color-bg-disabled: #D6D6D6
--border-radius: 12px
--color-navbar-bg: var(--color-midnight-black)
--color-navbar-font: var(--color-white)
--color-navbar-border: var(--color-white)
--color-font-main: var(--color-white)
--color-card-bg: var(--color-midnight-black)
--color-card-font: var(--color-white)
--color-card-box-shadow: 0px 0px 30px 0px rgba(255, 255, 255, 0.2)
--color-tab-bg: var(--color-text-primary)
--color-tab-bg--active: var(--color-text-primary)
--color-output-bg: var(--color-dark-grey)
--color-social-font: var(--color-white)
--color-social-border: var(--color-white)
--color-collapse-font: var(--color-white)
--color-collapse-bg: var(--color-dark-grey)
--color-collapse-border: var(--color-white)
--color-expand-icon: var(--color-white)
--chart-button-background-color: var(--color-primary)
--chart-button-background-color--hovered: var(--color-primary--hovered)
.container
margin-right: auto
margin-left: auto
padding-right: 16px
padding-left: 16px
box-sizing: content-box
// &.container--variant1
// @media (min-width: 768px)
// max-width: 800px
// padding: 24px 64px
@media (min-width: 768px)
max-width: 500px
.container2
@extend .container
@media (min-width: 768px)
max-width: 650px
.container3
@extend .container
@media (min-width: 768px)
max-width: 750px
.container4
@extend .container
@media (min-width: 768px)
max-width: 1100px
.row
display: flex
flex-direction: row
>*
flex: 1 1 auto
body
background-color: var(--color-primary)
margin: 0
*
box-sizing: border-box
a
color: inherit
text-decoration: none

View file

@ -0,0 +1,37 @@
@use "./global.sass"
#app
display: flex
flex-direction: column
min-height: 100vh
gap: 64px
position: relative
margin-bottom: 80px
@media (min-width: 768px)
margin-bottom: 0
footer
margin-top: auto
.social-links
display: flex
gap: 24px
.Vue-Toastification__container
&.harb-toast-container
&.top-right
@media (min-width: 768px)
top: 80px
// right: 20px
// left: unset
.pointer
&:hover, &:active, &:focus
cursor: pointer
html
scroll-behavior: smooth
scroll-padding-top: 100px
body
background: radial-gradient(100.02% 132.74% at -4.63% 2.69%, rgba(117, 80, 174, 0.00) 46.5%, rgba(117, 80, 174, 0.50) 100%), radial-gradient(91.35% 111.13% at 12.96% 16.43%, rgba(117, 80, 174, 0.00) 46.5%, rgba(117, 80, 174, 0.50) 100%), var(--Color, #202020)

View file

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View file

@ -0,0 +1,487 @@
<template>
<div class="hold-inner">
<div class="stake-inner">
<template v-if="!statCollection.initialized">
<div>
<f-loader></f-loader>
</div>
</template>
<template v-else>
<div class="subheader2">Token Amount</div>
<FSlider :min="minStakeAmount" :max="maxStakeAmount" v-model="stake.stakingAmountNumber"></FSlider>
<div class="formular">
<div class="row row-1">
<f-input label="Staking Amount" class="staking-amount" v-model="stake.stakingAmountNumber">
<template v-slot:details>
<div class="balance">Balance: {{ maxStakeAmount.toFixed(2) }} $KRK</div>
<div @click="setMaxAmount" class="staking-amount-max">
<b>Max</b>
</div>
</template>
</f-input>
<Icon class="stake-arrow" icon="mdi:chevron-triple-right"></Icon>
<f-input
label="Owner Slots"
class="staking-amount"
disabled
:modelValue="`${stakeSlots}(${supplyFreeze?.toFixed(4)})`"
>
<template #info>
Slots correspond to a percentage of ownership in the protocol.<br /><br />1,000 Slots =
1% Ownership<br /><br />When you unstake you get the exact percentage of the current
$KRK total supply. When the total supply increased since you staked you get more tokens
back than before.
</template>
</f-input>
<!-- <f-select :items="adjustTaxRate.taxRates" label="Tax" v-model="taxRate">
<template v-slot:info>
The tax you have to pay to keep your staking position open. The tax is
calculated on a yearly basis but paid continuously.
</template>
</f-select> -->
</div>
<div class="row row-2">
<f-select :items="adjustTaxRate.taxRates" label="Tax" v-model="taxRate">
<template v-slot:info>
The yearly tax you have to pay to keep your slots open. The tax is paid when unstaking
or manually in the dashboard. If someone pays a higher tax they can buy you out.
</template>
</f-select>
<f-input label="Floor Tax" disabled :modelValue="floorTax">
<template v-slot:info>
This is the current minimum tax you have to pay to claim owner slots from other owners.
</template>
</f-input>
<f-input label="Positions Buyout" disabled :modelValue="snatchAblePositions.length">
<template v-slot:info>
This shows you the numbers of staking positions you buy out from current owners by
paying a higher tax. If you get bought out yourself by new owners you get paid out the
current market value of your position incl. your profits.
</template>
</f-input>
</div>
</div>
<f-button size="large" disabled block v-if="stake.state === 'NoBalance'">Insufficient Balance</f-button>
<f-button size="large" disabled block v-else-if="stake.stakingAmountNumber < minStakeAmount"
>Stake amount too low</f-button
>
<f-button
size="large"
disabled
block
v-else-if="
!openPositionsAvailable && stake.state === 'StakeAble' && snatchAblePositions.length === 0
"
>taxRate too low to snatch</f-button
>
<f-button
size="large"
block
v-else-if="stake.state === 'StakeAble' && snatchAblePositions.length === 0"
@click="stakeSnatch"
>Stake</f-button
>
<f-button
size="large"
block
v-else-if="stake.state === 'StakeAble' && snatchAblePositions.length > 0"
@click="stakeSnatch"
>Snatch and Stake</f-button
>
<f-button size="large" outlined block v-else-if="stake.state === 'SignTransaction'"
>Sign Transaction ...</f-button
>
<f-button size="large" outlined block v-else-if="stake.state === 'Waiting'">Waiting ...</f-button>
</template>
</div>
<!-- <template v-if="myActivePositions.length > 0 && status === 'connected'">
<h5>Your Active Positions</h5>
<collapse-active
v-for="position in myActivePositions"
:taxRate="position.taxRatePercentage"
:amount="position.amount"
:treshold="tresholdValue"
:id="position.positionId"
:position="position"
:key="position.id"
></collapse-active>
</template>
<template v-if="myClosedPositions.length > 0 && status === 'connected'">
<h5>History</h5>
<collapse-history
v-for="position in myClosedPositions"
:taxRate="position.taxRatePercentage"
:taxPaid="formatBigNumber(position.taxPaid, 18)"
:amount="position.amount"
:treshold="tresholdValue"
:id="position.positionId"
:key="position.id"
:position="position"
></collapse-history>
</template> -->
</div>
</template>
<script setup lang="ts">
import FButton from "@/components/fcomponents/FButton.vue";
import FInput from "@/components/fcomponents/FInput.vue";
import FSelect from "@/components/fcomponents/FSelect.vue";
import FLoader from "@/components/fcomponents/FLoader.vue";
import FSlider from "@/components/fcomponents/FSlider.vue";
import FOutput from "@/components/fcomponents/FOutput.vue";
import { Icon } from "@iconify/vue";
import { formatBigIntDivision, InsertCommaNumber, formatBigNumber, bigInt2Number } from "@/utils/helper";
// import StatsOutput from "@/components/molecules/StatsOutput.vue";
// import ChartJs from "@/components/ChartJs.vue";
// import CollapseActive from "@/components/collapse/CollapseActive.vue";
// import CollapseHistory from "@/components/collapse/CollapseHistory.vue";
// import { bytesToUint256, uint256ToBytes } from "harb-lib";
// import { getSnatchList } from "harb-lib/dist/";
import { formatUnits } from "viem";
import axios from "axios";
import { useAccount } from "@wagmi/vue";
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
import { useStake } from "@/composables/useStake";
import { useClaim } from "@/composables/useClaim";
import { useAdjustTaxRate } from "@/composables/useAdjustTaxRates";
import { assetsToShares } from "@/contracts/stake";
import { getMinStake } from "@/contracts/harb";
import { useWallet } from "@/composables/useWallet";
import { ref, onMounted, watch, computed, inject, watchEffect } from "vue";
import { useStatCollection, loadStats } from "@/composables/useStatCollection";
import { useRoute } from "vue-router";
const demo = sessionStorage.getItem("demo") === "true";
const route = useRoute();
const adjustTaxRate = useAdjustTaxRate();
const activeTab = ref("stake");
const StakeMenuOpen = ref(false);
// let minStakeAmount = ref();
const taxRate = ref<number>(1.0);
// const positions = ref<Array<any>>([]);
const loading = ref<boolean>(true);
const stakeSnatchLoading = ref<boolean>(false);
const stake = useStake();
const claim = useClaim();
const wallet = useWallet();
const statCollection = useStatCollection();
const { activePositions } = usePositions();
const floorTax = ref(1);
const minStake = ref(0n);
const stakeSlots = ref();
const supplyFreeze = ref<number>(0);
let debounceTimer: ReturnType<typeof setTimeout>;
watchEffect(() => {
console.log("supplyFreeze");
if (!stake.stakingAmount) {
supplyFreeze.value = 0;
return;
}
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
stake.stakingAmountShares = await assetsToShares(stake.stakingAmount);
const stakingAmountSharesNumber = bigInt2Number(stake.stakingAmountShares, 18);
const stakeableSupplyNumber = bigInt2Number(statCollection.stakeableSupply, 18);
minStake.value = await getMinStake();
console.log(stakingAmountSharesNumber / stakeableSupplyNumber);
supplyFreeze.value = stakingAmountSharesNumber / stakeableSupplyNumber;
}, 500); // Verzögerung von 500ms
});
const openPositionsAvailable = computed(() => {
if (
bigInt2Number(statCollection.outstandingStake, 18) + bigInt2Number(stake.stakingAmountShares, 18) <=
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2
) {
return true;
} else {
return false;
}
});
watchEffect(() => {
console.log("stakeSlots");
stakeSlots.value = (supplyFreeze.value * 1000)?.toFixed(2);
});
// const stakeAbleHarbAmount = computed(() => statCollection.harbTotalSupply / 5n);
//war das mal so, wurde das geändert --> funktioniert nicht mehr
// const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
const tokenIssuance = computed(() => {
if (statCollection.harbTotalSupply === 0n) {
return 0n;
}
return (statCollection.nettoToken7d / statCollection.harbTotalSupply) * 100n;
});
function getMinFloorTax() {
const taxRate = activePositions.value.reduce((min, item) => {
return item.taxRate < min ? item.taxRate : min;
}, 1);
return taxRate * 100;
}
async function stakeSnatch() {
if (snatchAblePositions.value.length === 0) {
await stake.snatch(stake.stakingAmount, taxRate.value);
} else {
const snatchAblePositionsIds = snatchAblePositions.value.map((p: Position) => p.positionId);
await stake.snatch(stake.stakingAmount, taxRate.value, snatchAblePositionsIds);
}
stakeSnatchLoading.value = true;
await new Promise((resolve) => setTimeout(resolve, 10000));
await loadActivePositions();
await loadStats();
stakeSnatchLoading.value = false;
}
watch(
route,
async (to) => {
console.log("to", to.hash);
if (to.hash === "#stake") {
console.log("StakeMenuOpen", StakeMenuOpen.value);
StakeMenuOpen.value = true;
// if(element){
// element.scrollIntoView({behavior: "smooth"});
// }
// if(ele){
// window.scrollTo(ele.offsetLeft,ele.offsetTop);
// }
}
},
{ flush: "pre", immediate: true, deep: true }
);
onMounted(async () => {
try {
//on AccountChange
//TODO
// await getTotalSupply();
// await getTotalSupplyHarb();
// await getOutstandingSupply();
minStake.value = await getMinStake();
stake.stakingAmountNumber = minStakeAmount.value;
} catch (error) {
console.error("error", error);
} finally {
loading.value = false;
}
});
// async function getGraphData(): Promise<Array<any>> {
// let res = await axios.post("https://api.studio.thegraph.com/query/47986/harb/version/latest", {
// query: "query MyQuery {\n positions {\n id\n lastTaxTime\n owner\n share\n status\n taxRate\n creationTime\n }\n}",
// });
// return res.data.data.positions;
// }
// const result = useReadContract({
// abi: StakeContract.abi,
// address: StakeContract.contractAddress,
// functionName: "minStake",
// args: [],
// });
// minStakeAmount = result.data;
const minStakeAmount = computed(() => {
console.log("minStake", minStake.value);
return bigInt2Number(minStake.value, 18);
});
const maxStakeAmount = computed(() => {
if (wallet.balance?.value) {
// return Number(balance?.value?.value / 10n ** BigInt(balance?.value!.decimals));
console.log("wallet.balance.value", wallet.balance);
console.log("formatBigIntDivision(wallet.balance.value, 10n ** 18n)", bigInt2Number(wallet.balance.value, 18));
return bigInt2Number(wallet.balance.value, 18);
} else {
return 0;
}
});
watch(
minStakeAmount,
async (newValue) => {
console.log("newValue", newValue);
if (newValue > stake.stakingAmountNumber && stake.stakingAmountNumber === 0) {
stake.stakingAmountNumber = minStakeAmount.value;
}
},
{ immediate: true }
);
function setMaxAmount() {
console.log("maxStakeAmount.value", maxStakeAmount.value);
stake.stakingAmountNumber = maxStakeAmount.value;
}
const snatchAblePositions = ref<Array<any>>([]);
watchEffect(async () => {
console.log("watchEffect - snatchAblePositions");
let outStandingStake = statCollection.outstandingStake;
let positions = activePositions.value;
let selectedTaxRate = taxRate.value;
// Prüfe, ob bereits offene Positionen verfügbar sind, dann kein Snatching erforderlich
if (openPositionsAvailable.value) {
snatchAblePositions.value = [];
floorTax.value = getMinFloorTax();
return;
}
console.log("bigInt2Number(outStandingStake, 18)", bigInt2Number(outStandingStake, 18));
console.log("bigInt2Number(stake.stakingAmountShares, 18)", bigInt2Number(stake.stakingAmountShares, 18));
console.log(
"(bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2)",
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2
);
// **1. Berechnung der Differenz**
const difference =
bigInt2Number(outStandingStake, 18) +
bigInt2Number(stake.stakingAmountShares, 18) -
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2;
console.log("difference", difference);
// **2. Potentiell snatchbare Positionen holen**
const snatchAble = positions.filter((obj: Position) => {
if (demo) {
return obj.taxRatePercentage < selectedTaxRate;
}
return obj.taxRatePercentage < selectedTaxRate && !obj.iAmOwner;
// return obj.taxRatePercentage < selectedTaxRate;
});
console.log("snatchAblePositions", snatchAble);
if (snatchAble.length === 0) {
snatchAblePositions.value = [];
floorTax.value = getMinFloorTax();
return;
}
// 3. Positionen nach taxRate sortieren (aufsteigend)
const sortedPositions = [...snatchAble].sort((a, b) => a.taxRatePercentage - b.taxRatePercentage);
// 4. Die 50 % der niedrigsten Positionen auswählen
const halfIndex = Math.floor(sortedPositions.length / 2);
let candidatePositions = sortedPositions.slice(0, halfIndex);
// Falls zu wenig vorhanden, nimm gleich alle
if (candidatePositions.length === 0) {
candidatePositions = sortedPositions;
}
// 5. Zufällige Reihenfolge nur innerhalb der günstigeren Hälfte
const shuffled = [...candidatePositions].sort(() => Math.random() - 0.5);
let selectedPositions: Position[] = [];
let accumulatedStake = 0;
const minStakeShares = await assetsToShares(minStake.value);
const minStakeShareNumber = bigInt2Number(minStakeShares, 18);
// Erstversuch: mit der günstigen Hälfte
for (const position of shuffled) {
const harbDepositShares = await assetsToShares(position.harbDeposit);
const harbDepositNumber = bigInt2Number(harbDepositShares, 18);
if (accumulatedStake <= difference) {
selectedPositions.push(position);
accumulatedStake += harbDepositNumber;
}
if (accumulatedStake >= difference) break;
}
// Fallback: restliche Positionen sortiert von günstig nach teuer
if (accumulatedStake < difference && candidatePositions.length < sortedPositions.length) {
const remainingPositions = sortedPositions.slice(halfIndex); // bereits sortiert!
for (const position of remainingPositions) {
const harbDepositShares = await assetsToShares(position.harbDeposit);
const harbDepositNumber = bigInt2Number(harbDepositShares, 18);
if (!selectedPositions.includes(position)) {
selectedPositions.push(position);
accumulatedStake += harbDepositNumber;
}
if (accumulatedStake >= difference) break;
}
}
console.log("selectedPositions", selectedPositions);
snatchAblePositions.value = selectedPositions;
// **Berechnung von `floorTax.value`**
if (selectedPositions.length > 0) {
const taxRatePosition = Math.max(...selectedPositions.map((p) => p.taxRateIndex)) +1;
floorTax.value = adjustTaxRate.taxRates[taxRatePosition].year;
} else {
floorTax.value = getMinFloorTax();
}
});
</script>
<style lang="sass">
.hold-inner
.stake-inner
display: flex
flex-direction: column
gap: 24px
.formular
display: flex
flex-direction: column
gap: 8px
.row
>*
flex: 1 1 auto
.row-1
gap: 12px
>:nth-child(2)
flex: 0 0 auto
.staking-amount
.f-input--details
display: flex
gap: 8px
justify-content: flex-end
color: #9A9898
font-size: 14px
.staking-amount-max
font-weight: 600
&:hover, &:active, &:focus
cursor: pointer
.row-2
justify-content: space-between
>*
flex: 0 0 30%
// >:nth-child(2)
// flex: 0 0 22%
// >:nth-child(3)
// flex: 0 1 28%
.stake-arrow
align-self: center
font-size: 30px
</style>

View file

@ -0,0 +1,57 @@
<template>
<div class="stats-output" :styles="styles">
<f-card>
<h6>{{ props.headline }}</h6>
<f-output :name="props.name" :price="props.price">
<template #price>
<slot name="price"></slot>
</template>
</f-output>
</f-card>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import FCard from "@/components/fcomponents/FCard.vue"
import FOutput from "@/components/fcomponents/FOutput.vue"
interface Props {
name?: string;
headline: string;
price: string | number;
width?: number;
}
interface Styles {
width?: string;
}
const props = withDefaults(defineProps<Props>(), {});
const styles = computed(() => {
const returnObject: Styles = {};
if (props.width) {
returnObject.width = `${props.width}px`;
}
return returnObject;
});
</script>
<style lang="sass">
.stats-output
.f-card
background-color: var(--color-secondary)
h6
margin: 0
font-size: 16px
letter-spacing: 0.15px
font-weight: 300
.f-card__body
display: flex
flex-direction: column
gap: 8px
padding: 8px
height: 100%
.token-price-wrapper
padding: 8px 16px
</style>

View file

@ -0,0 +1,121 @@
<template>
<f-card :bg-color="bgcolor" :border-width="0" box-shadow="unset">
<div class="info-popup">
<div class="info-popup__header">
<h6>{{ props.header }}</h6>
</div>
<div class="info-popup__body">
<div>
{{ props.subheader }}
</div>
<div v-if="props.value">
<span class="number-big">{{ props.value }}</span
>&nbsp;<span>{{ props.token }}</span>
</div>
</div>
<div v-if="!props.value">
<hr />
</div>
<div class="info-popup__body2" v-if="props.info" v-html="props.info"></div>
<div class="info-popup__footer">
<f-button light block small @click="closeToast">Okay</f-button>
</div>
</div>
<!-- <div class="body">
<h6 class="header">test{{ props.header }}</h6>
<div class="subheader toast-header">test{{ props.subheader }}</div>
<div class="amount-number">
<div class="token-amount number-mobile">test{{ props.value }}</div>
<div class="token-name">test{{ props.token }}</div>
</div>
<div class="info toast-body">test{{ props.info }}</div>
</div> -->
</f-card>
</template>
<script setup lang="ts">
import { getCurrentInstance, computed } from "vue";
import FButton from "@/components/fcomponents/FButton.vue";
import FCard from "@/components/fcomponents/FCard.vue";
import { useToast } from "vue-toastification";
import type { ToastID } from "vue-toastification/dist/types/types";
const props = defineProps({
value: String,
header: String,
subheader: String,
info: String,
token: String,
type: String,
});
const bgcolor = computed(() => {
let color = "white";
console.log("props.type");
console.log(props.type);
switch (props.type) {
case "info":
color = "#5f4884";
break;
case "error":
color = "#8B0000";
break;
default:
break;
}
return color;
});
// element.classList.add("toast-open");
const toast = useToast();
const instance = getCurrentInstance();
instance!.parent!.parent!.vnode!.el!.classList.add("toast-open");
const id = instance!.attrs["toast-id"] as ToastID;
console.log("instance", instance!.attrs["toast-id"]);
function closeToast() {
instance!.parent!.parent!.vnode!.el!.classList.remove("toast-open");
toast.dismiss(id);
}
</script>
<style lang="sass">
.info-popup
width: 342px
display: flex
color: var(--color-white)
flex-direction: column
gap: 16px
font-size: var(--font-body1)
text-align: center
hr
border: 1px solid var(--color-grey)
.info-popup__header
h6
margin: 0
color: var(--color-white)
.info-popup__body
display: flex
flex-direction: column
gap: 8px
.Vue-Toastification__container
&.toast-open
background-color: rgb(15, 15, 15, 0.7)
height: 100vh
width: 100vw
position: fixed
left: 0
bottom: 0
z-index: 10
@media (min-width: 992px)
background-color: unset
&.top-center
@media (min-width: 600px)
left: unset
margin-left: unset
.Vue-Toastification__toast
box-shadow: unset
&.modal-overlay
background-color: unset
</style>

View file

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View file

@ -0,0 +1,68 @@
<template>
<chart-js
:snatchedPositions="snatchPositions.map((obj) => obj.id)"
:positions="activePositions"
:dark="darkTheme"
></chart-js>
</template>
<script setup lang="ts">
import ChartJs from "@/components/chart/ChartJs.vue";
import {bigInt2Number, formatBigIntDivision} from "@/utils/helper";
import { computed, ref } from "vue";
import { useStatCollection } from "@/composables/useStatCollection";
import { useStake } from "@/composables/useStake";
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
import { useDark } from "@/composables/useDark";
const { darkTheme } = useDark();
const { activePositions, myActivePositions, tresholdValue, myClosedPositions, createRandomPosition } = usePositions();
const ignoreOwner = ref(false);
const taxRate = ref<number>(1.0);
const minStakeAmount = computed(() => {
console.log("minStake", minStake.value);
return formatBigIntDivision(minStake.value, 10n ** 18n);
});
const stakeAbleHarbAmount = computed(() => statCollection.harbTotalSupply / 5n);
const minStake = computed(() => stakeAbleHarbAmount.value / 600n);
const stake = useStake();
const statCollection = useStatCollection();
const snatchPositions = computed(() => {
if (
bigInt2Number(statCollection.outstandingStake, 18) + stake.stakingAmountNumber <=
bigInt2Number(statCollection.harbTotalSupply, 18) * 0.2
) {
return [];
}
//Differenz aus outstandingSupply und totalSupply bestimmen, wie viel HARB kann zum Snatch verwendet werden
const difference =
bigInt2Number(statCollection.outstandingStake, 18) +
stake.stakingAmountNumber -
bigInt2Number(statCollection.harbTotalSupply, 18) * 0.2;
console.log("difference", difference);
//Division ohne Rest, um zu schauen wie viele Positionen gesnatched werden könnten
const snatchAblePositionsCount = Math.floor(difference / minStakeAmount.value);
//wenn mehr als 0 Positionen gesnatched werden könnten, wird geschaut wie viele Positionen in Frage kommen
if (snatchAblePositionsCount > 0) {
const snatchAblePositions = activePositions.value.filter((obj: Position) => {
if (ignoreOwner.value) {
return obj.taxRatePercentage < taxRate.value;
}
return obj.taxRatePercentage < taxRate.value && !obj.iAmOwner;
});
const slicedArray = snatchAblePositions.slice(0, snatchAblePositionsCount);
return slicedArray;
}
return [];
});
</script>

View file

@ -0,0 +1,464 @@
<template>
<div class="positions-graph">
<div class="chart-modal" :class="{ 'chart--fullscreen': fullscreenOpen }" @click="toggleFullscreen">
<div class="chart-inner" @click.stop>
<div class="chart--header">
<div class="chart--actions" v-if="props.positions?.length > 0">
<reset-zoom-button @click="resetZoom"></reset-zoom-button>
<fullscreen-button v-if="isMobile" @click="toggleFullscreen"></fullscreen-button>
</div>
</div>
<div
class="chart--body"
:class="{ 'disable-actions': props.positions?.length === 0, dark: props.dark }"
>
<canvas ref="Chart1"></canvas>
<template v-if="props.positions?.length === 0">
<p class="chart--no-positions">No positions</p>
</template>
</div>
</div>
</div>
<div class="tooltipRef" ref="tooltipRef" :class="{ iAmOwner: activePosition?.iAmOwner }">
<template v-if="activePosition?.iAmOwner">
<p>Your staking position</p>
</template>
<template v-else>
<p>Staking position</p>
</template>
<b>ID {{ activePosition?.id }}</b>
<b>{{ activePosition?.amount }} $KRK</b>
<b>Tax {{ activePosition?.taxRatePercentage }} %</b>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch, shallowRef, computed } from "vue";
import {
BarController,
BarElement,
CategoryScale,
Chart,
LineController,
LineElement,
LinearScale,
PointElement,
Tooltip,
} from "chart.js";
// import { Chart } from "chart.js";
import zoomPlugin from "chartjs-plugin-zoom";
import { useAccount } from "@wagmi/vue";
import { useMobile } from "@/composables/useMobile";
import type { Position } from "@/composables/usePositions";
import FullscreenButton from "@/components/chart/FullscreenButton.vue";
import ResetZoomButton from "@/components/chart/ResetZoomButton.vue";
Chart.register(
zoomPlugin,
LinearScale,
CategoryScale,
BarController,
BarElement,
LineController,
LineElement,
PointElement,
Tooltip
);
interface Props {
positions: Array<Position>;
snatchedPositions: Array<string>;
dark?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
dark: false,
});
const Chart1 = ref();
const fullscreenOpen = ref(false);
const myChart = ref();
const activePosition = ref();
const tooltipRef = ref();
const positionSnatched = ref();
const account = useAccount();
const isMobile = useMobile();
function resetZoom() {
console.log("resetZoom", { Chart1, myChart });
myChart.value.value.resetZoom();
}
function toggleFullscreen() {
if (fullscreenOpen.value) {
document.body.style.position = "unset";
document.body.style.overflow = "unset";
} else {
document.body.style.overflow = "hidden";
document.body.style.position = "relative";
}
fullscreenOpen.value = !fullscreenOpen.value;
window.scrollTo(0, 0);
}
watch(
() => props.positions,
(newData) => {
console.log("props.positions", props.positions);
myChart.value.value.destroy();
renderChart(props.positions);
// myChart.value.value.update();
// myChart.value.datasets[0].bars[0].fillColor = "green"; //bar 1
},
{
deep: true,
}
);
watch(
() => props.dark,
(newData) => {
myChart.value.value.destroy();
renderChart(props.positions);
// myChart.value.value.update();
// myChart.value.datasets[0].bars[0].fillColor = "green"; //bar 1
}
);
watch(
() => props.snatchedPositions,
(newData) => {
// myChart.value.value.destroy();
// renderChart(props.positions);
let positionIndex = 0;
if (myChart.value.value.data?.datasets[0]) {
let backgroundColorArray = myChart.value.value.data.datasets[0].backgroundColor;
for (let index = 0; index < backgroundColorArray.length; index++) {
const position: Position = myChart.value.value.data.datasets[0].data[index];
if (!position) {
continue;
}
if (!position.iAmOwner) {
positionIndex++;
}
if (positionIndex <= props.snatchedPositions.length && props.snatchedPositions.includes(position.id)) {
backgroundColorArray[index] = "##7550AE";
} else {
backgroundColorArray[index] = "##7550AE";
// const position = myChart.value.value.data.datasets[0].data[index];
if (position.iAmOwner) {
backgroundColorArray[index] = "#7550AE";
} else if (props.dark) {
backgroundColorArray[index] = "white";
} else {
backgroundColorArray[index] = "black";
}
}
}
myChart.value.value.data.datasets[0].backgroundColor = backgroundColorArray;
myChart.value.value.ctx.save();
myChart.value.value?.update();
}
},
{
deep: true,
}
);
const externalTooltipHandler = (context: any) => {
const { chart, tooltip } = context;
const tooltipEl = tooltipRef.value;
// Tooltip ausblenden, wenn keine Daten angezeigt werden sollen
if (!tooltip.opacity) {
tooltipEl.style.opacity = "0";
tooltipEl.style.display = "none";
return;
}
// Aktive Position setzen (Daten des angezeigten Punktes)
activePosition.value = tooltip.dataPoints[0].element.$context.raw;
// Positionierung des Tooltips
const { offsetLeft: chartX, offsetTop: chartY } = chart.canvas;
// Tooltip anpassen
tooltipEl.style.opacity = "1";
tooltipEl.style.display = "flex";
tooltipEl.style.position = "absolute";
// Tooltip mittig über dem Punkt platzieren
tooltipEl.style.left = `${chartX + tooltip.caretX}px`;
tooltipEl.style.top = `${chartY + tooltip.y}px`;
// Tooltip für saubere Mitte ausrichten
tooltipEl.style.transform = "translateX(-50%)";
tooltipEl.style.pointerEvents = "none";
};
function renderChart(data: any) {
console.log("renderChart");
const backgroundColors = [];
const data1 = data.map((obj: any) => {
return {
...obj,
// taxRatePercentage: obj.taxRate * 100,
iAmOwner: obj.owner?.toLowerCase() === account.address.value?.toLowerCase(),
};
});
console.log("data1", data1);
for (let index = 0; index < data1.length; index++) {
const position = data1[index];
// if(index < props.snatchedPositions){
// backgroundColors.push("rgba(26,84,244, 0.5)");
// positionSnatched.value = index;
// }
if (position.iAmOwner) {
backgroundColors.push("rgba(117,80,174, 0.8)");
} else if (props.dark) {
backgroundColors[index] = "white";
} else {
backgroundColors[index] = "black";
}
}
console.log("backgroundColors", backgroundColors);
myChart.value = shallowRef(
new Chart(Chart1.value, {
type: "bar",
data: {
labels: data1.map((row: any) => row.id),
datasets: [
{
type: "line",
label: "TaxRate",
data: data1,
backgroundColor: ["#7550AE"],
borderColor: ["#7550AE"],
yAxisID: "y",
parsing: {
yAxisKey: "taxRatePercentage",
xAxisKey: "id",
},
},
{
type: "bar",
label: "Amount",
data: data1,
backgroundColor: backgroundColors,
yAxisID: "y1",
parsing: {
yAxisKey: "amount",
xAxisKey: "id",
},
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 1,
},
interaction: {
intersect: false,
mode: "index",
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: "nearest",
external: externalTooltipHandler,
},
zoom: {
pan: {
enabled: true,
mode: "xy",
},
limits: {
y: { min: 0 },
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true,
},
mode: "xy",
onZoomComplete(object: any) {
// This update is needed to display up to date zoom level in the title.
// Without this, previous zoom level is displayed.
// The reason is: title uses the same beforeUpdate hook, and is evaluated before zoom.
object.chart?.update("none");
},
},
},
},
scales: {
y: {
// type: "linear",
display: true,
position: "left",
max: Math.max(...data.map((o: any) => o.taxRate)) * 100 * 1.5,
// min: 0,
title: {
display: true,
text: "Tax",
color: "#7550AE",
font: {
size: 16, // Hier die Schriftgröße ändern (z. B. 16px)
weight: "bold", // Falls du den Text fett machen möchtest
},
},
},
y1: {
// type: "linear",
display: true,
position: "right",
max: Math.max(...data.map((o: any) => o.amount)) * 1.5,
title: {
display: true,
text: "Slots",
color: "white",
font: {
size: 16, // Hier die Schriftgröße ändern (z. B. 16px)
weight: "bold", // Falls du den Text fett machen möchtest
},
},
grid: {
display: false,
},
// min: 0,
},
x: {
display: true,
ticks: {
display: false,
},
grid: {
display: false,
},
title: {
display: true,
text: "Positions",
color: "white",
font: {
size: 16, // Hier die Schriftgröße ändern (z. B. 16px)
weight: "bold", // Falls du den Text fett machen möchtest
},
},
},
},
},
plugins: [],
} as any)
);
}
onMounted(() => {
renderChart(props.positions);
});
</script>
<style lang="sass">
$margin: 72px
.positions-graph
width: 100%
.tooltipRef
position: absolute
background-color: rgba(133, 133, 133, 1)
padding: 8px
border-radius: 12px
display: flex
flex-direction: column
opacity: 0
display: none
pointer-events: none
z-index: 20
color: white
&.iAmOwner
background-color: var(--color-based-blue)
.chart-modal
&.chart--fullscreen
background-color: rgba(0, 0, 0, 0.32)
position: absolute
top: 0
height: 100vh
width: 100%
left: 0
overflow: hidden
z-index: 11
.chart-inner
background-color: var(--color-card-bg)
height: auto
display: flex
flex-direction: column
transition: all 200ms ease-in-out
@media (min-width: 992px)
//height: calc( 100% - $margin*2)
width: 90%
margin: auto
min-height: 100%
//margin: $margin
.chart--body
flex: 1 1 auto
height: 100%
canvas
height: auto
.chart-inner
.chart--header
flex: 0 0 50px
color: red
padding: 8px
display: flex
.chart--actions
gap: 12px
margin-left: auto
display: flex
.chart--body
height: 300px !important
position: relative
&.disable-actions
pointer-events: none
&::before
content: ''
position: absolute
top: 0
left: 0
width: 100%
height: 100%
background-color: rgba(255, 255, 255, 0.5)
z-index: 1
&.dark
&::before
background-color: rgba(0, 0, 0, 0.5)
.chart--no-positions
color: var(--color-font)
font-size: 30px
margin: auto
padding: 16px
text-align: center
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
z-index: 2 /* Ensure this is above the background overlay */
</style>

View file

@ -0,0 +1,30 @@
<template>
<button class="chart-button">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
class="stroke"
d="M10.8027 1.00195H16.002C16.3433 1.00195 16.6201 1.27869 16.6201 1.62006V6.81927"
stroke-width="1.85431"
/>
<path
class="stroke"
d="M16.6191 10.5273L16.6191 15.7266C16.6191 16.0679 16.3424 16.3447 16.001 16.3447L10.8018 16.3447"
stroke-width="1.85431"
/>
<path
class="stroke"
d="M7.0918 16.3457L1.89258 16.3457C1.55121 16.3457 1.27448 16.069 1.27448 15.7276L1.27448 10.5284"
stroke-width="1.85431"
/>
<path
class="stroke"
d="M1.27539 6.81836L1.27539 1.61914C1.27539 1.27777 1.55213 1.00104 1.8935 1.00104L7.09271 1.00104"
stroke-width="1.85431"
/>
</svg>
</button>
</template>
<style lang="sass">
@use 'style'
</style>

View file

@ -0,0 +1,22 @@
<template>
<button class="chart-button">
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_3412_52485" fill="white">
<path
d="M12.9797 15.2305C11.7286 16.2335 10.2045 16.8366 8.60589 16.9613C7.00726 17.086 5.4081 16.7265 4.01664 15.9296C2.62518 15.1327 1.50588 13.9353 0.804464 12.4934C0.103046 11.0515 -0.14799 9.43173 0.0840395 7.84511C0.316069 6.2585 1.02041 4.77849 2.10537 3.59778C3.19033 2.41708 4.60564 1.59038 6.16702 1.22532C7.72841 0.860254 9.36354 0.973741 10.8595 1.551C12.3555 2.12826 13.643 3.14256 14.5545 4.46182L13.109 5.46048C12.3981 4.4315 11.3938 3.64038 10.227 3.19013C9.0602 2.73989 7.78485 2.65137 6.56702 2.93611C5.34918 3.22084 4.24529 3.86565 3.39905 4.78656C2.55282 5.70747 2.00345 6.86183 1.82248 8.09935C1.6415 9.33686 1.8373 10.6002 2.38439 11.7249C2.93147 12.8495 3.80449 13.7835 4.88979 14.405C5.97508 15.0266 7.22237 15.307 8.46926 15.2097C9.71615 15.1124 10.9049 14.642 11.8807 13.8597L12.9797 15.2305Z"
/>
</mask>
<path
d="M12.9797 15.2305C11.7286 16.2335 10.2045 16.8366 8.60589 16.9613C7.00726 17.086 5.4081 16.7265 4.01664 15.9296C2.62518 15.1327 1.50588 13.9353 0.804464 12.4934C0.103046 11.0515 -0.14799 9.43173 0.0840395 7.84511C0.316069 6.2585 1.02041 4.77849 2.10537 3.59778C3.19033 2.41708 4.60564 1.59038 6.16702 1.22532C7.72841 0.860254 9.36354 0.973741 10.8595 1.551C12.3555 2.12826 13.643 3.14256 14.5545 4.46182L13.109 5.46048C12.3981 4.4315 11.3938 3.64038 10.227 3.19013C9.0602 2.73989 7.78485 2.65137 6.56702 2.93611C5.34918 3.22084 4.24529 3.86565 3.39905 4.78656C2.55282 5.70747 2.00345 6.86183 1.82248 8.09935C1.6415 9.33686 1.8373 10.6002 2.38439 11.7249C2.93147 12.8495 3.80449 13.7835 4.88979 14.405C5.97508 15.0266 7.22237 15.307 8.46926 15.2097C9.71615 15.1124 10.9049 14.642 11.8807 13.8597L12.9797 15.2305Z"
stroke="white"
stroke-width="3.54886"
mask="url(#path-1-inside-1_3412_52485)"
/>
<path d="M16.0005 6.89955L13.89 0.404049L9.32 5.47956L16.0005 6.89955Z" fill="white" />
</svg>
</button>
</template>
<style lang="sass">
@use 'style'
</style>

View file

@ -0,0 +1,18 @@
.chart-button
padding: 0 18px
border-radius: 12px
display: flex
align-items: center
border: none
height: 32px
background-color: var(--chart-button-background-color)
transition: .3s ease
svg
path
&.stroke
stroke: white
&.fill
fill: white
&:hover, &:active
background-color: var(--chart-button-background-color--hovered)
cursor: pointer

View file

@ -0,0 +1,261 @@
<template>
<f-collapse class="f-collapse-active" @collapse:opened="loadActivePositionData" :loading="loading">
<template v-slot:header>
<div class="collapse-header">
<div class="collapse-header-row1">
<div><span class="subheader2">Tax</span> {{ props.taxRate }} %</div>
<f-button size="tiny" @click="payTax(props.id)">Pay Tax</f-button>
<div class="position-id">
<span class="subheader2">ID</span> <span class="number-small">{{ props.id }}</span>
</div>
</div>
<div class="collapse-header-row2">
<div>
<div class="profit-stats-item">
<div><b>Initial Stake</b></div>
<div>{{ compactNumber(props.amount) }} $KRK</div>
</div>
</div>
<div class="tags-list">
<f-tag v-if="tag">{{ tag }}</f-tag>
</div>
</div>
<!-- <div class="collapse-amount">
<span class="number-small">{{ compactNumber(props.amount) }}</span>
<span class="caption"> $KRK</span>
</div> -->
</div>
</template>
<div class="collapsed-body">
<div class="profit-stats-wrapper">
<div class="profit-stats-item">
<div><b>Tax Paid</b></div>
<div>{{ taxPaidGes }} $KRK</div>
</div>
<div class="profit-stats-item">
<div><b>Issuance Earned</b></div>
<div>{{ profit }} $KRK</div>
</div>
<div class="profit-stats-item profit-stats-total">
<div><b>Total</b></div>
<div>{{ total.toFixed(5) }} $KRK</div>
</div>
</div>
</div>
<div class="collapsed-body--actions">
<div :class="{ 'collapse-menu-open': showTaxMenu }">
<f-button size="small" dense block outlined v-if="adjustTaxRate.state === 'SignTransaction'"
>Sign Transaction ...</f-button
>
<f-button
size="small"
dense
outlined
block
v-else-if="adjustTaxRate.state === 'Waiting'"
@click="unstakePosition"
>Waiting ...</f-button
>
<f-button
size="small"
dense
block
v-else-if="adjustTaxRate.state === 'Action' && !showTaxMenu"
@click="showTaxMenu = true"
>Adjust Tax Rate</f-button
>
<template v-else>
<div class="collapse-menu-input">
<f-select :items="filteredTaxRates" v-model="newTaxRate"> </f-select>
</div>
<div>
<f-button size="small" dense @click="changeTax(props.id, newTaxRate)">Confirm</f-button>
<f-button size="small" dense outlined @click="showTaxMenu = false">Cancel</f-button>
</div>
</template>
</div>
<div></div>
<div>
<f-button size="small" dense block outlined v-if="unstake.state === 'SignTransaction'"
>Sign Transaction ...</f-button
>
<f-button size="small" dense outlined block v-else-if="unstake.state === 'Waiting'"
>Waiting ...</f-button
>
<f-button size="small" dense block v-else-if="unstake.state === 'Unstakeable'" @click="unstakePosition"
>Unstake</f-button
>
</div>
</div>
</f-collapse>
</template>
<script setup lang="ts">
import FButton from "@/components/fcomponents/FButton.vue";
import FTag from "@/components/fcomponents/FTag.vue";
import FSelect from "@/components/fcomponents/FSelect.vue";
import FCollapse from "@/components/fcomponents/FCollapse.vue";
import { compactNumber, formatBigNumber } from "@/utils/helper";
import { useUnstake } from "@/composables/useUnstake";
import { useAdjustTaxRate } from "@/composables/useAdjustTaxRates";
import { computed, ref, onMounted } from "vue";
import { getTaxDue, payTax } from "@/contracts/stake";
import { type Position, loadPositions } from "@/composables/usePositions";
import { useStatCollection } from "@/composables/useStatCollection";
import { formatUnits } from "viem";
const unstake = useUnstake();
const adjustTaxRate = useAdjustTaxRate();
const statCollection = useStatCollection();
const props = defineProps<{
taxRate: number;
treshold: number;
id: bigint;
amount: number;
position: Position;
}>();
const showTaxMenu = ref(false);
const newTaxRate = ref<number>(1);
const taxDue = ref<bigint>();
const taxPaidGes = ref<string>();
const profit = ref<number>();
const loading = ref<boolean>(false);
const tag = computed(() => {
if (props.taxRate < props.treshold) {
return "Low Tax!";
}
return "";
});
const total = computed(() => props.amount + profit.value! + -taxPaidGes.value!);
async function changeTax(id: bigint, newTaxRate: number) {
await adjustTaxRate.changeTax(id, newTaxRate);
showTaxMenu.value = false;
}
async function unstakePosition() {
console.log(props.id);
await unstake.exitPosition(props.id);
loading.value = true;
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("loadPositions begin");
await loadPositions();
console.log("loadPositions end");
loading.value = false;
}
async function loadActivePositionData() {
console.log("loadActivePositionData", props.position);
//loadTaxDue
taxDue.value = await getTaxDue(props.id);
console.log("taxDue", taxDue.value);
taxPaidGes.value = formatBigNumber(taxDue.value + BigInt(props.position.taxPaid), 18);
console.log("loadActivePositionData", taxPaidGes.value);
//loadTotalSupply
const multiplier =
Number(formatUnits(props.position.totalSupplyInit, 18)) /
Number(formatUnits(statCollection.harbTotalSupply, 18));
console.log("props.position.totalSupplyInit", props.position.totalSupplyInit);
console.log("multiplier", multiplier);
profit.value =
Number(formatUnits(statCollection.harbTotalSupply, 18)) * multiplier -
Number(formatUnits(statCollection.harbTotalSupply, 18));
}
onMounted(() => {
const taxRate = adjustTaxRate.taxRates.find((obj) => obj.index === props.position.taxRateIndex + 1);
if (taxRate) {
newTaxRate.value = taxRate.year;
}
});
const filteredTaxRates = computed(() => adjustTaxRate.taxRates.filter((obj) => obj.year > props.taxRate));
</script>
<style lang="sass">
@use 'collapse'
.f-collapse
&.f-collapse-active
background-color: #07111B
.collapse-header
width: 100%
display: flex
justify-content: space-between
flex-direction: column
position: relative
gap: 16px
.collapse-header-row1
display: flex
flex-direction: row
gap: 16px
align-items: center
// margin-right: 32px
.position-id
margin-left: auto
.collapse-header-row2
display: flex
justify-content: space-between
>div
width: 50%
min-height: 22px
.profit-stats-item
display: grid
grid-template-columns: 1fr 1fr
gap: 8px
div
&:last-child
text-align: right
.tags-list
margin-right: 32px
text-align: right
.collapsableDiv
.collapsed-body
display: flex
flex-direction: column
margin-right: 32px
.collapsed-body--header
justify-content: space-between
.profit-stats-wrapper
display: flex
flex-direction: column
gap: 4px
margin: 0
margin-top: 4px
width: 100%
color: white
@media (min-width: 768px)
width: 50%
margin: 4px auto 0 0
.profit-stats-item
display: grid
grid-template-columns: 1fr 1fr
gap: 8px
div
&:last-child
text-align: right
&.profit-stats-total
border-top: 2px solid var(--color-font)
padding-top: 2px
.collapsed-body--actions
.collapse-menu-open
display: flex
align-items: center
gap: 4px
flex: 1 1 auto
.collapse-menu-input
display: flex
align-items: center
</style>

View file

@ -0,0 +1,63 @@
<template>
<f-collapse class="f-collapse-history">
<template v-slot:header>
<div class="collapse-header">
<div><span class="subheader2">Tax</span> {{ props.taxRate }} %</div>
<div>
<span class="subheader2">ID</span> <span class="number-small">{{ props.id }}</span>
</div>
<div class="collapse-amount">
<span class="number-small">{{ compactNumber(props.amount) }}</span>
<span class="caption"> $KRK</span>
</div>
</div>
</template>
<div class="collapsed-body history">
<div>
<span class="subheader2">Tax paid</span><span class="number-small">{{ props.taxPaid }}</span
><span class="caption"> $KRK</span>
</div>
<div>
<span class="subheader2">Profit</span><span class="number-small">{{ profit }}</span
><span class="caption"> $KRK</span>
</div>
</div>
</f-collapse>
</template>
<script setup lang="ts">
import type { Position } from "@/composables/usePositions";
import FCollapse from "@/components/fcomponents/FCollapse.vue";
import { compactNumber } from "@/utils/helper";
import { formatUnits } from "viem";
import { computed } from "vue";
const props = defineProps<{
taxRate: number;
taxPaid: string;
treshold: number;
id: bigint;
amount: number;
position: Position;
}>();
const profit = computed(() => {
const multiplier =
Number(formatUnits(props.position.totalSupplyInit, 18)) /
Number(formatUnits(props.position.totalSupplyEnd!, 18));
console.log("multiplier", multiplier);
console.log("props.position.amount", props.position.amount);
return (
Number(formatUnits(props.position.totalSupplyEnd!, 18)) * multiplier -
Number(formatUnits(props.position.totalSupplyEnd!, 18))
);
});
</script>
<style lang="sass">
@use 'collapse'
.f-collapse
&.f-collapse-history
.collapse-header, .collapsableDiv
margin-right: 32px
</style>

View file

@ -0,0 +1,27 @@
.f-collapse-inner
gap: 12px
.collapse-header
display: grid
width: 100%
grid-template-columns: auto auto auto auto auto
@media (min-width: 768px)
grid-template-columns: 20% 30% 30% auto 20%
.collapse-amount
grid-column: 5
text-align: right
.collapsableDiv
.collapsed-body
display: flex
justify-content: space-between
>div
display: flex
gap: 8px
&.history
flex-direction: row
.collapsed-body--actions
margin-top: 8px
display: flex
>div
flex: 1 1 auto
@media (min-width: 768px)
flex: 1 1 25%

View file

@ -0,0 +1,112 @@
<template>
<button class="f-btn" :class="classObject" :style="styleObject">
<slot></slot>
</button>
</template>
<script setup lang="ts">
interface Props {
size?: string;
dense?: boolean;
disabled?: boolean;
invert?: boolean;
block?: boolean;
outlined?: boolean;
bgColor?: string;
light?: boolean;
dark?: boolean;
}
import { computed } from "vue";
const props = withDefaults(defineProps<Props>(), {
size: "medium",
});
const classObject = computed(() => ({
"f-btn--tiny": props.size === "tiny",
"f-btn--small": props.size === "small",
"f-btn--medium": props.size === "medium",
"f-btn--large": props.size === "large",
"f-btn--dense": props.dense,
"f-btn--disabled": props.disabled,
"f-btn--invert": props.invert,
"f-btn--block": props.block,
"f-btn--outlined": props.outlined,
"f-btn--light": props.light,
"f-btn--dark": props.dark,
}));
const styleObject = computed(() => {
const returnObject: any = {};
if (props.bgColor) {
returnObject["background-color"] = props.bgColor;
}
return returnObject;
});
</script>
<style lang="sass">
@use 'sass:color'
.f-btn
padding: var(--btn-padding, 16px 32px)
gap: 8px
border-radius: var(--border-radius, 12px)
border: none
display: inline-flex
text-decoration: none
background-color: var(--color-button-bg)
color: var(--color-button-font, white)
letter-spacing: 0.15px
transition: color .25s
transition: background-color .25s
justify-content: center
font-weight: bold
font-size: var(--font-size-button-medium)
&.f-btn--tiny
font-size: 13px
letter-spacing: 0.46px
padding: 4px 16px
&.f-btn--small
font-size: var(--font-size-button-small)
letter-spacing: 0.46px
&.f-btn--large
font-size: var(--font-size-button-large)
&.f-btn--disabled
background-color: var(--color-bg-disabled, #D6D6D6)
color: var(--color-disabled, #A1A3A7)
&:hover,&:focus,&:active
background-color: var(--color-bg-disabled, #D6D6D6)
color: var(--color-disabled, #A1A3A7)
cursor: not-allowed
&.f-btn--dense
padding: var(--btn-dense-padding, 8px 16px)
&.f-btn--invert
background-color: var(--btn-color, white)
color: var(--color-button-bg)
border: 2px solid var(--color-button-bg)
&:hover,&:focus,&:active
background-color: color.adjust(white, $lightness: -20%)
&.f-btn--light
background-color: var(--color-white, white)
color: var(--color-secondary)
border: 2px solid var(--color-white)
&:hover,&:focus,&:active
background-color: color.adjust(white, $lightness: -20%)
border: 2px solid var(--color-white)
&.f-btn--block
display: flex
flex: 1 0 auto
min-width: 100%
&.f-btn--outlined
background-color: transparent
color: var(--color-button-bg)
border: 1px solid var(--color-button-border--outlined)
&:hover,&:focus,&:active
cursor: pointer
background-color: var(--color)
&:hover,&:focus,&:active
cursor: pointer
background-color: #5f4884
</style>

View file

@ -0,0 +1,57 @@
<template>
<div class="f-card" ref="fCard" :style="computedStyles">
<div v-if="props.title" class="f-card__title">
<h5>
{{ props.title }}
</h5>
</div>
<div class="f-card__body">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
const fCard = ref();
interface Props {
bgColor?: string;
borderWidth?: number;
boxShadow?: string;
title?: string;
}
const props = withDefaults(defineProps<Props>(), {});
const computedStyles = computed(() => ({
"border-width": props.borderWidth,
"background-color": props.bgColor,
"box-shadow": props.boxShadow,
}));
onMounted(() => {
// if (props.bgcolor) {
// fCard.value.style["background-color"] = props.bgcolor;
// }
});
</script>
<style lang="sass">
.f-card
border-radius: var(--card-border-radius)
overflow: hidden
height: 100%
box-shadow: var(--color-card-box-shadow)
background: var(--color-card-bg)
color: var(--color-card-font)
h6
color: var(--color-card-font)
.f-card__title
border-bottom: 2px solid var(--color-based-blue)
color: var(--color-based-blue)
padding: 12px
.f-card__body
padding: 16px
</style>

View file

@ -0,0 +1,97 @@
<template>
<div class="f-collapse">
<div class="f-collapse-wrapper" :class="{loading: props.loading}">
<template v-if="loading">
<f-loader></f-loader>
</template>
<div class="f-collapse-inner">
<slot name="header"></slot>
<!-- <img class="toggle-collapse" src="../assets/expand-less.svg?url" alt="expand less" /> -->
<Icon v-if="isShow" class="toggle-collapse" icon="mdi:chevron-down" @click="openClose"></Icon>
<Icon v-else icon="mdi:chevron-up" class="toggle-collapse" @click="openClose"></Icon>
<!-- <img
class="toggle-collapse"
src="../assets/expand-more.svg?url"
alt="expand more"
v-else
/> -->
</div>
<Transition name="collapse">
<div v-if="isShow" class="collapsableDiv">
<slot></slot>
</div>
</Transition>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import {Icon} from "@iconify/vue";
import FLoader from "@/components/fcomponents/FLoader.vue";
const emit = defineEmits(["collapse:opened", "collapse:closed"]);
const props = defineProps<{
loading?: boolean;
}>();
const isShow = ref(false);
const openClose = () => {
isShow.value = !isShow.value;
if(isShow.value){
emit("collapse:opened")
} else{
emit("collapse:closed")
}
};
</script>
<style lang="sass">
.f-collapse
border-radius: var(--border-radius)
border: 1px solid var(--color-collapse-border)
background-color: var(--color-collapse-bg)
.f-collapse-wrapper
padding: 8px 16px
&.loading
opacity: 0.5
position: relative
pointer-events: none
.cube
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
.f-collapse-inner
display: flex
position: relative
align-items: center
.toggle-collapse
font-size: 25px
align-self: flex-start
g
path
fill: var(--color-expand-icon)
&:hover, &:active, &:focus
cursor: pointer
.collapsableDiv
color: var(--color-collapse-font)
.collapse-enter-active
animation: collapse reverse 500ms ease
.collapse-leave-active
animation: collapse 500ms ease
@keyframes collapse
from
max-height: 200px
overflow: hidden
to
max-height: 0px
overflow: hidden
</style>

View file

@ -0,0 +1,135 @@
<template>
<div class="f-input" :class="classObject">
<div class="f-input-label subheader2">
<label v-if="props.label" :for="name">{{ props.label }}</label>
<icon>
<template v-slot:text v-if="slots.info">
<slot name="info"></slot>
</template>
</icon>
</div>
<div class="f-input__wrapper" ref="inputWrapper" @click="setFocus">
<input
:disabled="props.disabled"
:readonly="props.readonly"
:type="props.type"
:name="name"
:id="name"
@input="updateModelValue"
:value="props.modelValue"
/>
<div class="f-input--suffix" v-if="slots.suffix">
<slot name="suffix" >test </slot>
</div>
</div>
<div class="f-input--details" v-if="slots.details">
<slot name="details"> </slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, getCurrentInstance, useSlots, ref } from "vue";
import useClickOutside from "@/composables/useClickOutside"
import Icon from "@/components/icons/IconInfo.vue";
interface Props {
size?: string;
info?: string;
label?: string;
disabled?: boolean;
modelValue?: any;
type?: string;
readonly?: boolean
}
const slots = useSlots();
const inputWrapper = ref();
const instance = getCurrentInstance();
const name = `f-input-${instance!.uid}`;
const props = withDefaults(defineProps<Props>(), {
size: "normal",
disabled: false,
readonly: false,
type: "string",
});
useClickOutside(inputWrapper, () => {
removeFocus();
})
const emit = defineEmits(["update:modelValue"]);
function updateModelValue($event: any) {
emit("update:modelValue", $event.target.value);
}
const classObject = computed(() => ({
"f-input--normal": props.size === "normal",
"f-input--big": props.size === "big",
"f-input--disabled": props.disabled,
}));
function removeFocus(){
const target = inputWrapper.value as HTMLElement
target.classList.remove("f-input__wrapper--focused")
}
function setFocus(event:MouseEvent){
console.log("setFocus");
if(props.disabled){
return
}
const target = inputWrapper.value as HTMLElement
target.classList.add("f-input__wrapper--focused")
}
</script>
<style lang="sass">
.f-input
display: flex
flex-direction: column
.info-icon
position: static
@media (min-width: 768px)
position: relative
.f-input-label
display: flex
align-items: center
gap: 8px
.f-input__wrapper
display: flex
border: 1px solid black
border-radius: 12px
width: 100%
background-color: #2D2D2D
&.f-input__wrapper--focused
outline: none
border: 1px solid #7550AE
box-shadow: 0 0 5px #7550AE
input
border: 0
border-radius: 12px
padding: 8px 12px
width: 100%
background-color: #2D2D2D
color: #FFFFFF
outline: none
&.f-input--big
font-size: 18px
&.f-input--normal
font-size: 16px
&.f-input--disabled
input
background-color: #202020
color: #F0F0F0
.f-input--suffix
display: flex
align-items: center
margin-right: 10px
</style>

View file

@ -0,0 +1,77 @@
<template>
<div class="cube">
<div class="side"></div>
<div class="side"></div>
<div class="side"></div>
<div class="side"></div>
<div class="side"></div>
<div class="side"></div>
</div>
</template>
<style lang="sass">
.cube
margin: auto
font-size: 24px
height: 1em
width: 1em
position: relative
transform: rotatex(30deg) rotatey(45deg)
transform-style: preserve-3d
animation: cube-spin 1.5s infinite ease-in-out alternate
.side
position: absolute
top: 0
bottom: 0
left: 0
right: 0
transform-style: preserve-3d
&::before
content: ""
position: absolute
top: 0
bottom: 0
left: 0
right: 0
background-color: currentcolor
transform: translatez(0.5em)
animation: cube-explode 1.5s infinite ease-in-out
opacity: 0.5
&:nth-child(1)
transform: rotatey(90deg)
&:nth-child(2)
transform: rotatey(180deg)
&:nth-child(3)
transform: rotatey(270deg)
&:nth-child(4)
transform: rotatey(360deg)
&:nth-child(5)
transform: rotatex(90deg)
&:nth-child(6)
transform: rotatex(270deg)
@keyframes cube-spin
0%
transform: rotatex(30deg) rotatey(45deg)
100%
transform: rotatex(30deg) rotatey(405deg)
@keyframes cube-explode
0%
transform: translatez(0.5em)
50%
transform: translatez(0.75em)
100%
transform: translatez(0.5em)
</style>

View file

@ -0,0 +1,73 @@
<template>
<div
class="output-wrapper"
:class="{
'no-name': !props.name,
'output--variant-1': props.variant === 1,
'output--variant-2': props.variant === 2,
'output--variant-3': props.variant === 3,
}"
>
<div class="output-left">
<div class="output-text">{{ props.name }}</div>
<div class="output-price">
<slot name="price">
{{ props.price }}
</slot>
</div>
</div>
<div class="output-right" v-if="slots.end">
<slot name="end"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots } from "vue";
const props = defineProps({
name: String,
price: [String, Number],
variant: {
type: Number,
required: false,
default: 1,
},
});
const slots = useSlots();
</script>
<style lang="sass">
.output-wrapper
height: 100%
display: flex
flex-direction: row
&.no-name
gap: 0
align-items: center
justify-content: center
&.output--variant-1
padding: 16px
border: 1px solid #2D2D2D
border-radius: var(--border-radius, 12px)
background-color: var(--color-input)
&.output--variant-2
border-bottom: 1px solid var(--color-grey)
padding: 8px
&.output--variant-3
padding: 8px
.output-left
display: flex
flex-direction: column
gap: 8px
text-align: left
.output-price
font-size: 18px
letter-spacing: 3px
.output-text
font-size: 14px
font-weight: bold
.output-right
margin-left: auto
align-self: center
</style>

View file

@ -0,0 +1,234 @@
<template>
<div class="o-select" @click="clickSelect" ref="componentRef">
<f-input hide-details :clearable="props.clearable" :label="props.label" :selectedable="false"
:focus="showList" :modelValue="`${year} % yearly`" readonly>
<template #info v-if="slots.info">
<slot name="info"></slot>
</template>
<template #suffix>
<Icon class="toggle-collapse" v-if="showList" icon="mdi:chevron-down"></Icon>
<Icon class="toggle-collapse" v-else icon="mdi:chevron-up"></Icon>
</template>
</f-input>
<div class="select-list-wrapper" v-show="showList" ref="selectList" >
<div class="select-list-inner" @click.stop>
<div class="select-list-item" v-for="(item, index) in props.items" :key="item.year" :class="{'active': year === item.year, 'hovered': activeIndex === index}"
@click.stop="clickItem(item)" @mouseenter="mouseEnter($event, index)" @mouseleave="mouseLeave($event, index)">
<div class="circle">
<div class="active" v-if="year === item.year"></div>
<div class="hovered" v-else-if="activeIndex === index"></div>
</div>
<div class="yearly">
<div class="value">{{ item.year }} %</div>
<div class="label">yearly</div>
</div>
<div class="daily">
<div class="value">{{ item.daily.toFixed(4) }} %</div>
<div class="label">daily</div>
</div>
</div>
</div>
</div>
</div>
<slot :style="[!props.editor ? {display: 'none'} : {}]"></slot>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, getCurrentInstance, type ComputedRef, useSlots } from "vue"
import FInput from "@/components/fcomponents/FInput.vue"
import useClickOutside from "@/composables/useClickOutside"
import {Icon} from "@iconify/vue";
const emit = defineEmits(['update:modelValue'])
const slots = useSlots();
interface Item {
year: number;
daily: number;
}
const props = defineProps({
items: {
type: [Array<Item>],
required: false,
default: []
},
clearable: {
type: Boolean,
required: false,
default: false
},
itemTitle: {
type: String,
required: false,
default: "label"
},
itemValue: {
type: String,
required: false,
default: "value"
},
label: {
type: String,
required: false,
default: null
},
modelValue: {
type: Number,
required: false,
default: null
},
value: {
type: String,
required: false,
default: null
},
editor: {
type: Boolean,
required: false,
default: false
}
});
const showList = ref(<boolean>false);
const slotElements = ref(<any[]>[])
const componentRef = ref(<any>null);
const selectList = ref();
const activeIndex= ref();
useClickOutside(componentRef, () => {
showList.value = false
})
const year= computed({
// getter
get() {
return props.modelValue || props.items[0].year
},
// setter
set(newValue: number) {
emit("update:modelValue", newValue)
}
})
function mouseEnter(event:MouseEvent , index:number){
const target = event. target as HTMLElement;
activeIndex.value = index;
target.classList.add("active")
}
function mouseLeave(event:MouseEvent, index:number){
const elements = selectList.value.querySelectorAll('.select-list-item');
elements.forEach((element:HTMLElement) => {
element.classList.remove("active")
});
}
onMounted(() => {
})
function clickSelect(event: any) {
showList.value = !showList.value;
}
function clickItem(item: any) {
console.log("item", item);
year.value = item.year
showList.value = false;
console.log("showList.value", showList.value);
// emit('input', item)
}
</script>
<style lang="sass">
.o-select
position: relative
display: inline-block
.toggle-collapse
color: black
font-size: 20px
&:active, &:focus, &:hover
cursor: pointer
.select-list-wrapper
position: absolute
box-shadow: 0 1px 6px 0 #20212447
background-color: var(--color-card-bg)
z-index: 10
padding: 16px
border-radius: 16px
border: 1px solid black
right: 0
margin-top: 4px
.select-list-inner
height: 330px
overflow-y: scroll
display: grid
.select-list-item
display: grid
grid-template-columns: 15px 110px 126px
place-content: end
margin-right: 8px
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
padding: 4px 0
&:active, &:focus, &:hover
cursor: pointer
.daily, .yearly
display: flex
gap: 4px
justify-content: end
.daily
color: var(--color-grey)
.circle
height: 14px
width: 14px
.active
height: 14px
width: 14px
background-color: var(--color-based-blue)
border-radius: 14px
.hovered
height: 14px
width: 14px
background-color: var(--color-based-blue)
opacity: .5
border-radius: 14px
.select-list-inner
&::-webkit-scrollbar-track
border-radius: 10px
&::-webkit-scrollbar
width: 5px
&::-webkit-scrollbar-thumb
border-radius: 10px
background-color: lightgrey
</style>

View file

@ -0,0 +1,259 @@
<template>
<div>
<div class="slider-wrapper">
<div class="disabled-slider" :style="{ 'flex-basis': minPercentage + '%' }">
<div class="dot"></div>
</div>
<div class="range-slider">
<input
class="range-slider__range"
type="range"
ref="sliderInput"
:style="{
background: `linear-gradient(90deg, ${settings.fill} ${percentageDot}%, ${settings.background} ${percentageDot + 0.1}%)`,
}"
:value="props.modelValue"
:min="props.min"
:max="props.max"
@input="updateModelValue"
/>
<div
class="testbla"
@mousemove="testMove"
:style="{
left: percentageDot + '%',
}"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, computed } from "vue";
const sliderInput = ref();
const emit = defineEmits(["update:modelValue"]);
const props = defineProps<{
modelValue: number;
min: number;
max: number;
}>();
const minPercentage = computed(() => {
if (!props.min || !props.max) {
return 0n;
}
return (props.min * 100) / props.max;
});
function updateModelValue(event: any) {
emit("update:modelValue", event.target.valueAsNumber);
}
const sliderValue = computed({
// getter
get() {
return props.modelValue || props.min;
},
// setter
set(newValue) {
emit("update:modelValue", newValue);
},
});
const percentageDot = computed(() => {
let percentage = (100 * (sliderValue.value - props.min)) / (props.max - props.min);
if(percentage < 20){
percentage = percentage + 1;
} else if(percentage > 50){
percentage = percentage - 2;
} else if(percentage > 80){
percentage = percentage - 3;
}
return percentage
});
const settings = {
fill: "#7550AE",
background: "#000000",
};
onMounted(() => {
sliderInput.value.addEventListener("input", setSliderValue);
});
function setSliderValue(event: any){
sliderValue.value = event.target.value;
}
function testMove(event: any) {
}
onUnmounted(() => {
console.log("sliderInput.value", sliderInput.value);
if(sliderInput.value){
sliderInput.value.removeEventListener("input", setSliderValue);
}
})
</script>
<style lang="scss">
.slider-wrapper {
display: flex;
}
.testbla {
position: absolute;
height: 25px;
width: 25px;
background-color: var(--color-based-blue);
user-select: none;
top: 50%;
transform: translate(-50%, -50%);
right: 50%;
border-radius: 25px;
pointer-events: none;
z-index: 10;
}
.disabled-slider {
width: 60px;
height: 7px;
background-color: var(--color-border-main);
/* margin-top: auto; */
align-self: center;
margin-top: 2px;
position: relative;
z-index: 3;
pointer-events: none;
}
.dot {
border-radius: 50px;
height: 15px;
width: 15px;
background-color: var(--color-border-main);
position: absolute;
right: -14px;
/* bottom: 50%; */
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
// Base Colors
$shade-10: #4682b4 !default;
$shade-1: #d7dcdf !default;
$shade-0: #fff !default;
$teal: #4682b4 !default;
// Range Slider
$range-width: 100% !default;
$range-handle-color: $shade-10 !default;
$range-handle-color-hover: $teal !default;
$range-handle-size: 15px !default;
$range-track-color: $shade-1 !default;
$range-track-height: 7px !default;
$range-label-color: $shade-10 !default;
$range-label-width: 60px !default;
.range-slider {
width: $range-width;
position: relative;
accent-color: #7550AE;
}
.range-slider__range {
// width: calc(100% - (#{$range-label-width + 13px}));
width: 100%;
height: $range-track-height;
border-radius: 5px;
background: $range-track-color;
outline: none;
padding: 0;
margin: 0;
// Range Handle
&::-webkit-slider-thumb {
appearance: none;
width: 30px;
height: 30px;
opacity: 0;
padding: 5px;
border-radius: 50%;
cursor: pointer;
}
&:active::-webkit-slider-thumb {
background: $range-handle-color-hover;
}
&::-moz-range-thumb {
width: $range-handle-size;
height: $range-handle-size;
border: 0;
border-radius: 50%;
background: $range-handle-color;
cursor: pointer;
transition: background 0.15s ease-in-out;
&:hover {
background: $range-handle-color-hover;
}
}
&:active::-moz-range-thumb {
background: $range-handle-color-hover;
}
// Focus state
&:focus {
&::-webkit-slider-thumb {
background-color: transparent;
}
}
}
// Range Label
.range-slider__value {
display: inline-block;
position: relative;
width: $range-label-width;
color: $shade-0;
line-height: 20px;
text-align: center;
border-radius: 3px;
background: $range-label-color;
padding: 5px 10px;
margin-left: 8px;
&:after {
position: absolute;
top: 8px;
left: -7px;
width: 0;
height: 0;
border-top: 7px solid transparent;
border-right: 7px solid $range-label-color;
border-bottom: 7px solid transparent;
content: "";
}
}
// Firefox Overrides
::-moz-range-track {
background: $range-track-color;
border: 0;
}
input::-moz-focus-inner,
input::-moz-focus-outer {
border: 0;
}
</style>

View file

@ -0,0 +1,14 @@
<template>
<div class="f-tag">
<slot></slot>
</div>
</template>
<style lang="sass">
.f-tag
border: 1px solid var(--color-font)
border-radius: var(--border-radius, 12px)
padding: 2px 16px
font-size: 12px
display: inline
</style>

View file

@ -0,0 +1,64 @@
<template>
<section v-show="value === props.name" ref="tab" role="tabpanel" tabindex="-1">
<slot></slot>
</section>
</template>
<script setup lang="ts">
import { onMounted, onBeforeMount, getCurrentInstance, ref, computed, watch, inject } from "vue";
const instance = ref<any>();
const isActive = ref<any>(null);
const value = inject("value");
var updateTab: any = inject("updateTab");
defineExpose({
isActive,
});
onBeforeMount(() => {});
onMounted(() => {
instance.value = getCurrentInstance();
var addTab: any = inject("addTab");
addTab({
uid: instance.value!.uid,
name: props.name,
label: props.label,
});
// resolved = {"test123": 123, "blub": "abc"}
});
// const test = computed(() => {
// console.log(instance.value);
// return instance.value?.parent
// })
//Props
const props = defineProps({
label: {
type: String,
required: true,
default: null,
},
name: {
type: [String],
required: true,
default: null,
},
});
watch(props, (newValue, oldValue) => {
console.log("newValue", newValue);
console.log("instance", instance.value);
updateTab({
uid: instance.value!.uid,
name: props.name,
label: props.label,
});
});
</script>

View file

@ -0,0 +1,126 @@
<template>
<div class="f-tabs" ref="tabsRef">
<div class="f-tabs__header" ref="headerRef">
<div
class="f-tab"
ref="tabsRef1"
:id="`f-tab-${tab.uid}`"
v-for="tab in tabs"
:key="tab.uid"
@click="setActive(tab)"
>
<h5>{{ tab.label }}</h5>
</div>
</div>
<div class="f-tabs__content">
<slot ref="tabsTest"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, h, computed, useSlots, provide, getCurrentInstance } from "vue";
defineOptions({
inheritAttrs: false,
});
const props = defineProps({
modelValue: {
type: String,
required: false,
default: null,
},
});
const emit = defineEmits(["update:modelValue"]);
const slots1 = useSlots();
const tabsRef = ref<HTMLElement>();
const tabsRef1 = ref<HTMLElement>();
const headerRef = ref<HTMLElement>();
const tabsArray = ref<Array<HTMLElement>>();
const tabsComponents = ref<Array<any>>();
const content = ref<any>();
const target = ref<any>();
const slots = ref<any>();
const value = ref<any>();
const tabsTest = ref<any>();
const tabs = ref<Array<any>>([]);
provide("value", value);
provide("addTab", (tab: any) => {
tabs.value.push(tab);
});
provide("updateTab", (tab: any) => {
let changedTabIndex = tabs.value.findIndex((obj) => obj.uid === tab.uid);
if (changedTabIndex > -1) {
tabs.value[changedTabIndex] = tab;
}
});
// provide('addTab', tabs.value)
onMounted(() => {
nextTick(() => {
if(props.modelValue){
const tab = tabs.value.find((obj) => obj.name === props.modelValue)
setActive(tab);
} else {
setActive(tabs.value[0]);
}
});
});
function setActive(tab: any) {
nextTick(() => {
const tabElement: any = headerRef.value?.querySelector(`#f-tab-${tab.uid}`);
var array = Array.prototype.slice.call(tabsRef.value?.children[0].children);
array.forEach((element: HTMLElement) => {
element.classList.remove("f-tab--active");
});
tabElement.classList.add("f-tab--active");
value.value = tab.name;
emit("update:modelValue", tab.name);
});
}
</script>
<style lang="sass">
.f-tabs
display: flex
flex-direction: column
width: 100%
.f-tabs__header
display: flex
.f-tab, label
cursor: pointer
.f-tab
padding: 0.6em 1em
transition: background-color .2s,color .2s
align-items: center
justify-content: center
font-weight: 400
text-transform: uppercase
font-size: 24px
font-weight: bold
border-width: 0 0 2px 0
border-style: solid
border-color: transparent
flex: 1 1 auto
text-align: center
height: 50px
border-bottom: 2px solid var(--color-light-grey)
h5
color: var(--color-tab-font)
margin: 0
&.f-tab--active
border-bottom: 3px solid var(--color-tab-border--active)
h5
color: var(--color-tab-border--active)
.f-tabs__content
padding: 16px 12px
@media (min-width: 768px)
padding: 16px 24px
</style>

View file

@ -0,0 +1,72 @@
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import FTabs from './FTabs.vue';
import FTab from './FTab.vue';
describe('FTabs.vue', () => {
it('renders tabs correctly', () => {
const wrapper = mount(FTabs, {
slots: {
default: `
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
`,
},
global: {
components: { FTab },
},
});
expect(wrapper.text()).toContain('Tab 1');
expect(wrapper.text()).toContain('Tab 2');
});
it('activates the first tab by default via v-model', async () => {
const wrapper = mount(FTabs, {
slots: {
default: `
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
`,
},
props: {
modelValue: 'tab1', // v-model initial value
},
global: {
components: { FTab },
},
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const sections = wrapper.findAll('section');
expect(sections[0].attributes('style')).not.toContain('display: none');
expect(sections[1].attributes('style')).toContain('display: none');
});
it('switches to the correct tab on click', async () => {
const wrapper = mount(FTabs, {
slots: {
default: `
<f-tab label="Tab 1" name="tab1">Tab 1</f-tab>
<f-tab label="Tab 2" name="tab2">Tab 2</f-tab>
`,
},
global: {
components: { FTab },
},
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const tabs = wrapper.findAll('.f-tab');
await tabs[1].trigger('click');
const sections = wrapper.findAll('section');
expect(sections[0].attributes('style')).toContain('display: none');
expect(sections[1].attributes('style')).not.toContain('display: none');
});
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,29 @@
<template>
<svg width="23" height="18" viewBox="0 0 23 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2590_494)">
<path
ref="svgPath"
d="M19.419 3.40166C16.7691 1.45664 14.2483 1.51053 14.2483 1.51053L13.9906 1.79865C17.1188 2.73512 18.5723 4.08593 18.5723 4.08593C16.6585 3.05941 14.7817 2.55501 13.0336 2.35694C11.7088 2.21276 10.4391 2.24892 9.3167 2.39287C9.20634 2.39287 9.11433 2.41083 9.00397 2.42879C8.35994 2.48292 6.79584 2.71692 4.82702 3.56357C4.14628 3.86966 3.74131 4.08593 3.74131 4.08593C3.74131 4.08593 5.2687 2.66303 8.58065 1.72656L8.39664 1.51053C8.39664 1.51053 5.87579 1.4564 3.22598 3.40166C3.22598 3.40166 0.576172 8.10242 0.576172 13.9018C0.576172 13.9018 2.12191 16.5134 6.18851 16.6393C6.18851 16.6393 6.86925 15.8289 7.42129 15.1446C5.08444 14.4601 4.20109 13.0195 4.20109 13.0195C4.20109 13.0195 4.3851 13.1454 4.71642 13.3256C4.73477 13.3435 4.75313 13.3615 4.79007 13.3797C4.84537 13.4156 4.90043 13.4338 4.95573 13.4697C5.41576 13.7219 5.87579 13.92 6.29911 14.0821C7.05351 14.3703 7.95521 14.6584 9.00397 14.8565C10.3841 15.1087 12.0032 15.1987 13.7697 14.8744C14.6344 14.7303 15.5178 14.4783 16.4378 14.0999C17.0819 13.8656 17.7996 13.5236 18.554 13.0372C18.554 13.0372 17.6339 14.514 15.2234 15.1805C15.7754 15.865 16.4378 16.6393 16.4378 16.6393C20.5047 16.5131 22.0688 13.9018 22.0688 13.9018C22.0688 8.10242 19.419 3.40166 19.419 3.40166ZM7.8818 12.2267C6.85138 12.2267 6.00498 11.3262 6.00498 10.2276C6.00498 9.12894 6.83303 8.22841 7.8818 8.22841C8.93056 8.22841 9.77697 9.12894 9.75861 10.2276C9.75861 11.3262 8.93056 12.2267 7.8818 12.2267ZM14.598 12.2267C13.5675 12.2267 12.7211 11.3262 12.7211 10.2276C12.7211 9.12894 13.5492 8.22841 14.598 8.22841C15.6467 8.22841 16.4748 9.12894 16.4748 10.2276C16.4748 11.3262 15.647 12.2267 14.598 12.2267Z"
:fill="props.color"
/>
</g>
<defs>
<clipPath id="clip0_2590_494">
<rect width="22" height="17" fill="white" transform="translate(0.333984 0.58667)" />
</clipPath>
</defs>
</svg>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const svgPath = ref();
interface Props {
color?: string;
}
const props = withDefaults(defineProps<Props>(), {});
</script>

View file

@ -0,0 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<!-- Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. -->
<path
d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"
/>
</svg>
</template>

View file

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path
d="M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z"
/>
</svg>
</template>

View file

@ -0,0 +1,62 @@
<template>
<tippy v-if="slots.text" theme="my-theme" trigger="click">
<div class="info-icon">
<svg :width="props.size" :height="props.size" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.2 6.26416C6.2 5.84995 6.53579 5.51416 6.95 5.51416C7.36421 5.51416 7.7 5.84995 7.7 6.26416V9.76416C7.7 10.1784 7.36421 10.5142 6.95 10.5142C6.53579 10.5142 6.2 10.1784 6.2 9.76416V6.26416Z"
/>
<path
d="M7 3.01416C7.55228 3.01416 8 3.46188 8 4.01416C8 4.56644 7.55228 5.01416 7 5.01416C6.44772 5.01416 6 4.56644 6 4.01416C6 3.46188 6.44772 3.01416 7 3.01416Z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 7.01416C14 10.8802 10.866 14.0142 7 14.0142C3.13401 14.0142 0 10.8802 0 7.01416C0 3.14817 3.13401 0.0141602 7 0.0141602C10.866 0.0141602 14 3.14817 14 7.01416ZM1.5 7.01416C1.5 10.0517 3.96243 12.5142 7 12.5142C10.0376 12.5142 12.5 10.0517 12.5 7.01416C12.5 3.97659 10.0376 1.51416 7 1.51416C3.96243 1.51416 1.5 3.97659 1.5 7.01416Z"
/>
</svg>
</div>
<template #content>
<slot name="text"></slot>
</template>
</tippy>
</template>
<script setup lang="ts">
import { useSlots, ref, onMounted } from "vue";
import { Tippy } from "vue-tippy";
import "tippy.js/dist/tippy.css"; // optional for styling
const slots = useSlots();
interface Props {
size?: string;
}
const props = withDefaults(defineProps<Props>(), {
size: "15px",
});
</script>
<style lang="sass">
.info-icon
vertical-align: middle
display: inline-flex
position: relative
svg
path
fill: var(--color-font)
.tippy-arrow
color: rgba(15, 15, 15, 0.9) !important
.tippy-box[data-theme~='my-theme']
background-color: rgba(15, 15, 15, 0.9)
height: fit-content
color: white
font-size: 12px
max-width: 200px !important
border-radius: var(--border-radius)
// @media (min-width: 768px)
// max-width: 400px
</style>

View file

@ -0,0 +1,14 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="login-icon" viewBox="0 0 512 512">
<path
d="M352 96l64 0c17.7 0 32 14.3 32 32l0 256c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0c53 0 96-43 96-96l0-256c0-53-43-96-96-96l-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-9.4 182.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L242.7 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l210.7 0-73.4 73.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l128-128z"
/>
</svg>
</template>
<style lang="sass">
.login-icon
width: 35px
path
fill: var(--color-font)
</style>

View file

@ -0,0 +1,33 @@
<template>
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2590_499)">
<path
ref="svgPath"
d="M17.058 0.932576L1.74462 6.83727C0.699293 7.25659 0.705631 7.83977 1.55407 8.09947L5.48317 9.32593L6.83449 13.7733C7.01217 14.2637 6.92458 14.4583 7.43975 14.4583C7.83718 14.4583 8.01274 14.2765 8.2346 14.0608L10.1441 12.2041L14.1166 15.1392C14.8477 15.5426 15.3754 15.3336 15.5575 14.4606L18.1654 2.17133C18.4324 1.10065 17.7574 0.615059 17.058 0.932576Z"
:fill="props.color"
/>
<path
d="M7.45616 13.2757L6.16016 9.01067L16.1361 3.09253L8.76406 10.3509L7.45616 13.2757Z"
:fill="(props.color === 'black' ? 'white' : 'black')"
/>
</g>
<defs>
<clipPath id="clip0_2590_499">
<rect width="18.125" height="14.5" fill="white" transform="translate(0.9375 0.83667)" />
</clipPath>
</defs>
</svg>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const svgPath = ref();
interface Props {
color?: string;
}
const props = withDefaults(defineProps<Props>(), {});
</script>

View file

@ -0,0 +1,30 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="20px"
height="18px"
viewBox="0 0 19 18"
version="1.1"
>
<g id="surface1">
<path
ref="svgPath"
d="M 14.945312 0 L 17.859375 0 L 11.464844 7.636719 L 18.9375 18 L 13.070312 18 L 8.480469 11.703125 L 3.222656 18 L 0.308594 18 L 7.085938 9.832031 L -0.0703125 0 L 5.941406 0 L 10.089844 5.753906 Z M 13.925781 16.207031 L 15.542969 16.207031 L 5.09375 1.726562 L 3.355469 1.726562 Z M 13.925781 16.207031 "
:fill="props.color"
/>
</g>
</svg>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const svgPath = ref();
interface Props {
color?: string;
}
const props = withDefaults(defineProps<Props>(), {});
</script>

View file

@ -0,0 +1,58 @@
<template>
<template v-if="status === 'disconnected'">
<f-button class="connect-button--disconnected">Connect</f-button>
</template>
<template v-else-if="status === 'connected'">
<f-button class="connect-button connect-button--connected">
<img :src="getBlocky(address!)" alt="avatar" />
<div>{{ getAddressShortName(address!) }}</div>
</f-button>
</template>
<template v-else>
<f-button class="connect-button--loading">loading</f-button>
</template>
</template>
<script setup lang="ts">
import FButton from "@/components/fcomponents/FButton.vue";
import { getAddressShortName } from "@/utils/helper";
import { useAccount } from "@wagmi/vue";
import { getBlocky } from "@/utils/blockies";
const { address, status } = useAccount();
</script>
<style lang="sass">
:root,[data-theme="default"]
--color-connect-button--disconnected: var(--color-primary)
--font-color-connect-button--disconnected: var(--color-bright-white)
--color-connect-button--connected: unset
--font-color-connect-button--connected: var(--color-bright-white)
--color-connect-button-border--connected: var(--color-grey)
--color-connect-button-hovered--connected: #060f18
[data-theme="dark"]
--color-connect-button--disconnected: var(--color-based-blue)
--font-color-connect-button--disconnected: var(--color-bright-white)
--color-connect-button--connected: var(--color-midnight-black)
--font-color-connect-button--connected: var(--color-bright-white)
--color-connect-button-border--connected: var(--color-very-light-grey)
--color-connect-button-hovered--connected: var(--color-midnight-black-hovered)
.f-btn
&.connect-button
padding: 8px
align-items: center
gap: 12px
&.connect-button--disconnected
// background-color: var(--color-midnight-black)
background-color: var(--color-connect-button--disconnected)
color: var(--font-color-connect-button--disconnected)
&.connect-button--connected
background-color: var(--color-connect-button--connected)
border: 1px solid var(--color-connect-button-border--connected)
color: var(--font-color-connect-button--connected)
&:hover, &:focus, &:active
background-color: #5f4884
cursor: pointer
</style>

View file

@ -0,0 +1,236 @@
<template>
<div class="connect-wallet-wrapper">
<template v-if="status === 'connected'">
<div class="connected-header">
<div class="connected-header-avatar">
<img :src="getBlocky(address!)" alt="avatar" />
</div>
<div class="connected-header-address" :title="address">
{{ getAddressShortName(address!) }}
</div>
<div class="connected-header-logout" @click="() => disconnect()">
<img src="@/assets/logout.svg" alt="Logout" />
</div>
</div>
<h6 class="connected-tokens-headline">Your Tokens</h6>
<div class="connected-tokens">
<f-output name="$KRK" :price="harbAmount" :variant="3">
<template #end>
<f-button size="small" dense
><a :href="chain.chainData?.uniswap" target="_blank">Buy</a></f-button
>
</template>
</f-output>
<f-output name="Staked $KRK" :price="compactNumber(stakedAmount)" :variant="3">
<template #end>
<f-button size="small" dense @click="stakeLink">Stake</f-button>
</template>
</f-output>
</div>
</template>
<template v-else-if="status === 'disconnected'">
<h6 class="connect-wallet-headline low-mid-text">Connect your wallet</h6>
<div class="connectors-wrapper">
<div
class="connectors-element card"
:class="[[connector.id]]"
v-for="connector in connectors"
:key="connector.id"
@click="connectWallet(connector, chainId)"
>
<div class="connector-icon">
<img :src="loadConnectorImage(connector)" :alt="`${connector.name} Logo`" />
</div>
<div class="connector-name">
{{ connector.name }}
</div>
</div>
</div>
<div>Are you coming from Binance, Bybit, Kucoin or any other crypto exchange?</div>
<div><u>Follow these instructions first.</u></div>
</template>
<template v-else>
<div>loading</div>
</template>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, computed } from "vue";
import { useRouter } from "vue-router";
import { getAddressShortName, compactNumber, formatBigIntDivision } from "@/utils/helper";
import { getBlocky } from "@/utils/blockies";
import FButton from "@/components/fcomponents/FButton.vue";
import FOutput from "@/components/fcomponents/FOutput.vue";
// import { usePositions } from "@/composables/usePositions";
// import { useWallet } from "@/composables/useWallet";
import { usePositions } from "@/composables/usePositions";
import { useWallet } from "@/composables/useWallet";
import { useChain } from "@/composables/useChain";
import {
useAccount,
useDisconnect,
useConnect,
useChainId,
useBalance,
type CreateConnectorFn,
type Connector,
} from "@wagmi/vue";
const { address, status } = useAccount();
const { disconnect } = useDisconnect();
const { connectors, connect, error } = useConnect();
const router = useRouter();
const chainId = useChainId();
const { myActivePositions } = usePositions();
const wallet = useWallet();
const chain = useChain();
function loadConnectorImage(connector: Connector) {
if (connector.icon) {
return connector.icon;
} else {
return `./img/connectors/${connector.name}.svg`;
}
}
// const goerliKey = import.meta.env.VITE_GOERLI_KEY;
const harbAmount = computed(() => {
if (wallet.balance?.value) {
return compactNumber(formatBigIntDivision(wallet.balance?.value, 10n ** BigInt(wallet.balance.decimals)));
} else {
return "0";
}
});
const stakedAmount = computed(() =>
myActivePositions.value.reduce(function (a, b) {
return a + b.amount;
}, 0)
);
const instance = getCurrentInstance();
// const connectors = computed(() => {
// console.log();
// return wallet.connectors.filter((obj) => obj.id !== "injected");
// });
// async function disconnect() {
// console.log("disconnectWallet");
// try {
// await wallet.disconnectWallet();
// instance.parent.emit("update:modelValue", false);
// } catch (error) {
// console.log("error", error);
// }
// }
// function badgeClick(token) {
// var tokenData = tokenStore.getToken(token.contractAddress);
// if (tokenStore.memeToken.contractAddress === token.contractAddress) {
// //buy
// window.open(
// `https://app.uniswap.org/swap?inputCurrency=ETH&outputCurrency=${token.contractAddress}&chain=goerli`,
// "_blank"
// );
// } else {
// router.push({ name: "home" });
// instance.parent.emit("update:modelValue", false);
// }
// }
function closeModal() {
instance!.parent!.emit("update:modelValue", false);
}
// //special case for metaMask, but I think that is the most used wallet
async function connectWallet(connector: CreateConnectorFn | Connector, chainId: any) {
console.log("connector", connector);
console.log("connector", connector.name);
connect({ connector, chainId });
closeModal();
}
function stakeLink() {
router.push("/dashboard#stake");
closeModal();
}
</script>
<style lang="sass">
@use 'sass:color'
.connect-wallet-wrapper
display: flex
flex-direction: column
gap: 24px
color: white
.connect-wallet-headline
text-align: left
.connectors-wrapper
display: flex
flex-direction: column
gap: 8px
.connectors-element
display: flex
padding: 12px
border: 1px solid rgba(152, 161, 192, 0.24)
background-color: white
color: var(--midnight-black, #0F0F0F)
&:hover, &:active, &:focus
background-color: color.adjust(white, $lightness: -20%)
cursor: pointer
.connector-icon
border-radius: 12px
width: 40px
overflow: hidden
height: 40px
img
width: 40px
.connector-name
padding: 0 8px
align-self: center
font-size: 16px
@media (min-width: 768px)
font-size: 18px
.connected-header
display: flex
gap: 16px
align-items: center
.connected-header-avatar
img
border-radius: 8px
.connected-header-logout
margin-left: auto
&:hover, &:active, &:focus
cursor: pointer
svg
font-size: 25px
.connected-tokens-headline
text-align: left
.connected-tokens
display: flex
flex-direction: column
gap: 16px
margin-bottom: 24px
.connected-token
display: flex
gap: 16px
align-items: center
.connected-token-button
margin-left: auto
.bloodline-badge
padding: 4px 16px
color: black
font-size: 14px
&:hover, &:active, &:focus
cursor: pointer
background-color: var(--grey-light)
</style>

View file

@ -0,0 +1,57 @@
<template>
<div class="footer" :style="{ color: (props.dark) ? 'white': 'black' }">
<div class="footer-inner">
<div class="footer-headline subheader2">Follow HARBERG Protocol</div>
<div class="social-links">
<social-button :dark="props.dark" type="discord" :href="discord"></social-button>
<social-button :dark="props.dark" type="telegram" :href="telegram"></social-button>
<social-button :dark="props.dark" type="twitter" :href="twitter"></social-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import socialButton from '@/components/socialButton.vue';
interface Props {
dark?: boolean;
}
const discord = import.meta.env.VITE_DISCORD
const telegram = import.meta.env.VITE_TELEGRAM;
const twitter = import.meta.env.VITE_TWITTER;
const props = withDefaults(defineProps<Props>(), {});
</script>
<style lang="sass">
.footer
height: auto
background-color: var(--midnight-black)
border-top: 0.5px solid
margin-top: auto
@media (min-width: 768px)
height: 150px
min-height: 150px
display: flex
flex-direction: column
justify-content: center
padding-bottom: unset
padding-bottom: 72px
.footer-inner
padding: 24px 0
font-size: 24px
display: flex
flex-direction: column
gap: 16px
align-items: center
border-top: 1px solid #000
@media (min-width: 768px)
padding: 16px 0 24px 0
justify-content: center
border-top: unset
background-color: transparent
.footer-headline subheader2
font-size: 16px
</style>

View file

@ -0,0 +1,197 @@
<template>
<header>
<div class="navbar">
<div class="navbar-inner navigation-font">
<div class="navbar-left" @click="router.push('/')">
<div class="navbar-title">
<span>K</span>
<span class="big-spacing">r</span>
<span class="small-spacing">A</span>
<span class="big-spacing">I</span>
<span>ken</span>
</div>
</div>
<div class="navbar-center"></div>
<div class="navbar-end">
<nav v-if="!isMobile">
<RouterLink v-for="navbarRoute in navbarRoutes" :key="navbarRoute.name" :to="navbarRoute.path">
{{ navbarRoute.title }}
</RouterLink>
<a href="https://emberspirit007.github.io/KraikenLanding/#/docs/Introduction" target="_blank">Docs</a>
</nav>
<template v-if="!isMobile">
<div class="vertical-line"></div>
<network-changer></network-changer>
<div class="vertical-line"></div>
</template>
<connect-button @click="showPanel = true" v-if="!isMobile"></connect-button>
<icon-login @click="showPanel = true" v-else-if="isMobile && !address"></icon-login>
<img
@click="showPanel = true"
v-else-if="isMobile && address"
:src="getBlocky(address)"
alt="avatar"
/>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { getBlocky } from "@/utils/blockies";
import { RouterLink, useRouter } from "vue-router";
import IconLogin from "@/components/icons/IconLogin.vue";
import ThemeToggle from "@/components/layouts/ThemeToggle.vue";
import NetworkChanger from "@/components/layouts/NetworkChanger.vue";
import ConnectButton from "@/components/layouts/ConnectButton.vue";
import { computed, inject, ref } from "vue";
import { useAccount } from "@wagmi/vue";
import { useDark } from "@/composables/useDark";
const {darkTheme} = useDark();
const {address} = useAccount();
const router = useRouter();
const showPanel = inject<boolean>("showPanel", false);
const isMobile = inject<boolean>("isMobile", false);
const dark1 = ref(false)
const routes = router.getRoutes();
const navbarRoutes = routes.flatMap((obj) => {
if (obj.meta.group === "navbar") {
return { title: obj.meta.title, name: obj.name, path: obj.path };
} else {
return [];
}
});
</script>
<style lang="sass">
header
z-index: 10
position: fixed
width: 100%
.navbar
height: 80px
z-index: 3
box-sizing: border-box
transform: translate3d(0,0,0)
background-color: var(--color-navbar-bg)
transition: transform .3s ease,visibility 0s .3s linear
border-bottom: 1px solid var(--color-navbar-border)
.navbar-inner, .navbar-end, .navbar-center, .navbar-text
align-items: center
.navbar-inner
display: flex
padding: 16px 24px
box-sizing: border-box
height: 100%
align-items: center
@media (min-width: 768px)
padding: 16px 24px
.navbar-icon
margin-right: 16px
img
height: 50px
width: 50px
@media (min-width: 768px)
height: 56px
width: 56px
.navbar-text
font-size: 25px
letter-spacing: 0.138em
font-family: "Play-Bold"
font-weight: bold
@media (min-width: 768px)
font-size: 40px
a
text-decoration: none
color: white
.navbar-left
display: flex
gap: 8px
letter-spacing: 3.6px
align-items: center
font-size: 32px
font-weight: 400
&:hover, &:active, &:focus
cursor: pointer
.navbar-title
font-family: 'Audiowide', sans-serif
*>div
font-family: 'Audiowide', sans-serif
@media (min-width: 768px)
display: block
.big-spacing
letter-spacing: 5.76px
.small-spacing
letter-spacing: 1.8px
.navbar-center
display: flex
margin-left: auto
.navbar-end
display: flex
margin-left: auto
height: 100%
nav
display: flex
gap: 50px
a
&.router-link-active
border-bottom: 2px solid var(--color-based-blue)
.navbar-navigation
display: flex
align-items: center
gap: 48px
.navbar-navigation-link
display: flex
gap: 8px
border-bottom: 2px solid transparent
&.active
border-bottom: 2px solid white
&:active, &:hover, &:focus
cursor: pointer
.icon
img
width: 20px
height: 20px
.login-icon
color: var(--color-navbar-font)
padding: 8px
font-size: 25px
border-radius: 8px
width: 44px
height: 44px
display: flex
justify-content: center
align-items: center
img
width: 32px
height: 32px
border-radius: 8px
.login-icon-desktop
display: flex
padding: 8px
align-items: center
gap: 16px
border-radius: 16px
background: var(--Background-grey, #2D2D2D)
&:hover,&:active,&:focus
cursor: pointer
img
width: 32px
height: 32px
.vertical-line
width: 1px
background-color: var(--color-border-main)
height: 100%
margin: 0 32px
</style>

View file

@ -0,0 +1,127 @@
<template>
<div class="network-changer" v-click-outside="closeMenu">
<div class="network-changer-inner" @click="showMenu = !showMenu">
<icon-base></icon-base>
<Icon v-if="showMenu" class="toggle-icon" icon="mdi:chevron-down"></Icon>
<Icon v-else class="toggle-icon" icon="mdi:chevron-up"></Icon>
</div>
<Transition name="collapse-network">
<div v-show="showMenu" class="network-changer--list">
<div class="list-inner" ref="listInner">
<div class="list--element" v-for="chain in chains" :key="chain.id">
<template v-if="chain.id === wallet.account.chainId">
<icon-base></icon-base>
<div>{{ chain.name }}</div>
<div>Connected</div>
</template>
<template v-else>
<icon-base></icon-base>
<div>{{ chain.name }}</div>
<f-button outlined dense @click="switchNetwork(chain)">Switch Network</f-button>
</template>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import IconBase from "@/components/icons/IconBase.vue";
import { ref } from "vue";
import { useChains, useChainId, useSwitchChain } from "@wagmi/vue";
import {Icon} from "@iconify/vue";
import FButton from "@/components/fcomponents/FButton.vue";
import { useWallet } from "@/composables/useWallet";
const chains = useChains();
const wallet = useWallet();
const { switchChain } = useSwitchChain();
const showMenu = ref<boolean>(false);
const listInner = ref();
function closeMenu() {
if (showMenu.value) {
console.log("tesat");
showMenu.value = false;
}
}
async function switchNetwork(chain: any) {
console.log("chain", chain);
switchChain({ chainId: chain.id });
}
</script>
<style lang="sass">
:root,[data-theme="default"]
--color-network-changer: unset
--color-network-changer-hovered: #060f18
--color-network-changer-toggle-icon: white
[data-theme="dark"]
--color-network-changer: var(--color-text-primary)
--color-network-changer-hovered: var(--color-text-primary-hovered)
--color-network-changer-toggle-icon: var(--color-bright-white)
.network-changer
position: relative
.network-changer-inner
display: flex
background-color: var(--color-network-changer)
padding: 8px
border-radius: 16px
align-items: center
gap: 8px
&:hover, &:focus, &:active
background-color: var(--color-network-changer-hovered)
cursor: pointer
.toggle-icon
pointer-events: none
path
fill: var(--color-network-changer-toggle-icon)
.network-changer--list
margin-top: 8px
position: absolute
// padding: 8px
color: var(--color-white)
background-color: var(--color-text-primary)
// bottom: calc(-100% - 8px)
overflow: hidden
right: 0
width: 300px
// height: 108px
border-radius: 16px
.list-inner
padding: 8px 0px
font-size: var(--font-body1)
display: flex
flex-direction: column
gap: 0
height: 100%
.list--element
display: flex
align-items: center
gap: 8px
padding: 8px 16px
:last-child
margin-left: auto
.f-btn
padding: 6px 8px
.collapse-network-enter-active
animation: collapse reverse 200ms ease
.collapse-network-leave-active
animation: collapse 200ms ease
@keyframes collapse-network
from
max-height: 100%
// bottom: calc(-108px - 8px)
bottom: calc(-100% - 8px)
to
max-height: 0px
bottom: -8px
</style>

View file

@ -0,0 +1,71 @@
<template>
<div class="slideout-overlay" :class="{ open: props.modelValue }" @click="showPanel = false"></div>
<div :class="{ 'slideout-wrapper': true, open: props.modelValue }">
<div class="slideout-panel">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { inject, watchEffect } from "vue";
const props = defineProps({
modelValue: Boolean,
});
defineEmits(["update:modelValue"]);
const showPanel = inject("showPanel");
</script>
<style lang="sass">
@use 'sass:color'
.slideout-overlay
position: fixed
z-index: 100
&.open
height: 100%
width: 100%
background-color: rgb(15, 15, 15, 0.7)
.slideout-wrapper
z-index: 100
height: 100svh
position: fixed
display: flex
background-color: var(--midnight-black, #0F0F0F)
border-radius: 16px
transition: margin-bottom 250ms ease 0s
width: 100%
margin-bottom: -100vh
bottom: 0
overflow: auto
@media (min-width: 768px)
margin-right: -400px
transition: margin-right 250ms ease 0s
width: auto
right: -30px
top: 0
&.open
margin-bottom: -82px
@media (min-width: 768px)
margin-right: 30px
.slideout-panel
width: 100%
padding: 24px 24px 16px 24px
overflow: scroll
// height: calc( 100svh - 82px )
display: flex
flex-direction: column
height: calc( 100vh - 80px)
@media (min-width: 768px)
width: 400px
overflow: unset
height: 100%
.slideout-icon
cursor: pointer
&:hover, &:active, &:focus
background-color: color.adjust(#2D2D2D, $lightness: 20%)
svg
font-size: 30px
</style>

View file

@ -0,0 +1,217 @@
<template>
<div class="toggle-dark-light">
<input type="checkbox" id="darkmode-toggle" @click="toggleTheme" :checked="props.modelValue" />
<label for="darkmode-toggle">
<svg
version="1.1"
class="sun"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 496 496"
style="enable-background: new 0 0 496 496"
xml:space="preserve"
>
<rect
x="152.994"
y="58.921"
transform="matrix(0.3827 0.9239 -0.9239 0.3827 168.6176 -118.5145)"
width="40.001"
height="16"
/>
<rect
x="46.9"
y="164.979"
transform="matrix(0.9239 0.3827 -0.3827 0.9239 71.29 -12.4346)"
width="40.001"
height="16"
/>
<rect
x="46.947"
y="315.048"
transform="matrix(0.9239 -0.3827 0.3827 0.9239 -118.531 50.2116)"
width="40.001"
height="16"
/>
<rect
x="164.966"
y="409.112"
transform="matrix(-0.9238 -0.3828 0.3828 -0.9238 168.4872 891.7491)"
width="16"
height="39.999"
/>
<rect
x="303.031"
y="421.036"
transform="matrix(-0.3827 -0.9239 0.9239 -0.3827 50.2758 891.6655)"
width="40.001"
height="16"
/>
<rect
x="409.088"
y="315.018"
transform="matrix(-0.9239 -0.3827 0.3827 -0.9239 701.898 785.6559)"
width="40.001"
height="16"
/>
<rect
x="409.054"
y="165.011"
transform="matrix(-0.9239 0.3827 -0.3827 -0.9239 891.6585 168.6574)"
width="40.001"
height="16"
/>
<rect
x="315.001"
y="46.895"
transform="matrix(0.9238 0.3828 -0.3828 0.9238 50.212 -118.5529)"
width="16"
height="39.999"
/>
<path
d="M248,88c-88.224,0-160,71.776-160,160s71.776,160,160,160s160-71.776,160-160S336.224,88,248,88z M248,392
c-79.4,0-144-64.6-144-144s64.6-144,144-144s144,64.6,144,144S327.4,392,248,392z"
/>
<rect x="240" width="16" height="72" />
<rect
x="62.097"
y="90.096"
transform="matrix(0.7071 0.7071 -0.7071 0.7071 98.0963 -40.6334)"
width="71.999"
height="16"
/>
<rect y="240" width="72" height="16" />
<rect
x="90.091"
y="361.915"
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 -113.9157 748.643)"
width="16"
height="71.999"
/>
<rect x="240" y="424" width="16" height="72" />
<rect
x="361.881"
y="389.915"
transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 397.8562 960.6281)"
width="71.999"
height="16"
/>
<rect x="424" y="240" width="72" height="16" />
<rect
x="389.911"
y="62.091"
transform="matrix(0.7071 0.7071 -0.7071 0.7071 185.9067 -252.6357)"
width="16"
height="71.999"
/>
</svg>
<svg
version="1.1"
class="moon"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 49.739 49.739"
style="enable-background: new 0 0 49.739 49.739"
xml:space="preserve"
>
<path
d="M25.068,48.889c-9.173,0-18.017-5.06-22.396-13.804C-3.373,23.008,1.164,8.467,13.003,1.979l2.061-1.129l-0.615,2.268
c-1.479,5.459-0.899,11.25,1.633,16.306c2.75,5.493,7.476,9.587,13.305,11.526c5.831,1.939,12.065,1.492,17.559-1.258v0
c0.25-0.125,0.492-0.258,0.734-0.391l2.061-1.13l-0.585,2.252c-1.863,6.873-6.577,12.639-12.933,15.822
C32.639,48.039,28.825,48.888,25.068,48.889z M12.002,4.936c-9.413,6.428-12.756,18.837-7.54,29.253
c5.678,11.34,19.522,15.945,30.864,10.268c5.154-2.582,9.136-7.012,11.181-12.357c-5.632,2.427-11.882,2.702-17.752,0.748
c-6.337-2.108-11.473-6.557-14.463-12.528C11.899,15.541,11.11,10.16,12.002,4.936z"
/>
</svg>
</label>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(["update:modelValue"]);
const props = defineProps<{
modelValue: boolean;
}>();
function toggleTheme(event: any) {
emit("update:modelValue", event.target.checked);
}
</script>
<style lang="sass">
.toggle-dark-light
display: flex
label
width: 62.5px
height: 25px
position: relative
display: block
background: #ebebeb
border-radius: 50vh
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.4), inset 0px -1px 3px rgba(255, 255, 255, 0.4)
cursor: pointer
transition: 0.3s
&:after
content: ""
width: 36%
height: 90%
position: absolute
top: 5%
left: 2%
background: linear-gradient(180deg, #ffcc89, #d8860b)
border-radius: 180px
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.2)
transition: 0.3s
input
width: 0
height: 0
visibility: hidden
&:checked + label
background: #242424
&:after
left: 98%
transform: translateX(-100%)
background: linear-gradient(180deg, #777, #3a3a3a)
label:active:after
width: 52%
input:checked + label + .background
background: #242424
label svg
position: absolute
width: 24%
top: 20%
z-index: 100
&.sun
left: 8%
fill: #fff
transition: 0.3s
&.moon
left: 68%
fill: #7e7e7e
transition: 0.3s
input:checked + label svg
&.sun
fill: #7e7e7e
&.moon
fill: #fff
</style>

View file

@ -0,0 +1,97 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { defineAsyncComponent } from "vue";
import { mount, flushPromises } from "@vue/test-utils";
import SocialBadge from "./socialButton.vue";
const mockIconComponent = (name:string) => ({
default: {
name,
template: `<svg class='${name.toLowerCase()}'></svg>`,
},
});
vi.mock("../components/icons/IconDiscord.vue", () => mockIconComponent("IconDiscord"));
vi.mock("../components/icons/IconTwitter.vue", () => mockIconComponent("IconTwitter"));
vi.mock("../components/icons/IconTelegram.vue", () => mockIconComponent("IconTelegram"));
describe("SocialBadge.vue", () => {
let wrapper;
beforeEach(() => {
wrapper = null;
});
it("renders the correct link", () => {
wrapper = mount(SocialBadge, {
props: {
href: "https://example.com",
},
});
const link = wrapper.find("a");
expect(link.exists()).toBe(true);
expect(link.attributes("href")).toBe("https://example.com");
expect(link.attributes("target")).toBe("_blank");
});
it("applies the correct color for light mode", () => {
wrapper = mount(SocialBadge, {
props: {
dark: false,
},
});
const badge = wrapper.find(".social-badge");
expect(badge.attributes("style")).toContain("color: black");
expect(badge.attributes("style")).toContain("border-color: black");
});
it("applies the correct color for dark mode", () => {
wrapper = mount(SocialBadge, {
props: {
dark: true,
},
});
const badge = wrapper.find(".social-badge");
expect(badge.attributes("style")).toContain("color: white");
expect(badge.attributes("style")).toContain("border-color: white");
});
// it("renders the correct icon based on the type prop", async () => {
// wrapper = mount(SocialBadge, {
// props: {
// type: "discord",
// },
// });
// await flushPromises();
// const current = wrapper.getCurrentComponent()?.setupState?.img.__asyncResolved
// console.log("current", current.default.name);
// expect(current.default.name).toBe("IconDiscord");
// // expect(icon.exists()).toBe(true);
// });
it("does not render an icon if the type is unsupported", async () => {
wrapper = mount(SocialBadge, {
props: {
type: "unsupported",
},
});
await wrapper.vm.$nextTick();
const icon = wrapper.find(".social-badge-icon").find("component");
expect(icon.exists()).toBe(false);
});
it("renders without crashing when no props are provided", () => {
wrapper = mount(SocialBadge);
expect(wrapper.exists()).toBe(true);
});
});

View file

@ -0,0 +1,64 @@
<template>
<a :href="props.href" target="_blank">
<div
class="social-badge"
:style="{ color: color, 'border-color': color }"
:class="{ 'social-badge--dark': props.dark }"
>
<div class="social-badge-icon">
<component :color="color" :is="img" />
</div>
</div>
</a>
</template>
<script setup lang="ts">
import { defineAsyncComponent, computed } from "vue";
interface Props {
type?: string;
dark?: boolean;
href?: string;
}
const props = withDefaults(defineProps<Props>(), {});
const color = computed(() => (props.dark ? "white" : "black"));
const img = computed(() => {
let img;
switch (props.type) {
case "discord":
img = defineAsyncComponent(() => import(`../components/icons/IconDiscord.vue`));
break;
case "twitter":
img = defineAsyncComponent(() => import(`../components/icons/IconTwitter.vue`));
break;
case "telegram":
img = defineAsyncComponent(() => import(`../components/icons/IconTelegram.vue`));
break;
default:
break;
}
return img;
});
</script>
<style lang="sass">
.social-badge
border-radius: 14px
display: flex
border: 1px solid var(--color-social-border)
padding: 6px 20px
align-items: center
flex: 0 1 0
color: black
&:hover,&:active,&:focus
background-color: var(--color-white-hovered)
cursor: pointer
&.social-badge--dark
&:hover, &:active, &:focus
background-color: var(--color-black-hovered)
// font-size: 0
</style>

View file

@ -0,0 +1,98 @@
import { ref, onMounted, onUnmounted, reactive, computed, type ComputedRef } from "vue";
import * as StakeContract from "@/contracts/stake";
import { waitForTransactionReceipt } from "@wagmi/core";
import { config } from "@/wagmi";
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
import { useContractToast } from "./useContractToast";
const contractToast = useContractToast();
enum State {
SignTransaction = "SignTransaction",
Waiting = "Waiting",
Action = "Action",
}
export const taxRates = [
{ index: 0,year: 1, daily: 0.00274 },
{ index: 1, year: 3, daily: 0.00822 },
{ index: 2, year: 5, daily: 0.0137 },
{ index: 3, year: 8, daily: 0.02192 },
{ index: 4, year: 12, daily: 0.03288 },
{ index: 5, year: 18, daily: 0.04932 },
{ index: 6, year: 24, daily: 0.06575 },
{ index: 7, year: 30, daily: 0.08219 },
{ index: 8, year: 40, daily: 0.10959 },
{ index: 9, year: 50, daily: 0.13699 },
{ index: 10, year: 60, daily: 0.16438 },
{ index: 11, year: 80, daily: 0.21918 },
{ index: 12, year: 100, daily: 0.27397 },
{ index: 13, year: 130, daily: 0.35616 },
{ index: 14, year: 180, daily: 0.49315 },
{ index: 15, year: 250, daily: 0.68493 },
{ index: 16, year: 320, daily: 0.87671 },
{ index: 17, year: 420, daily: 1.15068 },
{ index: 18, year: 540, daily: 1.47945 },
{ index: 19, year: 700, daily: 1.91781 },
{ index: 20, year: 920, daily: 2.52055 },
{ index: 21, year: 1200, daily: 3.28767 },
{ index: 22, year: 1600, daily: 4.38356 },
{ index: 23, year: 2000, daily: 5.47945 },
{ index: 24, year: 2600, daily: 7.12329 },
{ index: 25, year: 3400, daily: 9.31507 },
{ index: 26, year: 4400, daily: 12.05479 },
{ index: 27, year: 5700, daily: 15.61644 },
{ index: 28, year: 7500, daily: 20.54795 },
{ index: 29, year: 9700, daily: 26.57534 },
];
export function useAdjustTaxRate() {
const loading = ref();
const waiting = ref();
const state: ComputedRef<State> = computed(() => {
if (loading.value) {
return State.SignTransaction;
} else if (waiting.value) {
return State.Waiting;
} else {
return State.Action;
}
});
async function changeTax(positionId: bigint, taxRate: number) {
try {
console.log("changeTax", { positionId, taxRate });
loading.value = true;
const index = taxRates.findIndex((obj) => obj.year === taxRate)
const hash = await StakeContract.changeTax(positionId, index);
console.log("hash", hash);
loading.value = false;
waiting.value = true;
const data = await waitForTransactionReceipt(config as any, {
hash: hash,
});
contractToast.showSuccessToast(
taxRate.toString(),
"Success!",
"You adjusted your position tax to",
"",
"%"
);
waiting.value = false;
} catch (error: any) {
console.error("error", error);
console.log(JSON.stringify(error, (_, v) => (typeof v === "bigint" ? v.toString() : v)));
contractToast.showFailToast(error.shortMessage);
} finally {
loading.value = false;
waiting.value = false;
}
}
return reactive({ state, taxRates, changeTax });
}

View file

@ -0,0 +1,51 @@
import { ref, reactive, computed } from "vue";
import { getChainId, watchChainId, getAccount, watchAccount } from "@wagmi/core";
import { config } from "@/wagmi";
import { setHarbContract } from "@/contracts/harb";
import { setStakeContract } from "@/contracts/stake";
import {chainsData} from "@/config"
import logger from "@/utils/logger";
const activeChain = ref()
let unwatch: any = null;
export const chainData = computed(() => {
return chainsData.find((obj) => obj.id === activeChain.value)
})
export function useChain() {
// if (!unwatch) {
// console.log("useChain function");
// const chain = getChainId(config as any)
// activeChain.value = chain
// unwatch = watchChainId(config as any, {
// async onChange(chainId) {
// console.log("Chain changed", chainId);
// activeChain.value = chainId
// setHarbContract()
// setStakeContract()
// },
// });
// }
// if (!unwatch) {
// console.log("useWallet function");
// unwatch = watchAccount(config as any, {
// async onChange(data) {
// console.log("watchaccount-useChain", data);
// if(!data.address) {
// } else if (activeChain.value !== data.chainId) {
// logger.info(`Chain changed!:`, data.chainId);
// }
// },
// });
// }
return reactive({ chainData });
}

View file

@ -0,0 +1,82 @@
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
import { type ComputedRef } from "vue";
import { config } from "@/wagmi";
import { AbiEncodingArrayLengthMismatchError, type WatchEventReturnType } from "viem";
import axios from "axios";
import { getAccount, watchContractEvent, readContract, waitForTransactionReceipt, watchAccount } from "@wagmi/core";
import { type WatchAccountReturnType } from "@wagmi/core";
import * as HarbContract from "@/contracts/harb";
// import HarbJson from "@/assets/contracts/harb.json";
import { type Abi, type Address } from "viem";
import { useWallet } from "./useWallet";
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
const wallet = useWallet();
const loading = ref(false);
const waiting = ref(false);
const ubiDue = HarbContract.ubiDue;
let unwatch: WatchAccountReturnType;
enum ClaimState {
NothingToClaim = "NothingToClaim",
StakeAble = "StakeAble",
SignTransaction = "SignTransaction",
Waiting = "Waiting",
NotEnoughApproval = "NotEnoughApproval",
}
export function useClaim() {
const state: ComputedRef<ClaimState> = computed(() => {
if (loading.value) {
return ClaimState.SignTransaction;
} else if (ubiDue.value === 0n) {
return ClaimState.NothingToClaim;
} else if (waiting.value) {
return ClaimState.Waiting;
} else {
return ClaimState.StakeAble;
}
});
async function claimUbi() {
try {
const address = wallet.account.address!;
loading.value = true;
const hash = await HarbContract.claimUbi(address);
console.log("hash", hash);
loading.value = false;
waiting.value = true;
const data = await waitForTransactionReceipt(config as any, {
hash: hash,
});
console.log("data.logs", data.logs);
} catch (error) {
console.error("error", error);
} finally {
loading.value = false;
waiting.value = false;
}
}
onMounted(async () => {});
if (!unwatch) {
console.log("useClaim function");
unwatch = watchAccount(config as any, {
async onChange(data) {
console.log("watchAccount", data);
if (data.address) {
await HarbContract.setHarbContract();
}
},
});
}
return reactive({ claimUbi, ubiDue, state });
}

View file

@ -0,0 +1,20 @@
import {onBeforeUnmount, onMounted} from 'vue'
export default function useClickOutside(component: any, callback: any) {
if (!component) return
const listener = (event: any) => {
if (event.target !== component.value && event.composedPath().includes(component.value)) {
return
}
if (typeof callback === 'function') {
callback()
}
}
onMounted(() => { window.addEventListener('click', listener) })
onBeforeUnmount(() => {window.removeEventListener('click', listener)})
return {listener}
}

View file

@ -0,0 +1,62 @@
import Toast from "@/components/Toast.vue";
import { POSITION, useToast } from "vue-toastification";
import { reactive } from "vue";
const toast = useToast();
export function useContractToast() {
function showFailToast(name?: string) {
console.log("name", name);
if (name === "UserRejectedRequestError") {
//
} else {
showSuccessToast(
"",
"Failed!",
"Your transaction didnt go through. Please try again.",
"If the issue persists, please send a message in #helpdesk on our Discord.",
"",
"error"
);
}
}
function showSuccessToast(
value: string,
header: string,
subheader: string,
info: string,
unit: string,
type: string = "info"
) {
// Define the content object with the component, props and listeners
const content = {
component: Toast,
// Any prop can be passed, but don't expect them to be reactive
props: {
value: value,
header: header,
subheader: subheader,
info: info,
token: unit,
type: type,
},
// Listen and react to events using callbacks. In this case we listen for
// the "click" event emitted when clicking the toast button
listeners: {},
};
// Render the toast and its contents
toast(content, {
position: POSITION.TOP_RIGHT,
icon: false,
closeOnClick: false,
toastClassName: "modal-overlay",
closeButton: false,
hideProgressBar: true,
timeout: 10000,
});
}
return reactive({ showSuccessToast, showFailToast });
}

View file

@ -0,0 +1,38 @@
import { ref, onMounted, watch } from 'vue'
// by convention, composable function names start with "use"
const darkTheme = ref(false)
export function useDark() {
onMounted(() => {
if(localStorage.getItem("theme") === "dark") {
document.documentElement.classList.add("dark")
darkTheme.value = true
} else {
darkTheme.value = false
}
})
watch(
() => darkTheme.value,
(newData) => {
document.documentElement.removeAttribute("data-theme");
localStorage.removeItem("theme")
if (newData) {
// import('@/assets/sass/elementplus-dark.scss');
document.documentElement.classList.add("dark")
document.documentElement.setAttribute("data-theme", "dark");
localStorage.setItem("theme", "dark");
} else {
// import('@/assets/sass/elementplus-light.scss');
document.documentElement.classList.remove("dark")
document.documentElement.setAttribute("data-theme", "default");
localStorage.setItem("theme", "default");
}
}
);
return { darkTheme }
}

View file

@ -0,0 +1,30 @@
import { ref, onMounted, onUnmounted } from "vue";
// by convention, composable function names start with "use"
export function useMobile() {
const isMobile = ref<boolean>(false);
const handleWindowSizeChange = () => {
isMobile.value = isMobileFunc();
};
isMobile.value = isMobileFunc();
function isMobileFunc() {
if (screen.width <= 768) {
return true;
} else {
return false;
}
}
onMounted(async () => {
window.addEventListener("resize", handleWindowSizeChange);
handleWindowSizeChange();
});
onUnmounted(() => {
window.removeEventListener("resize", handleWindowSizeChange);
});
return isMobile;
}

View file

@ -0,0 +1,311 @@
import { ref, onMounted, onUnmounted, reactive, computed, type ComputedRef } from "vue";
import { config } from "@/wagmi";
import { type WatchEventReturnType, toBytes, type Hex } from "viem";
import axios from "axios";
import { getAccount, watchContractEvent, watchChainId, watchAccount } from "@wagmi/core";
import type { WatchChainIdReturnType, WatchAccountReturnType, GetAccountReturnType} from "@wagmi/core";
import { HarbContract } from "@/contracts/harb";
import { bytesToUint256, uint256ToBytes } from "harb-lib";
import { bigInt2Number } from "@/utils/helper";
import { useChain } from "@/composables/useChain";
import { taxRates } from "@/composables/useAdjustTaxRates";
import { chainData } from "@/composables/useWallet";
import logger from "@/utils/logger";
const rawActivePositions = ref<Array<Position>>([]);
const rawClosedPositoins = ref<Array<Position>>([]);
const loading = ref(false);
const chain = useChain();
const activePositions = computed(() => {
const account = getAccount(config as any);
return rawActivePositions.value
.map((obj: any) => {
return {
...obj,
positionId: formatId(obj.id),
amount: bigInt2Number(obj.harbDeposit, 18),
taxRatePercentage: Number(obj.taxRate) * 100,
taxRate: Number(obj.taxRate),
taxRateIndex: taxRates.find((taxRate) => taxRate.year === Number(obj.taxRate) * 100)?.index,
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
totalSupplyEnd: obj.totalSupplyEnd ? BigInt(obj.totalSupplyEnd) : undefined,
totalSupplyInit: BigInt(obj.totalSupplyInit),
taxPaid: BigInt(obj.taxPaid),
share: Number(obj.share),
};
})
.sort((a, b) => {
if (a.taxRate > b.taxRate) {
return 1;
} else if (a.taxRate < b.taxRate) {
return -1;
} else {
return 0;
}
});
});
export interface Position {
creationTime: Date;
id: string;
positionId: bigint;
owner: string;
lastTaxTime: Date;
taxPaid: bigint;
taxRate: number;
taxRateIndex: number;
taxRatePercentage: number;
share: number;
status: string;
totalSupplyEnd?: bigint;
totalSupplyInit: bigint;
amount: number;
harbDeposit: bigint;
iAmOwner: boolean;
}
const myClosedPositions: ComputedRef<Position[]> = computed(() => {
const account = getAccount(config as any);
return rawClosedPositoins.value.map((obj: any) => {
const taxRatePosition = obj.taxRate * 100;
console.log("taxRatePosition", taxRatePosition);
console.log("taxRates[taxRatePosition]", taxRates[taxRatePosition]);
return {
...obj,
positionId: formatId(obj.id),
amount: obj.share * 1000000,
// amount: bigInt2Number(obj.harbDeposit, 18),
taxRatePercentage: Number(obj.taxRate) * 100,
taxRate: Number(obj.taxRate),
taxRateIndex: taxRates.find((taxRate) => taxRate.year === Number(obj.taxRate) * 100)?.index,
iAmOwner: obj.owner?.toLowerCase() === account.address?.toLowerCase(),
totalSupplyEnd: BigInt(obj.totalSupplyEnd),
totalSupplyInit: BigInt(obj.totalSupplyInit),
taxPaid: BigInt(obj.taxPaid),
share: Number(obj.share),
};
});
});
const myActivePositions: ComputedRef<Position[]> = computed(() =>
activePositions.value.filter((obj: Position) => {
return obj.iAmOwner;
})
);
const tresholdValue = computed(() => {
const arrayTaxRatePositions = activePositions.value.map((obj) => obj.taxRatePercentage);
const sortedPositions = arrayTaxRatePositions.sort((a: any, b: any) => (a > b ? 1 : -1));
const sumq = sortedPositions.reduce((partialSum, a) => partialSum + a, 0);
const avg = sumq / sortedPositions.length;
return avg / 2;
});
export async function loadActivePositions() {
logger.info(`loadActivePositions for chain: ${chainData.value?.path}`);
if (!chainData.value?.thegraph) {
return [];
}
console.log("chainData.value?.thegraph", chainData.value?.thegraph);
const res = await axios.post(chainData.value?.thegraph, {
query: `query MyQuery {
positions(where: {status: Active}, orderBy: taxRate, orderDirection: asc) {
id
lastTaxTime
owner
payout
share
harbDeposit
snatched
status
taxPaid
taxRate
totalSupplyEnd
totalSupplyInit
}
}`,
});
console.log("res", res.data);
return res.data.data.positions as Position[];
}
function formatId(id: Hex) {
const bytes = toBytes(id);
const bigIntId = bytesToUint256(bytes);
return bigIntId;
}
export async function loadMyClosedPositions(account: GetAccountReturnType) {
logger.info(`loadMyClosedPositions for chain: ${chainData.value?.path}`);
if (!chainData.value?.thegraph) {
return [];
}
const res = await axios.post(chainData.value?.thegraph, {
query: `query MyQuery {
positions(where: {status: Closed, owner: "${account.address?.toLowerCase()}"}) {
id
lastTaxTime
owner
payout
share
harbDeposit
snatched
status
taxPaid
taxRate
totalSupplyEnd
totalSupplyInit
}
}`,
});
if (res.data.errors?.length > 0) {
console.error("todo nur laden, wenn eingeloggt");
return [];
}
const positions: Position[] = res.data.data.positions;
return positions;
}
export async function loadPositions() {
loading.value = true;
rawActivePositions.value = await loadActivePositions();
//optimal wäre es: laden, wenn new Position meine Position geclosed hat
const account = getAccount(config as any);
if (account.address) {
rawClosedPositoins.value = await loadMyClosedPositions(account);
}
loading.value = false;
}
let unwatch: WatchEventReturnType;
let unwatchPositionRemovedEvent: WatchEventReturnType;
let unwatchChainSwitch: WatchChainIdReturnType;
let unwatchAccountChanged: WatchAccountReturnType;
export function usePositions() {
function watchEvent() {
unwatch = watchContractEvent(config as any, {
address: HarbContract.contractAddress,
abi: HarbContract.abi,
eventName: "PositionCreated",
async onLogs(logs) {
console.log("new Position", logs);
await loadPositions();
// await getMinStake();
},
});
}
function watchPositionRemoved() {
unwatchPositionRemovedEvent = watchContractEvent(config as any, {
address: HarbContract.contractAddress,
abi: HarbContract.abi,
eventName: "PositionRemoved",
async onLogs(logs) {
console.log("Position removed", logs);
await loadPositions();
// await getMinStake();
},
});
}
onMounted(async () => {
//initial loading positions
if (activePositions.value.length < 1 && loading.value === false) {
loadPositions();
// await getMinStake();
}
if (!unwatch) {
watchEvent();
}
if (!unwatchPositionRemovedEvent) {
watchPositionRemoved();
}
if (!unwatchChainSwitch) {
unwatchChainSwitch = watchChainId(config as any, {
async onChange(chainId) {
await loadPositions();
},
});
}
if (!unwatchAccountChanged) {
unwatchAccountChanged = watchAccount(config as any, {
async onChange() {
await loadPositions();
},
});
}
});
onUnmounted(() => {
// if (unwatch) {
// unwatch();
// }
// if (unwatchPositionRemovedEvent) {
// unwatchPositionRemovedEvent();
// }
});
function createRandomPosition(amount: number = 1) {
for (let index = 0; index < amount; index++) {
const newPosition: Position = {
creationTime: new Date(),
id: "123",
positionId: 123n,
owner: "bla",
lastTaxTime: new Date(),
taxPaid: 100n,
taxRate: randomInRange(0.01, 1),
taxRateIndex: randomInRange(1, 30),
taxRatePercentage: getRandomInt(1, 100),
share: getRandomInt(0.001, 0.09),
status: "active",
totalSupplyEnd: undefined,
totalSupplyInit: 1000000000000n,
amount: 150,
harbDeposit: getRandomBigInt(1000, 5000),
iAmOwner: false,
};
rawActivePositions.value.push(newPosition);
}
}
function randomInRange(min: number, max: number) {
return Math.random() < 0.5 ? (1 - Math.random()) * (max - min) + min : Math.random() * (max - min) + min;
}
function getRandomInt(min: number, max: number) {
const minCeiled = Math.ceil(min);
const maxFloored = Math.floor(max);
const randomNumber = Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive
return randomNumber;
}
function getRandomBigInt(min: number, max: number) {
const randomNumber = getRandomInt(min, max);
return BigInt(randomNumber) * 10n ** 18n;
}
return {
activePositions,
myActivePositions,
myClosedPositions,
tresholdValue,
watchEvent,
watchPositionRemoved,
createRandomPosition,
};
}

View file

@ -0,0 +1,188 @@
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
import { type ComputedRef } from "vue";
import { config } from "@/wagmi";
import { AbiEncodingArrayLengthMismatchError, type WatchEventReturnType, decodeEventLog, type Hex } from "viem";
import axios from "axios";
import {
getAccount,
watchContractEvent,
readContract,
signTypedData,
waitForTransactionReceipt,
watchAccount,
verifyTypedData,
} from "@wagmi/core";
import { HarbContract } from "@/contracts/harb";
import { type Abi, type Address } from "viem";
import { StakeContract, minStake, snatchService, permitAndSnatch, assetsToShares } from "@/contracts/stake";
import { getNonce, nonce, getName } from "@/contracts/harb";
import { useWallet } from "@/composables/useWallet";
import { createPermitObject, getSignatureRSV } from "@/utils/blockchain";
import { formatBigIntDivision, compactNumber } from "@/utils/helper";
import { useToast } from "vue-toastification";
import { taxRates } from "@/composables/useAdjustTaxRates";
import { useContractToast } from "./useContractToast";
const wallet = useWallet();
const toast = useToast();
const contractToast = useContractToast();
enum StakeState {
NoBalance = "NoBalance",
StakeAble = "StakeAble",
SignTransaction = "SignTransaction",
Waiting = "Waiting",
NotEnoughApproval = "NotEnoughApproval",
}
interface PositionCreatedEvent {
eventName: undefined;
args: PositionCreatedArgs;
}
interface PositionCreatedArgs {
creationTime: number;
owner: Hex;
harbergDeposit: bigint;
positionId: bigint;
share: bigint;
taxRate: number;
}
// const state = ref<StakeState>(StakeState.NoBalance);
export function useStake() {
const stakingAmountRaw = ref();
const stakingAmountShares = ref();
const loading = ref(false);
const waiting = ref(false);
onMounted(async () => {});
const state: ComputedRef<StakeState> = computed(() => {
const balance = wallet.balance.value;
console.log("balance123", balance);
console.log("wallet", wallet);
if (loading.value) {
return StakeState.SignTransaction;
} else if (minStake.value > balance || stakingAmount.value > balance) {
return StakeState.NoBalance;
} else if (waiting.value) {
return StakeState.Waiting;
} else {
return StakeState.StakeAble;
}
});
const stakingAmount = computed({
// getter
get() {
return stakingAmountRaw.value || minStake.value;
},
// setter
set(newValue) {
stakingAmountRaw.value = newValue;
},
});
const stakingAmountNumber = computed({
// getter
get() {
return formatBigIntDivision(stakingAmount.value, 10n ** 18n);
},
// setter
set(newValue) {
stakingAmount.value = BigInt(newValue * 10 ** 18);
},
});
// const stakingAmountNumber = computed(() => return staking)
async function snatch(stakingAmount: BigInt, taxRate: number, positions:Array<any> = []) {
console.log("snatch", { stakingAmount, taxRate, positions });
const account = getAccount(config as any);
const taxRateObj = taxRates.find((obj) => obj.year === taxRate);
try {
const assets: BigInt = stakingAmount;
const receiver = wallet.account.address!;
console.log("receiver", receiver);
// await snatchService(assets, receiver, taxRate, []);
// assets: BigInt, receiver: Address, taxRate: Number, positionsToSnatch: Array<BigInt>
const deadline = BigInt(Date.now()) / 1000n + 1200n;
const name = await getName();
const { types, message, domain, primaryType } = createPermitObject(
HarbContract.contractAddress,
account.address!,
StakeContract.contractAddress,
nonce.value,
deadline,
assets,
account.chainId!,
name
);
console.log("resultPermitObject", { types, message, domain, primaryType });
const signature = await signTypedData(config as any, {
domain: domain as any,
message: message,
primaryType: primaryType,
types: types,
});
console.log("signature", {
domain: domain as any,
message: message,
primaryType: primaryType,
types: types,
});
const { r, s, v } = getSignatureRSV(signature);
loading.value = true;
console.log("permitAndSnatch", assets, account.address!, taxRateObj?.index!, positions, deadline, v, r, s);
const hash = await permitAndSnatch(assets, account.address!, taxRateObj?.index!, positions, deadline, v, r, s);
console.log("hash", hash);
loading.value = false;
waiting.value = true;
const data = await waitForTransactionReceipt(config as any, {
hash: hash,
});
const topics: any = decodeEventLog({
abi: StakeContract.abi,
data: data.logs[3].data,
topics: data.logs[3].topics,
});
const eventArgs: PositionCreatedArgs = topics.args;
const amount = compactNumber(
formatBigIntDivision(eventArgs.harbergDeposit, 10n ** BigInt(wallet.balance.decimals))
);
contractToast.showSuccessToast(
amount,
"Success!",
"You Staked",
"Check your positions on the<br /> Staker Dashboard",
"$KRK"
);
waiting.value = false;
await getNonce();
} catch (error: any) {
console.error("error", error);
console.log(JSON.parse(JSON.stringify(error)));
contractToast.showFailToast(error.name);
} finally {
loading.value = false;
waiting.value = false;
}
}
return reactive({ stakingAmount, stakingAmountShares, stakingAmountNumber, state, snatch });
}

View file

@ -0,0 +1,217 @@
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
import axios from "axios";
import { chainData } from "./useWallet";
import { watchBlocks, watchChainId } from "@wagmi/core";
import { config } from "@/wagmi";
import logger from "@/utils/logger";
import type { WatchBlocksReturnType } from "viem";
import { bigInt2Number } from "@/utils/helper";
const demo = sessionStorage.getItem("demo") === "true";
interface statsCollection {
burnNextHourProjected: bigint;
burnedLastDay: bigint;
burnedLastWeek: bigint;
id: string;
lastUpdatedHour: number;
mintNextHourProjected: bigint;
mintedLastDay: bigint;
mintedLastWeek: bigint;
outstandingStake: bigint;
harbTotalSupply: bigint;
stakeTotalSupply: bigint;
ringBufferPointer: number;
totalBurned: bigint;
totalMinted: bigint;
}
const rawStatsCollections = ref<Array<statsCollection>>([]);
const loading = ref(false);
const initialized = ref(false);
export async function loadStatsCollection() {
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
if (!chainData.value?.thegraph) {
return [];
}
const res = await axios.post(chainData.value?.thegraph, {
query: `query MyQuery {
stats_collection {
burnNextHourProjected
burnedLastDay
burnedLastWeek
id
lastUpdatedHour
mintNextHourProjected
mintedLastDay
mintedLastWeek
outstandingStake
harbTotalSupply
stakeTotalSupply
ringBufferPointer
totalBurned
totalMinted
}
}`,
});
console.groupEnd();
return res.data?.data?.stats_collection;
}
const profit7d = computed(() => {
if (!statsCollection.value) {
return 0n;
}
return (
(BigInt(statsCollection.value?.mintedLastWeek) - BigInt(statsCollection.value?.burnedLastWeek)) /
(BigInt(statsCollection.value?.totalMinted) - BigInt(statsCollection.value?.totalBurned))
);
});
const nettoToken7d = computed(() => {
if (!statsCollection.value) {
return 0n;
}
return BigInt(statsCollection.value?.mintedLastWeek) - BigInt(statsCollection.value.burnedLastWeek);
});
const statsCollection = computed(() => {
if (rawStatsCollections.value?.length > 0) {
return rawStatsCollections.value[0];
} else {
return undefined;
}
});
const outstandingStake = computed(() => {
if (demo) {
// outStandingStake = 1990000000000000000000000n;
return 2000000000000000000000000n;
}
if (rawStatsCollections.value?.length > 0) {
return BigInt(rawStatsCollections.value[0].outstandingStake);
} else {
return 0n;
}
});
const harbTotalSupply = computed(() => {
if (rawStatsCollections.value?.length > 0) {
return BigInt(rawStatsCollections.value[0].harbTotalSupply);
} else {
return 0n;
}
});
const stakeTotalSupply = computed(() => {
if (rawStatsCollections.value?.length > 0) {
return BigInt(rawStatsCollections.value[0].stakeTotalSupply);
} else {
return 0n;
}
});
//Total Supply Change / 7d=mintedLastWeekburnedLastWeek
const totalSupplyChange7d = computed(() => {
if (rawStatsCollections.value?.length > 0) {
return BigInt(rawStatsCollections.value[0].mintedLastWeek - rawStatsCollections.value[0].burnedLastWeek);
} else {
return 0n;
}
});
//totalsupply Change7d / harbtotalsupply
const inflation7d = computed(() => {
if (rawStatsCollections.value?.length > 0 && rawStatsCollections.value[0].harbTotalSupply > 0) {
return BigInt(rawStatsCollections.value[0].mintedLastWeek - rawStatsCollections.value[0].burnedLastWeek);
} else {
return 0n;
}
});
const stakeableSupply = computed(() => {
if (rawStatsCollections.value?.length > 0 && rawStatsCollections.value[0].harbTotalSupply > 0) {
console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]);
return stakeTotalSupply.value / 5n;
} else {
return 0n;
}
});
//maxSlots
const maxSlots = computed(() => {
if (rawStatsCollections.value?.length > 0 && rawStatsCollections.value[0].harbTotalSupply > 0) {
console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]);
return (bigInt2Number(stakeTotalSupply.value, 18) * 0.2) / 100;
} else {
return 0;
}
});
const claimedSlots = computed(() => {
if (stakeTotalSupply.value > 0n) {
const stakeableSupplyNumber = bigInt2Number(stakeableSupply.value, 18);
const outstandingStakeNumber = bigInt2Number(outstandingStake.value, 18);
return (outstandingStakeNumber / stakeableSupplyNumber) * maxSlots.value;
} else {
return 0;
}
});
export async function loadStats() {
loading.value = true;
rawStatsCollections.value = await loadStatsCollection();
loading.value = false;
initialized.value = true;
}
let unwatch: any = null;
let unwatchBlock: WatchBlocksReturnType;
const loadingWatchBlock = ref(false);
export function useStatCollection() {
onMounted(async () => {
//initial loading stats
if (rawStatsCollections.value?.length === 0 && !loading.value) {
await loadStats();
}
});
if (!unwatch) {
console.log("watchChain");
//chain Switch reload stats for other chain
unwatch = watchChainId(config as any, {
async onChange(chainId) {
await loadStats();
},
});
// const unwatchBlock = watchBlocks(config as any, {
// async onBlock(block) {
// console.log('Block changed!', block)
// await loadStats();
// },
// })
}
onUnmounted(() => {
unwatch();
});
return reactive({
profit7d,
nettoToken7d,
inflation7d,
outstandingStake,
harbTotalSupply,
stakeTotalSupply,
totalSupplyChange7d,
initialized,
maxSlots,
stakeableSupply,
claimedSlots,
});
}

View file

@ -0,0 +1,77 @@
import { ref, onMounted, onUnmounted, reactive, computed, type ComputedRef } from "vue";
import * as StakeContract from "@/contracts/stake";
import { waitForTransactionReceipt } from "@wagmi/core";
import { config } from "@/wagmi";
import { useContractToast } from "./useContractToast";
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
import { decodeEventLog } from "viem";
import { useWallet } from "./useWallet";
const contractToast = useContractToast();
const wallet = useWallet();
enum StakeState {
SignTransaction = "SignTransaction",
Waiting = "Waiting",
Unstakeable = "Unstakeable",
}
interface PositionRemovedArgs {
lastTaxTime: number;
positionId: bigint;
harbergPayout: bigint;
}
export function useUnstake() {
const loading = ref();
const waiting = ref();
const state: ComputedRef<StakeState> = computed(() => {
if (loading.value) {
return StakeState.SignTransaction;
} else if (waiting.value) {
return StakeState.Waiting;
} else {
return StakeState.Unstakeable;
}
});
async function exitPosition(positionId: bigint) {
try {
console.log("positionId", positionId);
loading.value = true;
const hash = await StakeContract.exitPosition(positionId);
console.log("hash", hash);
loading.value = false;
waiting.value = true;
const data = await waitForTransactionReceipt(config as any, {
hash: hash,
});
const topics: any = decodeEventLog({
abi: StakeContract.StakeContract.abi,
data: data.logs[2].data,
topics: data.logs[2].topics,
});
const eventArgs: PositionRemovedArgs = topics.args;
const amount = compactNumber(
formatBigIntDivision(eventArgs.harbergPayout, 10n ** BigInt(wallet.balance.decimals))
);
contractToast.showSuccessToast(amount, "Success!", "You unstaked", "", "$KRK");
waiting.value = false;
wallet.loadBalance();
} catch (error: any) {
console.error("error", error);
contractToast.showFailToast(error.name);
} finally {
loading.value = false;
waiting.value = false;
}
}
return reactive({ state, exitPosition });
}

View file

@ -0,0 +1,102 @@
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
import { type Ref } from "vue";
import { getBalance, watchAccount, watchChainId } from "@wagmi/core";
import { type WatchAccountReturnType, type GetAccountReturnType, type GetBalanceReturnType } from "@wagmi/core";
import { config } from "@/wagmi";
import { getAllowance, HarbContract, getNonce } from "@/contracts/harb";
import logger from "@/utils/logger";
import { setHarbContract } from "@/contracts/harb";
import { setStakeContract } from "@/contracts/stake";
import {chainsData} from "@/config"
const balance = ref<GetBalanceReturnType>({
value: 0n,
decimals: 0,
symbol: "",
formatted: ""
});
const account = ref<GetAccountReturnType>({
address: undefined,
addresses: undefined,
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: false,
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
status: "disconnected",
});
export const chainData = computed(() => {
return chainsData.find((obj) => obj.id === account.value.chainId)
})
let unwatch: any = null;
let unwatchChain: any = null;
export function useWallet() {
async function loadBalance() {
logger.contract("loadBalance")
if (account.value.address) {
console.log("HarbContract",HarbContract );
balance.value = await getBalance(config as any, {
address: account.value.address,
token: HarbContract.contractAddress,
});
console.log("balance.value", balance.value);
return balance.value;
} else {
return 0n;
}
}
if (!unwatch) {
console.log("useWallet function");
unwatch = watchAccount(config as any, {
async onChange(data) {
console.log("watchaccount-useWallet", data);
if(!data.address) {
logger.info(`disconnected`);
balance.value = {
value: 0n,
decimals: 0,
symbol: "",
formatted: ""
}
} else if (account.value.address !== data.address || account.value.chainId !== data.chainId) {
logger.info(`Account changed!:`, data.address);
account.value = data;
await loadBalance();
await getAllowance();
// await loadPositions();
await getNonce();
setHarbContract()
setStakeContract()
}
},
});
}
//funzt nicht mehr-> library Änderung?
if(!unwatchChain){
console.log("unwatchChain");
unwatchChain = watchChainId(config as any, {
async onChange(chainId) {
console.log("chainId123", chainId);
await loadBalance();
await getAllowance();
await getNonce();
}
});
}
return reactive({ balance, account, loadBalance });
}

32
web-app/src/config.ts Normal file
View file

@ -0,0 +1,32 @@
export const chainsData = [
{
//sepolia
id: 11155111,
thegraph: "https://api.studio.thegraph.com/query/71271/harb/v0.0.50",
path: "sepolia",
stake: "0xCd21a41a137BCAf8743E47D048F57D92398f7Da9",
harb: "0x087F256D11fe533b0c7d372e44Ee0F9e47C89dF9",
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
}, {
//base-sepolia
id: 84532,
thegraph: "https://api.studio.thegraph.com/query/71271/harb-base-sepolia/v0.0.12",
path: "sepoliabase",
stake: "0xe28020BCdEeAf2779dd47c670A8eFC2973316EE2",
harb: "0x22c264Ecf8D4E49D1E3CabD8DD39b7C4Ab51C1B8",
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
},
{
//base
id: 8453,
thegraph: import.meta.env.VITE_BASE_URL,
path: "base",
stake: "0xed70707fab05d973ad41eae8d17e2bcd36192cfc",
harb: "0x45caa5929f6ee038039984205bdecf968b954820",
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
},
]
export function getChain(id:number){
return chainsData.find((obj) => obj.id === id)
}

View file

@ -0,0 +1,171 @@
import { ref, onMounted, onUnmounted, reactive, computed, watch } from "vue";
import { config } from "@/wagmi";
import { type WatchEventReturnType } from "viem";
import {
getAccount,
watchContractEvent,
readContract,
writeContract,
waitForTransactionReceipt,
type WaitForTransactionReceiptParameters,
getChainId
} from "@wagmi/core";
// import { HarbContract } from "@/contracts/harb";
import HarbJson from "@/assets/contracts/harb.json";
import { type Abi, type Address } from "viem";
import { StakeContract } from "@/contracts/stake";
import {getChain} from "@/config"
import logger from "@/utils/logger";
// const chain1 = useChain();
// console.log("chain1", chain1);
interface Contract {
abi: Abi;
contractAddress: Address;
}
export const allowance = ref();
export const nonce = ref();
export const name = ref();
export const ubiDue = ref();
export const totalSupply = ref(0n);
// export const HarbContract = await getHarbJson()
export let HarbContract = getHarbJson();
function getHarbJson(){
console.log("getHarbJson");
const chainId = getChainId(config as any);
console.log("chainId", chainId);
const chain = getChain(chainId)
return {abi: HarbJson, contractAddress: chain?.harb} as Contract;
}
export function setHarbContract(){
console.log("setHarbContract");
HarbContract = getHarbJson();
}
// watch(chainData, async (newQuestion, oldQuestion) => {
// console.log("log harb update");
// });
export async function getAllowance() {
logger.contract("getAllowance");
const account = getAccount(config as any);
if (!account.address) {
return 0n;
}
const result = await readContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "allowance",
args: [account.address, StakeContract.contractAddress],
});
allowance.value = result;
return result;
}
export async function getMinStake() {
logger.contract("getMinStake");
const result: bigint = await readContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "minStake",
args: [],
}) as bigint;
allowance.value = result;
return result;
}
export async function getNonce() {
logger.contract("getNonce");
const account = getAccount(config as any);
if (!account.address) {
return 0n;
}
console.log("HarbContract.contractAddress", HarbContract.contractAddress);
const result = await readContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "nonces",
args: [account.address],
});
nonce.value = result;
return result;
}
export async function getName() {
logger.contract("getName");
const result = await readContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "name",
args: [],
});
name.value = result;
return result as string;
}
export async function approve(amount: bigint): Promise<any> {
const account = getAccount(config as any);
if (!account.address) {
throw new Error("no address found");
}
const result = await writeContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "approve",
args: [StakeContract.contractAddress, amount],
});
console.log("result", result);
const transactionReceipt = waitForTransactionReceipt(config as any, {
hash: result,
});
console.log("transactionReceipt", transactionReceipt);
return transactionReceipt;
}
//claim
export async function claimUbi(address: Address): Promise<any> {
const result = await writeContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "claimUbi",
args: [address],
});
return result;
}
export async function getTotalSupply() {
logger.contract("getTotalSupply");
const result = await readContract(config as any, {
abi: HarbContract.abi,
address: HarbContract.contractAddress,
functionName: "totalSupply",
args: [],
});
totalSupply.value = result as bigint;
return result;
}

View file

@ -0,0 +1,164 @@
import { ref } from "vue";
import { config } from "@/wagmi";
import { readContract, writeContract, getChainId } from "@wagmi/core";
import StakeJson from "@/assets/contracts/stake.json";
import { type Abi, type Address } from "viem";
import {getChain} from "@/config"
import logger from "@/utils/logger";
const TAX_FLOOR_DURATION = 60 * 60 * 24 * 3;
interface Contract {
abi: Abi;
contractAddress: Address;
}
export const minStake = ref();
export const totalSupply = ref(0n);
export const outstandingSupply = ref(0n)
export let StakeContract = getStakeJson()
function getStakeJson(){
const chainId = getChainId(config as any);
console.log("chainId", chainId);
const chain = getChain(chainId)
return {abi: StakeJson, contractAddress: chain?.stake} as Contract;
}
export function setStakeContract(){
logger.contract("setStakeContract")
StakeContract = getStakeJson();
}
export async function snatchService(
assets: BigInt,
receiver: Address,
taxRate: Number,
positionsToSnatch: Array<BigInt>
) {
console.log("StakeContract", StakeContract);
const result = await writeContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "snatch",
args: [assets, receiver, taxRate, positionsToSnatch],
});
return result;
}
export async function exitPosition(positionId: bigint): Promise<any> {
const result = await writeContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "exitPosition",
args: [positionId],
});
return result;
}
//changeTax
export async function changeTax(positionId: bigint, taxRate: number): Promise<any> {
const result = await writeContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "changeTax",
args: [positionId, taxRate],
});
return result;
}
/**
* snatch/stake with permit
*/
export async function permitAndSnatch(
assets: BigInt,
receiver: Address,
taxRate: number,
positionsToSnatch: Array<BigInt>,
deadline: BigInt,
v: any,
r: any,
s: any
) {
console.log("permitAndSnatch", assets, receiver, taxRate, positionsToSnatch, deadline, v, r, s);
const result = await writeContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "permitAndSnatch",
args: [assets, receiver, taxRate, positionsToSnatch, deadline, v, r, s],
});
return result;
}
export async function getTotalSupply() {
logger.contract("getTotalSupply")
await setStakeContract();
const result = await readContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "totalSupply",
args: [],
});
console.log("result", result);
totalSupply.value = result as bigint;
return result;
}
export async function getOutstandingSupply() {
logger.contract("getOutstandingSupply")
const result = await readContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "outstandingStake",
args: [],
});
outstandingSupply.value = result as bigint;
return result;
}
export async function getTaxDue(positionID: bigint) {
logger.contract("getTaxDue")
const result = await readContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "taxDue",
args: [positionID, TAX_FLOOR_DURATION],
});
return result as bigint;
}
export async function payTax(positionID: bigint) {
console.log("payTax", positionID);
const result = await writeContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "payTax",
args: [positionID],
});
return result;
}
export async function assetsToShares(asset: bigint) {
console.log("assetsToShares", asset);
const result = await readContract(config as any, {
abi: StakeContract.abi,
address: StakeContract.contractAddress,
functionName: "assetsToShares",
args: [asset],
});
return result as bigint;
}

View file

@ -0,0 +1,17 @@
export default {
beforeMount(el: any, binding: any) {
el.clickOutsideEvent = function (event: any) {
// Check if the clicked element is neither the element
// to which the directive is applied nor its child
if (!(el === event.target || el.contains(event.target))) {
// Invoke the provided method
binding.value(event);
}
};
document.addEventListener("click", el.clickOutsideEvent);
},
unmounted(el: any) {
// Remove the event listener when the bound element is unmounted
document.removeEventListener("click", el.clickOutsideEvent);
},
};

View file

@ -0,0 +1,5 @@
<template>
<main>
<slot></slot>
</main>
</template>

View file

@ -0,0 +1,88 @@
<!-- src/layouts/NavbarLayout.vue -->
<template>
<navbar></navbar>
<slideout-panel v-model="showPanel">
<connect-wallet></connect-wallet>
<!-- <f-footer :dark="true"></f-footer> -->
</slideout-panel>
<div class="navbar-layout">
<main>
<slot></slot>
</main>
</div>
<!-- <footer>
<f-footer :dark="darkTheme"></f-footer>
</footer> -->
<div class="mobile-navigation-bar" v-if="isMobile">
<div class="mobile-navigation-tab" @click="router.push('/')">
<div class="mobile-navigation-tab__icon">
<icon-home />
</div>
<div class="mobile-navigation-tab__title">Stake</div>
</div>
<div class="mobile-navigation-tab" @click="openDocs">
<div class="mobile-navigation-tab__icon">
<icon-docs />
</div>
<div class="mobile-navigation-tab__title">Docs</div>
</div>
</div>
</template>
<script setup lang="ts">
import Navbar from "@/components/layouts/Navbar.vue";
import SlideoutPanel from "@/components/layouts/SlideoutPanel.vue";
import ConnectWallet from "@/components/layouts/ConnectWallet.vue";
import ThemeToggle from "@/components/layouts/ThemeToggle.vue";
import FFooter from "@/components/layouts/FFooter.vue";
import { useMobile } from "@/composables/useMobile";
import { useDark } from "@/composables/useDark";
import IconHome from "@/components/icons/IconHome.vue";
import IconDocs from "@/components/icons/IconDocs.vue";
import { ref, provide } from "vue";
import {useRouter } from "vue-router"
const showPanel = ref(false);
const { darkTheme } = useDark();
const router = useRouter()
const isMobile = useMobile();
provide("isMobile", isMobile);
provide("showPanel", showPanel);
function openDocs(){
window.open("https://emberspirit007.github.io/KraikenLanding/#/docs/Introduction")
}
</script>
<style lang="sass">
.navbar-layout
padding-top: 100px
.mobile-navigation-bar
height: 72px
z-index: 1
width: 100%
padding: 16px 48px
box-shadow: 0px -4px 6px 0px rgba(0, 0, 0, 0.25)
background: var(--midnight-black, #0F0F0F)
bottom: -1px
position: fixed
display: flex
align-items: center
.mobile-navigation-tab
font-size: 40px
cursor: pointer
flex: 1 1 50%
text-align: center
color: white
-webkit-tap-highlight-color: transparent
.mobile-navigation-tab__icon
svg
width: 20px
height: 20px
path
fill: white
.mobile-navigation-tab__title
font-size: 12px
</style>

25
web-app/src/main.ts Normal file
View file

@ -0,0 +1,25 @@
import { WagmiPlugin } from "@wagmi/vue";
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
import { createApp } from "vue";
import { config } from "./wagmi";
import ClickOutSide from "@/directives/ClickOutsideDirective";
import router from "./router";
import App from "./App.vue";
import "./assets/styles/main.sass";
import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
const queryClient = new QueryClient();
const app = createApp(App);
app.directive("click-outside", ClickOutSide);
app.use(WagmiPlugin, { config });
app.use(VueQueryPlugin, { queryClient });
app.use(router);
app.use(Toast, {
transition: "Vue-Toastification__fade",
containerClassName: "harb-toast-container",
});
app.mount("#app");

View file

@ -0,0 +1,12 @@
// src/router/authGuard.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
export function authGuard(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext): void {
const isAuthenticated: boolean = !!localStorage.getItem('authentificated'); // Prüft, ob Token existiert
if (isAuthenticated) {
next(); // Zugriff erlauben
} else {
next('/login'); // Weiterleitung zur Login-Seite
}
}

View file

@ -0,0 +1,35 @@
import { createRouter, createWebHashHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import { authGuard } from './authGuard';
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
name: "home",
redirect: "/stake",
},
{
path: "/stake",
name: "stake",
meta: {
title: "Stake",
group: "navbar",
layout: 'NavbarLayout'
},
beforeEnter: authGuard,
component: () => import("../views/StakeView.vue"),
},
{
path: "/login",
name: "login",
meta: {
layout: 'DefaultLayout'
, },
component: () => import("../views/LoginView.vue"),
},
],
});
export default router;

12
web-app/src/types/router.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
// src/types/router.d.ts
import 'vue-router';
// Verfügbare Layout-Namen definieren
export type LayoutName = 'DefaultLayout' | 'NavbarLayout';
// Erweiterung des Vue-Router Meta-Typs
declare module 'vue-router' {
interface RouteMeta {
layout?: LayoutName; // Layout ist optional
}
}

View file

@ -0,0 +1,85 @@
import { type Address, type TypedDataDomain, type Hex, slice, hexToNumber, hexToBigInt, recoverAddress } from "viem";
export function createPermitObject(
verifyingContract: Address,
fromAddress: Address,
spender: Address,
nonce: BigInt,
deadline: BigInt,
value: BigInt,
chain: number,
domainName: string
) {
const message = {
owner: fromAddress,
spender: spender,
nonce: nonce,
deadline: deadline,
value: value,
};
const domainType = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
];
const primaryType: "EIP712Domain" | "Permit" = "Permit";
const types = {
EIP712Domain: domainType,
Permit: [
{
name: "owner",
type: "address",
},
{
name: "spender",
type: "address",
},
{
name: "value",
type: "uint256",
},
{
name: "nonce",
type: "uint256",
},
{
name: "deadline",
type: "uint256",
},
],
};
const domain: TypedDataDomain | undefined = {
name: domainName,
version: "1",
chainId: chain,
verifyingContract: verifyingContract,
};
return {
types,
message,
domain,
primaryType,
};
}
export function getSignatureRSV2(sig: `0x${string}`) {
// splits the signature to r, s, and v values.
// const pureSig = sig.replace("0x", "");
const [r, s, v] = [slice(sig, 0, 32), slice(sig, 32, 64), slice(sig, 64, 65)];
return { r, s, v: Number(v) };
}
export function getSignatureRSV(signature: `0x${string}`) {
const r = signature.slice(0, 66) as `0x${string}`;
const s = `0x${signature.slice(66, 130)}` as `0x${string}`;
const v = hexToBigInt(`0x${signature.slice(130, 132)}`);
return { r, s, v };
}

View file

@ -0,0 +1,135 @@
var randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values
interface BlockiesOpt {
seed: string;
size: number;
scale: number;
color?: string;
bgcolor?: string;
spotcolor?: string;
}
export function getBlocky(address: string) {
if (!address || typeof address !== "string" ) {
return;
}
console.log("address", address);
var blockiesData = createIcon({
seed: address?.toLowerCase(),
size: 8,
scale: 4,
}).toDataURL();
return blockiesData;
}
function seedrand(seed: string) {
for (var i = 0; i < randseed.length; i++) {
randseed[i] = 0;
}
for (var i = 0; i < seed.length; i++) {
randseed[i % 4] = (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i);
}
}
function rand() {
// based on Java's String.hashCode(), expanded to 4 32bit values
var t = randseed[0] ^ (randseed[0] << 11);
randseed[0] = randseed[1];
randseed[1] = randseed[2];
randseed[2] = randseed[3];
randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8);
return (randseed[3] >>> 0) / ((1 << 31) >>> 0);
}
function createColor() {
//saturation is the whole color spectrum
var h = Math.floor(rand() * 360);
//saturation goes from 40 to 100, it avoids greyish colors
var s = rand() * 60 + 40 + "%";
//lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%
var l = (rand() + rand() + rand() + rand()) * 25 + "%";
var color = "hsl(" + h + "," + s + "," + l + ")";
return color;
}
function createImageData(size: number) {
var width = size; // Only support square icons for now
var height = size;
var dataWidth = Math.ceil(width / 2);
var mirrorWidth = width - dataWidth;
var data = [];
for (var y = 0; y < height; y++) {
var row = [];
for (var x = 0; x < dataWidth; x++) {
// this makes foreground and background color to have a 43% (1/2.3) probability
// spot color has 13% chance
row[x] = Math.floor(rand() * 2.3);
}
var r = row.slice(0, mirrorWidth);
r.reverse();
row = row.concat(r);
for (var i = 0; i < row.length; i++) {
data.push(row[i]);
}
}
return data;
}
function buildOpts(opts: any) {
var newOpts: any = {};
newOpts.seed = opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16);
seedrand(newOpts.seed);
newOpts.size = opts.size || 8;
newOpts.scale = opts.scale || 4;
newOpts.color = opts.color || createColor();
newOpts.bgcolor = opts.bgcolor || createColor();
newOpts.spotcolor = opts.spotcolor || createColor();
return newOpts;
}
function renderIcon(opts: BlockiesOpt, canvas: HTMLCanvasElement) {
opts = buildOpts(opts || {});
var imageData = createImageData(opts.size);
var width = Math.sqrt(imageData.length);
canvas.width = canvas.height = opts.size * opts.scale;
var cc = canvas.getContext("2d")!;
cc.fillStyle = opts.bgcolor!;
cc.fillRect(0, 0, canvas.width, canvas.height);
cc.fillStyle = opts.color!;
for (var i = 0; i < imageData.length; i++) {
// if data is 0, leave the background
if (imageData[i]) {
var row = Math.floor(i / width);
var col = i % width;
// if data is 2, choose spot color, if 1 choose foreground
cc.fillStyle = imageData[i] == 1 ? opts.color! : opts.spotcolor!;
cc.fillRect(col * opts.scale, row * opts.scale, opts.scale, opts.scale);
}
}
return canvas;
}
export function createIcon(opts: BlockiesOpt) {
var canvas = document.createElement("canvas");
renderIcon(opts, canvas);
return canvas;
}

View file

@ -0,0 +1,21 @@
/**
* @notice Converts Harberg token assets to shares of the total staking pool.
* @param assets Number of Harberg tokens to convert.
* @param totalSupply Total supply of shares.
* @param harbergTotalSupply Total supply of Harberg tokens.
* @returns Number of shares corresponding to the input assets.
*/
export function assetsToShares(assets: bigint, totalSupply: bigint, harbergTotalSupply: bigint): bigint {
return (assets * totalSupply) / harbergTotalSupply;
}
/**
* @notice Converts shares of the total staking pool back to Harberg token assets.
* @param shares Number of shares to convert.
* @param totalSupply Total supply of shares.
* @param harbergTotalSupply Total supply of Harberg tokens.
* @returns The equivalent number of Harberg tokens for the given shares.
*/
export function sharesToAssets(shares: bigint, totalSupply: bigint, harbergTotalSupply: bigint): bigint {
return (shares * harbergTotalSupply) / totalSupply;
}

View file

@ -0,0 +1,64 @@
import { formatUnits } from 'viem'
export function getAddressShortName(address: string) {
if (!address) {
return "";
}
const addressBegin = address.substring(0, 6);
const addressEnd = address.substring(address.length - 4, address.length);
return addressBegin + "..." + addressEnd;
}
export function compactNumber(number: number) {
return Intl.NumberFormat("en-US", {
notation: "compact",
// minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(number);
}
export function formatBigNumber(number: bigint, decimals: number, digits: number = 5) {
let bigIntNumber = number;
if(!bigIntNumber){
bigIntNumber = BigInt(0)
}
const formattedNumber = Number(formatUnits(bigIntNumber, decimals))
if(formattedNumber === 0){
return "0"
}
return formattedNumber.toFixed(digits)
}
export function bigInt2Number(number: bigint, decimals: number) {
let bigIntNumber = number;
if(!bigIntNumber){
bigIntNumber = BigInt(0)
}
const formattedNumber = Number(formatUnits(bigIntNumber, decimals))
return formattedNumber
}
export function InsertCommaNumber(number: any) {
if (!number) {
return 0;
}
const formattedWithOptions = number.toLocaleString("en-US");
return formattedWithOptions;
}
export function formatBigIntDivision(nominator: bigint, denominator: bigint, digits: number = 2) {
if (!nominator) {
return 0;
}
let display = nominator.toString();
const decimal = (Number(denominator) / 10).toString().length;
let [integer, fraction] = [display.slice(0, display.length - decimal), display.slice(display.length - decimal)];
// output type number
return Number(integer + "." + fraction);
}

View file

@ -0,0 +1,23 @@
export function info(text: string, data: any = null ){
if(data){
console.log(`%c ${text}`, 'color: #17a2b8', data);
} else{
console.log(`%c ${text}`, 'color: #17a2b8');
}
}
export function contract(text: string, data: any = null ){
if(data){
console.log(`%c ${text}`, 'color: #8732a8', data);
} else{
console.log(`%c ${text}`, 'color: #8732a8');
}
}
export default {
info,
contract
}

View file

@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

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