3 Commits

Author SHA1 Message Date
hueso
1264d91f5c Görli -> Sepolia
Some checks are pending
Deploy FrontEnd / deploy-staging (push) Waiting to run
Deploy FrontEnd / deploy-production (push) Waiting to run
CI script / lint (push) Waiting to run
CI script / build (push) Waiting to run
CI script / SonarCloud (push) Waiting to run
2024-07-18 18:57:30 -03:00
hueso
0108bbb0e9 add Rootstock testnet 2024-07-18 18:55:04 -03:00
hueso
511c7f980f fix chain detection 2024-07-18 16:36:05 -03:00
114 changed files with 8133 additions and 26826 deletions

View File

@@ -102,19 +102,3 @@ cd P2Pix-Front-End
# Run docker-compose up command # Run docker-compose up command
docker-compose up docker-compose up
``` ```
### Backend Communication
Backend Repo: `https://gitea.kosmos.org/hueso/helpix`
Backend Endpoint: `https://api.p2pix.co/release/1279331`
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {api-key}" \
-d '{"query": "{ depositAddeds { id seller token amount } }"}' \
https://api.studio.thegraph.com/query/113713/p-2-pix/sepolia
https://api.studio.thegraph.com/query/113713/p-2-pix/1
curl --request POST --url 'https://api.hm.bb.com.br/testes-portal-desenvolvedor/v1/boletos-pix/pagar?gw-app-key=95cad3f03fd9013a9d15005056825665' --header 'content-type: application/json' --data '{"pix":"00020101021226070503***63041654" }'

2
env.d.ts vendored
View File

@@ -1,3 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare const __APP_VERSION__: string;

12285
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,16 @@
{ {
"name": "p2pix-front-end", "name": "p2pix-front-end",
"version": "1.1.0", "version": "0.1.0",
"scripts": { "scripts": {
"start": "vite --host=0.0.0.0 --port 3000", "start": "vite --host=0.0.0.0 --port 3000",
"build": "run-p type-check build-only", "build": "run-p type-check build-only",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest",
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"coverage": "vitest run --coverage",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --skipLibCheck --noEmit", "type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore --fix", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
@@ -16,14 +18,12 @@
"@headlessui/vue": "^1.7.3", "@headlessui/vue": "^1.7.3",
"@heroicons/vue": "^2.0.12", "@heroicons/vue": "^2.0.12",
"@vueuse/core": "^9.12.0", "@vueuse/core": "^9.12.0",
"@web3-onboard/injected-wallets": "^2.11.2",
"@web3-onboard/vue": "^2.9.0",
"alchemy-sdk": "^2.3.0", "alchemy-sdk": "^2.3.0",
"axios": "^1.2.1", "axios": "^1.2.1",
"crc": "^3.8.0",
"marked": "^4.2.12", "marked": "^4.2.12",
"pinia": "^2.0.23",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"viem": "^2.31.3",
"vite-svg-loader": "^5.1.0",
"vue": "^3.2.41", "vue": "^3.2.41",
"vue-markdown": "^2.2.4", "vue-markdown": "^2.2.4",
"vue-router": "^4.1.5" "vue-router": "^4.1.5"
@@ -31,27 +31,33 @@
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@pinia/testing": "^0.0.14",
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@types/crc": "^3.8.0", "@types/crc": "^3.8.0",
"@types/jest": "^27.0.0",
"@types/marked": "^4.0.8", "@types/marked": "^4.0.8",
"@types/node": "^16.11.68", "@types/node": "^16.11.68",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
"@types/vue-markdown": "^2.2.1", "@types/vue-markdown": "^2.2.1",
"@vitejs/plugin-vue": "^3.1.2", "@vitejs/plugin-vue": "^3.1.2",
"@vitejs/plugin-vue-jsx": "^2.0.1", "@vitejs/plugin-vue-jsx": "^2.0.1",
"@vitest/coverage-c8": "^0.28.2",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.7",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"@wagmi/cli": "^2.3.1",
"autoprefixer": "^10.4.12", "autoprefixer": "^10.4.12",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",
"ethers": "^5.7.2",
"jsdom": "^21.1.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"tailwindcss": "^3.2.1", "tailwindcss": "^3.2.1",
"typescript": "~5.8.2", "typescript": "~4.7.4",
"vite": "^3.1.8", "vite": "^3.1.8",
"vue-tsc": "^2.2.8" "vitest": "^0.28.1",
"vue-tsc": "^1.0.8"
} }
} }

View File

@@ -1,57 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router";
import TopBar from "@/components/TopBar/TopBar.vue"; import TopBar from "@/components/TopBar/TopBar.vue";
import SpinnerComponent from "@/components/ui/SpinnerComponent.vue"; import SpinnerComponent from "@/components/SpinnerComponent.vue";
import ToasterComponent from "@/components/ui/ToasterComponent.vue";
import { init, useOnboard } from "@web3-onboard/vue";
import injectedModule from "@web3-onboard/injected-wallets";
import { Networks, DEFAULT_NETWORK } from "@/config/networks";
import { ref } from "vue";
const route = useRoute();
const injected = injectedModule();
const targetNetwork = ref(DEFAULT_NETWORK);
const web3Onboard = init({
wallets: [injected],
chains: Object.values(Networks).map((network) => ({
id: `0x${network.id.toString(16)}`,
token: network.nativeCurrency.symbol,
label: network.name,
rpcUrl: network.rpcUrls.default.http[0],
})),
connect: {
autoConnectLastWallet: true,
},
});
const { connectedWallet } = useOnboard();
if (!connectedWallet) {
web3Onboard.connectWallet();
}
</script> </script>
<template> <template>
<main class="p-3 sm:p-4 md:p-8">
<TopBar /> <TopBar />
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<template v-if="Component"> <template v-if="Component">
<Transition name="page" mode="out-in" appear>
<div :key="route.fullPath">
<Suspense> <Suspense>
<template #default> <component :is="Component"></component>
<component :is="Component" />
</template>
<template #fallback> <template #fallback>
<div class="flex w-full h-full justify-center items-center"> <div class="flex w-full h-full justify-center items-center">
<SpinnerComponent :width="'16'" :height="'16'" /> <SpinnerComponent :width="'16'" :height="'16'"></SpinnerComponent>
</div> </div>
</template> </template>
</Suspense> </Suspense>
</div>
</Transition>
</template> </template>
</RouterView> </RouterView>
<ToasterComponent :targetNetwork="targetNetwork" />
</main>
</template> </template>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

34
src/assets/bg.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="chevronDown" fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 508 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="white"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -1,3 +1,3 @@
<svg width="16" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3536 7.35355C13.1583 7.54882 12.8417 7.54882 12.6464 7.35355L7 1.70711L1.35355 7.35355C1.15829 7.54881 0.841709 7.54881 0.646446 7.35355C0.451184 7.15829 0.451184 6.84171 0.646446 6.64645L6.64645 0.646446C6.84171 0.451184 7.15829 0.451184 7.35355 0.646446L13.3536 6.64645C13.5488 6.84171 13.5488 7.15829 13.3536 7.35355Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.3536 7.35355C13.1583 7.54882 12.8417 7.54882 12.6464 7.35355L7 1.70711L1.35355 7.35355C1.15829 7.54881 0.841709 7.54881 0.646446 7.35355C0.451184 7.15829 0.451184 6.84171 0.646446 6.64645L6.64645 0.646446C6.84171 0.451184 7.15829 0.451184 7.35355 0.646446L13.3536 6.64645C13.5488 6.84171 13.5488 7.15829 13.3536 7.35355Z" fill="#111827"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 493 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.00016 0.333496C3.31816 0.333496 0.333496 3.32283 0.333496 7.0115C0.333496 9.9615 2.2435 12.4648 4.89283 13.3475C5.22616 13.4088 5.3475 13.2028 5.3475 13.0255C5.3475 12.8675 5.34216 12.4468 5.33883 11.8902C3.48416 12.2935 3.09283 10.9948 3.09283 10.9948C2.79016 10.2228 2.35283 10.0175 2.35283 10.0175C1.7475 9.60416 2.39883 9.61216 2.39883 9.61216C3.0675 9.65883 3.4195 10.3002 3.4195 10.3002C4.01416 11.3202 4.98016 11.0255 5.3595 10.8548C5.42083 10.4235 5.59283 10.1295 5.7835 9.96283C4.3035 9.79416 2.74683 9.22083 2.74683 6.66216C2.74683 5.9335 3.00683 5.33683 3.43283 4.87016C3.36416 4.7015 3.1355 4.02216 3.49816 3.1035C3.49816 3.1035 4.05816 2.9235 5.3315 3.7875C5.87534 3.63917 6.43645 3.56362 7.00016 3.56283C7.56683 3.5655 8.13683 3.6395 8.6695 3.7875C9.94216 2.9235 10.5008 3.10283 10.5008 3.10283C10.8648 4.02216 10.6355 4.7015 10.5675 4.87016C10.9942 5.33683 11.2528 5.9335 11.2528 6.66216C11.2528 9.2275 9.6935 9.79216 8.20883 9.9575C8.44816 10.1635 8.66083 10.5708 8.66083 11.1942C8.66083 12.0862 8.65283 12.8068 8.65283 13.0255C8.65283 13.2042 8.77283 13.4122 9.1115 13.3468C10.439 12.9016 11.5931 12.0504 12.4105 10.9135C13.2279 9.77669 13.6674 8.41171 13.6668 7.0115C13.6668 3.32283 10.6815 0.333496 7.00016 0.333496Z" fill="#1F2937"/>
<svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.41819,0,0,1.41819,0.0724699,0.290651)">
<path d="M7,0.333C3.318,0.333 0.333,3.323 0.333,7.012C0.333,9.961 2.244,12.465 4.893,13.348C5.226,13.409 5.348,13.203 5.348,13.025C5.348,12.868 5.342,12.447 5.339,11.89C3.484,12.294 3.093,10.995 3.093,10.995C2.79,10.223 2.353,10.018 2.353,10.018C1.748,9.604 2.399,9.612 2.399,9.612C3.068,9.659 3.42,10.3 3.42,10.3C4.014,11.32 4.98,11.025 5.36,10.855C5.421,10.424 5.593,10.13 5.784,9.963C4.304,9.794 2.747,9.221 2.747,6.662C2.747,5.934 3.007,5.337 3.433,4.87C3.364,4.702 3.136,4.022 3.498,3.104C3.498,3.104 4.058,2.924 5.332,3.788C5.875,3.639 6.436,3.564 7,3.563C7.567,3.566 8.137,3.64 8.67,3.788C9.942,2.924 10.501,3.103 10.501,3.103C10.865,4.022 10.636,4.702 10.568,4.87C10.994,5.337 11.253,5.934 11.253,6.662C11.253,9.227 9.694,9.792 8.209,9.958C8.448,10.164 8.661,10.571 8.661,11.194C8.661,12.086 8.653,12.807 8.653,13.025C8.653,13.204 8.773,13.412 9.112,13.347C10.439,12.902 11.593,12.05 12.411,10.914C13.228,9.777 13.667,8.412 13.667,7.012C13.667,3.323 10.682,0.333 7,0.333Z" style="fill:rgb(31,41,55);"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.426716,0,0,0.426716,-0.646464,-0.646464)">
<path d="M41,4L9,4C6.24,4 4,6.24 4,9L4,41C4,43.76 6.24,46 9,46L41,46C43.76,46 46,43.76 46,41L46,9C46,6.24 43.76,4 41,4ZM17,20L17,39L11,39L11,20L17,20ZM11,14.47C11,13.07 12.2,12 14,12C15.8,12 16.93,13.07 17,14.47C17,15.87 15.88,17 14,17C12.2,17 11,15.87 11,14.47ZM39,39L33,39L33,29C33,27 32,25 29.5,24.96L29.42,24.96C27,24.96 26,27.02 26,29L26,39L20,39L20,20L26,20L26,22.56C26,22.56 27.93,20 31.81,20C35.78,20 39,22.73 39,28.26L39,39Z" style="fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1001 B

View File

@@ -6,9 +6,11 @@
#app { #app {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 2rem;
height: fit-content; height: fit-content;
min-height: 100vh; min-height: 100vh;
background: radial-gradient(ellipse at 50% -50%, rgba(49, 46, 129, 1) 60%, rgba(24, 30, 42, 1) 80%); background-image: url( './bg.svg' );
background-size: cover;
font-weight: normal; font-weight: normal;
} }
@@ -24,13 +26,3 @@ a,
background-color: hsla(160, 100%, 37%, 0.2); background-color: hsla(160, 100%, 37%, 0.2);
} }
} }
.main-container {
@apply flex w-full md:max-w-lg flex-col justify-center items-center px-4 sm:px-8 py-4 sm:py-6 gap-4 rounded-lg border border-gray-500 backdrop-blur-md drop-shadow-lg shadow-lg mt-10;
}
input[type="number"] {
appearance: textfield;
-webkit-appearance: textfield;
-moz-appearance: textfield;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.16801 2.56435C8.09081 2.43177 7.89923 2.4319 7.82221 2.56458L4.82431 7.72899C4.76983 7.82284 4.80013 7.94301 4.89259 7.99981L7.89038 9.84139C7.95451 9.88079 8.03533 9.88085 8.09952 9.84154L11.1069 7.99986C11.1996 7.94308 11.23 7.82262 11.1752 7.72866L8.16801 2.56435ZM8.08754 10.7831C8.02182 10.8253 7.93759 10.8253 7.87181 10.7833L5.51555 9.27665C5.33379 9.16043 5.1222 9.37463 5.24065 9.55495L7.8123 13.4701C7.89136 13.5905 8.06789 13.5903 8.14678 13.4699L10.7098 9.55566C10.828 9.37517 10.6161 9.16127 10.4345 9.27775L8.08754 10.7831Z" fill="#3B82F6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.82232 2.56456C7.89934 2.43187 8.09092 2.43174 8.16812 2.56432L11.1754 7.72864C11.2301 7.8226 11.1997 7.94305 11.107 7.99984L8.09963 9.84152C8.03544 9.88082 7.95463 9.88077 7.89049 9.84137L4.8927 7.99979C4.80024 7.94299 4.76994 7.82282 4.82442 7.72897L7.82232 2.56456ZM7.87193 10.7833C7.9377 10.8253 8.02193 10.8253 8.08765 10.7831L10.4346 9.27773C10.6162 9.16125 10.8281 9.37515 10.7099 9.55563L8.1469 13.4698C8.06801 13.5903 7.89147 13.5904 7.81241 13.4701L5.24076 9.55492C5.12232 9.3746 5.3339 9.1604 5.51566 9.27662L7.87193 10.7833Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1 +0,0 @@
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" fill="#efb914" fill-rule="nonzero" r="16"/><path d="M21.002 9.855A7.947 7.947 0 0124 15.278l-2.847-.708a5.357 5.357 0 00-3.86-3.667c-2.866-.713-5.76.991-6.465 3.806s1.05 5.675 3.917 6.388a5.373 5.373 0 005.134-1.43l2.847.707a7.974 7.974 0 01-5.2 3.385L16.716 27l-2.596-.645.644-2.575a8.28 8.28 0 01-1.298-.323l-.643 2.575-2.596-.646.81-3.241c-2.378-1.875-3.575-4.996-2.804-8.081s3.297-5.281 6.28-5.823L15.323 5l2.596.645-.644 2.575a8.28 8.28 0 011.298.323l.643-2.575 2.596.646z" fill="#fff"/></g></svg>

Before

Width:  |  Height:  |  Size: 644 B

View File

@@ -1,32 +0,0 @@
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.3s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.page-enter-active,
.page-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
transform: translateY(15px);
}
.resize-enter-active,
.resize-leave-active {
max-height: 100px;
transition: all 0.3s ease;
}
.resize-enter-from,
.resize-leave-to {
max-height: 0px;
}

View File

@@ -1,7 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <path d="M4.52683 11.5021C9.55816 11.5021 12.3102 7.33344 12.3102 3.71877C12.3102 3.6001 12.3102 3.4821 12.3022 3.36544C12.8376 2.97779 13.2997 2.49784 13.6668 1.9481C13.1675 2.16944 12.6379 2.31461 12.0955 2.37877C12.6666 2.03681 13.094 1.499 13.2982 0.865436C12.7613 1.18403 12.174 1.40859 11.5615 1.52944C11.1491 1.09061 10.6035 0.799992 10.0092 0.702573C9.41498 0.605153 8.80517 0.706369 8.27424 0.990549C7.74331 1.27473 7.32088 1.72602 7.07236 2.27454C6.82383 2.82307 6.76307 3.43823 6.8995 4.02477C5.81189 3.97026 4.7479 3.68765 3.77659 3.19528C2.80529 2.70291 1.94838 2.01179 1.2615 1.16677C0.911708 1.7689 0.804555 2.48172 0.961853 3.16008C1.11915 3.83844 1.52907 4.43135 2.10816 4.8181C1.673 4.80551 1.24725 4.68844 0.866829 4.47677V4.51144C0.867089 5.14297 1.08576 5.75497 1.48576 6.24367C1.88576 6.73238 2.44247 7.06769 3.0615 7.19277C2.6587 7.30258 2.23609 7.31854 1.82616 7.23944C2.00094 7.78309 2.34128 8.2585 2.79958 8.59918C3.25788 8.93986 3.81121 9.12875 4.38216 9.13944C3.81491 9.58515 3.16535 9.91466 2.47065 10.1091C1.77594 10.3036 1.04971 10.3592 0.333496 10.2728C1.58457 11.0757 3.04029 11.5015 4.52683 11.4994" fill="#1F2937"/>
<svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.44084,0,0,1.44084,-0.0860978,1.23349)">
<path d="M4.527,11.502C9.558,11.502 12.31,7.333 12.31,3.719C12.31,3.6 12.31,3.482 12.302,3.365C12.838,2.978 13.3,2.498 13.667,1.948C13.168,2.169 12.638,2.315 12.096,2.379C12.667,2.037 13.094,1.499 13.298,0.865C12.761,1.184 12.174,1.409 11.562,1.529C11.149,1.091 10.604,0.8 10.009,0.703C9.415,0.605 8.805,0.706 8.274,0.991C7.743,1.275 7.321,1.726 7.072,2.275C6.824,2.823 6.763,3.438 6.9,4.025C5.812,3.97 4.748,3.688 3.777,3.195C2.805,2.703 1.948,2.012 1.262,1.167C0.912,1.769 0.805,2.482 0.962,3.16C1.119,3.838 1.529,4.431 2.108,4.818C1.673,4.806 1.247,4.688 0.867,4.477L0.867,4.511C0.867,5.143 1.086,5.755 1.486,6.244C1.886,6.732 2.442,7.068 3.062,7.193C2.659,7.303 2.236,7.319 1.826,7.239C2.001,7.783 2.341,8.259 2.8,8.599C3.258,8.94 3.811,9.129 4.382,9.139C3.815,9.585 3.165,9.915 2.471,10.109C1.776,10.304 1.05,10.359 0.333,10.273C1.585,11.076 3.04,11.502 4.527,11.499" style="fill:rgb(31,41,55);fill-rule:nonzero;"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,104 @@
import { expectTypeOf, it, expect } from "vitest";
import {
getTokenAddress,
getP2PixAddress,
getProviderUrl,
isPossibleNetwork,
possibleChains,
network2Chain,
} from "../addresses";
import { setActivePinia, createPinia } from "pinia";
import { NetworkEnum } from "@/model/NetworkEnum";
import { useEtherStore } from "@/store/ether";
describe("addresses.ts types", () => {
it("My addresses.ts types work properly", () => {
expectTypeOf(getTokenAddress).toBeFunction();
expectTypeOf(getP2PixAddress).toBeFunction();
expectTypeOf(getProviderUrl).toBeFunction();
expectTypeOf(isPossibleNetwork).toBeFunction();
expectTypeOf(possibleChains).toBeObject();
expectTypeOf(network2Chain).toBeObject();
});
});
describe("addresses.ts functions", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("getTokenAddress Ethereum", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.ethereum);
expect(getTokenAddress()).toBe(
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
it("getTokenAddress Polygon", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.polygon);
expect(getTokenAddress()).toBe(
"0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29"
);
});
it("getTokenAddress Default", () => {
expect(getTokenAddress()).toBe(
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
it("getP2PixAddress Ethereum", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.ethereum);
expect(getP2PixAddress()).toBe(
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
);
});
it("getP2PixAddress Polygon", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.polygon);
expect(getP2PixAddress()).toBe(
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
it("getP2PixAddress Default", () => {
expect(getP2PixAddress()).toBe(
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
);
});
it("getProviderUrl Ethereum", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.ethereum);
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
});
it("getProviderUrl Polygon", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.polygon);
expect(getProviderUrl()).toBe(import.meta.env.VITE_MUMBAI_API_URL);
});
it("getProviderUrl Default", () => {
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
});
it("isPossibleNetwork Returns", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.ethereum);
expect(isPossibleNetwork("0x5")).toBe(true);
expect(isPossibleNetwork("5")).toBe(true);
expect(isPossibleNetwork("0x13881")).toBe(true);
expect(isPossibleNetwork("80001")).toBe(true);
expect(isPossibleNetwork("")).toBe(false);
expect(isPossibleNetwork(" ")).toBe(false);
expect(isPossibleNetwork("0x55")).toBe(false);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
import { useEtherStore } from "@/store/ether";
import { NetworkEnum } from "@/model/NetworkEnum";
const getTokenAddress = (network?: NetworkEnum): string => {
const etherStore = useEtherStore();
const possibleTokenAddresses: { [key: string]: string } = {
Ethereum: "0x3eBE67A2C7bdB2081CBd34ba3281E90377462289",
Polygon: "0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29",
Rootstock: "0xfE841c74250e57640390f46d914C88d22C51e82e",
};
return possibleTokenAddresses[network ? network : etherStore.networkName];
};
const getP2PixAddress = (network?: NetworkEnum): string => {
const etherStore = useEtherStore();
const possibleP2PixAddresses: { [key: string]: string } = {
Ethereum: "0xb7cD135F5eFD9760981e02E2a898790b688939fe",
Polygon: "0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00",
Rootstock: "0x98ba35eb14b38D6Aa709338283af3e922476dE34",
};
return possibleP2PixAddresses[network ? network : etherStore.networkName];
};
const getProviderUrl = (): string => {
const etherStore = useEtherStore();
const possibleProvidersUrls: { [key: string]: string } = {
Ethereum: import.meta.env.VITE_SEPOLIA_API_URL,
Polygon: import.meta.env.VITE_MUMBAI_API_URL,
Rootstock: import.meta.env.VITE_RSK_API_URL,
};
return possibleProvidersUrls[etherStore.networkName];
};
const possibleChains: { [key: string]: NetworkEnum } = {
"11155111": NetworkEnum.ethereum,
"80001": NetworkEnum.polygon,
"31": NetworkEnum.rootstock,
};
const network2Chain: { [key: string]: string } = {
Ethereum: "0xAA36A7",
Polygon: "0x13881",
Localhost: "0x7a69",
Rootstock: "0x1f",
};
const isPossibleNetwork = (networkChain: string): boolean => {
if (Object.keys(possibleChains).includes(networkChain.toString())) {
return true;
}
return false;
};
export {
getTokenAddress,
getProviderUrl,
possibleChains,
network2Chain,
isPossibleNetwork,
getP2PixAddress,
};

View File

@@ -1,82 +1,100 @@
import { getContract } from "./provider"; import { useEtherStore } from "@/store/ether";
import { ChainContract } from "viem";
import {
parseEther,
type Address,
type TransactionReceipt,
} from "viem";
export const addLock = async ( import { getContract, getProvider } from "./provider";
sellerAddress: Address, import { getP2PixAddress, getTokenAddress } from "./addresses";
tokenAddress: Address,
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import { BigNumber, ethers } from "ethers";
import { parseEther } from "ethers/lib/utils";
const addLock = async (
seller: string,
token: string,
amount: number amount: number
): Promise<bigint> => { ): Promise<string> => {
const { address, abi, wallet, client, account } = await getContract(); const etherStore = useEtherStore();
const parsedAmount = parseEther(amount.toString());
if (!wallet) { const p2pContract = getContract();
throw new Error("Wallet not connected");
}
const { result, request } = await client.simulateContract({ const lock = await p2pContract.lock(
address, seller,
abi, token,
functionName: "lock", etherStore.walletAddress, // String "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" (Example)
args: [sellerAddress, tokenAddress, parsedAmount, [], []], ethers.constants.AddressZero, // String "0x0000000000000000000000000000000000000000"
account, 0,
}); parseEther(String(amount)), // BigNumber
const hash = await wallet.writeContract(request); [],
const receipt = await client.waitForTransactionReceipt({ hash }); []
);
if (!receipt.status) const lock_rec = await lock.wait();
throw new Error("Transaction failed: " + receipt.transactionHash); const [t] = lock_rec.events;
return result; return String(t.args.lockID);
}; };
export const withdrawDeposit = async ( const releaseLock = async (
amount: string, pixKey: number,
token: Address amount: number,
): Promise<boolean> => { e2eId: string,
const { address, abi, wallet, client, account } = await getContract(); lockId: string
): Promise<any> => {
const mockBacenSigner = new ethers.Wallet(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
);
if (!wallet) { const messageToSign = ethers.utils.solidityKeccak256(
throw new Error("Wallet not connected"); ["uint160", "uint256", "bytes32"],
} [
pixKey,
parseEther(String(amount)),
ethers.utils.formatBytes32String(e2eId),
]
);
const { request } = await client.simulateContract({ const messageHashBytes = ethers.utils.arrayify(messageToSign);
address, const flatSig = await mockBacenSigner.signMessage(messageHashBytes);
abi, const provider = getProvider();
functionName: "withdraw",
args: [token, parseEther(amount), []],
account
});
const hash = await wallet.writeContract(request); const sig = ethers.utils.splitSignature(flatSig);
const receipt = await client.waitForTransactionReceipt({ hash });
return receipt.status === "success"; const signer = provider.getSigner();
const p2pContract = new ethers.Contract(getP2PixAddress(), p2pix.abi, signer);
const release = await p2pContract.release(
BigNumber.from(lockId),
ethers.constants.AddressZero,
ethers.utils.formatBytes32String(e2eId),
sig.r,
sig.s,
sig.v
);
await release.wait();
return release;
}; };
export const releaseLock = async ( const cancelDeposit = async (depositId: BigNumber): Promise<any> => {
lockID: bigint, const contract = getContract();
pixTimestamp: `0x${string}`&{lenght:34},
signature: `0x${string}`
): Promise<TransactionReceipt> => {
const { address, abi, wallet, client, account } = await getContract();
if (!wallet) { const cancel = await contract.cancelDeposit(depositId);
throw new Error("Wallet not connected"); await cancel.wait();
}
const { request } = await client.simulateContract({ return cancel;
address,
abi,
functionName: "release",
args: [BigInt(lockID), pixTimestamp, signature],
account
});
const hash = await wallet.writeContract(request);
return client.waitForTransactionReceipt({ hash });
}; };
const withdrawDeposit = async (amount: string): Promise<any> => {
const contract = getContract();
const withdraw = await contract.withdraw(
getTokenAddress(),
parseEther(String(amount)),
[]
);
await withdraw.wait();
return withdraw;
};
export { cancelDeposit, withdrawDeposit, addLock, releaseLock };

