Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d6dc80f55 | ||
|
|
cea50bb22c | ||
|
|
a9d370ce9e | ||
| fe8cedf048 | |||
|
|
c47968333f | ||
|
|
7887f50b1a | ||
|
|
e182347f5e | ||
| 9f04c09704 | |||
| 36cd57b59e | |||
| 0bc4e4ccf3 | |||
| 00390ab0c3 | |||
| 5dc630acdf | |||
|
|
9828a44cf8 | ||
|
|
616302ffbe | ||
| cee0cc1296 | |||
| 72b9b64da8 | |||
| e2a03786e1 | |||
| 918e6fecde | |||
| 0cdc69d4da | |||
| ee783ea727 | |||
| 0761f74e1a | |||
| 289910df95 | |||
| c7a285c4a1 | |||
| c9ca178b24 | |||
| 555eac9e7c | |||
| 10569db3b1 | |||
| 2e2d2ff6d7 | |||
| e7bf32b6c3 | |||
| 4a09a323bd | |||
| 6c5c487874 | |||
| 4e65ab7ff0 | |||
| 7c17e940da | |||
| bdaffbd889 | |||
| 9c948d7da4 | |||
| af897e7dd4 | |||
| 663a0bce46 | |||
| 4469ccb30a | |||
| 98c6e04a16 | |||
| d63cb8c6d3 | |||
| c481d9d0a5 | |||
| 63f5ee017b | |||
| dacbeac019 | |||
| 18efb7543e | |||
| 6caf34b579 | |||
|
|
ec1053c660 | ||
|
|
46be71046a | ||
|
|
ebe03eb439 | ||
|
|
abeef0bd85 | ||
|
|
ea5773c7d0 | ||
|
|
95c3692bcb | ||
|
|
7cda8d5573 | ||
|
|
6cfe478177 | ||
|
|
f31fa15887 | ||
|
|
ad5b0a3a93 | ||
|
|
6979ba0402 | ||
|
|
43b955296a | ||
|
|
1cf9898e2d | ||
|
|
9c8ba43339 | ||
|
|
b655a3c4b6 | ||
|
|
42016d0101 | ||
|
|
674948120c | ||
|
|
3c8e9c0262 | ||
|
|
d686fca363 | ||
|
|
183fd698a9 | ||
|
|
100aab6b42 | ||
|
|
9a506acfa6 | ||
|
|
d603753654 | ||
|
|
b4f5134156 | ||
|
|
290e339f0c | ||
|
|
4c721e4431 | ||
|
|
1adef2dbb8 | ||
|
|
bf75cd766a | ||
|
|
d3eae76f91 | ||
|
|
a4163a2ba6 | ||
|
|
fad52d79d2 | ||
|
|
f64ea2ddf1 | ||
|
|
e67c8fcc77 | ||
|
|
ac670235cd | ||
|
|
38201bb254 | ||
|
|
fece86e305 | ||
|
|
b27b07fe47 | ||
|
|
d33d7f8538 | ||
|
|
57714fac9b | ||
|
|
9eee78fa91 | ||
|
|
4b4ade2bfa | ||
|
|
364cdd3b60 | ||
|
|
799f7cfe09 | ||
|
|
2117638305 | ||
|
|
a3e3f0506c | ||
|
|
976c48ac4b | ||
|
|
7bcf5d90c2 | ||
|
|
358ae7410f | ||
|
|
a906fa136d | ||
|
|
7ec73e8c6f | ||
|
|
84afed78fb | ||
|
|
2b6be86b2e | ||
|
|
fdc03068f2 | ||
|
|
c58e91e073 | ||
|
|
13c0fcc681 | ||
|
|
5c1d560d0c | ||
|
|
a24ee193d4 | ||
|
|
9b325ac917 | ||
|
|
c3d770f713 | ||
|
|
3ef1694217 | ||
|
|
2b707e81c2 | ||
|
|
f6a9ab854c | ||
|
|
474af2fbfc | ||
|
|
4af059f6b7 | ||
|
|
23163be99d | ||
|
|
c1542707c2 | ||
|
|
dd351acb2e | ||
|
|
2370051243 | ||
|
|
ed5d3b5726 | ||
|
|
2e246f7560 | ||
|
|
81c8b04c7a | ||
|
|
cf61f5ecfd | ||
|
|
73ba77ca4f | ||
|
|
c7b2f1643c | ||
|
|
fa2def812c | ||
|
|
8a1dab4764 | ||
|
|
75c02ed1b9 | ||
|
|
8eb10f493f | ||
|
|
1ec4780e14 | ||
|
|
0186afe971 | ||
|
|
bca93282ac | ||
|
|
fe06c46c3f | ||
|
|
4908dff58b | ||
|
|
9fa2b34a5d | ||
|
|
e93cac6086 | ||
|
|
3227e3209c | ||
|
|
54cff28ba0 | ||
|
|
d5f9c8f6fa | ||
|
|
0f17a67e00 | ||
|
|
c90f468d3c | ||
|
|
c4dae86b5f | ||
|
|
92f6cb4d35 | ||
|
|
9cda680494 | ||
|
|
1d98afbdfb | ||
|
|
be7c1c6132 | ||
|
|
5b49fdcffd | ||
|
|
b487949482 | ||
|
|
e29aa6f440 | ||
|
|
fc62f7b031 | ||
|
|
617388d8e1 | ||
|
|
9ddc843c01 | ||
|
|
659db5ef04 | ||
|
|
9205909f9f | ||
|
|
b61dfb2de0 | ||
|
|
53d52f3a14 | ||
|
|
e6117d77d7 | ||
|
|
6e59f77153 | ||
|
|
c8a5c74c71 | ||
|
|
6516ae0509 | ||
|
|
5408791d9f | ||
|
|
fc8fac4b48 | ||
|
|
149f0e4767 | ||
|
|
1287a0e9a7 | ||
|
|
bdf4e37018 | ||
|
|
61032e3d92 | ||
|
|
3cf9cd0cac | ||
|
|
8a45b3a9f8 | ||
|
|
e6b5bc7748 | ||
|
|
65c1dc0f06 | ||
|
|
15099c602f | ||
|
|
d796379119 | ||
|
|
475b0fbf01 | ||
|
|
135a01b7fd | ||
|
|
fd61376b36 | ||
|
|
42fcae0465 | ||
|
|
e483fd537c | ||
|
|
1f003f19bc | ||
|
|
4b4b5af550 | ||
|
|
659cc34d68 | ||
|
|
814e6a4eac | ||
|
|
f15361599f | ||
|
|
0a51a80e0c | ||
|
|
8ad42cc0f2 | ||
|
|
ae60edd324 | ||
|
|
fbe32e78ab | ||
|
|
b956c8ec2b | ||
|
|
1d429f039a |
@@ -1,18 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
},
|
||||
};
|
||||
60
.github/workflows/cd.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: Deploy FrontEnd
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
deploy-staging:
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID_STAGING }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID_STAGING }}
|
||||
ENV_VARIABLE: ${{ secrets.ENV_STAGING }}
|
||||
|
||||
steps:
|
||||
- name: 🏗 Setup repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🏗 Config .env
|
||||
run: echo "$ENV_VARIABLE" > .env
|
||||
|
||||
- name: 🏗 Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: 🏗 Pull staging app from vercel environment
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
- name: 📦 Build staging app artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
- name: 📦 Deploy staging app artifacts to vercel
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
deploy-production:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
ENV_VARIABLE: ${{ secrets.ENV_PROD }}
|
||||
|
||||
steps:
|
||||
- name: 🏗 Setup repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🏗 Config .env
|
||||
run: echo "$ENV_VARIABLE" > .env
|
||||
|
||||
- name: 🏗 Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: 🏗 Pull production vercel environment
|
||||
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
- name: 📦 Build app artifacts
|
||||
run: vercel build --prod --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
- name: 📦 Deploy app artifacts to vercel in production
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
64
.github/workflows/ci.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: CI script
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🏗 Setup repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🏗 Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: 'yarn'
|
||||
|
||||
- name: 🏗 Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: 📦 Lint with eslint
|
||||
run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID_STAGING }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID_STAGING }}
|
||||
steps:
|
||||
- name: 🏗 Setup repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🏗 Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: 🏗 Pull staging app from vercel environment
|
||||
run: vercel pull --yes --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
- name: 📦 Build staging app artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_AUTH_TOKEN }}
|
||||
|
||||
test-coverage:
|
||||
name: SonarCloud
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🏗 Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: 📦 Test and coverage
|
||||
run: yarn coverage
|
||||
|
||||
- name: 📦 SonarCloud Scan
|
||||
uses: SonarSource/sonarcloud-github-action@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
139
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: Deploy to IPFS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
tags: ['*']
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:lts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install bun
|
||||
run: npm install -g bun
|
||||
|
||||
- name: Install submodule deps (yarn 4 via corepack)
|
||||
run: corepack enable && yarn --cwd p2pix-smart-contracts install --immutable
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Generate contract ABIs
|
||||
env:
|
||||
ALCHEMY_API_KEY: dummy
|
||||
run: bun run wagmi:gen
|
||||
|
||||
- name: Build SPA
|
||||
run: bun run build-only
|
||||
|
||||
- name: Upload dist artifact
|
||||
# v3 pinned: Gitea Actions does not implement the v4 cache API
|
||||
# (the runner returns ECONNREFUSED on getCacheEntry).
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
deploy-rsync:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
container: node:lts
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Deploy via RSYNC
|
||||
uses: up9cloud/action-rsync@v1
|
||||
env:
|
||||
USER: ${{ secrets.SSH_USER }}
|
||||
HOST: ${{ secrets.SSH_HOST }}
|
||||
KEY: ${{ secrets.SSH_KEY }}
|
||||
SOURCE: dist/
|
||||
TARGET: ${{ github.ref_name }}
|
||||
|
||||
deploy-ipfs:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
container: node:lts
|
||||
outputs:
|
||||
cid: ${{ steps.ipfs.outputs.cid }}
|
||||
steps:
|
||||
- name: Install jq
|
||||
run: apt-get update && apt-get install -y --no-install-recommends jq
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Deploy to IPFS
|
||||
uses: ipshipyard/ipfs-deploy-action@v1
|
||||
id: ipfs
|
||||
with:
|
||||
path-to-deploy: dist
|
||||
set-github-status: false
|
||||
set-pr-comment: false
|
||||
kubo-api-url: ${{ secrets.KUBO_API_URL }}
|
||||
kubo-api-auth: ${{ secrets.KUBO_API_AUTH }}
|
||||
upload-car-artifact: false
|
||||
|
||||
deploy-pinata:
|
||||
needs: [deploy-ipfs, deploy-rsync]
|
||||
runs-on: ubuntu-latest
|
||||
container: node:lts
|
||||
steps:
|
||||
- name: Install jq
|
||||
run: apt-get update && apt-get install -y --no-install-recommends jq
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: package.json
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: Install Pinata CLI
|
||||
run: curl -fsSL https://cli.pinata.cloud/install | bash
|
||||
|
||||
- name: Authenticate Pinata CLI
|
||||
env:
|
||||
PINATA_JWT: ${{ secrets.PINATA_JWT }}
|
||||
run: echo -n "$PINATA_JWT" > ~/.pinata-files-cli
|
||||
|
||||
- name: Upload to Pinata
|
||||
env:
|
||||
PINATA_GROUP: ${{ secrets.PINATA_GROUP }}
|
||||
IPFS_CID: ${{ needs.deploy-ipfs.outputs.cid }}
|
||||
run: |
|
||||
export PATH="$HOME/.local/share/pinata/:$PATH"
|
||||
VERSION=$(jq -r .version package.json)
|
||||
UPLOAD_JSON=$(pinata upload --group "$PINATA_GROUP" --name "p2pix_${VERSION}" ./dist)
|
||||
CID=$(echo "$UPLOAD_JSON" | jq -r .cid)
|
||||
if [ -z "$CID" ] || [ "$CID" = "null" ]; then
|
||||
echo "Error: Could not parse CID. Output: $UPLOAD_JSON"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "## Deploy Summary"
|
||||
echo "- IPFS CID (kubo): \`$IPFS_CID\`"
|
||||
echo "- Pinata CID: \`$CID\`"
|
||||
echo "- Version: \`$VERSION\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
30
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Lint & Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
concurrency:
|
||||
group: lint-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
container: oven/bun:1-alpine
|
||||
steps:
|
||||
- name: Install required tools
|
||||
run: apk add --no-cache git
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: ESLint
|
||||
run: bun run lint
|
||||
|
||||
- name: Prettier check
|
||||
run: bun run format:check
|
||||
28
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
container: oven/bun:1-alpine
|
||||
steps:
|
||||
- name: Install required tools
|
||||
run: apk add --no-cache git
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Unit tests (vitest)
|
||||
run: bunx vitest run --passWithNoTests
|
||||
|
||||
8
.gitignore
vendored
@@ -13,6 +13,9 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
vendor/
|
||||
.dagrobin
|
||||
.claude
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@@ -28,4 +31,7 @@ coverage
|
||||
*.sw?
|
||||
.vercel
|
||||
|
||||
.env
|
||||
.env
|
||||
|
||||
# Codegen output (regenerated by `bun run wagmi:gen`, runs on prestart)
|
||||
src/blockchain/abi.ts
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "p2pix-smart-contracts"]
|
||||
path = p2pix-smart-contracts
|
||||
url = https://git.p2pix.co/doiim/p2pix-smart-contracts
|
||||
@@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
|
||||
12
Dockerfile
@@ -1,10 +1,10 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
FROM oven/bun:latest
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn
|
||||
COPY ./ ./
|
||||
COPY package.json bun.lock ./
|
||||
COPY vendor ./vendor
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["yarn", "start"]
|
||||
CMD ["bun", "run", "start"]
|
||||
|
||||
68
README.md
@@ -3,13 +3,6 @@
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<center>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=liftlearning_P2Pix-Front-End)
|
||||
[](https://sonarcloud.io/summary/new_code?id=liftlearning_P2Pix-Front-End)
|
||||
|
||||
</center>
|
||||
|
||||
This application aims to create a democratic and secure solution for the purchase and sale of ERC20 tokens, through the PIX, integrating the functionalities of smart contracts (smart contracts) of the blockchain with a receipt by digital signature. Allowing the integration of national financial system transactions to public blockchains, dispensing with custody through intermediaries.
|
||||
|
||||
# Table of Contents
|
||||
@@ -59,46 +52,69 @@ See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Dependencies
|
||||
|
||||
### API
|
||||
For full operation of the application, it is necessary to correctly configure the variable that points to the api in the .env file, in the repository there is an .env.example file, just rename it to just .env and modify the variable `VITE_API_URL`. The api can be run locally see [https://github.com/liftlearning/Pix-Explorer-Back-End](https://github.com/liftlearning/Pix-Explorer-Back-End), or it can be pointed to just her staging address: [https://p2pix-block-explorer-api-staging.vercel.app/](https://p2pix-block-explorer-api-staging.vercel.app/)
|
||||
### API + RPC
|
||||
|
||||
### Alchemy Keys
|
||||
In the .env file, set `VITE_GOERLI_API_URL=https://eth-goerli.g.alchemy.com/v2/Zu9m4b2U_EzVU_zd-vgZDOleY8OF1DNP` and `VITE_MUMBAI_API_URL=https://polygon-mumbai.g.alchemy.com/v2/ZANeCqfj6VsXGpOH6gWAP6SIVIgD9Pwv`
|
||||
Copy `.env.example` to `.env` and set the per-network variables:
|
||||
|
||||
You can also replace it with your own Alchemy Keys if you have one.
|
||||
| Var | Purpose |
|
||||
|---|---|
|
||||
| `VITE_APP_API_URL` | zkPix middleware base URL (default `http://localhost:3001`) |
|
||||
| `VITE_SEPOLIA_API_URL`, `VITE_MAINNET_API_URL`, `VITE_RSK_API_URL` | RPC endpoints per network (Alchemy, Infura, public RPC) |
|
||||
| `VITE_SEPOLIA_TOKEN_ADDRESS`, `VITE_MAINNET_TOKEN_ADDRESS`, `VITE_RSK_TOKEN_ADDRESS` | BRZ token address per network |
|
||||
| `VITE_SEPOLIA_SUBGRAPH_URL`, `VITE_MAINNET_SUBGRAPH_URL`, `VITE_RSK_SUBGRAPH_URL` | The Graph subgraph endpoints |
|
||||
|
||||
## Build Setup
|
||||
|
||||
The application can be tested by its trial version [https://p2pix-staging.vercel.app/](https://p2pix-staging.vercel.app/), the only requirement is to be running the smart contract of local way. To run the application locally, there are two different ways:
|
||||
|
||||
### Run with yarn
|
||||
```sh
|
||||
# Clone the repo
|
||||
git clone https://github.com/liftlearning/P2Pix-Front-End
|
||||
cd P2Pix-Front-End
|
||||
### Run with bun
|
||||
|
||||
# Install dependencies with yarn
|
||||
yarn install
|
||||
```sh
|
||||
# Pull the smart-contracts submodule (skip if you cloned with --recurse-submodules)
|
||||
git submodule update --init
|
||||
|
||||
# Install front-end dependencies
|
||||
bun install
|
||||
|
||||
# One-time bootstrap of the smart-contracts submodule (needed before wagmi:gen)
|
||||
cd p2pix-smart-contracts && bun install && cd ..
|
||||
|
||||
# Generate ABI bindings from the submodule (run again whenever contracts change)
|
||||
bun run wagmi:gen
|
||||
|
||||
# Type-Check, Compile and Minify for Production
|
||||
yarn build
|
||||
bun run build
|
||||
|
||||
# Compile and Hot-Reload for Development (port 3000)
|
||||
yarn start
|
||||
bun start
|
||||
|
||||
# Lint with [ESLint](https://eslint.org/)
|
||||
yarn lint
|
||||
bun run lint
|
||||
```
|
||||
### Run with docker-compose
|
||||
|
||||
```sh
|
||||
# Clone the repo
|
||||
git clone https://github.com/liftlearning/P2Pix-Front-End
|
||||
cd P2Pix-Front-End
|
||||
# Pull the smart-contracts submodule (skip if you cloned with --recurse-submodules)
|
||||
git submodule update --init
|
||||
|
||||
#1. Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/);
|
||||
#2. Install [Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
# Run docker-compose up command
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Backend Communication
|
||||
|
||||
Backend Repo: `https://gitea.kosmos.org/hueso/helpix`
|
||||
|
||||
Backend Endpoint: `https://api.p2pix.co/release/1279331`
|
||||
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer {api-key}" \
|
||||
-d '{"query": "{ depositAddeds { id seller token amount } }"}' \
|
||||
https://api.studio.thegraph.com/query/113713/p-2-pix/sepolia
|
||||
|
||||
https://api.studio.thegraph.com/query/113713/p-2-pix/1
|
||||
|
||||
curl --request POST --url 'https://api.hm.bb.com.br/testes-portal-desenvolvedor/v1/boletos-pix/pagar?gw-app-key=95cad3f03fd9013a9d15005056825665' --header 'content-type: application/json' --data '{"pix":"00020101021226070503***63041654" }'
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
},
|
||||
@@ -10,8 +10,8 @@ module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript",
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,5 @@ services:
|
||||
container_name: p2pix_frontend
|
||||
build:
|
||||
context: .
|
||||
volumes:
|
||||
- '.:/app'
|
||||
ports:
|
||||
- '3000:3000'
|
||||
2
env.d.ts
vendored
@@ -1 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
|
||||
31
eslint.config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import pluginVue from 'eslint-plugin-vue';
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
|
||||
import vuePrettierConfig from '@vue/eslint-config-prettier';
|
||||
|
||||
const sources = ['src/**/*.{ts,tsx,vue,js,mjs,cjs}', 'tests/**/*.{ts,tsx,vue,js,mjs,cjs}'];
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{ ignores: ['src/generated.ts', 'dist/**', 'node_modules/**'] },
|
||||
{
|
||||
files: sources,
|
||||
extends: [pluginVue.configs['flat/essential'], vueTsConfigs.recommended],
|
||||
},
|
||||
{
|
||||
files: sources,
|
||||
rules: {
|
||||
quotes: ['error', 'single', { avoidEscape: true }],
|
||||
'prettier/prettier': ['error', { singleQuote: true }],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
},
|
||||
},
|
||||
{ ...vuePrettierConfig, files: sources },
|
||||
);
|
||||
21
knip.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@latest/schema.json",
|
||||
"entry": [
|
||||
"src/router/index.ts",
|
||||
"index.html",
|
||||
"wagmi.config.ts"
|
||||
],
|
||||
"project": [
|
||||
"src/**/*.{ts,vue}",
|
||||
"*.config.{ts,js}"
|
||||
],
|
||||
"ignore": [
|
||||
"p2pix-smart-contracts/**",
|
||||
"babel.config.js"
|
||||
],
|
||||
"ignoreUnresolved": [
|
||||
"./abi",
|
||||
"@/blockchain/abi"
|
||||
],
|
||||
"tags": ["public"]
|
||||
}
|
||||
1
p2pix-smart-contracts
Submodule
103
package.json
@@ -1,63 +1,60 @@
|
||||
{
|
||||
"name": "p2pix-front-end",
|
||||
"version": "0.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite --host=0.0.0.0 --port 3000",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"serve": "vue-cli-service serve",
|
||||
"coverage": "vitest run --coverage",
|
||||
"build": "bun run type-check && bun run build-only",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --skipLibCheck --noEmit",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,vue,json}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,vue,json}\"",
|
||||
"wagmi:gen": "wagmi generate",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"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",
|
||||
"marked": "^4.2.12",
|
||||
"pinia": "^2.0.23",
|
||||
"qrcode": "^1.5.1",
|
||||
"vue": "^3.2.41",
|
||||
"vue-markdown": "^2.2.4",
|
||||
"vue-router": "^4.1.5"
|
||||
"@floating-ui/vue": "^1.1.11",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"@web3-onboard/injected-wallets": "^2.11.3",
|
||||
"@web3-onboard/vue": "^2.10.0",
|
||||
"marked": "^18.0.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"viem": "^2.48.8",
|
||||
"vite-svg-loader": "^5.1.1",
|
||||
"vue": "^3.5.33",
|
||||
"vue-router": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@pinia/testing": "^0.0.14",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/crc": "^3.8.0",
|
||||
"@types/jest": "^27.0.0",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^16.11.68",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/vue-markdown": "^2.2.1",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"@vitest/coverage-c8": "^0.28.2",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.2.7",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"ethers": "^5.7.2",
|
||||
"jsdom": "^21.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.18",
|
||||
"prettier": "^2.7.1",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^3.1.8",
|
||||
"vitest": "^0.28.1",
|
||||
"vue-tsc": "^1.0.8"
|
||||
}
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"@wagmi/cli": "^2.10.0",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.9.0",
|
||||
"happy-dom": "^20.9.0",
|
||||
"postcss": "^8.5.8",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5",
|
||||
"vue-tsc": "^3.2.7"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
67
src/App.vue
@@ -1,20 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import TopBar from "@/components/TopBar/TopBar.vue";
|
||||
import SpinnerComponent from "@/components/SpinnerComponent.vue";
|
||||
import { useRoute } from 'vue-router';
|
||||
import TopBar from '@/components/TopBar/TopBar.vue';
|
||||
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||
import ToasterComponent from '@/components/ui/ToasterComponent.vue';
|
||||
import VersionFooter from '@/components/ui/VersionFooter.vue';
|
||||
import { init, useOnboard } from '@web3-onboard/vue';
|
||||
import injectedModule from '@web3-onboard/injected-wallets';
|
||||
import { Networks, DEFAULT_NETWORK } from '@/config/networks';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const route = useRoute();
|
||||
const injected = injectedModule();
|
||||
const targetNetwork = ref(DEFAULT_NETWORK);
|
||||
|
||||
const web3Onboard = init({
|
||||
wallets: [injected],
|
||||
chains: Object.values(Networks).map((network) => ({
|
||||
id: `0x${network.id.toString(16)}`,
|
||||
token: network.nativeCurrency.symbol,
|
||||
label: network.name,
|
||||
rpcUrl: network.rpcUrls.default.http[0],
|
||||
})),
|
||||
connect: {
|
||||
autoConnectLastWallet: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { connectedWallet } = useOnboard();
|
||||
if (!connectedWallet) {
|
||||
web3Onboard.connectWallet();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TopBar />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<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>
|
||||
<main class="p-3 sm:p-4 md:p-8">
|
||||
<TopBar />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Transition name="page" mode="out-in" appear>
|
||||
<div :key="route.fullPath">
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<component :is="Component" />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div class="flex w-full h-full justify-center items-center">
|
||||
<SpinnerComponent :width="'16'" :height="'16'" />
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</Transition>
|
||||
</template>
|
||||
</RouterView>
|
||||
<ToasterComponent :targetNetwork="targetNetwork" />
|
||||
<VersionFooter />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
BIN
src/assets/Trial and expirations.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
3
src/assets/chevron.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="chevronDown" fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 491 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.64645 4.64645C1.84171 4.45118 2.15829 4.45118 2.35355 4.64645L8 10.2929L13.6464 4.64645C13.8417 4.45118 14.1583 4.45118 14.3536 4.64645C14.5488 4.84171 14.5488 5.15829 14.3536 5.35355L8.35355 11.3536C8.15829 11.5488 7.84171 11.5488 7.64645 11.3536L1.64645 5.35355C1.45118 5.15829 1.45118 4.84171 1.64645 4.64645Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 484 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="16" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3536 7.35355C13.1583 7.54882 12.8417 7.54882 12.6464 7.35355L7 1.70711L1.35355 7.35355C1.15829 7.54881 0.841709 7.54881 0.646446 7.35355C0.451184 7.15829 0.451184 6.84171 0.646446 6.64645L6.64645 0.646446C6.84171 0.451184 7.15829 0.451184 7.35355 0.646446L13.3536 6.64645C13.5488 6.84171 13.5488 7.15829 13.3536 7.35355Z" fill="#111827"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3536 7.35355C13.1583 7.54882 12.8417 7.54882 12.6464 7.35355L7 1.70711L1.35355 7.35355C1.15829 7.54881 0.841709 7.54881 0.646446 7.35355C0.451184 7.15829 0.451184 6.84171 0.646446 6.64645L6.64645 0.646446C6.84171 0.451184 7.15829 0.451184 7.35355 0.646446L13.3536 6.64645C13.5488 6.84171 13.5488 7.15829 13.3536 7.35355Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 498 B |
@@ -1,3 +1,7 @@
|
||||
<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"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.41819,0,0,1.41819,0.0724699,0.290651)">
|
||||
<path d="M7,0.333C3.318,0.333 0.333,3.323 0.333,7.012C0.333,9.961 2.244,12.465 4.893,13.348C5.226,13.409 5.348,13.203 5.348,13.025C5.348,12.868 5.342,12.447 5.339,11.89C3.484,12.294 3.093,10.995 3.093,10.995C2.79,10.223 2.353,10.018 2.353,10.018C1.748,9.604 2.399,9.612 2.399,9.612C3.068,9.659 3.42,10.3 3.42,10.3C4.014,11.32 4.98,11.025 5.36,10.855C5.421,10.424 5.593,10.13 5.784,9.963C4.304,9.794 2.747,9.221 2.747,6.662C2.747,5.934 3.007,5.337 3.433,4.87C3.364,4.702 3.136,4.022 3.498,3.104C3.498,3.104 4.058,2.924 5.332,3.788C5.875,3.639 6.436,3.564 7,3.563C7.567,3.566 8.137,3.64 8.67,3.788C9.942,2.924 10.501,3.103 10.501,3.103C10.865,4.022 10.636,4.702 10.568,4.87C10.994,5.337 11.253,5.934 11.253,6.662C11.253,9.227 9.694,9.792 8.209,9.958C8.448,10.164 8.661,10.571 8.661,11.194C8.661,12.086 8.653,12.807 8.653,13.025C8.653,13.204 8.773,13.412 9.112,13.347C10.439,12.902 11.593,12.05 12.411,10.914C13.228,9.777 13.667,8.412 13.667,7.012C13.667,3.323 10.682,0.333 7,0.333Z" style="fill:rgb(31,41,55);"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
7
src/assets/linkedinIcon.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.426716,0,0,0.426716,-0.646464,-0.646464)">
|
||||
<path d="M41,4L9,4C6.24,4 4,6.24 4,9L4,41C4,43.76 6.24,46 9,46L41,46C43.76,46 46,43.76 46,41L46,9C46,6.24 43.76,4 41,4ZM17,20L17,39L11,39L11,20L17,20ZM11,14.47C11,13.07 12.2,12 14,12C15.8,12 16.93,13.07 17,14.47C17,15.87 15.88,17 14,17C12.2,17 11,15.87 11,14.47ZM39,39L33,39L33,29C33,27 32,25 29.5,24.96L29.42,24.96C27,24.96 26,27.02 26,29L26,39L20,39L20,20L26,20L26,22.56C26,22.56 27.93,20 31.81,20C35.78,20 39,22.73 39,28.26L39,39Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1001 B |
@@ -1,16 +1,12 @@
|
||||
@import './base.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "./base.css" layer(base);
|
||||
@import "tailwindcss";
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
height: fit-content;
|
||||
min-height: 100vh;
|
||||
background-image: url( './bg.svg' );
|
||||
background-size: cover;
|
||||
background: radial-gradient(ellipse at 50% -50%, rgba(49, 46, 129, 1) 60%, rgba(24, 30, 42, 1) 80%);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -26,3 +22,13 @@ a,
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
@apply flex w-full md:max-w-lg flex-col justify-center items-center px-4 sm:px-8 py-4 sm:py-6 gap-4 rounded-lg border border-gray-500 backdrop-blur-md drop-shadow-lg shadow-lg mt-10;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
76
src/assets/networks/rootstock-testnet.svg
Normal file
|
After Width: | Height: | Size: 79 KiB |
4
src/assets/networks/sepolia.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8.16801 2.56435C8.09081 2.43177 7.89923 2.4319 7.82221 2.56458L4.82431 7.72899C4.76983 7.82284 4.80013 7.94301 4.89259 7.99981L7.89038 9.84139C7.95451 9.88079 8.03533 9.88085 8.09952 9.84154L11.1069 7.99986C11.1996 7.94308 11.23 7.82262 11.1752 7.72866L8.16801 2.56435ZM8.08754 10.7831C8.02182 10.8253 7.93759 10.8253 7.87181 10.7833L5.51555 9.27665C5.33379 9.16043 5.1222 9.37463 5.24065 9.55495L7.8123 13.4701C7.89136 13.5905 8.06789 13.5903 8.14678 13.4699L10.7098 9.55566C10.828 9.37517 10.6161 9.16127 10.4345 9.27775L8.08754 10.7831Z" fill="#3B82F6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.82232 2.56456C7.89934 2.43187 8.09092 2.43174 8.16812 2.56432L11.1754 7.72864C11.2301 7.8226 11.1997 7.94305 11.107 7.99984L8.09963 9.84152C8.03544 9.88082 7.95463 9.88077 7.89049 9.84137L4.8927 7.99979C4.80024 7.94299 4.76994 7.82282 4.82442 7.72897L7.82232 2.56456ZM7.87193 10.7833C7.9377 10.8253 8.02193 10.8253 8.08765 10.7831L10.4346 9.27773C10.6162 9.16125 10.8281 9.37515 10.7099 9.55563L8.1469 13.4698C8.06801 13.5903 7.89147 13.5904 7.81241 13.4701L5.24076 9.55492C5.12232 9.3746 5.3339 9.1604 5.51566 9.27662L7.87193 10.7833Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/tokens/brx.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" fill="#efb914" fill-rule="nonzero" r="16"/><path d="M21.002 9.855A7.947 7.947 0 0124 15.278l-2.847-.708a5.357 5.357 0 00-3.86-3.667c-2.866-.713-5.76.991-6.465 3.806s1.05 5.675 3.917 6.388a5.373 5.373 0 005.134-1.43l2.847.707a7.974 7.974 0 01-5.2 3.385L16.716 27l-2.596-.645.644-2.575a8.28 8.28 0 01-1.298-.323l-.643 2.575-2.596-.646.81-3.241c-2.378-1.875-3.575-4.996-2.804-8.081s3.297-5.281 6.28-5.823L15.323 5l2.596.645-.644 2.575a8.28 8.28 0 011.298.323l.643-2.575 2.596.646z" fill="#fff"/></g></svg>
|
||||
|
After Width: | Height: | Size: 644 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
32
src/assets/transitions.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.page-enter-from,
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
|
||||
.resize-enter-active,
|
||||
.resize-leave-active {
|
||||
max-height: 100px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.resize-enter-from,
|
||||
.resize-leave-to {
|
||||
max-height: 0px;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
<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"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.44084,0,0,1.44084,-0.0860978,1.23349)">
|
||||
<path d="M4.527,11.502C9.558,11.502 12.31,7.333 12.31,3.719C12.31,3.6 12.31,3.482 12.302,3.365C12.838,2.978 13.3,2.498 13.667,1.948C13.168,2.169 12.638,2.315 12.096,2.379C12.667,2.037 13.094,1.499 13.298,0.865C12.761,1.184 12.174,1.409 11.562,1.529C11.149,1.091 10.604,0.8 10.009,0.703C9.415,0.605 8.805,0.706 8.274,0.991C7.743,1.275 7.321,1.726 7.072,2.275C6.824,2.823 6.763,3.438 6.9,4.025C5.812,3.97 4.748,3.688 3.777,3.195C2.805,2.703 1.948,2.012 1.262,1.167C0.912,1.769 0.805,2.482 0.962,3.16C1.119,3.838 1.529,4.431 2.108,4.818C1.673,4.806 1.247,4.688 0.867,4.477L0.867,4.511C0.867,5.143 1.086,5.755 1.486,6.244C1.886,6.732 2.442,7.068 3.062,7.193C2.659,7.303 2.236,7.319 1.826,7.239C2.001,7.783 2.341,8.259 2.8,8.599C3.258,8.94 3.811,9.129 4.382,9.139C3.815,9.585 3.165,9.915 2.471,10.109C1.776,10.304 1.05,10.359 0.333,10.273C1.585,11.076 3.04,11.502 4.527,11.499" style="fill:rgb(31,41,55);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,104 +0,0 @@
|
||||
import { expectTypeOf, it, expect } from "vitest";
|
||||
import {
|
||||
getTokenAddress,
|
||||
getP2PixAddress,
|
||||
getProviderUrl,
|
||||
isPossibleNetwork,
|
||||
possibleChains,
|
||||
network2Chain,
|
||||
} from "../addresses";
|
||||
|
||||
import { setActivePinia, createPinia } from "pinia";
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
|
||||
describe("addresses.ts types", () => {
|
||||
it("My addresses.ts types work properly", () => {
|
||||
expectTypeOf(getTokenAddress).toBeFunction();
|
||||
expectTypeOf(getP2PixAddress).toBeFunction();
|
||||
expectTypeOf(getProviderUrl).toBeFunction();
|
||||
expectTypeOf(isPossibleNetwork).toBeFunction();
|
||||
|
||||
expectTypeOf(possibleChains).toBeObject();
|
||||
expectTypeOf(network2Chain).toBeObject();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addresses.ts functions", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("getTokenAddress Ethereum", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.ethereum);
|
||||
expect(getTokenAddress()).toBe(
|
||||
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
|
||||
);
|
||||
});
|
||||
|
||||
it("getTokenAddress Polygon", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.polygon);
|
||||
expect(getTokenAddress()).toBe(
|
||||
"0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29"
|
||||
);
|
||||
});
|
||||
|
||||
it("getTokenAddress Default", () => {
|
||||
expect(getTokenAddress()).toBe(
|
||||
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
|
||||
);
|
||||
});
|
||||
|
||||
it("getP2PixAddress Ethereum", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.ethereum);
|
||||
expect(getP2PixAddress()).toBe(
|
||||
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
|
||||
);
|
||||
});
|
||||
|
||||
it("getP2PixAddress Polygon", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.polygon);
|
||||
expect(getP2PixAddress()).toBe(
|
||||
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
|
||||
);
|
||||
});
|
||||
|
||||
it("getP2PixAddress Default", () => {
|
||||
expect(getP2PixAddress()).toBe(
|
||||
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
|
||||
);
|
||||
});
|
||||
|
||||
it("getProviderUrl Ethereum", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.ethereum);
|
||||
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
|
||||
});
|
||||
|
||||
it("getProviderUrl Polygon", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.polygon);
|
||||
expect(getProviderUrl()).toBe(import.meta.env.VITE_MUMBAI_API_URL);
|
||||
});
|
||||
|
||||
it("getProviderUrl Default", () => {
|
||||
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
|
||||
});
|
||||
|
||||
it("isPossibleNetwork Returns", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setNetworkName(NetworkEnum.ethereum);
|
||||
expect(isPossibleNetwork("0x5")).toBe(true);
|
||||
expect(isPossibleNetwork("5")).toBe(true);
|
||||
expect(isPossibleNetwork("0x13881")).toBe(true);
|
||||
expect(isPossibleNetwork("80001")).toBe(true);
|
||||
|
||||
expect(isPossibleNetwork("")).toBe(false);
|
||||
expect(isPossibleNetwork(" ")).toBe(false);
|
||||
expect(isPossibleNetwork("0x55")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
|
||||
const getTokenAddress = (network?: NetworkEnum): string => {
|
||||
const etherStore = useEtherStore();
|
||||
|
||||
const possibleTokenAddresses: { [key: string]: string } = {
|
||||
Ethereum: "0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00",
|
||||
Polygon: "0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29",
|
||||
Rootstock: "0xfE841c74250e57640390f46d914C88d22C51e82e",
|
||||
};
|
||||
|
||||
return possibleTokenAddresses[network ? network : etherStore.networkName];
|
||||
};
|
||||
|
||||
const getP2PixAddress = (network?: NetworkEnum): string => {
|
||||
const etherStore = useEtherStore();
|
||||
|
||||
const possibleP2PixAddresses: { [key: string]: string } = {
|
||||
Ethereum: "0x2414817FF64A114d91eCFA16a834d3fCf69103d4",
|
||||
Polygon: "0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00",
|
||||
Rootstock: "0x98ba35eb14b38D6Aa709338283af3e922476dE34",
|
||||
};
|
||||
|
||||
return possibleP2PixAddresses[network ? network : etherStore.networkName];
|
||||
};
|
||||
|
||||
const getProviderUrl = (): string => {
|
||||
const etherStore = useEtherStore();
|
||||
|
||||
const possibleProvidersUrls: { [key: string]: string } = {
|
||||
Ethereum: import.meta.env.VITE_GOERLI_API_URL,
|
||||
Polygon: import.meta.env.VITE_MUMBAI_API_URL,
|
||||
Rootstock: import.meta.env.VITE_RSK_API_URL,
|
||||
};
|
||||
|
||||
return possibleProvidersUrls[etherStore.networkName];
|
||||
};
|
||||
|
||||
const possibleChains: { [key: string]: NetworkEnum } = {
|
||||
"0x5": NetworkEnum.ethereum,
|
||||
"5": NetworkEnum.ethereum,
|
||||
"0x13881": NetworkEnum.polygon,
|
||||
"80001": NetworkEnum.polygon,
|
||||
"31": NetworkEnum.rootstock,
|
||||
};
|
||||
|
||||
const network2Chain: { [key: string]: string } = {
|
||||
Ethereum: "0x5",
|
||||
Polygon: "0x13881",
|
||||
Localhost: "0x7a69",
|
||||
Rootstock: "0x1f",
|
||||
};
|
||||
|
||||
const isPossibleNetwork = (networkChain: string): boolean => {
|
||||
if (Object.keys(possibleChains).includes(networkChain.toString())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export {
|
||||
getTokenAddress,
|
||||
getProviderUrl,
|
||||
possibleChains,
|
||||
network2Chain,
|
||||
isPossibleNetwork,
|
||||
getP2PixAddress,
|
||||
};
|
||||
@@ -1,100 +1,78 @@
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
import { getContract } from './provider';
|
||||
import { ChainContract } from 'viem';
|
||||
import { parseEther, type Address, type TransactionReceipt } from 'viem';
|
||||
|
||||
import { getContract, getProvider } from "./provider";
|
||||
import { getP2PixAddress, getTokenAddress } from "./addresses";
|
||||
|
||||
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
|
||||
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { parseEther } from "ethers/lib/utils";
|
||||
|
||||
const addLock = async (
|
||||
seller: string,
|
||||
token: string,
|
||||
amount: number
|
||||
): Promise<string> => {
|
||||
const etherStore = useEtherStore();
|
||||
|
||||
const p2pContract = getContract();
|
||||
|
||||
const lock = await p2pContract.lock(
|
||||
seller,
|
||||
token,
|
||||
etherStore.walletAddress, // String "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" (Example)
|
||||
ethers.constants.AddressZero, // String "0x0000000000000000000000000000000000000000"
|
||||
0,
|
||||
parseEther(String(amount)), // BigNumber
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const lock_rec = await lock.wait();
|
||||
const [t] = lock_rec.events;
|
||||
|
||||
return String(t.args.lockID);
|
||||
};
|
||||
|
||||
const releaseLock = async (
|
||||
pixKey: number,
|
||||
export const addLock = async (
|
||||
sellerAddress: Address,
|
||||
tokenAddress: Address,
|
||||
amount: number,
|
||||
e2eId: string,
|
||||
lockId: string
|
||||
): Promise<any> => {
|
||||
const mockBacenSigner = new ethers.Wallet(
|
||||
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
||||
);
|
||||
): Promise<bigint> => {
|
||||
const { address, abi, wallet, client, account } = await getContract();
|
||||
const parsedAmount = parseEther(amount.toString());
|
||||
|
||||
const messageToSign = ethers.utils.solidityKeccak256(
|
||||
["uint160", "uint256", "bytes32"],
|
||||
[
|
||||
pixKey,
|
||||
parseEther(String(amount)),
|
||||
ethers.utils.formatBytes32String(e2eId),
|
||||
]
|
||||
);
|
||||
if (!wallet) {
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
const messageHashBytes = ethers.utils.arrayify(messageToSign);
|
||||
const flatSig = await mockBacenSigner.signMessage(messageHashBytes);
|
||||
const provider = getProvider();
|
||||
const { result, request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'lock',
|
||||
args: [sellerAddress, tokenAddress, parsedAmount, [], []],
|
||||
account,
|
||||
});
|
||||
const hash = await wallet.writeContract(request);
|
||||
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||
|
||||
const sig = ethers.utils.splitSignature(flatSig);
|
||||
if (!receipt.status)
|
||||
throw new Error('Transaction failed: ' + receipt.transactionHash);
|
||||
|
||||
const signer = provider.getSigner();
|
||||
const p2pContract = new ethers.Contract(getP2PixAddress(), p2pix.abi, signer);
|
||||
|
||||
const release = await p2pContract.release(
|
||||
BigNumber.from(lockId),
|
||||
ethers.constants.AddressZero,
|
||||
ethers.utils.formatBytes32String(e2eId),
|
||||
sig.r,
|
||||
sig.s,
|
||||
sig.v
|
||||
);
|
||||
await release.wait();
|
||||
|
||||
return release;
|
||||
return result;
|
||||
};
|
||||
|
||||
const cancelDeposit = async (depositId: BigNumber): Promise<any> => {
|
||||
const contract = getContract();
|
||||
export const withdrawDeposit = async (
|
||||
amount: string,
|
||||
token: Address,
|
||||
): Promise<boolean> => {
|
||||
const { address, abi, wallet, client, account } = await getContract();
|
||||
|
||||
const cancel = await contract.cancelDeposit(depositId);
|
||||
await cancel.wait();
|
||||
if (!wallet) {
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
return cancel;
|
||||
const { request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'withdraw',
|
||||
args: [token, parseEther(amount), []],
|
||||
account,
|
||||
});
|
||||
|
||||
const hash = await wallet.writeContract(request);
|
||||
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||
|
||||
return receipt.status === 'success';
|
||||
};
|
||||
|
||||
const withdrawDeposit = async (amount: string): Promise<any> => {
|
||||
const contract = getContract();
|
||||
export const releaseLock = async (
|
||||
lockID: bigint,
|
||||
pixTimestamp: `0x${string}` & { lenght: 34 },
|
||||
signature: `0x${string}`,
|
||||
): Promise<TransactionReceipt> => {
|
||||
const { address, abi, wallet, client, account } = await getContract();
|
||||
|
||||
const withdraw = await contract.withdraw(
|
||||
getTokenAddress(),
|
||||
parseEther(String(amount)),
|
||||
[]
|
||||
);
|
||||
await withdraw.wait();
|
||||
if (!wallet) {
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
return withdraw;
|
||||
const { request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'release',
|
||||
args: [BigInt(lockID), pixTimestamp, signature],
|
||||
account,
|
||||
});
|
||||
|
||||
const hash = await wallet.writeContract(request);
|
||||
return client.waitForTransactionReceipt({ hash });
|
||||
};
|
||||
|
||||
export { cancelDeposit, withdrawDeposit, addLock, releaseLock };
|
||||
|
||||
@@ -1,141 +1,174 @@
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
import { Contract, ethers } from "ethers";
|
||||
|
||||
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";
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { formatEther, toHex, stringToHex } from 'viem';
|
||||
import type { PublicClient, Address } from 'viem';
|
||||
import { Networks } from '@/config/networks';
|
||||
import { getContract } from './provider';
|
||||
import { p2PixAbi } from './abi';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import type { UnreleasedLock } from '@/model/UnreleasedLock';
|
||||
import { ChainContract } from 'viem';
|
||||
|
||||
const getNetworksLiquidity = async (): Promise<void> => {
|
||||
const etherStore = useEtherStore();
|
||||
const user = useUser();
|
||||
user.setLoadingNetworkLiquidity(true);
|
||||
|
||||
const goerliProvider = new ethers.providers.JsonRpcProvider(
|
||||
import.meta.env.VITE_GOERLI_API_URL,
|
||||
5
|
||||
); // goerli provider
|
||||
const mumbaiProvider = new ethers.providers.JsonRpcProvider(
|
||||
import.meta.env.VITE_MUMBAI_API_URL,
|
||||
80001
|
||||
); // mumbai provider
|
||||
const rootstockProvider = new ethers.providers.JsonRpcProvider(
|
||||
import.meta.env.VITE_RSK_API_URL,
|
||||
31
|
||||
); // rootstock provider
|
||||
const depositLists: ValidDeposit[][] = [];
|
||||
|
||||
const p2pContractGoerli = new ethers.Contract(
|
||||
getP2PixAddress(NetworkEnum.ethereum),
|
||||
p2pix.abi,
|
||||
goerliProvider
|
||||
);
|
||||
const p2pContractMumbai = new ethers.Contract(
|
||||
getP2PixAddress(NetworkEnum.polygon),
|
||||
p2pix.abi,
|
||||
mumbaiProvider
|
||||
);
|
||||
for (const network of Object.values(Networks)) {
|
||||
const deposits = await getValidDeposits(
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
network,
|
||||
);
|
||||
if (deposits) depositLists.push(deposits);
|
||||
}
|
||||
|
||||
const p2pContractRootstock = new ethers.Contract(
|
||||
getP2PixAddress(NetworkEnum.rootstock),
|
||||
p2pix.abi,
|
||||
rootstockProvider
|
||||
);
|
||||
const allDeposits = depositLists.flat();
|
||||
user.setDepositsValidList(allDeposits);
|
||||
user.setLoadingNetworkLiquidity(false);
|
||||
};
|
||||
|
||||
etherStore.setLoadingNetworkLiquidity(true);
|
||||
const depositListGoerli = await getValidDeposits(
|
||||
getTokenAddress(NetworkEnum.ethereum),
|
||||
p2pContractGoerli
|
||||
);
|
||||
const getParticipantID = async (
|
||||
seller: Address,
|
||||
token: Address,
|
||||
): Promise<string> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
|
||||
const depositListMumbai = await getValidDeposits(
|
||||
getTokenAddress(NetworkEnum.polygon),
|
||||
p2pContractMumbai
|
||||
);
|
||||
const participantIDHex = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'getPixTarget',
|
||||
args: [seller, token],
|
||||
});
|
||||
|
||||
etherStore.setDepositsValidListGoerli(depositListGoerli);
|
||||
const depositListRootstock = await getValidDeposits(
|
||||
getTokenAddress(NetworkEnum.rootstock),
|
||||
p2pContractRootstock
|
||||
// Remove '0x' prefix and convert hex to UTF-8 string
|
||||
const hexString =
|
||||
typeof participantIDHex === 'string'
|
||||
? participantIDHex
|
||||
: toHex(participantIDHex as bigint);
|
||||
if (!hexString) throw new Error('Participant ID not found');
|
||||
const bytes = new Uint8Array(
|
||||
hexString
|
||||
.slice(2)
|
||||
.match(/.{1,2}/g)!
|
||||
.map((byte: string) => parseInt(byte, 16)),
|
||||
);
|
||||
etherStore.setDepositsValidListMumbai(depositListMumbai);
|
||||
etherStore.setDepositsValidListRootstock(depositListRootstock);
|
||||
etherStore.setLoadingNetworkLiquidity(false);
|
||||
// Remove null bytes from the end of the string
|
||||
return new TextDecoder().decode(bytes).replace(/\0/g, '');
|
||||
};
|
||||
|
||||
const getValidDeposits = async (
|
||||
token: string,
|
||||
contract?: Contract
|
||||
token: Address,
|
||||
network: NetworkConfig,
|
||||
contractInfo?: { client: PublicClient; address: Address },
|
||||
): Promise<ValidDeposit[]> => {
|
||||
let p2pContract: Contract;
|
||||
let client: PublicClient, abi;
|
||||
|
||||
if (contract) {
|
||||
p2pContract = contract;
|
||||
if (contractInfo) {
|
||||
({ client } = contractInfo);
|
||||
abi = p2PixAbi;
|
||||
} else {
|
||||
p2pContract = getContract(true);
|
||||
({ abi, client } = await getContract(true));
|
||||
}
|
||||
|
||||
const filterDeposits = p2pContract.filters.DepositAdded(null);
|
||||
const eventsDeposits = await p2pContract.queryFilter(filterDeposits);
|
||||
|
||||
if (!contract) p2pContract = getContract(); // get metamask provider contract
|
||||
const depositList: { [key: string]: ValidDeposit } = {};
|
||||
|
||||
await Promise.all(
|
||||
eventsDeposits.map(async (deposit) => {
|
||||
// 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 (mappedBalance._hex) {
|
||||
validDeposit = {
|
||||
token: token,
|
||||
blockNumber: deposit.blockNumber,
|
||||
remaining: Number(formatEther(mappedBalance._hex)),
|
||||
seller: deposit.args?.seller,
|
||||
pixKey: Number(mappedPixTarget._hex),
|
||||
};
|
||||
const body = {
|
||||
query: `
|
||||
{
|
||||
depositAddeds(where: { token: "${token}" }) {
|
||||
seller
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
if (validDeposit)
|
||||
depositList[deposit.args?.seller + token] = validDeposit;
|
||||
})
|
||||
const depositLogs = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// remove doubles from sellers list
|
||||
const depositData = await depositLogs.json();
|
||||
if (!depositData.data) {
|
||||
console.error('Error fetching deposit logs');
|
||||
return [];
|
||||
}
|
||||
const depositAddeds = depositData.data.depositAddeds;
|
||||
const uniqueSellers = depositAddeds.reduce(
|
||||
(acc: Record<Address, boolean>, deposit: any) => {
|
||||
acc[deposit.seller] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<Address, boolean>,
|
||||
);
|
||||
|
||||
if (!contractInfo) {
|
||||
// Get metamask provider contract
|
||||
({ abi, client } = await getContract(true));
|
||||
}
|
||||
|
||||
const depositList: { [key: string]: ValidDeposit } = {};
|
||||
|
||||
const sellersList = Object.keys(uniqueSellers) as Address[];
|
||||
// Use multicall to batch all getBalance requests
|
||||
const balanceCalls = sellersList.map((seller) => ({
|
||||
address: (network.contracts?.p2pix as ChainContract).address,
|
||||
abi,
|
||||
functionName: 'getBalance',
|
||||
args: [seller, token],
|
||||
}));
|
||||
|
||||
const balanceResults = await client.multicall({
|
||||
contracts: balanceCalls as any,
|
||||
});
|
||||
|
||||
// Process results into the depositList
|
||||
sellersList.forEach((seller, index) => {
|
||||
const mappedBalance = balanceResults[index];
|
||||
|
||||
if (!mappedBalance.error && mappedBalance.result) {
|
||||
const validDeposit: ValidDeposit = {
|
||||
token,
|
||||
blockNumber: 0,
|
||||
remaining: Number(formatEther(mappedBalance.result as bigint)),
|
||||
seller,
|
||||
network,
|
||||
participantID: '',
|
||||
};
|
||||
depositList[seller + token] = validDeposit;
|
||||
}
|
||||
});
|
||||
return Object.values(depositList);
|
||||
};
|
||||
|
||||
const getUnreleasedLockById = async (
|
||||
lockID: string
|
||||
lockID: bigint,
|
||||
): Promise<UnreleasedLock> => {
|
||||
const p2pContract = getContract();
|
||||
const pixData: Pix = {
|
||||
pixKey: "",
|
||||
};
|
||||
const { address, abi, client } = await getContract();
|
||||
|
||||
const lock = await p2pContract.mapLocks(lockID);
|
||||
|
||||
const pixTarget = lock.pixTarget;
|
||||
const amount = formatEther(lock?.amount);
|
||||
pixData.pixKey = String(Number(pixTarget));
|
||||
pixData.value = Number(amount);
|
||||
const [, , , amount, token, seller] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'mapLocks',
|
||||
args: [lockID],
|
||||
});
|
||||
|
||||
return {
|
||||
lockID: lockID,
|
||||
pix: pixData,
|
||||
lockID,
|
||||
amount: Number(formatEther(amount)),
|
||||
tokenAddress: token,
|
||||
sellerAddress: seller,
|
||||
};
|
||||
};
|
||||
|
||||
export { getValidDeposits, getNetworksLiquidity, getUnreleasedLockById };
|
||||
export {
|
||||
getValidDeposits,
|
||||
getNetworksLiquidity,
|
||||
getUnreleasedLockById,
|
||||
getParticipantID,
|
||||
};
|
||||
|
||||
@@ -1,95 +1,64 @@
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
|
||||
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
|
||||
|
||||
import { updateWalletStatus } from "./wallet";
|
||||
import { p2PixAbi } from './abi';
|
||||
import { updateWalletStatus } from './wallet';
|
||||
import {
|
||||
getProviderUrl,
|
||||
isPossibleNetwork,
|
||||
possibleChains,
|
||||
network2Chain,
|
||||
getP2PixAddress,
|
||||
} from "./addresses";
|
||||
createPublicClient,
|
||||
createWalletClient,
|
||||
custom,
|
||||
http,
|
||||
PublicClient,
|
||||
WalletClient,
|
||||
} from 'viem';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import type { ChainContract } from 'viem';
|
||||
|
||||
import { ethers } from "ethers";
|
||||
let walletClient: WalletClient | null = null;
|
||||
|
||||
const getProvider = (
|
||||
onlyAlchemyProvider: boolean = false
|
||||
): ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider => {
|
||||
const window_ = window as any;
|
||||
const connection = window_.ethereum;
|
||||
const getPublicClient = (): PublicClient => {
|
||||
const user = useUser();
|
||||
const rpcUrl = (user.network.value as NetworkConfig).rpcUrls.default.http[0];
|
||||
const chain = user.network.value;
|
||||
|
||||
if (!connection || onlyAlchemyProvider)
|
||||
return new ethers.providers.JsonRpcProvider(getProviderUrl()); // alchemy provider
|
||||
|
||||
return new ethers.providers.Web3Provider(connection); // metamask provider
|
||||
return createPublicClient({
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
});
|
||||
};
|
||||
|
||||
const getContract = (onlyAlchemyProvider: boolean = false) => {
|
||||
const provider = getProvider(onlyAlchemyProvider);
|
||||
const signer = provider.getSigner();
|
||||
return new ethers.Contract(getP2PixAddress(), p2pix.abi, signer);
|
||||
const getWalletClient = (): WalletClient | null => {
|
||||
return walletClient;
|
||||
};
|
||||
|
||||
const connectProvider = async (): Promise<void> => {
|
||||
const window_ = window as any;
|
||||
const connection = window_.ethereum;
|
||||
const provider = getProvider();
|
||||
const getContract = async (onlyRpcProvider = false) => {
|
||||
const client = getPublicClient();
|
||||
const user = useUser();
|
||||
const address = (user.network.value.contracts?.p2pix as ChainContract)
|
||||
.address;
|
||||
const abi = p2PixAbi;
|
||||
const wallet = onlyRpcProvider ? null : getWalletClient();
|
||||
|
||||
if (!(provider instanceof ethers.providers.Web3Provider)) {
|
||||
window.alert("Please, connect to metamask extension");
|
||||
return;
|
||||
if (!client) {
|
||||
throw new Error('Public client not initialized');
|
||||
}
|
||||
|
||||
const [account] = wallet ? await wallet.getAddresses() : [null];
|
||||
|
||||
return { address, abi, client, wallet, account };
|
||||
};
|
||||
|
||||
const connectProvider = async (p: any): Promise<void> => {
|
||||
const user = useUser();
|
||||
const chain = user.network.value;
|
||||
|
||||
const [account] = await p!.request({ method: 'eth_requestAccounts' });
|
||||
|
||||
walletClient = createWalletClient({
|
||||
account,
|
||||
chain,
|
||||
transport: custom(p),
|
||||
});
|
||||
|
||||
await updateWalletStatus();
|
||||
|
||||
listenToNetworkChange(connection);
|
||||
listenToWalletChange(connection);
|
||||
};
|
||||
|
||||
const listenToWalletChange = (connection: any): void => {
|
||||
connection.on("accountsChanged", async () => {
|
||||
console.log("Changed account!");
|
||||
updateWalletStatus();
|
||||
});
|
||||
};
|
||||
|
||||
const listenToNetworkChange = (connection: any) => {
|
||||
const etherStore = useEtherStore();
|
||||
|
||||
connection.on("chainChanged", (networkChain: string) => {
|
||||
console.log("Changed network!");
|
||||
|
||||
if (isPossibleNetwork(networkChain)) {
|
||||
etherStore.setNetworkName(possibleChains[networkChain]);
|
||||
updateWalletStatus();
|
||||
} else {
|
||||
window.alert("Invalid chain!");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const requestNetworkChange = async (network: string): Promise<boolean> => {
|
||||
const etherStore = useEtherStore();
|
||||
if (!etherStore.walletAddress) return true;
|
||||
|
||||
try {
|
||||
const window_ = window as any;
|
||||
await window_.ethereum.request({
|
||||
method: "wallet_switchEthereumChain",
|
||||
params: [{ chainId: network2Chain[network] }], // chainId must be in hexadecimal numbers
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export {
|
||||
getProvider,
|
||||
getContract,
|
||||
connectProvider,
|
||||
listenToNetworkChange,
|
||||
requestNetworkChange,
|
||||
};
|
||||
export { getPublicClient, getWalletClient, getContract, connectProvider };
|
||||
|
||||
@@ -1,44 +1,92 @@
|
||||
import { getContract, getProvider } from "./provider";
|
||||
import { getTokenAddress, getP2PixAddress } from "./addresses";
|
||||
import { parseEther } from "ethers/lib/utils";
|
||||
import { getContract, getPublicClient, getWalletClient } from './provider';
|
||||
import { parseEther, toHex, ChainContract } from 'viem';
|
||||
import { mockTokenAbi } from './abi';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { createParticipant } from '@/utils/bbPay';
|
||||
import type { Participant } from '@/utils/bbPay';
|
||||
import type { Address } from 'viem';
|
||||
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import mockToken from "../utils/smart_contract_files/MockToken.json";
|
||||
|
||||
const approveTokens = async (tokenQty: string): Promise<any> => {
|
||||
const provider = getProvider();
|
||||
const signer = provider.getSigner();
|
||||
|
||||
const tokenContract = new ethers.Contract(
|
||||
getTokenAddress(),
|
||||
mockToken.abi,
|
||||
signer
|
||||
);
|
||||
|
||||
const apprv = await tokenContract.approve(
|
||||
getP2PixAddress(),
|
||||
parseEther(tokenQty)
|
||||
);
|
||||
|
||||
await apprv.wait();
|
||||
return apprv;
|
||||
const getP2PixAddress = (): Address => {
|
||||
const user = useUser();
|
||||
return (user.network.value.contracts?.p2pix as ChainContract).address;
|
||||
};
|
||||
|
||||
const addDeposit = async (tokenQty: string, pixKey: string): Promise<any> => {
|
||||
const p2pContract = getContract();
|
||||
const approveTokens = async (participant: Participant): Promise<any> => {
|
||||
const user = useUser();
|
||||
const publicClient = getPublicClient();
|
||||
const walletClient = getWalletClient();
|
||||
|
||||
const deposit = await p2pContract.deposit(
|
||||
getTokenAddress(),
|
||||
parseEther(tokenQty),
|
||||
pixKey,
|
||||
true,
|
||||
ethers.utils.formatBytes32String("")
|
||||
);
|
||||
if (!publicClient || !walletClient) {
|
||||
throw new Error('Clients not initialized');
|
||||
}
|
||||
|
||||
await deposit.wait();
|
||||
user.setSeller(participant);
|
||||
const [account] = await walletClient.getAddresses();
|
||||
|
||||
return deposit;
|
||||
// Get token address
|
||||
const tokenAddress =
|
||||
user.network.value.tokens[user.selectedToken.value].address;
|
||||
|
||||
// Check if the token is already approved
|
||||
const allowance = await publicClient.readContract({
|
||||
address: tokenAddress,
|
||||
abi: mockTokenAbi,
|
||||
functionName: 'allowance',
|
||||
args: [account, getP2PixAddress()],
|
||||
});
|
||||
|
||||
if (allowance < parseEther(participant.offer.toString())) {
|
||||
// Approve tokens
|
||||
const chain = user.network.value;
|
||||
const hash = await walletClient.writeContract({
|
||||
address: tokenAddress,
|
||||
abi: mockTokenAbi,
|
||||
functionName: 'approve',
|
||||
args: [getP2PixAddress(), parseEther(participant.offer.toString())],
|
||||
account,
|
||||
chain,
|
||||
});
|
||||
|
||||
await publicClient.waitForTransactionReceipt({ hash });
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const addDeposit = async (): Promise<any> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
const walletClient = getWalletClient();
|
||||
const user = useUser();
|
||||
|
||||
if (!walletClient) {
|
||||
throw new Error('Wallet client not initialized');
|
||||
}
|
||||
|
||||
const [account] = await walletClient.getAddresses();
|
||||
|
||||
const sellerId = await createParticipant(user.seller.value);
|
||||
user.setSellerId(sellerId.id);
|
||||
if (!sellerId.id) {
|
||||
throw new Error('Failed to create participant');
|
||||
}
|
||||
const chain = user.network.value;
|
||||
const hash = await walletClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'deposit',
|
||||
args: [
|
||||
user.network.value.id + '-' + sellerId.id,
|
||||
toHex('', { size: 32 }),
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
parseEther(user.seller.value.offer.toString()),
|
||||
true,
|
||||
],
|
||||
account,
|
||||
chain,
|
||||
});
|
||||
|
||||
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||
return receipt;
|
||||
};
|
||||
|
||||
export { approveTokens, addDeposit };
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
import { formatEther, type Address } from 'viem';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
|
||||
import { getContract, getProvider } from "./provider";
|
||||
import { getTokenAddress, possibleChains, isPossibleNetwork } from "./addresses";
|
||||
import { getPublicClient, getWalletClient, getContract } from './provider';
|
||||
|
||||
import mockToken from "@/utils/smart_contract_files/MockToken.json";
|
||||
import { getValidDeposits, getUnreleasedLockById } from './events';
|
||||
|
||||
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 { LockStatus } from '@/model/LockStatus';
|
||||
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
||||
import type { UnreleasedLock } from "@/model/UnreleasedLock";
|
||||
import type { Pix } from "@/model/Pix";
|
||||
export const updateWalletStatus = async (): Promise<void> => {
|
||||
const user = useUser();
|
||||
|
||||
const updateWalletStatus = async (): Promise<void> => {
|
||||
const etherStore = useEtherStore();
|
||||
const publicClient = getPublicClient();
|
||||
const walletClient = getWalletClient();
|
||||
|
||||
const provider = getProvider();
|
||||
const signer = provider.getSigner();
|
||||
|
||||
const { chainId } = await provider.getNetwork();
|
||||
if(!isPossibleNetwork(chainId.toString())){
|
||||
window.alert("Invalid chain!:"+chainId);
|
||||
if (!publicClient || !walletClient) {
|
||||
console.error('Client not initialized');
|
||||
return;
|
||||
}
|
||||
etherStore.setNetworkName(possibleChains[chainId]);
|
||||
|
||||
const mockTokenContract = new ethers.Contract(
|
||||
getTokenAddress(),
|
||||
mockToken.abi,
|
||||
signer
|
||||
);
|
||||
// Get balance
|
||||
const [account] = await walletClient.getAddresses();
|
||||
const balance = await publicClient.getBalance({ address: account });
|
||||
|
||||
const walletAddress = await provider.send("eth_requestAccounts", []);
|
||||
const balance = await mockTokenContract.balanceOf(walletAddress[0]);
|
||||
|
||||
etherStore.setBalance(formatEther(balance));
|
||||
etherStore.setWalletAddress(ethers.utils.getAddress(walletAddress[0]));
|
||||
user.setWalletAddress(account);
|
||||
user.setBalance(formatEther(balance));
|
||||
};
|
||||
|
||||
const listValidDepositTransactionsByWalletAddress = async (
|
||||
walletAddress: string
|
||||
export const listValidDepositTransactionsByWalletAddress = async (
|
||||
walletAddress: Address,
|
||||
): Promise<ValidDeposit[]> => {
|
||||
const walletDeposits = await getValidDeposits(getTokenAddress());
|
||||
|
||||
const user = useUser();
|
||||
const walletDeposits = await getValidDeposits(
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
user.network.value,
|
||||
);
|
||||
if (walletDeposits) {
|
||||
return walletDeposits
|
||||
.filter((deposit) => deposit.seller == walletAddress)
|
||||
.sort((a, b) => {
|
||||
.sort((a: ValidDeposit, b: ValidDeposit) => {
|
||||
return b.blockNumber - a.blockNumber;
|
||||
});
|
||||
}
|
||||
@@ -56,191 +48,353 @@ 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<WalletTransaction[]> => {
|
||||
const p2pContract = getContract(true);
|
||||
|
||||
const filterDeposits = p2pContract.filters.DepositAdded([walletAddress]);
|
||||
const eventsDeposits = await p2pContract.queryFilter(filterDeposits);
|
||||
|
||||
const filterAddedLocks = p2pContract.filters.LockAdded([walletAddress]);
|
||||
const eventsAddedLocks = await p2pContract.queryFilter(filterAddedLocks);
|
||||
|
||||
const filterReleasedLocks = p2pContract.filters.LockReleased([walletAddress]);
|
||||
const eventsReleasedLocks = await p2pContract.queryFilter(
|
||||
filterReleasedLocks
|
||||
);
|
||||
|
||||
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
|
||||
const listReleaseTransactionByWalletAddress = async (
|
||||
walletAddress: string
|
||||
): Promise<Event[]> => {
|
||||
const p2pContract = getContract(true);
|
||||
|
||||
const filterReleasedLocks = p2pContract.filters.LockReleased([walletAddress]);
|
||||
const eventsReleasedLocks = await p2pContract.queryFilter(
|
||||
filterReleasedLocks
|
||||
);
|
||||
|
||||
return eventsReleasedLocks.sort((a, b) => {
|
||||
return b.blockNumber - a.blockNumber;
|
||||
const getLockStatus = async (id: bigint): Promise<LockStatus> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
const [sortedIDs, status] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'getLocksStatus',
|
||||
args: [[id]],
|
||||
});
|
||||
return status[0];
|
||||
};
|
||||
|
||||
const listLockTransactionByWalletAddress = async (
|
||||
walletAddress: string
|
||||
): Promise<Event[]> => {
|
||||
const p2pContract = getContract(true);
|
||||
export const listAllTransactionByWalletAddress = async (
|
||||
walletAddress: Address,
|
||||
): Promise<WalletTransaction[]> => {
|
||||
const user = useUser();
|
||||
|
||||
const filterAddedLocks = p2pContract.filters.LockAdded([walletAddress]);
|
||||
const eventsReleasedLocks = await p2pContract.queryFilter(filterAddedLocks);
|
||||
// Get the current network for the subgraph URL
|
||||
const network = user.network.value;
|
||||
|
||||
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: "",
|
||||
// Query subgraph for all relevant transactions
|
||||
const subgraphQuery = {
|
||||
query: `
|
||||
{
|
||||
depositAddeds(where: {seller: "${walletAddress.toLowerCase()}"}) {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
lockAddeds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
lockReleaseds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
||||
buyer
|
||||
lockId
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
depositWithdrawns(where: {seller: "${walletAddress.toLowerCase()}"}) {
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const addedLocks = await listLockTransactionByWalletAddress(walletAddress);
|
||||
const lockStatus = await p2pContract.getLocksStatus(
|
||||
addedLocks.map((lock) => lock.args?.lockID)
|
||||
);
|
||||
const unreleasedLockId = lockStatus[1].findIndex(
|
||||
(lockStatus: number) => lockStatus == 1
|
||||
);
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
|
||||
if (unreleasedLockId != -1) {
|
||||
const _lockID = lockStatus[0][unreleasedLockId];
|
||||
const lock = await p2pContract.mapLocks(_lockID);
|
||||
const data = await response.json();
|
||||
// Convert all transactions to common WalletTransaction format
|
||||
const transactions: WalletTransaction[] = [];
|
||||
|
||||
const pixTarget = lock.pixTarget;
|
||||
const amount = formatEther(lock?.amount);
|
||||
pixData.pixKey = String(Number(pixTarget));
|
||||
pixData.value = Number(amount);
|
||||
// Process deposit added events
|
||||
if (data.data?.depositAddeds) {
|
||||
for (const deposit of data.data.depositAddeds) {
|
||||
transactions.push({
|
||||
token: deposit.token,
|
||||
blockNumber: parseInt(deposit.blockNumber),
|
||||
blockTimestamp: parseInt(deposit.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(deposit.amount))),
|
||||
seller: deposit.seller,
|
||||
buyer: '',
|
||||
event: 'DepositAdded',
|
||||
lockStatus: undefined,
|
||||
transactionHash: deposit.transactionHash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lockID: _lockID,
|
||||
pix: pixData,
|
||||
};
|
||||
// Process lock added events
|
||||
if (data.data?.lockAddeds) {
|
||||
for (const lock of data.data.lockAddeds) {
|
||||
// Get lock status from the contract
|
||||
const lockStatus = await getLockStatus(BigInt(lock.lockID));
|
||||
|
||||
transactions.push({
|
||||
token: lock.token,
|
||||
blockNumber: parseInt(lock.blockNumber),
|
||||
blockTimestamp: parseInt(lock.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(lock.amount))),
|
||||
seller: lock.seller,
|
||||
buyer: lock.buyer,
|
||||
event: 'LockAdded',
|
||||
lockStatus: lockStatus,
|
||||
transactionHash: lock.transactionHash,
|
||||
transactionID: lock.lockID.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process lock released events
|
||||
if (data.data?.lockReleaseds) {
|
||||
for (const release of data.data.lockReleaseds) {
|
||||
transactions.push({
|
||||
token: undefined, // Subgraph doesn't provide token in this event, we could enhance this later
|
||||
blockNumber: parseInt(release.blockNumber),
|
||||
blockTimestamp: parseInt(release.blockTimestamp),
|
||||
amount: -1, // Amount not available in this event
|
||||
seller: '',
|
||||
buyer: release.buyer,
|
||||
event: 'LockReleased',
|
||||
lockStatus: undefined,
|
||||
transactionHash: release.transactionHash,
|
||||
transactionID: release.lockId.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process deposit withdrawn events
|
||||
if (data.data?.depositWithdrawns) {
|
||||
for (const withdrawal of data.data.depositWithdrawns) {
|
||||
transactions.push({
|
||||
token: withdrawal.token,
|
||||
blockNumber: parseInt(withdrawal.blockNumber),
|
||||
blockTimestamp: parseInt(withdrawal.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(withdrawal.amount))),
|
||||
seller: withdrawal.seller,
|
||||
buyer: '',
|
||||
event: 'DepositWithdrawn',
|
||||
lockStatus: undefined,
|
||||
transactionHash: withdrawal.transactionHash,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort transactions by block number (newest first)
|
||||
return transactions.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||
};
|
||||
|
||||
const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||
const user = useUser();
|
||||
const network = user.network.value;
|
||||
|
||||
// Query subgraph for lock added transactions
|
||||
const subgraphQuery = {
|
||||
query: `
|
||||
{
|
||||
lockAddeds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
try {
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data?.lockAddeds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Transform the subgraph data to match the event log decode format
|
||||
return data.data.lockAddeds
|
||||
.sort((a: any, b: any) => {
|
||||
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
|
||||
})
|
||||
.map((lock: any) => {
|
||||
try {
|
||||
// Create a structure similar to the decoded event log
|
||||
return {
|
||||
eventName: 'LockAdded',
|
||||
args: {
|
||||
buyer: lock.buyer,
|
||||
lockID: BigInt(lock.lockID),
|
||||
seller: lock.seller,
|
||||
token: undefined, // Token not available in LockAdded subgraph event
|
||||
amount: BigInt(lock.amount),
|
||||
},
|
||||
// Add other necessary fields to match the original format
|
||||
blockNumber: BigInt(lock.blockNumber),
|
||||
transactionHash: lock.transactionHash,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing subgraph data', error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((decoded: any) => decoded !== null);
|
||||
} catch (error) {
|
||||
console.error('Error fetching from subgraph:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getActiveLockAmount = async (walletAddress: string): Promise<number> => {
|
||||
const p2pContract = getContract();
|
||||
const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
const user = useUser();
|
||||
const network = user.network.value;
|
||||
|
||||
// Query subgraph for lock added transactions where seller matches
|
||||
const subgraphQuery = {
|
||||
query: `
|
||||
{
|
||||
lockAddeds(where: {seller: "${sellerAddress.toLowerCase()}"}) {
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
try {
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.data?.lockAddeds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Transform the subgraph data to match the event log decode format
|
||||
return data.data.lockAddeds
|
||||
.sort((a: any, b: any) => {
|
||||
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
|
||||
})
|
||||
.map((lock: any) => {
|
||||
try {
|
||||
// Create a structure similar to the decoded event log
|
||||
return {
|
||||
eventName: 'LockAdded',
|
||||
args: {
|
||||
buyer: lock.buyer,
|
||||
lockID: BigInt(lock.lockID),
|
||||
seller: lock.seller,
|
||||
token: undefined, // Token not available in LockAdded subgraph event
|
||||
amount: BigInt(lock.amount),
|
||||
},
|
||||
// Add other necessary fields to match the original format
|
||||
blockNumber: BigInt(lock.blockNumber),
|
||||
transactionHash: lock.transactionHash,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing subgraph data', error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((decoded: any) => decoded !== null);
|
||||
} catch (error) {
|
||||
console.error('Error fetching from subgraph:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const checkUnreleasedLock = async (
|
||||
walletAddress: Address,
|
||||
): Promise<UnreleasedLock | undefined> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
const addedLocks = await listLockTransactionByWalletAddress(walletAddress);
|
||||
|
||||
if (!addedLocks.length) return undefined;
|
||||
|
||||
const lockIds = addedLocks.map((lock: any) => lock.args.lockID);
|
||||
|
||||
const [sortedIDs, status] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'getLocksStatus',
|
||||
args: [lockIds],
|
||||
});
|
||||
|
||||
const unreleasedLockId = status.findIndex(
|
||||
(status: LockStatus) => status == LockStatus.Active,
|
||||
);
|
||||
|
||||
if (unreleasedLockId !== -1)
|
||||
return getUnreleasedLockById(sortedIDs[unreleasedLockId]);
|
||||
};
|
||||
|
||||
export const getActiveLockAmount = async (
|
||||
walletAddress: Address,
|
||||
): Promise<number> => {
|
||||
const { address, abi, client } = await getContract(true);
|
||||
const lockSeller = await listLockTransactionBySellerAddress(walletAddress);
|
||||
|
||||
const lockStatus = await p2pContract.getLocksStatus(
|
||||
lockSeller.map((lock) => lock.args?.lockID)
|
||||
if (!lockSeller.length) return 0;
|
||||
|
||||
const lockIds = lockSeller.map((lock: any) => lock.args.lockID);
|
||||
|
||||
const [sortedIDs, status] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'getLocksStatus',
|
||||
args: [lockIds],
|
||||
});
|
||||
|
||||
const mapLocksRequests = status.map((id: LockStatus) =>
|
||||
client.readContract({
|
||||
address: address,
|
||||
abi,
|
||||
functionName: 'mapLocks',
|
||||
args: [BigInt(id)],
|
||||
}),
|
||||
);
|
||||
|
||||
const activeLockAmount = await lockStatus[1].reduce(
|
||||
async (sumValue: Promise<number>, currentStatus: number, index: number) => {
|
||||
const currValue = await sumValue;
|
||||
let valueToSum = 0;
|
||||
const mapLocksResults = await client.multicall({
|
||||
contracts: mapLocksRequests as any,
|
||||
});
|
||||
|
||||
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,
|
||||
return mapLocksResults.reduce((total: number, lock: any, index: number) => {
|
||||
if (status[index] === 1) {
|
||||
return total + Number(formatEther(lock.amount));
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import BuyConfirmedComponent from "../BuyConfirmedComponent.vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
describe("BuyConfirmedComponent.vue", async () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
const wrapper = mount(BuyConfirmedComponent, {
|
||||
props: {
|
||||
tokenAmount: 1,
|
||||
isCurrentStep: false,
|
||||
},
|
||||
});
|
||||
|
||||
// test("Test component Header Text", () => {
|
||||
// expect(wrapper.html()).toContain("Os tokens já foram transferidos");
|
||||
// expect(wrapper.html()).toContain("para a sua carteira!");
|
||||
// });
|
||||
|
||||
// test("Test component Container Text", () => {
|
||||
// expect(wrapper.html()).toContain("Tokens recebidos");
|
||||
// expect(wrapper.html()).toContain("BRZ");
|
||||
// expect(wrapper.html()).toContain("Não encontrou os tokens?");
|
||||
// expect(wrapper.html()).toContain("Clique no botão abaixo para");
|
||||
// expect(wrapper.html()).toContain("cadastrar o BRZ em sua carteira.");
|
||||
// });
|
||||
|
||||
test("Test makeAnotherTransactionEmit", async () => {
|
||||
wrapper.vm.$emit("makeAnotherTransaction");
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted("makeAnotherTransaction")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { withdrawDeposit } from "@/blockchain/buyerMethods";
|
||||
import { withdrawDeposit } from '@/blockchain/buyerMethods';
|
||||
import {
|
||||
getActiveLockAmount,
|
||||
listAllTransactionByWalletAddress,
|
||||
listValidDepositTransactionsByWalletAddress,
|
||||
} from "@/blockchain/wallet";
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
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";
|
||||
} from '@/blockchain/wallet';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import ListingComponent from '@/components/ListingComponent/ListingComponent.vue';
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
@@ -19,8 +18,8 @@ const props = defineProps<{
|
||||
isCurrentStep: boolean;
|
||||
}>();
|
||||
|
||||
const etherStore = useEtherStore();
|
||||
const { walletAddress } = storeToRefs(etherStore);
|
||||
const user = useUser();
|
||||
const { walletAddress } = useUser();
|
||||
|
||||
const lastWalletTransactions = ref<WalletTransaction[]>([]);
|
||||
const depositList = ref<ValidDeposit[]>([]);
|
||||
@@ -29,14 +28,14 @@ const activeLockAmount = ref<number>(0);
|
||||
// methods
|
||||
|
||||
const getWalletTransactions = async () => {
|
||||
etherStore.setLoadingWalletTransactions(true);
|
||||
user.setLoadingWalletTransactions(true);
|
||||
if (walletAddress.value) {
|
||||
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
|
||||
walletAddress.value
|
||||
walletAddress.value,
|
||||
);
|
||||
|
||||
const allUserTransactions = await listAllTransactionByWalletAddress(
|
||||
walletAddress.value
|
||||
walletAddress.value,
|
||||
);
|
||||
|
||||
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
|
||||
@@ -48,25 +47,28 @@ const getWalletTransactions = async () => {
|
||||
lastWalletTransactions.value = allUserTransactions;
|
||||
}
|
||||
}
|
||||
etherStore.setLoadingWalletTransactions(false);
|
||||
user.setLoadingWalletTransactions(false);
|
||||
};
|
||||
|
||||
const callWithdraw = async (amount: string) => {
|
||||
if (amount) {
|
||||
etherStore.setLoadingWalletTransactions(true);
|
||||
const withdraw = await withdrawDeposit(amount);
|
||||
user.setLoadingWalletTransactions(true);
|
||||
const withdraw = await withdrawDeposit(
|
||||
amount,
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
);
|
||||
if (withdraw) {
|
||||
console.log("Saque realizado!");
|
||||
console.log('Saque realizado!');
|
||||
await getWalletTransactions();
|
||||
} else {
|
||||
console.log("Não foi possível realizar o saque!");
|
||||
console.log('Não foi possível realizar o saque!');
|
||||
}
|
||||
etherStore.setLoadingWalletTransactions(false);
|
||||
user.setLoadingWalletTransactions(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["makeAnotherTransaction"]);
|
||||
const emit = defineEmits(['makeAnotherTransaction']);
|
||||
|
||||
// observer
|
||||
watch(props, async (): Promise<void> => {
|
||||
@@ -86,19 +88,19 @@ onMounted(async () => {
|
||||
para a sua carteira!
|
||||
</span>
|
||||
</div>
|
||||
<div class="blur-container">
|
||||
<div
|
||||
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="main-container">
|
||||
<div class="flex flex-col w-full bg-white px-10 py-5 rounded-lg">
|
||||
<div>
|
||||
<p>Tokens recebidos</p>
|
||||
<p class="text-2xl text-gray-900">{{ props.tokenAmount }} BRZ</p>
|
||||
<p class="text-2xl text-gray-900">
|
||||
{{ props.tokenAmount }} {{ user.selectedToken }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="my-5">
|
||||
<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.
|
||||
cadastrar o {{ user.selectedToken }} em sua carteira.
|
||||
</p>
|
||||
</div>
|
||||
<CustomButton :text="'Cadastrar token'" @buttonClicked="() => {}" />
|
||||
@@ -128,6 +130,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
@@ -143,42 +146,13 @@ p {
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
.blur-container-row {
|
||||
@apply flex flex-row justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-8 w-1/3;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex w-full max-w-xs md:max-w-lg flex-col justify-center items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10;
|
||||
}
|
||||
|
||||
.last-release-info {
|
||||
@apply font-medium text-base text-gray-900;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
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>
|
||||
453
src/components/BuyerSteps/BuyerSearchComponent.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { verifyNetworkLiquidity } from '@/utils/networkLiquidity';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import { decimalCount } from '@/utils/decimalCount';
|
||||
import { getTokenImage, getNetworkImage } from '@/utils/imagesPath';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { Networks } from '@/config/networks';
|
||||
import { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { getContract } from '@/blockchain/provider';
|
||||
import { reputationAbi } from '@/blockchain/abi';
|
||||
import { type Address } from 'viem';
|
||||
|
||||
// Store reference
|
||||
const user = useUser();
|
||||
const selectTokenToggle = ref<boolean>(false);
|
||||
|
||||
const {
|
||||
walletAddress,
|
||||
network,
|
||||
selectedToken,
|
||||
depositsValidList,
|
||||
loadingNetworkLiquidity,
|
||||
} = user;
|
||||
|
||||
// html references
|
||||
const tokenDropdownRef = ref<any>(null);
|
||||
|
||||
// Reactive state
|
||||
const tokenValue = ref<number>(0);
|
||||
const enableConfirmButton = ref<boolean>(false);
|
||||
const hasLiquidity = ref<boolean>(true);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const identification = ref<string>('');
|
||||
const selectedDeposits = ref<ValidDeposit[]>();
|
||||
const reputationLimit = ref<number | null>(null);
|
||||
const exceedsReputationLimit = ref<boolean>(false);
|
||||
|
||||
import ChevronDown from '@/assets/chevronDown.svg';
|
||||
import { useOnboard } from '@web3-onboard/vue';
|
||||
import { getParticipantID } from '@/blockchain/events';
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['tokenBuy']);
|
||||
|
||||
const castAddrToKey = (address: Address): bigint => {
|
||||
return BigInt(address) << BigInt(12);
|
||||
};
|
||||
|
||||
const getUserCredit = async (userAddress: Address): Promise<bigint> => {
|
||||
try {
|
||||
const { address, abi, client } = await getContract(true);
|
||||
const userKey = castAddrToKey(userAddress);
|
||||
|
||||
const userCredit = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'userRecord',
|
||||
args: [userKey],
|
||||
});
|
||||
|
||||
return userCredit as bigint;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user credit:', error);
|
||||
return BigInt(0);
|
||||
}
|
||||
};
|
||||
|
||||
const getReputationAddress = async (): Promise<Address | null> => {
|
||||
try {
|
||||
const { address, abi, client } = await getContract(true);
|
||||
|
||||
const reputationAddr = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: 'reputation',
|
||||
});
|
||||
|
||||
return reputationAddr as Address;
|
||||
} catch (error) {
|
||||
console.error('Error fetching reputation address:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getSpendLimit = async (userCredit: bigint): Promise<bigint> => {
|
||||
try {
|
||||
const reputationAddr = await getReputationAddress();
|
||||
if (!reputationAddr) return BigInt(0);
|
||||
|
||||
const { client } = await getContract(true);
|
||||
|
||||
const spendLimit = await client.readContract({
|
||||
address: reputationAddr,
|
||||
abi: reputationAbi,
|
||||
functionName: 'limiter',
|
||||
args: [userCredit],
|
||||
});
|
||||
|
||||
return spendLimit as bigint;
|
||||
} catch (error) {
|
||||
console.error('Error fetching spend limit:', error);
|
||||
return BigInt(0);
|
||||
}
|
||||
};
|
||||
|
||||
const checkReputationLimit = async (inputValue: number): Promise<void> => {
|
||||
exceedsReputationLimit.value = false;
|
||||
|
||||
if (!walletAddress.value) {
|
||||
reputationLimit.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userCredit = await getUserCredit(walletAddress.value);
|
||||
const spendLimitRaw = await getSpendLimit(userCredit);
|
||||
|
||||
const spendLimitNumber = Number(spendLimitRaw);
|
||||
reputationLimit.value = spendLimitNumber;
|
||||
|
||||
exceedsReputationLimit.value = spendLimitNumber < inputValue;
|
||||
enableConfirmButton.value = !exceedsReputationLimit.value;
|
||||
} catch (error) {
|
||||
console.error('Error checking reputation limit:', error);
|
||||
reputationLimit.value = null;
|
||||
exceedsReputationLimit.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Blockchain methods
|
||||
const connectAccount = async (): Promise<void> => {
|
||||
const { connectWallet } = useOnboard();
|
||||
await connectWallet();
|
||||
};
|
||||
|
||||
const emitConfirmButton = async (): Promise<void> => {
|
||||
const deposit = selectedDeposits.value?.find(
|
||||
(d) => d.network === network.value,
|
||||
);
|
||||
if (!deposit) return;
|
||||
deposit.participantID = await getParticipantID(deposit.seller, deposit.token);
|
||||
emit('tokenBuy', deposit, tokenValue.value);
|
||||
};
|
||||
|
||||
// Debounce methods
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
tokenValue.value = Number(value);
|
||||
|
||||
if (decimalCount(String(tokenValue.value)) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
checkReputationLimit(tokenValue.value);
|
||||
verifyLiquidity();
|
||||
};
|
||||
|
||||
const openTokenSelection = (): void => {
|
||||
selectTokenToggle.value = true;
|
||||
};
|
||||
|
||||
onClickOutside(tokenDropdownRef, () => {
|
||||
selectTokenToggle.value = false;
|
||||
});
|
||||
|
||||
const handleSelectedToken = (token: TokenEnum): void => {
|
||||
user.setSelectedToken(token);
|
||||
selectTokenToggle.value = false;
|
||||
};
|
||||
|
||||
// Verify if there is a valid deposit to buy
|
||||
const verifyLiquidity = (): void => {
|
||||
enableConfirmButton.value = false;
|
||||
if (!walletAddress.value) return;
|
||||
const selDeposits = verifyNetworkLiquidity(
|
||||
tokenValue.value,
|
||||
walletAddress.value,
|
||||
depositsValidList.value,
|
||||
);
|
||||
selectedDeposits.value = selDeposits;
|
||||
hasLiquidity.value = !!selDeposits.find((d) => d.network === network.value);
|
||||
enableOrDisableConfirmButton();
|
||||
};
|
||||
|
||||
const enableOrDisableConfirmButton = (): void => {
|
||||
if (!selectedDeposits.value) {
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedDeposits.value.find((d) => d.network === network.value)) {
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
enableConfirmButton.value = true;
|
||||
};
|
||||
|
||||
watch(network, (): void => {
|
||||
verifyLiquidity();
|
||||
enableOrDisableConfirmButton();
|
||||
});
|
||||
|
||||
watch(walletAddress, (): void => {
|
||||
verifyLiquidity();
|
||||
});
|
||||
|
||||
const availableNetworks = computed(() => {
|
||||
if (!selectedDeposits.value) return [];
|
||||
return Object.values(Networks).filter((network) =>
|
||||
selectedDeposits.value?.some((d) => d.network.id === network.id),
|
||||
);
|
||||
});
|
||||
|
||||
// Add form submission handler
|
||||
const handleSubmit = async (e: Event): Promise<void> => {
|
||||
e.preventDefault();
|
||||
if (walletAddress.value) {
|
||||
await emitConfirmButton();
|
||||
} else {
|
||||
await connectAccount();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="text-container">
|
||||
<span
|
||||
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
|
||||
>
|
||||
Adquira cripto com apenas um Pix</span
|
||||
>
|
||||
<span class="text font-medium sm:text-base text-sm max-w-[28rem]"
|
||||
>Digite um valor, confira a oferta, conecte sua carteira e receba os
|
||||
tokens após realizar o Pix</span
|
||||
>
|
||||
</div>
|
||||
<form class="main-container" @submit="handleSubmit">
|
||||
<div class="backdrop-blur -z-10 w-full h-full"></div>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg">
|
||||
<div class="flex justify-between sm:w-full items-center">
|
||||
<input
|
||||
type="number"
|
||||
name="tokenAmount"
|
||||
class="border-none outline-none text-lg text-gray-900 sm:flex-1 max-w-[60%]"
|
||||
v-bind:class="{
|
||||
'font-semibold': tokenValue != undefined,
|
||||
'text-xl': tokenValue != undefined,
|
||||
}"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="0"
|
||||
step=".01"
|
||||
required
|
||||
/>
|
||||
<div class="relative overflow-visible ml-auto sm:ml-0">
|
||||
<button
|
||||
ref="tokenDropdownRef"
|
||||
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
|
||||
@click="openTokenSelection()"
|
||||
>
|
||||
<img
|
||||
alt="Token image"
|
||||
class="sm:w-fit w-4"
|
||||
:src="getTokenImage(selectedToken)"
|
||||
/>
|
||||
<span
|
||||
class="text-gray-900 sm:text-lg text-md font-medium"
|
||||
id="token"
|
||||
>{{ selectedToken }}</span
|
||||
>
|
||||
<ChevronDown
|
||||
class="pr-4 sm:pr-0 transition-all duration-500 ease-in-out invert"
|
||||
:class="{ 'scale-y-[-1]': selectTokenToggle }"
|
||||
alt="Chevron Down"
|
||||
/>
|
||||
</button>
|
||||
<transition name="dropdown">
|
||||
<div
|
||||
v-if="selectTokenToggle"
|
||||
class="mt-2 text-gray-900 absolute right-0 z-50 w-full min-w-max"
|
||||
>
|
||||
<div
|
||||
class="bg-white rounded-xl z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
||||
>
|
||||
<div
|
||||
v-for="token in TokenEnum"
|
||||
:key="token"
|
||||
class="flex menu-button gap-2 px-4 cursor-pointer hover:bg-gray-300 transition-colors"
|
||||
@click="handleSelectedToken(token)"
|
||||
>
|
||||
<img
|
||||
:alt="token + ' logo'"
|
||||
width="20"
|
||||
height="20"
|
||||
:src="getTokenImage(token)"
|
||||
/>
|
||||
<span
|
||||
class="text-gray-900 py-4 text-end font-semibold text-sm"
|
||||
>
|
||||
{{ token }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-divide py-2 mb-2"></div>
|
||||
<div class="flex justify-between" v-if="!loadingNetworkLiquidity">
|
||||
<p class="text-gray-500 font-normal text-sm w-auto">
|
||||
~ R$ {{ tokenValue.toFixed(2) }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<img
|
||||
v-for="network in availableNetworks"
|
||||
:key="network.id"
|
||||
:alt="`${network.name} image`"
|
||||
:src="getNetworkImage(network.name)"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center items-center"
|
||||
v-if="loadingNetworkLiquidity"
|
||||
>
|
||||
<span class="text-gray-900 font-normal text-sm mr-2"
|
||||
>Carregando liquidez das redes.</span
|
||||
>
|
||||
<SpinnerComponent width="4" height="4"></SpinnerComponent>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-if="!validDecimals && !loadingNetworkLiquidity"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize no máximo 2 casas decimais</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-else-if="
|
||||
!hasLiquidity &&
|
||||
!loadingNetworkLiquidity &&
|
||||
tokenValue > 0 &&
|
||||
!exceedsReputationLimit
|
||||
"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Atualmente não há liquidez nas rede selecionada para sua
|
||||
demanda</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-if="
|
||||
exceedsReputationLimit &&
|
||||
!loadingNetworkLiquidity &&
|
||||
reputationLimit !== null
|
||||
"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>O valor excede o limite permitido pela sua reputação. Limite
|
||||
máximo: {{ reputationLimit }} {{ selectedToken }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<input
|
||||
type="text"
|
||||
v-model="identification"
|
||||
maxlength="14"
|
||||
:pattern="'^\\d{11}$|^\\d{14}$'"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
||||
placeholder="Digite seu CPF ou CNPJ (somente números)"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<CustomButton
|
||||
v-if="walletAddress"
|
||||
type="submit"
|
||||
text="Confirmar Oferta"
|
||||
:isDisabled="!enableConfirmButton"
|
||||
/>
|
||||
<CustomButton
|
||||
v-else
|
||||
text="Conectar carteira"
|
||||
@buttonClicked="connectAccount()"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.custom-button {
|
||||
@apply w-full py-3 px-6 rounded-lg font-semibold text-white bg-indigo-600
|
||||
hover:bg-indigo-700 disabled:bg-gray-400 disabled:cursor-not-allowed
|
||||
transition-colors duration-200;
|
||||
}
|
||||
</style>
|
||||
291
src/components/BuyerSteps/QrCodeComponent.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import CustomModal from '@/components/ui/CustomModal.vue';
|
||||
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||
import { createSolicitation, getSolicitation, type Offer } from '@/utils/bbPay';
|
||||
import { getParticipantID } from '@/blockchain/events';
|
||||
import { getUnreleasedLockById } from '@/blockchain/events';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
lockID: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const qrCode = ref<string>('');
|
||||
const qrCodeSvg = ref<string>('');
|
||||
const showWarnModal = ref<boolean>(true);
|
||||
const pixTimestamp = ref<string>('');
|
||||
const releaseSignature = ref<string>('');
|
||||
const solicitationData = ref<any>(null);
|
||||
const pollingInterval = ref<NodeJS.Timeout | null>(null);
|
||||
const copyFeedback = ref<boolean>(false);
|
||||
const copyFeedbackTimeout = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Function to generate QR code SVG
|
||||
const generateQrCodeSvg = async (text: string) => {
|
||||
try {
|
||||
const svgString = await QRCode.toString(text, {
|
||||
type: 'svg',
|
||||
width: 192, // 48 * 4 for better quality
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
qrCodeSvg.value = svgString;
|
||||
} catch (error) {
|
||||
console.error('Error generating QR code SVG:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['pixValidated']);
|
||||
|
||||
// Function to check solicitation status
|
||||
const checkSolicitationStatus = async () => {
|
||||
if (!solicitationData.value?.numeroSolicitacao) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getSolicitation(
|
||||
solicitationData.value.numeroSolicitacao,
|
||||
);
|
||||
|
||||
if (response.signature) {
|
||||
pixTimestamp.value = response.pixTimestamp;
|
||||
releaseSignature.value = response.signature;
|
||||
// Stop polling when payment is confirmed
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking solicitation status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to start polling
|
||||
const startPolling = () => {
|
||||
// Clear any existing interval
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value);
|
||||
}
|
||||
|
||||
// Start new polling interval (10 seconds)
|
||||
pollingInterval.value = setInterval(checkSolicitationStatus, 10000);
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (!qrCode.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(qrCode.value);
|
||||
|
||||
if (copyFeedbackTimeout.value) {
|
||||
clearTimeout(copyFeedbackTimeout.value);
|
||||
}
|
||||
|
||||
copyFeedback.value = true;
|
||||
|
||||
copyFeedbackTimeout.value = setTimeout(() => {
|
||||
copyFeedback.value = false;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error copying to clipboard:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { tokenAddress, sellerAddress, amount } = await getUnreleasedLockById(
|
||||
BigInt(props.lockID),
|
||||
);
|
||||
|
||||
const participantId = await getParticipantID(sellerAddress, tokenAddress);
|
||||
|
||||
const offer: Offer = {
|
||||
amount,
|
||||
sellerId: participantId,
|
||||
};
|
||||
|
||||
const response = await createSolicitation(offer);
|
||||
solicitationData.value = response;
|
||||
|
||||
// Update qrCode if the response contains QR code data
|
||||
if (response?.informacoesPIX?.textoQrCode) {
|
||||
qrCode.value = response.informacoesPIX?.textoQrCode;
|
||||
// Generate SVG QR code
|
||||
await generateQrCodeSvg(qrCode.value);
|
||||
}
|
||||
|
||||
// Start polling for solicitation status
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
console.error('Error creating solicitation:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up interval on component unmount
|
||||
onUnmounted(() => {
|
||||
if (pollingInterval.value) {
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
}
|
||||
if (copyFeedbackTimeout.value) {
|
||||
clearTimeout(copyFeedbackTimeout.value);
|
||||
copyFeedbackTimeout.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="text-container">
|
||||
<span
|
||||
class="text font-extrabold lg:text-2xl text-xl sm:max-w-[30rem] max-w-[24rem]"
|
||||
>
|
||||
Utilize o QR Code ou copie o código para realizar o Pix
|
||||
</span>
|
||||
<span class="text font-medium lg:text-md text-sm max-w-[28rem]">
|
||||
Após realizar o Pix no banco de sua preferência, clique no botão abaixo
|
||||
para liberação dos tokens.
|
||||
</span>
|
||||
</div>
|
||||
<div class="main-container max-w-md text-black">
|
||||
<div
|
||||
class="flex-col items-center justify-center flex w-full bg-white sm:p-8 p-4 rounded-lg break-normal"
|
||||
>
|
||||
<div
|
||||
v-if="qrCodeSvg"
|
||||
v-html="qrCodeSvg"
|
||||
class="w-48 h-48 flex items-center justify-center"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="w-48 h-48 flex items-center justify-center rounded-lg"
|
||||
>
|
||||
<SpinnerComponent width="8" height="8"></SpinnerComponent>
|
||||
</div>
|
||||
<span class="text-center font-bold">Código pix</span>
|
||||
<div class="break-words w-4/5">
|
||||
<span class="text-center text-xs">
|
||||
{{ qrCode }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<img
|
||||
alt="Copy PIX code"
|
||||
src="@/assets/copyPix.svg?url"
|
||||
width="16"
|
||||
height="16"
|
||||
class="pt-2 cursor-pointer hover:opacity-70 transition-opacity"
|
||||
@click="copyToClipboard"
|
||||
/>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback"
|
||||
class="text-xs text-emerald-500 font-semibold"
|
||||
>
|
||||
Código copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
:is-disabled="releaseSignature === ''"
|
||||
:text="
|
||||
releaseSignature ? 'Enviar para a rede' : 'Validando pagamento...'
|
||||
"
|
||||
@button-clicked="
|
||||
emit('pixValidated', { pixTimestamp, signature: releaseSignature })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<CustomModal
|
||||
v-if="showWarnModal"
|
||||
@close-modal="showWarnModal = false"
|
||||
:isRedirectModal="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
/* Most modern browsers support this now. */
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #080808;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #080808;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply rounded-lg border border-gray-200 p-2 text-black;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply font-semibold tracking-wide text-emerald-50;
|
||||
}
|
||||
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-6 max-w-screen-sm;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/* Fade transition for copy feedback */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
text: String,
|
||||
isDisabled: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["buttonClicked"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
@click="emit('buttonClicked')"
|
||||
v-bind:class="{ 'opacity-70': props.isDisabled }"
|
||||
:disabled="props.isDisabled ? props.isDisabled : false"
|
||||
>
|
||||
{{ props.text }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button {
|
||||
@apply rounded-lg w-full text-base font-semibold text-gray-900 p-4 bg-amber-400;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import CustomButton from "../CustomButton.vue";
|
||||
|
||||
describe("CustomButton.vue", () => {
|
||||
test("Test button content", () => {
|
||||
const wrapper = mount(CustomButton, {
|
||||
props: {
|
||||
text: "Testing",
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toContain("Testing");
|
||||
});
|
||||
|
||||
test("Test if disabled props works", () => {
|
||||
const wrapper = mount(CustomButton, {
|
||||
props: {
|
||||
isDisabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
//@ts-ignore
|
||||
const button = wrapper.find(".button") as HTMLButtonElement;
|
||||
//@ts-ignore
|
||||
expect(button.element.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isRedirectModal: Boolean,
|
||||
});
|
||||
|
||||
const modalColor = ref<string>("white");
|
||||
const modalHeight = ref<string>("250px");
|
||||
const pFontSize = ref<string>("16px");
|
||||
|
||||
if (props.isRedirectModal) {
|
||||
modalColor.value = "rgba(251, 191, 36, 1)";
|
||||
modalHeight.value = "150px";
|
||||
pFontSize.value = "20px";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
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 só 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>
|
||||
@@ -1,27 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
78
src/components/Explorer/AnalyticsCard.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
changeType?: 'positive' | 'negative' | 'neutral';
|
||||
icon?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
changeType: 'neutral',
|
||||
loading: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="analytics-card">
|
||||
<div class="analytics-content">
|
||||
<div v-if="loading" class="analytics-value">
|
||||
<div class="animate-pulse bg-gray-300 h-8 w-16 rounded"></div>
|
||||
</div>
|
||||
<div v-else class="analytics-value">{{ value }}</div>
|
||||
<div class="analytics-title">{{ title }}</div>
|
||||
<div
|
||||
v-if="change && !loading"
|
||||
class="analytics-change"
|
||||
:class="`change-${changeType}`"
|
||||
>
|
||||
{{ change }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="icon && !loading" class="analytics-icon">
|
||||
<img :src="icon" :alt="`${title} icon`" class="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.analytics-card {
|
||||
@apply bg-white rounded-lg border border-gray-200 p-6 flex items-center justify-between shadow-lg;
|
||||
}
|
||||
|
||||
.analytics-content {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.analytics-value {
|
||||
@apply text-2xl font-bold text-amber-400 mb-1 break-words overflow-hidden;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.analytics-title {
|
||||
@apply text-sm text-gray-900 mb-1;
|
||||
}
|
||||
|
||||
.analytics-change {
|
||||
@apply text-xs font-medium;
|
||||
}
|
||||
|
||||
.change-positive {
|
||||
@apply text-green-600;
|
||||
}
|
||||
|
||||
.change-negative {
|
||||
@apply text-red-600;
|
||||
}
|
||||
|
||||
.change-neutral {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
.analytics-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
</style>
|
||||
311
src/components/Explorer/TransactionTable.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
type: 'deposit' | 'lock' | 'release' | 'return';
|
||||
timestamp: string;
|
||||
seller?: string;
|
||||
buyer?: string | null;
|
||||
amount: string;
|
||||
token: string;
|
||||
blockNumber: string;
|
||||
transactionHash: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
transactions: Transaction[];
|
||||
networkExplorerUrl: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const copyFeedback = ref<{ [key: string]: boolean }>({});
|
||||
const copyFeedbackTimeout = ref<{ [key: string]: NodeJS.Timeout | null }>({});
|
||||
|
||||
const getTransactionTypeInfo = (type: string) => {
|
||||
const typeMap = {
|
||||
deposit: { label: 'Depósito', status: 'completed' as const },
|
||||
lock: { label: 'Bloqueio', status: 'open' as const },
|
||||
release: { label: 'Liberação', status: 'completed' as const },
|
||||
return: { label: 'Retorno', status: 'expired' as const },
|
||||
};
|
||||
return (
|
||||
typeMap[type as keyof typeof typeMap] || {
|
||||
label: type,
|
||||
status: 'pending' as const,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getTransactionTypeColor = (type: string) => {
|
||||
const colorMap = {
|
||||
deposit: 'text-emerald-600',
|
||||
lock: 'text-amber-600',
|
||||
release: 'text-emerald-600',
|
||||
return: 'text-gray-600',
|
||||
};
|
||||
return colorMap[type as keyof typeof colorMap] || 'text-gray-600';
|
||||
};
|
||||
|
||||
const formatAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
const formatAmount = (amount: string, decimals: number = 18): string => {
|
||||
const num = parseFloat(amount) / Math.pow(10, decimals);
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const getExplorerUrl = (txHash: string) => {
|
||||
return `${props.networkExplorerUrl}/tx/${txHash}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (address: string, key: string) => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(address);
|
||||
|
||||
if (copyFeedbackTimeout.value[key]) {
|
||||
clearTimeout(copyFeedbackTimeout.value[key]!);
|
||||
}
|
||||
|
||||
copyFeedback.value[key] = true;
|
||||
|
||||
copyFeedbackTimeout.value[key] = setTimeout(() => {
|
||||
copyFeedback.value[key] = false;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error copying to clipboard:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="hidden lg:block overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">
|
||||
Horário
|
||||
</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Tipo</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">
|
||||
Participantes
|
||||
</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Valor</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Bloco</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
class="border-b border-gray-100 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<td class="py-4 px-4">
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ transaction.timestamp }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<span
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ getTransactionTypeInfo(transaction.type).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<div class="space-y-1">
|
||||
<div v-if="transaction.seller" class="text-sm">
|
||||
<span class="text-gray-600">Vendedor: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.seller,
|
||||
`seller-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.seller) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`seller-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="transaction.buyer" class="text-sm">
|
||||
<span class="text-gray-600">Comprador: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.buyer,
|
||||
`buyer-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.buyer) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`buyer-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<div class="text-sm font-semibold text-emerald-600">
|
||||
{{ formatAmount(transaction.amount, 18) }} BRZ
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<div class="text-sm text-gray-600 font-mono">
|
||||
#{{ transaction.blockNumber }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<a
|
||||
:href="getExplorerUrl(transaction.transactionHash)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors"
|
||||
>
|
||||
Explorador
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="lg:hidden space-y-4">
|
||||
<div
|
||||
v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ getTransactionTypeInfo(transaction.type).label }}
|
||||
</span>
|
||||
<div class="text-sm text-gray-600">{{ transaction.timestamp }}</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<div v-if="transaction.seller" class="text-sm">
|
||||
<span class="text-gray-600">Vendedor: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.seller,
|
||||
`seller-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.seller) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`seller-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="transaction.buyer" class="text-sm">
|
||||
<span class="text-gray-600">Comprador: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.buyer) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`buyer-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-600">Valor: </span>
|
||||
<span class="font-semibold text-emerald-600"
|
||||
>{{ formatAmount(transaction.amount, 18) }} BRZ</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-600">Bloco: </span>
|
||||
<span class="text-gray-900 font-mono"
|
||||
>#{{ transaction.blockNumber }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="getExplorerUrl(transaction.transactionHash)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors"
|
||||
>
|
||||
Ver no Explorador
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="transactions.length === 0" class="text-center py-12">
|
||||
<div class="text-gray-500 text-lg mb-2">📭</div>
|
||||
<p class="text-gray-600">Nenhuma transação encontrada</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
212
src/components/ListingComponent/BalanceCard.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { decimalCount } from '@/utils/decimalCount';
|
||||
import { useFloating, arrow, offset, flip, shift } from '@floating-ui/vue';
|
||||
import IconButton from '../ui/IconButton.vue';
|
||||
import withdrawIcon from '@/assets/withdraw.svg?url';
|
||||
|
||||
const props = defineProps<{
|
||||
validDeposits: ValidDeposit[];
|
||||
activeLockAmount: number;
|
||||
selectedToken: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
withdraw: [amount: string];
|
||||
}>();
|
||||
|
||||
const withdrawAmount = ref<string>('');
|
||||
const isCollapsibleOpen = ref<boolean>(false);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const validWithdrawAmount = ref<boolean>(true);
|
||||
const enableConfirmButton = ref<boolean>(false);
|
||||
const showInfoTooltip = ref<boolean>(false);
|
||||
const floatingArrow = ref(null);
|
||||
|
||||
const reference = ref<HTMLElement | null>(null);
|
||||
const floating = ref<HTMLElement | null>(null);
|
||||
const infoText = ref<HTMLElement | null>(null);
|
||||
|
||||
const remaining = computed(() => {
|
||||
if (props.validDeposits.length > 0) {
|
||||
const deposit = props.validDeposits[0];
|
||||
return deposit ? deposit.remaining : 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
if (decimalCount(String(value)) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
if (value > remaining.value) {
|
||||
validWithdrawAmount.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validWithdrawAmount.value = true;
|
||||
enableConfirmButton.value = true;
|
||||
};
|
||||
|
||||
const callWithdraw = () => {
|
||||
if (enableConfirmButton.value && withdrawAmount.value) {
|
||||
emit('withdraw', withdrawAmount.value);
|
||||
// Reset form after withdraw
|
||||
withdrawAmount.value = '';
|
||||
isCollapsibleOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openWithdrawForm = () => {
|
||||
isCollapsibleOpen.value = true;
|
||||
};
|
||||
|
||||
const cancelWithdraw = () => {
|
||||
isCollapsibleOpen.value = false;
|
||||
withdrawAmount.value = '';
|
||||
validDecimals.value = true;
|
||||
validWithdrawAmount.value = true;
|
||||
enableConfirmButton.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useFloating(reference, floating, {
|
||||
placement: 'right',
|
||||
middleware: [
|
||||
offset(10),
|
||||
flip(),
|
||||
shift(),
|
||||
arrow({ element: floatingArrow }),
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full bg-white p-4 sm:p-6 rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm leading-5 font-medium text-gray-600">
|
||||
Saldo disponível
|
||||
</p>
|
||||
<p class="text-xl leading-7 font-semibold text-gray-900">
|
||||
{{ remaining }} {{ selectedToken }}
|
||||
</p>
|
||||
<div class="flex gap-2 w-32 sm:w-56" v-if="activeLockAmount != 0">
|
||||
<span class="text-xs font-normal text-gray-400" ref="infoText">
|
||||
{{ `com ${activeLockAmount.toFixed(2)} ${selectedToken} em lock` }}
|
||||
</span>
|
||||
<div
|
||||
class="absolute mt-[2px] md-view"
|
||||
:style="{ left: `${(infoText?.clientWidth ?? 108) + 4}px` }"
|
||||
>
|
||||
<img
|
||||
alt="info image"
|
||||
src="@/assets/info.svg?url"
|
||||
aria-describedby="tooltip"
|
||||
ref="reference"
|
||||
@mouseover="showInfoTooltip = true"
|
||||
@mouseout="showInfoTooltip = false"
|
||||
/>
|
||||
<div
|
||||
role="tooltip"
|
||||
ref="floating"
|
||||
class="w-56 z-50 tooltip md-view"
|
||||
v-if="showInfoTooltip"
|
||||
>
|
||||
Valor "em lock" significa que a quantia está aguardando
|
||||
confirmação de compra e só estará disponível para saque caso a
|
||||
transação expire.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!isCollapsibleOpen" class="flex justify-end items-center">
|
||||
<IconButton
|
||||
text="Sacar"
|
||||
:icon="withdrawIcon"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:full-width="false"
|
||||
@click="openWithdrawForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-5">
|
||||
<div v-show="isCollapsibleOpen" class="py-2 w-100">
|
||||
<p class="text-sm leading-5 font-medium">Valor do saque</p>
|
||||
<input
|
||||
type="number"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="0"
|
||||
class="text-2xl text-gray-900 w-full outline-none"
|
||||
v-model="withdrawAmount"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center" v-if="!validDecimals">
|
||||
<span class="text-red-500 font-normal text-sm">
|
||||
Por favor utilize no máximo 2 casas decimais
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-center" v-else-if="!validWithdrawAmount">
|
||||
<span class="text-red-500 font-normal text-sm">
|
||||
Saldo insuficiente
|
||||
</span>
|
||||
</div>
|
||||
<hr v-show="isCollapsibleOpen" class="pb-3" />
|
||||
<div v-show="isCollapsibleOpen" class="flex justify-between items-center">
|
||||
<h1
|
||||
@click="cancelWithdraw"
|
||||
class="text-black font-medium cursor-pointer hover:text-gray-600 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</h1>
|
||||
|
||||
<IconButton
|
||||
text="Sacar"
|
||||
:icon="withdrawIcon"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:full-width="false"
|
||||
:disabled="!enableConfirmButton"
|
||||
@click="callWithdraw"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
p {
|
||||
@apply text-gray-900;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.md-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,13 @@
|
||||
<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 { 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";
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { ref, watch } from 'vue';
|
||||
import SpinnerComponent from '../ui/SpinnerComponent.vue';
|
||||
import BalanceCard from './BalanceCard.vue';
|
||||
import TransactionCard from './TransactionCard.vue';
|
||||
|
||||
const etherStore = useEtherStore();
|
||||
const user = useUser();
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
@@ -20,84 +16,14 @@ const props = defineProps<{
|
||||
activeLockAmount: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["depositWithdrawn"]);
|
||||
const emit = defineEmits(['depositWithdrawn']);
|
||||
|
||||
const { loadingWalletTransactions } = user;
|
||||
|
||||
const { loadingWalletTransactions } = storeToRefs(etherStore);
|
||||
const remaining = ref<number>(0.0);
|
||||
const itemsToShow = ref<WalletTransaction[]>([]);
|
||||
const 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 callWithdraw = (amount: string) => {
|
||||
emit('depositWithdrawn', amount);
|
||||
};
|
||||
|
||||
const showInitialItems = (): void => {
|
||||
@@ -105,46 +31,18 @@ const showInitialItems = (): void => {
|
||||
};
|
||||
|
||||
const openEtherscanUrl = (transactionHash: string): void => {
|
||||
const networkUrl =
|
||||
etherStore.networkName == NetworkEnum.ethereum
|
||||
? "goerli.etherscan.io"
|
||||
: "mumbai.polygonscan.com";
|
||||
const networkUrl = user.network.value.blockExplorers?.default.url;
|
||||
const url = `https://${networkUrl}/tx/${transactionHash}`;
|
||||
window.open(url, "_blank");
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const loadMore = (): void => {
|
||||
const itemsShowing = itemsToShow.value.length;
|
||||
itemsToShow.value?.push(
|
||||
...props.walletTransactions.slice(itemsShowing, itemsShowing + 3)
|
||||
...props.walletTransactions.slice(itemsShowing, itemsShowing + 3),
|
||||
);
|
||||
};
|
||||
|
||||
const getEventName = (event: string | undefined): string => {
|
||||
if (!event) return "Desconhecido";
|
||||
|
||||
const possibleEventName: { [key: string]: string } = {
|
||||
DepositAdded: "Oferta",
|
||||
LockAdded: "Reserva",
|
||||
LockReleased: "Compra",
|
||||
DepositWithdrawn: "Retirada",
|
||||
};
|
||||
|
||||
return possibleEventName[event];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useFloating(reference, floating, {
|
||||
placement: "right",
|
||||
middleware: [
|
||||
offset(10),
|
||||
flip(),
|
||||
shift(),
|
||||
arrow({ element: floatingArrow }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// watch props changes
|
||||
watch(props, async (): Promise<void> => {
|
||||
const itemsToShowQty = itemsToShow.value.length;
|
||||
@@ -161,181 +59,31 @@ showInitialItems();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="blur-container" v-if="loadingWalletTransactions">
|
||||
<div
|
||||
class="main-container max-w-md flex justify-center items-center min-h-[200px] w-16 h-16"
|
||||
v-if="loadingWalletTransactions"
|
||||
>
|
||||
Carregando ofertas...
|
||||
<SpinnerComponent width="8" height="8"></SpinnerComponent>
|
||||
</div>
|
||||
<div class="blur-container" v-if="!loadingWalletTransactions">
|
||||
<div
|
||||
class="w-full bg-white p-4 sm:p-6 rounded-lg"
|
||||
<div class="main-container max-w-md" v-else>
|
||||
<BalanceCard
|
||||
v-if="props.validDeposits.length > 0"
|
||||
>
|
||||
<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 só 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>
|
||||
:valid-deposits="props.validDeposits"
|
||||
:active-lock-amount="activeLockAmount"
|
||||
:selected-token="user.selectedToken.value"
|
||||
@withdraw="callWithdraw"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="withdraw-button flex gap-2 items-center justify-self-center border-2 p-2 border-amber-300 rounded-md"
|
||||
@click="callWithdraw"
|
||||
>
|
||||
<img
|
||||
alt="Withdraw image"
|
||||
src="@/assets/withdraw.svg"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
<span class="last-release-info">Sacar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full bg-white p-4 sm:p-6 rounded-lg"
|
||||
<TransactionCard
|
||||
v-for="item in itemsToShow"
|
||||
:key="item.blockNumber"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="bg-amber-300 status-text"
|
||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
|
||||
>
|
||||
Em Aberto
|
||||
</div>
|
||||
<div
|
||||
class="bg-[#94A3B8] status-text"
|
||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 2"
|
||||
>
|
||||
Expirado
|
||||
</div>
|
||||
<div
|
||||
class="bg-emerald-300 status-text"
|
||||
v-if="
|
||||
(getEventName(item.event) == 'Reserva' && item.lockStatus == 3) ||
|
||||
getEventName(item.event) != 'Reserva'
|
||||
"
|
||||
>
|
||||
Finalizado
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-2 cursor-pointer items-center justify-self-center w-full"
|
||||
@click="openEtherscanUrl(item.transactionHash)"
|
||||
v-if="getEventName(item.event) != 'Reserva' || item.lockStatus != 1"
|
||||
>
|
||||
<span class="last-release-info">{{ getExplorer() }}</span>
|
||||
<img
|
||||
alt="Redirect image"
|
||||
src="@/assets/redirect.svg"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-2 justify-self-center w-full"
|
||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'home',
|
||||
force: true,
|
||||
state: { lockID: item.transactionID },
|
||||
}"
|
||||
class="router-button"
|
||||
>Continuar</RouterLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:selected-token="user.selectedToken.value"
|
||||
:transaction="item"
|
||||
:network-name="user.network.value.name"
|
||||
@open-explorer="openEtherscanUrl"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col justify-center items-center w-full mt-2 gap-2"
|
||||
v-if="
|
||||
@@ -345,14 +93,14 @@ showInitialItems();
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-semibold"
|
||||
class="text-white font-semibold border-2 border-amber-300 rounded-lg px-4 py-2 hover:bg-amber-300/10 transition-colors cursor-pointer"
|
||||
@click="loadMore()"
|
||||
>
|
||||
Carregar mais
|
||||
</button>
|
||||
<span class="text-gray-300">
|
||||
({{ itemsToShow.length }} de {{ props.walletTransactions.length }}
|
||||
transações )
|
||||
<span class="text-gray-300 text-sm">
|
||||
{{ itemsToShow.length }} de {{ props.walletTransactions.length }}
|
||||
transações
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -363,66 +111,5 @@ showInitialItems();
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-gray-900;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
@apply text-xs sm:text-base font-medium text-gray-900 rounded-lg text-center mb-2 px-2 py-1 mt-4;
|
||||
}
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-4 py-3 sm:px-8 sm:py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md w-auto;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
@apply grid grid-cols-4 grid-flow-row items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10 w-auto;
|
||||
}
|
||||
|
||||
.last-release-info {
|
||||
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
|
||||
}
|
||||
|
||||
.router-button {
|
||||
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
|
||||
}
|
||||
|
||||
.withdraw-button {
|
||||
opacity: v-bind(withdrawButtonOpacity);
|
||||
cursor: v-bind(withdrawButtonCursor);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.md-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Minimal styles - most styles moved to child components */
|
||||
</style>
|
||||
|
||||
146
src/components/ListingComponent/TransactionCard.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { computed } from 'vue';
|
||||
import StatusBadge, { type StatusType } from '../ui/StatusBadge.vue';
|
||||
import { Networks } from '@/config/networks';
|
||||
|
||||
const props = defineProps<{
|
||||
transaction: WalletTransaction;
|
||||
networkName: keyof typeof Networks;
|
||||
selectedToken: TokenEnum;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
openExplorer: [transactionHash: string];
|
||||
}>();
|
||||
|
||||
const eventName = computed(() => {
|
||||
if (!props.transaction.event) return 'Desconhecido';
|
||||
|
||||
const possibleEventName: { [key: string]: string } = {
|
||||
DepositAdded: 'Oferta',
|
||||
LockAdded: 'Reserva',
|
||||
LockReleased: 'Compra',
|
||||
DepositWithdrawn: 'Retirada',
|
||||
};
|
||||
|
||||
return possibleEventName[props.transaction.event] || 'Desconhecido';
|
||||
});
|
||||
|
||||
const explorerName = computed(() => {
|
||||
return Networks[(props.networkName as string).toLowerCase()].blockExplorers
|
||||
?.default.name;
|
||||
});
|
||||
|
||||
const statusType = computed((): StatusType => {
|
||||
if (eventName.value === 'Reserva') {
|
||||
switch (props.transaction.lockStatus) {
|
||||
case 1:
|
||||
return 'open';
|
||||
case 2:
|
||||
return 'expired';
|
||||
case 3:
|
||||
return 'completed';
|
||||
default:
|
||||
return 'completed';
|
||||
}
|
||||
}
|
||||
return 'completed';
|
||||
});
|
||||
|
||||
const showExplorerLink = computed(() => {
|
||||
return eventName.value !== 'Reserva' || props.transaction.lockStatus !== 1;
|
||||
});
|
||||
|
||||
const showContinueButton = computed(() => {
|
||||
return eventName.value === 'Reserva' && props.transaction.lockStatus === 1;
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
if (!props.transaction.blockTimestamp) return '';
|
||||
|
||||
const timestamp = props.transaction.blockTimestamp;
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
});
|
||||
|
||||
const handleExplorerClick = () => {
|
||||
emit('openExplorer', props.transaction.transactionHash);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full bg-white p-4 sm:p-6 rounded-lg">
|
||||
<div class="item-container">
|
||||
<div class="flex flex-col self-start">
|
||||
<span class="text-xs sm:text-sm leading-5 font-medium text-gray-600">
|
||||
{{ eventName }}
|
||||
</span>
|
||||
<span class="text-xl sm:text-xl leading-7 font-semibold text-gray-900">
|
||||
{{ transaction.amount }} {{ selectedToken }}
|
||||
</span>
|
||||
<span
|
||||
v-if="formattedDate"
|
||||
class="text-xs sm:text-sm leading-5 font-normal text-gray-500 mt-1"
|
||||
>
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="mb-2 mt-4">
|
||||
<StatusBadge :status="statusType" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showExplorerLink"
|
||||
class="flex gap-2 cursor-pointer items-center justify-self-center w-full"
|
||||
@click="handleExplorerClick"
|
||||
>
|
||||
<span class="last-release-info">{{ explorerName }}</span>
|
||||
<img
|
||||
alt="Redirect image"
|
||||
src="@/assets/redirect.svg?url"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showContinueButton"
|
||||
class="flex gap-2 justify-self-center w-full"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'home',
|
||||
force: true,
|
||||
state: { lockID: transaction.transactionID },
|
||||
}"
|
||||
class="router-button"
|
||||
>
|
||||
Continuar
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.item-container {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
|
||||
.last-release-info {
|
||||
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
|
||||
}
|
||||
|
||||
.router-button {
|
||||
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { expect } from "vitest";
|
||||
import { MockValidDeposits } from "@/model/mock/ValidDepositMock";
|
||||
import { MockWalletTransactions } from "@/model/mock/WalletTransactionMock";
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
|
||||
describe("ListingComponent.vue", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
useEtherStore().setLoadingWalletTransactions(false);
|
||||
});
|
||||
|
||||
test("Test Message when an empty array is received", () => {
|
||||
const wrapper = mount(ListingComponent, {
|
||||
props: {
|
||||
validDeposits: [],
|
||||
walletTransactions: [],
|
||||
activeLockAmount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toContain("Não há nenhuma transação anterior");
|
||||
});
|
||||
|
||||
test("Test number of elements in the list first render", () => {
|
||||
const wrapper = mount(ListingComponent, {
|
||||
props: {
|
||||
validDeposits: [],
|
||||
walletTransactions: MockWalletTransactions,
|
||||
activeLockAmount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const elements = wrapper.findAll(".item-container");
|
||||
|
||||
expect(elements).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("Test load more button behavior", async () => {
|
||||
const wrapper = mount(ListingComponent, {
|
||||
props: {
|
||||
validDeposits: MockValidDeposits,
|
||||
walletTransactions: MockWalletTransactions,
|
||||
activeLockAmount: 0,
|
||||
},
|
||||
});
|
||||
const btn = wrapper.find("button");
|
||||
|
||||
let elements = wrapper.findAll(".item-container");
|
||||
expect(elements).toHaveLength(3);
|
||||
|
||||
await btn.trigger("click");
|
||||
|
||||
elements = wrapper.findAll(".item-container");
|
||||
|
||||
expect(elements).toHaveLength(5);
|
||||
});
|
||||
|
||||
test("Test withdraw offer button emit", async () => {
|
||||
const wrapper = mount(ListingComponent, {
|
||||
props: {
|
||||
validDeposits: MockValidDeposits,
|
||||
walletTransactions: MockWalletTransactions,
|
||||
activeLockAmount: 0,
|
||||
},
|
||||
});
|
||||
wrapper.vm.$emit("depositWithdrawn");
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted("depositWithdrawn")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Test should render lock info when active lock amount is greater than 0", () => {
|
||||
const wrapper = mount(ListingComponent, {
|
||||
props: {
|
||||
validDeposits: MockValidDeposits,
|
||||
walletTransactions: [],
|
||||
activeLockAmount: 50,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toContain("com 50.00 BRZ em lock");
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,254 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { pix } from "@/utils/QrCodePix";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
import CustomModal from "@/components//CustomModal/CustomModal.vue";
|
||||
import api from "@/services/index";
|
||||
|
||||
// props and store references
|
||||
const props = defineProps({
|
||||
pixTarget: String,
|
||||
tokenValue: Number,
|
||||
});
|
||||
|
||||
const windowSize = ref<number>(window.innerWidth);
|
||||
const qrCode = ref<string>("");
|
||||
const qrCodePayload = ref<string>("");
|
||||
const isPixValid = ref<boolean>(false);
|
||||
const isCodeInputEmpty = ref<boolean>(true);
|
||||
const showWarnModal = ref<boolean>(true);
|
||||
const e2eId = ref<string>("");
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["pixValidated"]);
|
||||
|
||||
const pixQrCode = pix({
|
||||
pixKey: props.pixTarget ?? "",
|
||||
value: props.tokenValue,
|
||||
});
|
||||
|
||||
pixQrCode.base64QrCode().then((code: string) => {
|
||||
qrCode.value = code;
|
||||
});
|
||||
|
||||
qrCodePayload.value = pixQrCode.payload();
|
||||
|
||||
const handleInputEvent = async (event: any): Promise<void> => {
|
||||
const { value } = event.target;
|
||||
e2eId.value = value;
|
||||
await validatePix();
|
||||
};
|
||||
|
||||
const validatePix = async (): Promise<void> => {
|
||||
if (e2eId.value == "") {
|
||||
isPixValid.value = false;
|
||||
isCodeInputEmpty.value = true;
|
||||
return;
|
||||
}
|
||||
const sellerPixKey = props.pixTarget;
|
||||
const transactionValue = props.tokenValue;
|
||||
|
||||
if (sellerPixKey && transactionValue) {
|
||||
const body_req = {
|
||||
e2e_id: e2eId.value,
|
||||
pix_key: sellerPixKey,
|
||||
pix_value: transactionValue,
|
||||
};
|
||||
|
||||
isCodeInputEmpty.value = false;
|
||||
|
||||
try {
|
||||
await api.post("validate_pix", body_req);
|
||||
isPixValid.value = true;
|
||||
} catch (error) {
|
||||
isPixValid.value = false;
|
||||
}
|
||||
} else {
|
||||
isCodeInputEmpty.value = false;
|
||||
isPixValid.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
() => (windowSize.value = window.innerWidth)
|
||||
);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener(
|
||||
"resize",
|
||||
() => (windowSize.value = window.innerWidth)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="text-container">
|
||||
<span
|
||||
class="text font-extrabold lg:text-2xl text-xl sm:max-w-[30rem] max-w-[24rem]"
|
||||
>
|
||||
Utilize o QR Code ou copie o código para realizar o Pix
|
||||
</span>
|
||||
<span class="text font-medium lg:text-md text-sm max-w-[28rem]">
|
||||
Após realizar o Pix no banco de sua preferência, insira o código de
|
||||
autenticação para enviar a transação para a rede.
|
||||
</span>
|
||||
</div>
|
||||
<div class="blur-container sm:max-w-[28rem] max-w-[20rem] text-black">
|
||||
<div
|
||||
class="flex-col items-center justify-center flex w-full bg-white sm:p-8 p-4 rounded-lg break-normal"
|
||||
>
|
||||
<img alt="Qr code image" :src="qrCode" class="w-48 h-48" />
|
||||
<span class="text-center font-bold">Código pix</span>
|
||||
<div class="break-words w-4/5">
|
||||
<span class="text-center text-xs">
|
||||
{{ qrCodePayload }}
|
||||
</span>
|
||||
</div>
|
||||
<img
|
||||
alt="Copy PIX code"
|
||||
src="@/assets/copyPix.svg"
|
||||
width="16"
|
||||
height="16"
|
||||
class="pt-2 lg:mb-5 cursor-pointer"
|
||||
/>
|
||||
<span class="text-xs text-start lg-view">
|
||||
<strong>ATENÇÃO!</strong> A transação só será processada após inserir
|
||||
o código de autenticação. Caso contrário não conseguiremos comprovar o
|
||||
seu depósito e não será possível transferir os tokens para sua
|
||||
carteira. Confira aqui como encontrar o código no comprovante.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex-col items-center justify-center flex w-full bg-white p-5 rounded-lg px-5"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Digite o código do comprovante PIX"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
class="sm:text-md text-sm w-full box-border p-2 sm:h-6 h-2 mb-2 outline-none"
|
||||
/>
|
||||
<div class="custom-divide" v-if="!isCodeInputEmpty"></div>
|
||||
<div
|
||||
class="flex flex-col w-full"
|
||||
v-if="!isPixValid && !isCodeInputEmpty"
|
||||
>
|
||||
<div class="flex items-center h-8">
|
||||
<img
|
||||
alt="Invalid Icon"
|
||||
src="@/assets/invalidIcon.svg"
|
||||
width="14"
|
||||
class="cursor-pointer align-middle inline-block"
|
||||
/>
|
||||
<span class="px-1 text-red-500 font-normal text-xs"
|
||||
>Código inválido. Por favor, confira e tente novamente.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full" v-else-if="isPixValid == true">
|
||||
<div class="flex items-center h-8">
|
||||
<img
|
||||
alt="Valid Icon"
|
||||
src="@/assets/validIcon.svg"
|
||||
width="14"
|
||||
class="cursor-pointer align-middle inline-block"
|
||||
/>
|
||||
<span class="px-1 text-green-500 font-normal text-sm">
|
||||
Código válido.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
:is-disabled="isPixValid == false"
|
||||
:text="'Enviar para a rede'"
|
||||
@button-clicked="emit('pixValidated', e2eId)"
|
||||
/>
|
||||
</div>
|
||||
<CustomModal
|
||||
v-if="showWarnModal && windowSize <= 500"
|
||||
@close-modal="showWarnModal = false"
|
||||
:isRedirectModal="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
/* Most modern browsers support this now. */
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #080808;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #080808;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply rounded-lg border border-gray-200 p-2 text-black;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply font-semibold tracking-wide text-emerald-50;
|
||||
}
|
||||
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-6;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.lg-view {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sm-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.lg-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sm-view {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,269 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { connectProvider } from "@/blockchain/provider";
|
||||
import { verifyNetworkLiquidity } from "@/utils/networkLiquidity";
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import SpinnerComponent from "./SpinnerComponent.vue";
|
||||
|
||||
// Store reference
|
||||
const etherStore = useEtherStore();
|
||||
|
||||
const {
|
||||
walletAddress,
|
||||
networkName,
|
||||
depositsValidListGoerli,
|
||||
depositsValidListMumbai,
|
||||
loadingNetworkLiquidity,
|
||||
} = storeToRefs(etherStore);
|
||||
|
||||
// Reactive state
|
||||
const tokenValue = ref<number>(0);
|
||||
const enableConfirmButton = ref<boolean>(false);
|
||||
const enableWalletButton = ref<boolean>(false);
|
||||
const hasLiquidity = ref<boolean>(true);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const selectedGoerliDeposit = ref<ValidDeposit>();
|
||||
const selectedMumbaiDeposit = ref<ValidDeposit>();
|
||||
const selectedRootstockDeposit = ref<ValidDeposit>();
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["tokenBuy"]);
|
||||
|
||||
// Blockchain methods
|
||||
const connectAccount = async (): Promise<void> => {
|
||||
await connectProvider();
|
||||
|
||||
enableOrDisableConfirmButton();
|
||||
};
|
||||
|
||||
const emitConfirmButton = (): void => {
|
||||
const selectedDeposit =
|
||||
networkName.value == NetworkEnum.ethereum
|
||||
? selectedGoerliDeposit.value
|
||||
: selectedMumbaiDeposit.value;
|
||||
emit("tokenBuy", selectedDeposit, tokenValue.value);
|
||||
};
|
||||
|
||||
// Debounce methods
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
tokenValue.value = Number(value);
|
||||
|
||||
if (decimalCount(String(tokenValue.value)) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
verifyLiquidity();
|
||||
};
|
||||
|
||||
// Verify if there is a valid deposit to buy
|
||||
const verifyLiquidity = (): void => {
|
||||
enableConfirmButton.value = false;
|
||||
selectedGoerliDeposit.value = undefined;
|
||||
selectedMumbaiDeposit.value = undefined;
|
||||
selectedRootstockDeposit.value = undefined;
|
||||
|
||||
if (tokenValue.value <= 0) {
|
||||
enableWalletButton.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedGoerliDeposit.value = verifyNetworkLiquidity(
|
||||
tokenValue.value,
|
||||
walletAddress.value,
|
||||
depositsValidListGoerli.value
|
||||
);
|
||||
selectedMumbaiDeposit.value = verifyNetworkLiquidity(
|
||||
tokenValue.value,
|
||||
walletAddress.value,
|
||||
depositsValidListMumbai.value
|
||||
);
|
||||
|
||||
enableOrDisableConfirmButton();
|
||||
if (selectedGoerliDeposit.value || selectedMumbaiDeposit.value) {
|
||||
hasLiquidity.value = true;
|
||||
enableWalletButton.value = true;
|
||||
} else {
|
||||
hasLiquidity.value = false;
|
||||
enableWalletButton.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const enableOrDisableConfirmButton = (): void => {
|
||||
if (selectedGoerliDeposit.value && networkName.value == NetworkEnum.ethereum)
|
||||
enableConfirmButton.value = true;
|
||||
else if (
|
||||
selectedMumbaiDeposit.value &&
|
||||
networkName.value == NetworkEnum.polygon
|
||||
)
|
||||
enableConfirmButton.value = true;
|
||||
else enableConfirmButton.value = false;
|
||||
};
|
||||
|
||||
watch(networkName, (): void => {
|
||||
verifyLiquidity();
|
||||
enableOrDisableConfirmButton();
|
||||
});
|
||||
|
||||
watch(walletAddress, (): void => {
|
||||
verifyLiquidity();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="text-container">
|
||||
<span
|
||||
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
|
||||
>
|
||||
Adquira cripto com apenas um Pix</span
|
||||
>
|
||||
<span class="text font-medium sm:text-base text-sm max-w-[28rem]"
|
||||
>Digite um valor, confira a oferta, conecte sua carteira e receba os
|
||||
tokens após realizar o Pix</span
|
||||
>
|
||||
</div>
|
||||
<div class="blur-container">
|
||||
<div class="backdrop-blur -z-10 w-full h-full"></div>
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex justify-between sm:w-full items-center">
|
||||
<input
|
||||
type="number"
|
||||
class="border-none outline-none text-lg text-gray-900 w-3/4"
|
||||
v-bind:class="{
|
||||
'font-semibold': tokenValue != undefined,
|
||||
'text-xl': tokenValue != undefined,
|
||||
}"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="0 "
|
||||
step=".01"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
|
||||
>
|
||||
<img
|
||||
alt="Token image"
|
||||
class="sm:w-fit w-4"
|
||||
src="@/assets/brz.svg"
|
||||
/>
|
||||
<span class="text-gray-900 sm:text-lg text-md w-fit" id="brz"
|
||||
>BRZ</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-divide py-2 mb-2"></div>
|
||||
<div
|
||||
class="flex justify-between"
|
||||
v-if="hasLiquidity && !loadingNetworkLiquidity"
|
||||
>
|
||||
<p class="text-gray-500 font-normal text-sm w-auto">
|
||||
~ R$ {{ tokenValue.toFixed(2) }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<img
|
||||
alt="Polygon image"
|
||||
src="@/assets/polygon.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
v-if="selectedMumbaiDeposit"
|
||||
/>
|
||||
<img
|
||||
alt="Ethereum image"
|
||||
src="@/assets/ethereum.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
v-if="selectedGoerliDeposit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center items-center"
|
||||
v-if="loadingNetworkLiquidity"
|
||||
>
|
||||
<span class="text-gray-900 font-normal text-sm mr-2"
|
||||
>Carregando liquidez das redes.</span
|
||||
>
|
||||
<SpinnerComponent width="4" height="4"></SpinnerComponent>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-if="!validDecimals && !loadingNetworkLiquidity"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize no máximo 2 casas decimais</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-else-if="!hasLiquidity && !loadingNetworkLiquidity"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Atualmente não há liquidez nas redes para sua demanda</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
v-if="!walletAddress"
|
||||
:text="'Conectar carteira'"
|
||||
:is-disabled="!enableWalletButton"
|
||||
@buttonClicked="connectAccount()"
|
||||
/>
|
||||
<CustomButton
|
||||
v-if="walletAddress"
|
||||
:text="'Confirmar compra'"
|
||||
:is-disabled="!enableConfirmButton"
|
||||
@buttonClicked="emitConfirmButton()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 mt-10;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
336
src/components/SellerSteps/SellerComponent.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import { postProcessKey } from '@/utils/pixKeyFormat';
|
||||
import { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { getTokenImage } from '@/utils/imagesPath';
|
||||
import { useOnboard } from '@web3-onboard/vue';
|
||||
import ChevronDown from '@/assets/chevron.svg';
|
||||
|
||||
// Import the bank list
|
||||
import bankList from '@/utils/files/isbpList.json';
|
||||
import type { Participant } from '@/utils/bbPay';
|
||||
|
||||
// Define Bank interface
|
||||
interface Bank {
|
||||
ISPB: string;
|
||||
longName: string;
|
||||
}
|
||||
|
||||
// html references
|
||||
const tokenDropdownRef = ref<any>(null);
|
||||
const formRef = ref<HTMLFormElement | null>(null);
|
||||
|
||||
// Reactive state
|
||||
const user = useUser();
|
||||
const { walletAddress, selectedToken } = user;
|
||||
|
||||
const offer = ref<string>('');
|
||||
const identification = ref<string>('');
|
||||
const account = ref<string>('');
|
||||
const branch = ref<string>('');
|
||||
const accountType = ref<string>('');
|
||||
const selectTokenToggle = ref<boolean>(false);
|
||||
const savingsVariation = ref<string>('');
|
||||
const errors = ref<{ [key: string]: string }>({});
|
||||
|
||||
// Bank selection
|
||||
const bankSearchQuery = ref<string>('');
|
||||
const showBankList = ref<boolean>(false);
|
||||
const selectedBank = ref<Bank | null>(null);
|
||||
|
||||
const filteredBanks = computed(() => {
|
||||
if (!bankSearchQuery.value) return [];
|
||||
return bankList
|
||||
.filter((bank) =>
|
||||
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase()),
|
||||
)
|
||||
.slice(0, 5);
|
||||
});
|
||||
|
||||
const handleBankSelect = (bank: Bank) => {
|
||||
selectedBank.value = bank;
|
||||
bankSearchQuery.value = bank.longName;
|
||||
showBankList.value = false;
|
||||
};
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['approveTokens']);
|
||||
|
||||
// Methods
|
||||
const connectAccount = async (): Promise<void> => {
|
||||
const { connectWallet } = useOnboard();
|
||||
await connectWallet();
|
||||
};
|
||||
|
||||
const handleSubmit = (e: Event): void => {
|
||||
e.preventDefault();
|
||||
|
||||
const processedIdentification = postProcessKey(identification.value);
|
||||
|
||||
const data: Participant = {
|
||||
offer: offer.value,
|
||||
chainID: user.network.value.id,
|
||||
identification: processedIdentification,
|
||||
bankIspb: selectedBank.value?.ISPB,
|
||||
accountType: accountType.value,
|
||||
account: account.value,
|
||||
branch: branch.value,
|
||||
savingsVariation: savingsVariation.value || '',
|
||||
};
|
||||
|
||||
emit('approveTokens', data);
|
||||
};
|
||||
|
||||
// Token selection
|
||||
const openTokenSelection = (): void => {
|
||||
selectTokenToggle.value = true;
|
||||
};
|
||||
|
||||
const handleSelectedToken = (token: TokenEnum): void => {
|
||||
user.setSelectedToken(token);
|
||||
selectTokenToggle.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page w-full">
|
||||
<div class="text-container">
|
||||
<span
|
||||
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
|
||||
>
|
||||
Venda cripto e receba em Pix
|
||||
</span>
|
||||
<span
|
||||
class="text font-medium sm:text-base text-xs sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide"
|
||||
>
|
||||
Digite sua oferta, informe a chave Pix, selecione a rede, aprove o envio
|
||||
da transação e confirme sua oferta.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form ref="formRef" @submit="handleSubmit" class="main-container">
|
||||
<!-- Offer input -->
|
||||
<div
|
||||
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg gap-4"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
v-model="offer"
|
||||
class="border-none outline-none text-gray-900 sm:w-fit w-3/4 flex-grow"
|
||||
:class="{
|
||||
'!font-medium': offer !== undefined && offer !== '',
|
||||
'text-xl': offer !== undefined && offer !== '',
|
||||
}"
|
||||
min="0.01"
|
||||
max="999999999.99"
|
||||
pattern="\d+(\.\d{0,2})?"
|
||||
placeholder="Digite sua oferta (mínimo R$0,01)"
|
||||
step=".01"
|
||||
required
|
||||
/>
|
||||
<div class="relative overflow-visible">
|
||||
<button
|
||||
ref="tokenDropdownRef"
|
||||
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
|
||||
@click="openTokenSelection()"
|
||||
>
|
||||
<img
|
||||
alt="Token image"
|
||||
class="sm:w-fit w-4"
|
||||
:src="getTokenImage(selectedToken)"
|
||||
/>
|
||||
<span
|
||||
class="text-gray-900 sm:text-lg text-md font-medium"
|
||||
id="token"
|
||||
>
|
||||
{{ selectedToken }}
|
||||
</span>
|
||||
<ChevronDown
|
||||
class="text-gray-900 pr-4 sm:pr-0 transition-all duration-500 ease-in-out"
|
||||
:class="{ 'scale-y-[-1]': selectTokenToggle }"
|
||||
alt="Chevron Down"
|
||||
/>
|
||||
</button>
|
||||
<transition name="dropdown">
|
||||
<div
|
||||
v-if="selectTokenToggle"
|
||||
class="mt-2 text-gray-900 absolute right-0 z-50 w-full min-w-max"
|
||||
>
|
||||
<div
|
||||
class="bg-white rounded-xl z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
||||
>
|
||||
<div
|
||||
v-for="token in TokenEnum"
|
||||
:key="token"
|
||||
class="flex menu-button gap-2 px-4 cursor-pointer hover:bg-gray-300 transition-colors"
|
||||
@click="handleSelectedToken(token)"
|
||||
>
|
||||
<img
|
||||
:alt="token + ' logo'"
|
||||
width="20"
|
||||
height="20"
|
||||
:src="getTokenImage(token)"
|
||||
/>
|
||||
<span
|
||||
class="text-gray-900 py-4 text-end font-semibold text-sm"
|
||||
>
|
||||
{{ token }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- CPF or CNPJ input -->
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<input
|
||||
type="text"
|
||||
v-model="identification"
|
||||
maxlength="14"
|
||||
:pattern="'^\\d{11}$|^\\d{14}$'"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
||||
:class="{ 'text-xl font-medium': identification }"
|
||||
placeholder="Digite seu CPF ou CNPJ (somente números)"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<!-- Bank selection -->
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
v-model="bankSearchQuery"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
||||
:class="{ 'text-xl font-medium': bankSearchQuery }"
|
||||
placeholder="Buscar banco"
|
||||
@focus="showBankList = true"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
v-if="showBankList && filteredBanks.length > 0"
|
||||
class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-50 max-h-60 overflow-y-auto"
|
||||
>
|
||||
<div
|
||||
v-for="bank in filteredBanks"
|
||||
:key="bank.ISPB"
|
||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer transition-colors"
|
||||
@click="handleBankSelect(bank)"
|
||||
>
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ bank.longName }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">ISPB: {{ bank.ISPB }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="errors.bank" class="text-red-500 text-sm mt-2">{{
|
||||
errors.bank
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Account and Branch inputs -->
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
v-model="account"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
||||
:class="{ 'text-xl font-medium': account }"
|
||||
placeholder="Número da conta"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
v-model="branch"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
||||
:class="{ 'text-xl font-medium': branch }"
|
||||
placeholder="Agência"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Account Type Selection -->
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
v-model="accountType"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
||||
required
|
||||
>
|
||||
<option value="" disabled selected>Tipo de conta</option>
|
||||
<option value="1">Conta Corrente</option>
|
||||
<option value="2">Conta Poupança</option>
|
||||
<option value="3">Conta Salário</option>
|
||||
<option value="4">Conta Pré-Paga</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Savings Account Variation -->
|
||||
<Transition name="resize">
|
||||
<input
|
||||
v-if="accountType === '2'"
|
||||
type="text"
|
||||
v-model="savingsVariation"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full bg-white sm:px-10 px-6 py-4 rounded-lg"
|
||||
:class="{ 'text-xl font-medium': savingsVariation }"
|
||||
placeholder="Variação da poupança"
|
||||
required
|
||||
/>
|
||||
</Transition>
|
||||
<!-- Action buttons -->
|
||||
<CustomButton v-if="walletAddress" type="submit" text="Aprovar tokens" />
|
||||
<CustomButton
|
||||
v-else
|
||||
text="Conectar carteira"
|
||||
@buttonClicked="connectAccount()"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply sm:text-lg text-sm;
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
|
||||
// Reactive state
|
||||
const tokenValue = ref<number>(0);
|
||||
const enableSelectButton = ref<boolean>(false);
|
||||
const hasLiquidity = ref<boolean>(true);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["tokenBuy"]);
|
||||
|
||||
// Debounce methods
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
tokenValue.value = Number(value);
|
||||
|
||||
if (decimalCount(String(tokenValue.value)) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableSelectButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
};
|
||||
</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-medium text-base 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="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex justify-between w-full items-center">
|
||||
<input
|
||||
type="number"
|
||||
class="border-none outline-none text-lg text-gray-900 w-fit"
|
||||
v-bind:class="{
|
||||
'font-semibold': tokenValue != undefined,
|
||||
'text-xl': tokenValue != undefined,
|
||||
}"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="0 "
|
||||
step=".01"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
|
||||
>
|
||||
<img alt="Token image" class="w-fit" src="@/assets/brz.svg" />
|
||||
<span class="text-gray-900 text-lg 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">
|
||||
<p class="text-gray-500 font-normal text-sm w-auto">
|
||||
~ R$ {{ tokenValue.toFixed(2) }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<img
|
||||
alt="Polygon image"
|
||||
src="@/assets/polygon.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
<img
|
||||
alt="Ethereum image"
|
||||
src="@/assets/ethereum.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex pt-2 justify-center" v-if="!validDecimals">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize no máximo 2 casas decimais</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex pt-2 justify-center" v-else-if="!hasLiquidity">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Atualmente não há liquidez nas redes para sua demanda</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
:text="'Conectar carteira'"
|
||||
@buttonClicked="emit('tokenBuy')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["sendNetwork"]);
|
||||
const emit = defineEmits(['sendNetwork']);
|
||||
|
||||
// props and store references
|
||||
const props = defineProps({
|
||||
pixKey: String,
|
||||
sellerId: String,
|
||||
offer: Number,
|
||||
selectedToken: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,24 +20,24 @@ const props = defineProps({
|
||||
>Envie sua oferta para a rede
|
||||
</span>
|
||||
<span
|
||||
class="text text-xl font-normal sm:text-base text-xs sm:max-w-[30rem] max-w-[22rem]"
|
||||
class="text 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
|
||||
>
|
||||
</div>
|
||||
<div class="blur-container">
|
||||
<div
|
||||
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="main-container">
|
||||
<div class="flex flex-col w-full bg-white px-10 py-5 rounded-lg">
|
||||
<div>
|
||||
<p>Tokens ofertados</p>
|
||||
<p class="text-2xl text-gray-900">{{ props.offer }} BRZ</p>
|
||||
<p class="text-2xl text-gray-900">
|
||||
{{ props.offer }} {{ props.selectedToken }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<p>Chave Pix</p>
|
||||
<p class="text-xl text-gray-900 break-words">
|
||||
{{ props.pixKey }}
|
||||
{{ props.sellerId }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
@@ -56,6 +57,7 @@ const props = defineProps({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
@@ -72,20 +74,12 @@ p {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-8 sm:w-1/3;
|
||||
}
|
||||
|
||||
.last-deposit-info {
|
||||
@apply font-medium text-base;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import CustomButton from "../CustomButton/CustomButton.vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import { pixFormatValidation, postProcessKey } from "@/utils/pixKeyFormat";
|
||||
import { useEtherStore } from "@/store/ether";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { connectProvider } from "@/blockchain/provider";
|
||||
|
||||
// Reactive state
|
||||
const etherStore = useEtherStore();
|
||||
const { walletAddress } = storeToRefs(etherStore);
|
||||
|
||||
const offer = ref<string>("");
|
||||
const pixKey = ref<string>("");
|
||||
|
||||
const enableSelectButton = ref<boolean>(false);
|
||||
const hasLiquidity = ref<boolean>(true);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const validPixFormat = ref<boolean>(true);
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["approveTokens"]);
|
||||
|
||||
// Debounce methods
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
offer.value = value;
|
||||
|
||||
if (decimalCount(offer.value) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableSelectButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
};
|
||||
|
||||
const handlePixKeyInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
pixKey.value = value;
|
||||
|
||||
if (pixFormatValidation(pixKey.value)) {
|
||||
validPixFormat.value = true;
|
||||
enableSelectButton.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
enableSelectButton.value = false;
|
||||
validPixFormat.value = false;
|
||||
};
|
||||
|
||||
const handleButtonClick = async (
|
||||
offer: string,
|
||||
pixKey: string
|
||||
): Promise<void> => {
|
||||
const postProcessedPixKey = postProcessKey(pixKey);
|
||||
if (walletAddress.value)
|
||||
emit("approveTokens", { offer, postProcessedPixKey });
|
||||
else await connectProvider();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page w-full">
|
||||
<div class="text-container">
|
||||
<span
|
||||
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
|
||||
>Venda cripto e receba em Pix</span
|
||||
>
|
||||
<span
|
||||
class="text font-medium sm:text-base text-xs sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide"
|
||||
>Digite sua oferta, informe a chave Pix, selecione a rede, aprove o
|
||||
envio da transação e confirme sua oferta.</span
|
||||
>
|
||||
</div>
|
||||
<div class="blur-container">
|
||||
<div class="backdrop-blur -z-10 w-full h-full"></div>
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<input
|
||||
type="number"
|
||||
v-model="offer"
|
||||
class="border-none outline-none text-gray-900 sm:w-fit w-3/4"
|
||||
v-bind:class="{
|
||||
'font-semibold': offer != undefined,
|
||||
'text-xl': offer != undefined,
|
||||
}"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="Digite sua oferta"
|
||||
step=".01"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-row p-2 px-3 bg-gray-300 rounded-3xl min-w-fit gap-1"
|
||||
>
|
||||
<img
|
||||
alt="Token image"
|
||||
class="sm:w-fit w-4"
|
||||
src="@/assets/brz.svg"
|
||||
/>
|
||||
<span class="text-gray-900 w-fit" id="brz"> BRZ </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex pt-2 justify-center" v-if="!validDecimals">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize no máximo 2 casas decimais</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex pt-2 justify-center" v-else-if="!hasLiquidity">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Atualmente não há liquidez nas redes para sua demanda</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-8 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex justify-between w-full items-center">
|
||||
<input
|
||||
@input="debounce(handlePixKeyInputEvent, 500)($event)"
|
||||
type="text"
|
||||
v-model="pixKey"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-fit"
|
||||
placeholder="Digite a chave Pix"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex pt-2 justify-center" v-if="!validPixFormat">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize telefone, CPF ou CNPJ</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
:text="walletAddress ? 'Aprovar tokens' : 'Conectar Carteira'"
|
||||
:isDisabled="!validDecimals || !validPixFormat"
|
||||
@buttonClicked="handleButtonClick(offer, pixKey)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center px-8 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 mt-10 w-auto;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply sm:text-lg text-sm;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import TopBar from "../TopBar.vue";
|
||||
import { useEtherStore } from "../../../store/ether";
|
||||
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
describe("TopBar.vue", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("should render connect wallet button", () => {
|
||||
const wrapper = shallowMount(TopBar);
|
||||
expect(wrapper.html()).toContain("Conectar carteira");
|
||||
});
|
||||
|
||||
it("should render button to change to seller view when in buyer screen", () => {
|
||||
const wrapper = shallowMount(TopBar);
|
||||
expect(wrapper.html()).toContain("Quero vender");
|
||||
});
|
||||
|
||||
it("should render button to change to seller view when in buyer screen", () => {
|
||||
const etherStore = useEtherStore();
|
||||
etherStore.setSellerView(true);
|
||||
const wrapper = shallowMount(TopBar);
|
||||
expect(wrapper.html()).toContain("Quero comprar");
|
||||
});
|
||||
|
||||
it("should render the P2Pix logo correctly", () => {
|
||||
const wrapper = shallowMount(TopBar);
|
||||
const img = wrapper.findAll(".logo");
|
||||
expect(img.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref } from 'vue';
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const alertText = ref<string>("");
|
||||
const alertPaddingLeft = ref<string>("18rem");
|
||||
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%";
|
||||
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":
|
||||
case 'buy':
|
||||
alertText.value =
|
||||
"Tudo certo! Os tokens já foram retirados da oferta e estão disponíveis na sua carteira.";
|
||||
'Tudo certo! Os tokens já foram retirados da oferta e estão disponíveis na sua carteira.';
|
||||
break;
|
||||
case "sell":
|
||||
case 'sell':
|
||||
alertText.value =
|
||||
"Tudo certo! Os tokens já foram reservados e sua oferta está disponível.";
|
||||
'Tudo certo! Os tokens já foram reservados e sua oferta está disponível.';
|
||||
break;
|
||||
case "redirect":
|
||||
alertText.value = "Existe uma compra em aberto. Continuar?";
|
||||
case 'redirect':
|
||||
alertText.value = 'Existe uma compra em aberto. Continuar?';
|
||||
break;
|
||||
case "withdraw":
|
||||
alertText.value = "Tudo certo! Saque realizado com sucesso!";
|
||||
case 'withdraw':
|
||||
alertText.value = 'Tudo certo! Saque realizado com sucesso!';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
@@ -38,9 +38,9 @@ switch (props.type) {
|
||||
<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="modal px-12 text-center sm:flex justify-between hidden">
|
||||
<div class="flex items-center">
|
||||
<p class="text-black tracking-tighter leading-tight my-2">
|
||||
<p class="text-black tracking-tighter leading-tight py-2">
|
||||
{{ alertText }}
|
||||
</p>
|
||||
<button v-if="props.type === 'redirect'" @click="$emit('go-to-lock')">
|
||||
@@ -110,7 +110,6 @@ switch (props.type) {
|
||||
border-radius: 10px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding-left: v-bind(alertPaddingLeft);
|
||||
}
|
||||
|
||||
.modal-mobile {
|
||||
162
src/components/ui/CustomButton.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
text: string;
|
||||
isDisabled?: boolean;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
icon?: string;
|
||||
iconPosition?: 'left' | 'right';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
isDisabled: false,
|
||||
variant: 'primary',
|
||||
size: 'xl',
|
||||
iconPosition: 'left',
|
||||
fullWidth: true,
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['buttonClicked']);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.isDisabled && !props.loading) {
|
||||
emit('buttonClicked');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'button',
|
||||
`variant-${variant}`,
|
||||
`size-${size}`,
|
||||
{ 'is-disabled': isDisabled || loading, 'full-width': fullWidth },
|
||||
]"
|
||||
:disabled="isDisabled || loading"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span v-if="loading" class="loader"></span>
|
||||
<template v-else>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'left'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
<span class="button-text">{{ text }}</span>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'right'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.button {
|
||||
@apply rounded-lg font-semibold transition-all duration-200 cursor-pointer flex items-center justify-center gap-2;
|
||||
}
|
||||
|
||||
.button:hover:not(.is-disabled) {
|
||||
@apply transform scale-[1.02];
|
||||
}
|
||||
|
||||
.button.is-disabled {
|
||||
@apply opacity-70 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.button.full-width {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* Variantes */
|
||||
.variant-primary {
|
||||
@apply bg-amber-400 text-gray-900 border-2 border-amber-400;
|
||||
}
|
||||
|
||||
.variant-primary:hover:not(.is-disabled) {
|
||||
@apply bg-amber-500 border-amber-500;
|
||||
}
|
||||
|
||||
.variant-secondary {
|
||||
@apply bg-gray-200 text-gray-900 border-2 border-gray-300;
|
||||
}
|
||||
|
||||
.variant-secondary:hover:not(.is-disabled) {
|
||||
@apply bg-gray-300 border-gray-400;
|
||||
}
|
||||
|
||||
.variant-outline {
|
||||
@apply bg-transparent text-gray-900 border-2 border-amber-400;
|
||||
}
|
||||
|
||||
.variant-outline:hover:not(.is-disabled) {
|
||||
@apply bg-amber-400/10;
|
||||
}
|
||||
|
||||
.variant-ghost {
|
||||
@apply bg-transparent text-gray-900 border-2 border-transparent;
|
||||
}
|
||||
|
||||
.variant-ghost:hover:not(.is-disabled) {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Tamanhos */
|
||||
.size-sm {
|
||||
@apply px-2 py-1 text-xs;
|
||||
}
|
||||
|
||||
.size-sm .button-icon {
|
||||
@apply w-3 h-3;
|
||||
}
|
||||
|
||||
.size-md {
|
||||
@apply px-3 py-2 text-sm;
|
||||
}
|
||||
|
||||
.size-md .button-icon {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
.size-lg {
|
||||
@apply px-4 py-3 text-base;
|
||||
}
|
||||
|
||||
.size-lg .button-icon {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
.size-xl {
|
||||
@apply p-4 text-base;
|
||||
}
|
||||
|
||||
.size-xl .button-icon {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
/* Loader animation */
|
||||
.loader {
|
||||
@apply w-5 h-5 border-2 border-gray-900 border-t-transparent rounded-full animate-spin;
|
||||
}
|
||||
</style>
|
||||
115
src/components/ui/CustomModal.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isRedirectModal: Boolean,
|
||||
});
|
||||
|
||||
const modalColor = ref<string>('white');
|
||||
const modalHeight = ref<string>('250px');
|
||||
const pFontSize = ref<string>('16px');
|
||||
|
||||
if (props.isRedirectModal) {
|
||||
modalColor.value = 'rgba(251, 191, 36, 1)';
|
||||
modalHeight.value = '150px';
|
||||
pFontSize.value = '20px';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="modal-overlay inset-0 fixed hidden md:block justify-center backdrop-blur-sm sm:backdrop-blur-none"
|
||||
v-if="!isRedirectModal"
|
||||
>
|
||||
<div class="modal px-5 text-center">
|
||||
<p
|
||||
class="text-black tracking-tighter leading-tight my-6 mx-2 text-justify"
|
||||
>
|
||||
<strong>ATENÇÃO!</strong>
|
||||
A transação só será processada após efetuado o pagamento do Pix. Caso
|
||||
contrário não conseguiremos comprovar o seu depósito e não será
|
||||
possível transferir os tokens para sua carteira.
|
||||
</p>
|
||||
<button
|
||||
@click="$emit('close-modal')"
|
||||
class="border-2 border-solid border-amber-400 mt-2"
|
||||
>
|
||||
Entendi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="modal-overlay inset-0 fixed justify-center backdrop-blur-sm"
|
||||
v-if="isRedirectModal"
|
||||
>
|
||||
<div class="modal px-5 text-center">
|
||||
<p
|
||||
class="text-black text-lg tracking-tighter leading-tight my-6 mx-2 text-justify font-semibold"
|
||||
>
|
||||
Retomar a última compra?
|
||||
</p>
|
||||
<div class="flex justify-around items-center px-2">
|
||||
<button
|
||||
@click="$emit('close-modal')"
|
||||
class="border-2 border-solid border-white-400 mt-2 font-semibold"
|
||||
>
|
||||
Não
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('go-to-lock')"
|
||||
class="border-2 border-solid border-white-400 mt-2 font-semibold"
|
||||
>
|
||||
Sim
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: v-bind(modalColor);
|
||||
height: v-bind(modalHeight);
|
||||
width: 300px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-img {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.check {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-weight: 500;
|
||||
font-size: 28px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: v-bind(pFontSize);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
color: black;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
55
src/components/ui/FormCard.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
type FormCardPadding = 'sm' | 'md' | 'lg';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
padding?: FormCardPadding;
|
||||
fullWidth?: boolean;
|
||||
noBorder?: boolean;
|
||||
}>(),
|
||||
{
|
||||
padding: 'md',
|
||||
fullWidth: true,
|
||||
noBorder: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'form-card',
|
||||
`padding-${padding}`,
|
||||
{ 'full-width': fullWidth, 'no-border': noBorder },
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.form-card {
|
||||
@apply flex flex-col bg-white rounded-lg;
|
||||
}
|
||||
|
||||
.form-card:not(.no-border) {
|
||||
@apply border-y;
|
||||
}
|
||||
|
||||
.form-card.full-width {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.padding-sm {
|
||||
@apply px-4 py-3;
|
||||
}
|
||||
|
||||
.padding-md {
|
||||
@apply sm:px-10 px-6 py-5;
|
||||
}
|
||||
|
||||
.padding-lg {
|
||||
@apply px-12 py-8;
|
||||
}
|
||||
</style>
|
||||
147
src/components/ui/IconButton.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
type IconButtonSize = 'sm' | 'md' | 'lg';
|
||||
type IconPosition = 'left' | 'right';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
text: string;
|
||||
icon?: string;
|
||||
variant?: IconButtonVariant;
|
||||
size?: IconButtonSize;
|
||||
iconPosition?: IconPosition;
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'md',
|
||||
iconPosition: 'left',
|
||||
disabled: false,
|
||||
fullWidth: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled) {
|
||||
emit('click');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'icon-button',
|
||||
`variant-${variant}`,
|
||||
`size-${size}`,
|
||||
{ 'is-disabled': disabled, 'full-width': fullWidth },
|
||||
]"
|
||||
:disabled="disabled"
|
||||
@click="handleClick"
|
||||
>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'left'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
<span class="button-text">{{ text }}</span>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'right'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.icon-button {
|
||||
@apply flex items-center justify-center gap-2 font-medium rounded-lg transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.icon-button:hover:not(.is-disabled) {
|
||||
@apply transform scale-[1.02];
|
||||
}
|
||||
|
||||
.icon-button.is-disabled {
|
||||
@apply opacity-60 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.icon-button.full-width {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* Variantes */
|
||||
.variant-primary {
|
||||
@apply bg-amber-400 text-gray-900 border-2 border-amber-400;
|
||||
}
|
||||
|
||||
.variant-primary:hover:not(.is-disabled) {
|
||||
@apply bg-amber-500 border-amber-500;
|
||||
}
|
||||
|
||||
.variant-secondary {
|
||||
@apply bg-gray-200 text-gray-900 border-2 border-gray-300;
|
||||
}
|
||||
|
||||
.variant-secondary:hover:not(.is-disabled) {
|
||||
@apply bg-gray-300 border-gray-400;
|
||||
}
|
||||
|
||||
.variant-outline {
|
||||
@apply bg-transparent text-gray-900 border-2 border-amber-300;
|
||||
}
|
||||
|
||||
.variant-outline:hover:not(.is-disabled) {
|
||||
@apply bg-amber-300/10;
|
||||
}
|
||||
|
||||
.variant-ghost {
|
||||
@apply bg-transparent text-gray-900 border-2 border-transparent;
|
||||
}
|
||||
|
||||
.variant-ghost:hover:not(.is-disabled) {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Tamanhos */
|
||||
.size-sm {
|
||||
@apply px-2 py-1 text-xs;
|
||||
}
|
||||
|
||||
.size-sm .button-icon {
|
||||
@apply w-3 h-3;
|
||||
}
|
||||
|
||||
.size-md {
|
||||
@apply px-3 py-2 text-sm;
|
||||
}
|
||||
|
||||
.size-md .button-icon {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
.size-lg {
|
||||
@apply px-4 py-3 text-base;
|
||||
}
|
||||
|
||||
.size-lg .button-icon {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
</style>
|
||||
@@ -12,19 +12,17 @@ const props = defineProps({
|
||||
<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" }}
|
||||
{{ props.title ? props.title : 'Confirme em sua carteira' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="blur-container sm:w-[26rem] w-[20rem]">
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="main-container max-w-md">
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg">
|
||||
<div
|
||||
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"
|
||||
src="@/assets/validating.svg"
|
||||
src="@/assets/validating.svg?url"
|
||||
width="96"
|
||||
height="48"
|
||||
/>
|
||||
@@ -38,6 +36,7 @@ const props = defineProps({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
@@ -60,16 +59,12 @@ const props = defineProps({
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.blur-container {
|
||||
@apply flex flex-col justify-center items-center sm:px-8 px-6 py-6 gap-2 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
@@ -10,10 +10,10 @@ const getCustomClass = () => {
|
||||
return [
|
||||
`w-${props.width}`,
|
||||
`h-${props.height}`,
|
||||
`fill-white`,
|
||||
"text-gray-200",
|
||||
"animate-spin",
|
||||
"dark:text-gray-600",
|
||||
'fill-white',
|
||||
'text-gray-200',
|
||||
'animate-spin',
|
||||
'dark:text-gray-600',
|
||||
];
|
||||
};
|
||||
</script>
|
||||
50
src/components/ui/StatusBadge.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
export type StatusType = 'open' | 'expired' | 'completed' | 'pending';
|
||||
|
||||
const props = defineProps<{
|
||||
status: StatusType;
|
||||
customText?: string;
|
||||
}>();
|
||||
|
||||
const statusConfig = computed(() => {
|
||||
const configs: Record<StatusType, { text: string; color: string }> = {
|
||||
open: {
|
||||
text: 'Em Aberto',
|
||||
color: 'bg-amber-300',
|
||||
},
|
||||
expired: {
|
||||
text: 'Expirado',
|
||||
color: 'bg-[#94A3B8]',
|
||||
},
|
||||
completed: {
|
||||
text: 'Finalizado',
|
||||
color: 'bg-emerald-300',
|
||||
},
|
||||
pending: {
|
||||
text: 'Pendente',
|
||||
color: 'bg-gray-300',
|
||||
},
|
||||
};
|
||||
|
||||
return configs[props.status];
|
||||
});
|
||||
|
||||
const displayText = computed(() => {
|
||||
return props.customText || statusConfig.value.text;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[statusConfig.color, 'status-badge']">
|
||||
{{ displayText }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.status-badge {
|
||||
@apply text-xs sm:text-base font-medium text-gray-900 rounded-lg text-center px-2 py-1;
|
||||
}
|
||||
</style>
|
||||
85
src/components/ui/ToasterComponent.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { useOnboard } from '@web3-onboard/vue';
|
||||
import { Networks } from '@/config/networks';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
|
||||
const { connectedWallet } = useOnboard();
|
||||
const user = useUser();
|
||||
const { network } = user;
|
||||
|
||||
const isWrongNetwork = ref(false);
|
||||
const targetNetworkName = computed(() => network.value.name);
|
||||
|
||||
const checkNetwork = () => {
|
||||
if (connectedWallet.value) {
|
||||
const chainId = connectedWallet.value.chains[0].id;
|
||||
isWrongNetwork.value = Number(chainId) !== network.value.id;
|
||||
} else {
|
||||
isWrongNetwork.value = false; // No wallet connected yet
|
||||
}
|
||||
};
|
||||
|
||||
const switchNetwork = async () => {
|
||||
try {
|
||||
if (connectedWallet.value && connectedWallet.value.provider) {
|
||||
const chainId = network.value.id.toString(16);
|
||||
await connectedWallet.value.provider.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [
|
||||
{
|
||||
chainId: `0x${chainId}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to switch network:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(checkNetwork);
|
||||
watch(connectedWallet, checkNetwork);
|
||||
watch(network, checkNetwork, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="slide-up" appear>
|
||||
<div
|
||||
v-if="isWrongNetwork"
|
||||
class="fixed bottom-0 left-0 right-0 bg-red-500 text-white p-4 flex justify-between items-center z-50"
|
||||
>
|
||||
<div>
|
||||
<span class="font-bold">Wrong network!</span>
|
||||
<span> Please switch to {{ targetNetworkName }}.</span>
|
||||
</div>
|
||||
<button
|
||||
@click="switchNetwork"
|
||||
class="bg-white text-red-500 px-4 py-2 rounded font-bold hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Switch Network
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-up-enter-to,
|
||||
.slide-up-leave-from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
14
src/components/ui/VersionFooter.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
const version =
|
||||
typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'dev';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fixed bottom-2 left-2 text-xs font-mono text-gray-50/40 hover:text-gray-50/80 transition-opacity pointer-events-none select-none z-10"
|
||||
:title="`P2Pix ${version}`"
|
||||
aria-label="Application version"
|
||||
>
|
||||
v{{ version }}
|
||||
</div>
|
||||
</template>
|
||||
468
src/composables/useGraphQL.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
import { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import { sepolia, rootstock, rootstockTestnet } from 'viem/chains';
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
type: 'deposit' | 'lock' | 'release' | 'return';
|
||||
timestamp: string;
|
||||
blockTimestamp: string;
|
||||
seller?: string;
|
||||
buyer?: string | null;
|
||||
amount: string;
|
||||
token: string;
|
||||
blockNumber: string;
|
||||
transactionHash: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsData {
|
||||
totalVolume: string;
|
||||
totalTransactions: string;
|
||||
totalLocks: string;
|
||||
totalDeposits: string;
|
||||
totalReleases: string;
|
||||
}
|
||||
|
||||
export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
const searchAddress = ref('');
|
||||
const selectedType = ref('all');
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const analyticsLoading = ref(false);
|
||||
|
||||
const transactionsData = ref<Transaction[]>([]);
|
||||
const analyticsData = ref<AnalyticsData>({
|
||||
totalVolume: '0',
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0',
|
||||
});
|
||||
|
||||
const executeQuery = async (query: string, variables: any = {}) => {
|
||||
const url = network.value.subgraphUrls[0]; // TODO: try all available URLs
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(data.errors[0]?.message || 'GraphQL error');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (err) {
|
||||
console.error('GraphQL query error:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllActivity = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const query = `
|
||||
query GetAllActivity($first: Int = 50) {
|
||||
depositAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
depositWithdrawns(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReleaseds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReturneds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await executeQuery(query, { first: 50 });
|
||||
transactionsData.value = processActivityData(data);
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error ? err.message : 'Failed to fetch transactions';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserActivity = async (userAddress: string) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const query = `
|
||||
query GetUserActivity($userAddress: String!, $first: Int = 50) {
|
||||
depositAddeds(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
depositWithdrawns(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockAddeds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReleaseds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReturneds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await executeQuery(query, { userAddress, first: 50 });
|
||||
transactionsData.value = processActivityData(data);
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to fetch user transactions';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearData = () => {
|
||||
transactionsData.value = [];
|
||||
analyticsData.value = {
|
||||
totalVolume: '0',
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0',
|
||||
};
|
||||
};
|
||||
|
||||
const fetchAnalytics = async () => {
|
||||
analyticsLoading.value = true;
|
||||
|
||||
const query = `
|
||||
query GetAnalytics {
|
||||
depositAddeds(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
depositWithdrawns(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
lockAddeds(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
lockReleaseds(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
lockReturneds(first: 1000) {
|
||||
blockTimestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await executeQuery(query);
|
||||
analyticsData.value = processAnalyticsData(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch analytics:', err);
|
||||
} finally {
|
||||
analyticsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const processActivityData = (data: any): Transaction[] => {
|
||||
if (!data) return [];
|
||||
|
||||
const activities: Transaction[] = [];
|
||||
|
||||
if (data.depositAddeds) {
|
||||
data.depositAddeds.forEach((deposit: any) => {
|
||||
activities.push({
|
||||
id: deposit.id,
|
||||
blockNumber: deposit.blockNumber,
|
||||
blockTimestamp: deposit.blockTimestamp,
|
||||
transactionHash: deposit.transactionHash,
|
||||
type: 'deposit',
|
||||
seller: deposit.seller,
|
||||
buyer: undefined,
|
||||
amount: deposit.amount,
|
||||
token: deposit.token,
|
||||
timestamp: formatTimestamp(deposit.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.depositWithdrawns) {
|
||||
data.depositWithdrawns.forEach((withdrawal: any) => {
|
||||
activities.push({
|
||||
id: withdrawal.id,
|
||||
blockNumber: withdrawal.blockNumber,
|
||||
blockTimestamp: withdrawal.blockTimestamp,
|
||||
transactionHash: withdrawal.transactionHash,
|
||||
type: 'deposit', // Treat as deposit withdrawal
|
||||
seller: withdrawal.seller,
|
||||
buyer: undefined,
|
||||
amount: withdrawal.amount,
|
||||
token: withdrawal.token,
|
||||
timestamp: formatTimestamp(withdrawal.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockAddeds) {
|
||||
data.lockAddeds.forEach((lock: any) => {
|
||||
activities.push({
|
||||
id: lock.id,
|
||||
blockNumber: lock.blockNumber,
|
||||
blockTimestamp: lock.blockTimestamp,
|
||||
transactionHash: lock.transactionHash,
|
||||
type: 'lock',
|
||||
seller: lock.seller,
|
||||
buyer: lock.buyer,
|
||||
amount: lock.amount,
|
||||
token: lock.token,
|
||||
timestamp: formatTimestamp(lock.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReleaseds) {
|
||||
data.lockReleaseds.forEach((release: any) => {
|
||||
activities.push({
|
||||
id: release.id,
|
||||
blockNumber: release.blockNumber,
|
||||
blockTimestamp: release.blockTimestamp,
|
||||
transactionHash: release.transactionHash,
|
||||
type: 'release',
|
||||
seller: undefined, // Release doesn't have seller info
|
||||
buyer: release.buyer,
|
||||
amount: release.amount,
|
||||
token: 'BRZ', // Default token
|
||||
timestamp: formatTimestamp(release.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReturneds) {
|
||||
data.lockReturneds.forEach((returnTx: any) => {
|
||||
activities.push({
|
||||
id: returnTx.id,
|
||||
blockNumber: returnTx.blockNumber,
|
||||
blockTimestamp: returnTx.blockTimestamp,
|
||||
transactionHash: returnTx.transactionHash,
|
||||
type: 'return',
|
||||
seller: undefined, // Return doesn't have seller info
|
||||
buyer: returnTx.buyer,
|
||||
amount: '0', // Return doesn't have amount
|
||||
token: 'BRZ', // Default token
|
||||
timestamp: formatTimestamp(returnTx.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return activities.sort(
|
||||
(a, b) => parseInt(b.blockTimestamp) - parseInt(a.blockTimestamp),
|
||||
);
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string): string => {
|
||||
const now = Date.now() / 1000;
|
||||
const diff = now - parseInt(timestamp);
|
||||
|
||||
if (diff < 60) return 'Just now';
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
|
||||
return `${Math.floor(diff / 86400)} days ago`;
|
||||
};
|
||||
|
||||
const formatAmount = (amount: string): string => {
|
||||
const num = parseFloat(amount);
|
||||
if (num >= 1000000000000000)
|
||||
return `${(num / 1000000000000000).toFixed(1)}Q`;
|
||||
if (num >= 1000000000000) return `${(num / 1000000000000).toFixed(1)}T`;
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`;
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
||||
if (num < 1) return num.toFixed(4);
|
||||
return num.toFixed(2);
|
||||
};
|
||||
|
||||
const processAnalyticsData = (data: any): AnalyticsData => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalVolume: '0',
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0',
|
||||
};
|
||||
}
|
||||
|
||||
let totalVolume = 0;
|
||||
let totalTransactions = 0;
|
||||
let totalLocks = 0;
|
||||
let totalDeposits = 0;
|
||||
let totalReleases = 0;
|
||||
|
||||
if (data.depositAddeds) {
|
||||
data.depositAddeds.forEach((deposit: any) => {
|
||||
totalVolume += parseFloat(deposit.amount || '0');
|
||||
totalTransactions++;
|
||||
totalDeposits++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.depositWithdrawns) {
|
||||
data.depositWithdrawns.forEach((withdrawal: any) => {
|
||||
totalVolume += parseFloat(withdrawal.amount || '0');
|
||||
totalTransactions++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockAddeds) {
|
||||
data.lockAddeds.forEach((lock: any) => {
|
||||
totalVolume += parseFloat(lock.amount || '0');
|
||||
totalTransactions++;
|
||||
totalLocks++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReleaseds) {
|
||||
data.lockReleaseds.forEach((release: any) => {
|
||||
totalVolume += parseFloat(release.amount || '0');
|
||||
totalTransactions++;
|
||||
totalReleases++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReturneds) {
|
||||
data.lockReturneds.forEach((returnTx: any) => {
|
||||
totalTransactions++;
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
totalVolume: formatAmount(totalVolume.toString()),
|
||||
totalTransactions: totalTransactions.toString(),
|
||||
totalLocks: totalLocks.toString(),
|
||||
totalDeposits: totalDeposits.toString(),
|
||||
totalReleases: totalReleases.toString(),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const filteredTransactions = computed(() => {
|
||||
let filtered = transactionsData.value;
|
||||
|
||||
if (selectedType.value !== 'all') {
|
||||
filtered = filtered.filter((tx) => tx.type === selectedType.value);
|
||||
}
|
||||
|
||||
if (searchAddress.value) {
|
||||
const searchLower = searchAddress.value.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(tx) =>
|
||||
tx.seller?.toLowerCase().includes(searchLower) ||
|
||||
tx.buyer?.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
return {
|
||||
searchAddress,
|
||||
selectedType,
|
||||
transactions: filteredTransactions,
|
||||
analytics: analyticsData,
|
||||
loading,
|
||||
error,
|
||||
analyticsLoading,
|
||||
fetchAllActivity,
|
||||
fetchUserActivity,
|
||||
fetchAnalytics,
|
||||
clearData,
|
||||
};
|
||||
}
|
||||
125
src/composables/useUser.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ref } from 'vue';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { Participant } from '../utils/bbPay';
|
||||
import type { Address } from 'viem';
|
||||
import { DEFAULT_NETWORK, Networks } from '@/config/networks';
|
||||
import { TokenEnum, NetworkConfig } from '@/model/NetworkEnum';
|
||||
|
||||
const walletAddress = ref<Address | null>(null);
|
||||
const balance = ref('');
|
||||
const network = ref(DEFAULT_NETWORK);
|
||||
const selectedToken = ref<TokenEnum>(TokenEnum.BRZ);
|
||||
const loadingLock = ref(false);
|
||||
const sellerView = ref(false);
|
||||
const depositsValidList = ref<ValidDeposit[]>([]);
|
||||
const loadingWalletTransactions = ref(false);
|
||||
const loadingNetworkLiquidity = ref(false);
|
||||
const seller = ref<Participant>({} as Participant);
|
||||
const sellerId = ref('');
|
||||
|
||||
export function useUser() {
|
||||
// Actions become regular functions
|
||||
const setWalletAddress = (address: Address | null) => {
|
||||
walletAddress.value = address;
|
||||
};
|
||||
|
||||
const setBalance = (newBalance: string) => {
|
||||
balance.value = newBalance;
|
||||
};
|
||||
|
||||
const setSelectedToken = (token: TokenEnum) => {
|
||||
selectedToken.value = token;
|
||||
};
|
||||
|
||||
const setNetwork = (chain: NetworkConfig) => {
|
||||
network.value = chain;
|
||||
};
|
||||
|
||||
const setNetworkById = (id: string | number) => {
|
||||
let chainId: number;
|
||||
|
||||
if (typeof id === 'string') {
|
||||
// Parse hex string or number string to number
|
||||
if (id.startsWith('0x')) {
|
||||
chainId = parseInt(id, 16);
|
||||
} else {
|
||||
chainId = parseInt(id, 10);
|
||||
}
|
||||
} else {
|
||||
chainId = id;
|
||||
}
|
||||
|
||||
// Find network by chain ID
|
||||
const chain = Object.values(Networks).find((n) => n.id === chainId);
|
||||
if (chain) {
|
||||
network.value = chain;
|
||||
}
|
||||
};
|
||||
|
||||
const setLoadingLock = (isLoading: boolean) => {
|
||||
loadingLock.value = isLoading;
|
||||
};
|
||||
|
||||
const setSellerView = (view: boolean) => {
|
||||
sellerView.value = view;
|
||||
};
|
||||
|
||||
const setDepositsValidList = (deposits: ValidDeposit[]) => {
|
||||
depositsValidList.value = deposits;
|
||||
};
|
||||
|
||||
const setLoadingWalletTransactions = (isLoading: boolean) => {
|
||||
loadingWalletTransactions.value = isLoading;
|
||||
};
|
||||
|
||||
const setLoadingNetworkLiquidity = (isLoading: boolean) => {
|
||||
loadingNetworkLiquidity.value = isLoading;
|
||||
};
|
||||
|
||||
const setSeller = (newSeller: Participant) => {
|
||||
seller.value = newSeller;
|
||||
};
|
||||
|
||||
const setSellerId = (id: string) => {
|
||||
sellerId.value = id;
|
||||
};
|
||||
|
||||
// Getters become computed or regular functions
|
||||
const getValidDepositByWalletAddress = (address: string) => {
|
||||
return depositsValidList.value
|
||||
.filter((deposit) => deposit.seller == address)
|
||||
.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
walletAddress,
|
||||
balance,
|
||||
network,
|
||||
selectedToken,
|
||||
loadingLock,
|
||||
sellerView,
|
||||
depositsValidList,
|
||||
loadingWalletTransactions,
|
||||
loadingNetworkLiquidity,
|
||||
seller,
|
||||
sellerId,
|
||||
|
||||
// Actions
|
||||
setWalletAddress,
|
||||
setBalance,
|
||||
setSelectedToken,
|
||||
setNetwork,
|
||||
setNetworkById,
|
||||
setLoadingLock,
|
||||
setSellerView,
|
||||
setDepositsValidList,
|
||||
setLoadingWalletTransactions,
|
||||
setLoadingNetworkLiquidity,
|
||||
setSeller,
|
||||
setSellerId,
|
||||
|
||||
// Getters
|
||||
getValidDepositByWalletAddress,
|
||||
};
|
||||
}
|
||||
60
src/config/networks.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { mainnet, sepolia, rootstock, rootstockTestnet } from 'viem/chains';
|
||||
import { NetworkConfig } from '@/model/NetworkEnum';
|
||||
// TODO: import addresses from p2pix-smart-contracts deployments
|
||||
|
||||
export const Networks: { [key: string]: NetworkConfig } = {
|
||||
mainnet: {
|
||||
...mainnet,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_MAINNET_API_URL] } },
|
||||
contracts: {
|
||||
...mainnet.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_MAINNET_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_MAINNET_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_MAINNET_SUBGRAPH_URL],
|
||||
},
|
||||
rootstock: {
|
||||
...rootstock,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL] } },
|
||||
contracts: {
|
||||
...rootstock.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL],
|
||||
},
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const NetworksTestnet: { [key: string]: NetworkConfig } = {
|
||||
sepolia: {
|
||||
...sepolia,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_SEPOLIA_API_URL] } },
|
||||
contracts: {
|
||||
...sepolia.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_SEPOLIA_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_SEPOLIA_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL],
|
||||
},
|
||||
rootstockTestnet: {
|
||||
...rootstockTestnet,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL] } },
|
||||
contracts: {
|
||||
...rootstockTestnet.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL],
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_NETWORK = Networks.mainnet;
|
||||
13
src/main.ts
@@ -1,13 +1,12 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import "./assets/main.css";
|
||||
import './assets/main.css';
|
||||
import './assets/transitions.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(createPinia());
|
||||
|
||||
app.mount("#app");
|
||||
app.mount('#app');
|
||||
|
||||
6
src/model/AppVersion.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface AppVersion {
|
||||
tag: string;
|
||||
ipfsHash: string;
|
||||
releaseDate: string;
|
||||
description?: string;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
export type Faq = Section[];
|
||||
|
||||
export type Section = {
|
||||
type Section = {
|
||||
name: string;
|
||||
items: Question[];
|
||||
};
|
||||
|
||||
export type Question = {
|
||||
type Question = {
|
||||
title: string;
|
||||
content: string;
|
||||
isOpen?: boolean;
|
||||
|
||||
9
src/model/LockStatus.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Address } from 'viem';
|
||||
|
||||
export enum LockStatus {
|
||||
// from DataTypes.sol
|
||||
Inexistent = 0, // Uninitialized Lock
|
||||
Active = 1, // Valid Lock
|
||||
Expired = 2, // Expired Lock
|
||||
Released = 3, // Already released Lock
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
export enum NetworkEnum {
|
||||
ethereum = "Ethereum",
|
||||
polygon = "Polygon",
|
||||
rootstock = "Rootstock",
|
||||
import type { Chain, ChainContract } from 'viem';
|
||||
|
||||
export enum TokenEnum {
|
||||
BRZ = 'BRZ',
|
||||
// BRX = 'BRX'
|
||||
}
|
||||
export type NetworkConfig = Chain & {
|
||||
tokens: Record<TokenEnum, ChainContract>;
|
||||
subgraphUrls: string[];
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export type Pix = {
|
||||
pixKey: string;
|
||||
merchantCity?: string;
|
||||
merchantName?: string;
|
||||
value?: number;
|
||||
transactionId?: string;
|
||||
message?: string;
|
||||
cep?: string;
|
||||
currency?: number;
|
||||
countryCode?: string;
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Pix } from "./Pix";
|
||||
import { Address } from 'viem';
|
||||
|
||||
export type UnreleasedLock = {
|
||||
lockID: string;
|
||||
pix: Pix;
|
||||
lockID: bigint;
|
||||
sellerAddress: Address;
|
||||
tokenAddress: Address;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { Address } from 'viem';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
|
||||
export type ValidDeposit = {
|
||||
token: string;
|
||||
token: Address;
|
||||
blockNumber: number;
|
||||
remaining: number;
|
||||
seller: string;
|
||||
pixKey: number;
|
||||
seller: Address;
|
||||
participantID: string;
|
||||
network: NetworkConfig;
|
||||
open?: boolean;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { LockStatus } from '@/model/LockStatus';
|
||||
import type { Address } from 'viem';
|
||||
|
||||
export type WalletTransaction = {
|
||||
token: string;
|
||||
token?: Address;
|
||||
blockNumber: number;
|
||||
blockTimestamp?: number;
|
||||
amount: number;
|
||||
seller: string;
|
||||
buyer: string;
|
||||
event: string;
|
||||
lockStatus: number;
|
||||
lockStatus?: LockStatus;
|
||||
transactionHash: string;
|
||||
transactionID?: string;
|
||||
};
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { Event } from "ethers";
|
||||
import { vi } from "vitest";
|
||||
|
||||
export const MockEvents: Event[] = [
|
||||
{
|
||||
blockNumber: 1,
|
||||
blockHash: "0x8",
|
||||
transactionIndex: 1,
|
||||
removed: false,
|
||||
address: "0x0",
|
||||
data: "0x0",
|
||||
topics: ["0x0", "0x0"],
|
||||
transactionHash: "0x0",
|
||||
logIndex: 1,
|
||||
event: "DepositAdded",
|
||||
eventSignature: "DepositAdded(address,uint256,address,uint256)",
|
||||
args: [
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x6c6b935b8bbd400000",
|
||||
},
|
||||
],
|
||||
getBlock: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
getTransaction: vi.fn(),
|
||||
getTransactionReceipt: vi.fn(),
|
||||
},
|
||||
{
|
||||
blockNumber: 2,
|
||||
blockHash: "0x8",
|
||||
transactionIndex: 2,
|
||||
removed: false,
|
||||
address: "0x0",
|
||||
data: "0x0",
|
||||
topics: ["0x0", "0x0"],
|
||||
transactionHash: "0x0",
|
||||
logIndex: 2,
|
||||
event: "LockAdded",
|
||||
eventSignature: "LockAdded(address,uint256,address,uint256)",
|
||||
args: [
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x6c6b935b8bbd400000",
|
||||
},
|
||||
],
|
||||
getBlock: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
getTransaction: vi.fn(),
|
||||
getTransactionReceipt: vi.fn(),
|
||||
},
|
||||
{
|
||||
blockNumber: 3,
|
||||
blockHash: "0x8",
|
||||
transactionIndex: 3,
|
||||
removed: false,
|
||||
address: "0x0",
|
||||
data: "0x0",
|
||||
topics: ["0x0", "0x0"],
|
||||
transactionHash: "0x0",
|
||||
logIndex: 3,
|
||||
event: "LockReleased",
|
||||
eventSignature: "LockReleased(address,uint256,address,uint256)",
|
||||
args: [
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x6c6b935b8bbd400000",
|
||||
},
|
||||
],
|
||||
getBlock: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
getTransaction: vi.fn(),
|
||||
getTransactionReceipt: vi.fn(),
|
||||
},
|
||||
{
|
||||
blockNumber: 4,
|
||||
blockHash: "0x8",
|
||||
transactionIndex: 4,
|
||||
removed: false,
|
||||
address: "0x0",
|
||||
data: "0x0",
|
||||
topics: ["0x0", "0x0"],
|
||||
transactionHash: "0x0",
|
||||
logIndex: 4,
|
||||
event: "LockReleased",
|
||||
eventSignature: "LockReleased(address,uint256,address,uint256)",
|
||||
args: [
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
"0x0",
|
||||
{
|
||||
type: "BigNumber",
|
||||
hex: "0x6c6b935b8bbd400000",
|
||||
},
|
||||
],
|
||||
getBlock: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
getTransaction: vi.fn(),
|
||||
getTransactionReceipt: vi.fn(),
|
||||
},
|
||||
];
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { ValidDeposit } from "../ValidDeposit";
|
||||
|
||||
export const MockValidDeposits: ValidDeposit[] = [
|
||||
{
|
||||
blockNumber: 1,
|
||||
token: "1",
|
||||
remaining: 70,
|
||||
seller: "mockedSellerAddress",
|
||||
pixKey: 123456789,
|
||||
},
|
||||
{
|
||||
blockNumber: 2,
|
||||
token: "2",
|
||||
remaining: 200,
|
||||
seller: "mockedSellerAddress",
|
||||
pixKey: 123456789,
|
||||
},
|
||||
{
|
||||
blockNumber: 3,
|
||||
token: "3",
|
||||
remaining: 1250,
|
||||
seller: "mockedSellerAddress",
|
||||
pixKey: 123456789,
|
||||
},
|
||||
{
|
||||
blockNumber: 4,
|
||||
token: "4",
|
||||
remaining: 4000,
|
||||
seller: "mockedSellerAddress",
|
||||
pixKey: 123456789,
|
||||
},
|
||||
{
|
||||
blockNumber: 5,
|
||||
token: "5",
|
||||
remaining: 2000,
|
||||
seller: "mockedSellerAddress",
|
||||
pixKey: 123456789,
|
||||
},
|
||||
];
|
||||
@@ -1,54 +0,0 @@
|
||||
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",
|
||||
},
|
||||
];
|
||||
@@ -1,38 +1,57 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import FaqView from "@/views/FaqView.vue";
|
||||
import ManageBidsView from "@/views/ManageBidsView.vue";
|
||||
import SellerView from "@/views/SellerView.vue";
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
} from 'vue-router';
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import FaqView from '@/views/FaqView.vue';
|
||||
import ManageBidsView from '@/views/ManageBidsView.vue';
|
||||
import SellerView from '@/views/SellerView.vue';
|
||||
import ExploreView from '@/views/ExploreView.vue';
|
||||
import VersionsView from '@/views/VersionsView.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history:
|
||||
import.meta.env.MODE === 'production' && import.meta.env.BASE_URL === './'
|
||||
? createWebHashHistory()
|
||||
: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/:lockID",
|
||||
name: "redirect buy",
|
||||
path: '/:lockID',
|
||||
name: 'redirect buy',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/seller",
|
||||
name: "seller",
|
||||
path: '/seller',
|
||||
name: 'seller',
|
||||
component: SellerView,
|
||||
},
|
||||
{
|
||||
path: "/manage_bids",
|
||||
name: "manage bids",
|
||||
path: '/manage_bids',
|
||||
name: 'manage bids',
|
||||
component: ManageBidsView,
|
||||
},
|
||||
{
|
||||
path: "/faq",
|
||||
name: "faq",
|
||||
path: '/faq',
|
||||
name: 'faq',
|
||||
component: FaqView,
|
||||
},
|
||||
{
|
||||
path: '/explore',
|
||||
name: 'explore',
|
||||
component: ExploreView,
|
||||
},
|
||||
{
|
||||
path: '/versions',
|
||||
name: 'versions',
|
||||
component: VersionsView,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import axios from "axios";
|
||||
|
||||
const defaultConfig = {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
};
|
||||
|
||||
const api = axios.create({
|
||||
...defaultConfig,
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
export default api;
|
||||
4
src/shims-vue.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
const component: DefineComponent;
|
||||
export default component;
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useEtherStore = defineStore("ether", {
|
||||
state: () => ({
|
||||
walletAddress: "",
|
||||
balance: "",
|
||||
networkName: NetworkEnum.ethereum,
|
||||
loadingLock: false,
|
||||
sellerView: false,
|
||||
// Depósitos válidos para compra GOERLI
|
||||
depositsValidListGoerli: [] as ValidDeposit[],
|
||||
// Depósitos válidos para compra MUMBAI
|
||||
depositsValidListMumbai: [] as ValidDeposit[],
|
||||
// Depósitos válidos para compra ROOTSTOCK
|
||||
depositsValidListRootstock: [] as ValidDeposit[],
|
||||
loadingWalletTransactions: false,
|
||||
loadingNetworkLiquidity: false,
|
||||
}),
|
||||
actions: {
|
||||
setWalletAddress(walletAddress: string) {
|
||||
this.walletAddress = walletAddress;
|
||||
},
|
||||
setBalance(balance: string) {
|
||||
this.balance = balance;
|
||||
},
|
||||
setNetworkName(networkName: NetworkEnum) {
|
||||
this.networkName = networkName;
|
||||
},
|
||||
setLoadingLock(isLoadingLock: boolean) {
|
||||
this.loadingLock = isLoadingLock;
|
||||
},
|
||||
setSellerView(sellerView: boolean) {
|
||||
this.sellerView = sellerView;
|
||||
},
|
||||
setDepositsValidListGoerli(depositsValidList: ValidDeposit[]) {
|
||||
this.depositsValidListGoerli = depositsValidList;
|
||||
},
|
||||
setDepositsValidListMumbai(depositsValidList: ValidDeposit[]) {
|
||||
this.depositsValidListMumbai = depositsValidList;
|
||||
},
|
||||
setDepositsValidListRootstock(depositsValidList: ValidDeposit[]) {
|
||||
this.depositsValidListRootstock = depositsValidList;
|
||||
},
|
||||
setLoadingWalletTransactions(isLoadingWalletTransactions: boolean) {
|
||||
this.loadingWalletTransactions = isLoadingWalletTransactions;
|
||||
},
|
||||
setLoadingNetworkLiquidity(isLoadingNetworkLiquidity: boolean) {
|
||||
this.loadingNetworkLiquidity = isLoadingNetworkLiquidity;
|
||||
},
|
||||
},
|
||||
// Alterar para integrar com mumbai
|
||||
getters: {
|
||||
getValidDepositByWalletAddress: (state) => {
|
||||
return (walletAddress: string) =>
|
||||
state.depositsValidListGoerli
|
||||
.filter((deposit) => deposit.seller == walletAddress)
|
||||
.sort((a, b) => {
|
||||
return b.blockNumber - a.blockNumber;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
module.exports = {
|
||||
process() {
|
||||
return {
|
||||
code: `module.exports = {};`,
|
||||
code: 'module.exports = {};',
|
||||
};
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return "svgTransform";
|
||||
return 'svgTransform';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import qrcode from "qrcode";
|
||||
import type { QRCodeToDataURLOptions } from "qrcode";
|
||||
import { crc16ccitt } from "crc";
|
||||
import type { Pix } from "@/model/Pix";
|
||||
|
||||
const pix = ({
|
||||
pixKey,
|
||||
merchantCity = "city",
|
||||
merchantName = "name",
|
||||
value,
|
||||
message,
|
||||
cep,
|
||||
transactionId = "***",
|
||||
currency = 986,
|
||||
countryCode = "BR",
|
||||
}: Pix) => {
|
||||
const payloadKeyString = generatePixKey(pixKey, message);
|
||||
|
||||
const payload: string[] = [
|
||||
formatEMV("00", "01"), //Payload Format Indicator
|
||||
formatEMV("26", payloadKeyString), // Merchant Account Information
|
||||
formatEMV("52", "0000"), //Merchant Category Code
|
||||
formatEMV("53", String(currency)), // Transaction Currency
|
||||
];
|
||||
|
||||
if (String(value) === "0") {
|
||||
value = undefined;
|
||||
}
|
||||
if (value) {
|
||||
payload.push(formatEMV("54", value.toFixed(2)));
|
||||
}
|
||||
|
||||
payload.push(formatEMV("58", countryCode.toUpperCase())); // Country Code
|
||||
payload.push(formatEMV("59", merchantName)); // Merchant Name
|
||||
payload.push(formatEMV("60", merchantCity)); // Merchant City
|
||||
|
||||
if (cep) {
|
||||
payload.push(formatEMV("61", cep)); // Postal Code
|
||||
}
|
||||
|
||||
payload.push(formatEMV("62", formatEMV("05", transactionId))); // Additional Data Field Template
|
||||
|
||||
payload.push("6304");
|
||||
|
||||
const stringPayload = payload.join("");
|
||||
const crcResult = crc16ccitt(stringPayload)
|
||||
.toString(16)
|
||||
.toUpperCase()
|
||||
.padStart(4, "0");
|
||||
|
||||
const payloadPIX = `${stringPayload}${crcResult}`;
|
||||
|
||||
return {
|
||||
payload: (): string => payloadPIX,
|
||||
base64QrCode: (options?: QRCodeToDataURLOptions): Promise<string> =>
|
||||
qrcode.toDataURL(payloadPIX, options),
|
||||
};
|
||||
};
|
||||
|
||||
const generatePixKey = (pixKey: string, message?: string): string => {
|
||||
const payload: string[] = [
|
||||
formatEMV("00", "BR.GOV.BCB.PIX"),
|
||||
formatEMV("01", pixKey),
|
||||
];
|
||||
if (message) {
|
||||
payload.push(formatEMV("02", message));
|
||||
}
|
||||
return payload.join("");
|
||||
};
|
||||
|
||||
const formatEMV = (id: string, param: string): string => {
|
||||
const len = param?.length?.toString().padStart(2, "0");
|
||||
return `${id}${len}${param}`;
|
||||
};
|
||||
|
||||
export { pix };
|
||||