Merge pull request #61 from liftlearning/develop

V2
This commit is contained in:
Ronyell Henrique 2023-03-01 18:54:36 -03:00 committed by GitHub
commit b956c8ec2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 2385 additions and 735 deletions

View File

@ -1,3 +0,0 @@
VITE_API_URL=http://localhost:8000/
VITE_GOERLI_API_URL={GOERLI_API_URL_ALCHEMY}
VITE_MUMBAI_API_URL={MUMBAI_API_URL_ALCHEMY}

View File

@ -14,8 +14,10 @@
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@floating-ui/vue": "^0.2.1",
"@headlessui/vue": "^1.7.3",
"@heroicons/vue": "^2.0.12",
"@vueuse/core": "^9.12.0",
"alchemy-sdk": "^2.3.0",
"axios": "^1.2.1",
"crc": "^3.8.0",

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import TopBar from "./components/TopBar/TopBar.vue";
import TopBar from "@/components/TopBar/TopBar.vue";
import SpinnerComponent from "@/components/SpinnerComponent.vue";
</script>
<template>
@ -8,6 +9,11 @@ import TopBar from "./components/TopBar/TopBar.vue";
<template v-if="Component">
<Suspense>
<component :is="Component"></component>
<template #fallback>
<div class="flex w-full h-full justify-center items-center">
<SpinnerComponent :width="'16'" :height="'16'"></SpinnerComponent>
</div>
</template>
</Suspense>
</template>
</RouterView>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 744 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0105 1.69682C13.2058 1.50155 13.2058 1.18497 13.0105 0.989708C12.8153 0.794446 12.4987 0.794446 12.3034 0.989708L7.00016 6.29297L1.6969 0.989708C1.50163 0.794446 1.18505 0.794446 0.989788 0.989708C0.794526 1.18497 0.794526 1.50155 0.989788 1.69682L6.29305 7.00008L0.989708 12.3034C0.794446 12.4987 0.794446 12.8153 0.989708 13.0105C1.18497 13.2058 1.50155 13.2058 1.69681 13.0105L7.00016 7.70718L12.3035 13.0105C12.4988 13.2058 12.8153 13.2058 13.0106 13.0105C13.2059 12.8153 13.2059 12.4987 13.0106 12.3034L7.70726 7.00008L13.0105 1.69682Z" fill="#1F2937"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.636235 0.636155C1.02676 0.245631 1.65992 0.245631 2.05045 0.636155L7.00016 5.58586L11.9499 0.636155C12.3404 0.245631 12.9736 0.245631 13.3641 0.636155C13.7546 1.02668 13.7546 1.65984 13.3641 2.05037L8.41437 7.00008L13.3642 11.9499C13.7547 12.3404 13.7547 12.9736 13.3642 13.3641C12.9736 13.7546 12.3405 13.7546 11.9499 13.3641L7.00016 8.41429L2.05037 13.3641C1.65984 13.7546 1.02668 13.7546 0.636155 13.3641C0.245632 12.9736 0.24563 12.3404 0.636154 11.9499L5.58594 7.00008L0.636235 2.05037C0.245711 1.65984 0.245711 1.02668 0.636235 0.636155Z" fill="#1F2937"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5501 1.03713C12.513 0.549805 11.4071 0.199929 10.2451 0C10.1014 0.256159 9.93891 0.599787 9.8202 0.874689C8.58939 0.687256 7.37107 0.687256 6.16525 0.874689C6.05279 0.599787 5.87785 0.256159 5.7404 0C4.58456 0.199929 3.47246 0.556053 2.43533 1.03713C0.348566 4.19226 -0.213734 7.27242 0.0674159 10.3088C1.45442 11.346 2.7977 11.9707 4.11598 12.3831C4.44086 11.9333 4.73451 11.4584 4.98442 10.9586C4.50959 10.7774 4.0535 10.5525 3.6224 10.2963C3.73486 10.2089 3.84732 10.1214 3.95978 10.0339C6.5901 11.2648 9.45158 11.2648 12.0507 10.0339C12.1631 10.1277 12.2693 10.2151 12.388 10.2963C11.9569 10.5588 11.4946 10.7837 11.0198 10.9649C11.2697 11.4647 11.5571 11.9458 11.8882 12.3894C13.2065 11.977 14.5498 11.3522 15.9368 10.3151C16.2554 6.79134 15.3557 3.74242 13.5501 1.03713ZM5.34054 8.44075C4.55332 8.44075 3.90355 7.70351 3.90355 6.80383C3.90355 5.90415 4.53458 5.16692 5.34054 5.16692C6.14651 5.16692 6.79003 5.90415 6.77753 6.80383C6.77753 7.70351 6.14026 8.44075 5.34054 8.44075ZM10.6512 8.44075C9.86394 8.44075 9.21417 7.70351 9.21417 6.80383C9.21417 5.90415 9.84519 5.16692 10.6512 5.16692C11.4571 5.16692 12.1006 5.90415 12.0881 6.80383C12.0881 7.70351 11.4571 8.44075 10.6512 8.44075Z" fill="#1F2937"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/faqEthereum.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>

After

Width:  |  Height:  |  Size: 1.4 KiB

5
src/assets/info.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 13.125C3.61726 13.125 0.875 10.3827 0.875 7C0.875 3.61726 3.61726 0.875 7 0.875C10.3827 0.875 13.125 3.61726 13.125 7C13.125 10.3827 10.3827 13.125 7 13.125ZM7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z" fill="#6B7280"/>
<path d="M7.81437 5.7644L5.80973 6.01562L5.73795 6.34888L6.13272 6.42065C6.38907 6.48218 6.44034 6.57446 6.38395 6.83081L5.73795 9.86597C5.56876 10.6504 5.83023 11.0195 6.44547 11.0195C6.92228 11.0195 7.47599 10.7991 7.72721 10.4966L7.80411 10.1326C7.6298 10.2864 7.37345 10.3479 7.20426 10.3479C6.96329 10.3479 6.87613 10.1787 6.93766 9.88135L7.81437 5.7644Z" fill="#6B7280"/>
<path d="M7.875 3.9375C7.875 4.42075 7.48325 4.8125 7 4.8125C6.51675 4.8125 6.125 4.42075 6.125 3.9375C6.125 3.45425 6.51675 3.0625 7 3.0625C7.48325 3.0625 7.875 3.45425 7.875 3.9375Z" fill="#6B7280"/>
</svg>

After

Width:  |  Height:  |  Size: 975 B

5
src/assets/logo2.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.32352 0C3.37332 0 4.26027 0.697928 4.54816 1.65467C5.65322 1.73319 6.71174 2.21301 7.494 3.00109L11.7165 7.22065C11.731 7.23519 11.7485 7.24973 11.763 7.26718L16.3926 11.8939C16.9567 12.4609 17.7361 12.7837 18.5329 12.7837C18.5358 12.7837 18.5387 12.7837 18.5416 12.7837H18.7975C19.1465 11.9433 19.9753 11.353 20.9437 11.353C22.2261 11.353 23.2672 12.394 23.2672 13.6765C23.2672 14.9589 22.2261 16 20.9437 16C19.9084 16 19.036 15.3253 18.7336 14.3889H18.5445C18.5416 14.3889 18.5358 14.3889 18.5329 14.3889C17.3144 14.3889 16.1221 13.8946 15.2643 13.0309L11.0244 8.79099C11.0069 8.77644 10.9866 8.7619 10.9691 8.74446L6.35405 4.12941C5.84806 3.6205 5.16467 3.30934 4.44929 3.25118C4.0916 4.07125 3.27445 4.64413 2.32061 4.64413C1.04108 4.64413 0 3.60596 0 2.32352C0 1.04108 1.04108 0 2.32352 0Z" fill="#F9FAFB"/>
<path d="M2.32352 11.3531C3.29189 11.3531 4.11778 11.9434 4.46965 12.7838H4.72555C4.72846 12.7838 4.73137 12.7838 4.73428 12.7838C5.53399 12.7838 6.31334 12.461 6.87459 11.894L9.63722 9.13135L10.7684 10.2626L8.00291 13.0281C7.14213 13.8918 5.95274 14.3862 4.73428 14.3862C4.73137 14.3862 4.72555 14.3862 4.72265 14.3862H4.53362C4.2341 15.3196 3.35878 15.9972 2.32352 15.9972C1.04108 15.9972 0 14.9561 0 13.6737C0 12.3913 1.04108 11.3531 2.32352 11.3531Z" fill="#F9FAFB"/>
<path d="M20.9405 4.64413C19.9722 4.64413 19.1463 4.0538 18.7944 3.21338H18.5385C18.5356 3.21338 18.5327 3.21338 18.5298 3.21338C17.7301 3.21338 16.9507 3.53617 16.3895 4.10324L13.6268 6.86587L12.4956 5.73464L15.2611 2.9691C16.1219 2.10542 17.3113 1.61105 18.5298 1.61105C18.5327 1.61105 18.5385 1.61105 18.5414 1.61105H18.7304C19.0329 0.674664 19.9082 0 20.9405 0C22.223 0 23.2641 1.04108 23.2641 2.32352C23.2641 3.60596 22.223 4.64413 20.9405 4.64413Z" fill="#F9FAFB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -4,8 +4,9 @@
@tailwind utilities;
#app {
width: 100%;
margin: 0 auto;
padding: 2rem 4rem;
padding: 2rem;
height: fit-content;
min-height: 100vh;
background-image: url( './bg.svg' );

View File