View File

@@ -1,174 +1,143 @@
import { useUser } from "@/composables/useUser"; import { useEtherStore } from "@/store/ether";
import { formatEther, toHex, stringToHex } from "viem"; import { Contract, ethers } from "ethers";
import type { PublicClient, Address } from "viem";
import { Networks } from "@/config/networks"; import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import { formatEther } from "ethers/lib/utils";
import { getContract } from "./provider"; import { getContract } from "./provider";
import { p2PixAbi } from "./abi"
import type { ValidDeposit } from "@/model/ValidDeposit"; import type { ValidDeposit } from "@/model/ValidDeposit";
import type { NetworkConfig } from "@/model/NetworkEnum"; import { getP2PixAddress, getTokenAddress } from "./addresses";
import { NetworkEnum } from "@/model/NetworkEnum";
import type { UnreleasedLock } from "@/model/UnreleasedLock"; import type { UnreleasedLock } from "@/model/UnreleasedLock";
import { ChainContract } from "viem"; import type { Pix } from "@/model/Pix";
const getNetworksLiquidity = async (): Promise<void> => { const getNetworksLiquidity = async (): Promise<void> => {
const user = useUser(); const etherStore = useEtherStore();
user.setLoadingNetworkLiquidity(true);
const depositLists: ValidDeposit[][] = []; const sepoliaProvider = new ethers.providers.JsonRpcProvider(
import.meta.env.VITE_SEPOLIA_API_URL,
11155111
); // sepolia provider
const mumbaiProvider = new ethers.providers.JsonRpcProvider(
import.meta.env.VITE_MUMBAI_API_URL,
80001
); // mumbai provider
const rootstockProvider = new ethers.providers.JsonRpcProvider(
import.meta.env.VITE_RSK_API_URL,
31
); // rootstock provider
for (const network of Object.values(Networks)) { const p2pContractSepolia = new ethers.Contract(
const deposits = await getValidDeposits( getP2PixAddress(NetworkEnum.ethereum),
user.network.value.tokens[user.selectedToken.value].address, p2pix.abi,
network sepoliaProvider
); );
if (deposits) depositLists.push(deposits); const p2pContractMumbai = new ethers.Contract(
} getP2PixAddress(NetworkEnum.polygon),
p2pix.abi,
const allDeposits = depositLists.flat(); mumbaiProvider
user.setDepositsValidList(allDeposits);
user.setLoadingNetworkLiquidity(false);
};
const getParticipantID = async (
seller: Address,
token: Address
): Promise<string> => {
const { address, abi, client } = await getContract();
const participantIDHex = await client.readContract({
address,
abi,
functionName: "getPixTarget",
args: [seller, token],
});
// Remove '0x' prefix and convert hex to UTF-8 string
const hexString =
typeof participantIDHex === "string"
? participantIDHex
: toHex(participantIDHex as bigint);
if (!hexString) throw new Error("Participant ID not found");
const bytes = new Uint8Array(
hexString
.slice(2)
.match(/.{1,2}/g)!
.map((byte: string) => parseInt(byte, 16))
); );
// Remove null bytes from the end of the string
return new TextDecoder().decode(bytes).replace(/\0/g, ""); const p2pContractRootstock = new ethers.Contract(
getP2PixAddress(NetworkEnum.rootstock),
p2pix.abi,
rootstockProvider
);
etherStore.setLoadingNetworkLiquidity(true);
const depositListSepolia = await getValidDeposits(
getTokenAddress(NetworkEnum.ethereum),
p2pContractSepolia
);
const depositListMumbai = await getValidDeposits(
getTokenAddress(NetworkEnum.polygon),
p2pContractMumbai
);
const depositListRootstock = await getValidDeposits(
getTokenAddress(NetworkEnum.rsktestnet),
p2pContractRootstock
);
etherStore.setDepositsValidListSepolia(depositListSepolia);
etherStore.setDepositsValidListMumbai(depositListMumbai);
etherStore.setDepositsValidListRootstock(depositListRootstock);
etherStore.setLoadingNetworkLiquidity(false);
}; };
const getValidDeposits = async ( const getValidDeposits = async (
token: Address, token: string,
network: NetworkConfig, contract?: Contract
contractInfo?: { client: PublicClient; address: Address }
): Promise<ValidDeposit[]> => { ): Promise<ValidDeposit[]> => {
let client: PublicClient, abi; let p2pContract: Contract;
if (contractInfo) { if (contract) {
({ client } = contractInfo); p2pContract = contract;
abi = p2PixAbi;
} else { } else {
({ abi, client } = await getContract(true)); p2pContract = getContract(true);
} }
const body = { const filterDeposits = p2pContract.filters.DepositAdded(null);
query: ` const eventsDeposits = await p2pContract.queryFilter(filterDeposits);
{
depositAddeds(where: { token: "${token}" }) {
seller
amount
blockTimestamp
blockNumber
}
}
`,
};
const depositLogs = await fetch( network.subgraphUrls[0], {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
// remove doubles from sellers list
const depositData = await depositLogs.json();
if (!depositData.data) {
console.error("Error fetching deposit logs");
return [];
}
const depositAddeds = depositData.data.depositAddeds;
const uniqueSellers = depositAddeds.reduce(
(acc: Record<Address, boolean>, deposit: any) => {
acc[deposit.seller] = true;
return acc;
},
{} as Record<Address, boolean>
);
if (!contractInfo) {
// Get metamask provider contract
({ abi, client } = await getContract(true));
}
if (!contract) p2pContract = getContract(); // get metamask provider contract
const depositList: { [key: string]: ValidDeposit } = {}; const depositList: { [key: string]: ValidDeposit } = {};
const sellersList = Object.keys(uniqueSellers) as Address[]; await Promise.all(
// Use multicall to batch all getBalance requests eventsDeposits.map(async (deposit) => {
const balanceCalls = sellersList.map((seller) => ({ // Get liquidity only for the selected token
address: (network.contracts?.p2pix as ChainContract).address, if (deposit.args?.token != token) return null;
abi,
functionName: "getBalance",
args: [seller, token],
}));
const balanceResults = await client.multicall({ const mappedBalance = await p2pContract.getBalance(
contracts: balanceCalls as any, deposit.args?.seller,
}); token
);
// Process results into the depositList const mappedPixTarget = await p2pContract.getPixTarget(
sellersList.forEach((seller, index) => { deposit.args?.seller,
const mappedBalance = balanceResults[index]; token
);
if (!mappedBalance.error && mappedBalance.result) { let validDeposit: ValidDeposit | null = null;
const validDeposit: ValidDeposit = {
token, if (mappedBalance._hex) {
blockNumber: 0, validDeposit = {
remaining: Number(formatEther(mappedBalance.result as bigint)), token: token,
seller, blockNumber: deposit.blockNumber,
network, remaining: Number(formatEther(mappedBalance._hex)),
participantID: "", seller: deposit.args?.seller,
pixKey: Number(mappedPixTarget._hex),
}; };
depositList[seller + token] = validDeposit;
} }
});
if (validDeposit)
depositList[deposit.args?.seller + token] = validDeposit;
})
);
return Object.values(depositList); return Object.values(depositList);
}; };
const getUnreleasedLockById = async ( const getUnreleasedLockById = async (
lockID: bigint lockID: string
): Promise<UnreleasedLock> => { ): Promise<UnreleasedLock> => {
const { address, abi, client } = await getContract(); const p2pContract = getContract();
const pixData: Pix = {
pixKey: "",
};
const [ , , , amount, token, seller ] = await client.readContract({ const lock = await p2pContract.mapLocks(lockID);
address,
abi, const pixTarget = lock.pixTarget;
functionName: "mapLocks", const amount = formatEther(lock?.amount);
args: [lockID], pixData.pixKey = String(Number(pixTarget));
}); pixData.value = Number(amount);
return { return {
lockID, lockID: lockID,
amount: Number(formatEther(amount)), pix: pixData,
tokenAddress: token,
sellerAddress: seller,
}; };
}; };
export { export { getValidDeposits, getNetworksLiquidity, getUnreleasedLockById };
getValidDeposits,
getNetworksLiquidity,
getUnreleasedLockById,
getParticipantID,
};

View File

@@ -1,63 +1,95 @@
import { p2PixAbi } from "./abi"; import { useEtherStore } from "@/store/ether";
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import { updateWalletStatus } from "./wallet"; import { updateWalletStatus } from "./wallet";
import { import {
createPublicClient, getProviderUrl,
createWalletClient, isPossibleNetwork,
custom, possibleChains,
http, network2Chain,
PublicClient, getP2PixAddress,
WalletClient, } from "./addresses";
} from "viem";
import { useUser } from "@/composables/useUser";
import type { NetworkConfig } from "@/model/NetworkEnum";
import type { ChainContract } from "viem";
let walletClient: WalletClient | null = null; import { ethers } from "ethers";
const getPublicClient = (): PublicClient => { const getProvider = (
const user = useUser(); onlyAlchemyProvider: boolean = false
const rpcUrl = (user.network.value as NetworkConfig).rpcUrls.default.http[0]; ): ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider => {
const chain = user.network.value; const window_ = window as any;
const connection = window_.ethereum;
return createPublicClient({ if (!connection || onlyAlchemyProvider)
chain, return new ethers.providers.JsonRpcProvider(getProviderUrl()); // alchemy provider
transport: http(rpcUrl),
}); return new ethers.providers.Web3Provider(connection); // metamask provider
}; };
const getWalletClient = (): WalletClient | null => { const getContract = (onlyAlchemyProvider: boolean = false) => {
return walletClient; const provider = getProvider(onlyAlchemyProvider);
const signer = provider.getSigner();
return new ethers.Contract(getP2PixAddress(), p2pix.abi, signer);
}; };
const getContract = async (onlyRpcProvider = false) => { const connectProvider = async (): Promise<void> => {
const client = getPublicClient(); const window_ = window as any;
const user = useUser(); const connection = window_.ethereum;
const address = (user.network.value.contracts?.p2pix as ChainContract).address; const provider = getProvider();
const abi = p2PixAbi;
const wallet = onlyRpcProvider ? null : getWalletClient();
if (!client) { if (!(provider instanceof ethers.providers.Web3Provider)) {
throw new Error("Public client not initialized"); window.alert("Please, connect to metamask extension");
return;
} }
const [account] = wallet ? await wallet.getAddresses() : [null];
return { address, abi, client, wallet, account };
};
const connectProvider = async (p: any): Promise<void> => {
const user = useUser();
const chain = user.network.value;
const [account] = await p!.request({ method: "eth_requestAccounts" });
walletClient = createWalletClient({
account,
chain,
transport: custom(p),
});
await updateWalletStatus(); await updateWalletStatus();
listenToNetworkChange(connection);
listenToWalletChange(connection);
}; };
export { getPublicClient, getWalletClient, getContract, connectProvider }; const listenToWalletChange = (connection: any): void => {
connection.on("accountsChanged", async () => {
console.log("Changed account!");
updateWalletStatus();
});
};
const listenToNetworkChange = (connection: any) => {
const etherStore = useEtherStore();
connection.on("chainChanged", (networkChain: string) => {
console.log("Changed network!");
if (isPossibleNetwork(networkChain)) {
etherStore.setNetworkName(possibleChains[networkChain]);
updateWalletStatus();
} else {
window.alert("Invalid chain!");
}
});
};
const requestNetworkChange = async (network: string): Promise<boolean> => {
const etherStore = useEtherStore();
if (!etherStore.walletAddress) return true;
try {
const window_ = window as any;
await window_.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: network2Chain[network] }], // chainId must be in hexadecimal numbers
});
} catch {
return false;
}
return true;
};
export {
getProvider,
getContract,
connectProvider,
listenToNetworkChange,
requestNetworkChange,
};

View File

@@ -1,91 +1,44 @@
import { getContract, getPublicClient, getWalletClient } from "./provider"; import { getContract, getProvider } from "./provider";
import { parseEther, toHex, ChainContract } from "viem"; import { getTokenAddress, getP2PixAddress } from "./addresses";
import { mockTokenAbi } from "./abi"; import { parseEther } from "ethers/lib/utils";
import { useUser } from "@/composables/useUser";
import { createParticipant } from "@/utils/bbPay";
import type { Participant } from "@/utils/bbPay";
import type { Address } from "viem";
const getP2PixAddress = (): Address => { import { ethers } from "ethers";
const user = useUser();
return (user.network.value.contracts?.p2pix as ChainContract).address; import mockToken from "../utils/smart_contract_files/MockToken.json";
const approveTokens = async (tokenQty: string): Promise<any> => {
const provider = getProvider();
const signer = provider.getSigner();
const tokenContract = new ethers.Contract(
getTokenAddress(),
mockToken.abi,
signer
);
const apprv = await tokenContract.approve(
getP2PixAddress(),
parseEther(tokenQty)
);
await apprv.wait();
return apprv;
}; };
const approveTokens = async (participant: Participant): Promise<any> => { const addDeposit = async (tokenQty: string, pixKey: string): Promise<any> => {
const user = useUser(); const p2pContract = getContract();
const publicClient = getPublicClient();
const walletClient = getWalletClient();
if (!publicClient || !walletClient) { const deposit = await p2pContract.deposit(
throw new Error("Clients not initialized"); getTokenAddress(),
} parseEther(tokenQty),
pixKey,
user.setSeller(participant);
const [account] = await walletClient.getAddresses();
// Get token address
const tokenAddress = user.network.value.tokens[user.selectedToken.value].address;
// Check if the token is already approved
const allowance = await publicClient.readContract({
address: tokenAddress,
abi: mockTokenAbi,
functionName: "allowance",
args: [account, getP2PixAddress()],
});
if ( allowance < parseEther(participant.offer.toString()) ) {
// Approve tokens
const chain = user.network.value;
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: mockTokenAbi,
functionName: "approve",
args: [getP2PixAddress(), parseEther(participant.offer.toString())],
account,
chain,
});
await publicClient.waitForTransactionReceipt({ hash });
return true;
}
return true;
};
const addDeposit = async (): Promise<any> => {
const { address, abi, client } = await getContract();
const walletClient = getWalletClient();
const user = useUser();
if (!walletClient) {
throw new Error("Wallet client not initialized");
}
const [account] = await walletClient.getAddresses();
const sellerId = await createParticipant(user.seller.value);
user.setSellerId(sellerId.id);
if (!sellerId.id) {
throw new Error("Failed to create participant");
}
const chain = user.network.value;
const hash = await walletClient.writeContract({
address,
abi,
functionName: "deposit",
args: [
user.network.value.id + "-" + sellerId.id,
toHex("", { size: 32 }),
user.network.value.tokens[user.selectedToken.value].address,
parseEther(user.seller.value.offer.toString()),
true, true,
], ethers.utils.formatBytes32String("")
account, );
chain,
});
const receipt = await client.waitForTransactionReceipt({ hash }); await deposit.wait();
return receipt;
return deposit;
}; };
export { approveTokens, addDeposit }; export { approveTokens, addDeposit };

View File

@@ -1,46 +1,54 @@
import { formatEther, type Address } from "viem"; import { useEtherStore } from "@/store/ether";
import { useUser } from "@/composables/useUser";
import { getPublicClient, getWalletClient, getContract } from "./provider"; import { getContract, getProvider } from "./provider";
import { getTokenAddress, possibleChains, isPossibleNetwork } from "./addresses";
import { getValidDeposits, getUnreleasedLockById } from "./events"; import mockToken from "@/utils/smart_contract_files/MockToken.json";
import { ethers, type Event, type BigNumber } from "ethers";
import { formatEther } from "ethers/lib/utils";
import { getValidDeposits } from "./events";
import type { ValidDeposit } from "@/model/ValidDeposit"; import type { ValidDeposit } from "@/model/ValidDeposit";
import type { WalletTransaction } from "@/model/WalletTransaction"; import type { WalletTransaction } from "@/model/WalletTransaction";
import type { UnreleasedLock } from "@/model/UnreleasedLock"; import type { UnreleasedLock } from "@/model/UnreleasedLock";
import { LockStatus } from "@/model/LockStatus"; import type { Pix } from "@/model/Pix";
export const updateWalletStatus = async (): Promise<void> => { const updateWalletStatus = async (): Promise<void> => {
const user = useUser(); const etherStore = useEtherStore();
const publicClient = getPublicClient(); const provider = getProvider();
const walletClient = getWalletClient(); const signer = provider.getSigner();
if (!publicClient || !walletClient) { const { chainId } = await provider.getNetwork();
console.error("Client not initialized"); if(!isPossibleNetwork(chainId)){
return; window.alert("Invalid chain!:"+chainId);
return false;
} }
etherStore.setNetworkName(possibleChains[chainId]);
// Get balance const mockTokenContract = new ethers.Contract(
const [account] = await walletClient.getAddresses(); getTokenAddress(),
const balance = await publicClient.getBalance({ address: account }); mockToken.abi,
signer
);
user.setWalletAddress(account); const walletAddress = await provider.send("eth_requestAccounts", []);
user.setBalance(formatEther(balance)); const balance = await mockTokenContract.balanceOf(walletAddress[0]);
etherStore.setBalance(formatEther(balance));
etherStore.setWalletAddress(ethers.utils.getAddress(walletAddress[0]));
}; };
export const listValidDepositTransactionsByWalletAddress = async ( const listValidDepositTransactionsByWalletAddress = async (
walletAddress: Address walletAddress: string
): Promise<ValidDeposit[]> => { ): Promise<ValidDeposit[]> => {
const user = useUser(); const walletDeposits = await getValidDeposits(getTokenAddress());
const walletDeposits = await getValidDeposits(
user.network.value.tokens[user.selectedToken.value].address,
user.network.value
);
if (walletDeposits) { if (walletDeposits) {
return walletDeposits return walletDeposits
.filter((deposit) => deposit.seller == walletAddress) .filter((deposit) => deposit.seller == walletAddress)
.sort((a: ValidDeposit, b: ValidDeposit) => { .sort((a, b) => {
return b.blockNumber - a.blockNumber; return b.blockNumber - a.blockNumber;
}); });
} }
@@ -48,419 +56,191 @@ export const listValidDepositTransactionsByWalletAddress = async (
return []; return [];
}; };
const getLockStatus = async (id: bigint): Promise<LockStatus> => { const getLockStatus = async (id: [BigNumber]): Promise<number> => {
const { address, abi, client } = await getContract(); const p2pContract = getContract();
const [ sortedIDs , status ] = await client.readContract({ const res = await p2pContract.getLocksStatus([id]);
address,
abi, return res[1][0];
functionName: "getLocksStatus",
args: [[id]],
});
return status[0];
}; };
export const listAllTransactionByWalletAddress = async ( const filterLockStatus = async (
walletAddress: Address transactions: Event[]
): Promise<WalletTransaction[]> => { ): Promise<WalletTransaction[]> => {
const user = useUser(); const txs = await Promise.all(
transactions.map(async (transaction) => {
// Get the current network for the subgraph URL const tx: WalletTransaction = {
const network = user.network.value; token: transaction.args?.token ? transaction.args?.token : "",
blockNumber: transaction.blockNumber ? transaction.blockNumber : -1,
// Query subgraph for all relevant transactions amount: transaction.args?.amount
const subgraphQuery = { ? Number(formatEther(transaction.args?.amount))
query: ` : -1,
{ seller: transaction.args?.seller ? transaction.args?.seller : "",
depositAddeds(where: {seller: "${walletAddress.toLowerCase()}"}) { buyer: transaction.args?.buyer ? transaction.args?.buyer : "",
id event: transaction.event ? transaction.event : "",
seller lockStatus:
token transaction.event == "LockAdded"
amount ? await getLockStatus(transaction.args?.lockID)
blockTimestamp : -1,
blockNumber transactionHash: transaction.transactionHash
transactionHash ? transaction.transactionHash
} : "",
lockAddeds(where: {buyer: "${walletAddress.toLowerCase()}"}) { transactionID: transaction.args?.lockID
buyer ? String(transaction.args?.lockID)
lockID : "",
seller
amount
blockTimestamp
blockNumber
transactionHash
}
lockReleaseds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
buyer
lockId
blockTimestamp
blockNumber
transactionHash
}
depositWithdrawns(where: {seller: "${walletAddress.toLowerCase()}"}) {
seller
token
amount
blockTimestamp
blockNumber
transactionHash
}
}
`,
}; };
const response = await fetch(network.subgraphUrls[0], { return tx;
method: "POST", })
headers: { );
"Content-Type": "application/json",
},
body: JSON.stringify(subgraphQuery),
});
const data = await response.json(); return txs;
// Convert all transactions to common WalletTransaction format };
const transactions: WalletTransaction[] = [];
// Process deposit added events const listAllTransactionByWalletAddress = async (
if (data.data?.depositAddeds) { walletAddress: string
for (const deposit of data.data.depositAddeds) { ): Promise<WalletTransaction[]> => {
transactions.push({ const p2pContract = getContract(true);
token: deposit.token,
blockNumber: parseInt(deposit.blockNumber),
blockTimestamp: parseInt(deposit.blockTimestamp),
amount: parseFloat(formatEther(BigInt(deposit.amount))),
seller: deposit.seller,
buyer: "",
event: "DepositAdded",
lockStatus: undefined,
transactionHash: deposit.transactionHash,
});
}
}
// Process lock added events const filterDeposits = p2pContract.filters.DepositAdded([walletAddress]);
if (data.data?.lockAddeds) { const eventsDeposits = await p2pContract.queryFilter(filterDeposits);
for (const lock of data.data.lockAddeds) {
// Get lock status from the contract
const lockStatus = await getLockStatus(BigInt(lock.lockID));
transactions.push({ const filterAddedLocks = p2pContract.filters.LockAdded([walletAddress]);
token: lock.token, const eventsAddedLocks = await p2pContract.queryFilter(filterAddedLocks);
blockNumber: parseInt(lock.blockNumber),
blockTimestamp: parseInt(lock.blockTimestamp),
amount: parseFloat(formatEther(BigInt(lock.amount))),
seller: lock.seller,
buyer: lock.buyer,
event: "LockAdded",
lockStatus: lockStatus,
transactionHash: lock.transactionHash,
transactionID: lock.lockID.toString(),
});
}
}
// Process lock released events const filterReleasedLocks = p2pContract.filters.LockReleased([walletAddress]);
if (data.data?.lockReleaseds) { const eventsReleasedLocks = await p2pContract.queryFilter(
for (const release of data.data.lockReleaseds) { filterReleasedLocks
transactions.push({ );
token: undefined, // Subgraph doesn't provide token in this event, we could enhance this later
blockNumber: parseInt(release.blockNumber),
blockTimestamp: parseInt(release.blockTimestamp),
amount: -1, // Amount not available in this event
seller: "",
buyer: release.buyer,
event: "LockReleased",
lockStatus: undefined,
transactionHash: release.transactionHash,
transactionID: release.lockId.toString(),
});
}
}
// Process deposit withdrawn events const filterWithdrawnDeposits = p2pContract.filters.DepositWithdrawn([
if (data.data?.depositWithdrawns) { walletAddress,
for (const withdrawal of data.data.depositWithdrawns) { ]);
transactions.push({ const eventsWithdrawnDeposits = await p2pContract.queryFilter(
token: withdrawal.token, filterWithdrawnDeposits
blockNumber: parseInt(withdrawal.blockNumber), );
blockTimestamp: parseInt(withdrawal.blockTimestamp),
amount: parseFloat(formatEther(BigInt(withdrawal.amount))),
seller: withdrawal.seller,
buyer: "",
event: "DepositWithdrawn",
lockStatus: undefined,
transactionHash: withdrawal.transactionHash,
});
}
}
// Sort transactions by block number (newest first) const lockStatusFiltered = await filterLockStatus(
return transactions.sort((a, b) => b.blockNumber - a.blockNumber); [
...eventsDeposits,
...eventsAddedLocks,
...eventsReleasedLocks,
...eventsWithdrawnDeposits,
].sort((a, b) => {
return b.blockNumber - a.blockNumber;
})
);
return lockStatusFiltered;
}; };
// get wallet's release transactions // get wallet's release transactions
export const listReleaseTransactionByWalletAddress = async ( const listReleaseTransactionByWalletAddress = async (
walletAddress: Address walletAddress: string
) => { ): Promise<Event[]> => {
const user = useUser(); const p2pContract = getContract(true);
const network = user.network.value;
// Query subgraph for release transactions const filterReleasedLocks = p2pContract.filters.LockReleased([walletAddress]);
const subgraphQuery = { const eventsReleasedLocks = await p2pContract.queryFilter(
query: ` filterReleasedLocks
{
lockReleaseds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
buyer
lockId
e2eId
blockTimestamp
blockNumber
transactionHash
}
}
`,
};
// Fetch data from subgraph
const response = await fetch(network.subgraphUrls[0], {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subgraphQuery),
});
const data = await response.json();
// Process the subgraph response into the same format as the previous implementation
if (!data.data?.lockReleaseds) {
return [];
}
// Transform the subgraph data to match the event log decode format
return data.data.lockReleaseds
.sort((a: any, b: any) => {
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
})
.map((release: any) => {
try {
// Create a structure similar to the decoded event log
return {
eventName: "LockReleased",
args: {
buyer: release.buyer,
lockID: BigInt(release.lockId),
e2eId: release.e2eId,
},
// Add any other necessary fields to match the original return format
blockNumber: BigInt(release.blockNumber),
transactionHash: release.transactionHash,
};
} catch (error) {
console.error("Error processing subgraph data", error);
return null;
}
})
.filter((decoded: any) => decoded !== null);
};
const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
const user = useUser();
const network = user.network.value;
// Query subgraph for lock added transactions
const subgraphQuery = {
query: `
{
lockAddeds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
buyer
lockID
seller
amount
blockTimestamp
blockNumber
transactionHash
}
}
`,
};
try {
// Fetch data from subgraph
const response = await fetch(network.subgraphUrls[0], {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subgraphQuery),
});
const data = await response.json();
if (!data.data?.lockAddeds) {
return [];
}
// Transform the subgraph data to match the event log decode format
return data.data.lockAddeds
.sort((a: any, b: any) => {
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
})
.map((lock: any) => {
try {
// Create a structure similar to the decoded event log
return {
eventName: "LockAdded",
args: {
buyer: lock.buyer,
lockID: BigInt(lock.lockID),
seller: lock.seller,
token: undefined, // Token not available in LockAdded subgraph event
amount: BigInt(lock.amount),
},
// Add other necessary fields to match the original format
blockNumber: BigInt(lock.blockNumber),
transactionHash: lock.transactionHash,
};
} catch (error) {
console.error("Error processing subgraph data", error);
return null;
}
})
.filter((decoded: any) => decoded !== null);
} catch (error) {
console.error("Error fetching from subgraph:", error);
}
};
const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
const user = useUser();
const network = user.network.value;
// Query subgraph for lock added transactions where seller matches
const subgraphQuery = {
query: `
{
lockAddeds(where: {seller: "${sellerAddress.toLowerCase()}"}) {
buyer
lockID
seller
amount
blockTimestamp
blockNumber
transactionHash
}
}
`,
};
try {
// Fetch data from subgraph
const response = await fetch(network.subgraphUrls[0], {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subgraphQuery),
});
const data = await response.json();
if (!data.data?.lockAddeds) {
return [];
}
// Transform the subgraph data to match the event log decode format
return data.data.lockAddeds
.sort((a: any, b: any) => {
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
})
.map((lock: any) => {
try {
// Create a structure similar to the decoded event log
return {
eventName: "LockAdded",
args: {
buyer: lock.buyer,
lockID: BigInt(lock.lockID),
seller: lock.seller,
token: undefined, // Token not available in LockAdded subgraph event
amount: BigInt(lock.amount),
},
// Add other necessary fields to match the original format
blockNumber: BigInt(lock.blockNumber),
transactionHash: lock.transactionHash,
};
} catch (error) {
console.error("Error processing subgraph data", error);
return null;
}
})
.filter((decoded: any) => decoded !== null);
} catch (error) {
console.error("Error fetching from subgraph:", error);
return [];
}
};
export const checkUnreleasedLock = async (
walletAddress: Address
): Promise<UnreleasedLock | undefined> => {
const { address, abi, client } = await getContract();
const addedLocks = await listLockTransactionByWalletAddress(walletAddress);
if (!addedLocks.length) return undefined;
const lockIds = addedLocks.map((lock: any) => lock.args.lockID);
const [ sortedIDs, status ] = await client.readContract({
address,
abi,
functionName: "getLocksStatus",
args: [lockIds],
});
const unreleasedLockId = status.findIndex(
(status: LockStatus) => status == LockStatus.Active
); );
if (unreleasedLockId !== -1) return eventsReleasedLocks.sort((a, b) => {
return getUnreleasedLockById(sortedIDs[unreleasedLockId]); return b.blockNumber - a.blockNumber;
});
}; };
export const getActiveLockAmount = async ( const listLockTransactionByWalletAddress = async (
walletAddress: Address walletAddress: string
): Promise<number> => { ): Promise<Event[]> => {
const { address, abi, client } = await getContract(true); const p2pContract = getContract(true);
const filterAddedLocks = p2pContract.filters.LockAdded([walletAddress]);
const eventsReleasedLocks = await p2pContract.queryFilter(filterAddedLocks);
return eventsReleasedLocks.sort((a, b) => {
return b.blockNumber - a.blockNumber;
});
};
const listLockTransactionBySellerAddress = async (
sellerAddress: string
): Promise<Event[]> => {
const p2pContract = getContract(true);
const filterAddedLocks = p2pContract.filters.LockAdded();
const eventsReleasedLocks = await p2pContract.queryFilter(filterAddedLocks);
return eventsReleasedLocks.filter((lock) =>
lock.args?.seller
.toHexString()
.substring(3)
.includes(sellerAddress.substring(2).toLowerCase())
);
};
const checkUnreleasedLock = async (
walletAddress: string
): Promise<UnreleasedLock | undefined> => {
const p2pContract = getContract();
const pixData: Pix = {
pixKey: "",
};
const addedLocks = await listLockTransactionByWalletAddress(walletAddress);
const lockStatus = await p2pContract.getLocksStatus(
addedLocks.map((lock) => lock.args?.lockID)
);
const unreleasedLockId = lockStatus[1].findIndex(
(lockStatus: number) => lockStatus == 1
);
if (unreleasedLockId != -1) {
const _lockID = lockStatus[0][unreleasedLockId];
const lock = await p2pContract.mapLocks(_lockID);
const pixTarget = lock.pixTarget;
const amount = formatEther(lock?.amount);
pixData.pixKey = String(Number(pixTarget));
pixData.value = Number(amount);
return {
lockID: _lockID,
pix: pixData,
};
}
};
const getActiveLockAmount = async (walletAddress: string): Promise<number> => {
const p2pContract = getContract();
const lockSeller = await listLockTransactionBySellerAddress(walletAddress); const lockSeller = await listLockTransactionBySellerAddress(walletAddress);
if (!lockSeller.length) return 0; const lockStatus = await p2pContract.getLocksStatus(
lockSeller.map((lock) => lock.args?.lockID)
const lockIds = lockSeller.map((lock: any) => lock.args.lockID);
const [ sortedIDs, status ] = await client.readContract({
address,
abi,
functionName: "getLocksStatus",
args: [lockIds],
});
const mapLocksRequests = status.map((id: LockStatus) =>
client.readContract({
address: address,
abi,
functionName: "mapLocks",
args: [BigInt(id)],
})
); );
const mapLocksResults = await client.multicall({ const activeLockAmount = await lockStatus[1].reduce(
contracts: mapLocksRequests as any, async (sumValue: Promise<number>, currentStatus: number, index: number) => {
}); const currValue = await sumValue;
let valueToSum = 0;
return mapLocksResults.reduce((total: number, lock: any, index: number) => { if (currentStatus == 1) {
if (status[index] === 1) { const lock = await p2pContract.mapLocks(lockStatus[0][index]);
return total + Number(formatEther(lock.amount)); valueToSum = Number(formatEther(lock?.amount));
} }
return total;
}, 0); return currValue + valueToSum;
},
Promise.resolve(0)
);
return activeLockAmount;
};
export {
updateWalletStatus,
listValidDepositTransactionsByWalletAddress,
listAllTransactionByWalletAddress,
listReleaseTransactionByWalletAddress,
checkUnreleasedLock,
getActiveLockAmount,
}; };

View File

@@ -5,12 +5,13 @@ import {
listAllTransactionByWalletAddress, listAllTransactionByWalletAddress,
listValidDepositTransactionsByWalletAddress, listValidDepositTransactionsByWalletAddress,
} from "@/blockchain/wallet"; } from "@/blockchain/wallet";
import CustomButton from "@/components/ui/CustomButton.vue"; import CustomButton from "@/components/CustomButton/CustomButton.vue";
import type { ValidDeposit } from "@/model/ValidDeposit"; import type { ValidDeposit } from "@/model/ValidDeposit";
import type { WalletTransaction } from "@/model/WalletTransaction"; import type { WalletTransaction } from "@/model/WalletTransaction";
import { useUser } from "@/composables/useUser"; import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue"; import ListingComponent from "../ListingComponent/ListingComponent.vue";
// props // props
const props = defineProps<{ const props = defineProps<{
@@ -18,8 +19,8 @@ const props = defineProps<{
isCurrentStep: boolean; isCurrentStep: boolean;
}>(); }>();
const user = useUser(); const etherStore = useEtherStore();
const { walletAddress } = useUser(); const { walletAddress } = storeToRefs(etherStore);
const lastWalletTransactions = ref<WalletTransaction[]>([]); const lastWalletTransactions = ref<WalletTransaction[]>([]);
const depositList = ref<ValidDeposit[]>([]); const depositList = ref<ValidDeposit[]>([]);
@@ -28,7 +29,7 @@ const activeLockAmount = ref<number>(0);
// methods // methods
const getWalletTransactions = async () => { const getWalletTransactions = async () => {
user.setLoadingWalletTransactions(true); etherStore.setLoadingWalletTransactions(true);
if (walletAddress.value) { if (walletAddress.value) {
const walletDeposits = await listValidDepositTransactionsByWalletAddress( const walletDeposits = await listValidDepositTransactionsByWalletAddress(
walletAddress.value walletAddress.value
@@ -47,20 +48,20 @@ const getWalletTransactions = async () => {
lastWalletTransactions.value = allUserTransactions; lastWalletTransactions.value = allUserTransactions;
} }
} }
user.setLoadingWalletTransactions(false); etherStore.setLoadingWalletTransactions(false);
}; };
const callWithdraw = async (amount: string) => { const callWithdraw = async (amount: string) => {
if (amount) { if (amount) {
user.setLoadingWalletTransactions(true); etherStore.setLoadingWalletTransactions(true);
const withdraw = await withdrawDeposit(amount, user.network.value.tokens[user.selectedToken.value].address); const withdraw = await withdrawDeposit(amount);
if (withdraw) { if (withdraw) {
console.log("Saque realizado!"); console.log("Saque realizado!");
await getWalletTransactions(); await getWalletTransactions();
} else { } else {
console.log("Não foi possível realizar o saque!"); console.log("Não foi possível realizar o saque!");
} }
user.setLoadingWalletTransactions(false); etherStore.setLoadingWalletTransactions(false);
} }
}; };
@@ -85,21 +86,19 @@ onMounted(async () => {
para a sua carteira! para a sua carteira!
</span> </span>
</div> </div>
<div class="main-container"> <div class="blur-container">
<div <div
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10" class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
> >
<div> <div>
<p>Tokens recebidos</p> <p>Tokens recebidos</p>
<p class="text-2xl text-gray-900"> <p class="text-2xl text-gray-900">{{ props.tokenAmount }} BRZ</p>
{{ props.tokenAmount }} {{ user.selectedToken }}
</p>
</div> </div>
<div class="my-5"> <div class="my-5">
<p class="text-sm"> <p class="text-sm">
<b>Não encontrou os tokens? </b><br />Clique no botão abaixo para <b>Não encontrou os tokens? </b><br />Clique no botão abaixo para
<br /> <br />
cadastrar o {{ user.selectedToken }} em sua carteira. cadastrar o BRZ em sua carteira.
</p> </p>
</div> </div>
<CustomButton :text="'Cadastrar token'" @buttonClicked="() => {}" /> <CustomButton :text="'Cadastrar token'" @buttonClicked="() => {}" />
@@ -144,13 +143,42 @@ p {
.text { .text {
@apply text-white text-center; @apply text-white text-center;
} }
.blur-container-row {
@apply flex flex-row justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-8 w-1/3;
}
.blur-container {
@apply flex w-full max-w-xs md:max-w-lg flex-col justify-center items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10;
}
.last-release-info { .last-release-info {
@apply font-medium text-base text-gray-900; @apply font-medium text-base text-gray-900;
} }
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
} }
.lg-view {
display: inline-block;
}
.sm-view {
display: none;
}
@media screen and (max-width: 500px) {
.lg-view {
display: none;
}
.sm-view {
display: inline-block;
}
}
</style> </style>

View File

@@ -0,0 +1,37 @@
import { mount } from "@vue/test-utils";
import BuyConfirmedComponent from "../BuyConfirmedComponent.vue";
import { createPinia, setActivePinia } from "pinia";
describe("BuyConfirmedComponent.vue", async () => {
beforeEach(() => {
setActivePinia(createPinia());
});
const wrapper = mount(BuyConfirmedComponent, {
props: {
tokenAmount: 1,
isCurrentStep: false,
},
});
// test("Test component Header Text", () => {
// expect(wrapper.html()).toContain("Os tokens já foram transferidos");
// expect(wrapper.html()).toContain("para a sua carteira!");
// });
// test("Test component Container Text", () => {
// expect(wrapper.html()).toContain("Tokens recebidos");
// expect(wrapper.html()).toContain("BRZ");
// expect(wrapper.html()).toContain("Não encontrou os tokens?");
// expect(wrapper.html()).toContain("Clique no botão abaixo para");
// expect(wrapper.html()).toContain("cadastrar o BRZ em sua carteira.");
// });
test("Test makeAnotherTransactionEmit", async () => {
wrapper.vm.$emit("makeAnotherTransaction");
await wrapper.vm.$nextTick();
expect(wrapper.emitted("makeAnotherTransaction")).toBeTruthy();
});
});

View File

@@ -1,347 +0,0 @@
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { useUser } from "@/composables/useUser";
import SpinnerComponent from "@/components/ui/SpinnerComponent.vue";
import CustomButton from "@/components/ui/CustomButton.vue";
import { debounce } from "@/utils/debounce";
import { verifyNetworkLiquidity } from "@/utils/networkLiquidity";
import type { ValidDeposit } from "@/model/ValidDeposit";
import { decimalCount } from "@/utils/decimalCount";
import { getTokenImage, getNetworkImage } from "@/utils/imagesPath";
import { onClickOutside } from "@vueuse/core";
import { Networks } from "@/config/networks";
import { TokenEnum } from "@/model/NetworkEnum";
// Store reference
const user = useUser();
const selectTokenToggle = ref<boolean>(false);
const {
walletAddress,
network,
selectedToken,
depositsValidList,
loadingNetworkLiquidity,
} = user;
// html references
const tokenDropdownRef = ref<any>(null);
// Reactive state
const tokenValue = ref<number>(0);
const enableConfirmButton = ref<boolean>(false);
const hasLiquidity = ref<boolean>(true);
const validDecimals = ref<boolean>(true);
const identification = ref<string>("");
const selectedDeposits = ref<ValidDeposit[]>();
import ChevronDown from "@/assets/chevronDown.svg";
import { useOnboard } from "@web3-onboard/vue";
import { getParticipantID } from "@/blockchain/events";
// Emits
const emit = defineEmits(["tokenBuy"]);
// Blockchain methods
const connectAccount = async (): Promise<void> => {
const { connectWallet } = useOnboard();
await connectWallet();
};
const emitConfirmButton = async (): Promise<void> => {
const deposit = selectedDeposits.value?.find(
(d) => d.network === network.value
);
if (!deposit) return;
deposit.participantID = await getParticipantID(deposit.seller, deposit.token);
emit("tokenBuy", deposit, tokenValue.value);
};
// Debounce methods
const handleInputEvent = (event: any): void => {
const { value } = event.target;
tokenValue.value = Number(value);
if (decimalCount(String(tokenValue.value)) > 2) {
validDecimals.value = false;
enableConfirmButton.value = false;
return;
}
validDecimals.value = true;
verifyLiquidity();
};
const openTokenSelection = (): void => {
selectTokenToggle.value = true;
};
onClickOutside(tokenDropdownRef, () => {
selectTokenToggle.value = false;
});
const handleSelectedToken = (token: TokenEnum): void => {
user.setSelectedToken(token);
selectTokenToggle.value = false;
};
// Verify if there is a valid deposit to buy
const verifyLiquidity = (): void => {
enableConfirmButton.value = false;
if (!walletAddress.value)
return;
const selDeposits = verifyNetworkLiquidity(
tokenValue.value,
walletAddress.value,
depositsValidList.value
);
selectedDeposits.value = selDeposits;
hasLiquidity.value = !!selDeposits.find(
(d) => d.network === network.value
);
enableOrDisableConfirmButton();
};
const enableOrDisableConfirmButton = (): void => {
if (!selectedDeposits.value) {
enableConfirmButton.value = false;
return;
}
if (!selectedDeposits.value.find((d) => d.network === network.value)) {
enableConfirmButton.value = false;
return;
}
enableConfirmButton.value = true;
};
watch(network, (): void => {
verifyLiquidity();
enableOrDisableConfirmButton();
});
watch(walletAddress, (): void => {
verifyLiquidity();
});
const availableNetworks = computed(() => {
if (!selectedDeposits.value) return [];
return Object.values(Networks).filter((network) =>
selectedDeposits.value?.some((d) => d.network.id === network.id)
);
});
// Add form submission handler
const handleSubmit = async (e: Event): Promise<void> => {
e.preventDefault();
if (walletAddress.value) {
await emitConfirmButton();
} else {
await connectAccount();
}
};
</script>
<template>
<div class="page">
<div class="text-container">
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>
Adquira cripto com apenas um Pix</span
>
<span class="text font-medium sm:text-base text-sm max-w-[28rem]"
>Digite um valor, confira a oferta, conecte sua carteira e receba os
tokens após realizar o Pix</span
>
</div>
<form class="main-container" @submit="handleSubmit">
<div class="backdrop-blur -z-10 w-full h-full"></div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
>
<div class="flex justify-between sm:w-full items-center">
<input
type="number"
name="tokenAmount"
class="border-none outline-none text-lg text-gray-900 sm:flex-1 max-w-[60%]"
v-bind:class="{
'font-semibold': tokenValue != undefined,
'text-xl': tokenValue != undefined,
}"
@input="debounce(handleInputEvent, 500)($event)"
placeholder="0"
step=".01"
required
/>
<div class="relative overflow-visible ml-auto sm:ml-0">
<button
ref="tokenDropdownRef"
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
@click="openTokenSelection()"
>
<img
alt="Token image"
class="sm:w-fit w-4"
:src="getTokenImage(selectedToken)"
/>
<span
class="text-gray-900 sm:text-lg text-md font-medium"
id="token"
>{{ selectedToken }}</span
>
<ChevronDown
class="pr-4 sm:pr-0 transition-all duration-500 ease-in-out invert"
:class="{ 'scale-y-[-1]': selectTokenToggle }"
alt="Chevron Down"
/>
</button>
<transition name="dropdown">
<div
v-if="selectTokenToggle"
class="mt-2 text-gray-900 absolute right-0 z-50 w-full min-w-max"
>
<div
class="bg-white rounded-xl z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
>
<div
v-for="token in TokenEnum"
:key="token"
class="flex menu-button gap-2 px-4 cursor-pointer hover:bg-gray-300 transition-colors"
@click="handleSelectedToken(token)"
>
<img
:alt="token + ' logo'"
width="20"
height="20"
:src="getTokenImage(token)"
/>
<span
class="text-gray-900 py-4 text-end font-semibold text-sm"
>
{{ token }}
</span>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
</div>
</div>
</transition>
</div>
</div>
<div class="custom-divide py-2 mb-2"></div>
<div class="flex justify-between" v-if="!loadingNetworkLiquidity">
<p class="text-gray-500 font-normal text-sm w-auto">
~ R$ {{ tokenValue.toFixed(2) }}
</p>
<div class="flex gap-2">
<img
v-for="network in availableNetworks"
:key="network.id"
:alt="`${network.name} image`"
:src="getNetworkImage(network.name)"
width="24"
height="24"
/>
</div>
</div>
<div
class="flex justify-center items-center"
v-if="loadingNetworkLiquidity"
>
<span class="text-gray-900 font-normal text-sm mr-2"
>Carregando liquidez das redes.</span
>
<SpinnerComponent width="4" height="4"></SpinnerComponent>
</div>
<div
class="flex justify-center"
v-if="!validDecimals && !loadingNetworkLiquidity"
>
<span class="text-red-500 font-normal text-sm"
>Por favor utilize no máximo 2 casas decimais</span
>
</div>
<div
class="flex justify-center"
v-else-if="
!hasLiquidity && !loadingNetworkLiquidity && tokenValue > 0
"
>
<span class="text-red-500 font-normal text-sm"
>Atualmente não há liquidez nas rede selecionada para sua
demanda</span
>
</div>
</div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<input
type="text"
v-model="identification"
maxlength="14"
:pattern="'^\\d{11}$|^\\d{14}$'"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
placeholder="Digite seu CPF ou CNPJ (somente números)"
required
/>
</div>
<!-- Action buttons -->
<CustomButton
v-if="walletAddress"
type="submit"
text="Confirmar Oferta"
/>
<CustomButton
v-else
text="Conectar carteira"
@buttonClicked="connectAccount()"
/>
</form>
</div>
</template>
<style scoped>
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
.custom-button {
@apply w-full py-3 px-6 rounded-lg font-semibold text-white bg-indigo-600
hover:bg-indigo-700 disabled:bg-gray-400 disabled:cursor-not-allowed
transition-colors duration-200;
}
</style>

View File

@@ -1,294 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import CustomButton from "@/components/ui/CustomButton.vue";
import CustomModal from "@/components/ui/CustomModal.vue";
import SpinnerComponent from "@/components/ui/SpinnerComponent.vue";
import { createSolicitation, getSolicitation, type Offer } from "@/utils/bbPay";
import { getParticipantID } from "@/blockchain/events";
import { getUnreleasedLockById } from "@/blockchain/events";
import QRCode from "qrcode";
// Props
interface Props {
lockID: string;
}
const props = defineProps<Props>();
const qrCode = ref<string>("");
const qrCodeSvg = ref<string>("");
const showWarnModal = ref<boolean>(true);
const pixTimestamp = ref<string>("");
const releaseSignature = ref<string>("");
const solicitationData = ref<any>(null);
const pollingInterval = ref<NodeJS.Timeout | null>(null);
const copyFeedback = ref<boolean>(false);
const copyFeedbackTimeout = ref<NodeJS.Timeout | null>(null);
// Function to generate QR code SVG
const generateQrCodeSvg = async (text: string) => {
try {
const svgString = await QRCode.toString(text, {
type: "svg",
width: 192, // 48 * 4 for better quality
margin: 2,
color: {
dark: "#000000",
light: "#FFFFFF",
},
});
qrCodeSvg.value = svgString;
} catch (error) {
console.error("Error generating QR code SVG:", error);
}
};
// Emits
const emit = defineEmits(["pixValidated"]);
// Function to check solicitation status
const checkSolicitationStatus = async () => {
if (!solicitationData.value?.numeroSolicitacao) {
return;
}
try {
const response = await getSolicitation(
solicitationData.value.numeroSolicitacao
);
if (response.signature) {
pixTimestamp.value = response.pixTimestamp;
releaseSignature.value = response.signature;
// Stop polling when payment is confirmed
if (pollingInterval.value) {
clearInterval(pollingInterval.value);
pollingInterval.value = null;
}
}
} catch (error) {
console.error("Error checking solicitation status:", error);
}
};
// Function to start polling
const startPolling = () => {
// Clear any existing interval
if (pollingInterval.value) {
clearInterval(pollingInterval.value);
}
// Start new polling interval (10 seconds)
pollingInterval.value = setInterval(checkSolicitationStatus, 10000);
};
const copyToClipboard = async () => {
if (!qrCode.value) {
return;
}
try {
await navigator.clipboard.writeText(qrCode.value);
if (copyFeedbackTimeout.value) {
clearTimeout(copyFeedbackTimeout.value);
}
copyFeedback.value = true;
copyFeedbackTimeout.value = setTimeout(() => {
copyFeedback.value = false;
}, 2000);
} catch (error) {
console.error("Error copying to clipboard:", error);
}
};
onMounted(async () => {
try {
const { tokenAddress, sellerAddress, amount } = await getUnreleasedLockById(
BigInt(props.lockID)
);
const participantId = await getParticipantID(
sellerAddress,
tokenAddress
);
const offer: Offer = {
amount,
sellerId: participantId,
};
const response = await createSolicitation(offer);
solicitationData.value = response;
// Update qrCode if the response contains QR code data
if (response?.informacoesPIX?.textoQrCode) {
qrCode.value = response.informacoesPIX?.textoQrCode;
// Generate SVG QR code
await generateQrCodeSvg(qrCode.value);
}
// Start polling for solicitation status
startPolling();
} catch (error) {
console.error("Error creating solicitation:", error);
}
});
// Clean up interval on component unmount
onUnmounted(() => {
if (pollingInterval.value) {
clearInterval(pollingInterval.value);
pollingInterval.value = null;
}
if (copyFeedbackTimeout.value) {
clearTimeout(copyFeedbackTimeout.value);
copyFeedbackTimeout.value = null;
}
});
</script>
<template>
<div class="page">
<div class="text-container">
<span
class="text font-extrabold lg:text-2xl text-xl sm:max-w-[30rem] max-w-[24rem]"
>
Utilize o QR Code ou copie o código para realizar o Pix
</span>
<span class="text font-medium lg:text-md text-sm max-w-[28rem]">
Após realizar o Pix no banco de sua preferência, clique no botão abaixo
para liberação dos tokens.
</span>
</div>
<div class="main-container max-w-md text-black">
<div
class="flex-col items-center justify-center flex w-full bg-white sm:p-8 p-4 rounded-lg break-normal"
>
<div
v-if="qrCodeSvg"
v-html="qrCodeSvg"
class="w-48 h-48 flex items-center justify-center"
></div>
<div
v-else
class="w-48 h-48 flex items-center justify-center rounded-lg"
>
<SpinnerComponent width="8" height="8"></SpinnerComponent>
</div>
<span class="text-center font-bold">Código pix</span>
<div class="break-words w-4/5">
<span class="text-center text-xs">
{{ qrCode }}
</span>
</div>
<div class="flex flex-col items-center gap-1">
<img
alt="Copy PIX code"
src="@/assets/copyPix.svg?url"
width="16"
height="16"
class="pt-2 cursor-pointer hover:opacity-70 transition-opacity"
@click="copyToClipboard"
/>
<transition name="fade">
<span
v-if="copyFeedback"
class="text-xs text-emerald-500 font-semibold"
>
Código copiado!
</span>
</transition>
</div>
</div>
<CustomButton
:is-disabled="releaseSignature === ''"
:text="
releaseSignature ? 'Enviar para a rede' : 'Validando pagamento...'
"
@button-clicked="
emit('pixValidated', { pixTimestamp, signature: releaseSignature })
"
/>
</div>
<CustomModal
v-if="showWarnModal"
@close-modal="showWarnModal = false"
:isRedirectModal="false"
/>
</div>
</template>
<style scoped>
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
::placeholder {
/* Most modern browsers support this now. */
color: #9ca3af;
}
h4 {
color: #080808;
font-size: 14px;
}
h2 {
color: #080808;
}
.form-input {
@apply rounded-lg border border-gray-200 p-2 text-black;
}
.form-label {
@apply font-semibold tracking-wide text-emerald-50;
}
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
.blur-container {
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-6 max-w-screen-sm;
}
input[type="number"] {
appearance: textfield;
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
/* Fade transition for copy feedback */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -38,9 +38,9 @@ switch (props.type) {
<div <div
class="modal-overlay sm:h-12 h-full inset-0 absolute backdrop-blur-sm sm:backdrop-blur-none" class="modal-overlay sm:h-12 h-full inset-0 absolute backdrop-blur-sm sm:backdrop-blur-none"
> >
<div class="modal px-12 text-center sm:flex justify-between hidden"> <div class="modal px-12 pl-72 text-center sm:flex justify-between hidden">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-black tracking-tighter leading-tight py-2"> <p class="text-black tracking-tighter leading-tight my-2">
{{ alertText }} {{ alertText }}
</p> </p>
<button v-if="props.type === 'redirect'" @click="$emit('go-to-lock')"> <button v-if="props.type === 'redirect'" @click="$emit('go-to-lock')">
@@ -110,6 +110,7 @@ switch (props.type) {
border-radius: 10px; border-radius: 10px;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
padding-left: v-bind(alertPaddingLeft);
} }
.modal-mobile { .modal-mobile {

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
const props = defineProps({
text: String,
isDisabled: Boolean,
});
const emit = defineEmits(["buttonClicked"]);
</script>
<template>
<button
type="button"
class="button"
@click="emit('buttonClicked')"
v-bind:class="{ 'opacity-70': props.isDisabled }"
:disabled="props.isDisabled ? props.isDisabled : false"
>
{{ props.text }}
</button>
</template>
<style scoped>
.button {
@apply rounded-lg w-full text-base font-semibold text-gray-900 p-4 bg-amber-400;
}
</style>

View File

@@ -0,0 +1,27 @@
import { mount } from "@vue/test-utils";
import CustomButton from "../CustomButton.vue";
describe("CustomButton.vue", () => {
test("Test button content", () => {
const wrapper = mount(CustomButton, {
props: {
text: "Testing",
},
});
expect(wrapper.html()).toContain("Testing");
});
test("Test if disabled props works", () => {
const wrapper = mount(CustomButton, {
props: {
isDisabled: true,
},
});
//@ts-ignore
const button = wrapper.find(".button") as HTMLButtonElement;
//@ts-ignore
expect(button.element.disabled).toBe(true);
});
});

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps({
isRedirectModal: Boolean,
});
const modalColor = ref<string>("white");
const modalHeight = ref<string>("250px");
const pFontSize = ref<string>("16px");
if (props.isRedirectModal) {
modalColor.value = "rgba(251, 191, 36, 1)";
modalHeight.value = "150px";
pFontSize.value = "20px";
}
</script>
<template>
<div
class="modal-overlay inset-0 fixed justify-center backdrop-blur-sm sm:backdrop-blur-none"
v-if="!isRedirectModal"
>
<div class="modal px-5 text-center">
<p
class="text-black tracking-tighter leading-tight my-6 mx-2 text-justify"
>
<strong>ATENÇÃO!</strong>
A transação será processada após inserir o código de autenticação.
Caso contrário não conseguiremos comprovar o seu depósito e não será
possível transferir os tokens para sua carteira.
</p>
<button
@click="$emit('close-modal')"
class="border-2 border-solid border-amber-400 mt-2"
>
Entendi
</button>
</div>
</div>
<div
class="modal-overlay inset-0 fixed justify-center backdrop-blur-sm"
v-if="isRedirectModal"
>
<div class="modal px-5 text-center">
<p
class="text-black text-lg tracking-tighter leading-tight my-6 mx-2 text-justify font-semibold"
>
Retomar a última compra?
</p>
<div class="flex justify-around items-center px-2">
<button
@click="$emit('close-modal')"
class="border-2 border-solid border-white-400 mt-2 font-semibold"
>
Não
</button>
<button
@click="$emit('go-to-lock')"
class="border-2 border-solid border-white-400 mt-2 font-semibold"
>
Sim
</button>
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
display: flex !important;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
.modal {
background-color: v-bind(modalColor);
height: v-bind(modalHeight);
width: 300px;
border-radius: 10px;
}
.close {
cursor: pointer;
}
.close-img {
width: 25px;
}
.check {
width: 150px;
}
h6 {
font-weight: 500;
font-size: 28px;
margin: 20px 0;
}
p {
font-size: v-bind(pFontSize);
}
button {
width: 100px;
height: 40px;
color: black;
font-size: 14px;
border-radius: 10px;
}
</style>

View File

@@ -0,0 +1,27 @@
import { mount } from "@vue/test-utils";
import CustomModal from "../CustomModal.vue";
describe("CustomModal test", () => {
test("Test custom modal when receive is redirect modal props as false", () => {
const wrapper = mount(CustomModal, {
props: {
isRedirectModal: false,
},
});
expect(wrapper.html()).toContain("ATENÇÃO!");
expect(wrapper.html()).toContain("Entendi");
});
test("Test custom modal when receive is redirect modal props as true", () => {
const wrapper = mount(CustomModal, {
props: {
isRedirectModal: true,
},
});
expect(wrapper.html()).toContain("Retomar a última compra?");
expect(wrapper.html()).toContain("Não");
expect(wrapper.html()).toContain("Sim");
});
});

View File

@@ -1,73 +0,0 @@
<script setup lang="ts">
interface Props {
title: string;
value: string;
change?: string;
changeType?: 'positive' | 'negative' | 'neutral';
icon?: string;
loading?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
changeType: 'neutral',
loading: false
});
</script>
<template>
<div class="analytics-card">
<div class="analytics-content">
<div v-if="loading" class="analytics-value">
<div class="animate-pulse bg-gray-300 h-8 w-16 rounded"></div>
</div>
<div v-else class="analytics-value">{{ value }}</div>
<div class="analytics-title">{{ title }}</div>
<div v-if="change && !loading" class="analytics-change" :class="`change-${changeType}`">
{{ change }}
</div>
</div>
<div v-if="icon && !loading" class="analytics-icon">
<img :src="icon" :alt="`${title} icon`" class="w-8 h-8" />
</div>
</div>
</template>
<style scoped>
.analytics-card {
@apply bg-white rounded-lg border border-gray-200 p-6 flex items-center justify-between shadow-lg;
}
.analytics-content {
@apply flex flex-col;
}
.analytics-value {
@apply text-2xl font-bold text-amber-400 mb-1 break-words overflow-hidden;
word-break: break-all;
max-width: 100%;
}
.analytics-title {
@apply text-sm text-gray-900 mb-1;
}
.analytics-change {
@apply text-xs font-medium;
}
.change-positive {
@apply text-green-600;
}
.change-negative {
@apply text-red-600;
}
.change-neutral {
@apply text-gray-600;
}
.analytics-icon {
@apply flex-shrink-0;
}
</style>

View File

@@ -1,279 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
interface Transaction {
id: string;
type: 'deposit' | 'lock' | 'release' | 'return';
timestamp: string;
seller?: string;
buyer?: string | null;
amount: string;
token: string;
blockNumber: string;
transactionHash: string;
}
interface Props {
transactions: Transaction[];
networkExplorerUrl: string;
}
const props = defineProps<Props>();
const copyFeedback = ref<{ [key: string]: boolean }>({});
const copyFeedbackTimeout = ref<{ [key: string]: NodeJS.Timeout | null }>({});
const getTransactionTypeInfo = (type: string) => {
const typeMap = {
deposit: { label: 'Depósito', status: 'completed' as const },
lock: { label: 'Bloqueio', status: 'open' as const },
release: { label: 'Liberação', status: 'completed' as const },
return: { label: 'Retorno', status: 'expired' as const }
};
return typeMap[type as keyof typeof typeMap] || { label: type, status: 'pending' as const };
};
const getTransactionTypeColor = (type: string) => {
const colorMap = {
deposit: 'text-emerald-600',
lock: 'text-amber-600',
release: 'text-emerald-600',
return: 'text-gray-600'
};
return colorMap[type as keyof typeof colorMap] || 'text-gray-600';
};
const formatAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
const formatAmount = (amount: string, decimals: number = 18): string => {
const num = parseFloat(amount) / Math.pow(10, decimals);
return num.toString();
};
const getExplorerUrl = (txHash: string) => {
return `${props.networkExplorerUrl}/tx/${txHash}`;
};
const copyToClipboard = async (address: string, key: string) => {
if (!address) {
return;
}
try {
await navigator.clipboard.writeText(address);
if (copyFeedbackTimeout.value[key]) {
clearTimeout(copyFeedbackTimeout.value[key]!);
}
copyFeedback.value[key] = true;
copyFeedbackTimeout.value[key] = setTimeout(() => {
copyFeedback.value[key] = false;
}, 2000);
} catch (error) {
console.error('Error copying to clipboard:', error);
}
};
</script>
<template>
<div>
<div class="hidden lg:block overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4 text-gray-700 font-medium">Horário</th>
<th class="text-left py-3 px-4 text-gray-700 font-medium">Tipo</th>
<th class="text-left py-3 px-4 text-gray-700 font-medium">Participantes</th>
<th class="text-left py-3 px-4 text-gray-700 font-medium">Valor</th>
<th class="text-left py-3 px-4 text-gray-700 font-medium">Bloco</th>
<th class="text-left py-3 px-4 text-gray-700 font-medium">Ações</th>
</tr>
</thead>
<tbody>
<tr
v-for="transaction in transactions"
:key="transaction.id"
class="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<td class="py-4 px-4">
<div class="text-sm text-gray-600">{{ transaction.timestamp }}</div>
</td>
<td class="py-4 px-4">
<span
:class="getTransactionTypeColor(transaction.type)"
class="text-sm font-medium"
>
{{ getTransactionTypeInfo(transaction.type).label }}
</span>
</td>
<td class="py-4 px-4">
<div class="space-y-1">
<div v-if="transaction.seller" class="text-sm">
<span class="text-gray-600">Vendedor: </span>
<div class="relative inline-block">
<span
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
@click="copyToClipboard(transaction.seller, `seller-${transaction.id}`)"
title="Copiar"
>
{{ formatAddress(transaction.seller) }}
</span>
<transition name="fade">
<span
v-if="copyFeedback[`seller-${transaction.id}`]"
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
>
Copiado!
</span>
</transition>
</div>
</div>
<div v-if="transaction.buyer" class="text-sm">
<span class="text-gray-600">Comprador: </span>
<div class="relative inline-block">
<span
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
@click="copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)"
title="Copiar"
>
{{ formatAddress(transaction.buyer) }}
</span>
<transition name="fade">
<span
v-if="copyFeedback[`buyer-${transaction.id}`]"
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
>
Copiado!
</span>
</transition>
</div>
</div>
</div>
</td>
<td class="py-4 px-4">
<div class="text-sm font-semibold text-emerald-600">
{{ formatAmount(transaction.amount, 18) }} BRZ
</div>
</td>
<td class="py-4 px-4">
<div class="text-sm text-gray-600 font-mono">
#{{ transaction.blockNumber }}
</div>
</td>
<td class="py-4 px-4">
<a
:href="getExplorerUrl(transaction.transactionHash)"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors"
>
Explorador
</a>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Mobile Cards -->
<div class="lg:hidden space-y-4">
<div
v-for="transaction in transactions"
:key="transaction.id"
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
>
<div class="flex items-center justify-between mb-3">
<span
:class="getTransactionTypeColor(transaction.type)"
class="text-sm font-medium"
>
{{ getTransactionTypeInfo(transaction.type).label }}
</span>
<div class="text-sm text-gray-600">{{ transaction.timestamp }}</div>
</div>
<div class="space-y-2 mb-4">
<div v-if="transaction.seller" class="text-sm">
<span class="text-gray-600">Vendedor: </span>
<div class="relative inline-block">
<span
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
@click="copyToClipboard(transaction.seller, `seller-${transaction.id}`)"
title="Copiar"
>
{{ formatAddress(transaction.seller) }}
</span>
<transition name="fade">
<span
v-if="copyFeedback[`seller-${transaction.id}`]"
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
>
Copiado!
</span>
</transition>
</div>
</div>
<div v-if="transaction.buyer" class="text-sm">
<span class="text-gray-600">Comprador: </span>
<div class="relative inline-block">
<span
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
@click="copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)"
title="Copiar"
>
{{ formatAddress(transaction.buyer) }}
</span>
<transition name="fade">
<span
v-if="copyFeedback[`buyer-${transaction.id}`]"
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
>
Copiado!
</span>
</transition>
</div>
</div>
<div class="text-sm">
<span class="text-gray-600">Valor: </span>
<span class="font-semibold text-emerald-600">{{ formatAmount(transaction.amount, 18) }} BRZ</span>
</div>
<div class="text-sm">
<span class="text-gray-600">Bloco: </span>
<span class="text-gray-900 font-mono">#{{ transaction.blockNumber }}</span>
</div>
</div>
<a
:href="getExplorerUrl(transaction.transactionHash)"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors"
>
Ver no Explorador
</a>
</div>
</div>
<!-- Empty State -->
<div v-if="transactions.length === 0" class="text-center py-12">
<div class="text-gray-500 text-lg mb-2">📭</div>
<p class="text-gray-600">Nenhuma transação encontrada</p>
</div>
</div>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,215 +0,0 @@
<script setup lang="ts">
import type { ValidDeposit } from "@/model/ValidDeposit";
import { ref, watch, onMounted, computed } from "vue";
import { debounce } from "@/utils/debounce";
import { decimalCount } from "@/utils/decimalCount";
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
import IconButton from "../ui/IconButton.vue";
import withdrawIcon from "@/assets/withdraw.svg?url";
const props = defineProps<{
validDeposits: ValidDeposit[];
activeLockAmount: number;
selectedToken: string;
}>();
const emit = defineEmits<{
withdraw: [amount: string];
}>();
const withdrawAmount = ref<string>("");
const isCollapsibleOpen = ref<boolean>(false);
const validDecimals = ref<boolean>(true);
const validWithdrawAmount = ref<boolean>(true);
const enableConfirmButton = ref<boolean>(false);
const showInfoTooltip = ref<boolean>(false);
const floatingArrow = ref(null);
const reference = ref<HTMLElement | null>(null);
const floating = ref<HTMLElement | null>(null);
const infoText = ref<HTMLElement | null>(null);
const remaining = computed(() => {
if (props.validDeposits.length > 0) {
const deposit = props.validDeposits[0];
return deposit ? deposit.remaining : 0;
}
return 0;
});
const handleInputEvent = (event: any): void => {
const { value } = event.target;
if (decimalCount(String(value)) > 2) {
validDecimals.value = false;
enableConfirmButton.value = false;
return;
}
validDecimals.value = true;
if (value > remaining.value) {
validWithdrawAmount.value = false;
enableConfirmButton.value = false;
return;
}
validWithdrawAmount.value = true;
enableConfirmButton.value = true;
};
const callWithdraw = () => {
if (enableConfirmButton.value && withdrawAmount.value) {
emit("withdraw", withdrawAmount.value);
// Reset form after withdraw
withdrawAmount.value = "";
isCollapsibleOpen.value = false;
}
};
const openWithdrawForm = () => {
isCollapsibleOpen.value = true;
};
const cancelWithdraw = () => {
isCollapsibleOpen.value = false;
withdrawAmount.value = "";
validDecimals.value = true;
validWithdrawAmount.value = true;
enableConfirmButton.value = false;
};
onMounted(() => {
useFloating(reference, floating, {
placement: "right",
middleware: [
offset(10),
flip(),
shift(),
arrow({ element: floatingArrow }),
],
});
});
</script>
<template>
<div class="w-full bg-white p-4 sm:p-6 rounded-lg">
<div class="flex justify-between items-center">
<div>
<p class="text-sm leading-5 font-medium text-gray-600">
Saldo disponível
</p>
<p class="text-xl leading-7 font-semibold text-gray-900">
{{ remaining }} {{ selectedToken }}
</p>
<div class="flex gap-2 w-32 sm:w-56" v-if="activeLockAmount != 0">
<span class="text-xs font-normal text-gray-400" ref="infoText">
{{ `com ${activeLockAmount.toFixed(2)} ${selectedToken} em lock` }}
</span>
<div
class="absolute mt-[2px] md-view"
:style="{ left: `${(infoText?.clientWidth ?? 108) + 4}px` }"
>
<img
alt="info image"
src="@/assets/info.svg?url"
aria-describedby="tooltip"
ref="reference"
@mouseover="showInfoTooltip = true"
@mouseout="showInfoTooltip = false"
/>
<div
role="tooltip"
ref="floating"
class="w-56 z-50 tooltip md-view"
v-if="showInfoTooltip"
>
Valor "em lock" significa que a quantia está aguardando
confirmação de compra e estará disponível para saque caso a
transação expire.
</div>
</div>
</div>
</div>
<div v-show="!isCollapsibleOpen" class="flex justify-end items-center">
<IconButton
text="Sacar"
:icon="withdrawIcon"
variant="outline"
size="md"
:full-width="false"
@click="openWithdrawForm"
/>
</div>
</div>
<div class="pt-5">
<div v-show="isCollapsibleOpen" class="py-2 w-100">
<p class="text-sm leading-5 font-medium">Valor do saque</p>
<input
type="number"
@input="debounce(handleInputEvent, 500)($event)"
placeholder="0"
class="text-2xl text-gray-900 w-full outline-none"
v-model="withdrawAmount"
/>
</div>
<div class="flex justify-center" v-if="!validDecimals">
<span class="text-red-500 font-normal text-sm">
Por favor utilize no máximo 2 casas decimais
</span>
</div>
<div class="flex justify-center" v-else-if="!validWithdrawAmount">
<span class="text-red-500 font-normal text-sm">
Saldo insuficiente
</span>
</div>
<hr v-show="isCollapsibleOpen" class="pb-3" />
<div
v-show="isCollapsibleOpen"
class="flex justify-between items-center"
>
<h1
@click="cancelWithdraw"
class="text-black font-medium cursor-pointer hover:text-gray-600 transition-colors"
>
Cancelar
</h1>
<IconButton
text="Sacar"
:icon="withdrawIcon"
variant="outline"
size="md"
:full-width="false"
:disabled="!enableConfirmButton"
@click="callWithdraw"
/>
</div>
</div>
</div>
</template>
<style scoped>
p {
@apply text-gray-900;
}
.tooltip {
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
}
input[type="number"] {
appearance: textfield;
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
@media screen and (max-width: 640px) {
.md-view {
display: none;
}
}
</style>

View File

@@ -1,13 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { withdrawDeposit } from "@/blockchain/buyerMethods";
import { NetworkEnum } from "@/model/NetworkEnum";
import type { ValidDeposit } from "@/model/ValidDeposit"; import type { ValidDeposit } from "@/model/ValidDeposit";
import type { WalletTransaction } from "@/model/WalletTransaction"; import type { WalletTransaction } from "@/model/WalletTransaction";
import { useUser } from "@/composables/useUser"; import { useEtherStore } from "@/store/ether";
import { ref, watch } from "vue"; import { storeToRefs } from "pinia";
import SpinnerComponent from "../ui/SpinnerComponent.vue"; import { ref, watch, onMounted } from "vue";
import BalanceCard from "./BalanceCard.vue"; import SpinnerComponent from "../SpinnerComponent.vue";
import TransactionCard from "./TransactionCard.vue"; import { decimalCount } from "@/utils/decimalCount";
import { debounce } from "@/utils/debounce";
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
const user = useUser(); const etherStore = useEtherStore();
// props // props
const props = defineProps<{ const props = defineProps<{
@@ -18,12 +22,82 @@ const props = defineProps<{
const emit = defineEmits(["depositWithdrawn"]); const emit = defineEmits(["depositWithdrawn"]);
const { loadingWalletTransactions } = user; const { loadingWalletTransactions } = storeToRefs(etherStore);
const remaining = ref<number>(0.0);
const itemsToShow = ref<WalletTransaction[]>([]); const itemsToShow = ref<WalletTransaction[]>([]);
const withdrawAmount = ref<string>("");
const withdrawButtonOpacity = ref<number>(0.6);
const withdrawButtonCursor = ref<string>("not-allowed");
const isCollapsibleOpen = ref<boolean>(false);
const validDecimals = ref<boolean>(true);
const validWithdrawAmount = ref<boolean>(true);
const enableConfirmButton = ref<boolean>(false);
const showInfoTooltip = ref<boolean>(false);
const floatingArrow = ref(null);
const callWithdraw = (amount: string) => { const reference = ref<HTMLElement | null>(null);
emit("depositWithdrawn", amount); const floating = ref<HTMLElement | null>(null);
const infoText = ref<HTMLElement | null>(null);
// Debounce methods
const handleInputEvent = (event: any): void => {
const { value } = event.target;
if (decimalCount(String(value)) > 2) {
validDecimals.value = false;
enableConfirmButton.value = false;
return;
}
validDecimals.value = true;
if (value > remaining.value) {
validWithdrawAmount.value = false;
enableConfirmButton.value = false;
return;
}
validWithdrawAmount.value = true;
enableConfirmButton.value = true;
};
const callWithdraw = () => {
emit("depositWithdrawn", withdrawAmount.value);
};
watch(enableConfirmButton, (): void => {
if (!enableConfirmButton.value) {
withdrawButtonOpacity.value = 0.7;
withdrawButtonCursor.value = "not-allowed";
} else {
withdrawButtonOpacity.value = 1;
withdrawButtonCursor.value = "pointer";
}
});
watch(withdrawAmount, (): void => {
if (!withdrawAmount.value || !enableConfirmButton.value) {
withdrawButtonOpacity.value = 0.7;
withdrawButtonCursor.value = "not-allowed";
} else {
withdrawButtonOpacity.value = 1;
withdrawButtonCursor.value = "pointer";
}
});
const getRemaining = (): number => {
if (props.validDeposits instanceof Array) {
// Here we are getting only the first element of the list because
// in this release only the BRL token is being used.
const deposit = props.validDeposits[0];
remaining.value = deposit ? deposit.remaining : 0;
return deposit ? deposit.remaining : 0;
}
return 0;
};
const getExplorer = (): string => {
return etherStore.networkName == NetworkEnum.ethereum
? "Etherscan"
: "Polygonscan";
}; };
const showInitialItems = (): void => { const showInitialItems = (): void => {
@@ -31,7 +105,10 @@ const showInitialItems = (): void => {
}; };
const openEtherscanUrl = (transactionHash: string): void => { const openEtherscanUrl = (transactionHash: string): void => {
const networkUrl = user.network.value.blockExplorers?.default.url; const networkUrl =
etherStore.networkName == NetworkEnum.ethereum
? "sepolia.etherscan.io"
: "mumbai.polygonscan.com";
const url = `https://${networkUrl}/tx/${transactionHash}`; const url = `https://${networkUrl}/tx/${transactionHash}`;
window.open(url, "_blank"); window.open(url, "_blank");
}; };
@@ -43,6 +120,31 @@ const loadMore = (): void => {
); );
}; };
const getEventName = (event: string | undefined): string => {
if (!event) return "Desconhecido";
const possibleEventName: { [key: string]: string } = {
DepositAdded: "Oferta",
LockAdded: "Reserva",
LockReleased: "Compra",
DepositWithdrawn: "Retirada",
};
return possibleEventName[event];
};
onMounted(() => {
useFloating(reference, floating, {
placement: "right",
middleware: [
offset(10),
flip(),
shift(),
arrow({ element: floatingArrow }),
],
});
});
// watch props changes // watch props changes
watch(props, async (): Promise<void> => { watch(props, async (): Promise<void> => {
const itemsToShowQty = itemsToShow.value.length; const itemsToShowQty = itemsToShow.value.length;
@@ -59,31 +161,181 @@ showInitialItems();
</script> </script>
<template> <template>
<div <div class="blur-container" v-if="loadingWalletTransactions">
class="main-container max-w-md flex justify-center items-center min-h-[200px] w-16 h-16"
v-if="loadingWalletTransactions"
>
Carregando ofertas...
<SpinnerComponent width="8" height="8"></SpinnerComponent> <SpinnerComponent width="8" height="8"></SpinnerComponent>
</div> </div>
<div class="main-container max-w-md" v-else> <div class="blur-container" v-if="!loadingWalletTransactions">
<BalanceCard <div
class="w-full bg-white p-4 sm:p-6 rounded-lg"
v-if="props.validDeposits.length > 0" v-if="props.validDeposits.length > 0"
:valid-deposits="props.validDeposits" >
:active-lock-amount="activeLockAmount" <div class="flex justify-between items-center">
:selected-token="user.selectedToken.value" <div>
@withdraw="callWithdraw" <p class="text-sm leading-5 font-medium text-gray-600">
Saldo disponível
</p>
<p class="text-xl leading-7 font-semibold text-gray-900">
{{ getRemaining() }} BRZ
</p>
<div class="flex gap-2 w-32 sm:w-56" v-if="activeLockAmount != 0">
<span class="text-xs font-normal text-gray-400" ref="infoText">{{
`com ${activeLockAmount.toFixed(2)} BRZ em lock`
}}</span>
<div
class="absolute mt-[2px] md-view"
:style="{ left: `${(infoText?.clientWidth ?? 108) + 4}px` }"
>
<img
alt="info image"
src="@/assets/info.svg"
aria-describedby="tooltip"
ref="reference"
@mouseover="showInfoTooltip = true"
@mouseout="showInfoTooltip = false"
/> />
<div
role="tooltip"
ref="floating"
class="w-56 z-50 tooltip md-view"
v-if="showInfoTooltip"
>
Valor em lock significa que a quantia está aguardando
confirmação de compra e estará disponível para saque caso a
transação expire.
</div>
</div>
</div>
</div>
<div v-show="!isCollapsibleOpen" class="flex justify-end items-center">
<div
class="flex gap-2 cursor-pointer items-center justify-self-center border-2 p-2 border-amber-300 rounded-md"
@click="[(isCollapsibleOpen = true)]"
>
<img
alt="Withdraw image"
src="@/assets/withdraw.svg"
class="w-3 h-3 sm:w-4 sm:h-4"
/>
<span class="last-release-info">Sacar</span>
</div>
</div>
</div>
<div class="pt-5">
<div v-show="isCollapsibleOpen" class="py-2 w-100">
<p class="text-sm leading-5 font-medium">Valor do saque</p>
<input
type="number"
name=""
@input="debounce(handleInputEvent, 500)($event)"
placeholder="0"
class="text-2xl text-gray-900 w-full outline-none"
v-model="withdrawAmount"
/>
</div>
<div class="flex justify-center" v-if="!validDecimals">
<span class="text-red-500 font-normal text-sm"
>Por favor utilize no máximo 2 casas decimais</span
>
</div>
<div class="flex justify-center" v-else-if="!validWithdrawAmount">
<span class="text-red-500 font-normal text-sm"
>Saldo insuficiente</span
>
</div>
<hr v-show="isCollapsibleOpen" class="pb-3" />
<div
v-show="isCollapsibleOpen"
class="flex justify-between items-center"
>
<h1
@click="[(isCollapsibleOpen = false)]"
class="text-black font-medium cursor-pointer"
>
Cancelar
</h1>
<TransactionCard <div
class="withdraw-button flex gap-2 items-center justify-self-center border-2 p-2 border-amber-300 rounded-md"
@click="callWithdraw"
>
<img
alt="Withdraw image"
src="@/assets/withdraw.svg"
class="w-3 h-3 sm:w-4 sm:h-4"
/>
<span class="last-release-info">Sacar</span>
</div>
</div>
</div>
</div>
<div
class="w-full bg-white p-4 sm:p-6 rounded-lg"
v-for="item in itemsToShow" v-for="item in itemsToShow"
:key="item.blockNumber" :key="item.blockNumber"
:selected-token="user.selectedToken.value" >
:transaction="item" <div class="item-container">
:network-name="user.network.value.name" <div class="flex flex-col self-start">
@open-explorer="openEtherscanUrl" <span class="text-xs sm:text-sm leading-5 font-medium text-gray-600">
{{ getEventName(item.event) }}
</span>
<span
class="text-xl sm:text-xl leading-7 font-semibold text-gray-900"
>
{{ item.amount }}
BRZ
</span>
</div>
<div>
<div
class="bg-amber-300 status-text"
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
>
Em Aberto
</div>
<div
class="bg-[#94A3B8] status-text"
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 2"
>
Expirado
</div>
<div
class="bg-emerald-300 status-text"
v-if="
(getEventName(item.event) == 'Reserva' && item.lockStatus == 3) ||
getEventName(item.event) != 'Reserva'
"
>
Finalizado
</div>
<div
class="flex gap-2 cursor-pointer items-center justify-self-center w-full"
@click="openEtherscanUrl(item.transactionHash)"
v-if="getEventName(item.event) != 'Reserva' || item.lockStatus != 1"
>
<span class="last-release-info">{{ getExplorer() }}</span>
<img
alt="Redirect image"
src="@/assets/redirect.svg"
class="w-3 h-3 sm:w-4 sm:h-4"
/> />
</div>
<div
class="flex gap-2 justify-self-center w-full"
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
>
<RouterLink
:to="{
name: 'home',
force: true,
state: { lockID: item.transactionID },
}"
class="router-button"
>Continuar</RouterLink
>
</div>
</div>
</div>
</div>
<div <div
class="flex flex-col justify-center items-center w-full mt-2 gap-2" class="flex flex-col justify-center items-center w-full mt-2 gap-2"
v-if=" v-if="
@@ -93,14 +345,14 @@ showInitialItems();
> >
<button <button
type="button" type="button"
class="text-white font-semibold border-2 border-amber-300 rounded-lg px-4 py-2 hover:bg-amber-300/10 transition-colors cursor-pointer" class="text-white font-semibold"
@click="loadMore()" @click="loadMore()"
> >
Carregar mais Carregar mais
</button> </button>
<span class="text-gray-300 text-sm"> <span class="text-gray-300">
{{ itemsToShow.length }} de {{ props.walletTransactions.length }} ({{ itemsToShow.length }} de {{ props.walletTransactions.length }}
transações transações )
</span> </span>
</div> </div>
@@ -111,5 +363,66 @@ showInitialItems();
</template> </template>
<style scoped> <style scoped>
/* Minimal styles - most styles moved to child components */ .page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
p {
@apply text-gray-900;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.item-container {
@apply flex justify-between items-center;
}
.status-text {
@apply text-xs sm:text-base font-medium text-gray-900 rounded-lg text-center mb-2 px-2 py-1 mt-4;
}
.text {
@apply text-white text-center;
}
.blur-container {
@apply flex flex-col justify-center items-center px-4 py-3 sm:px-8 sm:py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md w-auto;
}
.grid-container {
@apply grid grid-cols-4 grid-flow-row items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10 w-auto;
}
.last-release-info {
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
}
.tooltip {
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
}
.router-button {
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
}
.withdraw-button {
opacity: v-bind(withdrawButtonOpacity);
cursor: v-bind(withdrawButtonCursor);
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
@media screen and (max-width: 640px) {
.md-view {
display: none;
}
}
</style> </style>

View File

@@ -1,145 +0,0 @@
<script setup lang="ts">
import type { WalletTransaction } from "@/model/WalletTransaction";
import { TokenEnum } from "@/model/NetworkEnum";
import { computed } from "vue";
import StatusBadge, { type StatusType } from "../ui/StatusBadge.vue";
import { Networks } from "@/config/networks";
const props = defineProps<{
transaction: WalletTransaction;
networkName: keyof typeof Networks;
selectedToken: TokenEnum;
}>();
const emit = defineEmits<{
openExplorer: [transactionHash: string];
}>();
const eventName = computed(() => {
if (!props.transaction.event) return "Desconhecido";
const possibleEventName: { [key: string]: string } = {
DepositAdded: "Oferta",
LockAdded: "Reserva",
LockReleased: "Compra",
DepositWithdrawn: "Retirada",
};
return possibleEventName[props.transaction.event] || "Desconhecido";
});
const explorerName = computed(() => {
return Networks[(props.networkName as string).toLowerCase()].blockExplorers?.default.name;
});
const statusType = computed((): StatusType => {
if (eventName.value === "Reserva") {
switch (props.transaction.lockStatus) {
case 1:
return "open";
case 2:
return "expired";
case 3:
return "completed";
default:
return "completed";
}
}
return "completed";
});
const showExplorerLink = computed(() => {
return eventName.value !== "Reserva" || props.transaction.lockStatus !== 1;
});
const showContinueButton = computed(() => {
return eventName.value === "Reserva" && props.transaction.lockStatus === 1;
});
const formattedDate = computed(() => {
if (!props.transaction.blockTimestamp) return "";
const timestamp = props.transaction.blockTimestamp;
const date = new Date(timestamp * 1000);
const day = String(date.getDate()).padStart(2, "0");
const month = String(date.getMonth() + 1).padStart(2, "0");
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${day}/${month}/${year} ${hours}:${minutes}`;
});
const handleExplorerClick = () => {
emit("openExplorer", props.transaction.transactionHash);
};
</script>
<template>
<div class="w-full bg-white p-4 sm:p-6 rounded-lg">
<div class="item-container">
<div class="flex flex-col self-start">
<span class="text-xs sm:text-sm leading-5 font-medium text-gray-600">
{{ eventName }}
</span>
<span class="text-xl sm:text-xl leading-7 font-semibold text-gray-900">
{{ transaction.amount }} {{ selectedToken }}
</span>
<span
v-if="formattedDate"
class="text-xs sm:text-sm leading-5 font-normal text-gray-500 mt-1"
>
{{ formattedDate }}
</span>
</div>
<div class="flex flex-col items-center justify-center">
<div class="mb-2 mt-4">
<StatusBadge :status="statusType" />
</div>
<div
v-if="showExplorerLink"
class="flex gap-2 cursor-pointer items-center justify-self-center w-full"
@click="handleExplorerClick"
>
<span class="last-release-info">{{ explorerName }}</span>
<img
alt="Redirect image"
src="@/assets/redirect.svg?url"
class="w-3 h-3 sm:w-4 sm:h-4"
/>
</div>
<div
v-if="showContinueButton"
class="flex gap-2 justify-self-center w-full"
>
<RouterLink
:to="{
name: 'home',
force: true,
state: { lockID: transaction.transactionID },
}"
class="router-button"
>
Continuar
</RouterLink>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.item-container {
@apply flex justify-between items-center;
}
.last-release-info {
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
}
.router-button {
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
}
</style>

View File

@@ -0,0 +1,87 @@
import { mount } from "@vue/test-utils";
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
import { createPinia, setActivePinia } from "pinia";
import { expect } from "vitest";
import { MockValidDeposits } from "@/model/mock/ValidDepositMock";
import { MockWalletTransactions } from "@/model/mock/WalletTransactionMock";
import { useEtherStore } from "@/store/ether";
describe("ListingComponent.vue", () => {
beforeEach(() => {
setActivePinia(createPinia());
useEtherStore().setLoadingWalletTransactions(false);
});
test("Test Message when an empty array is received", () => {
const wrapper = mount(ListingComponent, {
props: {
validDeposits: [],
walletTransactions: [],
activeLockAmount: 0,
},
});
expect(wrapper.html()).toContain("Não há nenhuma transação anterior");
});
test("Test number of elements in the list first render", () => {
const wrapper = mount(ListingComponent, {
props: {
validDeposits: [],
walletTransactions: MockWalletTransactions,
activeLockAmount: 0,
},
});
const elements = wrapper.findAll(".item-container");
expect(elements).toHaveLength(3);
});
test("Test load more button behavior", async () => {
const wrapper = mount(ListingComponent, {
props: {
validDeposits: MockValidDeposits,
walletTransactions: MockWalletTransactions,
activeLockAmount: 0,
},
});
const btn = wrapper.find("button");
let elements = wrapper.findAll(".item-container");
expect(elements).toHaveLength(3);
await btn.trigger("click");
elements = wrapper.findAll(".item-container");
expect(elements).toHaveLength(5);
});
test("Test withdraw offer button emit", async () => {
const wrapper = mount(ListingComponent, {
props: {
validDeposits: MockValidDeposits,
walletTransactions: MockWalletTransactions,
activeLockAmount: 0,
},
});
wrapper.vm.$emit("depositWithdrawn");
await wrapper.vm.$nextTick();
expect(wrapper.emitted("depositWithdrawn")).toBeTruthy();
});
test("Test should render lock info when active lock amount is greater than 0", () => {
const wrapper = mount(ListingComponent, {
props: {
validDeposits: MockValidDeposits,
walletTransactions: [],
activeLockAmount: 50,
},
});
expect(wrapper.html()).toContain("com 50.00 BRZ em lock");
});
});

View File

@@ -15,7 +15,7 @@ const props = defineProps({
{{ props.title ? props.title : "Confirme em sua carteira" }} {{ props.title ? props.title : "Confirme em sua carteira" }}
</span> </span>
</div> </div>
<div class="main-container max-w-md"> <div class="blur-container sm:w-[26rem] w-[20rem]">
<div <div
class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg border-y-10" class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg border-y-10"
> >
@@ -24,7 +24,7 @@ const props = defineProps({
> >
<img <img
alt="Polygon image" alt="Polygon image"
src="@/assets/validating.svg?url" src="@/assets/validating.svg"
width="96" width="96"
height="48" height="48"
/> />
@@ -60,6 +60,10 @@ const props = defineProps({
@apply text-white text-center; @apply text-white text-center;
} }
.blur-container {
@apply flex flex-col justify-center items-center sm:px-8 px-6 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10;
}
input[type="number"] { input[type="number"] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }

View File

@@ -0,0 +1,27 @@
import { mount } from "@vue/test-utils";
import LoadingComponent from "../LoadingComponent.vue";
describe("Loading.vue", () => {
test("Test loading content with received props", () => {
const wrapper = mount(LoadingComponent, {
props: {
title: "MockTitle",
message: "MockMessage",
},
});
expect(wrapper.html()).toContain("MockTitle");
expect(wrapper.html()).toContain("MockMessage");
});
test("Test default text if props title isnt passed", () => {
const wrapper = mount(LoadingComponent, {
props: {
message: "MockMessage",
},
});
expect(wrapper.html()).toContain("Confirme em sua carteira");
expect(wrapper.html()).toContain("MockMessage");
});
});

View File

@@ -0,0 +1,254 @@
<script setup lang="ts">
import { pix } from "@/utils/QrCodePix";
import { onMounted, onUnmounted, ref } from "vue";
import { debounce } from "@/utils/debounce";
import CustomButton from "@/components/CustomButton/CustomButton.vue";
import CustomModal from "@/components//CustomModal/CustomModal.vue";
import api from "@/services/index";
// props and store references
const props = defineProps({
pixTarget: String,
tokenValue: Number,
});
const windowSize = ref<number>(window.innerWidth);
const qrCode = ref<string>("");
const qrCodePayload = ref<string>("");
const isPixValid = ref<boolean>(false);
const isCodeInputEmpty = ref<boolean>(true);
const showWarnModal = ref<boolean>(true);
const e2eId = ref<string>("");
// Emits
const emit = defineEmits(["pixValidated"]);
const pixQrCode = pix({
pixKey: props.pixTarget ?? "",
value: props.tokenValue,
});
pixQrCode.base64QrCode().then((code: string) => {
qrCode.value = code;
});
qrCodePayload.value = pixQrCode.payload();
const handleInputEvent = async (event: any): Promise<void> => {
const { value } = event.target;
e2eId.value = value;
await validatePix();
};
const validatePix = async (): Promise<void> => {
if (e2eId.value == "") {
isPixValid.value = false;
isCodeInputEmpty.value = true;
return;
}
const sellerPixKey = props.pixTarget;
const transactionValue = props.tokenValue;
if (sellerPixKey && transactionValue) {
const body_req = {
e2e_id: e2eId.value,
pix_key: sellerPixKey,
pix_value: transactionValue,
};
isCodeInputEmpty.value = false;
try {
await api.post("validate_pix", body_req);
isPixValid.value = true;
} catch (error) {
isPixValid.value = false;
}
} else {
isCodeInputEmpty.value = false;
isPixValid.value = false;
}
};
onMounted(() => {
window.addEventListener(
"resize",
() => (windowSize.value = window.innerWidth)
);
});
onUnmounted(() => {
window.removeEventListener(
"resize",
() => (windowSize.value = window.innerWidth)
);
});
</script>
<template>
<div class="page">
<div class="text-container">
<span
class="text font-extrabold lg:text-2xl text-xl sm:max-w-[30rem] max-w-[24rem]"
>
Utilize o QR Code ou copie o código para realizar o Pix
</span>
<span class="text font-medium lg:text-md text-sm max-w-[28rem]">
Após realizar o Pix no banco de sua preferência, insira o código de
autenticação para enviar a transação para a rede.
</span>
</div>
<div class="blur-container sm:max-w-[28rem] max-w-[20rem] text-black">
<div
class="flex-col items-center justify-center flex w-full bg-white sm:p-8 p-4 rounded-lg break-normal"
>
<img alt="Qr code image" :src="qrCode" class="w-48 h-48" />
<span class="text-center font-bold">Código pix</span>
<div class="break-words w-4/5">
<span class="text-center text-xs">
{{ qrCodePayload }}
</span>
</div>
<img
alt="Copy PIX code"
src="@/assets/copyPix.svg"
width="16"
height="16"
class="pt-2 lg:mb-5 cursor-pointer"
/>
<span class="text-xs text-start lg-view">
<strong>ATENÇÃO!</strong> A transação será processada após inserir
o código de autenticação. Caso contrário não conseguiremos comprovar o
seu depósito e não será possível transferir os tokens para sua
carteira. Confira aqui como encontrar o código no comprovante.
</span>
</div>
<div
class="flex-col items-center justify-center flex w-full bg-white p-5 rounded-lg px-5"
>
<input
type="text"
placeholder="Digite o código do comprovante PIX"
@input="debounce(handleInputEvent, 500)($event)"
class="sm:text-md text-sm w-full box-border p-2 sm:h-6 h-2 mb-2 outline-none"
/>
<div class="custom-divide" v-if="!isCodeInputEmpty"></div>
<div
class="flex flex-col w-full"
v-if="!isPixValid && !isCodeInputEmpty"
>
<div class="flex items-center h-8">
<img
alt="Invalid Icon"
src="@/assets/invalidIcon.svg"
width="14"
class="cursor-pointer align-middle inline-block"
/>
<span class="px-1 text-red-500 font-normal text-xs"
>Código inválido. Por favor, confira e tente novamente.</span
>
</div>
</div>
<div class="flex flex-col w-full" v-else-if="isPixValid == true">
<div class="flex items-center h-8">
<img
alt="Valid Icon"
src="@/assets/validIcon.svg"
width="14"
class="cursor-pointer align-middle inline-block"
/>
<span class="px-1 text-green-500 font-normal text-sm">
Código válido.
</span>
</div>
</div>
</div>
<CustomButton
:is-disabled="isPixValid == false"
:text="'Enviar para a rede'"
@button-clicked="emit('pixValidated', e2eId)"
/>
</div>
<CustomModal
v-if="showWarnModal && windowSize <= 500"
@close-modal="showWarnModal = false"
:isRedirectModal="false"
/>
</div>
</template>
<style scoped>
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
::placeholder {
/* Most modern browsers support this now. */
color: #9ca3af;
}
h4 {
color: #080808;
font-size: 14px;
}
h2 {
color: #080808;
}
.form-input {
@apply rounded-lg border border-gray-200 p-2 text-black;
}
.form-label {
@apply font-semibold tracking-wide text-emerald-50;
}
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
.blur-container {
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-6;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
.lg-view {
display: inline-block;
}
.sm-view {
display: none;
}
@media screen and (max-width: 500px) {
.lg-view {
display: none;
}
.sm-view {
display: inline-block;
}
}
</style>

View File

@@ -0,0 +1,269 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import CustomButton from "@/components/CustomButton/CustomButton.vue";
import { debounce } from "@/utils/debounce";
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { connectProvider } from "@/blockchain/provider";
import { verifyNetworkLiquidity } from "@/utils/networkLiquidity";
import { NetworkEnum } from "@/model/NetworkEnum";
import type { ValidDeposit } from "@/model/ValidDeposit";
import { decimalCount } from "@/utils/decimalCount";
import SpinnerComponent from "./SpinnerComponent.vue";
// Store reference
const etherStore = useEtherStore();
const {
walletAddress,
networkName,
depositsValidListSepolia,
depositsValidListMumbai,
loadingNetworkLiquidity,
} = storeToRefs(etherStore);
// Reactive state
const tokenValue = ref<number>(0);
const enableConfirmButton = ref<boolean>(false);
const enableWalletButton = ref<boolean>(false);
const hasLiquidity = ref<boolean>(true);
const validDecimals = ref<boolean>(true);
const selectedSepoliaDeposit = ref<ValidDeposit>();
const selectedMumbaiDeposit = ref<ValidDeposit>();
const selectedRootstockDeposit = ref<ValidDeposit>();
// Emits
const emit = defineEmits(["tokenBuy"]);
// Blockchain methods
const connectAccount = async (): Promise<void> => {
await connectProvider();
enableOrDisableConfirmButton();
};
const emitConfirmButton = (): void => {
const selectedDeposit =
networkName.value == NetworkEnum.ethereum
? selectedSepoliaDeposit.value
: selectedMumbaiDeposit.value;
emit("tokenBuy", selectedDeposit, tokenValue.value);
};
// Debounce methods
const handleInputEvent = (event: any): void => {
const { value } = event.target;
tokenValue.value = Number(value);
if (decimalCount(String(tokenValue.value)) > 2) {
validDecimals.value = false;
enableConfirmButton.value = false;
return;
}
validDecimals.value = true;
verifyLiquidity();
};
// Verify if there is a valid deposit to buy
const verifyLiquidity = (): void => {
enableConfirmButton.value = false;
selectedSepoliaDeposit.value = undefined;
selectedMumbaiDeposit.value = undefined;
selectedRootstockDeposit.value = undefined;
if (tokenValue.value <= 0) {
enableWalletButton.value = false;
return;
}
selectedSepoliaDeposit.value = verifyNetworkLiquidity(
tokenValue.value,
walletAddress.value,
depositsValidListSepolia.value
);
selectedMumbaiDeposit.value = verifyNetworkLiquidity(
tokenValue.value,
walletAddress.value,
depositsValidListMumbai.value
);
enableOrDisableConfirmButton();
if (selectedSepoliaDeposit.value || selectedMumbaiDeposit.value) {
hasLiquidity.value = true;
enableWalletButton.value = true;
} else {
hasLiquidity.value = false;
enableWalletButton.value = true;
}
};
const enableOrDisableConfirmButton = (): void => {
if (selectedSepoliaDeposit.value && networkName.value == NetworkEnum.ethereum)
enableConfirmButton.value = true;
else if (
selectedMumbaiDeposit.value &&
networkName.value == NetworkEnum.polygon
)
enableConfirmButton.value = true;
else enableConfirmButton.value = false;
};
watch(networkName, (): void => {
verifyLiquidity();
enableOrDisableConfirmButton();
});
watch(walletAddress, (): void => {
verifyLiquidity();
});
</script>
<template>
<div class="page">
<div class="text-container">
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>
Adquira cripto com apenas um Pix</span
>
<span class="text font-medium sm:text-base text-sm max-w-[28rem]"
>Digite um valor, confira a oferta, conecte sua carteira e receba os
tokens após realizar o Pix</span
>
</div>
<div class="blur-container">
<div class="backdrop-blur -z-10 w-full h-full"></div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
>
<div class="flex justify-between sm:w-full items-center">
<input
type="number"
class="border-none outline-none text-lg text-gray-900 w-3/4"
v-bind:class="{
'font-semibold': tokenValue != undefined,
'text-xl': tokenValue != undefined,
}"
@input="debounce(handleInputEvent, 500)($event)"
placeholder="0 "
step=".01"
/>
<div
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
>
<img
alt="Token image"
class="sm:w-fit w-4"
src="@/assets/brz.svg"
/>
<span class="text-gray-900 sm:text-lg text-md w-fit" id="brz"
>BRZ</span
>
</div>
</div>
<div class="custom-divide py-2 mb-2"></div>
<div
class="flex justify-between"
v-if="hasLiquidity && !loadingNetworkLiquidity"
>
<p class="text-gray-500 font-normal text-sm w-auto">
~ R$ {{ tokenValue.toFixed(2) }}
</p>
<div class="flex gap-2">
<img
alt="Polygon image"
src="@/assets/polygon.svg"
width="24"
height="24"
v-if="selectedMumbaiDeposit"
/>
<img
alt="Ethereum image"
src="@/assets/ethereum.svg"
width="24"
height="24"
v-if="selectedSepoliaDeposit"
/>
</div>
</div>
<div
class="flex justify-center items-center"
v-if="loadingNetworkLiquidity"
>
<span class="text-gray-900 font-normal text-sm mr-2"
>Carregando liquidez das redes.</span
>
<SpinnerComponent width="4" height="4"></SpinnerComponent>
</div>
<div
class="flex justify-center"
v-if="!validDecimals && !loadingNetworkLiquidity"
>
<span class="text-red-500 font-normal text-sm"
>Por favor utilize no máximo 2 casas decimais</span
>
</div>
<div
class="flex justify-center"
v-else-if="!hasLiquidity && !loadingNetworkLiquidity"
>
<span class="text-red-500 font-normal text-sm"
>Atualmente não liquidez nas redes para sua demanda</span
>
</div>
</div>
<CustomButton
v-if="!walletAddress"
:text="'Conectar carteira'"
:is-disabled="!enableWalletButton"
@buttonClicked="connectAccount()"
/>
<CustomButton
v-if="walletAddress"
:text="'Confirmar compra'"
:is-disabled="!enableConfirmButton"
@buttonClicked="emitConfirmButton()"
/>
</div>
</div>
</template>
<style scoped>
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
.blur-container {
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 mt-10;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
</style>

View File

@@ -1,343 +0,0 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { useUser } from "@/composables/useUser";
import CustomButton from "@/components/ui/CustomButton.vue";
import { postProcessKey } from "@/utils/pixKeyFormat";
import { TokenEnum } from "@/model/NetworkEnum";
import { getTokenImage } from "@/utils/imagesPath";
import { useOnboard } from "@web3-onboard/vue";
import ChevronDown from "@/assets/chevron.svg";
// Import the bank list
import bankList from "@/utils/files/isbpList.json";
import type { Participant } from "@/utils/bbPay";
// Define Bank interface
interface Bank {
ISPB: string;
longName: string;
}
// html references
const tokenDropdownRef = ref<any>(null);
const formRef = ref<HTMLFormElement | null>(null);
// Reactive state
const user = useUser();
const { walletAddress, selectedToken } = user;
const offer = ref<string>("");
const identification = ref<string>("");
const account = ref<string>("");
const branch = ref<string>("");
const accountType = ref<string>("");
const selectTokenToggle = ref<boolean>(false);
const savingsVariation = ref<string>("");
const errors = ref<{ [key: string]: string }>({});
// Bank selection
const bankSearchQuery = ref<string>("");
const showBankList = ref<boolean>(false);
const selectedBank = ref<Bank | null>(null);
const filteredBanks = computed(() => {
if (!bankSearchQuery.value) return [];
return bankList
.filter((bank) =>
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase())
)
.slice(0, 5);
});
const handleBankSelect = (bank: Bank) => {
selectedBank.value = bank;
bankSearchQuery.value = bank.longName;
showBankList.value = false;
};
// Emits
const emit = defineEmits(["approveTokens"]);
// Methods
const connectAccount = async (): Promise<void> => {
const { connectWallet } = useOnboard();
await connectWallet();
};
const handleSubmit = (e: Event): void => {
e.preventDefault();
const processedIdentification = postProcessKey(identification.value);
const data: Participant = {
offer: offer.value,
chainID: user.network.value.id,
identification: processedIdentification,
bankIspb: selectedBank.value?.ISPB,
accountType: accountType.value,
account: account.value,
branch: branch.value,
savingsVariation: savingsVariation.value || "",
};
emit("approveTokens", data);
};
// Token selection
const openTokenSelection = (): void => {
selectTokenToggle.value = true;
};
const handleSelectedToken = (token: TokenEnum): void => {
user.setSelectedToken(token);
selectTokenToggle.value = false;
};
</script>
<template>
<div class="page w-full">
<div class="text-container">
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>
Venda cripto e receba em Pix
</span>
<span
class="text font-medium sm:text-base text-xs sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide"
>
Digite sua oferta, informe a chave Pix, selecione a rede, aprove o envio
da transação e confirme sua oferta.
</span>
</div>
<form ref="formRef" @submit="handleSubmit" class="main-container">
<!-- Offer input -->
<div
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10 gap-4"
>
<input
type="number"
v-model="offer"
class="border-none outline-none text-gray-900 sm:w-fit w-3/4 flex-grow"
:class="{
'!font-medium': offer !== undefined && offer !== '',
'text-xl': offer !== undefined && offer !== '',
}"
min="0.01"
max="999999999.99"
pattern="\d+(\.\d{0,2})?"
placeholder="Digite sua oferta (mínimo R$0,01)"
step=".01"
required
/>
<div class="relative overflow-visible">
<button
ref="tokenDropdownRef"
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
@click="openTokenSelection()"
>
<img
alt="Token image"
class="sm:w-fit w-4"
:src="getTokenImage(selectedToken)"
/>
<span
class="text-gray-900 sm:text-lg text-md font-medium"
id="token"
>
{{ selectedToken }}
</span>
<ChevronDown
class="text-gray-900 pr-4 sm:pr-0 transition-all duration-500 ease-in-out"
:class="{ 'scale-y-[-1]': selectTokenToggle }"
alt="Chevron Down"
/>
</button>
<transition name="dropdown">
<div
v-if="selectTokenToggle"
class="mt-2 text-gray-900 absolute right-0 z-50 w-full min-w-max"
>
<div
class="bg-white rounded-xl z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
>
<div
v-for="token in TokenEnum"
:key="token"
class="flex menu-button gap-2 px-4 cursor-pointer hover:bg-gray-300 transition-colors"
@click="handleSelectedToken(token)"
>
<img
:alt="token + ' logo'"
width="20"
height="20"
:src="getTokenImage(token)"
/>
<span
class="text-gray-900 py-4 text-end font-semibold text-sm"
>
{{ token }}
</span>
</div>
</div>
</div>
</transition>
</div>
</div>
<!-- CPF or CNPJ input -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<input
type="text"
v-model="identification"
maxlength="14"
:pattern="'^\\d{11}$|^\\d{14}$'"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': identification }"
placeholder="Digite seu CPF ou CNPJ (somente números)"
required
/>
</div>
<!-- Bank selection -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<div class="relative">
<input
type="text"
v-model="bankSearchQuery"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': bankSearchQuery }"
placeholder="Buscar banco"
@focus="showBankList = true"
required
/>
<div
v-if="showBankList && filteredBanks.length > 0"
class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-50 max-h-60 overflow-y-auto"
>
<div
v-for="bank in filteredBanks"
:key="bank.ISPB"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer transition-colors"
@click="handleBankSelect(bank)"
>
<div class="text-sm font-medium text-gray-900">
{{ bank.longName }}
</div>
<div class="text-xs text-gray-500">ISPB: {{ bank.ISPB }}</div>
</div>
</div>
</div>
<span v-if="errors.bank" class="text-red-500 text-sm mt-2">{{
errors.bank
}}</span>
</div>
<!-- Account and Branch inputs -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<div class="flex gap-4">
<div class="flex-1">
<input
type="text"
v-model="account"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': account }"
placeholder="Número da conta"
required
/>
</div>
<div class="flex-1">
<input
type="text"
v-model="branch"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': branch }"
placeholder="Agência"
required
/>
</div>
</div>
</div>
<!-- Account Type Selection -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<div class="flex gap-4">
<div class="flex-1">
<select
v-model="accountType"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
required
>
<option value="" disabled selected>Tipo de conta</option>
<option value="1">Conta Corrente</option>
<option value="2">Conta Poupança</option>
<option value="3">Conta Salário</option>
<option value="4">Conta Pré-Paga</option>
</select>
</div>
</div>
</div>
<!-- Savings Account Variation -->
<Transition name="resize">
<input
v-if="accountType === '2'"
type="text"
v-model="savingsVariation"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
:class="{ 'text-xl font-medium': savingsVariation }"
placeholder="Variação da poupança"
required
/>
</Transition>
<!-- Action buttons -->
<CustomButton v-if="walletAddress" type="submit" text="Aprovar tokens" />
<CustomButton
v-else
text="Conectar carteira"
@buttonClicked="connectAccount()"
/>
</form>
</div>
</template>
<style scoped>
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input {
@apply sm:text-lg text-sm;
}
</style>

View File

@@ -1,15 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { useUser } from "@/composables/useUser"; import CustomButton from "@/components/CustomButton/CustomButton.vue";
import CustomButton from "@/components/ui/CustomButton.vue";
import { debounce } from "@/utils/debounce"; import { debounce } from "@/utils/debounce";
import { decimalCount } from "@/utils/decimalCount"; import { decimalCount } from "@/utils/decimalCount";
import { getTokenImage } from "@/utils/imagesPath";
import { useOnboard } from "@web3-onboard/vue";
// Store
const user = useUser();
const { walletAddress } = user;
// Reactive state // Reactive state
const tokenValue = ref<number>(0); const tokenValue = ref<number>(0);
@@ -20,12 +13,6 @@ const validDecimals = ref<boolean>(true);
// Emits // Emits
const emit = defineEmits(["tokenBuy"]); const emit = defineEmits(["tokenBuy"]);
// Blockchain methods
const connectAccount = async (): Promise<void> => {
const { connectWallet } = useOnboard();
await connectWallet();
};
// Debounce methods // Debounce methods
const handleInputEvent = (event: any): void => { const handleInputEvent = (event: any): void => {
const { value } = event.target; const { value } = event.target;
@@ -52,7 +39,7 @@ const handleInputEvent = (event: any): void => {
tokens após realizar o Pix</span tokens após realizar o Pix</span
> >
</div> </div>
<div class="main-container"> <div class="blur-container">
<div <div
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10" class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
> >
@@ -71,14 +58,8 @@ const handleInputEvent = (event: any): void => {
<div <div
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1" class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
> >
<img <img alt="Token image" class="w-fit" src="@/assets/brz.svg" />
alt="Token image" <span class="text-gray-900 text-lg w-fit" id="brz">BRZ</span>
class="w-fit"
:src="getTokenImage(user.selectedToken.value)"
/>
<span class="text-gray-900 text-lg w-fit" id="token">{{
user.selectedToken
}}</span>
</div> </div>
</div> </div>
@@ -90,13 +71,13 @@ const handleInputEvent = (event: any): void => {
<div class="flex gap-2"> <div class="flex gap-2">
<img <img
alt="Polygon image" alt="Polygon image"
src="@/assets/networks/polygon.svg?url" src="@/assets/polygon.svg"
width="24" width="24"
height="24" height="24"
/> />
<img <img
alt="Ethereum image" alt="Ethereum image"
src="@/assets/networks/ethereum.svg?url" src="@/assets/ethereum.svg"
width="24" width="24"
height="24" height="24"
/> />
@@ -113,17 +94,10 @@ const handleInputEvent = (event: any): void => {
> >
</div> </div>
</div> </div>
<CustomButton <CustomButton
v-if="walletAddress"
:text="'Conectar carteira'" :text="'Conectar carteira'"
@buttonClicked="emit('tokenBuy')" @buttonClicked="emit('tokenBuy')"
/> />
<CustomButton
v-if="!walletAddress"
:text="'Conectar carteira'"
@buttonClicked="connectAccount()"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -151,6 +125,14 @@ const handleInputEvent = (event: any): void => {
@apply text-white text-center; @apply text-white text-center;
} }
.blur-container {
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;

View File

@@ -1,14 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import CustomButton from "@/components/ui/CustomButton.vue"; import CustomButton from "@/components/CustomButton/CustomButton.vue";
// Emits // Emits
const emit = defineEmits(["sendNetwork"]); const emit = defineEmits(["sendNetwork"]);
// props and store references // props and store references
const props = defineProps({ const props = defineProps({
sellerId: String, pixKey: String,
offer: Number, offer: Number,
selectedToken: String,
}); });
</script> </script>
@@ -20,26 +19,24 @@ const props = defineProps({
>Envie sua oferta para a rede >Envie sua oferta para a rede
</span> </span>
<span <span
class="text font-normal sm:text-base text-xs sm:max-w-[30rem] max-w-[22rem]" class="text text-xl font-normal sm:text-base text-xs sm:max-w-[30rem] max-w-[22rem]"
>Após a confirmação sua oferta estará disponível para outros usuários. >Após a confirmação sua oferta estará disponível para outros usuários.
Caso deseje retirar a oferta, será necessário aguardar 24h para receber Caso deseje retirar a oferta, será necessário aguardar 24h para receber
os tokens de volta.</span os tokens de volta.</span
> >
</div> </div>
<div class="main-container"> <div class="blur-container">
<div <div
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10" class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
> >
<div> <div>
<p>Tokens ofertados</p> <p>Tokens ofertados</p>
<p class="text-2xl text-gray-900"> <p class="text-2xl text-gray-900">{{ props.offer }} BRZ</p>
{{ props.offer }} {{ props.selectedToken }}
</p>
</div> </div>
<div class="my-3"> <div class="my-3">
<p>Chave Pix</p> <p>Chave Pix</p>
<p class="text-xl text-gray-900 break-words"> <p class="text-xl text-gray-900 break-words">
{{ props.sellerId }} {{ props.pixKey }}
</p> </p>
</div> </div>
<div class="mb-5"> <div class="mb-5">
@@ -75,10 +72,18 @@ p {
@apply text-white text-center; @apply text-white text-center;
} }
.blur-container {
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-8 sm:w-1/3;
}
.last-deposit-info { .last-deposit-info {
@apply font-medium text-base; @apply font-medium text-base;
} }
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;

View File

@@ -0,0 +1,186 @@
<script setup lang="ts">
import { ref } from "vue";
import CustomButton from "../CustomButton/CustomButton.vue";
import { debounce } from "@/utils/debounce";
import { decimalCount } from "@/utils/decimalCount";
import { pixFormatValidation, postProcessKey } from "@/utils/pixKeyFormat";
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { connectProvider } from "@/blockchain/provider";
// Reactive state
const etherStore = useEtherStore();
const { walletAddress } = storeToRefs(etherStore);
const offer = ref<string>("");
const pixKey = ref<string>("");
const enableSelectButton = ref<boolean>(false);
const hasLiquidity = ref<boolean>(true);
const validDecimals = ref<boolean>(true);
const validPixFormat = ref<boolean>(true);
// Emits
const emit = defineEmits(["approveTokens"]);
// Debounce methods
const handleInputEvent = (event: any): void => {
const { value } = event.target;
offer.value = value;
if (decimalCount(offer.value) > 2) {
validDecimals.value = false;
enableSelectButton.value = false;
return;
}
validDecimals.value = true;
};
const handlePixKeyInputEvent = (event: any): void => {
const { value } = event.target;
pixKey.value = value;
if (pixFormatValidation(pixKey.value)) {
validPixFormat.value = true;
enableSelectButton.value = true;
return;
}
enableSelectButton.value = false;
validPixFormat.value = false;
};
const handleButtonClick = async (
offer: string,
pixKey: string
): Promise<void> => {
const postProcessedPixKey = postProcessKey(pixKey);
if (walletAddress.value)
emit("approveTokens", { offer, postProcessedPixKey });
else await connectProvider();
};
</script>
<template>
<div class="page w-full">
<div class="text-container">
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>Venda cripto e receba em Pix</span
>
<span
class="text font-medium sm:text-base text-xs sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide"
>Digite sua oferta, informe a chave Pix, selecione a rede, aprove o
envio da transação e confirme sua oferta.</span
>
</div>
<div class="blur-container">
<div class="backdrop-blur -z-10 w-full h-full"></div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
>
<div class="flex justify-between items-center">
<input
type="number"
v-model="offer"
class="border-none outline-none text-gray-900 sm:w-fit w-3/4"
v-bind:class="{
'font-semibold': offer != undefined,
'text-xl': offer != undefined,
}"
@input="debounce(handleInputEvent, 500)($event)"
placeholder="Digite sua oferta"
step=".01"
/>
<div
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
>
<img
alt="Token image"
class="sm:w-fit w-4"
src="@/assets/brz.svg"
/>
<span class="text-gray-900 w-fit" id="brz"> BRZ </span>
</div>
</div>
<div class="flex pt-2 justify-center" v-if="!validDecimals">
<span class="text-red-500 font-normal text-sm"
>Por favor utilize no máximo 2 casas decimais</span
>
</div>
<div class="flex pt-2 justify-center" v-else-if="!hasLiquidity">
<span class="text-red-500 font-normal text-sm"
>Atualmente não liquidez nas redes para sua demanda</span
>
</div>
</div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-8 rounded-lg border-y-10"
>
<div class="flex justify-between w-full items-center">
<input
@input="debounce(handlePixKeyInputEvent, 500)($event)"
type="text"
v-model="pixKey"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-fit"
placeholder="Digite a chave Pix"
/>
</div>
<div class="flex pt-2 justify-center" v-if="!validPixFormat">
<span class="text-red-500 font-normal text-sm"
>Por favor utilize telefone, CPF ou CNPJ</span
>
</div>
</div>
<CustomButton
:text="walletAddress ? 'Aprovar tokens' : 'Conectar Carteira'"
:isDisabled="!validDecimals || !validPixFormat"
@buttonClicked="handleButtonClick(offer, pixKey)"
/>
</div>
</div>
</template>
<style scoped>
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
.blur-container {
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 mt-10 w-auto;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input {
@apply sm:text-lg text-sm;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
/* eslint-disable no-undef */
import { shallowMount } from "@vue/test-utils";
import TopBar from "../TopBar.vue";
import { useEtherStore } from "../../../store/ether";
import { createPinia, setActivePinia } from "pinia";
describe("TopBar.vue", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("should render connect wallet button", () => {
const wrapper = shallowMount(TopBar);
expect(wrapper.html()).toContain("Conectar carteira");
});
it("should render button to change to seller view when in buyer screen", () => {
const wrapper = shallowMount(TopBar);
expect(wrapper.html()).toContain("Quero vender");
});
it("should render button to change to seller view when in buyer screen", () => {
const etherStore = useEtherStore();
etherStore.setSellerView(true);
const wrapper = shallowMount(TopBar);
expect(wrapper.html()).toContain("Quero comprar");
});
it("should render the P2Pix logo correctly", () => {
const wrapper = shallowMount(TopBar);
const img = wrapper.findAll(".logo");
expect(img.length).toBe(2);
});
});

View File

@@ -1,208 +0,0 @@
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { TokenEnum } from "@/model/NetworkEnum";
import { decimalCount } from "@/utils/decimalCount";
import { debounce } from "@/utils/debounce";
import TokenSelector from "./TokenSelector.vue";
import ErrorMessage from "./ErrorMessage.vue";
const props = withDefaults(
defineProps<{
modelValue: number;
selectedToken: TokenEnum;
placeholder?: string;
showTokenSelector?: boolean;
showConversion?: boolean;
conversionRate?: number;
minValue?: number;
maxValue?: number;
disabled?: boolean;
required?: boolean;
}>(),
{
placeholder: "0",
showTokenSelector: true,
showConversion: true,
conversionRate: 1,
minValue: 0,
disabled: false,
required: false,
}
);
const emit = defineEmits<{
"update:modelValue": [value: number];
"update:selectedToken": [token: TokenEnum];
error: [message: string | null];
valid: [isValid: boolean];
}>();
const inputValue = ref<string>(String(props.modelValue || ""));
const validDecimals = ref(true);
const validRange = ref(true);
const convertedValue = computed(() => {
return (props.modelValue * props.conversionRate).toFixed(2);
});
const errorMessage = computed(() => {
if (!validDecimals.value) {
return "Por favor utilize no máximo 2 casas decimais";
}
if (!validRange.value) {
if (props.minValue && props.modelValue < props.minValue) {
return `Valor mínimo: ${props.minValue}`;
}
if (props.maxValue && props.modelValue > props.maxValue) {
return `Valor máximo: ${props.maxValue}`;
}
}
return null;
});
const isValid = computed(() => {
return validDecimals.value && validRange.value;
});
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
const value = target.value;
inputValue.value = value;
const numValue = Number(value);
// Validar decimais
if (decimalCount(value) > 2) {
validDecimals.value = false;
emit("error", "Por favor utilize no máximo 2 casas decimais");
emit("valid", false);
return;
}
validDecimals.value = true;
// Validar range
if (props.minValue !== undefined && numValue < props.minValue) {
validRange.value = false;
emit("error", `Valor mínimo: ${props.minValue}`);
emit("valid", false);
return;
}
if (props.maxValue !== undefined && numValue > props.maxValue) {
validRange.value = false;
emit("error", `Valor máximo: ${props.maxValue}`);
emit("valid", false);
return;
}
validRange.value = true;
emit("update:modelValue", numValue);
emit("error", null);
emit("valid", true);
};
const debouncedHandleInput = debounce(handleInput, 500);
const handleTokenChange = (token: TokenEnum) => {
emit("update:selectedToken", token);
};
watch(() => props.modelValue, (newVal) => {
if (newVal !== Number(inputValue.value)) {
inputValue.value = String(newVal || "");
}
});
</script>
<template>
<div class="amount-input-container">
<div class="input-row">
<input
type="number"
:value="inputValue"
:placeholder="placeholder"
:disabled="disabled"
:required="required"
class="amount-input"
:class="{
'font-semibold text-xl': modelValue > 0,
'has-error': !isValid,
}"
step="0.01"
@input="debouncedHandleInput"
/>
<TokenSelector
v-if="showTokenSelector"
:model-value="selectedToken"
:disabled="disabled"
size="md"
@update:model-value="handleTokenChange"
/>
<div v-else class="token-display">
{{ selectedToken }}
</div>
</div>
<div class="divider"></div>
<div class="info-row">
<p v-if="showConversion" class="conversion-text">
~ R$ {{ convertedValue }}
</p>
<slot name="extra-info"></slot>
</div>
<ErrorMessage
v-if="errorMessage"
:message="errorMessage"
type="error"
/>
</div>
</template>
<style scoped>
.amount-input-container {
@apply flex flex-col w-full gap-2;
}
.input-row {
@apply flex justify-between items-center w-full gap-4;
}
.amount-input {
@apply border-none outline-none text-lg text-gray-900 flex-1 bg-transparent;
appearance: textfield;
-moz-appearance: textfield;
}
.amount-input::-webkit-inner-spin-button,
.amount-input::-webkit-outer-spin-button {
-webkit-appearance: none;
}
.amount-input:disabled {
@apply opacity-50 cursor-not-allowed;
}
.amount-input.has-error {
@apply text-red-500;
}
.token-display {
@apply flex items-center px-3 py-2 bg-gray-300 rounded-3xl min-w-fit text-gray-900 font-medium;
}
.divider {
@apply w-full border-b border-gray-300 my-2;
}
.info-row {
@apply flex justify-between items-center;
}
.conversion-text {
@apply text-gray-500 font-normal text-sm;
}
</style>

View File

@@ -1,137 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import bankList from "@/utils/files/isbpList.json";
export interface Bank {
ISPB: string;
longName: string;
}
const props = withDefaults(
defineProps<{
modelValue: string | null;
disabled?: boolean;
placeholder?: string;
}>(),
{
disabled: false,
placeholder: "Busque e selecione seu banco",
}
);
const emit = defineEmits<{
"update:modelValue": [value: string];
change: [bank: Bank];
}>();
const bankItems = computed(() => {
return bankList.map((bank) => ({
value: bank.ISPB,
label: bank.longName,
bank: bank,
}));
});
const selectedItem = computed(() => {
if (!props.modelValue) return null;
return bankItems.value.find((item) => item.value === props.modelValue);
});
const searchQuery = computed({
get: () => selectedItem.value?.label || "",
set: (value: string) => {
// Handled by input
},
});
const filteredBanks = computed(() => {
if (!searchQuery.value) return [];
const query = searchQuery.value.toLowerCase();
return bankList
.filter((bank) => bank.longName.toLowerCase().includes(query))
.slice(0, 10);
});
const showBankList = computed(() => {
return filteredBanks.value.length > 0 && searchQuery.value.length > 0;
});
const selectBank = (bank: Bank) => {
emit("update:modelValue", bank.ISPB);
emit("change", bank);
};
</script>
<template>
<div class="bank-selector">
<input
type="text"
v-model="searchQuery"
:placeholder="placeholder"
:disabled="disabled"
class="bank-input"
autocomplete="off"
/>
<transition name="dropdown-fade">
<div v-if="showBankList" class="bank-list">
<div
v-for="bank in filteredBanks"
:key="bank.ISPB"
class="bank-item"
@click="selectBank(bank)"
>
<span class="bank-name">{{ bank.longName }}</span>
<span class="bank-ispb">{{ bank.ISPB }}</span>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.bank-selector {
@apply relative w-full;
}
.bank-input {
@apply w-full px-4 py-3 border-none outline-none rounded-lg bg-white text-gray-900 text-sm;
}
.bank-input:focus {
@apply ring-2 ring-indigo-800;
}
.bank-input:disabled {
@apply opacity-50 cursor-not-allowed bg-gray-100;
}
.bank-list {
@apply absolute top-full left-0 right-0 mt-2 bg-white rounded-lg border border-gray-300 shadow-lg z-50 max-h-64 overflow-y-auto;
}
.bank-item {
@apply flex justify-between items-center px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors;
}
.bank-name {
@apply text-gray-900 font-medium text-sm flex-1;
}
.bank-ispb {
@apply text-gray-500 text-xs ml-2;
}
/* Animação */
.dropdown-fade-enter-active,
.dropdown-fade-leave-active {
@apply transition-all duration-200;
}
.dropdown-fade-enter-from,
.dropdown-fade-leave-to {
@apply opacity-0 -translate-y-2;
}
</style>

View File

@@ -1,161 +0,0 @@
<script setup lang="ts">
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost";
export type ButtonSize = "sm" | "md" | "lg" | "xl";
const props = withDefaults(
defineProps<{
text: string;
isDisabled?: boolean;
variant?: ButtonVariant;
size?: ButtonSize;
icon?: string;
iconPosition?: "left" | "right";
fullWidth?: boolean;
loading?: boolean;
}>(),
{
isDisabled: false,
variant: "primary",
size: "xl",
iconPosition: "left",
fullWidth: true,
loading: false,
}
);
const emit = defineEmits(["buttonClicked"]);
const handleClick = () => {
if (!props.isDisabled && !props.loading) {
emit("buttonClicked");
}
};
</script>
<template>
<button
type="button"
:class="[
'button',
`variant-${variant}`,
`size-${size}`,
{ 'is-disabled': isDisabled || loading, 'full-width': fullWidth },
]"
:disabled="isDisabled || loading"
@click="handleClick"
>
<span v-if="loading" class="loader"></span>
<template v-else>
<img
v-if="icon && iconPosition === 'left'"
:src="icon"
:alt="`${text} icon`"
class="button-icon"
/>
<span class="button-text">{{ text }}</span>
<img
v-if="icon && iconPosition === 'right'"
:src="icon"
:alt="`${text} icon`"
class="button-icon"
/>
</template>
</button>
</template>
<style scoped>
.button {
@apply rounded-lg font-semibold transition-all duration-200 cursor-pointer flex items-center justify-center gap-2;
}
.button:hover:not(.is-disabled) {
@apply transform scale-[1.02];
}
.button.is-disabled {
@apply opacity-70 cursor-not-allowed;
}
.button.full-width {
@apply w-full;
}
/* Variantes */
.variant-primary {
@apply bg-amber-400 text-gray-900 border-2 border-amber-400;
}
.variant-primary:hover:not(.is-disabled) {
@apply bg-amber-500 border-amber-500;
}
.variant-secondary {
@apply bg-gray-200 text-gray-900 border-2 border-gray-300;
}
.variant-secondary:hover:not(.is-disabled) {
@apply bg-gray-300 border-gray-400;
}
.variant-outline {
@apply bg-transparent text-gray-900 border-2 border-amber-400;
}
.variant-outline:hover:not(.is-disabled) {
@apply bg-amber-400/10;
}
.variant-ghost {
@apply bg-transparent text-gray-900 border-2 border-transparent;
}
.variant-ghost:hover:not(.is-disabled) {
@apply bg-gray-100;
}
/* Tamanhos */
.size-sm {
@apply px-2 py-1 text-xs;
}
.size-sm .button-icon {
@apply w-3 h-3;
}
.size-md {
@apply px-3 py-2 text-sm;
}
.size-md .button-icon {
@apply w-4 h-4;
}
.size-lg {
@apply px-4 py-3 text-base;
}
.size-lg .button-icon {
@apply w-5 h-5;
}
.size-xl {
@apply p-4 text-base;
}
.size-xl .button-icon {
@apply w-5 h-5;
}
.button-icon {
@apply flex-shrink-0;
}
.button-text {
@apply font-semibold;
}
/* Loader animation */
.loader {
@apply w-5 h-5 border-2 border-gray-900 border-t-transparent rounded-full animate-spin;
}
</style>

View File

@@ -1,115 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps({
isRedirectModal: Boolean,
});
const modalColor = ref<string>("white");
const modalHeight = ref<string>("250px");
const pFontSize = ref<string>("16px");
if (props.isRedirectModal) {
modalColor.value = "rgba(251, 191, 36, 1)";
modalHeight.value = "150px";
pFontSize.value = "20px";
}
</script>
<template>
<div>
<div
class="modal-overlay inset-0 fixed hidden md:block justify-center backdrop-blur-sm sm:backdrop-blur-none"
v-if="!isRedirectModal"
>
<div class="modal px-5 text-center">
<p
class="text-black tracking-tighter leading-tight my-6 mx-2 text-justify"
>
<strong>ATENÇÃO!</strong>
A transação será processada após efetuado o pagamento do Pix. Caso
contrário não conseguiremos comprovar o seu depósito e não será
possível transferir os tokens para sua carteira.
</p>
<button
@click="$emit('close-modal')"
class="border-2 border-solid border-amber-400 mt-2"
>
Entendi
</button>
</div>
</div>
<div
class="modal-overlay inset-0 fixed justify-center backdrop-blur-sm"
v-if="isRedirectModal"
>
<div class="modal px-5 text-center">
<p
class="text-black text-lg tracking-tighter leading-tight my-6 mx-2 text-justify font-semibold"
>
Retomar a última compra?
</p>
<div class="flex justify-around items-center px-2">
<button
@click="$emit('close-modal')"
class="border-2 border-solid border-white-400 mt-2 font-semibold"
>
Não
</button>
<button
@click="$emit('go-to-lock')"
class="border-2 border-solid border-white-400 mt-2 font-semibold"
>
Sim
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
display: flex !important;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
.modal {
background-color: v-bind(modalColor);
height: v-bind(modalHeight);
width: 300px;
border-radius: 10px;
}
.close {
cursor: pointer;
}
.close-img {
width: 25px;
}
.check {
width: 150px;
}
h6 {
font-weight: 500;
font-size: 28px;
margin: 20px 0;
}
p {
font-size: v-bind(pFontSize);
}
button {
width: 100px;
height: 40px;
color: black;
font-size: 14px;
border-radius: 10px;
}
</style>

View File

@@ -1,248 +0,0 @@
<script setup lang="ts" generic="T">
import { ref, computed } from "vue";
import { onClickOutside } from "@vueuse/core";
import ChevronDown from "@/assets/chevronDown.svg";
export interface DropdownItem<T = any> {
value: T;
label: string;
icon?: string;
disabled?: boolean;
}
const props = withDefaults(
defineProps<{
items: DropdownItem<T>[];
modelValue: T;
placeholder?: string;
searchable?: boolean;
disabled?: boolean;
size?: "sm" | "md" | "lg";
showIcon?: boolean;
}>(),
{
placeholder: "Selecione...",
searchable: false,
disabled: false,
size: "md",
showIcon: true,
}
);
const emit = defineEmits<{
"update:modelValue": [value: T];
change: [value: T];
}>();
const isOpen = ref(false);
const searchQuery = ref("");
const dropdownRef = ref<HTMLElement | null>(null);
const selectedItem = computed(() => {
return props.items.find((item) => item.value === props.modelValue);
});
const filteredItems = computed(() => {
if (!props.searchable || !searchQuery.value) {
return props.items;
}
const query = searchQuery.value.toLowerCase();
return props.items.filter((item) =>
item.label.toLowerCase().includes(query)
);
});
const toggleDropdown = () => {
if (!props.disabled) {
isOpen.value = !isOpen.value;
if (!isOpen.value) {
searchQuery.value = "";
}
}
};
const selectItem = (item: DropdownItem<T>) => {
if (!item.disabled) {
emit("update:modelValue", item.value);
emit("change", item.value);
isOpen.value = false;
searchQuery.value = "";
}
};
onClickOutside(dropdownRef, () => {
isOpen.value = false;
searchQuery.value = "";
});
</script>
<template>
<div ref="dropdownRef" class="dropdown-container">
<button
type="button"
:class="[
'dropdown-trigger',
`size-${size}`,
{ disabled: disabled, open: isOpen },
]"
@click="toggleDropdown"
>
<img
v-if="selectedItem?.icon && showIcon"
:src="selectedItem.icon"
:alt="selectedItem.label"
class="item-icon"
/>
<span class="selected-text">
{{ selectedItem?.label || placeholder }}
</span>
<ChevronDown
class="chevron"
:class="{ rotated: isOpen }"
/>
</button>
<transition name="dropdown-fade">
<div v-if="isOpen" class="dropdown-menu">
<input
v-if="searchable"
v-model="searchQuery"
type="text"
class="search-input"
placeholder="Buscar..."
@click.stop
/>
<div class="items-container">
<div
v-for="item in filteredItems"
:key="String(item.value)"
:class="[
'dropdown-item',
{
selected: item.value === modelValue,
disabled: item.disabled,
},
]"
@click="selectItem(item)"
>
<img
v-if="item.icon && showIcon"
:src="item.icon"
:alt="item.label"
class="item-icon"
/>
<span class="item-label">{{ item.label }}</span>
</div>
<div v-if="filteredItems.length === 0" class="no-results">
Nenhum resultado encontrado
</div>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.dropdown-container {
@apply relative inline-block;
}
.dropdown-trigger {
@apply flex items-center gap-2 bg-gray-300 hover:bg-gray-200 rounded-3xl transition-colors cursor-pointer border-none outline-none;
}
.dropdown-trigger:focus {
@apply outline-2 outline-indigo-800;
}
.dropdown-trigger.disabled {
@apply opacity-50 cursor-not-allowed;
}
.dropdown-trigger.disabled:hover {
@apply bg-gray-300;
}
.size-sm {
@apply px-2 py-1 text-sm;
}
.size-md {
@apply px-3 py-2 text-base;
}
.size-lg {
@apply px-4 py-3 text-lg;
}
.item-icon {
@apply sm:w-fit w-4 flex-shrink-0;
}
.selected-text {
@apply text-gray-900 font-medium min-w-fit;
}
.chevron {
@apply transition-transform duration-300 invert pr-1;
}
.chevron.rotated {
@apply rotate-180;
}
.dropdown-menu {
@apply absolute right-0 mt-2 bg-white rounded-xl border border-gray-300 shadow-md z-50 min-w-max w-full;
}
.search-input {
@apply w-full px-4 py-3 border-b border-gray-200 outline-none text-gray-900;
}
.search-input:focus {
@apply border-indigo-800;
}
.items-container {
@apply max-h-64 overflow-y-auto;
}
.dropdown-item {
@apply flex items-center gap-2 px-4 py-4 cursor-pointer hover:bg-gray-300 transition-colors text-gray-900 font-semibold text-sm;
}
.dropdown-item.selected {
@apply bg-gray-100;
}
.dropdown-item.disabled {
@apply opacity-50 cursor-not-allowed;
}
.dropdown-item.disabled:hover {
@apply bg-transparent;
}
.item-label {
@apply text-end;
}
.no-results {
@apply px-4 py-6 text-center text-gray-500 text-sm;
}
/* Animação */
.dropdown-fade-enter-active,
.dropdown-fade-leave-active {
@apply transition-all duration-200;
}
.dropdown-fade-enter-from,
.dropdown-fade-leave-to {
@apply opacity-0 -translate-y-2;
}
</style>

View File

@@ -1,55 +0,0 @@
<script setup lang="ts">
export type ErrorType = "error" | "warning" | "info";
const props = withDefaults(
defineProps<{
message: string;
type?: ErrorType;
centered?: boolean;
icon?: boolean;
}>(),
{
type: "error",
centered: true,
icon: false,
}
);
const colorClasses = {
error: "text-red-500",
warning: "text-amber-500",
info: "text-blue-500",
};
</script>
<template>
<div :class="['error-message-container', { centered: centered }]">
<div :class="['error-message', colorClasses[type]]">
<span v-if="icon" class="icon"></span>
<span class="message">{{ message }}</span>
</div>
</div>
</template>
<style scoped>
.error-message-container {
@apply flex w-full;
}
.error-message-container.centered {
@apply justify-center;
}
.error-message {
@apply font-normal text-sm flex items-center gap-2;
}
.icon {
@apply text-base;
}
.message {
@apply leading-tight;
}
</style>

View File

@@ -1,55 +0,0 @@
<script setup lang="ts">
export type FormCardPadding = "sm" | "md" | "lg";
const props = withDefaults(
defineProps<{
padding?: FormCardPadding;
fullWidth?: boolean;
noBorder?: boolean;
}>(),
{
padding: "md",
fullWidth: true,
noBorder: false,
}
);
</script>
<template>
<div
:class="[
'form-card',
`padding-${padding}`,
{ 'full-width': fullWidth, 'no-border': noBorder },
]"
>
<slot></slot>
</div>
</template>
<style scoped>
.form-card {
@apply flex flex-col bg-white rounded-lg;
}
.form-card:not(.no-border) {
@apply border-y;
}
.form-card.full-width {
@apply w-full;
}
.padding-sm {
@apply px-4 py-3;
}
.padding-md {
@apply sm:px-10 px-6 py-5;
}
.padding-lg {
@apply px-12 py-8;
}
</style>

View File

@@ -1,147 +0,0 @@
<script setup lang="ts">
export type IconButtonVariant = "primary" | "secondary" | "outline" | "ghost";
export type IconButtonSize = "sm" | "md" | "lg";
export type IconPosition = "left" | "right";
const props = withDefaults(
defineProps<{
text: string;
icon?: string;
variant?: IconButtonVariant;
size?: IconButtonSize;
iconPosition?: IconPosition;
disabled?: boolean;
fullWidth?: boolean;
}>(),
{
variant: "outline",
size: "md",
iconPosition: "left",
disabled: false,
fullWidth: false,
}
);
const emit = defineEmits<{
click: [];
}>();
const handleClick = () => {
if (!props.disabled) {
emit("click");
}
};
</script>
<template>
<button
type="button"
:class="[
'icon-button',
`variant-${variant}`,
`size-${size}`,
{ 'is-disabled': disabled, 'full-width': fullWidth },
]"
:disabled="disabled"
@click="handleClick"
>
<img
v-if="icon && iconPosition === 'left'"
:src="icon"
:alt="`${text} icon`"
class="button-icon"
/>
<span class="button-text">{{ text }}</span>
<img
v-if="icon && iconPosition === 'right'"
:src="icon"
:alt="`${text} icon`"
class="button-icon"
/>
</button>
</template>
<style scoped>
.icon-button {
@apply flex items-center justify-center gap-2 font-medium rounded-lg transition-all duration-200 cursor-pointer;
}
.icon-button:hover:not(.is-disabled) {
@apply transform scale-[1.02];
}
.icon-button.is-disabled {
@apply opacity-60 cursor-not-allowed;
}
.icon-button.full-width {
@apply w-full;
}
/* Variantes */
.variant-primary {
@apply bg-amber-400 text-gray-900 border-2 border-amber-400;
}
.variant-primary:hover:not(.is-disabled) {
@apply bg-amber-500 border-amber-500;
}
.variant-secondary {
@apply bg-gray-200 text-gray-900 border-2 border-gray-300;
}
.variant-secondary:hover:not(.is-disabled) {
@apply bg-gray-300 border-gray-400;
}
.variant-outline {
@apply bg-transparent text-gray-900 border-2 border-amber-300;
}
.variant-outline:hover:not(.is-disabled) {
@apply bg-amber-300/10;
}
.variant-ghost {
@apply bg-transparent text-gray-900 border-2 border-transparent;
}
.variant-ghost:hover:not(.is-disabled) {
@apply bg-gray-100;
}
/* Tamanhos */
.size-sm {
@apply px-2 py-1 text-xs;
}
.size-sm .button-icon {
@apply w-3 h-3;
}
.size-md {
@apply px-3 py-2 text-sm;
}
.size-md .button-icon {
@apply w-4 h-4;
}
.size-lg {
@apply px-4 py-3 text-base;
}
.size-lg .button-icon {
@apply w-5 h-5;
}
.button-text {
@apply font-semibold;
}
.button-icon {
@apply flex-shrink-0;
}
</style>

View File

@@ -1,91 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
const props = withDefaults(
defineProps<{
text: string;
placement?: "top" | "bottom" | "left" | "right";
iconSrc?: string;
showOnHover?: boolean;
}>(),
{
placement: "right",
iconSrc: "",
showOnHover: true,
}
);
const showTooltip = ref<boolean>(false);
const reference = ref<HTMLElement | null>(null);
const floating = ref<HTMLElement | null>(null);
const floatingArrow = ref(null);
onMounted(() => {
useFloating(reference, floating, {
placement: props.placement,
middleware: [offset(10), flip(), shift(), arrow({ element: floatingArrow })],
});
});
const handleMouseOver = () => {
if (props.showOnHover) {
showTooltip.value = true;
}
};
const handleMouseOut = () => {
if (props.showOnHover) {
showTooltip.value = false;
}
};
const toggleTooltip = () => {
if (!props.showOnHover) {
showTooltip.value = !showTooltip.value;
}
};
</script>
<template>
<div class="info-tooltip-container">
<img
:src="iconSrc || '/src/assets/info.svg'"
alt="info icon"
class="info-icon"
ref="reference"
@mouseover="handleMouseOver"
@mouseout="handleMouseOut"
@click="toggleTooltip"
/>
<div
v-if="showTooltip"
role="tooltip"
ref="floating"
class="tooltip-content"
>
{{ text }}
</div>
</div>
</template>
<style scoped>
.info-tooltip-container {
@apply relative inline-block;
}
.info-icon {
@apply cursor-pointer transition-opacity hover:opacity-70;
}
.tooltip-content {
@apply bg-white text-gray-900 font-medium text-xs md:text-sm px-3 py-2 rounded border-2 border-emerald-500 z-50 max-w-xs shadow-lg;
}
@media screen and (max-width: 640px) {
.tooltip-content {
display: none;
}
}
</style>

View File

@@ -1,60 +0,0 @@
<script setup lang="ts">
import SpinnerComponent from "./SpinnerComponent.vue";
const props = withDefaults(
defineProps<{
message?: string;
size?: "sm" | "md" | "lg";
centered?: boolean;
inline?: boolean;
}>(),
{
message: "Carregando...",
size: "md",
centered: true,
inline: false,
}
);
const sizeMap = {
sm: { spinner: "4", text: "text-sm" },
md: { spinner: "6", text: "text-base" },
lg: { spinner: "8", text: "text-lg" },
};
</script>
<template>
<div
:class="[
'loading-state',
{ centered: centered, inline: inline },
]"
>
<span v-if="message" :class="['loading-message', sizeMap[size].text]">
{{ message }}
</span>
<SpinnerComponent
:width="sizeMap[size].spinner"
:height="sizeMap[size].spinner"
/>
</div>
</template>
<style scoped>
.loading-state {
@apply flex items-center gap-2;
}
.loading-state.centered {
@apply justify-center;
}
.loading-state.inline {
@apply inline-flex;
}
.loading-message {
@apply text-gray-900 font-normal;
}
</style>

View File

@@ -1,72 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import { getNetworkImage } from "@/utils/imagesPath";
import type { NetworkConfig } from "@/model/NetworkEnum";
const props = withDefaults(
defineProps<{
networks: NetworkConfig[];
size?: "sm" | "md" | "lg";
showLabel?: boolean;
}>(),
{
size: "md",
showLabel: false,
}
);
const sizeMap = {
sm: 16,
md: 24,
lg: 32,
};
const networkData = computed(() => {
return props.networks.map((network) => ({
network,
image: getNetworkImage(network.name),
name: network.name,
}));
});
</script>
<template>
<div class="network-badges">
<div
v-for="data in networkData"
:key="data.network.id"
class="network-badge"
:title="data.name"
>
<img
:alt="`${data.name} logo`"
:src="data.image"
:width="sizeMap[size]"
:height="sizeMap[size]"
class="network-icon"
/>
<span v-if="showLabel" class="network-label">
{{ data.name }}
</span>
</div>
</div>
</template>
<style scoped>
.network-badges {
@apply flex gap-2 items-center;
}
.network-badge {
@apply flex items-center gap-1;
}
.network-icon {
@apply flex-shrink-0;
}
.network-label {
@apply text-sm font-medium text-gray-900;
}
</style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import { Networks } from "@/config/networks";
import type { NetworkConfig } from "@/model/NetworkEnum";
import { getNetworkImage } from "@/utils/imagesPath";
import Dropdown, { type DropdownItem } from "./Dropdown.vue";
const props = withDefaults(
defineProps<{
modelValue: NetworkConfig;
disabled?: boolean;
size?: "sm" | "md" | "lg";
availableNetworks?: NetworkConfig[];
}>(),
{
disabled: false,
size: "md",
}
);
const emit = defineEmits<{
"update:modelValue": [value: NetworkConfig];
change: [value: NetworkConfig];
}>();
const networkItems = computed((): DropdownItem<NetworkConfig>[] => {
return Object.values(Networks).map((network) => ({
value: network,
label: network.name,
icon: getNetworkImage(network.name),
}));
});
const handleChange = (value: NetworkConfig) => {
emit("update:modelValue", value);
emit("change", value);
};
</script>
<template>
<Dropdown
:model-value="modelValue"
:items="networkItems"
:disabled="disabled"
:size="size"
:show-icon="true"
@update:model-value="handleChange"
/>
</template>

View File

@@ -1,66 +0,0 @@
<script setup lang="ts">
export type HeaderSize = "sm" | "md" | "lg";
const props = withDefaults(
defineProps<{
title: string;
subtitle?: string;
size?: HeaderSize;
centered?: boolean;
}>(),
{
size: "lg",
centered: true,
}
);
</script>
<template>
<div
:class="['page-header', `size-${size}`, { centered: centered }]"
>
<h1 class="title text-white font-extrabold">
{{ title }}
</h1>
<p v-if="subtitle" class="subtitle text-white font-medium">
{{ subtitle }}
</p>
<slot></slot>
</div>
</template>
<style scoped>
.page-header {
@apply flex flex-col gap-4;
}
.page-header.centered {
@apply items-center justify-center text-center;
}
/* Tamanhos */
.size-sm .title {
@apply sm:text-2xl text-xl sm:max-w-[20rem] max-w-[16rem];
}
.size-sm .subtitle {
@apply sm:text-sm text-xs sm:max-w-[18rem] max-w-[14rem];
}
.size-md .title {
@apply sm:text-4xl text-2xl sm:max-w-[28rem] max-w-[22rem];
}
.size-md .subtitle {
@apply sm:text-base text-sm sm:max-w-[26rem] max-w-[20rem];
}
.size-lg .title {
@apply sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem];
}
.size-lg .subtitle {
@apply sm:text-base text-sm sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide;
}
</style>

View File

@@ -1,50 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
export type StatusType = "open" | "expired" | "completed" | "pending";
const props = defineProps<{
status: StatusType;
customText?: string;
}>();
const statusConfig = computed(() => {
const configs: Record<StatusType, { text: string; color: string }> = {
open: {
text: "Em Aberto",
color: "bg-amber-300",
},
expired: {
text: "Expirado",
color: "bg-[#94A3B8]",
},
completed: {
text: "Finalizado",
color: "bg-emerald-300",
},
pending: {
text: "Pendente",
color: "bg-gray-300",
},
};
return configs[props.status];
});
const displayText = computed(() => {
return props.customText || statusConfig.value.text;
});
</script>
<template>
<div :class="[statusConfig.color, 'status-badge']">
{{ displayText }}
</div>
</template>
<style scoped>
.status-badge {
@apply text-xs sm:text-base font-medium text-gray-900 rounded-lg text-center px-2 py-1;
}
</style>

View File

@@ -1,83 +0,0 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import { useOnboard } from "@web3-onboard/vue";
import { Networks } from "@/config/networks";
import { useUser } from "@/composables/useUser";
const { connectedWallet } = useOnboard();
const user = useUser();
const { network } = user;
const isWrongNetwork = ref(false);
const targetNetworkName = computed(() => network.value.name);
const checkNetwork = () => {
if (connectedWallet.value) {
const chainId = connectedWallet.value.chains[0].id;
isWrongNetwork.value = Number(chainId) !== network.value.id;
} else {
isWrongNetwork.value = false; // No wallet connected yet
}
};
const switchNetwork = async () => {
try {
if (connectedWallet.value && connectedWallet.value.provider) {
let chainId = network.value.id.toString(16);
await connectedWallet.value.provider.request({
method: "wallet_switchEthereumChain",
params: [
{
chainId: `0x${chainId}`,
},
],
});
}
} catch (error) {
console.error("Failed to switch network:", error);
}
};
onMounted(checkNetwork);
watch(connectedWallet, checkNetwork);
watch(network, checkNetwork, { immediate: true });
</script>
<template>
<transition name="slide-up" appear>
<div
v-if="isWrongNetwork"
class="fixed bottom-0 left-0 right-0 bg-red-500 text-white p-4 flex justify-between items-center z-50"
>
<div>
<span class="font-bold">Wrong network!</span>
<span> Please switch to {{ targetNetworkName }}.</span>
</div>
<button
@click="switchNetwork"
class="bg-white text-red-500 px-4 py-2 rounded font-bold hover:bg-gray-100 transition-colors"
>
Switch Network
</button>
</div>
</transition>
</template>
<style scoped>
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(100%);
opacity: 0;
}
.slide-up-enter-to,
.slide-up-leave-from {
transform: translateY(0);
opacity: 1;
}
</style>

View File

@@ -1,48 +0,0 @@
<script setup lang="ts">
import { computed } from "vue";
import { TokenEnum } from "@/model/NetworkEnum";
import { getTokenImage } from "@/utils/imagesPath";
import Dropdown, { type DropdownItem } from "./Dropdown.vue";
const props = withDefaults(
defineProps<{
modelValue: TokenEnum;
disabled?: boolean;
size?: "sm" | "md" | "lg";
}>(),
{
disabled: false,
size: "md",
}
);
const emit = defineEmits<{
"update:modelValue": [value: TokenEnum];
change: [value: TokenEnum];
}>();
const tokenItems = computed((): DropdownItem<TokenEnum>[] => {
return Object.values(TokenEnum).map((token) => ({
value: token,
label: token,
icon: getTokenImage(token),
}));
});
const handleChange = (value: TokenEnum) => {
emit("update:modelValue", value);
emit("change", value);
};
</script>
<template>
<Dropdown
:model-value="modelValue"
:items="tokenItems"
:disabled="disabled"
:size="size"
:show-icon="true"
@update:model-value="handleChange"
/>
</template>

View File

@@ -1,152 +0,0 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { onClickOutside } from "@vueuse/core";
import CustomButton from "./CustomButton.vue";
const props = withDefaults(
defineProps<{
walletAddress: string | null;
variant?: "primary" | "secondary" | "outline";
showMenu?: boolean;
}>(),
{
variant: "primary",
showMenu: true,
}
);
const emit = defineEmits<{
connect: [];
disconnect: [];
viewTransactions: [];
}>();
const menuOpen = ref(false);
const menuRef = ref<HTMLElement | null>(null);
const isConnected = computed(() => {
return !!props.walletAddress;
});
const formattedAddress = computed(() => {
if (!props.walletAddress) return "";
const address = props.walletAddress;
const length = address.length;
const start = address.substring(0, 5);
const end = address.substring(length - 4, length);
return `${start}...${end}`;
});
const handleConnect = () => {
emit("connect");
};
const handleDisconnect = () => {
menuOpen.value = false;
emit("disconnect");
};
const handleViewTransactions = () => {
menuOpen.value = false;
emit("viewTransactions");
};
const toggleMenu = () => {
if (isConnected.value && props.showMenu) {
menuOpen.value = !menuOpen.value;
}
};
onClickOutside(menuRef, () => {
menuOpen.value = false;
});
</script>
<template>
<div class="wallet-connect-container">
<CustomButton
v-if="!isConnected"
text="Conectar carteira"
:variant="variant"
@button-clicked="handleConnect"
/>
<div v-else ref="menuRef" class="wallet-connected">
<button
type="button"
class="wallet-button"
@click="toggleMenu"
>
<span class="wallet-address">{{ formattedAddress }}</span>
<div class="wallet-indicator"></div>
</button>
<transition name="menu-fade">
<div v-if="menuOpen && showMenu" class="wallet-menu">
<button
type="button"
class="menu-item"
@click="handleViewTransactions"
>
<span>Ver transações</span>
</button>
<button
type="button"
class="menu-item disconnect"
@click="handleDisconnect"
>
<span>Desconectar</span>
</button>
</div>
</transition>
</div>
</div>
</template>
<style scoped>
.wallet-connect-container {
@apply relative inline-block;
}
.wallet-connected {
@apply relative;
}
.wallet-button {
@apply flex items-center gap-3 px-4 py-2 bg-white border-2 border-amber-400 rounded-lg hover:bg-amber-50 transition-colors cursor-pointer;
}
.wallet-address {
@apply text-gray-900 font-semibold text-sm;
}
.wallet-indicator {
@apply w-2 h-2 bg-emerald-500 rounded-full;
}
.wallet-menu {
@apply absolute top-full right-0 mt-2 bg-white rounded-lg border border-gray-300 shadow-lg z-50 min-w-[200px] overflow-hidden;
}
.menu-item {
@apply w-full px-4 py-3 text-left text-gray-900 font-medium text-sm hover:bg-gray-100 transition-colors cursor-pointer border-none;
}
.menu-item.disconnect {
@apply text-red-500 hover:bg-red-50;
}
/* Animação */
.menu-fade-enter-active,
.menu-fade-leave-active {
@apply transition-all duration-200;
}
.menu-fade-enter-from,
.menu-fade-leave-to {
@apply opacity-0 -translate-y-2;
}
</style>

View File

@@ -1,475 +0,0 @@
import { NetworkConfig } from '@/model/NetworkEnum';
import { ref, computed, type Ref } from 'vue';
import { isTestnetEnvironment } from '@/config/networks';
import { sepolia, rootstock, rootstockTestnet } from "viem/chains";
export interface Transaction {
id: string;
type: 'deposit' | 'lock' | 'release' | 'return';
timestamp: string;
blockTimestamp: string;
seller?: string;
buyer?: string | null;
amount: string;
token: string;
blockNumber: string;
transactionHash: string;
}
export interface AnalyticsData {
totalVolume: string;
totalTransactions: string;
totalLocks: string;
totalDeposits: string;
totalReleases: string;
}
export function useGraphQL(network: Ref<NetworkConfig>) {
const searchAddress = ref('');
const selectedType = ref('all');
const loading = ref(false);
const error = ref<string | null>(null);
const analyticsLoading = ref(false);
const transactionsData = ref<Transaction[]>([]);
const analyticsData = ref<AnalyticsData>({
totalVolume: '0',
totalTransactions: '0',
totalLocks: '0',
totalDeposits: '0',
totalReleases: '0'
});
const getGraphQLUrl = () => {
const rskNetworkName = isTestnetEnvironment() ? rootstockTestnet.name : rootstock.name;
switch (network.value.name) {
case sepolia.name:
return import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL!;
case rskNetworkName:
return import.meta.env.VITE_RSK_SUBGRAPH_URL!;
default:
throw new Error(`Unsupported network: ${network.value.name}`);
}
};
const executeQuery = async (query: string, variables: any = {}) => {
const url = getGraphQLUrl();
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.errors) {
throw new Error(data.errors[0]?.message || 'GraphQL error');
}
return data.data;
} catch (err) {
console.error('GraphQL query error:', err);
throw err;
}
};
const fetchAllActivity = async () => {
loading.value = true;
error.value = null;
const query = `
query GetAllActivity($first: Int = 50) {
depositAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
id
seller
token
amount
blockNumber
blockTimestamp
transactionHash
}
depositWithdrawns(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
id
seller
token
amount
blockNumber
blockTimestamp
transactionHash
}
lockAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
id
buyer
lockID
seller
amount
blockNumber
blockTimestamp
transactionHash
}
lockReleaseds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
id
buyer
lockId
amount
blockNumber
blockTimestamp
transactionHash
}
lockReturneds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
id
buyer
lockId
blockNumber
blockTimestamp
transactionHash
}
}
`;
try {
const data = await executeQuery(query, { first: 50 });
transactionsData.value = processActivityData(data);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch transactions';
} finally {
loading.value = false;
}
};
const fetchUserActivity = async (userAddress: string) => {
loading.value = true;
error.value = null;
const query = `
query GetUserActivity($userAddress: String!, $first: Int = 50) {
depositAddeds(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
id
seller
token
amount
blockNumber
blockTimestamp
transactionHash
}
depositWithdrawns(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
id
seller
token
amount
blockNumber
blockTimestamp
transactionHash
}
lockAddeds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
id
buyer
lockID
seller
amount
blockNumber
blockTimestamp
transactionHash
}
lockReleaseds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
id
buyer
lockId
amount
blockNumber
blockTimestamp
transactionHash
}
lockReturneds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
id
buyer
lockId
blockNumber
blockTimestamp
transactionHash
}
}
`;
try {
const data = await executeQuery(query, { userAddress, first: 50 });
transactionsData.value = processActivityData(data);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch user transactions';
} finally {
loading.value = false;
}
};
const clearData = () => {
transactionsData.value = [];
analyticsData.value = {
totalVolume: '0',
totalTransactions: '0',
totalLocks: '0',
totalDeposits: '0',
totalReleases: '0'
};
};
const fetchAnalytics = async () => {
analyticsLoading.value = true;
const query = `
query GetAnalytics {
depositAddeds(first: 1000) {
amount
blockTimestamp
}
depositWithdrawns(first: 1000) {
amount
blockTimestamp
}
lockAddeds(first: 1000) {
amount
blockTimestamp
}
lockReleaseds(first: 1000) {
amount
blockTimestamp
}
lockReturneds(first: 1000) {
blockTimestamp
}
}
`;
try {
const data = await executeQuery(query);
analyticsData.value = processAnalyticsData(data);
} catch (err) {
console.error('Failed to fetch analytics:', err);
} finally {
analyticsLoading.value = false;
}
};
const processActivityData = (data: any): Transaction[] => {
if (!data) return [];
const activities: Transaction[] = [];
if (data.depositAddeds) {
data.depositAddeds.forEach((deposit: any) => {
activities.push({
id: deposit.id,
blockNumber: deposit.blockNumber,
blockTimestamp: deposit.blockTimestamp,
transactionHash: deposit.transactionHash,
type: 'deposit',
seller: deposit.seller,
buyer: undefined,
amount: deposit.amount,
token: deposit.token,
timestamp: formatTimestamp(deposit.blockTimestamp)
});
});
}
if (data.depositWithdrawns) {
data.depositWithdrawns.forEach((withdrawal: any) => {
activities.push({
id: withdrawal.id,
blockNumber: withdrawal.blockNumber,
blockTimestamp: withdrawal.blockTimestamp,
transactionHash: withdrawal.transactionHash,
type: 'deposit', // Treat as deposit withdrawal
seller: withdrawal.seller,
buyer: undefined,
amount: withdrawal.amount,
token: withdrawal.token,
timestamp: formatTimestamp(withdrawal.blockTimestamp)
});
});
}
if (data.lockAddeds) {
data.lockAddeds.forEach((lock: any) => {
activities.push({
id: lock.id,
blockNumber: lock.blockNumber,
blockTimestamp: lock.blockTimestamp,
transactionHash: lock.transactionHash,
type: 'lock',
seller: lock.seller,
buyer: lock.buyer,
amount: lock.amount,
token: lock.token,
timestamp: formatTimestamp(lock.blockTimestamp)
});
});
}
if (data.lockReleaseds) {
data.lockReleaseds.forEach((release: any) => {
activities.push({
id: release.id,
blockNumber: release.blockNumber,
blockTimestamp: release.blockTimestamp,
transactionHash: release.transactionHash,
type: 'release',
seller: undefined, // Release doesn't have seller info
buyer: release.buyer,
amount: release.amount,
token: 'BRZ', // Default token
timestamp: formatTimestamp(release.blockTimestamp)
});
});
}
if (data.lockReturneds) {
data.lockReturneds.forEach((returnTx: any) => {
activities.push({
id: returnTx.id,
blockNumber: returnTx.blockNumber,
blockTimestamp: returnTx.blockTimestamp,
transactionHash: returnTx.transactionHash,
type: 'return',
seller: undefined, // Return doesn't have seller info
buyer: returnTx.buyer,
amount: '0', // Return doesn't have amount
token: 'BRZ', // Default token
timestamp: formatTimestamp(returnTx.blockTimestamp)
});
});
}
return activities.sort((a, b) => parseInt(b.blockTimestamp) - parseInt(a.blockTimestamp));
};
const formatTimestamp = (timestamp: string): string => {
const now = Date.now() / 1000;
const diff = now - parseInt(timestamp);
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
return `${Math.floor(diff / 86400)} days ago`;
};
const formatAmount = (amount: string): string => {
const num = parseFloat(amount);
if (num >= 1000000000000000) return `${(num / 1000000000000000).toFixed(1)}Q`;
if (num >= 1000000000000) return `${(num / 1000000000000).toFixed(1)}T`;
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`;
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
if (num < 1) return num.toFixed(4);
return num.toFixed(2);
};
const processAnalyticsData = (data: any): AnalyticsData => {
if (!data) {
return {
totalVolume: '0',
totalTransactions: '0',
totalLocks: '0',
totalDeposits: '0',
totalReleases: '0'
};
}
let totalVolume = 0;
let totalTransactions = 0;
let totalLocks = 0;
let totalDeposits = 0;
let totalReleases = 0;
if (data.depositAddeds) {
data.depositAddeds.forEach((deposit: any) => {
totalVolume += parseFloat(deposit.amount || '0');
totalTransactions++;
totalDeposits++;
});
}
if (data.depositWithdrawns) {
data.depositWithdrawns.forEach((withdrawal: any) => {
totalVolume += parseFloat(withdrawal.amount || '0');
totalTransactions++;
});
}
if (data.lockAddeds) {
data.lockAddeds.forEach((lock: any) => {
totalVolume += parseFloat(lock.amount || '0');
totalTransactions++;
totalLocks++;
});
}
if (data.lockReleaseds) {
data.lockReleaseds.forEach((release: any) => {
totalVolume += parseFloat(release.amount || '0');
totalTransactions++;
totalReleases++;
});
}
if (data.lockReturneds) {
data.lockReturneds.forEach((returnTx: any) => {
totalTransactions++;
});
}
const result = {
totalVolume: formatAmount(totalVolume.toString()),
totalTransactions: totalTransactions.toString(),
totalLocks: totalLocks.toString(),
totalDeposits: totalDeposits.toString(),
totalReleases: totalReleases.toString()
};
return result;
};
const filteredTransactions = computed(() => {
let filtered = transactionsData.value;
if (selectedType.value !== 'all') {
filtered = filtered.filter(tx => tx.type === selectedType.value);
}
if (searchAddress.value) {
const searchLower = searchAddress.value.toLowerCase();
filtered = filtered.filter(tx =>
tx.seller?.toLowerCase().includes(searchLower) ||
tx.buyer?.toLowerCase().includes(searchLower)
);
}
return filtered;
});
return {
searchAddress,
selectedType,
transactions: filteredTransactions,
analytics: analyticsData,
loading,
error,
analyticsLoading,
fetchAllActivity,
fetchUserActivity,
fetchAnalytics,
clearData
};
}

View File

@@ -1,125 +0,0 @@
import { ref } from "vue";
import type { ValidDeposit } from "@/model/ValidDeposit";
import type { Participant } from "../utils/bbPay";
import type { Address } from "viem"
import { DEFAULT_NETWORK, Networks } from "@/config/networks";
import { TokenEnum, NetworkConfig } from "@/model/NetworkEnum"
const walletAddress = ref<Address | null>(null);
const balance = ref("");
const network = ref(DEFAULT_NETWORK);
const selectedToken = ref<TokenEnum>(TokenEnum.BRZ);
const loadingLock = ref(false);
const sellerView = ref(false);
const depositsValidList = ref<ValidDeposit[]>([]);
const loadingWalletTransactions = ref(false);
const loadingNetworkLiquidity = ref(false);
const seller = ref<Participant>({} as Participant);
const sellerId = ref("");
export function useUser() {
// Actions become regular functions
const setWalletAddress = (address: Address | null) => {
walletAddress.value = address;
};
const setBalance = (newBalance: string) => {
balance.value = newBalance;
};
const setSelectedToken = (token: TokenEnum) => {
selectedToken.value = token;
};
const setNetwork = (chain: NetworkConfig) => {
network.value = chain;
};
const setNetworkById = (id: string | number) => {
let chainId: number;
if (typeof id === 'string') {
// Parse hex string or number string to number
if (id.startsWith('0x')) {
chainId = parseInt(id, 16);
} else {
chainId = parseInt(id, 10);
}
} else {
chainId = id;
}
// Find network by chain ID
const chain = Object.values(Networks).find(n => n.id === chainId);
if (chain) {
network.value = chain;
}
};
const setLoadingLock = (isLoading: boolean) => {
loadingLock.value = isLoading;
};
const setSellerView = (view: boolean) => {
sellerView.value = view;
};
const setDepositsValidList = (deposits: ValidDeposit[]) => {
depositsValidList.value = deposits;
};
const setLoadingWalletTransactions = (isLoading: boolean) => {
loadingWalletTransactions.value = isLoading;
};
const setLoadingNetworkLiquidity = (isLoading: boolean) => {
loadingNetworkLiquidity.value = isLoading;
};
const setSeller = (newSeller: Participant) => {
seller.value = newSeller;
};
const setSellerId = (id: string) => {
sellerId.value = id;
};
// Getters become computed or regular functions
const getValidDepositByWalletAddress = (address: string) => {
return depositsValidList.value
.filter((deposit) => deposit.seller == address)
.sort((a, b) => b.blockNumber - a.blockNumber);
};
return {
// State
walletAddress,
balance,
network,
selectedToken,
loadingLock,
sellerView,
depositsValidList,
loadingWalletTransactions,
loadingNetworkLiquidity,
seller,
sellerId,
// Actions
setWalletAddress,
setBalance,
setSelectedToken,
setNetwork,
setNetworkById,
setLoadingLock,
setSellerView,
setDepositsValidList,
setLoadingWalletTransactions,
setLoadingNetworkLiquidity,
setSeller,
setSellerId,
// Getters
getValidDepositByWalletAddress,
};
}

View File

@@ -1,30 +0,0 @@
import { sepolia, rootstock, rootstockTestnet } from "viem/chains";
import { NetworkConfig } from "@/model/NetworkEnum"
// TODO: import addresses from p2pix-smart-contracts deployments
export const isTestnetEnvironment = () => {
return import.meta.env.VITE_ENVIRONMENT === 'testnet' ||
import.meta.env.NODE_ENV === 'development' ||
import.meta.env.MODE === 'development';
};
export const Networks: {[key:string]: NetworkConfig} = {
sepolia: { ...sepolia,
rpcUrls: { default: { http: [import.meta.env.VITE_SEPOLIA_API_URL]}},
contracts: { ...sepolia.contracts,
p2pix: { address: import.meta.env.VITE_SEPOLIA_P2PIX_ADDRESS } },
tokens: {
BRZ: { address: import.meta.env.VITE_SEPOLIA_TOKEN_ADDRESS } },
subgraphUrls: [import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL]
},
rootstock: { ...(isTestnetEnvironment() ? rootstockTestnet : rootstock),
rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL]}},
contracts: { ...(isTestnetEnvironment() ? rootstockTestnet.contracts : rootstock.contracts),
p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS } },
tokens: {
BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS } },
subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL]
},
};
export const DEFAULT_NETWORK = Networks.sepolia;