@ -0,0 +1,3 @@
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -33,7 +33,7 @@ describe("addresses.ts functions", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.ethereum);
expect(getTokenAddress()).toBe(
"0x294003F602c321627152c6b7DED3EAb5bEa853Ee"
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
@ -41,13 +41,13 @@ describe("addresses.ts functions", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.polygon);
expect(getTokenAddress()).toBe(
"0x294003F602c321627152c6b7DED3EAb5bEa853Ee"
"0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29"
);
});
it("getTokenAddress Default", () => {
expect(getTokenAddress()).toBe(
"0x294003F602c321627152c6b7DED3EAb5bEa853Ee"
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
@ -55,7 +55,7 @@ describe("addresses.ts functions", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.ethereum);
expect(getP2PixAddress()).toBe(
"0x5f3EFA9A90532914545CEf527C530658af87e196"
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
);
});
@ -63,13 +63,13 @@ describe("addresses.ts functions", () => {
const etherStore = useEtherStore();
etherStore.setNetworkName(NetworkEnum.polygon);
expect(getP2PixAddress()).toBe(
"0x5f3EFA9A90532914545CEf527C530658af87e196"
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
it("getP2PixAddress Default", () => {
expect(getP2PixAddress()).toBe(
"0x5f3EFA9A90532914545CEf527C530658af87e196"
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
);
});
@ -102,9 +102,3 @@ describe("addresses.ts functions", () => {
expect(isPossibleNetwork("0x55")).toBe(false);
});
});
describe("addresses.ts Unset Store", () => {
it("getProviderUrl Unset", () => {
expect(getProviderUrl()).toBe(undefined);
});
});

View File

@ -1,26 +1,26 @@
import { useEtherStore } from "@/store/ether";
import { NetworkEnum } from "@/model/NetworkEnum";
const getTokenAddress = (): string => {
const getTokenAddress = (network?: NetworkEnum): string => {
const etherStore = useEtherStore();
const possibleTokenAddresses: { [key: string]: string } = {
Ethereum: "0x294003F602c321627152c6b7DED3EAb5bEa853Ee",
Polygon: "0x294003F602c321627152c6b7DED3EAb5bEa853Ee",
Ethereum: "0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00",
Polygon: "0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29",
};
return possibleTokenAddresses[etherStore.networkName];
return possibleTokenAddresses[network ? network : etherStore.networkName];
};
const getP2PixAddress = (): string => {
const getP2PixAddress = (network?: NetworkEnum): string => {
const etherStore = useEtherStore();
const possibleP2PixAddresses: { [key: string]: string } = {
Ethereum: "0x5f3EFA9A90532914545CEf527C530658af87e196",
Polygon: "0x5f3EFA9A90532914545CEf527C530658af87e196",
Ethereum: "0x2414817FF64A114d91eCFA16a834d3fCf69103d4",
Polygon: "0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00",
};
return possibleP2PixAddresses[etherStore.networkName];
return possibleP2PixAddresses[network ? network : etherStore.networkName];
};
const getProviderUrl = (): string => {

View File

@ -1,15 +1,16 @@
import { useEtherStore } from "@/store/ether";
import { getContract, getProvider } from "./provider";
import { getP2PixAddress } from "./addresses";
import { getP2PixAddress, getTokenAddress } from "./addresses";
import p2pix from "../utils/smart_contract_files/P2PIX.json";
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import { BigNumber, ethers } from "ethers";
import { parseEther } from "ethers/lib/utils";
const addLock = async (
depositId: BigNumber,
seller: string,
token: string,
amount: number
): Promise<string> => {
const etherStore = useEtherStore();
@ -17,7 +18,8 @@ const addLock = async (
const p2pContract = getContract();
const lock = await p2pContract.lock(
depositId, // BigNumber
seller,
token,
etherStore.walletAddress, // String "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" (Example)
ethers.constants.AddressZero, // String "0x0000000000000000000000000000000000000000"
0,
@ -29,11 +31,11 @@ const addLock = async (
const lock_rec = await lock.wait();
const [t] = lock_rec.events;
return t.args.lockID;
return String(t.args.lockID);
};
const releaseLock = async (
pixKey: string,
pixKey: number,
amount: number,
e2eId: string,
lockId: string
@ -43,7 +45,7 @@ const releaseLock = async (
);
const messageToSign = ethers.utils.solidityKeccak256(
["string", "uint256", "bytes32"],
["uint160", "uint256", "bytes32"],
[
pixKey,
parseEther(String(amount)),
@ -61,7 +63,7 @@ const releaseLock = async (
const p2pContract = new ethers.Contract(getP2PixAddress(), p2pix.abi, signer);
const release = await p2pContract.release(
lockId,
BigNumber.from(lockId),
ethers.constants.AddressZero,
ethers.utils.formatBytes32String(e2eId),
sig.r,
@ -82,10 +84,14 @@ const cancelDeposit = async (depositId: BigNumber): Promise<any> => {
return cancel;
};
const withdrawDeposit = async (depositId: BigNumber): Promise<any> => {
const withdrawDeposit = async (amount: string): Promise<any> => {
const contract = getContract();
const withdraw = await contract.withdraw(depositId, []);
const withdraw = await contract.withdraw(
getTokenAddress(),
parseEther(String(amount)),
[]
);
await withdraw.wait();
return withdraw;

View File

@ -1,14 +1,17 @@
import { useEtherStore } from "@/store/ether";
import { Contract, ethers } from "ethers";
import p2pix from "../utils/smart_contract_files/P2PIX.json";
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import { formatEther } from "ethers/lib/utils";
import { getContract } from "./provider";
import type { ValidDeposit } from "@/model/ValidDeposit";
import { getP2PixAddress, getTokenAddress } from "./addresses";
import { NetworkEnum } from "@/model/NetworkEnum";
import type { UnreleasedLock } from "@/model/UnreleasedLock";
import type { Pix } from "@/model/Pix";
const getNetworksLiquidity = async (): Promise<void> => {
const etherStore = useEtherStore();
console.log("Loading events");
const goerliProvider = new ethers.providers.JsonRpcProvider(
import.meta.env.VITE_GOERLI_API_URL,
@ -20,28 +23,34 @@ const getNetworksLiquidity = async (): Promise<void> => {
); // mumbai provider
const p2pContractGoerli = new ethers.Contract(
"0x5f3EFA9A90532914545CEf527C530658af87e196",
getP2PixAddress(NetworkEnum.ethereum),
p2pix.abi,
goerliProvider
);
const p2pContractMumbai = new ethers.Contract(
"0x5f3EFA9A90532914545CEf527C530658af87e196",
getP2PixAddress(NetworkEnum.polygon),
p2pix.abi,
mumbaiProvider
);
const depositListGoerli = await getValidDeposits(p2pContractGoerli);
etherStore.setLoadingNetworkLiquidity(true);
const depositListGoerli = await getValidDeposits(
getTokenAddress(NetworkEnum.ethereum),
p2pContractGoerli
);
const depositListMumbai = await getValidDeposits(p2pContractMumbai);
const depositListMumbai = await getValidDeposits(
getTokenAddress(NetworkEnum.polygon),
p2pContractMumbai
);
etherStore.setDepositsValidListGoerli(depositListGoerli);
console.log(depositListGoerli);
etherStore.setDepositsValidListMumbai(depositListMumbai);
console.log(depositListMumbai);
etherStore.setLoadingNetworkLiquidity(false);
};
const getValidDeposits = async (
token: string,
contract?: Contract
): Promise<ValidDeposit[]> => {
let p2pContract: Contract;
@ -56,29 +65,62 @@ const getValidDeposits = async (
const eventsDeposits = await p2pContract.queryFilter(filterDeposits);
if (!contract) p2pContract = getContract(); // get metamask provider contract
const depositList: { [key: string]: ValidDeposit } = {};
const depositList = await Promise.all(
await Promise.all(
eventsDeposits.map(async (deposit) => {
const mappedDeposit = await p2pContract.mapDeposits(
deposit.args?.depositID
// Get liquidity only for the selected token
if (deposit.args?.token != token) return null;
const mappedBalance = await p2pContract.getBalance(
deposit.args?.seller,
token
);
const mappedPixTarget = await p2pContract.getPixTarget(
deposit.args?.seller,
token
);
let validDeposit: ValidDeposit | null = null;
if (mappedDeposit.valid) {
if (mappedBalance._hex) {
validDeposit = {
token: token,
blockNumber: deposit.blockNumber,
depositID: deposit.args?.depositID,
remaining: Number(formatEther(mappedDeposit.remaining)),
seller: mappedDeposit.seller,
pixKey: mappedDeposit.pixTarget,
remaining: Number(formatEther(mappedBalance._hex)),
seller: deposit.args?.seller,
pixKey: Number(mappedPixTarget._hex),
};
}
return validDeposit;
if (validDeposit)
depositList[deposit.args?.seller + token] = validDeposit;
})
);
return depositList.filter((deposit) => deposit) as ValidDeposit[];
return Object.values(depositList);
};
export { getValidDeposits, getNetworksLiquidity };
const getUnreleasedLockById = async (
lockID: string
): Promise<UnreleasedLock> => {
const p2pContract = getContract();
const pixData: Pix = {
pixKey: "",
};
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,
};
};
export { getValidDeposits, getNetworksLiquidity, getUnreleasedLockById };

View File

@ -1,6 +1,6 @@
import { useEtherStore } from "@/store/ether";
import p2pix from "../utils/smart_contract_files/P2PIX.json";
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import { updateWalletStatus } from "./wallet";
import {

View File

@ -22,7 +22,6 @@ const approveTokens = async (tokenQty: string): Promise<any> => {
);
await apprv.wait();
console.log(apprv);
return apprv;
};
@ -33,6 +32,7 @@ const addDeposit = async (tokenQty: string, pixKey: string): Promise<any> => {
getTokenAddress(),
parseEther(tokenQty),
pixKey,
true,
ethers.utils.formatBytes32String("")
);

View File

@ -3,12 +3,16 @@ import { useEtherStore } from "@/store/ether";
import { getContract, getProvider } from "./provider";
import { getTokenAddress, possibleChains } from "./addresses";
import mockToken from "../utils/smart_contract_files/MockToken.json";
import mockToken from "@/utils/smart_contract_files/MockToken.json";
import { ethers, type Event } from "ethers";
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 { WalletTransaction } from "@/model/WalletTransaction";
import type { UnreleasedLock } from "@/model/UnreleasedLock";
import type { Pix } from "@/model/Pix";
const updateWalletStatus = async (): Promise<void> => {
const etherStore = useEtherStore();
@ -35,7 +39,7 @@ const updateWalletStatus = async (): Promise<void> => {
const listValidDepositTransactionsByWalletAddress = async (
walletAddress: string
): Promise<ValidDeposit[]> => {
const walletDeposits = await getValidDeposits();
const walletDeposits = await getValidDeposits(getTokenAddress());
if (walletDeposits) {
return walletDeposits
@ -48,10 +52,50 @@ const listValidDepositTransactionsByWalletAddress = async (
return [];
};
const getLockStatus = async (id: [BigNumber]): Promise<number> => {
const p2pContract = getContract();
const res = await p2pContract.getLocksStatus([id]);
return res[1][0];
};
const filterLockStatus = async (
transactions: Event[]
): Promise<WalletTransaction[]> => {
const txs = await Promise.all(
transactions.map(async (transaction) => {
const tx: WalletTransaction = {
token: transaction.args?.token ? transaction.args?.token : "",
blockNumber: transaction.blockNumber ? transaction.blockNumber : -1,
amount: transaction.args?.amount
? Number(formatEther(transaction.args?.amount))
: -1,
seller: transaction.args?.seller ? transaction.args?.seller : "",
buyer: transaction.args?.buyer ? transaction.args?.buyer : "",
event: transaction.event ? transaction.event : "",
lockStatus:
transaction.event == "LockAdded"
? await getLockStatus(transaction.args?.lockID)
: -1,
transactionHash: transaction.transactionHash
? transaction.transactionHash
: "",
transactionID: transaction.args?.lockID
? String(transaction.args?.lockID)
: "",
};
return tx;
})
);
return txs;
};
const listAllTransactionByWalletAddress = async (
walletAddress: string
): Promise<Event[]> => {
const p2pContract = getContract();
): Promise<WalletTransaction[]> => {
const p2pContract = getContract(true);
const filterDeposits = p2pContract.filters.DepositAdded([walletAddress]);
const eventsDeposits = await p2pContract.queryFilter(filterDeposits);
@ -64,11 +108,25 @@ const listAllTransactionByWalletAddress = async (
filterReleasedLocks
);
return [...eventsDeposits, ...eventsAddedLocks, ...eventsReleasedLocks].sort(
(a, b) => {
return b.blockNumber - a.blockNumber;
}
const filterWithdrawnDeposits = p2pContract.filters.DepositWithdrawn([
walletAddress,
]);
const eventsWithdrawnDeposits = await p2pContract.queryFilter(
filterWithdrawnDeposits
);
const lockStatusFiltered = await filterLockStatus(
[
...eventsDeposits,
...eventsAddedLocks,
...eventsReleasedLocks,
...eventsWithdrawnDeposits,
].sort((a, b) => {
return b.blockNumber - a.blockNumber;
})
);
return lockStatusFiltered;
};
// get wallet's release transactions
@ -87,9 +145,98 @@ const listReleaseTransactionByWalletAddress = async (
});
};
const listLockTransactionByWalletAddress = async (
walletAddress: string
): Promise<Event[]> => {
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 lockStatus = await p2pContract.getLocksStatus(
lockSeller.map((lock) => lock.args?.lockID)
);
const activeLockAmount = await lockStatus[1].reduce(
async (sumValue: Promise<number>, currentStatus: number, index: number) => {
const currValue = await sumValue;
let valueToSum = 0;
if (currentStatus == 1) {
const lock = await p2pContract.mapLocks(lockStatus[0][index]);
valueToSum = Number(formatEther(lock?.amount));
}
return currValue + valueToSum;
},
Promise.resolve(0)
);
return activeLockAmount;
};
export {
updateWalletStatus,
listValidDepositTransactionsByWalletAddress,
listAllTransactionByWalletAddress,
listReleaseTransactionByWalletAddress,
checkUnreleasedLock,
getActiveLockAmount,
};

View File

@ -1,22 +1,87 @@
<script setup lang="ts">
import { withdrawDeposit } from "@/blockchain/buyerMethods";
import {
getActiveLockAmount,
listAllTransactionByWalletAddress,
listValidDepositTransactionsByWalletAddress,
} from "@/blockchain/wallet";
import CustomButton from "@/components/CustomButton/CustomButton.vue";
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
import type { Event } from "ethers";
import type { ValidDeposit } from "@/model/ValidDeposit";
import type { WalletTransaction } from "@/model/WalletTransaction";
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import ListingComponent from "../ListingComponent/ListingComponent.vue";
// props
const props = defineProps<{
lastWalletReleaseTransactions: Event[];
tokenAmount: number | undefined;
isCurrentStep: boolean;
}>();
const etherStore = useEtherStore();
const { walletAddress } = storeToRefs(etherStore);
const lastWalletTransactions = ref<WalletTransaction[]>([]);
const depositList = ref<ValidDeposit[]>([]);
const activeLockAmount = ref<number>(0);
// methods
const getWalletTransactions = async () => {
etherStore.setLoadingWalletTransactions(true);
if (walletAddress.value) {
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
walletAddress.value
);
const allUserTransactions = await listAllTransactionByWalletAddress(
walletAddress.value
);
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
if (walletDeposits) {
depositList.value = walletDeposits;
}
if (allUserTransactions) {
lastWalletTransactions.value = allUserTransactions;
}
}
etherStore.setLoadingWalletTransactions(false);
};
const callWithdraw = async (amount: string) => {
if (amount) {
etherStore.setLoadingWalletTransactions(true);
const withdraw = await withdrawDeposit(amount);
if (withdraw) {
console.log("Saque realizado!");
await getWalletTransactions();
} else {
console.log("Não foi possível realizar o saque!");
}
etherStore.setLoadingWalletTransactions(false);
}
};
// Emits
const emit = defineEmits(["makeAnotherTransaction"]);
// observer
watch(props, async (): Promise<void> => {
if (props.isCurrentStep) await getWalletTransactions();
});
onMounted(async () => {
await getWalletTransactions();
});
</script>
<template>
<div class="page">
<div class="text-container">
<span class="text font-extrabold text-5xl max-w-[50rem]"
<span class="text font-extrabold sm:text-5xl text-xl max-w-[50rem]"
>Os tokens foram transferidos <br />
para a sua carteira!
</span>
@ -30,15 +95,13 @@ const emit = defineEmits(["makeAnotherTransaction"]);
<p class="text-2xl text-gray-900">{{ props.tokenAmount }} BRZ</p>
</div>
<div class="my-5">
<p>
<b>Não encontrou os tokens? </b>Clique no botão abaixo para <br />
<p class="text-sm">
<b>Não encontrou os tokens? </b><br />Clique no botão abaixo para
<br />
cadastrar o BRZ em sua carteira.
</p>
</div>
<CustomButton
:text="'Cadastrar token na carteira'"
@buttonClicked="() => {}"
/>
<CustomButton :text="'Cadastrar token'" @buttonClicked="() => {}" />
</div>
<button
type="button"
@ -48,17 +111,18 @@ const emit = defineEmits(["makeAnotherTransaction"]);
Fazer nova transação
</button>
</div>
<div class="text-container mt-16">
<span class="text font-extrabold text-3xl max-w-[50rem]"
>Histórico de compras
</span>
</div>
<div class="w-full max-w-4xl">
<ListingComponent
:walletTransactions="lastWalletReleaseTransactions"
:isManageMode="false"
<div
class="flex justify-center mt-8 mb-6 text-white text-xl md:text-3xl font-bold"
>
</ListingComponent>
Gerenciar transações
</div>
<div class="w-full max-w-xs md:max-w-lg">
<ListingComponent
:valid-deposits="depositList"
:wallet-transactions="lastWalletTransactions"
:active-lock-amount="activeLockAmount"
@deposit-withdrawn="callWithdraw"
></ListingComponent>
</div>
</div>
</template>
@ -77,14 +141,14 @@ p {
}
.text {
@apply text-gray-800 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 flex-col justify-center items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10 w-auto;
@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 {
@ -99,4 +163,22 @@ 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

@ -1,32 +1,31 @@
import { shallowMount } from "@vue/test-utils";
import { mount } from "@vue/test-utils";
import BuyConfirmedComponent from "../BuyConfirmedComponent.vue";
import { createPinia, setActivePinia } from "pinia";
import { MockEvents } from "@/model/mock/EventMock";
describe("BuyConfirmedComponent.vue", () => {
describe("BuyConfirmedComponent.vue", async () => {
beforeEach(() => {
setActivePinia(createPinia());
});
const wrapper = shallowMount(BuyConfirmedComponent, {
const wrapper = mount(BuyConfirmedComponent, {
props: {
lastWalletReleaseTransactions: MockEvents,
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 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 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");

View File

@ -0,0 +1,154 @@
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps<{
type: string;
}>();
const alertText = ref<string>("");
const alertPaddingLeft = ref<string>("18rem");
if (props.type === "sell") {
alertPaddingLeft.value = "30%";
} else if (props.type === "buy") {
alertPaddingLeft.value = "30%";
} else if (props.type === "withdraw") {
alertPaddingLeft.value = "40%";
} else if (props.type === "redirect") {
alertPaddingLeft.value = "35%";
}
switch (props.type) {
case "buy":
alertText.value =
"Tudo certo! Os tokens já foram retirados da oferta e estão disponíveis na sua carteira.";
break;
case "sell":
alertText.value =
"Tudo certo! Os tokens já foram reservados e sua oferta está disponível.";
break;
case "redirect":
alertText.value = "Existe uma compra em aberto. Continuar?";
break;
case "withdraw":
alertText.value = "Tudo certo! Saque realizado com sucesso!";
break;
}
</script>
<template>
<div
class="modal-overlay sm:h-12 h-full inset-0 absolute backdrop-blur-sm sm:backdrop-blur-none"
>
<div class="modal px-12 pl-72 text-center sm:flex justify-between hidden">
<div class="flex items-center">
<p class="text-black tracking-tighter leading-tight my-2">
{{ alertText }}
</p>
<button v-if="props.type === 'redirect'" @click="$emit('go-to-lock')">
Sim
</button>
</div>
<img
src="../../assets/closeAlertIcon.svg"
alt="close alert"
class="w-3 cursor-pointer"
@click="$emit('close-alert')"
/>
</div>
<div
class="modal-mobile px-7 py-3 text-center inline-block sm:hidden mt-24 h-48"
v-if="props.type !== 'redirect'"
>
<p class="text-black tracking-tighter leading-tight my-2 text-start mb-4">
{{ alertText }}
</p>
<button
@click="$emit('close-alert')"
class="border-2 border-solid border-amber-400 mt-2"
>
Ok
</button>
</div>
<div
class="modal-mobile px-5 text-center inline-block sm:hidden mt-24 h-40"
v-if="props.type === 'redirect'"
>
<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-alert')"
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;
width: 100%;
padding-top: 7rem;
z-index: 10;
}
.modal {
background-color: rgba(251, 191, 36, 1);
height: 3rem;
width: 96%;
border-radius: 10px;
align-items: center;
white-space: nowrap;
padding-left: v-bind(alertPaddingLeft);
}
.modal-mobile {
background-color: rgba(251, 191, 36, 1);
/* height: 200px; */
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: 20px;
font-weight: 600;
}
button {
width: 50px;
height: 30px;
color: black;
font-size: 16px;
border-radius: 10px;
border: 2px solid white;
margin-left: 1rem;
}
</style>

View File

@ -21,6 +21,6 @@ const emit = defineEmits(["buttonClicked"]);
<style scoped>
.button {
@apply rounded-lg w-full text-base font-semibold text-gray-900 py-4 bg-amber-400;
@apply rounded-lg w-full text-base font-semibold text-gray-900 p-4 bg-amber-400;
}
</style>

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,26 +1,103 @@
<script setup lang="ts">
import { withdrawDeposit } from "@/blockchain/buyerMethods";
import { NetworkEnum } from "@/model/NetworkEnum";
import type { ValidDeposit } from "@/model/ValidDeposit";
import type { WalletTransaction } from "@/model/WalletTransaction";
import { useEtherStore } from "@/store/ether";
import { formatEther } from "@ethersproject/units";
import type { BigNumber, Event } from "ethers";
import { ref, watch } from "vue";
// props
const props = defineProps<{
walletTransactions: (Event | ValidDeposit)[];
isManageMode: boolean;
}>();
import { storeToRefs } from "pinia";
import { ref, watch, onMounted } from "vue";
import SpinnerComponent from "../SpinnerComponent.vue";
import { decimalCount } from "@/utils/decimalCount";
import { debounce } from "@/utils/debounce";
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
const etherStore = useEtherStore();
const itemsToShow = ref<(Event | ValidDeposit)[]>([]);
// props
const props = defineProps<{
validDeposits: ValidDeposit[];
walletTransactions: WalletTransaction[];
activeLockAmount: number;
}>();
// Methods
const isValidDeposit = (
deposit: Event | ValidDeposit
): deposit is ValidDeposit => {
return (deposit as ValidDeposit).depositID !== undefined;
const emit = defineEmits(["depositWithdrawn"]);
const { loadingWalletTransactions } = storeToRefs(etherStore);
const remaining = ref<number>(0.0);
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 reference = ref<HTMLElement | null>(null);
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 => {
@ -48,17 +125,25 @@ const getEventName = (event: string | undefined): string => {
const possibleEventName: { [key: string]: string } = {
DepositAdded: "Oferta",
LockAdded: "Compra",
LockReleased: "Reserva",
LockAdded: "Reserva",
LockReleased: "Compra",
DepositWithdrawn: "Retirada",
};
return possibleEventName[event];
};
const getAmountFormatted = (amount?: BigNumber): string => {
if (!amount) return "";
return formatEther(amount);
};
onMounted(() => {
useFloating(reference, floating, {
placement: "right",
middleware: [
offset(10),
flip(),
shift(),
arrow({ element: floatingArrow }),
],
});
});
// watch props changes
watch(props, async (): Promise<void> => {
@ -71,79 +156,184 @@ watch(props, async (): Promise<void> => {
: props.walletTransactions;
});
//emits
const emit = defineEmits(["cancelDeposit", "withdrawDeposit"]);
// initial itemsToShow value
// initial itemsToShow valueb
showInitialItems();
</script>
<template>
<div class="blur-container">
<div class="blur-container" v-if="loadingWalletTransactions">
<SpinnerComponent width="8" height="8"></SpinnerComponent>
</div>
<div class="blur-container" v-if="!loadingWalletTransactions">
<div
class="grid grid-cols-4 grid-flow-row w-full px-6"
v-if="itemsToShow.length != 0"
class="w-full bg-white p-4 sm:p-6 rounded-lg"
v-if="props.validDeposits.length > 0"
>
<span class="text-xs text-gray-50 font-medium justify-self-center"
>Valor</span
>
<span class="text-xs text-gray-50 font-medium justify-self-center"
>Data</span
>
<span class="text-xs text-gray-50 font-medium justify-self-center">{{
props.isManageMode ? "Cancelar oferta" : "Tipo de transação"
}}</span>
<span class="text-xs text-gray-50 font-medium justify-self-center">{{
props.isManageMode ? "Retirar tokens" : "Checar transação"
<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">
{{ 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>
<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="grid grid-cols-4 grid-flow-row w-full bg-white px-6 py-4 rounded-lg"
v-for="(item, index) in itemsToShow"
class="w-full bg-white p-4 sm:p-6 rounded-lg"
v-for="item in itemsToShow"
:key="item.blockNumber"
>
<span class="last-release-info">
{{
isValidDeposit(item)
? item.remaining
: getAmountFormatted(item.args?.amount)
}}
<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">
{{ getEventName(item.event) }}
</span>
<span
class="text-xl sm:text-xl leading-7 font-semibold text-gray-900"
>
{{ item.amount }}
BRZ
</span>
<span class="last-release-info transaction-date"> 20 out 2022 </span>
<span class="last-release-info" v-if="!props.isManageMode">
{{ getEventName((item as Event).event) }}
</span>
<div
v-if="!props.isManageMode"
class="flex gap-2 cursor-pointer items-center justify-self-center"
@click="openEtherscanUrl((item as Event)?.transactionHash)"
>
<span class="last-release-info">Etherscan</span>
<img alt="Redirect image" src="@/assets/redirect.svg" />
</div>
<div>
<div
v-if="props.isManageMode"
class="flex gap-2 cursor-pointer items-center justify-self-center"
@click="emit('cancelDeposit', (item as ValidDeposit).depositID, index)"
class="bg-amber-300 status-text"
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
>
<span class="last-release-info">Cancelar</span>
<img alt="Cancel image" src="@/assets/cancel.svg" />
Em Aberto
</div>
<div
v-if="props.isManageMode"
class="flex gap-2 cursor-pointer items-center justify-self-center"
@click="
emit('withdrawDeposit', (item as ValidDeposit).depositID, index)
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'
"
>
<span class="last-release-info">Retirar</span>
<img alt="Withdraw image" src="@/assets/withdraw.svg" />
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
@ -162,11 +352,11 @@ showInitialItems();
</button>
<span class="text-gray-300">
({{ itemsToShow.length }} de {{ props.walletTransactions.length }}
{{ isManageMode ? "ofertas" : "transações" }})
transações )
</span>
</div>
<span class="font-bold text-gray-900" v-if="itemsToShow.length == 0">
<span class="font-bold text-gray-300" v-if="itemsToShow.length == 0">
Não nenhuma transação anterior
</span>
</div>
@ -185,15 +375,19 @@ p {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-gray-800 text-center;
.item-container {
@apply flex justify-between items-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;
.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-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md w-auto;
@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 {
@ -201,7 +395,20 @@ p {
}
.last-release-info {
@apply font-medium text-base text-gray-900 justify-self-center;
@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"] {
@ -212,4 +419,10 @@ 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

@ -3,50 +3,37 @@ import ListingComponent from "@/components/ListingComponent/ListingComponent.vue
import { createPinia, setActivePinia } from "pinia";
import { expect } from "vitest";
import { MockValidDeposits } from "@/model/mock/ValidDepositMock";
import { MockEvents } from "@/model/mock/EventMock";
import { MockWalletTransactions } from "@/model/mock/WalletTransactionMock";
import { useEtherStore } from "@/store/ether";
describe("ListingComponent.vue", () => {
beforeEach(() => {
setActivePinia(createPinia());
useEtherStore().setLoadingWalletTransactions(false);
});
test("Test Headers on List in Manage Mode", () => {
test("Test Message when an empty array is received", () => {
const wrapper = mount(ListingComponent, {
props: {
walletTransactions: MockValidDeposits,
isManageMode: true,
validDeposits: [],
walletTransactions: [],
activeLockAmount: 0,
},
});
expect(wrapper.html()).toContain("Valor");
expect(wrapper.html()).toContain("Data");
expect(wrapper.html()).toContain("Cancelar oferta");
expect(wrapper.html()).toContain("Retirar tokens");
});
test("Test Headers on List in Unmanage Mode", () => {
const wrapper = mount(ListingComponent, {
props: {
walletTransactions: MockEvents,
isManageMode: false,
},
});
expect(wrapper.html()).toContain("Valor");
expect(wrapper.html()).toContain("Data");
expect(wrapper.html()).toContain("Tipo de transação");
expect(wrapper.html()).toContain("Checar transação");
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: {
walletTransactions: MockEvents,
isManageMode: false,
validDeposits: [],
walletTransactions: MockWalletTransactions,
activeLockAmount: 0,
},
});
const elements = wrapper.findAll(".transaction-date");
const elements = wrapper.findAll(".item-container");
expect(elements).toHaveLength(3);
});
@ -54,47 +41,47 @@ describe("ListingComponent.vue", () => {
test("Test load more button behavior", async () => {
const wrapper = mount(ListingComponent, {
props: {
walletTransactions: MockValidDeposits,
isManageMode: false,
validDeposits: MockValidDeposits,
walletTransactions: MockWalletTransactions,
activeLockAmount: 0,
},
});
const btn = wrapper.find("button");
let elements = wrapper.findAll(".transaction-date");
let elements = wrapper.findAll(".item-container");
expect(elements).toHaveLength(3);
await btn.trigger("click");
elements = wrapper.findAll(".transaction-date");
elements = wrapper.findAll(".item-container");
expect(elements).toHaveLength(5);
});
test("Test cancel offer button emit", async () => {
const wrapper = mount(ListingComponent, {
props: {
walletTransactions: MockValidDeposits,
isManageMode: true,
},
});
wrapper.vm.$emit("cancelDeposit");
await wrapper.vm.$nextTick();
expect(wrapper.emitted("cancelDeposit")).toBeTruthy();
});
test("Test withdraw offer button emit", async () => {
const wrapper = mount(ListingComponent, {
props: {
walletTransactions: MockValidDeposits,
isManageMode: true,
validDeposits: MockValidDeposits,
walletTransactions: MockWalletTransactions,
activeLockAmount: 0,
},
});
wrapper.vm.$emit("withdrawDeposit");
wrapper.vm.$emit("depositWithdrawn");
await wrapper.vm.$nextTick();
expect(wrapper.emitted("withdrawDeposit")).toBeTruthy();
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

@ -9,16 +9,18 @@ const props = defineProps({
<template>
<div class="page">
<div class="text-container">
<span class="text font-bold text-3xl max-w-[29rem]">{{
props.title ? props.title : "Confirme em sua carteira"
}}</span>
<span
class="text font-bold sm:text-3xl text-2xl sm:max-w-[29rem] max-w-[20rem]"
>
{{ props.title ? props.title : "Confirme em sua carteira" }}
</span>
</div>
<div class="blur-container w-[26rem]">
<div class="blur-container sm:w-[26rem] w-[20rem]">
<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 sm:px-10 px-4 py-5 rounded-lg border-y-10"
>
<div
class="flex flex-col text-center justify-center w-full items-center p-2 px-3 rounded-3xl min-w-fit gap-1"
class="flex flex-col text-center justify-center w-full items-center p-2 px-3 rounded-3xl lg:min-w-fit gap-1"
>
<img
alt="Polygon image"
@ -26,7 +28,7 @@ const props = defineProps({
width="96"
height="48"
/>
<span class="text-black font-medium text-sm px-12 mt-4">
<span class="text-black font-medium text-sm lg:px-12 px-4 mt-4">
{{ $props.message }}
</span>
</div>
@ -55,11 +57,11 @@ const props = defineProps({
}
.text {
@apply text-gray-800 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;
@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"] {

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

@ -1,9 +1,10 @@
<script setup lang="ts">
import { pix } from "../utils/QrCodePix";
import { ref } from "vue";
import { pix } from "@/utils/QrCodePix";
import { onMounted, onUnmounted, ref } from "vue";
import { debounce } from "@/utils/debounce";
import CustomButton from "./CustomButton/CustomButton.vue";
import api from "../services/index";
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({
@ -11,10 +12,12 @@ const props = defineProps({
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
@ -66,22 +69,37 @@ const validatePix = async (): Promise<void> => {
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 text-2xl max-w-[30rem]">
<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 text-md max-w-[28rem]">
<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 max-w-[28rem] text-black">
<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 p-8 rounded-lg break-normal"
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>
@ -95,9 +113,9 @@ const validatePix = async (): Promise<void> => {
src="@/assets/copyPix.svg"
width="16"
height="16"
class="pt-2 mb-5 cursor-pointer"
class="pt-2 lg:mb-5 cursor-pointer"
/>
<span class="text-xs text-start">
<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
@ -111,7 +129,7 @@ const validatePix = async (): Promise<void> => {
type="text"
placeholder="Digite o código do comprovante PIX"
@input="debounce(handleInputEvent, 500)($event)"
class="text-md w-full box-border p-2 h-6 mb-2 outline-none"
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
@ -150,6 +168,11 @@ const validatePix = async (): Promise<void> => {
@button-clicked="emit('pixValidated', e2eId)"
/>
</div>
<CustomModal
v-if="showWarnModal && windowSize <= 500"
@close-modal="showWarnModal = false"
:isRedirectModal="false"
/>
</div>
</template>
@ -195,7 +218,7 @@ h2 {
}
.text {
@apply text-gray-800 text-center;
@apply text-white text-center;
}
.blur-container {
@ -210,4 +233,22 @@ 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

@ -9,6 +9,7 @@ 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();
@ -18,6 +19,7 @@ const {
networkName,
depositsValidListGoerli,
depositsValidListMumbai,
loadingNetworkLiquidity,
} = storeToRefs(etherStore);
// Reactive state
@ -107,29 +109,37 @@ const enableOrDisableConfirmButton = (): void => {
};
watch(networkName, (): void => {
verifyLiquidity();
enableOrDisableConfirmButton();
});
watch(walletAddress, (): void => {
verifyLiquidity();
});
</script>
<template>
<div class="page">
<div class="text-container">
<span class="text font-extrabold text-5xl max-w-[29rem]"
>Adquira cripto com apenas um Pix</span
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>
<span class="text font-medium text-base max-w-[28rem]"
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 px-10 py-5 rounded-lg border-y-10"
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 w-full items-center">
<div class="flex justify-between sm:w-full items-center">
<input
type="number"
class="border-none outline-none text-lg text-gray-900 w-fit"
class="border-none outline-none text-lg text-gray-900 w-3/4"
v-bind:class="{
'font-semibold': tokenValue != undefined,
'text-xl': tokenValue != undefined,
@ -141,13 +151,22 @@ watch(networkName, (): void => {
<div
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
>
<img alt="Token image" class="w-fit" src="@/assets/brz.svg" />
<span class="text-gray-900 text-lg w-fit" id="brz">BRZ</span>
<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"></div>
<div class="flex justify-between pt-2" v-if="hasLiquidity">
<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>
@ -168,12 +187,27 @@ watch(networkName, (): void => {
/>
</div>
</div>
<div class="flex pt-2 justify-center" v-if="!validDecimals">
<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 pt-2 justify-center" v-else-if="!hasLiquidity">
<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
>
@ -215,11 +249,11 @@ watch(networkName, (): void => {
}
.text {
@apply text-gray-800 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;
@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"] {

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import CustomButton from "../CustomButton/CustomButton.vue";
import CustomButton from "@/components/CustomButton/CustomButton.vue";
import { debounce } from "@/utils/debounce";
import { decimalCount } from "@/utils/decimalCount";
@ -122,7 +122,7 @@ const handleInputEvent = (event: any): void => {
}
.text {
@apply text-gray-800 text-center;
@apply text-white text-center;
}
.blur-container {

View File

@ -14,10 +14,12 @@ const props = defineProps({
<template>
<div class="page">
<div class="text-container">
<span class="text font-extrabold text-5xl max-w-[50rem]"
<span
class="text font-extrabold sm:text-5xl text-xl sm:max-w-[50rem] max-w-[20rem]"
>Envie sua oferta para a rede
</span>
<span class="text text-xl font-medium text-base max-w-[30rem]"
<span
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.
Caso deseje retirar a oferta, será necessário aguardar 24h para receber
os tokens de volta.</span
@ -63,18 +65,15 @@ p {
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
@apply flex flex-col items-center justify-center sm:gap-4 gap-2;
}
.text {
@apply text-gray-800 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;
@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 w-1/3;
@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 {

View File

@ -3,6 +3,7 @@ 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";
@ -17,6 +18,7 @@ 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"]);
@ -35,35 +37,55 @@ const handleInputEvent = (event: any): void => {
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> => {
if (walletAddress.value) emit("approveTokens", { offer, pixKey });
const postProcessedPixKey = postProcessKey(pixKey);
if (walletAddress.value)
emit("approveTokens", { offer, postProcessedPixKey });
else await connectProvider();
};
</script>
<template>
<div class="page">
<div class="page w-full">
<div class="text-container">
<span class="text font-extrabold text-5xl max-w-[29rem]"
<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 text-base max-w-[28rem]"
<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 px-10 py-5 rounded-lg border-y-10"
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 w-full items-center">
<div class="flex justify-between items-center">
<input
type="number"
v-model="offer"
class="border-none outline-none text-lg text-gray-900 w-fit"
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,
@ -75,8 +97,12 @@ const handleButtonClick = async (
<div
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
>
<img alt="Token image" class="w-fit" src="@/assets/brz.svg" />
<span class="text-gray-900 text-lg w-fit" id="brz">BRZ</span>
<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>
@ -92,19 +118,26 @@ const handleButtonClick = async (
</div>
</div>
<div
class="flex flex-col w-full bg-white px-10 py-8 rounded-lg border-y-10"
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 text-lg text-gray-900 w-fit"
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>
@ -131,11 +164,11 @@ const handleButtonClick = async (
}
.text {
@apply text-gray-800 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;
@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"] {
@ -146,4 +179,8 @@ 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

@ -0,0 +1,40 @@
<script setup lang="ts">
// prop
const props = defineProps({
width: String,
height: String,
show: Boolean,
});
const getCustomClass = () => {
return [
`w-${props.width}`,
`h-${props.height}`,
`fill-white`,
"text-gray-200",
"animate-spin",
"dark:text-gray-600",
];
};
</script>
<template>
<div v-if="props.show ? props.show : true">
<svg
aria-hidden="true"
:class="getCustomClass()"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</template>

View File

@ -2,6 +2,7 @@
import { storeToRefs } from "pinia";
import { useEtherStore } from "@/store/ether";
import { ref } from "vue";
import { onClickOutside } from "@vueuse/core";
import { NetworkEnum } from "@/model/NetworkEnum";
import { connectProvider, requestNetworkChange } from "@/blockchain/provider";
import ethereumImage from "@/assets/ethereum.svg";
@ -15,8 +16,12 @@ const { walletAddress, sellerView } = storeToRefs(etherStore);
const menuOpenToggle = ref<boolean>(false);
const menuHoverToggle = ref<boolean>(false);
const infoMenuOpenToggle = ref<boolean>(false);
const currencyMenuOpenToggle = ref<boolean>(false);
const currencyMenuHoverToggle = ref<boolean>(false);
const infoMenuRef = ref<any>(null);
const walletAddressRef = ref<any>(null);
const currencyRef = ref<any>(null);
//Methods
const connectMetaMask = async (): Promise<void> => {
@ -58,32 +63,161 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
return validImages[networkName];
};
onClickOutside(walletAddressRef, () => {
menuHoverToggle.value = false;
menuOpenToggle.value = false;
});
onClickOutside(currencyRef, () => {
currencyMenuOpenToggle.value = false;
currencyMenuHoverToggle.value = false;
});
onClickOutside(infoMenuRef, () => {
infoMenuOpenToggle.value = false;
});
</script>
<template>
<header>
<header class="z-20">
<RouterLink :to="'/'" class="default-button">
<img
alt="P2Pix logo"
class="logo"
class="logo lg-view"
src="@/assets/logo.svg"
width="75"
height="75"
/>
<img
alt="P2Pix logo"
class="logo sm-view w-10/12"
src="@/assets/logo2.svg"
width="40"
height="40"
/>
</RouterLink>
<div class="flex gap-4 items-center">
<RouterLink :to="'/faq'" class="default-button"> FAQ </RouterLink>
<RouterLink :to="sellerView ? '/' : '/seller'" class="default-button">
<div class="flex sm:gap-4 gap-2 items-center">
<div class="flex flex-col">
<div
v-show="infoMenuOpenToggle"
class="mt-10 absolute w-full text-gray-900 lg-view"
>
<div class="mt-2">
<div class="bg-white rounded-md z-10 -left-32 w-52">
<div class="menu-button gap-2 px-4 rounded-md cursor-pointer">
<span class="text-gray-900 py-4 text-end font-semibold text-sm">
Documentação
</span>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<RouterLink
:to="'/faq'"
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
>
<span
class="text-gray-900 py-4 text-end font-semibold text-sm whitespace-nowrap"
>
Perguntas frequentes
</span>
</RouterLink>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<div
class="sm:text-center sm:justify-center ml-8 sm:ml-0 gap-2 px-4 rounded-md float-right"
>
<div class="redirect_button flex mr-4">
<div class="mr-6">
<img
alt="Twitter"
width="20"
height="20"
src="@/assets/twitterIcon.svg"
class="cursor-pointer"
onclick="location.href = 'https://www.twitter.com/doiim';"
/>
</div>
<div class="mr-6">
<img
alt="Discord"
width="20"
height="20"
src="@/assets/discordIcon.svg"
class="cursor-pointer"
/>
</div>
<img
alt="Github"
width="20"
height="20"
src="@/assets/githubIcon.svg"
class="cursor-pointer"
onclick="location.href = 'https://github.com/doiim';"
/>
</div>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
</div>
</div>
</div>
<div
ref="infoMenuRef"
class="default-button lg-view cursor-pointer"
@click="
[
(infoMenuOpenToggle = !infoMenuOpenToggle),
(menuOpenToggle = false),
(currencyMenuOpenToggle = false),
]
"
>
<h1
class="text-gray-50 font-semibold sm:text-base"
:style="{
'border-bottom': infoMenuOpenToggle
? '2px solid white'
: 'transparent',
}"
>
Menu
</h1>
</div>
</div>
<RouterLink
:to="'/faq'"
v-if="!walletAddress"
class="default-button sm-view"
>
FAQ
</RouterLink>
<RouterLink
:to="sellerView ? '/' : '/seller'"
class="default-button sm:whitespace-normal whitespace-nowrap lg-view"
>
{{ sellerView ? "Quero comprar" : "Quero vender" }}
</RouterLink>
<RouterLink
:to="sellerView ? '/' : '/seller'"
v-if="!walletAddress"
class="default-button sm:whitespace-normal whitespace-nowrap sm-view"
>
{{ sellerView ? "Quero comprar" : "Quero vender" }}
</RouterLink>
<div class="flex flex-col" v-if="walletAddress">
<div
class="group top-bar-info cursor-pointer hover:bg-white"
ref="currencyRef"
class="group top-bar-info cursor-pointer hover:bg-white h-10"
@click="
[
(currencyMenuOpenToggle = !currencyMenuOpenToggle),
(menuOpenToggle = false),
(infoMenuOpenToggle = false),
]
"
@mouseover="currencyMenuHoverToggle = true"
@ -101,7 +235,7 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
:src="getNetworkImage(etherStore.networkName)"
/>
<span
class="default-text group-hover:text-gray-900"
class="default-text group-hover:text-gray-900 lg-view"
:style="{
color: currencyMenuOpenToggle
? '#000000'
@ -131,7 +265,7 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
</div>
<div
v-show="currencyMenuOpenToggle"
class="mt-10 pl-3 absolute w-full text-gray-900"
class="mt-10 pl-3 absolute w-full text-gray-900 lg-view"
>
<div class="mt-2">
<div class="bg-white rounded-md z-10">
@ -176,19 +310,29 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
<button
type="button"
v-if="!walletAddress"
class="border-amber-500 border-2 rounded default-button"
class="border-amber-500 border-2 rounded default-button lg-view"
@click="connectMetaMask()"
>
Conectar carteira
</button>
<button
type="button"
v-if="!walletAddress"
class="border-amber-500 border-2 rounded default-button sm-view"
@click="connectMetaMask()"
>
Conectar
</button>
<div v-if="walletAddress" class="account-info">
<div class="flex flex-col">
<div
class="top-bar-info cursor-pointer"
ref="walletAddressRef"
class="top-bar-info cursor-pointer h-10"
@click="
[
(menuOpenToggle = !menuOpenToggle),
(currencyMenuOpenToggle = false),
(infoMenuOpenToggle = false),
]
"
@mouseover="menuHoverToggle = true"
@ -203,7 +347,7 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
>
<img alt="Account image" src="@/assets/account.svg" />
<span
class="default-text text-sm"
class="default-text"
:style="{
color: menuOpenToggle
? '#000000'
@ -233,18 +377,10 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
</div>
<div
v-show="menuOpenToggle"
class="mt-10 absolute w-full text-gray-900"
class="mt-10 absolute w-full text-gray-900 lg-view"
>
<div class="pl-4 mt-2">
<div class="bg-white rounded-md z-10">
<div class="menu-button" @click="closeMenu()">
<RouterLink to="/transaction_history" class="redirect_button">
Histórico de transações
</RouterLink>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<div class="menu-button" @click="closeMenu()">
<RouterLink to="/manage_bids" class="redirect_button">
Gerenciar Ofertas
@ -264,6 +400,111 @@ const getNetworkImage = (networkName: NetworkEnum): string => {
</div>
</div>
</div>
<div
v-show="menuOpenToggle"
class="mobile-menu fixed w-4/5 text-gray-900 sm-view"
>
<div class="pl-4 mt-2 h-full">
<div class="bg-white rounded-md z-10 h-full">
<div class="menu-button" @click="closeMenu()">
<RouterLink
:to="sellerView ? '/' : '/seller'"
class="redirect_button mt-2"
>
{{ sellerView ? "Quero comprar" : "Quero vender" }}
</RouterLink>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<div class="menu-button" @click="closeMenu()">
<RouterLink to="/manage_bids" class="redirect_button">
Gerenciar Ofertas
</RouterLink>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<div class="menu-button" @click="disconnectUser">
<RouterLink to="/" class="redirect_button">
Desconectar
</RouterLink>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<div class="menu-button pb-10">
<div class="redirect_button flex">
<img
alt="Twitter"
width="20"
height="20"
src="@/assets/twitterIcon.svg"
class="mr-6"
onclick="location.href = 'https://www.twitter.com/doiim';"
/>
<img
alt="Discord"
width="20"
height="20"
src="@/assets/discordIcon.svg"
class="mr-6"
/>
<img
alt="Github"
width="20"
height="20"
src="@/assets/githubIcon.svg"
onclick="location.href = 'https://github.com/doiim';"
/>
</div>
</div>
</div>
</div>
</div>
<div
v-show="currencyMenuOpenToggle"
class="mobile-menu fixed w-4/5 text-gray-900 sm-view"
>
<div class="pl-4 mt-2 h-full">
<div class="bg-white rounded-md z-10 h-full">
<div
class="menu-button gap-2 sm:px-4 rounded-md cursor-pointer py-2"
@click="networkChange(NetworkEnum.ethereum)"
>
<img
alt="Ethereum image"
width="20"
height="20"
src="@/assets/ethereum.svg"
/>
<span class="text-gray-900 py-4 text-end font-bold text-sm">
Ethereum
</span>
</div>
<div class="w-full flex justify-center">
<hr class="w-4/5" />
</div>
<div
class="menu-button gap-2 sm:px-4 rounded-md cursor-pointer py-2"
@click="networkChange(NetworkEnum.polygon)"
>
<img
alt="Polygon image"
width="20"
height="20"
src="@/assets/polygon.svg"
/>
<span class="text-gray-900 py-4 text-end font-bold text-sm">
Polygon
</span>
</div>
<div class="w-full flex justify-center pb-12">
<hr class="w-4/5" />
</div>
</div>
</div>
</div>
</header>
</template>
@ -273,7 +514,7 @@ header {
}
.default-button {
@apply px-4 py-2 rounded text-gray-50 font-semibold text-base hover:bg-transparent;
@apply sm:px-4 px-3 py-2 rounded text-gray-50 font-semibold sm:text-base text-xs hover:bg-transparent;
}
.account-info {
@ -285,18 +526,42 @@ header {
}
.top-bar-info {
@apply flex justify-between gap-2 px-4 py-2 border-amber-500 border-2 rounded;
@apply flex justify-between gap-2 px-4 py-2 border-amber-500 border-2 sm:rounded rounded-lg items-center;
}
.redirect_button {
@apply py-4 text-gray-900 font-semibold text-xs w-full;
@apply py-5 text-gray-900 sm:font-semibold font-bold sm:text-xs text-sm w-full;
}
.menu-button {
@apply flex text-center justify-center hover:bg-gray-200;
@apply flex sm:text-center sm:justify-center hover:bg-gray-200 ml-8 sm:ml-0;
}
a:hover {
@apply bg-gray-200 rounded;
}
.lg-view {
display: inline-block;
}
.sm-view {
display: none;
}
.mobile-menu {
margin-top: 1400px;
bottom: 0px;
height: auto;
}
@media screen and (max-width: 500px) {
.lg-view {
display: none;
}
.sm-view {
display: inline-block;
}
}
</style>

View File

@ -30,6 +30,6 @@ describe("TopBar.vue", () => {
it("should render the P2Pix logo correctly", () => {
const wrapper = shallowMount(TopBar);
const img = wrapper.findAll(".logo");
expect(img.length).toBe(1);
expect(img.length).toBe(2);
});
});

View File

@ -0,0 +1,6 @@
import type { Pix } from "./Pix";
export type UnreleasedLock = {
lockID: string;
pix: Pix;
};

View File

@ -1,10 +1,8 @@
import type { BigNumber } from "ethers";
export type ValidDeposit = {
depositID: BigNumber;
token: string;
blockNumber: number;
remaining: number;
seller: string;
pixKey: string;
pixTarget?: string;
pixKey: number;
open?: boolean;
};

View File

@ -0,0 +1,11 @@
export type WalletTransaction = {
token: string;
blockNumber: number;
amount: number;
seller: string;
buyer: string;
event: string;
lockStatus: number;
transactionHash: string;
transactionID?: string;
};

View File

@ -89,4 +89,33 @@ export const MockEvents: Event[] = [
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

@ -1,40 +1,39 @@
import { parseEther } from "ethers/lib/utils";
import type { ValidDeposit } from "../ValidDeposit";
export const MockValidDeposits: ValidDeposit[] = [
{
blockNumber: 1,
depositID: parseEther("1"),
token: "1",
remaining: 70,
seller: "mockedSellerAddress",
pixKey: "123456789",
pixKey: 123456789,
},
{
blockNumber: 2,
depositID: parseEther("2"),
token: "2",
remaining: 200,
seller: "mockedSellerAddress",
pixKey: "123456789",
pixKey: 123456789,
},
{
blockNumber: 3,
depositID: parseEther("3"),
token: "3",
remaining: 1250,
seller: "mockedSellerAddress",
pixKey: "123456789",
pixKey: 123456789,
},
{
blockNumber: 4,
depositID: parseEther("4"),
token: "4",
remaining: 4000,
seller: "mockedSellerAddress",
pixKey: "123456789",
pixKey: 123456789,
},
{
blockNumber: 5,
depositID: parseEther("5"),
token: "5",
remaining: 2000,
seller: "mockedSellerAddress",
pixKey: "123456789",
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,8 +1,7 @@
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import TransactionHistoryView from "../views/TransactionHistoryView.vue";
import FaqView from "../views/FaqView.vue";
import ManageBidsView from "../views/ManageBidsView.vue";
import HomeView from "@/views/HomeView.vue";
import FaqView from "@/views/FaqView.vue";
import ManageBidsView from "@/views/ManageBidsView.vue";
import SellerView from "@/views/SellerView.vue";
const router = createRouter({
@ -12,17 +11,18 @@ const router = createRouter({
path: "/",
name: "home",
component: HomeView,
props: true,
},
{
path: "/:lockID",
name: "redirect buy",
component: HomeView,
},
{
path: "/seller",
name: "seller",
component: SellerView,
},
{
path: "/transaction_history",
name: "transaction history",
component: TransactionHistoryView,
},
{
path: "/manage_bids",
name: "manage bids",

View File

@ -13,6 +13,8 @@ export const useEtherStore = defineStore("ether", {
depositsValidListGoerli: [] as ValidDeposit[],
// Depósitos válidos para compra MUMBAI
depositsValidListMumbai: [] as ValidDeposit[],
loadingWalletTransactions: false,
loadingNetworkLiquidity: false,
}),
actions: {
setWalletAddress(walletAddress: string) {
@ -36,6 +38,12 @@ export const useEtherStore = defineStore("ether", {
setDepositsValidListMumbai(depositsValidList: ValidDeposit[]) {
this.depositsValidListMumbai = depositsValidList;
},
setLoadingWalletTransactions(isLoadingWalletTransactions: boolean) {
this.loadingWalletTransactions = isLoadingWalletTransactions;
},
setLoadingNetworkLiquidity(isLoadingNetworkLiquidity: boolean) {
this.loadingNetworkLiquidity = isLoadingNetworkLiquidity;
},
},
// Alterar para integrar com mumbai
getters: {

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

@ -0,0 +1,53 @@
[
{
"name": "1. Como Começar",
"items": [
{
"title": "O que é uma carteira (wallet)?",
"content": "Cripto-wallet é um software que armazena criptomoedas e criptoativos de forma segura. Existem também dispositivos físicos chamados de cold/hard wallets. Com a carteira-cripto você consegue comprar e vender tokens sem a necessidade de uma corretora (exchange), por exemplo. Cripto-ativos não ficam de fato armazenados nessa carteira, mas sim as informações que possibilitam acessá-los numa blockchain, como suas chaves pública e privada.\n<br>\n\n* **O que é um endereço de carteira (wallet address)?**\n Sequência alfanumérica que permite que você envie e receba suas criptomoedas em segurança. Por exemplo: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy.\n\n* **Como conectar sua carteira ao p2pix?**\n Clique em 'Conectar Carteira'. Escolha qual wallet ou ponte (bridge) utilizará para se conectar (MetaMask ou WalletConnect). Ao clicar, sua carteira se abrirá. Caso seja seu 1º acesso, certifique-se de que permitiu sua carteira a se conectar com nossa plataforma. \n\n* **Como instalar MetaMask no Google Chrome?** \n MetaMask é uma web3 wallet que pode ser adicionada como extensão no seu browser (navegador). Para instalar esse plugin (add-on) no Google Chrome, basta você acessar a Chrome Web Store e pesquisar “MetaMask”. Certifique-se de que é a versão legítima da carteira pelo número de downloads (bastante alto, pois é a maior cold wallet da web3), então é só clicar em “Adicionar ao Chrome”. Depois da instalação ser concluída, basta seguir o passo-a-passo proposto na interface da MetaMask para a criação da conta, login e senha. O caminho para a instalação em outros browsers como Firefox, Opera, entre outros navegadores, é o mesmo. \n\n<br>\nNota: Não esqueça de guardar sua seed phrase em um local seguro! Sem ela é impossível recuperar o acesso à sua carteira (caso perca sua senha) e também aos seus cripto ativos, eles serão perdidos - literalmente - para sempre.\n"
},
{
"title": "O que é uma rede (network)?",
"content": "Criptomoedas são transacionadas em redes de computadores, nas blockchains é que ficam armazenadas as informações sobre suas transações, não em um servidor. Existem [diversas redes no ecossistema cripto](https://www.gemini.com/cryptopedia/blockchain-technology-explained), como a rede do Bitcoin, da Ethereum, para citar as duas mais conhecidas. \n<br>\n\n* **Como selecionar a rede?**\nVocê deve ficar atento a rede que está selecionada em sua carteira. Na MetaMask, escolha a você escolhe a rede que vai operar clicando na primeira caixa ao lado do seu avatar:\n![Image Ethereum](/src/assets/faqEthereum.jpeg)\nCaso você copie um endereço na sua MetaMask estando selecionada a Rede Ethereum Mainnet e queira operar na Polygon, seus cripto ativos serão perdidos. \n\n* **O que é taxa de rede (gas fee)?**\nCusto de transação do blockchain. Você paga essa taxa para a rede que vai remunerar os validadores. É uma forma de incentivar os validadores a dar continuidade nos seus serviços. "
}
]
},
{
"name": "2. Comprar tokens",
"items": [
{
"title": "Como comprar?",
"content": "1. Conectar wallet (MetaMask ou WalletConnect)\n2. Digitar amount (quantidade);\n3. Solicitar token (criptomoeda);\n4. Identificar vendedor (da lista de pessoas);\n * Escanear QRCode (ou acrescenta chave manualmente);\n * Colar e2eID (identificação end-to-end) a.k.a. código de identificação (API Pix / comprovante bancário);\n5. Receber na carteira (wallet conectada."
},
{
"title": "O que é uma stablecoin?",
"content": "É uma criptomoeda com lastro em moeda fiduciária (por exemplo, Real brasileiro ou Dólar americando). Cada stablecoin gerada por um projeto emissor tem a mesma quantidade em reserva da moeda fiduciária em seu caixa. Alguns exemplos de stablecoins pareadas com dólar americano são USDt, DAI, USDc. Já vinculadas ao preço do Real são BRZ, MBRL e cREAL.\n<br>\n\n* **O que é o BRZ?**\n [BRZ](https://www.brztoken.io/) é a sigla para Brazilian Digital Token. É um tipo de criptomoeda que tem valor pareado com o Real (BRL). Ou seja, 1 BRZ tem valor igual a 1 BRL."
},
{
"title": "De quem estou comprando?",
"content": "Vendedores fazem as ofertas e indicam a chave-Pix que irão receber o valor transacionado. Essa oferta fica travada no smart contract até que as transações sejam concluídas e você possa fazer o saque dos seus tokens. \n\n<br>\n\n* **Onde encontrar comprovante do Pix?**\n Quando você faz um Pix um comprovante é gerado automaticamente para o pagador e para o recebedor. Esse comprovante pode ser acessado no momento da transação via app/site do banco ou pelo seu extrato bancário convencional.\n* **Para onde vão os tokens que eu comprei?**\n Os tokens comprados terão como destino a carteira que você conectou ao p2pix. \n Nota: Lembre-se sempre de configurar a sua carteira na rede correta em que quer receber seus tokens. "
}
]
},
{
"name": "3. Vender tokens",
"items": [
{
"title": "Como vender?",
"content": "1. Enviar tokens;\n2. Indique sua chave-Pix;\n3. Criar [whitelist](https://academy.binance.com/en/glossary/whitelist) onde quer receber isso;\n4. Withdraw (saque) para remoção dos tokens;\n5. Lock (trava de 24h) para esperar transações antes dos saques.\n"
},
{
"title": "O que é aprovar tokens?",
"content": "Aprovações de tokens funcionam como permissões de sua carteira a um determinado [dApp](https://ethereum.org/en/developers/docs/dapps/) para movimentar os tokens. Ao aprovar tokens, você permite que nosso contrato inteligente acesse e execute as transações na sua carteira web3."
},
{
"title": "O que é o lock na rede?",
"content": "\"Travamento\" na rede é uma forma de especificar um tempo para retirada dos ativos digitais. O contrato inteligente do p2pix 'trava' os tokens enviados para a rede por 24 horas. Só depois de transcorridas essas 24h que o saque dos tokens estará liberado. Isso ocorre para garantir uma transação segura para os vendedores."
},
{
"title": "Como retirar a oferta?",
"content": "Caso você desista da sua oferta, você pode invalidar sua ordem de venda e impedir um novo lock na rede."
}
]
}
]

16
src/utils/pixKeyFormat.ts Normal file
View File

@ -0,0 +1,16 @@
export const pixFormatValidation = (pixKey: string): boolean => {
const cpf = /(^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$)/g;
const cnpj = /(^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$)/g;
const telefone = /(^[0-9]{2})?(\s|-)?(9?[0-9]{4})-?([0-9]{4}$)/g;
if (pixKey.match(cpf) || pixKey.match(cnpj) || pixKey.match(telefone)) {
return true;
}
return false;
};
export const postProcessKey = (pixKey: string): string => {
pixKey = pixKey.replace(/[-.()/]/g, "");
return pixKey;
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
{
"signers": [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
],
"p2pix": "0x5f3EFA9A90532914545CEf527C530658af87e196",
"token": "0x294003F602c321627152c6b7DED3EAb5bEa853Ee"
}

View File

@ -1,8 +0,0 @@
{
"signers": [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
],
"p2pix": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
"token": "0x5FbDB2315678afecb367f032d93F642f64180aa3"
}

View File

@ -1,8 +0,0 @@
{
"signers": [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
],
"p2pix": "0x5f3EFA9A90532914545CEf527C530658af87e196",
"token": "0x294003F602c321627152c6b7DED3EAb5bEa853Ee"
}

View File

@ -1,6 +0,0 @@
{
"wallets":[
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"
]
}

View File

@ -2,69 +2,9 @@
import type { Faq } from "@/model/Faq";
import { ref } from "vue";
import { marked } from "marked";
import faqContent from "@/utils/files/faqContent.json";
const faq = ref<Faq>([
{
name: "1. Como Começar",
items: [
{
title: "O que é uma carteira (wallet)",
content:
"# Lorem ipsum dolor sit **amet** consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
{
title: "O que é uma carteira (wallet)",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
{
title: "O que é uma carteira (wallet)",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
],
},
{
name: "2. Comprar tokens",
items: [
{
title: "O que é um endereço de carteira (wallet address)",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
{
title: "O que é um endereço de carteira (wallet address)",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
{
title: "O que é um endereço de carteira (wallet address)",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
],
},
{
name: "3. Vender tokens",
items: [
{
title: "Como conectar a carteira ao p2pix",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
{
title: "Como conectar a carteira ao p2pix",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
{
title: "Como conectar a carteira ao p2pix",
content:
"Lorem ipsum dolor sit amet consectetur. Neque libero magna mi purus. Aliquam vitae feugiat quis sapien. Pharetra gravida nisi donec bibendum mauris aliquam molestie. Et nunc placerat in ac integer maecenas arcu. Lotuam tincidunt morbi ac sed fames habitasse velit et nunc.",
},
],
},
]);
const faq = ref<Faq>(faqContent);
const selectedSection = ref<number>(0);
@ -76,9 +16,15 @@ const openItem = (index: number) => {
faq.value[selectedSection.value].items[index].isOpen =
!faq.value[selectedSection.value].items[index].isOpen;
marked.setOptions({
breaks: true,
gfm: true,
});
faq.value[selectedSection.value].items[index].content = marked(
faq.value[selectedSection.value].items[index].content
);
console.log(marked(faq.value[selectedSection.value].items[index].content));
};
</script>
@ -96,7 +42,7 @@ const openItem = (index: number) => {
<div class="flex justify-between w-10/12 mt-20">
<div>
<h1 class="text-3xl text-gray-800 font-bold">Sumário</h1>
<h1 class="text-3xl text-white font-bold">Sumário</h1>
<h3
:class="index == selectedSection ? 'selected-sumario' : 'sumario'"
v-for="(f, index) in faq"
@ -125,13 +71,9 @@ const openItem = (index: number) => {
class="mr-3"
v-if="item.isOpen"
/>
<h4>{{ item.title }}</h4>
<h4 class="text-white">{{ item.title }}</h4>
</div>
<div
style="padding-top: 24px"
v-if="item.isOpen"
v-html="item.content"
></div>
<div class="content" v-if="item.isOpen" v-html="item.content"></div>
<div class="hr"></div>
</div>
</div>
@ -154,17 +96,30 @@ const openItem = (index: number) => {
@apply flex flex-col items-center justify-center w-full mt-16;
}
div.content {
padding-top: 24px;
color: white;
}
.content :deep(ul) {
@apply list-disc m-2 p-3;
}
.content :deep(ol) {
@apply list-decimal m-2 p-3;
}
.content :deep(ol ul) {
@apply list-disc m-1 p-1;
}
.hr {
border: 1px solid #1f2937;
margin: 24px 0;
}
p,
h2,
h3,
h4 {
@apply text-gray-800 text-xl;
color: #1f2937;
h3 {
@apply text-white;
}
h2,
@ -177,7 +132,7 @@ h4 {
}
.text {
@apply text-gray-800 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;

View File

@ -1,19 +1,17 @@
<script setup lang="ts">
import SearchComponent from "@/components/SearchComponent.vue";
import ValidationComponent from "@/components/LoadingComponent.vue";
import LoadingComponent from "@/components/LoadingComponent/LoadingComponent.vue";
import BuyConfirmedComponent from "@/components/BuyConfirmedComponent/BuyConfirmedComponent.vue";
import { ref, onMounted } from "vue";
import { ref, onMounted, watch } from "vue";
import { useEtherStore } from "@/store/ether";
import QrCodeComponent from "@/components/QrCodeComponent.vue";
import { storeToRefs } from "pinia";
import { addLock, releaseLock } from "@/blockchain/buyerMethods";
import {
updateWalletStatus,
listReleaseTransactionByWalletAddress,
} from "@/blockchain/wallet";
import { updateWalletStatus, checkUnreleasedLock } from "@/blockchain/wallet";
import { getNetworksLiquidity } from "@/blockchain/events";
import type { Event } from "ethers";
import type { ValidDeposit } from "@/model/ValidDeposit";
import { getUnreleasedLockById } from "@/blockchain/events";
import CustomAlert from "@/components/CustomAlert/CustomAlert.vue";
enum Step {
Search,
@ -25,13 +23,15 @@ const etherStore = useEtherStore();
etherStore.setSellerView(false);
// States
const { loadingLock, walletAddress } = storeToRefs(etherStore);
const { loadingLock, walletAddress, networkName } = storeToRefs(etherStore);
const flowStep = ref<Step>(Step.Search);
const pixTarget = ref<string>("");
const pixTarget = ref<number>();
const tokenAmount = ref<number>();
const _lockID = ref<string>("");
const lockID = ref<string>("");
const loadingRelease = ref<boolean>(false);
const lastWalletReleaseTransactions = ref<Event[]>([]);
const showModal = ref<boolean>(false);
const showBuyAlert = ref<boolean>(false);
const paramLockID = window.history.state?.lockID;
const confirmBuyClick = async (
selectedDeposit: ValidDeposit,
@ -46,9 +46,9 @@ const confirmBuyClick = async (
flowStep.value = Step.Buy;
etherStore.setLoadingLock(true);
await addLock(selectedDeposit.depositID, tokenValue)
.then((lockID) => {
_lockID.value = lockID;
await addLock(selectedDeposit.seller, selectedDeposit.token, tokenValue)
.then((_lockID) => {
lockID.value = _lockID;
})
.catch((err) => {
console.log(err);
@ -61,31 +61,60 @@ const confirmBuyClick = async (
const releaseTransaction = async (e2eId: string) => {
flowStep.value = Step.List;
showBuyAlert.value = true;
loadingRelease.value = true;
if (_lockID.value && tokenAmount.value) {
if (lockID.value && tokenAmount.value && pixTarget.value) {
const release = await releaseLock(
pixTarget.value,
tokenAmount.value,
e2eId,
_lockID.value
lockID.value
);
release.wait();
await listReleaseTransactionByWalletAddress(
walletAddress.value.toLowerCase()
).then((releaseTransactions) => {
if (releaseTransactions)
lastWalletReleaseTransactions.value = releaseTransactions;
});
await release.wait();
await updateWalletStatus();
loadingRelease.value = false;
}
};
const checkForUnreleasedLocks = async (): Promise<void> => {
const walletLocks = await checkUnreleasedLock(walletAddress.value);
if (walletLocks) {
lockID.value = walletLocks.lockID;
tokenAmount.value = walletLocks.pix.value;
pixTarget.value = Number(walletLocks.pix.pixKey);
showModal.value = true;
} else {
flowStep.value = Step.Search;
showModal.value = false;
}
};
if (paramLockID) {
const lockToRedirect = await getUnreleasedLockById(paramLockID as string);
if (lockToRedirect) {
lockID.value = lockToRedirect.lockID;
tokenAmount.value = lockToRedirect.pix.value;
pixTarget.value = Number(lockToRedirect.pix.pixKey);
flowStep.value = Step.Buy;
} else {
flowStep.value = Step.Search;
}
} else {
watch(walletAddress, async () => {
await checkForUnreleasedLocks();
});
watch(networkName, async () => {
if (walletAddress.value) await checkForUnreleasedLocks();
});
}
onMounted(async () => {
await getNetworksLiquidity();
if (walletAddress.value && !paramLockID) await checkForUnreleasedLocks();
window.history.state.lockID = "";
});
</script>
@ -94,26 +123,40 @@ onMounted(async () => {
v-if="flowStep == Step.Search"
@token-buy="confirmBuyClick"
/>
<CustomAlert
v-if="flowStep == Step.Search && showModal"
:type="'redirect'"
@close-alert="showModal = false"
@go-to-lock="flowStep = Step.Buy"
/>
<CustomAlert
v-if="
flowStep == Step.List && showBuyAlert && !loadingLock && !loadingRelease
"
:type="'buy'"
@close-alert="showBuyAlert = false"
/>
<div v-if="flowStep == Step.Buy">
<QrCodeComponent
:pixTarget="pixTarget"
:pixTarget="String(pixTarget)"
:tokenValue="tokenAmount"
@pix-validated="releaseTransaction"
v-if="!loadingLock"
/>
<ValidationComponent
<LoadingComponent
v-if="loadingLock"
:message="'A transação está sendo enviada para a rede'"
/>
</div>
<div v-if="flowStep == Step.List">
<div class="flex flex-col gap-10" v-if="!loadingRelease">
<BuyConfirmedComponent
v-if="!loadingRelease"
:last-wallet-release-transactions="lastWalletReleaseTransactions"
:tokenAmount="tokenAmount"
:is-current-step="flowStep == Step.List"
@make-another-transaction="flowStep = Step.Search"
/>
<ValidationComponent
</div>
<LoadingComponent
v-if="loadingRelease"
:message="'A transação está sendo enviada para a rede. Em breve os tokens serão depositados em sua carteira.'"
/>

View File

@ -2,75 +2,115 @@
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
import type { BigNumber } from "ethers";
import LoadingComponent from "@/components/LoadingComponent/LoadingComponent.vue";
import CustomAlert from "@/components/CustomAlert/CustomAlert.vue";
import { ref, watch, onMounted } from "vue";
import { cancelDeposit, withdrawDeposit } from "@/blockchain/buyerMethods";
import { listValidDepositTransactionsByWalletAddress } from "@/blockchain/wallet";
import {
listValidDepositTransactionsByWalletAddress,
listAllTransactionByWalletAddress,
getActiveLockAmount,
} from "@/blockchain/wallet";
import { withdrawDeposit } from "@/blockchain/buyerMethods";
import type { ValidDeposit } from "@/model/ValidDeposit";
import type { WalletTransaction } from "@/model/WalletTransaction";
import router from "@/router/index";
const etherStore = useEtherStore();
const { walletAddress, networkName } = storeToRefs(etherStore);
const depositList = ref<ValidDeposit[]>([]);
const loadingWithdraw = ref<boolean>(false);
const showAlert = ref<boolean>(false);
onMounted(async () => {
const depositList = ref<ValidDeposit[]>([]);
const transactionsList = ref<WalletTransaction[]>([]);
const activeLockAmount = ref<number>(0);
const callWithdraw = async (amount: string) => {
if (amount) {
loadingWithdraw.value = true;
let withdraw;
try {
withdraw = await withdrawDeposit(amount);
} catch {
loadingWithdraw.value = false;
}
if (withdraw) {
console.log("Saque realizado!");
await getWalletTransactions();
showAlert.value = true;
} else {
console.log("Não foi possível realizar o saque!");
}
loadingWithdraw.value = false;
}
};
const getWalletTransactions = async () => {
etherStore.setLoadingWalletTransactions(true);
if (walletAddress.value) {
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
walletAddress.value
);
const allUserTransactions = await listAllTransactionByWalletAddress(
walletAddress.value
);
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
if (walletDeposits) {
depositList.value = walletDeposits;
}
if (allUserTransactions) {
transactionsList.value = allUserTransactions;
}
}
etherStore.setLoadingWalletTransactions(false);
};
onMounted(async () => {
if (!walletAddress.value) {
router.push({ name: "home" });
}
await getWalletTransactions();
});
const handleCancelDeposit = async (depositID: BigNumber, index: number) => {
const response = await cancelDeposit(depositID);
if (response) {
console.log("Depósito cancelado com sucesso.");
depositList.value.splice(index, 1);
}
};
const handleWithDrawDeposit = async (depositID: BigNumber, index: number) => {
const response = await withdrawDeposit(depositID);
if (response) {
console.log("Token retirado com sucesso.");
depositList.value.splice(index, 1);
}
};
watch(walletAddress, async () => {
await listValidDepositTransactionsByWalletAddress(walletAddress.value)
.then((res) => {
if (res) depositList.value = res;
})
.catch(() => {
depositList.value = [];
});
await getWalletTransactions();
});
watch(networkName, async () => {
await listValidDepositTransactionsByWalletAddress(walletAddress.value)
.then((res) => {
if (res) depositList.value = res;
})
.catch(() => {
depositList.value = [];
});
await getWalletTransactions();
});
</script>
<template>
<CustomAlert
v-if="showAlert"
:type="'withdraw'"
@close-alert="showAlert = false"
/>
<div class="page">
<div class="header">Gerenciar ofertas</div>
<div class="header" v-if="!loadingWithdraw && !walletAddress">
Por Favor Conecte Sua Carteira
</div>
<div class="header" v-if="!loadingWithdraw && walletAddress">
Gerenciar Ofertas
</div>
<div class="w-full max-w-4xl">
<ListingComponent
:wallet-transactions="depositList"
:is-manage-mode="true"
@cancel-deposit="handleCancelDeposit"
@withdraw-deposit="handleWithDrawDeposit"
v-if="!loadingWithdraw && walletAddress"
:valid-deposits="depositList"
:wallet-transactions="transactionsList"
:active-lock-amount="activeLockAmount"
@deposit-withdrawn="callWithdraw"
></ListingComponent>
<LoadingComponent
v-if="loadingWithdraw"
:message="'A transação está sendo enviada para a rede. Em breve os tokens serão depositados em sua carteira.'"
/>
</div>
</div>
</template>
@ -81,6 +121,6 @@ watch(networkName, async () => {
}
.header {
@apply text-3xl text-gray-900 leading-9 font-bold justify-center flex;
@apply text-3xl text-white leading-9 font-bold justify-center flex;
}
</style>

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import WantSellComponent from "../components/SellerSteps/WantSellComponent.vue";
import SendNetwork from "../components/SellerSteps/SendNetwork.vue";
import ValidationComponent from "../components/LoadingComponent.vue";
import { approveTokens, addDeposit } from "../blockchain/sellerMethods";
import WantSellComponent from "@/components/SellerSteps/WantSellComponent.vue";
import SendNetwork from "@/components/SellerSteps/SendNetwork.vue";
import LoadingComponent from "@/components/LoadingComponent/LoadingComponent.vue";
import { approveTokens, addDeposit } from "@/blockchain/sellerMethods";
import { ref } from "vue";
import { useEtherStore } from "@/store/ether";
import CustomAlert from "@/components/CustomAlert/CustomAlert.vue";
enum Step {
Search,
@ -21,13 +22,17 @@ const loading = ref<boolean>(false);
const offerValue = ref<string>("");
const pixKeyBuyer = ref<string>("");
const showAlert = ref<boolean>(false);
// Verificar tipagem
const approveOffer = async (args: { offer: string; pixKey: string }) => {
const approveOffer = async (args: {
offer: string;
postProcessedPixKey: string;
}) => {
loading.value = true;
try {
offerValue.value = args.offer;
pixKeyBuyer.value = args.pixKey;
pixKeyBuyer.value = args.postProcessedPixKey;
await approveTokens(args.offer);
flowStep.value = Step.Network;
loading.value = false;
@ -45,6 +50,7 @@ const sendNetwork = async () => {
await addDeposit(String(offerValue.value), pixKeyBuyer.value);
flowStep.value = Step.Sell;
loading.value = false;
showAlert.value = true;
}
} catch (err) {
console.log(err);
@ -57,11 +63,16 @@ const sendNetwork = async () => {
<template>
<div v-if="flowStep == Step.Sell">
<WantSellComponent v-if="!loading" @approve-tokens="approveOffer" />
<ValidationComponent
<LoadingComponent
v-if="loading"
:message="'A transação está sendo enviada para a rede.'"
/>
</div>
<CustomAlert
v-if="flowStep == Step.Sell && showAlert"
:type="'sell'"
@close-alert="showAlert = false"
/>
<div v-if="flowStep == Step.Network">
<SendNetwork
:pixKey="pixKeyBuyer"
@ -69,7 +80,7 @@ const sendNetwork = async () => {
v-if="!loading"
@send-network="sendNetwork"
/>
<ValidationComponent
<LoadingComponent
v-if="loading"
:message="'A transação está sendo enviada para a rede.'"
/>

View File

@ -1,63 +0,0 @@
<script setup lang="ts">
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { ref, watch, onMounted } from "vue";
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
import { listAllTransactionByWalletAddress } from "@/blockchain/wallet";
import type { Event } from "ethers";
import type { ValidDeposit } from "@/model/ValidDeposit";
const etherStore = useEtherStore();
const { walletAddress, networkName } = storeToRefs(etherStore);
const allUserTransactions = ref<(Event | ValidDeposit)[]>([]);
onMounted(async () => {
if (walletAddress.value) {
await listAllTransactionByWalletAddress(walletAddress.value).then((res) => {
if (res) allUserTransactions.value = res;
});
}
});
watch(walletAddress, async (newValue) => {
await listAllTransactionByWalletAddress(newValue)
.then((res) => {
if (res) allUserTransactions.value = res;
})
.catch(() => {
allUserTransactions.value = [];
});
});
watch(networkName, async () => {
await listAllTransactionByWalletAddress(walletAddress.value)
.then((res) => {
if (res) allUserTransactions.value = res;
})
.catch(() => {
allUserTransactions.value = [];
});
});
</script>
<template>
<div class="page">
<div class="header">Histórico de transações</div>
<div class="w-full max-w-4xl">
<ListingComponent
:wallet-transactions="allUserTransactions"
:is-manage-mode="false"
></ListingComponent>
</div>
</div>
</template>
<style scoped>
.page {
@apply flex flex-col gap-10 mt-20 w-full items-center;
}
.header {
@apply text-3xl text-gray-900 leading-9 font-bold justify-center flex;
}
</style>

View File

@ -13,6 +13,7 @@ export default defineConfig({
provider: "c8",
all: true,
src: ["./src"],
exclude: ["model/**", "**/__tests__/**"],
reporter: ["text", "lcov", "html"],
},
},

View File

@ -1320,6 +1320,26 @@
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@floating-ui/core@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.2.1.tgz#074182a1d277f94569c50a6b456e62585d463c8e"
integrity sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==
"@floating-ui/dom@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.2.1.tgz#8f93906e1a3b9f606ce78afb058e874344dcbe07"
integrity sha512-Rt45SmRiV8eU+xXSB9t0uMYiQ/ZWGE/jumse2o3i5RGlyvcbqOF4q+1qBnzLE2kZ5JGhq0iMkcGXUKbFe7MpTA==
dependencies:
"@floating-ui/core" "^1.2.1"
"@floating-ui/vue@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@floating-ui/vue/-/vue-0.2.1.tgz#a52b66e020897ad0535d0d0d3b09932446fc6231"
integrity sha512-HE+EIeakID7wI6vUwF0yMpaW48bNaPj8QtnQaRMkaQFhQReVBA4bY6fmJ3J7X+dqVgDbMhyfCG0fBJfdQMdWxQ==
dependencies:
"@floating-ui/dom" "^1.2.1"
vue-demi "^0.13.11"
"@headlessui/vue@^1.7.3":
version "1.7.3"
resolved "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.3.tgz"
@ -1498,6 +1518,11 @@
dependencies:
vue "^2.5.16"
"@types/web-bluetooth@^0.0.16":
version "0.0.16"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
"@typescript-eslint/eslint-plugin@^5.0.0":
version "5.40.1"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.1.tgz"
@ -1873,6 +1898,28 @@
resolved "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.1.3.tgz"
integrity sha512-kQVsh8yyWPvHpb8gIc9l/HIDiiVUy1amynLNpCy8p+FoCiZXCo6fQos5/097MmnNZc9AtseDsCrfkhqCrJ8Olg==
"@vueuse/core@^9.12.0":
version "9.12.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.12.0.tgz#e5b20f901e081c7ae5fe0e5f3af217929034eefe"
integrity sha512-h/Di8Bvf6xRcvS/PvUVheiMYYz3U0tH3X25YxONSaAUBa841ayMwxkuzx/DGUMCW/wHWzD8tRy2zYmOC36r4sg==
dependencies:
"@types/web-bluetooth" "^0.0.16"
"@vueuse/metadata" "9.12.0"
"@vueuse/shared" "9.12.0"
vue-demi "*"
"@vueuse/metadata@9.12.0":
version "9.12.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.12.0.tgz#19a0fefcba6a66a2382af10a7a67ebad6eec1f27"
integrity sha512-9oJ9MM9lFLlmvxXUqsR1wLt1uF7EVbP5iYaHJYqk+G2PbMjY6EXvZeTjbdO89HgoF5cI6z49o2zT/jD9SVoNpQ==
"@vueuse/shared@9.12.0":
version "9.12.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.12.0.tgz#e6597da80084cba8fc3d6545f4c2fa9817b80428"
integrity sha512-TWuJLACQ0BVithVTRbex4Wf1a1VaRuSpVeyEd4vMUWl54PzlE0ciFUshKCXnlLuD0lxIaLK4Ypj3NXYzZh4+SQ==
dependencies:
vue-demi "*"
abab@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
@ -5196,7 +5243,7 @@ vitest@^0.28.1:
vite-node "0.28.1"
why-is-node-running "^2.2.2"
vue-demi@*:
vue-demi@*, vue-demi@^0.13.11:
version "0.13.11"
resolved "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==