View File

@@ -1,12 +1,13 @@
import { createApp } from "vue"; import { createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
import { createPinia } from "pinia";
import "./assets/main.css"; import "./assets/main.css";
import "./assets/transitions.css";
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);
app.use(createPinia());
app.mount("#app"); app.mount("#app");

View File

@@ -1,8 +0,0 @@
export interface AppVersion {
tag: string;
ipfsHash: string;
releaseDate: string;
description?: string;
}

View File

@@ -1,5 +0,0 @@
export interface Bank {
COMPE: string;
ISPB: string;
longName: string;
}

View File

@@ -1,8 +0,0 @@
import type { Address } from "viem";
export enum LockStatus { // from DataTypes.sol
Inexistent = 0, // Uninitialized Lock
Active = 1, // Valid Lock
Expired = 2, // Expired Lock
Released = 3 // Already released Lock
}

View File

@@ -1,10 +1,5 @@
import type { Chain, ChainContract } from "viem"; export enum NetworkEnum {
ethereum = "Ethereum",
export enum TokenEnum { polygon = "Polygon",
BRZ = 'BRZ', rootstock = "Rootstock",
// BRX = 'BRX'
} }
export type NetworkConfig = Chain & {
tokens: Record<TokenEnum, ChainContract>,
subgraphUrls: string[]
};

View File

@@ -1,8 +1,6 @@
import { Address } from "viem"; import type { Pix } from "./Pix";
export type UnreleasedLock = { export type UnreleasedLock = {
lockID: bigint; lockID: string;
sellerAddress: Address; pix: Pix;
tokenAddress: Address;
amount: number;
}; };

View File

@@ -1,12 +1,8 @@
import type { Address } from "viem";
import type { NetworkConfig } from "@/model/NetworkEnum";
export type ValidDeposit = { export type ValidDeposit = {
token: Address; token: string;
blockNumber: number; blockNumber: number;
remaining: number; remaining: number;
seller: Address; seller: string;
participantID: string; pixKey: number;
network: NetworkConfig;
open?: boolean; open?: boolean;
}; };

View File

@@ -1,15 +1,11 @@
import type { LockStatus } from "@/model/LockStatus"
import type { Address } from "viem"
export type WalletTransaction = { export type WalletTransaction = {
token?: Address; token: string;
blockNumber: number; blockNumber: number;
blockTimestamp?: number;
amount: number; amount: number;
seller: string; seller: string;
buyer: string; buyer: string;
event: string; event: string;
lockStatus?: LockStatus; lockStatus: number;
transactionHash: string; transactionHash: string;
transactionID?: string; transactionID?: string;
}; };

121
src/model/mock/EventMock.ts Normal file
View File

@@ -0,0 +1,121 @@
import type { Event } from "ethers";
import { vi } from "vitest";
export const MockEvents: Event[] = [
{
blockNumber: 1,
blockHash: "0x8",
transactionIndex: 1,
removed: false,
address: "0x0",
data: "0x0",
topics: ["0x0", "0x0"],
transactionHash: "0x0",
logIndex: 1,
event: "DepositAdded",
eventSignature: "DepositAdded(address,uint256,address,uint256)",
args: [
"0x0",
{
type: "BigNumber",
hex: "0x00",
},
"0x0",
{
type: "BigNumber",
hex: "0x6c6b935b8bbd400000",
},
],
getBlock: vi.fn(),
removeListener: vi.fn(),
getTransaction: vi.fn(),
getTransactionReceipt: vi.fn(),
},
{
blockNumber: 2,
blockHash: "0x8",
transactionIndex: 2,
removed: false,
address: "0x0",
data: "0x0",
topics: ["0x0", "0x0"],
transactionHash: "0x0",
logIndex: 2,
event: "LockAdded",
eventSignature: "LockAdded(address,uint256,address,uint256)",
args: [
"0x0",
{
type: "BigNumber",
hex: "0x00",
},
"0x0",
{
type: "BigNumber",
hex: "0x6c6b935b8bbd400000",
},
],
getBlock: vi.fn(),
removeListener: vi.fn(),
getTransaction: vi.fn(),
getTransactionReceipt: vi.fn(),
},
{
blockNumber: 3,
blockHash: "0x8",
transactionIndex: 3,
removed: false,
address: "0x0",
data: "0x0",
topics: ["0x0", "0x0"],
transactionHash: "0x0",
logIndex: 3,
event: "LockReleased",
eventSignature: "LockReleased(address,uint256,address,uint256)",
args: [
"0x0",
{
type: "BigNumber",
hex: "0x00",
},
"0x0",
{
type: "BigNumber",
hex: "0x6c6b935b8bbd400000",
},
],
getBlock: vi.fn(),
removeListener: vi.fn(),
getTransaction: vi.fn(),
getTransactionReceipt: vi.fn(),
},
{
blockNumber: 4,
blockHash: "0x8",
transactionIndex: 4,
removed: false,
address: "0x0",
data: "0x0",
topics: ["0x0", "0x0"],
transactionHash: "0x0",
logIndex: 4,
event: "LockReleased",
eventSignature: "LockReleased(address,uint256,address,uint256)",
args: [
"0x0",
{
type: "BigNumber",
hex: "0x00",
},
"0x0",
{
type: "BigNumber",
hex: "0x6c6b935b8bbd400000",
},
],
getBlock: vi.fn(),
removeListener: vi.fn(),
getTransaction: vi.fn(),
getTransactionReceipt: vi.fn(),
},
];

View File

@@ -0,0 +1,39 @@
import type { ValidDeposit } from "../ValidDeposit";
export const MockValidDeposits: ValidDeposit[] = [
{
blockNumber: 1,
token: "1",
remaining: 70,
seller: "mockedSellerAddress",
pixKey: 123456789,
},
{
blockNumber: 2,
token: "2",
remaining: 200,
seller: "mockedSellerAddress",
pixKey: 123456789,
},
{
blockNumber: 3,
token: "3",
remaining: 1250,
seller: "mockedSellerAddress",
pixKey: 123456789,
},
{
blockNumber: 4,
token: "4",
remaining: 4000,
seller: "mockedSellerAddress",
pixKey: 123456789,
},
{
blockNumber: 5,
token: "5",
remaining: 2000,
seller: "mockedSellerAddress",
pixKey: 123456789,
},
];

View File

@@ -0,0 +1,54 @@
import type { WalletTransaction } from "../WalletTransaction";
export const MockWalletTransactions: WalletTransaction[] = [
{
blockNumber: 1,
token: "1",
amount: 70,
seller: "mockedSellerAddress",
buyer: "mockedBuyerAddress",
event: "Deposit",
lockStatus: 0,
transactionHash: "1",
},
{
blockNumber: 2,
token: "2",
amount: 200,
seller: "mockedSellerAddress",
buyer: "mockedBuyerAddress",
event: "Lock",
lockStatus: 1,
transactionHash: "2",
},
{
blockNumber: 3,
token: "3",
amount: 1250,
seller: "mockedSellerAddress",
buyer: "mockedBuyerAddress",
event: "Release",
lockStatus: 2,
transactionHash: "3",
},
{
blockNumber: 4,
token: "4",
amount: 4000,
seller: "mockedSellerAddress",
buyer: "mockedBuyerAddress",
event: "Deposit",
lockStatus: 0,
transactionHash: "4",
},
{
blockNumber: 5,
token: "5",
amount: 2000,
seller: "mockedSellerAddress",
buyer: "mockedBuyerAddress",
event: "Deposit",
lockStatus: 3,
transactionHash: "5",
},
];

View File

@@ -1,15 +1,11 @@
import { createRouter, createWebHistory, createWebHashHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import HomeView from "@/views/HomeView.vue"; import HomeView from "@/views/HomeView.vue";
import FaqView from "@/views/FaqView.vue"; import FaqView from "@/views/FaqView.vue";
import ManageBidsView from "@/views/ManageBidsView.vue"; import ManageBidsView from "@/views/ManageBidsView.vue";
import SellerView from "@/views/SellerView.vue"; import SellerView from "@/views/SellerView.vue";
import ExploreView from "@/views/ExploreView.vue";
import VersionsView from "@/views/VersionsView.vue";
const router = createRouter({ const router = createRouter({
history: import.meta.env.MODE === 'production' && import.meta.env.BASE_URL === './' history: createWebHistory(import.meta.env.BASE_URL),
? createWebHashHistory()
: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: "/", path: "/",
@@ -37,16 +33,6 @@ const router = createRouter({
name: "faq", name: "faq",
component: FaqView, component: FaqView,
}, },
{
path: "/explore",
name: "explore",
component: ExploreView,
},
{
path: "/versions",
name: "versions",
component: VersionsView,
},
], ],
}); });

14
src/services/index.ts Normal file
View File

@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import axios from "axios";
const defaultConfig = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
};
const api = axios.create({
...defaultConfig,
baseURL: import.meta.env.VITE_API_URL,
});
export default api;

62
src/store/ether.ts Normal file
View File

@@ -0,0 +1,62 @@
import { NetworkEnum } from "@/model/NetworkEnum";
import type { ValidDeposit } from "@/model/ValidDeposit";
import { defineStore } from "pinia";
export const useEtherStore = defineStore("ether", {
state: () => ({
walletAddress: "",
balance: "",
networkName: NetworkEnum.ethereum,
loadingLock: false,
sellerView: false,
// Depósitos válidos para compra SEPOLIA
depositsValidListSepolia: [] as ValidDeposit[],
// Depósitos válidos para compra MUMBAI
depositsValidListMumbai: [] as ValidDeposit[],
loadingWalletTransactions: false,
loadingNetworkLiquidity: false,
}),
actions: {
setWalletAddress(walletAddress: string) {
this.walletAddress = walletAddress;
},
setBalance(balance: string) {
this.balance = balance;
},
setNetworkName(networkName: NetworkEnum) {
this.networkName = networkName;
},
setLoadingLock(isLoadingLock: boolean) {
this.loadingLock = isLoadingLock;
},
setSellerView(sellerView: boolean) {
this.sellerView = sellerView;
},
setDepositsValidListSepolia(depositsValidList: ValidDeposit[]) {
this.depositsValidListSepolia = depositsValidList;
},
setDepositsValidListMumbai(depositsValidList: ValidDeposit[]) {
this.depositsValidListMumbai = depositsValidList;
},
setDepositsValidListRootstock(depositsValidList: ValidDeposit[]) {
this.depositsValidListRootstock = depositsValidList;
},
setLoadingWalletTransactions(isLoadingWalletTransactions: boolean) {
this.loadingWalletTransactions = isLoadingWalletTransactions;
},
setLoadingNetworkLiquidity(isLoadingNetworkLiquidity: boolean) {
this.loadingNetworkLiquidity = isLoadingNetworkLiquidity;
},
},
// Alterar para integrar com mumbai
getters: {
getValidDepositByWalletAddress: (state) => {
return (walletAddress: string) =>
state.depositsValidListSepolia
.filter((deposit) => deposit.seller == walletAddress)
.sort((a, b) => {
return b.blockNumber - a.blockNumber;
});
},
},
});

76
src/utils/QrCodePix.ts Normal file
View File

@@ -0,0 +1,76 @@
import qrcode from "qrcode";
import type { QRCodeToDataURLOptions } from "qrcode";
import { crc16ccitt } from "crc";
import type { Pix } from "@/model/Pix";
const pix = ({
pixKey,
merchantCity = "city",
merchantName = "name",
value,
message,
cep,
transactionId = "***",
currency = 986,
countryCode = "BR",
}: Pix) => {
const payloadKeyString = generatePixKey(pixKey, message);
const payload: string[] = [
formatEMV("00", "01"), //Payload Format Indicator
formatEMV("26", payloadKeyString), // Merchant Account Information
formatEMV("52", "0000"), //Merchant Category Code
formatEMV("53", String(currency)), // Transaction Currency
];
if (String(value) === "0") {
value = undefined;
}
if (value) {
payload.push(formatEMV("54", value.toFixed(2)));
}
payload.push(formatEMV("58", countryCode.toUpperCase())); // Country Code
payload.push(formatEMV("59", merchantName)); // Merchant Name
payload.push(formatEMV("60", merchantCity)); // Merchant City
if (cep) {
payload.push(formatEMV("61", cep)); // Postal Code
}
payload.push(formatEMV("62", formatEMV("05", transactionId))); // Additional Data Field Template
payload.push("6304");
const stringPayload = payload.join("");
const crcResult = crc16ccitt(stringPayload)
.toString(16)
.toUpperCase()
.padStart(4, "0");
const payloadPIX = `${stringPayload}${crcResult}`;
return {
payload: (): string => payloadPIX,
base64QrCode: (options?: QRCodeToDataURLOptions): Promise<string> =>
qrcode.toDataURL(payloadPIX, options),
};
};
const generatePixKey = (pixKey: string, message?: string): string => {
const payload: string[] = [
formatEMV("00", "BR.GOV.BCB.PIX"),
formatEMV("01", pixKey),
];
if (message) {
payload.push(formatEMV("02", message));
}
return payload.join("");
};
const formatEMV = (id: string, param: string): string => {
const len = param?.length?.toString().padStart(2, "0");
return `${id}${len}${param}`;
};
export { pix };

View File

@@ -0,0 +1,24 @@
import { it, expect, vi, type Mock } from "vitest";
import { debounce } from "../debounce";
vi.useFakeTimers();
describe("debounce function test", () => {
let mockFunction: Mock;
let debounceFunction: Function;
beforeEach(() => {
mockFunction = vi.fn();
debounceFunction = debounce(mockFunction, 1000);
});
it("debounce function will be executed just once", () => {
for (let i = 0; i < 100; i++) {
debounceFunction();
}
vi.runAllTimers();
expect(mockFunction).toBeCalledTimes(1);
});
});

View File

@@ -0,0 +1,12 @@
import { it, expect } from "vitest";
import { decimalCount } from "../decimalCount";
describe("decimalCount function test", () => {
it("decimalCount should return length 1 of decimal", () => {
expect(decimalCount("4.1")).toEqual(1);
});
it("decimalCount should return length 0 because no decimal found", () => {
expect(decimalCount("5")).toEqual(0);
});
});

View File

@@ -0,0 +1,25 @@
import { MockValidDeposits } from "@/model/mock/ValidDepositMock";
import { it, expect, vi } from "vitest";
import { verifyNetworkLiquidity } from "../networkLiquidity";
vi.useFakeTimers();
describe("verifyNetworkLiquidity function test", () => {
it("verifyNetworkLiquidity should return an element from valid deposit list when searching for other deposits", () => {
const liquidityElement = verifyNetworkLiquidity(
MockValidDeposits[0].remaining,
"strangeWalletAddress",
MockValidDeposits
);
expect(liquidityElement).toEqual(MockValidDeposits[0]);
});
it("verifyNetworkLiquidity should return undefined when all deposits on valid deposit list match connected wallet addres", () => {
const liquidityElement = verifyNetworkLiquidity(
MockValidDeposits[0].remaining,
MockValidDeposits[0].seller,
[MockValidDeposits[0]]
);
expect(liquidityElement).toEqual(undefined);
});
});

View File

@@ -1,75 +0,0 @@
export interface Participant {
offer: string;
chainID: number;
identification: string;
bankIspb?: string;
accountType: string;
account: string;
branch: string;
savingsVariation?: string;
}
export interface ParticipantWithID extends Participant {
id: string;
}
export interface Offer {
amount: number;
sellerId: string;
}
// Specs for BB Pay Sandbox
// https://apoio.developers.bb.com.br/sandbox/spec/665797498bb48200130fc32c
export const createParticipant = async (participant: Participant) => {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chainID: participant.chainID,
tipoDocumento: 1,
numeroDocumento: participant.identification,
numeroConta: participant.account,
numeroAgencia: participant.branch,
tipoConta: participant.accountType,
codigoIspb: participant.bankIspb,
}),
});
if (!response.ok) {
throw new Error(`Error creating participant: ${response.statusText}`);
}
const data = await response.json();
if (data.errors || data.erros) {
throw new Error(`Error creating participant: ${JSON.stringify(data)}`);
}
return { ...participant, id: data.numeroParticipante } as ParticipantWithID;
};
export const createSolicitation = async (offer: Offer) => {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/request`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: offer.amount,
pixTarget: offer.sellerId.split("-").pop(),
}),
});
return response.json();
};
export const getSolicitation = async (id: bigint): Promise<{pixTimestamp: `0x${string}`, signature: `0x${string}`}> => {
const response = await fetch(
`${import.meta.env.VITE_APP_API_URL}/release/${id}`
);
const obj = await response.json();
return {
pixTimestamp: obj.pixTimestamp,
signature: obj.signature,
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
import type { TokenEnum } from "@/model/NetworkEnum";
import { Networks } from "@/config/networks";
export const getNetworkImage = (networkName: string): string => {
const normalizedName = networkName.toLowerCase().replace(/[^a-z0-9]/g, '-');
return new URL(`../assets/networks/${normalizedName}.svg`, import.meta.url).href;
};
export const getTokenImage = (tokenName: TokenEnum): string => {
return new URL(`../assets/tokens/${tokenName.toLowerCase()}.svg`, import.meta.url).href;
};

View File

@@ -1,13 +1,11 @@
import type { ValidDeposit } from "@/model/ValidDeposit"; import type { ValidDeposit } from "@/model/ValidDeposit";
import type { Address } from "viem";
const verifyNetworkLiquidity = ( const verifyNetworkLiquidity = (
tokenValue: number, tokenValue: number,
walletAddress: Address, walletAddress: string,
validDepositList: ValidDeposit[] validDepositList: ValidDeposit[]
): ValidDeposit[] => { ): ValidDeposit | undefined => {
const filteredDepositList = validDepositList const element = validDepositList.find((element) => {
.filter((element) => {
const remaining = element.remaining; const remaining = element.remaining;
if ( if (
tokenValue!! <= remaining && tokenValue!! <= remaining &&
@@ -17,24 +15,9 @@ const verifyNetworkLiquidity = (
return true; return true;
} }
return false; return false;
})
.sort((a, b) => {
return b.remaining - a.remaining;
}); });
const uniqueNetworkDeposits = filteredDepositList.reduce( return element;
(acc: ValidDeposit[], current) => {
const existingNetwork = acc.find(
(deposit) => deposit.network === current.network
);
if (!existingNetwork) {
acc.push(current);
}
return acc;
},
[]
);
return uniqueNetworkDeposits;
}; };
export { verifyNetworkLiquidity }; export { verifyNetworkLiquidity };

File diff suppressed because one or more lines are too long

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