Compare commits
131 Commits
3227e3209c
...
develop
| 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 | ||
|
|
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
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
vendor/
|
||||||
|
.dagrobin
|
||||||
|
.claude
|
||||||
|
|
||||||
/cypress/videos/
|
/cypress/videos/
|
||||||
/cypress/screenshots/
|
/cypress/screenshots/
|
||||||
@@ -28,4 +31,7 @@ coverage
|
|||||||
*.sw?
|
*.sw?
|
||||||
.vercel
|
.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
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json bun.lock ./
|
||||||
RUN yarn
|
COPY vendor ./vendor
|
||||||
COPY ./ ./
|
RUN bun install --frozen-lockfile
|
||||||
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["yarn", "start"]
|
CMD ["bun", "run", "start"]
|
||||||
|
|||||||
68
README.md
@@ -3,13 +3,6 @@
|
|||||||
</p>
|
</p>
|
||||||
<br />
|
<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.
|
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
|
# Table of Contents
|
||||||
@@ -59,46 +52,69 @@ See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
### API
|
### API + RPC
|
||||||
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/)
|
|
||||||
|
|
||||||
### Alchemy Keys
|
Copy `.env.example` to `.env` and set the per-network variables:
|
||||||
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`
|
|
||||||
|
|
||||||
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
|
## 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:
|
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
|
### Run with bun
|
||||||
```sh
|
|
||||||
# Clone the repo
|
|
||||||
git clone https://github.com/liftlearning/P2Pix-Front-End
|
|
||||||
cd P2Pix-Front-End
|
|
||||||
|
|
||||||
# Install dependencies with yarn
|
```sh
|
||||||
yarn install
|
# 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
|
# Type-Check, Compile and Minify for Production
|
||||||
yarn build
|
bun run build
|
||||||
|
|
||||||
# Compile and Hot-Reload for Development (port 3000)
|
# Compile and Hot-Reload for Development (port 3000)
|
||||||
yarn start
|
bun start
|
||||||
|
|
||||||
# Lint with [ESLint](https://eslint.org/)
|
# Lint with [ESLint](https://eslint.org/)
|
||||||
yarn lint
|
bun run lint
|
||||||
```
|
```
|
||||||
### Run with docker-compose
|
### Run with docker-compose
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Clone the repo
|
# Pull the smart-contracts submodule (skip if you cloned with --recurse-submodules)
|
||||||
git clone https://github.com/liftlearning/P2Pix-Front-End
|
git submodule update --init
|
||||||
cd P2Pix-Front-End
|
|
||||||
|
|
||||||
#1. Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/);
|
#1. Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/);
|
||||||
#2. Install [Docker Compose](https://docs.docker.com/compose/install/).
|
#2. Install [Docker Compose](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
# Run docker-compose up command
|
|
||||||
docker-compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Backend Communication
|
||||||
|
|
||||||
|
Backend Repo: `https://gitea.kosmos.org/hueso/helpix`
|
||||||
|
|
||||||
|
Backend Endpoint: `https://api.p2pix.co/release/1279331`
|
||||||
|
|
||||||
|
curl -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer {api-key}" \
|
||||||
|
-d '{"query": "{ depositAddeds { id seller token amount } }"}' \
|
||||||
|
https://api.studio.thegraph.com/query/113713/p-2-pix/sepolia
|
||||||
|
|
||||||
|
https://api.studio.thegraph.com/query/113713/p-2-pix/1
|
||||||
|
|
||||||
|
curl --request POST --url 'https://api.hm.bb.com.br/testes-portal-desenvolvedor/v1/boletos-pix/pagar?gw-app-key=95cad3f03fd9013a9d15005056825665' --header 'content-type: application/json' --data '{"pix":"00020101021226070503***63041654" }'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
"@babel/preset-env",
|
'@babel/preset-env',
|
||||||
{
|
{
|
||||||
modules: false,
|
modules: false,
|
||||||
},
|
},
|
||||||
@@ -10,8 +10,8 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
test: {
|
test: {
|
||||||
presets: [
|
presets: [
|
||||||
["@babel/preset-env", { targets: { node: "current" } }],
|
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||||
"@babel/preset-typescript",
|
'@babel/preset-typescript',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,5 @@ services:
|
|||||||
container_name: p2pix_frontend
|
container_name: p2pix_frontend
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
volumes:
|
|
||||||
- '.:/app'
|
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
2
env.d.ts
vendored
@@ -1 +1,3 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <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
105
package.json
@@ -1,67 +1,60 @@
|
|||||||
{
|
{
|
||||||
"name": "p2pix-front-end",
|
"name": "p2pix-front-end",
|
||||||
"version": "0.1.0",
|
"version": "1.2.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite --host=0.0.0.0 --port 3000",
|
"start": "vite --host=0.0.0.0 --port 3000",
|
||||||
"build": "run-p type-check build-only",
|
"build": "bun run type-check && bun run build-only",
|
||||||
"preview": "vite preview",
|
|
||||||
"test": "vitest",
|
|
||||||
"serve": "vue-cli-service serve",
|
|
||||||
"coverage": "vitest run --coverage",
|
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
"type-check": "vue-tsc --skipLibCheck --noEmit",
|
"type-check": "vue-tsc --skipLibCheck --noEmit",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
"lint": "eslint",
|
||||||
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
"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": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^0.2.1",
|
"@floating-ui/vue": "^1.1.11",
|
||||||
"@headlessui/vue": "^1.7.3",
|
"@vueuse/core": "^14.3.0",
|
||||||
"@heroicons/vue": "^2.0.12",
|
"@web3-onboard/injected-wallets": "^2.11.3",
|
||||||
"@vueuse/core": "^9.12.0",
|
"@web3-onboard/vue": "^2.10.0",
|
||||||
"@web3-onboard/injected-wallets": "^2.11.2",
|
"marked": "^18.0.3",
|
||||||
"@web3-onboard/vue": "^2.9.0",
|
"qrcode": "^1.5.4",
|
||||||
"alchemy-sdk": "^2.3.0",
|
"viem": "^2.48.8",
|
||||||
"axios": "^1.2.1",
|
"vite-svg-loader": "^5.1.1",
|
||||||
"crc": "^3.8.0",
|
"vue": "^3.5.33",
|
||||||
"ethers": "^6.13.4",
|
"vue-router": "^5.0.6"
|
||||||
"marked": "^4.2.12",
|
|
||||||
"pinia": "^2.0.23",
|
|
||||||
"qrcode": "^1.5.1",
|
|
||||||
"viem": "2.x",
|
|
||||||
"vite-svg-loader": "^5.1.0",
|
|
||||||
"vue": "^3.2.41",
|
|
||||||
"vue-markdown": "^2.2.4",
|
|
||||||
"vue-router": "^4.1.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@playwright/test": "^1.59.1",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@pinia/testing": "^0.0.14",
|
"@types/node": "^25.6.0",
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/crc": "^3.8.0",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"@types/jest": "^27.0.0",
|
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||||
"@types/marked": "^4.0.8",
|
"@vitest/coverage-v8": "^4.1.5",
|
||||||
"@types/node": "^16.11.68",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@vue/eslint-config-typescript": "^14.7.0",
|
||||||
"@types/vue-markdown": "^2.2.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"@vitejs/plugin-vue": "^3.1.2",
|
"@wagmi/cli": "^2.10.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
"autoprefixer": "^10.5.0",
|
||||||
"@vitest/coverage-c8": "^0.28.2",
|
"eslint": "^10.3.0",
|
||||||
"@vue/eslint-config-prettier": "^7.0.0",
|
"eslint-plugin-vue": "^10.9.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.0",
|
"happy-dom": "^20.9.0",
|
||||||
"@vue/test-utils": "^2.2.7",
|
"postcss": "^8.5.8",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"prettier": "^3.5.3",
|
||||||
"autoprefixer": "^10.4.12",
|
"tailwindcss": "^4.2.4",
|
||||||
"eslint": "^8.22.0",
|
"typescript": "^6.0.3",
|
||||||
"eslint-plugin-vue": "^9.3.0",
|
"vite": "^8.0.10",
|
||||||
"jsdom": "^21.1.0",
|
"vitest": "^4.1.5",
|
||||||
"npm-run-all": "^4.1.5",
|
"vue-tsc": "^3.2.7"
|
||||||
"postcss": "^8.4.18",
|
},
|
||||||
"prettier": "^2.7.1",
|
"trustedDependencies": [
|
||||||
"tailwindcss": "^3.2.1",
|
"esbuild",
|
||||||
"typescript": "~5.8.2",
|
"vue-demi"
|
||||||
"vite": "^3.1.8",
|
]
|
||||||
"vitest": "^0.28.1",
|
|
||||||
"vue-tsc": "^2.2.8"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
43
src/App.vue
@@ -1,31 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from 'vue-router';
|
||||||
import TopBar from "@/components/TopBar/TopBar.vue";
|
import TopBar from '@/components/TopBar/TopBar.vue';
|
||||||
import SpinnerComponent from "@/components/SpinnerComponent.vue";
|
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||||
import { init, useOnboard } from "@web3-onboard/vue";
|
import ToasterComponent from '@/components/ui/ToasterComponent.vue';
|
||||||
import injectedModule from "@web3-onboard/injected-wallets";
|
import VersionFooter from '@/components/ui/VersionFooter.vue';
|
||||||
import { Networks } from "./model/Networks";
|
import { init, useOnboard } from '@web3-onboard/vue';
|
||||||
import { NetworkEnum } from "./model/NetworkEnum";
|
import injectedModule from '@web3-onboard/injected-wallets';
|
||||||
|
import { Networks, DEFAULT_NETWORK } from '@/config/networks';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const injected = injectedModule();
|
const injected = injectedModule();
|
||||||
|
const targetNetwork = ref(DEFAULT_NETWORK);
|
||||||
|
|
||||||
const web3Onboard = init({
|
const web3Onboard = init({
|
||||||
wallets: [injected],
|
wallets: [injected],
|
||||||
chains: [
|
chains: Object.values(Networks).map((network) => ({
|
||||||
{
|
id: `0x${network.id.toString(16)}`,
|
||||||
id: Networks[NetworkEnum.sepolia].chainId,
|
token: network.nativeCurrency.symbol,
|
||||||
token: "ETH",
|
label: network.name,
|
||||||
label: "Sepolia",
|
rpcUrl: network.rpcUrls.default.http[0],
|
||||||
rpcUrl: import.meta.env.VITE_SEPOLIA_API_URL,
|
})),
|
||||||
},
|
|
||||||
{
|
|
||||||
id: Networks[NetworkEnum.rootstock].chainId,
|
|
||||||
token: "tRBTC",
|
|
||||||
label: "Rootstock Testnet",
|
|
||||||
rpcUrl: import.meta.env.VITE_ROOTSTOCK_API_URL,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
connect: {
|
connect: {
|
||||||
autoConnectLastWallet: true,
|
autoConnectLastWallet: true,
|
||||||
},
|
},
|
||||||
@@ -38,7 +33,7 @@ if (!connectedWallet) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-3 sm:p-4 md:p-8">
|
<main class="p-3 sm:p-4 md:p-8">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<template v-if="Component">
|
<template v-if="Component">
|
||||||
@@ -58,5 +53,7 @@ if (!connectedWallet) {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</div>
|
<ToasterComponent :targetNetwork="targetNetwork" />
|
||||||
|
<VersionFooter />
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
@import './base.css';
|
@import "./base.css" layer(base);
|
||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -28,3 +26,9 @@ a,
|
|||||||
.main-container {
|
.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;
|
@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 |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 644 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,98 +0,0 @@
|
|||||||
import { expectTypeOf, it, expect } from "vitest";
|
|
||||||
import {
|
|
||||||
getTokenAddress,
|
|
||||||
getP2PixAddress,
|
|
||||||
getProviderUrl,
|
|
||||||
isPossibleNetwork,
|
|
||||||
} from "../addresses";
|
|
||||||
|
|
||||||
import { setActivePinia, createPinia } from "pinia";
|
|
||||||
import { NetworkEnum, TokenEnum } 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("addresses.ts functions", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePinia(createPinia());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getTokenAddress Ethereum", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.sepolia);
|
|
||||||
expect(getTokenAddress(TokenEnum.BRZ)).toBe(
|
|
||||||
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getTokenAddress Rootstock", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.rootstock);
|
|
||||||
expect(getTokenAddress(TokenEnum.BRZ)).toBe(
|
|
||||||
"0xfE841c74250e57640390f46d914C88d22C51e82e"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getTokenAddress Default", () => {
|
|
||||||
expect(getTokenAddress(TokenEnum.BRZ)).toBe(
|
|
||||||
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getP2PixAddress Ethereum", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.sepolia);
|
|
||||||
expect(getP2PixAddress()).toBe(
|
|
||||||
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getP2PixAddress Rootstock", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.rootstock);
|
|
||||||
expect(getP2PixAddress()).toBe(
|
|
||||||
"0x98ba35eb14b38D6Aa709338283af3e922476dE34"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getP2PixAddress Default", () => {
|
|
||||||
expect(getP2PixAddress()).toBe(
|
|
||||||
"0x2414817FF64A114d91eCFA16a834d3fCf69103d4"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getProviderUrl Ethereum", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.sepolia);
|
|
||||||
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getProviderUrl Rootstock", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.rootstock);
|
|
||||||
expect(getProviderUrl()).toBe(import.meta.env.VITE_ROOTSTOCK_API_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getProviderUrl Default", () => {
|
|
||||||
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("isPossibleNetwork Returns", () => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
etherStore.setNetworkId(NetworkEnum.sepolia);
|
|
||||||
expect(isPossibleNetwork(0x5 as NetworkEnum)).toBe(true);
|
|
||||||
expect(isPossibleNetwork(5 as NetworkEnum)).toBe(true);
|
|
||||||
expect(isPossibleNetwork(0x13881 as NetworkEnum)).toBe(true);
|
|
||||||
expect(isPossibleNetwork(80001 as NetworkEnum)).toBe(true);
|
|
||||||
|
|
||||||
expect(isPossibleNetwork(NaN as NetworkEnum)).toBe(false);
|
|
||||||
expect(isPossibleNetwork(0x55 as NetworkEnum)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { useEtherStore } from "@/store/ether";
|
|
||||||
import { NetworkEnum, TokenEnum } from "@/model/NetworkEnum";
|
|
||||||
import { JsonRpcProvider } from "ethers";
|
|
||||||
|
|
||||||
const Tokens: { [key in NetworkEnum]: { [key in TokenEnum]: string } } = {
|
|
||||||
[NetworkEnum.sepolia]: {
|
|
||||||
BRZ: "0x3eBE67A2C7bdB2081CBd34ba3281E90377462289",
|
|
||||||
// BRX: "0x3eBE67A2C7bdB2081CBd34ba3281E90377462289",
|
|
||||||
},
|
|
||||||
[NetworkEnum.rootstock]: {
|
|
||||||
BRZ: "0xfE841c74250e57640390f46d914C88d22C51e82e",
|
|
||||||
// BRX: "0xfE841c74250e57640390f46d914C88d22C51e82e",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTokenByAddress = (address: string) => {
|
|
||||||
for (const network of Object.values(NetworkEnum).filter(
|
|
||||||
(v) => !isNaN(Number(v))
|
|
||||||
)) {
|
|
||||||
for (const token of Object.keys(Tokens[network as NetworkEnum])) {
|
|
||||||
if (address === Tokens[network as NetworkEnum][token as TokenEnum]) {
|
|
||||||
return token as TokenEnum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTokenAddress = (token: TokenEnum, network?: NetworkEnum): string => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
return Tokens[network ? network : etherStore.networkName][token];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getP2PixAddress = (network?: NetworkEnum): string => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
const possibleP2PixAddresses: { [key in NetworkEnum]: string } = {
|
|
||||||
[NetworkEnum.sepolia]: "0xb7cD135F5eFD9760981e02E2a898790b688939fe",
|
|
||||||
[NetworkEnum.rootstock]: "0x98ba35eb14b38D6Aa709338283af3e922476dE34",
|
|
||||||
};
|
|
||||||
|
|
||||||
return possibleP2PixAddresses[network ? network : etherStore.networkName];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProviderUrl = (network?: NetworkEnum): string => {
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
const possibleProvidersUrls: { [key in NetworkEnum]: string } = {
|
|
||||||
[NetworkEnum.sepolia]: import.meta.env.VITE_SEPOLIA_API_URL,
|
|
||||||
[NetworkEnum.rootstock]: import.meta.env.VITE_RSK_API_URL,
|
|
||||||
};
|
|
||||||
|
|
||||||
return possibleProvidersUrls[network || etherStore.networkName];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProviderByNetwork = (network: NetworkEnum): JsonRpcProvider => {
|
|
||||||
console.log("network", network);
|
|
||||||
return new JsonRpcProvider(getProviderUrl(network), network);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isPossibleNetwork = (networkChain: NetworkEnum): boolean => {
|
|
||||||
return Number(networkChain) in NetworkEnum;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
getTokenAddress,
|
|
||||||
getProviderUrl,
|
|
||||||
isPossibleNetwork,
|
|
||||||
getP2PixAddress,
|
|
||||||
getProviderByNetwork,
|
|
||||||
};
|
|
||||||
@@ -1,94 +1,78 @@
|
|||||||
import { getContract, getProvider } from "./provider";
|
import { getContract } from './provider';
|
||||||
import { getP2PixAddress, getTokenAddress } from "./addresses";
|
import { ChainContract } from 'viem';
|
||||||
import { encodeBytes32String, Signature, Contract, parseEther } from "ethers";
|
import { parseEther, type Address, type TransactionReceipt } from 'viem';
|
||||||
|
|
||||||
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
|
export const addLock = async (
|
||||||
|
sellerAddress: Address,
|
||||||
|
tokenAddress: Address,
|
||||||
|
amount: number,
|
||||||
|
): Promise<bigint> => {
|
||||||
|
const { address, abi, wallet, client, account } = await getContract();
|
||||||
|
const parsedAmount = parseEther(amount.toString());
|
||||||
|
|
||||||
import type { TokenEnum } from "@/model/NetworkEnum";
|
if (!wallet) {
|
||||||
import { createSolicitation } from "../utils/bbPay";
|
throw new Error('Wallet not connected');
|
||||||
import type { Offer } from "../utils/bbPay";
|
}
|
||||||
|
|
||||||
const addLock = async (
|
const { result, request } = await client.simulateContract({
|
||||||
sellerId: string,
|
address,
|
||||||
token: string,
|
abi,
|
||||||
amount: number
|
functionName: 'lock',
|
||||||
): Promise<string> => {
|
args: [sellerAddress, tokenAddress, parsedAmount, [], []],
|
||||||
const p2pContract = await getContract();
|
account,
|
||||||
|
});
|
||||||
|
const hash = await wallet.writeContract(request);
|
||||||
|
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||||
|
|
||||||
const lock = await p2pContract.lock(
|
if (!receipt.status)
|
||||||
sellerId,
|
throw new Error('Transaction failed: ' + receipt.transactionHash);
|
||||||
token,
|
|
||||||
parseEther(String(amount)), // BigNumber
|
|
||||||
[],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const lock_rec = await lock.wait();
|
return result;
|
||||||
const [t] = lock_rec.events;
|
|
||||||
|
|
||||||
const offer: Offer = {
|
|
||||||
amount,
|
|
||||||
lockId: String(t.args.lockID),
|
|
||||||
sellerId: sellerId,
|
|
||||||
};
|
|
||||||
const solicitation = await createSolicitation(offer);
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const releaseLock = async (solicitation: any): Promise<any> => {
|
export const withdrawDeposit = async (
|
||||||
// const mockBacenSigner = new Wallet(
|
|
||||||
// "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const messageToSign = solidityPackedKeccak256(
|
|
||||||
// ["bytes32", "uint256", "bytes32"],
|
|
||||||
// [sellerId, parseEther(String(amount)), encodeBytes32String(signature)]
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const messageHashBytes = getBytes(messageToSign);
|
|
||||||
// const flatSig = await mockBacenSigner.signMessage(messageHashBytes);
|
|
||||||
|
|
||||||
const provider = getProvider();
|
|
||||||
|
|
||||||
const sig = Signature.from(flatSig);
|
|
||||||
console.log(sig);
|
|
||||||
const signer = await provider.getSigner();
|
|
||||||
const p2pContract = new Contract(getP2PixAddress(), p2pix.abi, signer);
|
|
||||||
|
|
||||||
const release = await p2pContract.release(
|
|
||||||
BigInt(lockId),
|
|
||||||
encodeBytes32String(e2eId),
|
|
||||||
flatSig
|
|
||||||
);
|
|
||||||
await release.wait();
|
|
||||||
|
|
||||||
return release;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelDeposit = async (depositId: bigint): Promise<any> => {
|
|
||||||
const contract = await getContract();
|
|
||||||
|
|
||||||
const cancel = await contract.cancelDeposit(depositId);
|
|
||||||
await cancel.wait();
|
|
||||||
|
|
||||||
return cancel;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withdrawDeposit = async (
|
|
||||||
amount: string,
|
amount: string,
|
||||||
token: TokenEnum
|
token: Address,
|
||||||
): Promise<any> => {
|
): Promise<boolean> => {
|
||||||
const contract = await getContract();
|
const { address, abi, wallet, client, account } = await getContract();
|
||||||
|
|
||||||
const withdraw = await contract.withdraw(
|
if (!wallet) {
|
||||||
getTokenAddress(token),
|
throw new Error('Wallet not connected');
|
||||||
parseEther(String(amount)),
|
}
|
||||||
[]
|
|
||||||
);
|
|
||||||
await withdraw.wait();
|
|
||||||
|
|
||||||
return withdraw;
|
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';
|
||||||
};
|
};
|
||||||
|
|
||||||
export { cancelDeposit, withdrawDeposit, addLock, releaseLock };
|
export const releaseLock = async (
|
||||||
|
lockID: bigint,
|
||||||
|
pixTimestamp: `0x${string}` & { lenght: 34 },
|
||||||
|
signature: `0x${string}`,
|
||||||
|
): Promise<TransactionReceipt> => {
|
||||||
|
const { address, abi, wallet, client, account } = await getContract();
|
||||||
|
|
||||||
|
if (!wallet) {
|
||||||
|
throw new Error('Wallet not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,131 +1,168 @@
|
|||||||
import { useEtherStore } from "@/store/ether";
|
import { useUser } from '@/composables/useUser';
|
||||||
import { Contract, formatEther, Interface } from "ethers";
|
import { formatEther, toHex, stringToHex } from 'viem';
|
||||||
|
import type { PublicClient, Address } from 'viem';
|
||||||
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
|
import { Networks } from '@/config/networks';
|
||||||
import { getContract } from "./provider";
|
import { getContract } from './provider';
|
||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
import { p2PixAbi } from './abi';
|
||||||
import {
|
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||||
getP2PixAddress,
|
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||||
getProviderByNetwork,
|
import type { UnreleasedLock } from '@/model/UnreleasedLock';
|
||||||
getTokenAddress,
|
import { ChainContract } from 'viem';
|
||||||
} from "./addresses";
|
|
||||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
|
||||||
import type { UnreleasedLock } from "@/model/UnreleasedLock";
|
|
||||||
import type { Pix } from "@/model/Pix";
|
|
||||||
|
|
||||||
const getNetworksLiquidity = async (): Promise<void> => {
|
const getNetworksLiquidity = async (): Promise<void> => {
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
etherStore.setLoadingNetworkLiquidity(true);
|
user.setLoadingNetworkLiquidity(true);
|
||||||
|
|
||||||
const depositLists: ValidDeposit[][] = [];
|
const depositLists: ValidDeposit[][] = [];
|
||||||
|
|
||||||
for (const network of Object.values(NetworkEnum).filter(
|
for (const network of Object.values(Networks)) {
|
||||||
(v) => !isNaN(Number(v))
|
const deposits = await getValidDeposits(
|
||||||
)) {
|
user.network.value.tokens[user.selectedToken.value].address,
|
||||||
console.log("getNetworksLiquidity", network);
|
network,
|
||||||
const p2pContract = new Contract(
|
|
||||||
getP2PixAddress(network as NetworkEnum),
|
|
||||||
p2pix.abi,
|
|
||||||
getProviderByNetwork(network as NetworkEnum)
|
|
||||||
);
|
|
||||||
|
|
||||||
depositLists.push(
|
|
||||||
await getValidDeposits(
|
|
||||||
getTokenAddress(etherStore.selectedToken, network as NetworkEnum),
|
|
||||||
network as NetworkEnum,
|
|
||||||
p2pContract
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
if (deposits) depositLists.push(deposits);
|
||||||
}
|
}
|
||||||
|
|
||||||
etherStore.setDepositsValidList(depositLists.flat());
|
const allDeposits = depositLists.flat();
|
||||||
etherStore.setLoadingNetworkLiquidity(false);
|
user.setDepositsValidList(allDeposits);
|
||||||
|
user.setLoadingNetworkLiquidity(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPixKey = async (seller: string, token: string): Promise<string> => {
|
const getParticipantID = async (
|
||||||
const p2pContract = await getContract();
|
seller: Address,
|
||||||
const pixKeyHex = await p2pContract.getPixTarget(seller, token);
|
token: Address,
|
||||||
|
): Promise<string> => {
|
||||||
|
const { address, abi, client } = await getContract();
|
||||||
|
|
||||||
|
const participantIDHex = await client.readContract({
|
||||||
|
address,
|
||||||
|
abi,
|
||||||
|
functionName: 'getPixTarget',
|
||||||
|
args: [seller, token],
|
||||||
|
});
|
||||||
|
|
||||||
// Remove '0x' prefix and convert hex to UTF-8 string
|
// 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(
|
const bytes = new Uint8Array(
|
||||||
pixKeyHex
|
hexString
|
||||||
.slice(2)
|
.slice(2)
|
||||||
.match(/.{1,2}/g)
|
.match(/.{1,2}/g)!
|
||||||
.map((byte: string) => parseInt(byte, 16))
|
.map((byte: string) => parseInt(byte, 16)),
|
||||||
);
|
);
|
||||||
// Remove null bytes from the end of the string
|
// Remove null bytes from the end of the string
|
||||||
return new TextDecoder().decode(bytes).replace(/\0/g, "");
|
return new TextDecoder().decode(bytes).replace(/\0/g, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValidDeposits = async (
|
const getValidDeposits = async (
|
||||||
token: string,
|
token: Address,
|
||||||
network: NetworkEnum,
|
network: NetworkConfig,
|
||||||
contract?: Contract
|
contractInfo?: { client: PublicClient; address: Address },
|
||||||
): Promise<ValidDeposit[]> => {
|
): Promise<ValidDeposit[]> => {
|
||||||
let p2pContract: Contract;
|
let client: PublicClient, abi;
|
||||||
|
|
||||||
if (contract) {
|
if (contractInfo) {
|
||||||
p2pContract = contract;
|
({ client } = contractInfo);
|
||||||
|
abi = p2PixAbi;
|
||||||
} else {
|
} else {
|
||||||
p2pContract = await getContract(true);
|
({ abi, client } = await getContract(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterDeposits = p2pContract.filters.DepositAdded(null);
|
const body = {
|
||||||
const eventsDeposits = await p2pContract.queryFilter(
|
query: `
|
||||||
filterDeposits
|
{
|
||||||
// 0,
|
depositAddeds(where: { token: "${token}" }) {
|
||||||
// "latest"
|
seller
|
||||||
|
amount
|
||||||
|
blockTimestamp
|
||||||
|
blockNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const depositLogs = await fetch(network.subgraphUrls[0], {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove doubles from sellers list
|
||||||
|
const depositData = await depositLogs.json();
|
||||||
|
if (!depositData.data) {
|
||||||
|
console.error('Error fetching deposit logs');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const depositAddeds = depositData.data.depositAddeds;
|
||||||
|
const uniqueSellers = depositAddeds.reduce(
|
||||||
|
(acc: Record<Address, boolean>, deposit: any) => {
|
||||||
|
acc[deposit.seller] = true;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<Address, boolean>,
|
||||||
);
|
);
|
||||||
if (!contract) p2pContract = await getContract(); // get metamask provider contract
|
|
||||||
|
if (!contractInfo) {
|
||||||
|
// Get metamask provider contract
|
||||||
|
({ abi, client } = await getContract(true));
|
||||||
|
}
|
||||||
|
|
||||||
const depositList: { [key: string]: ValidDeposit } = {};
|
const depositList: { [key: string]: ValidDeposit } = {};
|
||||||
|
|
||||||
for (const deposit of eventsDeposits) {
|
const sellersList = Object.keys(uniqueSellers) as Address[];
|
||||||
const IPix2Pix = new Interface(p2pix.abi);
|
// Use multicall to batch all getBalance requests
|
||||||
const decoded = IPix2Pix.parseLog({
|
const balanceCalls = sellersList.map((seller) => ({
|
||||||
topics: deposit.topics,
|
address: (network.contracts?.p2pix as ChainContract).address,
|
||||||
data: deposit.data,
|
abi,
|
||||||
});
|
functionName: 'getBalance',
|
||||||
// Get liquidity only for the selected token
|
args: [seller, token],
|
||||||
if (decoded?.args.token != token) continue;
|
}));
|
||||||
const mappedBalance = await p2pContract.getBalance(
|
|
||||||
decoded.args.seller,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
let validDeposit: ValidDeposit | null = null;
|
|
||||||
|
|
||||||
if (mappedBalance) {
|
const balanceResults = await client.multicall({
|
||||||
validDeposit = {
|
contracts: balanceCalls as any,
|
||||||
token: token,
|
});
|
||||||
blockNumber: deposit.blockNumber,
|
|
||||||
remaining: Number(formatEther(mappedBalance)),
|
// Process results into the depositList
|
||||||
seller: decoded.args.seller,
|
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,
|
network,
|
||||||
pixKey: "",
|
participantID: '',
|
||||||
};
|
};
|
||||||
|
depositList[seller + token] = validDeposit;
|
||||||
}
|
}
|
||||||
if (validDeposit) depositList[decoded.args.seller + token] = validDeposit;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return Object.values(depositList);
|
return Object.values(depositList);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUnreleasedLockById = async (
|
const getUnreleasedLockById = async (
|
||||||
lockID: string
|
lockID: bigint,
|
||||||
): Promise<UnreleasedLock> => {
|
): Promise<UnreleasedLock> => {
|
||||||
const p2pContract = await getContract();
|
const { address, abi, client } = await getContract();
|
||||||
const pixData: Pix = {
|
|
||||||
pixKey: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const lock = await p2pContract.mapLocks(lockID);
|
const [, , , amount, token, seller] = await client.readContract({
|
||||||
|
address,
|
||||||
const pixTarget = lock.pixTarget;
|
abi,
|
||||||
const amount = formatEther(lock?.amount);
|
functionName: 'mapLocks',
|
||||||
pixData.pixKey = pixTarget;
|
args: [lockID],
|
||||||
pixData.value = Number(amount);
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lockID: lockID,
|
lockID,
|
||||||
pix: pixData,
|
amount: Number(formatEther(amount)),
|
||||||
|
tokenAddress: token,
|
||||||
|
sellerAddress: seller,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,5 +170,5 @@ export {
|
|||||||
getValidDeposits,
|
getValidDeposits,
|
||||||
getNetworksLiquidity,
|
getNetworksLiquidity,
|
||||||
getUnreleasedLockById,
|
getUnreleasedLockById,
|
||||||
getPixKey,
|
getParticipantID,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,64 @@
|
|||||||
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
|
import { p2PixAbi } from './abi';
|
||||||
import { updateWalletStatus } from "./wallet";
|
import { updateWalletStatus } from './wallet';
|
||||||
import { getProviderUrl, getP2PixAddress } from "./addresses";
|
import {
|
||||||
import { BrowserProvider, JsonRpcProvider, Contract } from "ethers";
|
createPublicClient,
|
||||||
|
createWalletClient,
|
||||||
|
custom,
|
||||||
|
http,
|
||||||
|
PublicClient,
|
||||||
|
WalletClient,
|
||||||
|
} from 'viem';
|
||||||
|
import { useUser } from '@/composables/useUser';
|
||||||
|
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||||
|
import type { ChainContract } from 'viem';
|
||||||
|
|
||||||
let provider: BrowserProvider | JsonRpcProvider | null = null;
|
let walletClient: WalletClient | null = null;
|
||||||
|
|
||||||
const getProvider = (onlyAlchemyProvider: boolean = false) => {
|
const getPublicClient = (): PublicClient => {
|
||||||
if (onlyAlchemyProvider) return new JsonRpcProvider(getProviderUrl()); // alchemy provider
|
const user = useUser();
|
||||||
return provider;
|
const rpcUrl = (user.network.value as NetworkConfig).rpcUrls.default.http[0];
|
||||||
|
const chain = user.network.value;
|
||||||
|
|
||||||
|
return createPublicClient({
|
||||||
|
chain,
|
||||||
|
transport: http(rpcUrl),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContract = async (onlyAlchemyProvider: boolean = false) => {
|
const getWalletClient = (): WalletClient | null => {
|
||||||
const p = getProvider(onlyAlchemyProvider);
|
return walletClient;
|
||||||
try {
|
};
|
||||||
const signer = await p?.getSigner();
|
|
||||||
return new Contract(getP2PixAddress(), p2pix.abi, signer);
|
const getContract = async (onlyRpcProvider = false) => {
|
||||||
} catch (err) {
|
const client = getPublicClient();
|
||||||
return new Contract(getP2PixAddress(), p2pix.abi, p);
|
const user = useUser();
|
||||||
|
const address = (user.network.value.contracts?.p2pix as ChainContract)
|
||||||
|
.address;
|
||||||
|
const abi = p2PixAbi;
|
||||||
|
const wallet = onlyRpcProvider ? null : getWalletClient();
|
||||||
|
|
||||||
|
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 connectProvider = async (p: any): Promise<void> => {
|
||||||
provider = new BrowserProvider(p, "any");
|
const user = useUser();
|
||||||
|
const chain = user.network.value;
|
||||||
|
|
||||||
|
const [account] = await p!.request({ method: 'eth_requestAccounts' });
|
||||||
|
|
||||||
|
walletClient = createWalletClient({
|
||||||
|
account,
|
||||||
|
chain,
|
||||||
|
transport: custom(p),
|
||||||
|
});
|
||||||
|
|
||||||
await updateWalletStatus();
|
await updateWalletStatus();
|
||||||
};
|
};
|
||||||
export { getProvider, getContract, connectProvider };
|
|
||||||
|
export { getPublicClient, getWalletClient, getContract, connectProvider };
|
||||||
|
|||||||
@@ -1,60 +1,92 @@
|
|||||||
import { getContract, getProvider } from "./provider";
|
import { getContract, getPublicClient, getWalletClient } from './provider';
|
||||||
import { getTokenAddress, getP2PixAddress } from "./addresses";
|
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 { encodeBytes32String, Contract, parseEther } from "ethers";
|
const getP2PixAddress = (): Address => {
|
||||||
|
const user = useUser();
|
||||||
import mockToken from "../utils/smart_contract_files/MockToken.json";
|
return (user.network.value.contracts?.p2pix as ChainContract).address;
|
||||||
import { useEtherStore } from "@/store/ether";
|
};
|
||||||
import { createParticipant } from "@/utils/bbPay";
|
|
||||||
import type { Participant } from "@/utils/bbPay";
|
|
||||||
|
|
||||||
const approveTokens = async (participant: Participant): Promise<any> => {
|
const approveTokens = async (participant: Participant): Promise<any> => {
|
||||||
const provider = getProvider();
|
const user = useUser();
|
||||||
const signer = await provider?.getSigner();
|
const publicClient = getPublicClient();
|
||||||
const etherStore = useEtherStore();
|
const walletClient = getWalletClient();
|
||||||
|
|
||||||
etherStore.setSeller(participant);
|
if (!publicClient || !walletClient) {
|
||||||
const tokenContract = new Contract(
|
throw new Error('Clients not initialized');
|
||||||
getTokenAddress(etherStore.selectedToken),
|
}
|
||||||
mockToken.abi,
|
|
||||||
signer
|
user.setSeller(participant);
|
||||||
);
|
const [account] = await walletClient.getAddresses();
|
||||||
|
|
||||||
|
// Get token address
|
||||||
|
const tokenAddress =
|
||||||
|
user.network.value.tokens[user.selectedToken.value].address;
|
||||||
|
|
||||||
// Check if the token is already approved
|
// Check if the token is already approved
|
||||||
const approved = await tokenContract.allowance(
|
const allowance = await publicClient.readContract({
|
||||||
await signer?.getAddress(),
|
address: tokenAddress,
|
||||||
getP2PixAddress()
|
abi: mockTokenAbi,
|
||||||
);
|
functionName: 'allowance',
|
||||||
if (approved < parseEther(participant.offer)) {
|
args: [account, getP2PixAddress()],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allowance < parseEther(participant.offer.toString())) {
|
||||||
// Approve tokens
|
// Approve tokens
|
||||||
const apprv = await tokenContract.approve(
|
const chain = user.network.value;
|
||||||
getP2PixAddress(),
|
const hash = await walletClient.writeContract({
|
||||||
parseEther(participant.offer)
|
address: tokenAddress,
|
||||||
);
|
abi: mockTokenAbi,
|
||||||
await apprv.wait();
|
functionName: 'approve',
|
||||||
|
args: [getP2PixAddress(), parseEther(participant.offer.toString())],
|
||||||
|
account,
|
||||||
|
chain,
|
||||||
|
});
|
||||||
|
|
||||||
|
await publicClient.waitForTransactionReceipt({ hash });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addDeposit = async (): Promise<any> => {
|
const addDeposit = async (): Promise<any> => {
|
||||||
const p2pContract = await getContract();
|
const { address, abi, client } = await getContract();
|
||||||
const etherStore = useEtherStore();
|
const walletClient = getWalletClient();
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
const sellerId = await createParticipant(etherStore.seller);
|
if (!walletClient) {
|
||||||
etherStore.setSellerId(sellerId.id);
|
throw new Error('Wallet client not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
const deposit = await p2pContract.deposit(
|
const [account] = await walletClient.getAddresses();
|
||||||
sellerId,
|
|
||||||
encodeBytes32String(""),
|
|
||||||
getTokenAddress(etherStore.selectedToken),
|
|
||||||
parseEther(etherStore.seller.offer),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
await deposit.wait();
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
return deposit;
|
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||||
|
return receipt;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { approveTokens, addDeposit };
|
export { approveTokens, addDeposit };
|
||||||
|
|||||||
@@ -1,64 +1,46 @@
|
|||||||
import {
|
import { formatEther, type Address } from 'viem';
|
||||||
Contract,
|
import { useUser } from '@/composables/useUser';
|
||||||
formatEther,
|
|
||||||
getAddress,
|
|
||||||
Interface,
|
|
||||||
Log,
|
|
||||||
LogDescription,
|
|
||||||
} from "ethers";
|
|
||||||
import { useEtherStore } from "@/store/ether";
|
|
||||||
|
|
||||||
import { getContract, getProvider } from "./provider";
|
import { getPublicClient, getWalletClient, getContract } from './provider';
|
||||||
import { getTokenAddress, isPossibleNetwork } from "./addresses";
|
|
||||||
|
|
||||||
import mockToken from "@/utils/smart_contract_files/MockToken.json";
|
import { getValidDeposits, getUnreleasedLockById } from './events';
|
||||||
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
|
|
||||||
|
|
||||||
import { getValidDeposits } from "./events";
|
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||||
|
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
import type { UnreleasedLock } from '@/model/UnreleasedLock';
|
||||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
import { LockStatus } from '@/model/LockStatus';
|
||||||
import type { UnreleasedLock } from "@/model/UnreleasedLock";
|
|
||||||
import type { Pix } from "@/model/Pix";
|
|
||||||
|
|
||||||
export const updateWalletStatus = async (): Promise<void> => {
|
export const updateWalletStatus = async (): Promise<void> => {
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
|
|
||||||
const provider = await getProvider();
|
const publicClient = getPublicClient();
|
||||||
const signer = await provider?.getSigner();
|
const walletClient = getWalletClient();
|
||||||
const network = await provider?.getNetwork();
|
|
||||||
const chainId = network?.chainId;
|
if (!publicClient || !walletClient) {
|
||||||
if (!isPossibleNetwork(Number(chainId))) {
|
console.error('Client not initialized');
|
||||||
window.alert("Invalid chain!:" + chainId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
etherStore.setNetworkId(Number(chainId));
|
|
||||||
|
|
||||||
const mockTokenContract = new Contract(
|
// Get balance
|
||||||
getTokenAddress(etherStore.selectedToken),
|
const [account] = await walletClient.getAddresses();
|
||||||
mockToken.abi,
|
const balance = await publicClient.getBalance({ address: account });
|
||||||
signer
|
|
||||||
);
|
|
||||||
|
|
||||||
const walletAddress = await provider?.send("eth_requestAccounts", []);
|
user.setWalletAddress(account);
|
||||||
const balance = await mockTokenContract.balanceOf(walletAddress[0]);
|
user.setBalance(formatEther(balance));
|
||||||
|
|
||||||
etherStore.setBalance(formatEther(balance));
|
|
||||||
etherStore.setWalletAddress(getAddress(walletAddress[0]));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listValidDepositTransactionsByWalletAddress = async (
|
export const listValidDepositTransactionsByWalletAddress = async (
|
||||||
walletAddress: string
|
walletAddress: Address,
|
||||||
): Promise<ValidDeposit[]> => {
|
): Promise<ValidDeposit[]> => {
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
const walletDeposits = await getValidDeposits(
|
const walletDeposits = await getValidDeposits(
|
||||||
getTokenAddress(etherStore.selectedToken),
|
user.network.value.tokens[user.selectedToken.value].address,
|
||||||
etherStore.networkName
|
user.network.value,
|
||||||
);
|
);
|
||||||
if (walletDeposits) {
|
if (walletDeposits) {
|
||||||
return walletDeposits
|
return walletDeposits
|
||||||
.filter((deposit) => deposit.seller == walletAddress)
|
.filter((deposit) => deposit.seller == walletAddress)
|
||||||
.sort((a, b) => {
|
.sort((a: ValidDeposit, b: ValidDeposit) => {
|
||||||
return b.blockNumber - a.blockNumber;
|
return b.blockNumber - a.blockNumber;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -66,235 +48,353 @@ export const listValidDepositTransactionsByWalletAddress = async (
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLockStatus = async (id: [BigInt]): Promise<number> => {
|
const getLockStatus = async (id: bigint): Promise<LockStatus> => {
|
||||||
const p2pContract = await getContract();
|
const { address, abi, client } = await getContract();
|
||||||
const res = await p2pContract.getLocksStatus([id]);
|
const [sortedIDs, status] = await client.readContract({
|
||||||
return res[1][0];
|
address,
|
||||||
};
|
abi,
|
||||||
|
functionName: 'getLocksStatus',
|
||||||
const filterLockStatus = async (
|
args: [[id]],
|
||||||
transactions: Log[]
|
});
|
||||||
): Promise<WalletTransaction[]> => {
|
return status[0];
|
||||||
const txs: WalletTransaction[] = [];
|
|
||||||
|
|
||||||
for (const transaction of transactions) {
|
|
||||||
const IPix2Pix = new Interface(p2pix.abi);
|
|
||||||
const decoded = IPix2Pix.parseLog({
|
|
||||||
topics: transaction.topics,
|
|
||||||
data: transaction.data,
|
|
||||||
});
|
|
||||||
if (!decoded) continue;
|
|
||||||
const tx: WalletTransaction = {
|
|
||||||
token: decoded.args.token ? decoded.args.token : "",
|
|
||||||
blockNumber: transaction.blockNumber,
|
|
||||||
amount: decoded.args.amount
|
|
||||||
? Number(formatEther(decoded.args.amount))
|
|
||||||
: -1,
|
|
||||||
seller: decoded.args.seller ? decoded.args.seller : "",
|
|
||||||
buyer: decoded.args.buyer ? decoded.args.buyer : "",
|
|
||||||
event: decoded.name,
|
|
||||||
lockStatus:
|
|
||||||
decoded.name == "LockAdded"
|
|
||||||
? await getLockStatus(decoded.args.lockID)
|
|
||||||
: -1,
|
|
||||||
transactionHash: transaction.transactionHash
|
|
||||||
? transaction.transactionHash
|
|
||||||
: "",
|
|
||||||
transactionID: decoded.args.lockID ? decoded.args.lockID.toString() : "",
|
|
||||||
};
|
|
||||||
txs.push(tx);
|
|
||||||
}
|
|
||||||
return txs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listAllTransactionByWalletAddress = async (
|
export const listAllTransactionByWalletAddress = async (
|
||||||
walletAddress: string
|
walletAddress: Address,
|
||||||
): Promise<WalletTransaction[]> => {
|
): Promise<WalletTransaction[]> => {
|
||||||
const p2pContract = await getContract(true);
|
const user = useUser();
|
||||||
|
|
||||||
// Get deposits
|
// Get the current network for the subgraph URL
|
||||||
const filterDeposits = p2pContract.filters.DepositAdded([walletAddress]);
|
const network = user.network.value;
|
||||||
const eventsDeposits = await p2pContract.queryFilter(
|
|
||||||
filterDeposits,
|
|
||||||
0,
|
|
||||||
"latest"
|
|
||||||
);
|
|
||||||
console.log("Fetched all wallet deposits");
|
|
||||||
|
|
||||||
// Get locks
|
// Query subgraph for all relevant transactions
|
||||||
const filterAddedLocks = p2pContract.filters.LockAdded([walletAddress]);
|
const subgraphQuery = {
|
||||||
const eventsAddedLocks = await p2pContract.queryFilter(
|
query: `
|
||||||
filterAddedLocks,
|
{
|
||||||
0,
|
depositAddeds(where: {seller: "${walletAddress.toLowerCase()}"}) {
|
||||||
"latest"
|
id
|
||||||
);
|
seller
|
||||||
console.log("Fetched all wallet locks");
|
token
|
||||||
|
amount
|
||||||
// Get released locks
|
blockTimestamp
|
||||||
const filterReleasedLocks = p2pContract.filters.LockReleased([walletAddress]);
|
blockNumber
|
||||||
const eventsReleasedLocks = await p2pContract.queryFilter(
|
transactionHash
|
||||||
filterReleasedLocks,
|
}
|
||||||
0,
|
lockAddeds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
||||||
"latest"
|
buyer
|
||||||
);
|
lockID
|
||||||
console.log("Fetched all wallet released locks");
|
seller
|
||||||
|
amount
|
||||||
// Get withdrawn deposits
|
blockTimestamp
|
||||||
const filterWithdrawnDeposits = p2pContract.filters.DepositWithdrawn([
|
blockNumber
|
||||||
walletAddress,
|
transactionHash
|
||||||
]);
|
}
|
||||||
const eventsWithdrawnDeposits = await p2pContract.queryFilter(
|
lockReleaseds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
||||||
filterWithdrawnDeposits
|
buyer
|
||||||
);
|
lockId
|
||||||
console.log("Fetched all wallet withdrawn deposits");
|
blockTimestamp
|
||||||
|
blockNumber
|
||||||
const lockStatusFiltered = await filterLockStatus(
|
transactionHash
|
||||||
[
|
}
|
||||||
...eventsDeposits,
|
depositWithdrawns(where: {seller: "${walletAddress.toLowerCase()}"}) {
|
||||||
...eventsAddedLocks,
|
seller
|
||||||
...eventsReleasedLocks,
|
token
|
||||||
...eventsWithdrawnDeposits,
|
amount
|
||||||
].sort((a, b) => {
|
blockTimestamp
|
||||||
return b.blockNumber - a.blockNumber;
|
blockNumber
|
||||||
})
|
transactionHash
|
||||||
);
|
}
|
||||||
|
}
|
||||||
return lockStatusFiltered;
|
`,
|
||||||
};
|
|
||||||
|
|
||||||
// get wallet's release transactions
|
|
||||||
export const listReleaseTransactionByWalletAddress = async (
|
|
||||||
walletAddress: string
|
|
||||||
): Promise<LogDescription[]> => {
|
|
||||||
const p2pContract = await getContract(true);
|
|
||||||
|
|
||||||
const filterReleasedLocks = p2pContract.filters.LockReleased([walletAddress]);
|
|
||||||
const eventsReleasedLocks = await p2pContract.queryFilter(
|
|
||||||
filterReleasedLocks,
|
|
||||||
0,
|
|
||||||
"latest"
|
|
||||||
);
|
|
||||||
|
|
||||||
return eventsReleasedLocks
|
|
||||||
.sort((a, b) => {
|
|
||||||
return b.blockNumber - a.blockNumber;
|
|
||||||
})
|
|
||||||
.map((lock) => {
|
|
||||||
const IPix2Pix = new Interface(p2pix.abi);
|
|
||||||
const decoded = IPix2Pix.parseLog({
|
|
||||||
topics: lock.topics,
|
|
||||||
data: lock.data,
|
|
||||||
});
|
|
||||||
return decoded;
|
|
||||||
})
|
|
||||||
.filter((lock) => lock !== null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const listLockTransactionByWalletAddress = async (
|
|
||||||
walletAddress: string
|
|
||||||
): Promise<LogDescription[]> => {
|
|
||||||
const p2pContract = await getContract(true);
|
|
||||||
|
|
||||||
const filterAddedLocks = p2pContract.filters.LockAdded([walletAddress]);
|
|
||||||
const eventsReleasedLocks = await p2pContract.queryFilter(filterAddedLocks);
|
|
||||||
|
|
||||||
return eventsReleasedLocks
|
|
||||||
.sort((a, b) => {
|
|
||||||
return b.blockNumber - a.blockNumber;
|
|
||||||
})
|
|
||||||
.map((lock) => {
|
|
||||||
const IPix2Pix = new Interface(p2pix.abi);
|
|
||||||
const decoded = IPix2Pix.parseLog({
|
|
||||||
topics: lock.topics,
|
|
||||||
data: lock.data,
|
|
||||||
});
|
|
||||||
return decoded;
|
|
||||||
})
|
|
||||||
.filter((lock) => lock !== null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const listLockTransactionBySellerAddress = async (
|
|
||||||
sellerAddress: string
|
|
||||||
): Promise<LogDescription[]> => {
|
|
||||||
const p2pContract = await getContract(true);
|
|
||||||
console.log("Will get locks as seller", sellerAddress);
|
|
||||||
const filterAddedLocks = p2pContract.filters.LockAdded();
|
|
||||||
const eventsReleasedLocks = await p2pContract.queryFilter(
|
|
||||||
filterAddedLocks
|
|
||||||
// 0,
|
|
||||||
// "latest"
|
|
||||||
);
|
|
||||||
return eventsReleasedLocks
|
|
||||||
.map((lock) => {
|
|
||||||
const IPix2Pix = new Interface(p2pix.abi);
|
|
||||||
const decoded = IPix2Pix.parseLog({
|
|
||||||
topics: lock.topics,
|
|
||||||
data: lock.data,
|
|
||||||
});
|
|
||||||
return decoded;
|
|
||||||
})
|
|
||||||
.filter((lock) => lock !== null)
|
|
||||||
.filter(
|
|
||||||
(lock) => lock.args.seller.toLowerCase() == sellerAddress.toLowerCase()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkUnreleasedLock = async (
|
|
||||||
walletAddress: string
|
|
||||||
): Promise<UnreleasedLock | undefined> => {
|
|
||||||
const p2pContract = await getContract();
|
|
||||||
const pixData: Pix = {
|
|
||||||
pixKey: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addedLocks = await listLockTransactionByWalletAddress(walletAddress);
|
const response = await fetch(network.subgraphUrls[0], {
|
||||||
const lockStatus = await p2pContract.getLocksStatus(
|
method: 'POST',
|
||||||
addedLocks.map((lock) => lock.args?.lockID)
|
headers: {
|
||||||
);
|
'Content-Type': 'application/json',
|
||||||
const unreleasedLockId = lockStatus[1].findIndex(
|
},
|
||||||
(lockStatus: number) => lockStatus == 1
|
body: JSON.stringify(subgraphQuery),
|
||||||
);
|
});
|
||||||
|
|
||||||
if (unreleasedLockId != -1) {
|
const data = await response.json();
|
||||||
const _lockID = lockStatus[0][unreleasedLockId];
|
// Convert all transactions to common WalletTransaction format
|
||||||
const lock = await p2pContract.mapLocks(_lockID);
|
const transactions: WalletTransaction[] = [];
|
||||||
|
|
||||||
const pixTarget = lock.pixTarget;
|
// Process deposit added events
|
||||||
const amount = formatEther(lock?.amount);
|
if (data.data?.depositAddeds) {
|
||||||
pixData.pixKey = pixTarget;
|
for (const deposit of data.data.depositAddeds) {
|
||||||
pixData.value = Number(amount);
|
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 {
|
// Process lock added events
|
||||||
lockID: _lockID,
|
if (data.data?.lockAddeds) {
|
||||||
pix: pixData,
|
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 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 (
|
export const getActiveLockAmount = async (
|
||||||
walletAddress: string
|
walletAddress: Address,
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
const p2pContract = await getContract(true);
|
const { address, abi, client } = await getContract(true);
|
||||||
const lockSeller = await listLockTransactionBySellerAddress(walletAddress);
|
const lockSeller = await listLockTransactionBySellerAddress(walletAddress);
|
||||||
|
|
||||||
const lockStatus = await p2pContract.getLocksStatus(
|
if (!lockSeller.length) return 0;
|
||||||
lockSeller.map((lock) => lock.args?.lockID)
|
|
||||||
|
const lockIds = lockSeller.map((lock: any) => lock.args.lockID);
|
||||||
|
|
||||||
|
const [sortedIDs, status] = await client.readContract({
|
||||||
|
address,
|
||||||
|
abi,
|
||||||
|
functionName: 'getLocksStatus',
|
||||||
|
args: [lockIds],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapLocksRequests = status.map((id: LockStatus) =>
|
||||||
|
client.readContract({
|
||||||
|
address: address,
|
||||||
|
abi,
|
||||||
|
functionName: 'mapLocks',
|
||||||
|
args: [BigInt(id)],
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLockAmount = await lockStatus[1].reduce(
|
const mapLocksResults = await client.multicall({
|
||||||
async (sumValue: Promise<number>, currentStatus: number, index: number) => {
|
contracts: mapLocksRequests as any,
|
||||||
const currValue = await sumValue;
|
});
|
||||||
let valueToSum = 0;
|
|
||||||
|
|
||||||
if (currentStatus == 1) {
|
return mapLocksResults.reduce((total: number, lock: any, index: number) => {
|
||||||
const lock = await p2pContract.mapLocks(lockStatus[0][index]);
|
if (status[index] === 1) {
|
||||||
valueToSum = Number(formatEther(lock?.amount));
|
return total + Number(formatEther(lock.amount));
|
||||||
}
|
}
|
||||||
|
return total;
|
||||||
return currValue + valueToSum;
|
}, 0);
|
||||||
},
|
|
||||||
Promise.resolve(0)
|
|
||||||
);
|
|
||||||
|
|
||||||
return activeLockAmount;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
<script setup lang="ts">
|
||||||
import { withdrawDeposit } from "@/blockchain/buyerMethods";
|
import { withdrawDeposit } from '@/blockchain/buyerMethods';
|
||||||
import {
|
import {
|
||||||
getActiveLockAmount,
|
getActiveLockAmount,
|
||||||
listAllTransactionByWalletAddress,
|
listAllTransactionByWalletAddress,
|
||||||
listValidDepositTransactionsByWalletAddress,
|
listValidDepositTransactionsByWalletAddress,
|
||||||
} from "@/blockchain/wallet";
|
} from '@/blockchain/wallet';
|
||||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||||
import { useEtherStore } from "@/store/ether";
|
import { useUser } from '@/composables/useUser';
|
||||||
import { storeToRefs } from "pinia";
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { onMounted, ref, watch } from "vue";
|
import ListingComponent from '@/components/ListingComponent/ListingComponent.vue';
|
||||||
import ListingComponent from "../ListingComponent/ListingComponent.vue";
|
|
||||||
|
|
||||||
// props
|
// props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -19,8 +18,8 @@ const props = defineProps<{
|
|||||||
isCurrentStep: boolean;
|
isCurrentStep: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
const { walletAddress } = storeToRefs(etherStore);
|
const { walletAddress } = useUser();
|
||||||
|
|
||||||
const lastWalletTransactions = ref<WalletTransaction[]>([]);
|
const lastWalletTransactions = ref<WalletTransaction[]>([]);
|
||||||
const depositList = ref<ValidDeposit[]>([]);
|
const depositList = ref<ValidDeposit[]>([]);
|
||||||
@@ -29,14 +28,14 @@ const activeLockAmount = ref<number>(0);
|
|||||||
// methods
|
// methods
|
||||||
|
|
||||||
const getWalletTransactions = async () => {
|
const getWalletTransactions = async () => {
|
||||||
etherStore.setLoadingWalletTransactions(true);
|
user.setLoadingWalletTransactions(true);
|
||||||
if (walletAddress.value) {
|
if (walletAddress.value) {
|
||||||
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
|
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
|
||||||
walletAddress.value
|
walletAddress.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const allUserTransactions = await listAllTransactionByWalletAddress(
|
const allUserTransactions = await listAllTransactionByWalletAddress(
|
||||||
walletAddress.value
|
walletAddress.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
|
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
|
||||||
@@ -48,25 +47,28 @@ const getWalletTransactions = async () => {
|
|||||||
lastWalletTransactions.value = allUserTransactions;
|
lastWalletTransactions.value = allUserTransactions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
etherStore.setLoadingWalletTransactions(false);
|
user.setLoadingWalletTransactions(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const callWithdraw = async (amount: string) => {
|
const callWithdraw = async (amount: string) => {
|
||||||
if (amount) {
|
if (amount) {
|
||||||
etherStore.setLoadingWalletTransactions(true);
|
user.setLoadingWalletTransactions(true);
|
||||||
const withdraw = await withdrawDeposit(amount, etherStore.selectedToken);
|
const withdraw = await withdrawDeposit(
|
||||||
|
amount,
|
||||||
|
user.network.value.tokens[user.selectedToken.value].address,
|
||||||
|
);
|
||||||
if (withdraw) {
|
if (withdraw) {
|
||||||
console.log("Saque realizado!");
|
console.log('Saque realizado!');
|
||||||
await getWalletTransactions();
|
await getWalletTransactions();
|
||||||
} else {
|
} else {
|
||||||
console.log("Não foi possível realizar o saque!");
|
console.log('Não foi possível realizar o saque!');
|
||||||
}
|
}
|
||||||
etherStore.setLoadingWalletTransactions(false);
|
user.setLoadingWalletTransactions(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(["makeAnotherTransaction"]);
|
const emit = defineEmits(['makeAnotherTransaction']);
|
||||||
|
|
||||||
// observer
|
// observer
|
||||||
watch(props, async (): Promise<void> => {
|
watch(props, async (): Promise<void> => {
|
||||||
@@ -87,20 +89,18 @@ onMounted(async () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div
|
<div class="flex flex-col w-full bg-white px-10 py-5 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<p>Tokens recebidos</p>
|
<p>Tokens recebidos</p>
|
||||||
<p class="text-2xl text-gray-900">
|
<p class="text-2xl text-gray-900">
|
||||||
{{ props.tokenAmount }} {{ etherStore.selectedToken }}
|
{{ props.tokenAmount }} {{ user.selectedToken }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-5">
|
<div class="my-5">
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
<b>Não encontrou os tokens? </b><br />Clique no botão abaixo para
|
<b>Não encontrou os tokens? </b><br />Clique no botão abaixo para
|
||||||
<br />
|
<br />
|
||||||
cadastrar o {{ etherStore.selectedToken }} em sua carteira.
|
cadastrar o {{ user.selectedToken }} em sua carteira.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<CustomButton :text="'Cadastrar token'" @buttonClicked="() => {}" />
|
<CustomButton :text="'Cadastrar token'" @buttonClicked="() => {}" />
|
||||||
@@ -130,6 +130,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
.page {
|
.page {
|
||||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||||
}
|
}
|
||||||
@@ -150,12 +151,8 @@ p {
|
|||||||
@apply font-medium text-base text-gray-900;
|
@apply font-medium text-base text-gray-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
-moz-appearance: textfield;
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,30 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch, computed } from 'vue';
|
||||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
import { useUser } from '@/composables/useUser';
|
||||||
import { debounce } from "@/utils/debounce";
|
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||||
import { useEtherStore } from "@/store/ether";
|
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||||
import { storeToRefs } from "pinia";
|
import { debounce } from '@/utils/debounce';
|
||||||
import { verifyNetworkLiquidity } from "@/utils/networkLiquidity";
|
import { verifyNetworkLiquidity } from '@/utils/networkLiquidity';
|
||||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
import { decimalCount } from '@/utils/decimalCount';
|
||||||
import { decimalCount } from "@/utils/decimalCount";
|
import { getTokenImage, getNetworkImage } from '@/utils/imagesPath';
|
||||||
import SpinnerComponent from "./SpinnerComponent.vue";
|
import { onClickOutside } from '@vueuse/core';
|
||||||
import { getTokenImage } from "@/utils/imagesPath";
|
import { Networks } from '@/config/networks';
|
||||||
import { onClickOutside } from "@vueuse/core";
|
import { TokenEnum } from '@/model/NetworkEnum';
|
||||||
|
import { getContract } from '@/blockchain/provider';
|
||||||
import { TokenEnum } from "@/model/NetworkEnum";
|
import { reputationAbi } from '@/blockchain/abi';
|
||||||
|
import { type Address } from 'viem';
|
||||||
|
|
||||||
// Store reference
|
// Store reference
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
const selectTokenToggle = ref<boolean>(false);
|
const selectTokenToggle = ref<boolean>(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
walletAddress,
|
walletAddress,
|
||||||
networkName,
|
network,
|
||||||
selectedToken,
|
selectedToken,
|
||||||
depositsValidList,
|
depositsValidList,
|
||||||
loadingNetworkLiquidity,
|
loadingNetworkLiquidity,
|
||||||
} = storeToRefs(etherStore);
|
} = user;
|
||||||
|
|
||||||
// html references
|
// html references
|
||||||
const tokenDropdownRef = ref<any>(null);
|
const tokenDropdownRef = ref<any>(null);
|
||||||
@@ -34,15 +35,106 @@ const tokenValue = ref<number>(0);
|
|||||||
const enableConfirmButton = ref<boolean>(false);
|
const enableConfirmButton = ref<boolean>(false);
|
||||||
const hasLiquidity = ref<boolean>(true);
|
const hasLiquidity = ref<boolean>(true);
|
||||||
const validDecimals = ref<boolean>(true);
|
const validDecimals = ref<boolean>(true);
|
||||||
const identification = ref<string>("");
|
const identification = ref<string>('');
|
||||||
const selectedDeposits = ref<ValidDeposit[]>();
|
const selectedDeposits = ref<ValidDeposit[]>();
|
||||||
|
const reputationLimit = ref<number | null>(null);
|
||||||
|
const exceedsReputationLimit = ref<boolean>(false);
|
||||||
|
|
||||||
import ChevronDown from "@/assets/chevronDown.svg";
|
import ChevronDown from '@/assets/chevronDown.svg';
|
||||||
import { useOnboard } from "@web3-onboard/vue";
|
import { useOnboard } from '@web3-onboard/vue';
|
||||||
import { getPixKey } from "@/blockchain/events";
|
import { getParticipantID } from '@/blockchain/events';
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(["tokenBuy"]);
|
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
|
// Blockchain methods
|
||||||
const connectAccount = async (): Promise<void> => {
|
const connectAccount = async (): Promise<void> => {
|
||||||
@@ -52,11 +144,11 @@ const connectAccount = async (): Promise<void> => {
|
|||||||
|
|
||||||
const emitConfirmButton = async (): Promise<void> => {
|
const emitConfirmButton = async (): Promise<void> => {
|
||||||
const deposit = selectedDeposits.value?.find(
|
const deposit = selectedDeposits.value?.find(
|
||||||
(d) => d.network === networkName.value
|
(d) => d.network === network.value,
|
||||||
);
|
);
|
||||||
if (!deposit) return;
|
if (!deposit) return;
|
||||||
deposit.pixKey = await getPixKey(deposit.seller, deposit.token);
|
deposit.participantID = await getParticipantID(deposit.seller, deposit.token);
|
||||||
emit("tokenBuy", deposit, tokenValue.value);
|
emit('tokenBuy', deposit, tokenValue.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debounce methods
|
// Debounce methods
|
||||||
@@ -72,6 +164,7 @@ const handleInputEvent = (event: any): void => {
|
|||||||
}
|
}
|
||||||
validDecimals.value = true;
|
validDecimals.value = true;
|
||||||
|
|
||||||
|
checkReputationLimit(tokenValue.value);
|
||||||
verifyLiquidity();
|
verifyLiquidity();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,23 +177,21 @@ onClickOutside(tokenDropdownRef, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectedToken = (token: TokenEnum): void => {
|
const handleSelectedToken = (token: TokenEnum): void => {
|
||||||
etherStore.setSelectedToken(token);
|
user.setSelectedToken(token);
|
||||||
selectTokenToggle.value = false;
|
selectTokenToggle.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify if there is a valid deposit to buy
|
// Verify if there is a valid deposit to buy
|
||||||
const verifyLiquidity = (): void => {
|
const verifyLiquidity = (): void => {
|
||||||
enableConfirmButton.value = false;
|
enableConfirmButton.value = false;
|
||||||
|
if (!walletAddress.value) return;
|
||||||
const selDeposits = verifyNetworkLiquidity(
|
const selDeposits = verifyNetworkLiquidity(
|
||||||
tokenValue.value,
|
tokenValue.value,
|
||||||
walletAddress.value,
|
walletAddress.value,
|
||||||
depositsValidList.value
|
depositsValidList.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedDeposits.value = selDeposits;
|
selectedDeposits.value = selDeposits;
|
||||||
hasLiquidity.value = !!selDeposits.find(
|
hasLiquidity.value = !!selDeposits.find((d) => d.network === network.value);
|
||||||
(d) => d.network === networkName.value
|
|
||||||
);
|
|
||||||
enableOrDisableConfirmButton();
|
enableOrDisableConfirmButton();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,7 +201,7 @@ const enableOrDisableConfirmButton = (): void => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedDeposits.value.find((d) => d.network === networkName.value)) {
|
if (!selectedDeposits.value.find((d) => d.network === network.value)) {
|
||||||
enableConfirmButton.value = false;
|
enableConfirmButton.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,7 +209,7 @@ const enableOrDisableConfirmButton = (): void => {
|
|||||||
enableConfirmButton.value = true;
|
enableConfirmButton.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(networkName, (): void => {
|
watch(network, (): void => {
|
||||||
verifyLiquidity();
|
verifyLiquidity();
|
||||||
enableOrDisableConfirmButton();
|
enableOrDisableConfirmButton();
|
||||||
});
|
});
|
||||||
@@ -127,11 +218,16 @@ watch(walletAddress, (): void => {
|
|||||||
verifyLiquidity();
|
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
|
// Add form submission handler
|
||||||
const handleSubmit = async (e: Event): Promise<void> => {
|
const handleSubmit = async (e: Event): Promise<void> => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!enableConfirmButton.value) return;
|
|
||||||
|
|
||||||
if (walletAddress.value) {
|
if (walletAddress.value) {
|
||||||
await emitConfirmButton();
|
await emitConfirmButton();
|
||||||
} else {
|
} else {
|
||||||
@@ -155,14 +251,12 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
</div>
|
</div>
|
||||||
<form class="main-container" @submit="handleSubmit">
|
<form class="main-container" @submit="handleSubmit">
|
||||||
<div class="backdrop-blur -z-10 w-full h-full"></div>
|
<div class="backdrop-blur -z-10 w-full h-full"></div>
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg">
|
||||||
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">
|
<div class="flex justify-between sm:w-full items-center">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="tokenAmount"
|
name="tokenAmount"
|
||||||
class="border-none outline-none text-lg text-gray-900"
|
class="border-none outline-none text-lg text-gray-900 sm:flex-1 max-w-[60%]"
|
||||||
v-bind:class="{
|
v-bind:class="{
|
||||||
'font-semibold': tokenValue != undefined,
|
'font-semibold': tokenValue != undefined,
|
||||||
'text-xl': tokenValue != undefined,
|
'text-xl': tokenValue != undefined,
|
||||||
@@ -172,7 +266,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
step=".01"
|
step=".01"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div class="relative overflow-visible">
|
<div class="relative overflow-visible ml-auto sm:ml-0">
|
||||||
<button
|
<button
|
||||||
ref="tokenDropdownRef"
|
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"
|
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"
|
||||||
@@ -235,24 +329,12 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<img
|
<img
|
||||||
alt="Rootstock image"
|
v-for="network in availableNetworks"
|
||||||
src="@/assets/rootstock.svg?url"
|
:key="network.id"
|
||||||
|
:alt="`${network.name} image`"
|
||||||
|
:src="getNetworkImage(network.name)"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
v-if="
|
|
||||||
selectedDeposits &&
|
|
||||||
selectedDeposits.find((d) => d.network == NetworkEnum.rootstock)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt="Ethereum image"
|
|
||||||
src="@/assets/ethereum.svg?url"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
v-if="
|
|
||||||
selectedDeposits &&
|
|
||||||
selectedDeposits.find((d) => d.network == NetworkEnum.sepolia)
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,18 +357,34 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
v-else-if="!hasLiquidity && !loadingNetworkLiquidity"
|
v-else-if="
|
||||||
|
!hasLiquidity &&
|
||||||
|
!loadingNetworkLiquidity &&
|
||||||
|
tokenValue > 0 &&
|
||||||
|
!exceedsReputationLimit
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="text-red-500 font-normal text-sm"
|
<span class="text-red-500 font-normal text-sm"
|
||||||
>Atualmente não há liquidez nas rede selecionada para sua
|
>Atualmente não há liquidez nas rede selecionada para sua
|
||||||
demanda</span
|
demanda</span
|
||||||
>
|
>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="identification"
|
v-model="identification"
|
||||||
@@ -303,6 +401,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
v-if="walletAddress"
|
v-if="walletAddress"
|
||||||
type="submit"
|
type="submit"
|
||||||
text="Confirmar Oferta"
|
text="Confirmar Oferta"
|
||||||
|
:isDisabled="!enableConfirmButton"
|
||||||
/>
|
/>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
v-else
|
v-else
|
||||||
@@ -314,6 +413,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
.custom-divide {
|
.custom-divide {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid #d1d5db;
|
border-bottom: 1px solid #d1d5db;
|
||||||
@@ -336,12 +436,12 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
|||||||
@apply text-white text-center;
|
@apply text-white text-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type='number'] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,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,18 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { withdrawDeposit } from "@/blockchain/buyerMethods";
|
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
import { useUser } from '@/composables/useUser';
|
||||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
import { ref, watch } from 'vue';
|
||||||
import { useEtherStore } from "@/store/ether";
|
import SpinnerComponent from '../ui/SpinnerComponent.vue';
|
||||||
import { storeToRefs } from "pinia";
|
import BalanceCard from './BalanceCard.vue';
|
||||||
import { ref, watch, onMounted } from "vue";
|
import TransactionCard from './TransactionCard.vue';
|
||||||
import SpinnerComponent from "../SpinnerComponent.vue";
|
|
||||||
import { decimalCount } from "@/utils/decimalCount";
|
|
||||||
import { debounce } from "@/utils/debounce";
|
|
||||||
import { getTokenByAddress } from "@/blockchain/addresses";
|
|
||||||
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
|
|
||||||
|
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
|
|
||||||
// props
|
// props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -21,84 +16,14 @@ const props = defineProps<{
|
|||||||
activeLockAmount: number;
|
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 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 callWithdraw = (amount: string) => {
|
||||||
const floating = ref<HTMLElement | null>(null);
|
emit('depositWithdrawn', amount);
|
||||||
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.sepolia
|
|
||||||
? "Etherscan"
|
|
||||||
: "Polygonscan";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const showInitialItems = (): void => {
|
const showInitialItems = (): void => {
|
||||||
@@ -106,46 +31,18 @@ const showInitialItems = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEtherscanUrl = (transactionHash: string): void => {
|
const openEtherscanUrl = (transactionHash: string): void => {
|
||||||
const networkUrl =
|
const networkUrl = user.network.value.blockExplorers?.default.url;
|
||||||
etherStore.networkName == NetworkEnum.sepolia
|
|
||||||
? "sepolia.etherscan.io"
|
|
||||||
: "mumbai.polygonscan.com";
|
|
||||||
const url = `https://${networkUrl}/tx/${transactionHash}`;
|
const url = `https://${networkUrl}/tx/${transactionHash}`;
|
||||||
window.open(url, "_blank");
|
window.open(url, '_blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMore = (): void => {
|
const loadMore = (): void => {
|
||||||
const itemsShowing = itemsToShow.value.length;
|
const itemsShowing = itemsToShow.value.length;
|
||||||
itemsToShow.value?.push(
|
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 changes
|
||||||
watch(props, async (): Promise<void> => {
|
watch(props, async (): Promise<void> => {
|
||||||
const itemsToShowQty = itemsToShow.value.length;
|
const itemsToShowQty = itemsToShow.value.length;
|
||||||
@@ -162,183 +59,31 @@ showInitialItems();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-container max-w-md" 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>
|
<SpinnerComponent width="8" height="8"></SpinnerComponent>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-container max-w-md" v-if="!loadingWalletTransactions">
|
<div class="main-container max-w-md" v-else>
|
||||||
<div
|
<BalanceCard
|
||||||
class="w-full bg-white p-4 sm:p-6 rounded-lg"
|
|
||||||
v-if="props.validDeposits.length > 0"
|
v-if="props.validDeposits.length > 0"
|
||||||
>
|
:valid-deposits="props.validDeposits"
|
||||||
<div class="flex justify-between items-center">
|
:active-lock-amount="activeLockAmount"
|
||||||
<div>
|
:selected-token="user.selectedToken.value"
|
||||||
<p class="text-sm leading-5 font-medium text-gray-600">
|
@withdraw="callWithdraw"
|
||||||
Saldo disponível
|
/>
|
||||||
</p>
|
|
||||||
<p class="text-xl leading-7 font-semibold text-gray-900">
|
|
||||||
{{ getRemaining() }} {{ etherStore.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)} ${
|
|
||||||
etherStore.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">
|
|
||||||
<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?url"
|
|
||||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
|
||||||
/>
|
|
||||||
<span class="last-release-info">Sacar</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-5">
|
|
||||||
<div v-show="isCollapsibleOpen" class="py-2 w-100">
|
|
||||||
<p class="text-sm leading-5 font-medium">Valor do saque</p>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name=""
|
|
||||||
@input="debounce(handleInputEvent, 500)($event)"
|
|
||||||
placeholder="0"
|
|
||||||
class="text-2xl text-gray-900 w-full outline-none"
|
|
||||||
v-model="withdrawAmount"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center" v-if="!validDecimals">
|
|
||||||
<span class="text-red-500 font-normal text-sm"
|
|
||||||
>Por favor utilize no máximo 2 casas decimais</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center" v-else-if="!validWithdrawAmount">
|
|
||||||
<span class="text-red-500 font-normal text-sm"
|
|
||||||
>Saldo insuficiente</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<hr v-show="isCollapsibleOpen" class="pb-3" />
|
|
||||||
<div
|
|
||||||
v-show="isCollapsibleOpen"
|
|
||||||
class="flex justify-between items-center"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
@click="[(isCollapsibleOpen = false)]"
|
|
||||||
class="text-black font-medium cursor-pointer"
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div
|
<TransactionCard
|
||||||
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?url"
|
|
||||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
|
||||||
/>
|
|
||||||
<span class="last-release-info">Sacar</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-full bg-white p-4 sm:p-6 rounded-lg"
|
|
||||||
v-for="item in itemsToShow"
|
v-for="item in itemsToShow"
|
||||||
:key="item.blockNumber"
|
:key="item.blockNumber"
|
||||||
>
|
:selected-token="user.selectedToken.value"
|
||||||
<div class="item-container">
|
:transaction="item"
|
||||||
<div class="flex flex-col self-start">
|
:network-name="user.network.value.name"
|
||||||
<span class="text-xs sm:text-sm leading-5 font-medium text-gray-600">
|
@open-explorer="openEtherscanUrl"
|
||||||
{{ getEventName(item.event) }}
|
/>
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="text-xl sm:text-xl leading-7 font-semibold text-gray-900"
|
|
||||||
>
|
|
||||||
{{ item.amount }}
|
|
||||||
{{ getTokenByAddress(item.token) }}
|
|
||||||
</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?url"
|
|
||||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex gap-2 justify-self-center w-full"
|
|
||||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
|
|
||||||
>
|
|
||||||
<RouterLink
|
|
||||||
:to="{
|
|
||||||
name: 'home',
|
|
||||||
force: true,
|
|
||||||
state: { lockID: item.transactionID },
|
|
||||||
}"
|
|
||||||
class="router-button"
|
|
||||||
>Continuar</RouterLink
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-center items-center w-full mt-2 gap-2"
|
class="flex flex-col justify-center items-center w-full mt-2 gap-2"
|
||||||
v-if="
|
v-if="
|
||||||
@@ -348,14 +93,14 @@ showInitialItems();
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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()"
|
@click="loadMore()"
|
||||||
>
|
>
|
||||||
Carregar mais
|
Carregar mais
|
||||||
</button>
|
</button>
|
||||||
<span class="text-gray-300">
|
<span class="text-gray-300 text-sm">
|
||||||
({{ itemsToShow.length }} de {{ props.walletTransactions.length }}
|
{{ itemsToShow.length }} de {{ props.walletTransactions.length }}
|
||||||
transações )
|
transações
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -366,62 +111,5 @@ showInitialItems();
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
/* Minimal styles - most styles moved to child components */
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-container {
|
|
||||||
@apply grid grid-cols-4 grid-flow-row items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10 w-auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last-release-info {
|
|
||||||
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
|
|
||||||
}
|
|
||||||
|
|
||||||
.router-button {
|
|
||||||
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.withdraw-button {
|
|
||||||
opacity: v-bind(withdrawButtonOpacity);
|
|
||||||
cursor: v-bind(withdrawButtonCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.md-view {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
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,140 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
|
||||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
|
||||||
import CustomModal from "@/components//CustomModal/CustomModal.vue";
|
|
||||||
import QRCode from "qrcode";
|
|
||||||
|
|
||||||
// props and store references
|
|
||||||
const props = defineProps({
|
|
||||||
sellerId: String,
|
|
||||||
amount: Number,
|
|
||||||
qrcode: String,
|
|
||||||
});
|
|
||||||
|
|
||||||
const windowSize = ref<number>(window.innerWidth);
|
|
||||||
const qrCode = ref<string>("");
|
|
||||||
const isPixValid = ref<boolean>(false);
|
|
||||||
const showWarnModal = ref<boolean>(true);
|
|
||||||
const releaseSignature = ref<string>("");
|
|
||||||
|
|
||||||
// Emits
|
|
||||||
const emit = defineEmits(["pixValidated"]);
|
|
||||||
|
|
||||||
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, 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"
|
|
||||||
>
|
|
||||||
<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">
|
|
||||||
{{ qrCode }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
alt="Copy PIX code"
|
|
||||||
src="@/assets/copyPix.svg?url"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
class="pt-2 lg:mb-5 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<CustomButton
|
|
||||||
:is-disabled="isPixValid == false"
|
|
||||||
:text="'Enviar para a rede'"
|
|
||||||
@button-clicked="emit('pixValidated', releaseSignature)"
|
|
||||||
/>
|
|
||||||
</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 max-w-screen-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,17 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { ref, computed } from 'vue';
|
||||||
import CustomButton from "../CustomButton/CustomButton.vue";
|
import { useUser } from '@/composables/useUser';
|
||||||
import { pixFormatValidation, postProcessKey } from "@/utils/pixKeyFormat";
|
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||||
import { useEtherStore } from "@/store/ether";
|
import { postProcessKey } from '@/utils/pixKeyFormat';
|
||||||
import { storeToRefs } from "pinia";
|
import { TokenEnum } from '@/model/NetworkEnum';
|
||||||
import { TokenEnum } from "@/model/NetworkEnum";
|
import { getTokenImage } from '@/utils/imagesPath';
|
||||||
import { getTokenImage } from "@/utils/imagesPath";
|
import { useOnboard } from '@web3-onboard/vue';
|
||||||
import { useOnboard } from "@web3-onboard/vue";
|
import ChevronDown from '@/assets/chevron.svg';
|
||||||
import ChevronDown from "@/assets/chevron.svg";
|
|
||||||
|
|
||||||
// Import the bank list
|
// Import the bank list
|
||||||
import bankList from "@/utils/files/isbpList.json";
|
import bankList from '@/utils/files/isbpList.json';
|
||||||
import type { Participant } from "@/utils/bbPay";
|
import type { Participant } from '@/utils/bbPay';
|
||||||
|
|
||||||
// Define Bank interface
|
// Define Bank interface
|
||||||
interface Bank {
|
interface Bank {
|
||||||
@@ -24,21 +23,20 @@ const tokenDropdownRef = ref<any>(null);
|
|||||||
const formRef = ref<HTMLFormElement | null>(null);
|
const formRef = ref<HTMLFormElement | null>(null);
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
const { walletAddress, selectedToken } = storeToRefs(etherStore);
|
const { walletAddress, selectedToken } = user;
|
||||||
|
|
||||||
const fullName = ref<string>("");
|
const offer = ref<string>('');
|
||||||
const offer = ref<string>("");
|
const identification = ref<string>('');
|
||||||
const identification = ref<string>("");
|
const account = ref<string>('');
|
||||||
const account = ref<string>("");
|
const branch = ref<string>('');
|
||||||
const branch = ref<string>("");
|
const accountType = ref<string>('');
|
||||||
const accountType = ref<string>("");
|
|
||||||
const selectTokenToggle = ref<boolean>(false);
|
const selectTokenToggle = ref<boolean>(false);
|
||||||
const savingsVariation = ref<string>("");
|
const savingsVariation = ref<string>('');
|
||||||
const errors = ref<{ [key: string]: string }>({});
|
const errors = ref<{ [key: string]: string }>({});
|
||||||
|
|
||||||
// Bank selection
|
// Bank selection
|
||||||
const bankSearchQuery = ref<string>("");
|
const bankSearchQuery = ref<string>('');
|
||||||
const showBankList = ref<boolean>(false);
|
const showBankList = ref<boolean>(false);
|
||||||
const selectedBank = ref<Bank | null>(null);
|
const selectedBank = ref<Bank | null>(null);
|
||||||
|
|
||||||
@@ -46,7 +44,7 @@ const filteredBanks = computed(() => {
|
|||||||
if (!bankSearchQuery.value) return [];
|
if (!bankSearchQuery.value) return [];
|
||||||
return bankList
|
return bankList
|
||||||
.filter((bank) =>
|
.filter((bank) =>
|
||||||
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase())
|
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase()),
|
||||||
)
|
)
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
});
|
});
|
||||||
@@ -58,7 +56,7 @@ const handleBankSelect = (bank: Bank) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(["approveTokens"]);
|
const emit = defineEmits(['approveTokens']);
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const connectAccount = async (): Promise<void> => {
|
const connectAccount = async (): Promise<void> => {
|
||||||
@@ -73,16 +71,16 @@ const handleSubmit = (e: Event): void => {
|
|||||||
|
|
||||||
const data: Participant = {
|
const data: Participant = {
|
||||||
offer: offer.value,
|
offer: offer.value,
|
||||||
fullName: fullName.value,
|
chainID: user.network.value.id,
|
||||||
identification: processedIdentification,
|
identification: processedIdentification,
|
||||||
bankIspb: selectedBank.value?.ISPB,
|
bankIspb: selectedBank.value?.ISPB,
|
||||||
accountType: accountType.value,
|
accountType: accountType.value,
|
||||||
account: account.value,
|
account: account.value,
|
||||||
branch: branch.value,
|
branch: branch.value,
|
||||||
savingsVariation: savingsVariation.value || "",
|
savingsVariation: savingsVariation.value || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
emit("approveTokens", data);
|
emit('approveTokens', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Token selection
|
// Token selection
|
||||||
@@ -91,7 +89,7 @@ const openTokenSelection = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectedToken = (token: TokenEnum): void => {
|
const handleSelectedToken = (token: TokenEnum): void => {
|
||||||
etherStore.setSelectedToken(token);
|
user.setSelectedToken(token);
|
||||||
selectTokenToggle.value = false;
|
selectTokenToggle.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -115,7 +113,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
<form ref="formRef" @submit="handleSubmit" class="main-container">
|
<form ref="formRef" @submit="handleSubmit" class="main-container">
|
||||||
<!-- Offer input -->
|
<!-- Offer input -->
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10 gap-4"
|
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg gap-4"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -186,25 +184,8 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Full name input -->
|
|
||||||
<div
|
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="fullName"
|
|
||||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
|
|
||||||
maxlength="60"
|
|
||||||
:class="{ 'text-xl font-medium': fullName }"
|
|
||||||
placeholder="Digite seu nome completo"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- CPF or CNPJ input -->
|
<!-- CPF or CNPJ input -->
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="identification"
|
v-model="identification"
|
||||||
@@ -217,9 +198,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bank selection -->
|
<!-- Bank selection -->
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -252,9 +231,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Account and Branch inputs -->
|
<!-- Account and Branch inputs -->
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
@@ -279,9 +256,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Account Type Selection -->
|
<!-- Account Type Selection -->
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<select
|
<select
|
||||||
@@ -304,7 +279,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
v-if="accountType === '2'"
|
v-if="accountType === '2'"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="savingsVariation"
|
v-model="savingsVariation"
|
||||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
class="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 }"
|
:class="{ 'text-xl font-medium': savingsVariation }"
|
||||||
placeholder="Variação da poupança"
|
placeholder="Variação da poupança"
|
||||||
required
|
required
|
||||||
@@ -322,6 +297,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
.custom-divide {
|
.custom-divide {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid #d1d5db;
|
border-bottom: 1px solid #d1d5db;
|
||||||
@@ -344,13 +320,13 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
|||||||
@apply text-white text-center;
|
@apply text-white text-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type='number'] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,164 +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";
|
|
||||||
|
|
||||||
import { useEtherStore } from "@/store/ether";
|
|
||||||
import { getTokenImage } from "@/utils/imagesPath";
|
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
import { useOnboard } from "@web3-onboard/vue";
|
|
||||||
|
|
||||||
// Store
|
|
||||||
const etherStore = useEtherStore();
|
|
||||||
const { walletAddress } = storeToRefs(etherStore);
|
|
||||||
|
|
||||||
// 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"]);
|
|
||||||
|
|
||||||
// Blockchain methods
|
|
||||||
const connectAccount = async (): Promise<void> => {
|
|
||||||
const { connectWallet } = useOnboard();
|
|
||||||
await connectWallet();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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="main-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="getTokenImage(etherStore.selectedToken)"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-900 text-lg w-fit" id="token">{{
|
|
||||||
etherStore.selectedToken
|
|
||||||
}}</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?url"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt="Ethereum image"
|
|
||||||
src="@/assets/ethereum.svg?url"
|
|
||||||
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
|
|
||||||
v-if="walletAddress"
|
|
||||||
:text="'Conectar carteira'"
|
|
||||||
@buttonClicked="emit('tokenBuy')"
|
|
||||||
/>
|
|
||||||
<CustomButton
|
|
||||||
v-if="!walletAddress"
|
|
||||||
:text="'Conectar carteira'"
|
|
||||||
@buttonClicked="connectAccount()"
|
|
||||||
/>
|
|
||||||
</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(["sendNetwork"]);
|
const emit = defineEmits(['sendNetwork']);
|
||||||
|
|
||||||
// props and store references
|
// props and store references
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -26,10 +26,8 @@ const props = defineProps({
|
|||||||
os tokens de volta.</span
|
os tokens de volta.</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-container sm:w-1/3">
|
<div class="main-container">
|
||||||
<div
|
<div class="flex flex-col w-full bg-white px-10 py-5 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white px-10 py-5 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<p>Tokens ofertados</p>
|
<p>Tokens ofertados</p>
|
||||||
<p class="text-2xl text-gray-900">
|
<p class="text-2xl text-gray-900">
|
||||||
@@ -59,6 +57,7 @@ const props = defineProps({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
.page {
|
.page {
|
||||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||||
}
|
}
|
||||||
@@ -79,12 +78,8 @@ p {
|
|||||||
@apply font-medium text-base;
|
@apply font-medium text-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
-moz-appearance: textfield;
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
}
|
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from "pinia";
|
import { ref, watch } from 'vue';
|
||||||
import { useEtherStore } from "@/store/ether";
|
import { useUser } from '@/composables/useUser';
|
||||||
import { ref, watch } from "vue";
|
import { onClickOutside } from '@vueuse/core';
|
||||||
import { onClickOutside } from "@vueuse/core";
|
import { getNetworkImage } from '@/utils/imagesPath';
|
||||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
import { Networks } from '@/config/networks';
|
||||||
import { getNetworkImage } from "@/utils/imagesPath";
|
import { useOnboard } from '@web3-onboard/vue';
|
||||||
import { Networks } from "@/model/Networks";
|
|
||||||
|
|
||||||
import { useOnboard } from "@web3-onboard/vue";
|
import ChevronDown from '@/assets/chevronDown.svg';
|
||||||
|
import TwitterIcon from '@/assets/twitterIcon.svg';
|
||||||
|
import LinkedinIcon from '@/assets/linkedinIcon.svg';
|
||||||
|
import GithubIcon from '@/assets/githubIcon.svg';
|
||||||
|
import { connectProvider } from '@/blockchain/provider';
|
||||||
|
import { DEFAULT_NETWORK } from '@/config/networks';
|
||||||
|
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||||
|
|
||||||
import ChevronDown from "@/assets/chevronDown.svg";
|
interface MenuOption {
|
||||||
import TwitterIcon from "@/assets/twitterIcon.svg";
|
label: string;
|
||||||
import LinkedinIcon from "@/assets/linkedinIcon.svg";
|
route?: string;
|
||||||
import GithubIcon from "@/assets/githubIcon.svg";
|
action?: () => void;
|
||||||
import { connectProvider } from "@/blockchain/provider";
|
showInDesktop?: boolean;
|
||||||
|
showInMobile?: boolean;
|
||||||
|
isDynamic?: boolean;
|
||||||
|
dynamicLabel?: () => string;
|
||||||
|
dynamicRoute?: () => string;
|
||||||
|
showVersion?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Store reference
|
// Use the new composable
|
||||||
const etherStore = useEtherStore();
|
const user = useUser();
|
||||||
|
const { walletAddress, sellerView, network } = user;
|
||||||
const { walletAddress, sellerView } = storeToRefs(etherStore);
|
|
||||||
|
|
||||||
const menuOpenToggle = ref<boolean>(false);
|
const menuOpenToggle = ref<boolean>(false);
|
||||||
const infoMenuOpenToggle = ref<boolean>(false);
|
const infoMenuOpenToggle = ref<boolean>(false);
|
||||||
@@ -37,27 +47,39 @@ const connnectWallet = async (): Promise<void> => {
|
|||||||
|
|
||||||
watch(connectedWallet, async (newVal: any) => {
|
watch(connectedWallet, async (newVal: any) => {
|
||||||
connectProvider(newVal.provider);
|
connectProvider(newVal.provider);
|
||||||
const addresses = await newVal.provider.request({ method: "eth_accounts" });
|
const addresses = await newVal.provider.request({ method: 'eth_accounts' });
|
||||||
etherStore.setWalletAddress(addresses.shift());
|
user.setWalletAddress(addresses.shift());
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(connectedChain, (newVal: any) => {
|
watch(connectedChain, (newVal: any) => {
|
||||||
etherStore.setNetworkId(newVal?.id);
|
// Check if connected chain is valid, otherwise default to Sepolia
|
||||||
|
if (
|
||||||
|
!newVal ||
|
||||||
|
!Object.values(Networks).some((network) => network.id === Number(newVal.id))
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
'Invalid or unsupported network detected, defaulting to Sepolia',
|
||||||
|
);
|
||||||
|
user.setNetwork(DEFAULT_NETWORK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
user.setNetworkById(newVal?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatWalletAddress = (): string => {
|
const formatWalletAddress = (): string => {
|
||||||
|
if (!walletAddress.value) throw new Error('Wallet not connected');
|
||||||
const walletAddressLength = walletAddress.value.length;
|
const walletAddressLength = walletAddress.value.length;
|
||||||
const initialText = walletAddress.value.substring(0, 5);
|
const initialText = walletAddress.value.substring(0, 5);
|
||||||
const finalText = walletAddress.value.substring(
|
const finalText = walletAddress.value.substring(
|
||||||
walletAddressLength - 4,
|
walletAddressLength - 4,
|
||||||
walletAddressLength
|
walletAddressLength,
|
||||||
);
|
);
|
||||||
return `${initialText}...${finalText}`;
|
return `${initialText}...${finalText}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnectUser = async (): Promise<void> => {
|
const disconnectUser = async (): Promise<void> => {
|
||||||
etherStore.setWalletAddress("");
|
user.setWalletAddress(null);
|
||||||
await disconnectWallet({ label: connectedWallet.value?.label || "" });
|
await disconnectWallet({ label: connectedWallet.value?.label || '' });
|
||||||
closeMenu();
|
closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,16 +87,24 @@ const closeMenu = (): void => {
|
|||||||
menuOpenToggle.value = false;
|
menuOpenToggle.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const networkChange = async (network: NetworkEnum): Promise<void> => {
|
const networkChange = async (network: NetworkConfig): Promise<void> => {
|
||||||
currencyMenuOpenToggle.value = false;
|
currencyMenuOpenToggle.value = false;
|
||||||
try {
|
|
||||||
await setChain({
|
// If wallet is connected, try to change chain in wallet
|
||||||
chainId: Networks[network].chainId,
|
if (connectedWallet.value) {
|
||||||
wallet: connectedWallet.value?.label || "",
|
const chainId = network.id.toString(16);
|
||||||
});
|
try {
|
||||||
etherStore.setNetworkId(network);
|
await setChain({
|
||||||
} catch (error) {
|
chainId: `0x${chainId}`,
|
||||||
console.log("Error changing network", error);
|
wallet: connectedWallet.value.label,
|
||||||
|
});
|
||||||
|
user.setNetwork(network);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error changing network', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no wallet connected, just update the network state
|
||||||
|
user.setNetwork(network);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,11 +119,83 @@ onClickOutside(currencyRef, () => {
|
|||||||
onClickOutside(infoMenuRef, () => {
|
onClickOutside(infoMenuRef, () => {
|
||||||
infoMenuOpenToggle.value = false;
|
infoMenuOpenToggle.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const infoMenuOptions: MenuOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Explorar Transações',
|
||||||
|
route: '/explore',
|
||||||
|
showInDesktop: true,
|
||||||
|
showInMobile: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Perguntas frequentes',
|
||||||
|
route: '/faq',
|
||||||
|
showInDesktop: true,
|
||||||
|
showInMobile: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Versões',
|
||||||
|
route: '/versions',
|
||||||
|
showInDesktop: true,
|
||||||
|
showInMobile: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const walletMenuOptions: MenuOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Quero vender',
|
||||||
|
isDynamic: true,
|
||||||
|
dynamicLabel: () => (sellerView.value ? 'Quero comprar' : 'Quero vender'),
|
||||||
|
dynamicRoute: () => (sellerView.value ? '/' : '/seller'),
|
||||||
|
showInDesktop: false,
|
||||||
|
showInMobile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Explorar Transações',
|
||||||
|
route: '/explore',
|
||||||
|
showInDesktop: false,
|
||||||
|
showInMobile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gerenciar Ofertas',
|
||||||
|
route: '/manage_bids',
|
||||||
|
showInDesktop: true,
|
||||||
|
showInMobile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Perguntas frequentes',
|
||||||
|
route: '/faq',
|
||||||
|
showInDesktop: false,
|
||||||
|
showInMobile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Versões',
|
||||||
|
route: '/versions',
|
||||||
|
showInDesktop: false,
|
||||||
|
showInMobile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Desconectar',
|
||||||
|
route: '/',
|
||||||
|
action: disconnectUser,
|
||||||
|
showInDesktop: true,
|
||||||
|
showInMobile: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleMenuOptionClick = (option: MenuOption): void => {
|
||||||
|
if (!option.action) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="z-20">
|
<header class="z-20">
|
||||||
<RouterLink :to="'/'" class="default-button">
|
<RouterLink
|
||||||
|
:to="'/'"
|
||||||
|
class="default-button flex items-center md:h-auto md:py-2 h-10 py-0"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
alt="P2Pix logo"
|
alt="P2Pix logo"
|
||||||
class="logo hidden md:inline-block"
|
class="logo hidden md:inline-block"
|
||||||
@@ -103,7 +205,7 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
alt="P2Pix logo"
|
alt="P2Pix logo"
|
||||||
class="logo inline-block md:hidden w-10/12"
|
class="logo inline-block md:hidden h-10"
|
||||||
width="40"
|
width="40"
|
||||||
height="40"
|
height="40"
|
||||||
src="@/assets/logo2.svg?url"
|
src="@/assets/logo2.svg?url"
|
||||||
@@ -115,13 +217,11 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
<button
|
<button
|
||||||
ref="infoMenuRef"
|
ref="infoMenuRef"
|
||||||
class="default-button hidden md:inline-block cursor-pointer"
|
class="default-button hidden md:inline-block cursor-pointer"
|
||||||
@click="
|
@click="[
|
||||||
[
|
(infoMenuOpenToggle = !infoMenuOpenToggle),
|
||||||
(infoMenuOpenToggle = !infoMenuOpenToggle),
|
(menuOpenToggle = false),
|
||||||
(menuOpenToggle = false),
|
(currencyMenuOpenToggle = false),
|
||||||
(currencyMenuOpenToggle = false),
|
]"
|
||||||
]
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
class="topbar-text topbar-link"
|
class="topbar-text topbar-link"
|
||||||
@@ -141,26 +241,45 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
>
|
>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div class="bg-white rounded-md z-10 -left-36 w-52">
|
<div class="bg-white rounded-md z-10 -left-36 w-52">
|
||||||
<div class="menu-button gap-2 px-4 rounded-md cursor-pointer">
|
<template
|
||||||
<span
|
v-for="(option, index) in infoMenuOptions.filter(
|
||||||
class="text-gray-900 py-4 text-end font-semibold text-sm"
|
(opt) => opt.showInDesktop,
|
||||||
>
|
)"
|
||||||
Documentação
|
:key="index"
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex justify-center">
|
|
||||||
<hr class="w-4/5" />
|
|
||||||
</div>
|
|
||||||
<RouterLink
|
|
||||||
:to="'/faq'"
|
|
||||||
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<span
|
<RouterLink
|
||||||
class="text-gray-900 py-4 text-end font-semibold text-sm whitespace-nowrap"
|
v-if="option.route"
|
||||||
|
:to="option.route"
|
||||||
|
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
|
||||||
>
|
>
|
||||||
Perguntas frequentes
|
<span
|
||||||
</span>
|
class="text-gray-900 py-4 text-end font-semibold text-sm whitespace-nowrap"
|
||||||
</RouterLink>
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-gray-900 py-4 text-end font-semibold text-sm"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
index <
|
||||||
|
infoMenuOptions.filter((opt) => opt.showInDesktop)
|
||||||
|
.length -
|
||||||
|
1
|
||||||
|
"
|
||||||
|
class="w-full flex justify-center"
|
||||||
|
>
|
||||||
|
<hr class="w-4/5" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<hr class="w-4/5" />
|
<hr class="w-4/5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -206,46 +325,28 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<RouterLink
|
|
||||||
:to="'/faq'"
|
|
||||||
v-if="!walletAddress"
|
|
||||||
class="default-button inline-block md:hidden"
|
|
||||||
>
|
|
||||||
FAQ
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="sellerView ? '/' : '/seller'"
|
:to="sellerView ? '/' : '/seller'"
|
||||||
class="default-button whitespace-nowrap w-40 sm:w-44 md:w-36 hidden md:inline-block"
|
class="default-button whitespace-nowrap w-40 sm:w-44 md:w-36 hidden md:inline-block"
|
||||||
>
|
>
|
||||||
<div class="topbar-text topbar-link text-center mx-auto inline-block">
|
<div class="topbar-text topbar-link text-center mx-auto inline-block">
|
||||||
{{ sellerView ? "Quero comprar" : "Quero vender" }}
|
{{ sellerView ? 'Quero comprar' : 'Quero vender' }}
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<div class="flex flex-col relative">
|
||||||
:to="sellerView ? '/' : '/seller'"
|
|
||||||
v-if="!walletAddress"
|
|
||||||
class="default-button sm:whitespace-normal whitespace-nowrap inline-block md:hidden w-40 sm:w-44 md:w-36"
|
|
||||||
>
|
|
||||||
<div class="topbar-text topbar-link text-center mx-auto inline-block">
|
|
||||||
{{ sellerView ? "Quero comprar" : "Quero vender" }}
|
|
||||||
</div>
|
|
||||||
</RouterLink>
|
|
||||||
<div class="flex flex-col relative" v-if="walletAddress">
|
|
||||||
<div
|
<div
|
||||||
ref="currencyRef"
|
ref="currencyRef"
|
||||||
class="top-bar-info cursor-pointer h-10 group hover:bg-gray-50 transition-all duration-500 ease-in-out"
|
class="top-bar-info cursor-pointer h-10 group hover:bg-gray-50 transition-all duration-500 ease-in-out"
|
||||||
:class="{ 'bg-gray-50': currencyMenuOpenToggle }"
|
:class="{ 'bg-gray-50': currencyMenuOpenToggle }"
|
||||||
@click="
|
@click="[
|
||||||
[
|
(currencyMenuOpenToggle = !currencyMenuOpenToggle),
|
||||||
(currencyMenuOpenToggle = !currencyMenuOpenToggle),
|
(menuOpenToggle = false),
|
||||||
(menuOpenToggle = false),
|
(infoMenuOpenToggle = false),
|
||||||
(infoMenuOpenToggle = false),
|
]"
|
||||||
]
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Choosed network image"
|
alt="Choosed network image"
|
||||||
:src="getNetworkImage(NetworkEnum[etherStore.networkName])"
|
:src="getNetworkImage(network.name)"
|
||||||
height="24"
|
height="24"
|
||||||
width="24"
|
width="24"
|
||||||
/>
|
/>
|
||||||
@@ -253,7 +354,7 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
class="default-text hidden sm:inline-block text-gray-50 group-hover:text-gray-900 transition-all duration-500 ease-in-out whitespace-nowrap text-ellipsis overflow-hidden"
|
class="default-text hidden sm:inline-block text-gray-50 group-hover:text-gray-900 transition-all duration-500 ease-in-out whitespace-nowrap text-ellipsis overflow-hidden"
|
||||||
:class="{ '!text-gray-900': currencyMenuOpenToggle }"
|
:class="{ '!text-gray-900': currencyMenuOpenToggle }"
|
||||||
>
|
>
|
||||||
{{ Networks[etherStore.networkName].chainName }}
|
{{ user.network.value.name || 'Invalid Chain' }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="transition-all duration-500 ease-in-out mt-1"
|
class="transition-all duration-500 ease-in-out mt-1"
|
||||||
@@ -275,20 +376,20 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
class="mt-2 bg-white rounded-md border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
class="mt-2 bg-white rounded-md border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(chainData, network) in Networks"
|
v-for="network in Networks"
|
||||||
:key="network"
|
:key="network.id"
|
||||||
class="menu-button p-4 gap-2 cursor-pointer hover:bg-gray-200 flex items-center !justify-start whitespace-nowrap transition-colors duration-150 ease-in-out"
|
class="menu-button p-4 gap-2 cursor-pointer hover:bg-gray-200 flex items-center !justify-start whitespace-nowrap transition-colors duration-150 ease-in-out"
|
||||||
@click="networkChange(network)"
|
@click="networkChange(network)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:alt="chainData.chainName + ' image'"
|
:alt="network.name + ' image'"
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
:src="getNetworkImage(NetworkEnum[network])"
|
:src="getNetworkImage(network.name)"
|
||||||
class="mr-2 ml-1"
|
class="mr-2 ml-1"
|
||||||
/>
|
/>
|
||||||
<span class="text-gray-900 font-semibold text-sm">
|
<span class="text-gray-900 font-semibold text-sm">
|
||||||
{{ chainData.chainName }}
|
{{ network.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
@@ -301,7 +402,7 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
v-if="!walletAddress"
|
v-if="!walletAddress"
|
||||||
class="border-amber-500 border-2 rounded default-button hidden md:inline-block"
|
class="border-amber-500 border-2 sm:rounded !rounded-lg default-button hidden md:inline-block"
|
||||||
@click="connnectWallet()"
|
@click="connnectWallet()"
|
||||||
>
|
>
|
||||||
Conectar carteira
|
Conectar carteira
|
||||||
@@ -309,7 +410,7 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
v-if="!walletAddress"
|
v-if="!walletAddress"
|
||||||
class="border-amber-500 border-2 rounded default-button inline-block md:hidden"
|
class="border-amber-500 border-2 sm:rounded !rounded-lg default-button inline-block md:hidden h-10"
|
||||||
@click="connnectWallet()"
|
@click="connnectWallet()"
|
||||||
>
|
>
|
||||||
Conectar
|
Conectar
|
||||||
@@ -320,13 +421,11 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
ref="walletAddressRef"
|
ref="walletAddressRef"
|
||||||
class="top-bar-info cursor-pointer h-10 group hover:bg-gray-50 transition-all duration-500 ease-in-out"
|
class="top-bar-info cursor-pointer h-10 group hover:bg-gray-50 transition-all duration-500 ease-in-out"
|
||||||
:class="{ 'bg-gray-50': menuOpenToggle }"
|
:class="{ 'bg-gray-50': menuOpenToggle }"
|
||||||
@click="
|
@click="[
|
||||||
[
|
(menuOpenToggle = !menuOpenToggle),
|
||||||
(menuOpenToggle = !menuOpenToggle),
|
(currencyMenuOpenToggle = false),
|
||||||
(currencyMenuOpenToggle = false),
|
(infoMenuOpenToggle = false),
|
||||||
(infoMenuOpenToggle = false),
|
]"
|
||||||
]
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<img alt="Account image" src="@/assets/account.svg?url" />
|
<img alt="Account image" src="@/assets/account.svg?url" />
|
||||||
<span
|
<span
|
||||||
@@ -355,23 +454,40 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
<div
|
<div
|
||||||
class="bg-white rounded-md z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
class="bg-white rounded-md z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
||||||
>
|
>
|
||||||
<RouterLink
|
<template
|
||||||
to="/manage_bids"
|
v-for="(option, index) in walletMenuOptions.filter(
|
||||||
class="redirect_button menu-button"
|
(opt) => opt.showInDesktop,
|
||||||
@click="closeMenu()"
|
)"
|
||||||
|
:key="index"
|
||||||
>
|
>
|
||||||
Gerenciar Ofertas
|
<RouterLink
|
||||||
</RouterLink>
|
v-if="option.route && !option.action"
|
||||||
<div class="w-full flex justify-center">
|
:to="option.route"
|
||||||
<hr class="w-4/5" />
|
class="redirect_button menu-button"
|
||||||
</div>
|
@click="closeMenu()"
|
||||||
<RouterLink
|
>
|
||||||
to="/"
|
{{ option.label }}
|
||||||
class="redirect_button menu-button"
|
</RouterLink>
|
||||||
@click="disconnectUser"
|
<RouterLink
|
||||||
>
|
v-else-if="option.route && option.action"
|
||||||
Desconectar
|
:to="option.route"
|
||||||
</RouterLink>
|
class="redirect_button menu-button"
|
||||||
|
@click="option.action"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</RouterLink>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
index <
|
||||||
|
walletMenuOptions.filter((opt) => opt.showInDesktop)
|
||||||
|
.length -
|
||||||
|
1
|
||||||
|
"
|
||||||
|
class="w-full flex justify-center"
|
||||||
|
>
|
||||||
|
<hr class="w-4/5" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,32 +499,49 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
v-show="menuOpenToggle"
|
v-show="menuOpenToggle"
|
||||||
class="mobile-menu fixed w-4/5 text-gray-900 inline-block md:hidden"
|
class="mobile-menu fixed w-4/5 text-gray-900 inline-block md:hidden"
|
||||||
>
|
>
|
||||||
<div class="pl-4 mt-2 h-full">
|
<div class="pl-4 h-full">
|
||||||
<div class="bg-white rounded-md z-10 h-full">
|
<div class="bg-white rounded-md z-10 h-full">
|
||||||
<div class="menu-button" @click="closeMenu()">
|
<template
|
||||||
<RouterLink
|
v-for="(option, index) in walletMenuOptions.filter(
|
||||||
:to="sellerView ? '/' : '/seller'"
|
(opt) => opt.showInMobile,
|
||||||
class="redirect_button mt-2"
|
)"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<div class="menu-button" @click="handleMenuOptionClick(option)">
|
||||||
|
<RouterLink
|
||||||
|
v-if="option.isDynamic"
|
||||||
|
:to="option.dynamicRoute ? option.dynamicRoute() : '/'"
|
||||||
|
class="redirect_button"
|
||||||
|
:class="{ 'mt-2': index === 0 }"
|
||||||
|
>
|
||||||
|
{{ option.dynamicLabel ? option.dynamicLabel() : option.label }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
v-else-if="option.route && !option.action"
|
||||||
|
:to="option.route"
|
||||||
|
class="redirect_button"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
v-else-if="option.route && option.action"
|
||||||
|
:to="option.route"
|
||||||
|
class="redirect_button"
|
||||||
|
@click.stop="option.action"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
index <
|
||||||
|
walletMenuOptions.filter((opt) => opt.showInMobile).length - 1
|
||||||
|
"
|
||||||
|
class="w-full flex justify-center"
|
||||||
>
|
>
|
||||||
{{ sellerView ? "Quero comprar" : "Quero vender" }}
|
<hr class="w-4/5" />
|
||||||
</RouterLink>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="w-full flex justify-center">
|
|
||||||
<hr class="w-4/5" />
|
|
||||||
</div>
|
|
||||||
<div class="menu-button" @click="closeMenu()">
|
|
||||||
<RouterLink to="/manage_bids" class="redirect_button">
|
|
||||||
Gerenciar Ofertas
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex justify-center">
|
|
||||||
<hr class="w-4/5" />
|
|
||||||
</div>
|
|
||||||
<div class="menu-button" @click="disconnectUser">
|
|
||||||
<RouterLink to="/" class="redirect_button">
|
|
||||||
Desconectar
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<hr class="w-4/5" />
|
<hr class="w-4/5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -442,22 +575,22 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
v-show="currencyMenuOpenToggle"
|
v-show="currencyMenuOpenToggle"
|
||||||
class="mobile-menu fixed w-4/5 text-gray-900 inline-block md:hidden"
|
class="mobile-menu fixed w-4/5 text-gray-900 inline-block md:hidden"
|
||||||
>
|
>
|
||||||
<div class="pl-4 mt-2 h-full">
|
<div class="pl-4 h-full">
|
||||||
<div class="bg-white rounded-md z-10 h-full">
|
<div class="bg-white rounded-md z-10 h-full">
|
||||||
<div
|
<div
|
||||||
v-for="(chainData, network) in Networks"
|
v-for="network in Networks"
|
||||||
:key="network"
|
:key="network.id"
|
||||||
class="menu-button gap-2 sm:px-4 rounded-md cursor-pointer py-2 px-4"
|
class="menu-button gap-2 sm:px-4 rounded-md cursor-pointer py-2 px-4"
|
||||||
@click="networkChange(network)"
|
@click="networkChange(network)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:alt="chainData.chainName + 'image'"
|
:alt="network.name + 'image'"
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
:src="getNetworkImage(NetworkEnum[network])"
|
:src="getNetworkImage(network.name)"
|
||||||
/>
|
/>
|
||||||
<span class="text-gray-900 py-4 text-end font-bold text-sm">
|
<span class="text-gray-900 py-4 text-end font-bold text-sm">
|
||||||
{{ chainData.chainName }}
|
{{ network.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,6 +600,7 @@ onClickOutside(infoMenuRef, () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
header {
|
header {
|
||||||
@apply flex flex-row justify-between w-full items-center;
|
@apply flex flex-row justify-between w-full items-center;
|
||||||
}
|
}
|
||||||
@@ -507,8 +641,14 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-menu {
|
.mobile-menu {
|
||||||
margin-top: 1400px;
|
top: 60px;
|
||||||
bottom: 0px;
|
right: 10px;
|
||||||
height: auto;
|
left: auto;
|
||||||
|
max-height: calc(100vh - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
</style>
|
</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">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string;
|
type: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const alertText = ref<string>("");
|
const alertText = ref<string>('');
|
||||||
const alertPaddingLeft = ref<string>("18rem");
|
const alertPaddingLeft = ref<string>('18rem');
|
||||||
|
|
||||||
if (props.type === "sell") {
|
if (props.type === 'sell') {
|
||||||
alertPaddingLeft.value = "30%";
|
alertPaddingLeft.value = '30%';
|
||||||
} else if (props.type === "buy") {
|
} else if (props.type === 'buy') {
|
||||||
alertPaddingLeft.value = "30%";
|
alertPaddingLeft.value = '30%';
|
||||||
} else if (props.type === "withdraw") {
|
} else if (props.type === 'withdraw') {
|
||||||
alertPaddingLeft.value = "40%";
|
alertPaddingLeft.value = '40%';
|
||||||
} else if (props.type === "redirect") {
|
} else if (props.type === 'redirect') {
|
||||||
alertPaddingLeft.value = "35%";
|
alertPaddingLeft.value = '35%';
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case "buy":
|
case 'buy':
|
||||||
alertText.value =
|
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;
|
break;
|
||||||
case "sell":
|
case 'sell':
|
||||||
alertText.value =
|
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;
|
break;
|
||||||
case "redirect":
|
case 'redirect':
|
||||||
alertText.value = "Existe uma compra em aberto. Continuar?";
|
alertText.value = 'Existe uma compra em aberto. Continuar?';
|
||||||
break;
|
break;
|
||||||
case "withdraw":
|
case 'withdraw':
|
||||||
alertText.value = "Tudo certo! Saque realizado com sucesso!";
|
alertText.value = 'Tudo certo! Saque realizado com sucesso!';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
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>
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isRedirectModal: Boolean,
|
isRedirectModal: Boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
const modalColor = ref<string>("white");
|
const modalColor = ref<string>('white');
|
||||||
const modalHeight = ref<string>("250px");
|
const modalHeight = ref<string>('250px');
|
||||||
const pFontSize = ref<string>("16px");
|
const pFontSize = ref<string>('16px');
|
||||||
|
|
||||||
if (props.isRedirectModal) {
|
if (props.isRedirectModal) {
|
||||||
modalColor.value = "rgba(251, 191, 36, 1)";
|
modalColor.value = 'rgba(251, 191, 36, 1)';
|
||||||
modalHeight.value = "150px";
|
modalHeight.value = '150px';
|
||||||
pFontSize.value = "20px";
|
pFontSize.value = '20px';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="modal-overlay inset-0 fixed justify-center backdrop-blur-sm sm:backdrop-blur-none"
|
class="modal-overlay inset-0 fixed hidden md:block justify-center backdrop-blur-sm sm:backdrop-blur-none"
|
||||||
v-if="!isRedirectModal"
|
v-if="!isRedirectModal"
|
||||||
>
|
>
|
||||||
<div class="modal px-5 text-center">
|
<div class="modal px-5 text-center">
|
||||||
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,13 +12,11 @@ const props = defineProps({
|
|||||||
<span
|
<span
|
||||||
class="text font-bold sm:text-3xl text-2xl sm:max-w-[29rem] max-w-[20rem]"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-container max-w-md">
|
<div class="main-container max-w-md">
|
||||||
<div
|
<div class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg">
|
||||||
class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg border-y-10"
|
|
||||||
>
|
|
||||||
<div
|
<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"
|
class="flex flex-col text-center justify-center w-full items-center p-2 px-3 rounded-3xl lg:min-w-fit gap-1"
|
||||||
>
|
>
|
||||||
@@ -38,6 +36,7 @@ const props = defineProps({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
.custom-divide {
|
.custom-divide {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid #d1d5db;
|
border-bottom: 1px solid #d1d5db;
|
||||||
@@ -60,12 +59,12 @@ const props = defineProps({
|
|||||||
@apply text-white text-center;
|
@apply text-white text-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type='number'] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -10,10 +10,10 @@ const getCustomClass = () => {
|
|||||||
return [
|
return [
|
||||||
`w-${props.width}`,
|
`w-${props.width}`,
|
||||||
`h-${props.height}`,
|
`h-${props.height}`,
|
||||||
`fill-white`,
|
'fill-white',
|
||||||
"text-gray-200",
|
'text-gray-200',
|
||||||
"animate-spin",
|
'animate-spin',
|
||||||
"dark:text-gray-600",
|
'dark:text-gray-600',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
</script>
|
</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;
|
||||||
14
src/main.ts
@@ -1,14 +1,12 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from 'vue';
|
||||||
import App from "./App.vue";
|
import App from './App.vue';
|
||||||
import router from "./router";
|
import router from './router';
|
||||||
import { createPinia } from "pinia";
|
|
||||||
|
|
||||||
import "./assets/main.css";
|
import './assets/main.css';
|
||||||
import "./assets/transitions.css";
|
import './assets/transitions.css';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(createPinia());
|
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount('#app');
|
||||||
|
|||||||
6
src/model/AppVersion.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface AppVersion {
|
||||||
|
tag: string;
|
||||||
|
ipfsHash: string;
|
||||||
|
releaseDate: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface Bank {
|
|
||||||
COMPE: string;
|
|
||||||
ISPB: string;
|
|
||||||
longName: string;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
export type Faq = Section[];
|
export type Faq = Section[];
|
||||||
|
|
||||||
export type Section = {
|
type Section = {
|
||||||
name: string;
|
name: string;
|
||||||
items: Question[];
|
items: Question[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Question = {
|
type Question = {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
isOpen?: boolean;
|
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,9 +1,10 @@
|
|||||||
export enum NetworkEnum {
|
import type { Chain, ChainContract } from 'viem';
|
||||||
sepolia = 11155111,
|
|
||||||
rootstock = 31,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TokenEnum {
|
export enum TokenEnum {
|
||||||
BRZ = "BRZ",
|
BRZ = 'BRZ',
|
||||||
// BRX = 'BRX'
|
// BRX = 'BRX'
|
||||||
}
|
}
|
||||||
|
export type NetworkConfig = Chain & {
|
||||||
|
tokens: Record<TokenEnum, ChainContract>;
|
||||||
|
subgraphUrls: string[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
|
||||||
|
|
||||||
export const Networks = {
|
|
||||||
[NetworkEnum.sepolia]: {
|
|
||||||
chainId: "0xAA36A7",
|
|
||||||
chainName: "Sepolia Testnet",
|
|
||||||
},
|
|
||||||
[NetworkEnum.rootstock]: {
|
|
||||||
chainId: "0x1F",
|
|
||||||
chainName: "Rootstock Testnet",
|
|
||||||
rpcUrls: ["https://public-node.testnet.rsk.co/"],
|
|
||||||
iconUrls: [
|
|
||||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAPOSURBVHgBxVhNUhpBFH6vGdxp4S4LoSYnEE8gnEA4AbpMJUQ4gXgCRJK1egLxBOIJJCdwJElVllMu49Cd1z04zD/dY1H5qihmut/M93VPv59uhHdAXFaPAaEDgA2/BaeA4hq/zG+gIBAKQoyr9yshid4Jdn+2oQAYFIC4rA2zxUhgS3yrDqEAjGdIDD/YYG09aRl7L7vYd10wgPkMlcoNfdvtFhjCXJBAeyO2S5gLQuFo25bEIxjCCt8oN2Z46I+Mu4A4SbjwojQBi1+BDl5LP+JNYlhtQRmPsjjQN1ILldwY7JTXOuD9bWL/jxO8dFy7oL9TyMcIu/PeSghxlLduQUA9jwPXiAk98HLw5jFiaFfAEjRLImPR0qi7z+2VmArZ7zzqcDAS01ljCKqf7QSjxb7jKkIhTohu6rOCq64RjsNiFEo7x7ocSNMvlddhPWb0CQ6gAAw4HKZpKGFDcWhzSEG6kbQCm4dLbi9m+XlpBTHea2D31zTSNtxrAGMNdcP5FPuxfhlKdCHgASUJxcd7zUcobkAPXvkzWGyf7uVCt2M2DtkMljaHSxu92WWLAz8OjWsD+juD/4tzcpqBSh3yQrmwoNFFMZNuDB7bJRsp/hzMMQqeT+NQ96KtNEBK+SG+23XgHgUyy8FPjpPozy3M4sZwh1/nLRMOK26Mn50Z5IHjA6XkBugJSn1XHkeBbK8dJsxsl0jMEOUpm0o9+gkX+7+TI0E+0x6Hsk0ijyNYQ/4OAqWn2aF+5cLxEoRq6idqtyEPtFhp/XyMNI2p9ADFUc/iYL5h7YzEXEEyptj04mvVHxkGP4F8MS4sWDsqRr4DbyGZRiIcqCKtpRMYeTMcpVVAFewqMVPSjUkMVQTBp6BPVKeiTqN65E0qP1AvIArWC98qcQsms39oDeBEtoXFKFgLbQ76ZKiXiRH2E01UF9Go+kGDh32/LWHZAD2OQ7mGdLO4ndrqWaHZyNyD6XJUWEq6yIQqReOweCe49ivD2DNUIutjJgXpHwyUtyPbY/IMWehfBA0IZxQSQoW9rKXL+ltq0oKqYC+RB6yLKys4xEw/Idde5R02cTGOcgh1LSNnid+nihIqcN0tr48MhL89L2uoG+Dqv5Px/IwqAhkqnEi296M1OyLPqVCgdKhcuKNjlUnQL4X78cRk1E1JlMkBME1sFE0gRrRJZGs3iT44bRZP5z0wQJHzIZMMbpztN1t+FDhsMBe0YNfatimHDetgLGiZGkYapqPwYt6YIAWPDYI9fSrETfjkwwSFT2EVrV/USY+r+/GGNp2I7zoW/gdR9aOdZ/lPGgAAAABJRU5ErkJggg==",
|
|
||||||
],
|
|
||||||
nativeCurrency: {
|
|
||||||
name: "tRBTC",
|
|
||||||
symbol: "tRBTC",
|
|
||||||
decimals: 18,
|
|
||||||
},
|
|
||||||
blockExplorerUrls: ["https://explorer.testnet.rootstock.io/"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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 = {
|
export type UnreleasedLock = {
|
||||||
lockID: string;
|
lockID: bigint;
|
||||||
pix: Pix;
|
sellerAddress: Address;
|
||||||
|
tokenAddress: Address;
|
||||||
|
amount: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { NetworkEnum } from "./NetworkEnum";
|
import type { Address } from 'viem';
|
||||||
|
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||||
|
|
||||||
export type ValidDeposit = {
|
export type ValidDeposit = {
|
||||||
token: string;
|
token: Address;
|
||||||
blockNumber: number;
|
blockNumber: number;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
seller: string;
|
seller: Address;
|
||||||
pixKey: string;
|
participantID: string;
|
||||||
network: NetworkEnum;
|
network: NetworkConfig;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import type { LockStatus } from '@/model/LockStatus';
|
||||||
|
import type { Address } from 'viem';
|
||||||
|
|
||||||
export type WalletTransaction = {
|
export type WalletTransaction = {
|
||||||
token: string;
|
token?: Address;
|
||||||
blockNumber: number;
|
blockNumber: number;
|
||||||
|
blockTimestamp?: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
seller: string;
|
seller: string;
|
||||||
buyer: string;
|
buyer: string;
|
||||||
event: string;
|
event: string;
|
||||||
lockStatus: number;
|
lockStatus?: LockStatus;
|
||||||
transactionHash: string;
|
transactionHash: string;
|
||||||
transactionID?: string;
|
transactionID?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
export const MockEvents = [
|
|
||||||
{
|
|
||||||
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,45 +0,0 @@
|
|||||||
import type { ValidDeposit } from "../ValidDeposit";
|
|
||||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
|
||||||
|
|
||||||
export const MockValidDeposits: ValidDeposit[] = [
|
|
||||||
{
|
|
||||||
blockNumber: 1,
|
|
||||||
token: "1",
|
|
||||||
remaining: 70,
|
|
||||||
seller: "mockedSellerAddress",
|
|
||||||
pixKey: "123456789",
|
|
||||||
network: NetworkEnum.sepolia,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
blockNumber: 2,
|
|
||||||
token: "2",
|
|
||||||
remaining: 200,
|
|
||||||
seller: "mockedSellerAddress",
|
|
||||||
pixKey: "123456789",
|
|
||||||
network: NetworkEnum.sepolia,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
blockNumber: 3,
|
|
||||||
token: "3",
|
|
||||||
remaining: 1250,
|
|
||||||
seller: "mockedSellerAddress",
|
|
||||||
pixKey: "123456789",
|
|
||||||
network: NetworkEnum.sepolia,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
blockNumber: 4,
|
|
||||||
token: "4",
|
|
||||||
remaining: 4000,
|
|
||||||
seller: "mockedSellerAddress",
|
|
||||||
pixKey: "123456789",
|
|
||||||
network: NetworkEnum.sepolia,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
blockNumber: 5,
|
|
||||||
token: "5",
|
|
||||||
remaining: 2000,
|
|
||||||
seller: "mockedSellerAddress",
|
|
||||||
pixKey: "123456789",
|
|
||||||
network: NetworkEnum.sepolia,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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 {
|
||||||
import HomeView from "@/views/HomeView.vue";
|
createRouter,
|
||||||
import FaqView from "@/views/FaqView.vue";
|
createWebHistory,
|
||||||
import ManageBidsView from "@/views/ManageBidsView.vue";
|
createWebHashHistory,
|
||||||
import SellerView from "@/views/SellerView.vue";
|
} 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({
|
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: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: '/',
|
||||||
name: "home",
|
name: 'home',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:lockID",
|
path: '/:lockID',
|
||||||
name: "redirect buy",
|
name: 'redirect buy',
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/seller",
|
path: '/seller',
|
||||||
name: "seller",
|
name: 'seller',
|
||||||
component: SellerView,
|
component: SellerView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/manage_bids",
|
path: '/manage_bids',
|
||||||
name: "manage bids",
|
name: 'manage bids',
|
||||||
component: ManageBidsView,
|
component: ManageBidsView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/faq",
|
path: '/faq',
|
||||||
name: "faq",
|
name: 'faq',
|
||||||
component: FaqView,
|
component: FaqView,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/explore',
|
||||||
|
name: 'explore',
|
||||||
|
component: ExploreView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/versions',
|
||||||
|
name: 'versions',
|
||||||
|
component: VersionsView,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
4
src/shims-vue.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
declare module "*.vue" {
|
declare module '*.vue' {
|
||||||
import { DefineComponent } from "vue";
|
import { DefineComponent } from 'vue';
|
||||||
const component: DefineComponent;
|
const component: DefineComponent;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { NetworkEnum, TokenEnum } from "../model/NetworkEnum";
|
|
||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
|
||||||
import type { Participant } from "../utils/bbPay";
|
|
||||||
import { defineStore } from "pinia";
|
|
||||||
|
|
||||||
export const useEtherStore = defineStore("ether", {
|
|
||||||
state: () => ({
|
|
||||||
walletAddress: "",
|
|
||||||
balance: "",
|
|
||||||
networkName: NetworkEnum.sepolia,
|
|
||||||
selectedToken: TokenEnum.BRZ,
|
|
||||||
loadingLock: false,
|
|
||||||
sellerView: false,
|
|
||||||
depositsValidList: [] as ValidDeposit[],
|
|
||||||
loadingWalletTransactions: false,
|
|
||||||
loadingNetworkLiquidity: false,
|
|
||||||
seller: {} as Participant,
|
|
||||||
sellerId: "",
|
|
||||||
}),
|
|
||||||
actions: {
|
|
||||||
setWalletAddress(walletAddress: string) {
|
|
||||||
this.walletAddress = walletAddress;
|
|
||||||
},
|
|
||||||
setBalance(balance: string) {
|
|
||||||
this.balance = balance;
|
|
||||||
},
|
|
||||||
setSelectedToken(token: TokenEnum) {
|
|
||||||
this.selectedToken = token;
|
|
||||||
},
|
|
||||||
setNetworkId(networkName: NetworkEnum) {
|
|
||||||
this.networkName = Number(networkName);
|
|
||||||
},
|
|
||||||
setLoadingLock(isLoadingLock: boolean) {
|
|
||||||
this.loadingLock = isLoadingLock;
|
|
||||||
},
|
|
||||||
setSellerView(sellerView: boolean) {
|
|
||||||
this.sellerView = sellerView;
|
|
||||||
},
|
|
||||||
setDepositsValidList(depositsValidList: ValidDeposit[]) {
|
|
||||||
this.depositsValidList = depositsValidList;
|
|
||||||
},
|
|
||||||
setLoadingWalletTransactions(isLoadingWalletTransactions: boolean) {
|
|
||||||
this.loadingWalletTransactions = isLoadingWalletTransactions;
|
|
||||||
},
|
|
||||||
setLoadingNetworkLiquidity(isLoadingNetworkLiquidity: boolean) {
|
|
||||||
this.loadingNetworkLiquidity = isLoadingNetworkLiquidity;
|
|
||||||
},
|
|
||||||
setSeller(seller: Participant) {
|
|
||||||
this.seller = seller;
|
|
||||||
},
|
|
||||||
setSellerId(sellerId: string) {
|
|
||||||
this.sellerId = sellerId;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
getValidDepositByWalletAddress: (state) => {
|
|
||||||
return (walletAddress: string) =>
|
|
||||||
state.depositsValidList
|
|
||||||
.filter((deposit) => deposit.seller == walletAddress)
|
|
||||||
.sort((a, b) => {
|
|
||||||
return b.blockNumber - a.blockNumber;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
process() {
|
process() {
|
||||||
return {
|
return {
|
||||||
code: `module.exports = {};`,
|
code: 'module.exports = {};',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getCacheKey() {
|
getCacheKey() {
|
||||||
// The output is always the same.
|
// 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 };
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { it, expect, vi, type Mock } from "vitest";
|
|
||||||
import { debounce } from "../debounce";
|
|
||||||
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
describe("debounce function test", () => {
|
|
||||||
let mockFunction: Mock;
|
|
||||||
let debounceFunction: Function;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFunction = vi.fn();
|
|
||||||
debounceFunction = debounce(mockFunction, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debounce function will be executed just once", () => {
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
debounceFunction();
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.runAllTimers();
|
|
||||||
|
|
||||||
expect(mockFunction).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { it, expect } from "vitest";
|
|
||||||
import { decimalCount } from "../decimalCount";
|
|
||||||
|
|
||||||
describe("decimalCount function test", () => {
|
|
||||||
it("decimalCount should return length 1 of decimal", () => {
|
|
||||||
expect(decimalCount("4.1")).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decimalCount should return length 0 because no decimal found", () => {
|
|
||||||
expect(decimalCount("5")).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { MockValidDeposits } from "@/model/mock/ValidDepositMock";
|
|
||||||
import { it, expect, vi } from "vitest";
|
|
||||||
import { verifyNetworkLiquidity } from "../networkLiquidity";
|
|
||||||
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
describe("verifyNetworkLiquidity function test", () => {
|
|
||||||
it("verifyNetworkLiquidity should return an element from valid deposit list when searching for other deposits", () => {
|
|
||||||
const liquidityElement = verifyNetworkLiquidity(
|
|
||||||
MockValidDeposits[0].remaining,
|
|
||||||
"strangeWalletAddress",
|
|
||||||
MockValidDeposits
|
|
||||||
);
|
|
||||||
expect(liquidityElement).toEqual(MockValidDeposits[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("verifyNetworkLiquidity should return undefined when all deposits on valid deposit list match connected wallet addres", () => {
|
|
||||||
const liquidityElement = verifyNetworkLiquidity(
|
|
||||||
MockValidDeposits[0].remaining,
|
|
||||||
MockValidDeposits[0].seller,
|
|
||||||
[MockValidDeposits[0]]
|
|
||||||
);
|
|
||||||
expect(liquidityElement).toEqual(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface Participant {
|
export interface Participant {
|
||||||
offer: string;
|
offer: string;
|
||||||
fullName: string;
|
chainID: number;
|
||||||
identification: string;
|
identification: string;
|
||||||
bankIspb?: string;
|
bankIspb?: string;
|
||||||
accountType: string;
|
accountType: string;
|
||||||
@@ -9,13 +9,12 @@ export interface Participant {
|
|||||||
savingsVariation?: string;
|
savingsVariation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParticipantWithID extends Participant {
|
interface ParticipantWithID extends Participant {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Offer {
|
export interface Offer {
|
||||||
amount: number;
|
amount: number;
|
||||||
lockId: string;
|
|
||||||
sellerId: string;
|
sellerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,35 +22,56 @@ export interface Offer {
|
|||||||
// https://apoio.developers.bb.com.br/sandbox/spec/665797498bb48200130fc32c
|
// https://apoio.developers.bb.com.br/sandbox/spec/665797498bb48200130fc32c
|
||||||
|
|
||||||
export const createParticipant = async (participant: Participant) => {
|
export const createParticipant = async (participant: Participant) => {
|
||||||
const response = await fetch(`${process.env.VUE_APP_API_URL}/participants`, {
|
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/register`, {
|
||||||
method: "PUT",
|
method: 'POST',
|
||||||
body: JSON.stringify(participant),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
chainID: participant.chainID,
|
||||||
|
tipoDocumento: 1,
|
||||||
|
numeroDocumento: participant.identification,
|
||||||
|
numeroConta: participant.account,
|
||||||
|
numeroAgencia: participant.branch,
|
||||||
|
tipoConta: participant.accountType,
|
||||||
|
codigoIspb: participant.bankIspb,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error creating participant: ${response.statusText}`);
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return { ...participant, id: data.id } as ParticipantWithID;
|
if (data.errors || data.erros) {
|
||||||
|
throw new Error(`Error creating participant: ${JSON.stringify(data)}`);
|
||||||
|
}
|
||||||
|
return { ...participant, id: data.numeroParticipante } as ParticipantWithID;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSolicitation = async (offer: Offer) => {
|
export const createSolicitation = async (offer: Offer) => {
|
||||||
const response = await fetch(`${process.env.VUE_APP_API_URL}/solicitation`, {
|
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/request`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: JSON.stringify(offer),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: offer.amount,
|
||||||
|
pixTarget: offer.sellerId.split('-').pop(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSolicitation = async (id: string) => {
|
export const getSolicitation = async (
|
||||||
|
id: bigint,
|
||||||
|
): Promise<{ pixTimestamp: `0x${string}`; signature: `0x${string}` }> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${process.env.VUE_APP_API_URL}/solicitation/${id}`
|
`${import.meta.env.VITE_APP_API_URL}/release/${id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const obj: any = response.json();
|
const obj = await response.json();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: obj.numeroSolicitacao,
|
pixTimestamp: obj.pixTimestamp,
|
||||||
lockId: obj.codigoConciliacaoSolicitacao,
|
signature: obj.signature,
|
||||||
amount: obj.valorSolicitacao,
|
|
||||||
qrcode: obj.pix.textoQrCode,
|
|
||||||
status: obj.valorSomatorioPagamentosEfetivados >= obj.valorSolicitacao,
|
|
||||||
signature: obj.assinatura,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const decimalCount = (numStr: string): number => {
|
export const decimalCount = (numStr: string): number => {
|
||||||
if (numStr.includes(".")) {
|
if (numStr.includes('.')) {
|
||||||
return numStr.split(".")[1].length;
|
return numStr.split('.')[1].length;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "1. Como Começar",
|
"name": "1. Como Começar",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"title": "O que é uma carteira (wallet)?",
|
"title": "O que é uma carteira (wallet)?",
|
||||||
"content": "Cripto-wallet é um software que armazena criptomoedas e criptoativos de forma segura. Existem também dispositivos físicos chamados de cold/hard wallets. Com a carteira-cripto você consegue comprar e vender tokens sem a necessidade de uma corretora (exchange), por exemplo. Cripto-ativos não ficam de fato armazenados nessa carteira, mas sim as informações que possibilitam acessá-los numa blockchain, como suas chaves pública e privada.\n<br>\n\n* **O que é um endereço de carteira (wallet address)?**\n Sequência alfanumérica que permite que você envie e receba suas criptomoedas em segurança. Por exemplo: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy.\n\n* **Como conectar sua carteira ao p2pix?**\n Clique em 'Conectar Carteira'. Escolha qual wallet ou ponte (bridge) utilizará para se conectar (MetaMask ou WalletConnect). Ao clicar, sua carteira se abrirá. Caso seja seu 1º acesso, certifique-se de que permitiu sua carteira a se conectar com nossa plataforma. \n\n* **Como instalar MetaMask no Google Chrome?** \n MetaMask é uma web3 wallet que pode ser adicionada como extensão no seu browser (navegador). Para instalar esse plugin (add-on) no Google Chrome, basta você acessar a Chrome Web Store e pesquisar “MetaMask”. Certifique-se de que é a versão legítima da carteira pelo número de downloads (bastante alto, pois é a maior cold wallet da web3), então é só clicar em “Adicionar ao Chrome”. Depois da instalação ser concluída, basta seguir o passo-a-passo proposto na interface da MetaMask para a criação da conta, login e senha. O caminho para a instalação em outros browsers como Firefox, Opera, entre outros navegadores, é o mesmo. \n\n<br>\nNota: Não esqueça de guardar sua seed phrase em um local seguro! Sem ela é impossível recuperar o acesso à sua carteira (caso perca sua senha) e também aos seus cripto ativos, eles serão perdidos - literalmente - para sempre.\n"
|
"content": "Cripto-wallet é um software que armazena criptomoedas e criptoativos de forma segura. Existem também dispositivos físicos chamados de cold/hard wallets. Com a carteira-cripto você consegue comprar e vender tokens sem a necessidade de uma corretora (exchange), por exemplo. Cripto-ativos não ficam de fato armazenados nessa carteira, mas sim as informações que possibilitam acessá-los numa blockchain, como suas chaves pública e privada.\n<br>\n\n* **O que é um endereço de carteira (wallet address)?**\n Sequência alfanumérica que permite que você envie e receba suas criptomoedas em segurança. Por exemplo: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy.\n\n* **Como conectar sua carteira ao p2pix?**\n Clique em 'Conectar Carteira'. Escolha qual wallet ou ponte (bridge) utilizará para se conectar (MetaMask ou WalletConnect). Ao clicar, sua carteira se abrirá. Caso seja seu 1º acesso, certifique-se de que permitiu sua carteira a se conectar com nossa plataforma. \n\n* **Como instalar MetaMask no Google Chrome?** \n MetaMask é uma web3 wallet que pode ser adicionada como extensão no seu browser (navegador). Para instalar esse plugin (add-on) no Google Chrome, basta você acessar a Chrome Web Store e pesquisar “MetaMask”. Certifique-se de que é a versão legítima da carteira pelo número de downloads (bastante alto, pois é a maior cold wallet da web3), então é só clicar em “Adicionar ao Chrome”. Depois da instalação ser concluída, basta seguir o passo-a-passo proposto na interface da MetaMask para a criação da conta, login e senha. O caminho para a instalação em outros browsers como Firefox, Opera, entre outros navegadores, é o mesmo. \n\n<br>\nNota: Não esqueça de guardar sua seed phrase em um local seguro! Sem ela é impossível recuperar o acesso à sua carteira (caso perca sua senha) e também aos seus cripto ativos, eles serão perdidos - literalmente - para sempre.\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "O que é uma rede (network)?",
|
"title": "O que é uma rede (network)?",
|
||||||
"content": "Criptomoedas são transacionadas em redes de computadores, nas blockchains é que ficam armazenadas as informações sobre suas transações, não em um servidor. Existem [diversas redes no ecossistema cripto](https://www.gemini.com/cryptopedia/blockchain-technology-explained), como a rede do Bitcoin, da Ethereum, para citar as duas mais conhecidas. \n<br>\n\n* **Como selecionar a rede?**\nVocê deve ficar atento a rede que está selecionada em sua carteira. Na MetaMask, escolha a você escolhe a rede que vai operar clicando na primeira caixa ao lado do seu avatar:\n\nCaso você copie um endereço na sua MetaMask estando selecionada a Rede Ethereum Mainnet e queira operar na Polygon, seus cripto ativos serão perdidos. \n\n* **O que é taxa de rede (gas fee)?**\nCusto de transação do blockchain. Você paga essa taxa para a rede que vai remunerar os validadores. É uma forma de incentivar os validadores a dar continuidade nos seus serviços. "
|
"content": "Criptomoedas são transacionadas em redes de computadores, nas blockchains é que ficam armazenadas as informações sobre suas transações, não em um servidor. Existem [diversas redes no ecossistema cripto](https://www.gemini.com/cryptopedia/blockchain-technology-explained), como a rede do Bitcoin, da Ethereum, para citar as duas mais conhecidas. \n<br>\n\n* **Como selecionar a rede?**\nVocê deve ficar atento a rede que está selecionada em sua carteira. Na MetaMask, escolha a você escolhe a rede que vai operar clicando na primeira caixa ao lado do seu avatar:\n\nCaso você copie um endereço na sua MetaMask estando selecionada a Rede Ethereum Mainnet e queira operar na Polygon, seus cripto ativos serão perdidos. \n\n* **O que é taxa de rede (gas fee)?**\nCusto de transação do blockchain. Você paga essa taxa para a rede que vai remunerar os validadores. É uma forma de incentivar os validadores a dar continuidade nos seus serviços. "
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2. Comprar tokens",
|
"name": "2. Comprar tokens",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"title": "Como comprar?",
|
"title": "Como comprar?",
|
||||||
"content": "1. Conectar wallet (MetaMask ou WalletConnect)\n2. Digitar amount (quantidade);\n3. Solicitar token (criptomoeda);\n4. Identificar vendedor (da lista de pessoas);\n * Escanear QRCode (ou acrescenta chave manualmente);\n * Colar e2eID (identificação end-to-end) a.k.a. código de identificação (API Pix / comprovante bancário);\n5. Receber na carteira (wallet conectada."
|
"content": "1. Conectar wallet (MetaMask ou WalletConnect)\n2. Digitar amount (quantidade);\n3. Solicitar token (criptomoeda);\n4. Identificar vendedor (da lista de pessoas);\n * Escanear QRCode (ou acrescenta chave manualmente);\n * Colar e2eID (identificação end-to-end) a.k.a. código de identificação (API Pix / comprovante bancário);\n5. Receber na carteira (wallet conectada."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "O que é uma stablecoin?",
|
"title": "O que é uma stablecoin?",
|
||||||
"content": "É uma criptomoeda com lastro em moeda fiduciária (por exemplo, Real brasileiro ou Dólar americando). Cada stablecoin gerada por um projeto emissor tem a mesma quantidade em reserva da moeda fiduciária em seu caixa. Alguns exemplos de stablecoins pareadas com dólar americano são USDt, DAI, USDc. Já vinculadas ao preço do Real são BRZ, MBRL e cREAL.\n<br>\n\n* **O que é o BRZ?**\n [BRZ](https://www.brztoken.io/) é a sigla para Brazilian Digital Token. É um tipo de criptomoeda que tem valor pareado com o Real (BRL). Ou seja, 1 BRZ tem valor igual a 1 BRL."
|
"content": "É uma criptomoeda com lastro em moeda fiduciária (por exemplo, Real brasileiro ou Dólar americando). Cada stablecoin gerada por um projeto emissor tem a mesma quantidade em reserva da moeda fiduciária em seu caixa. Alguns exemplos de stablecoins pareadas com dólar americano são USDt, DAI, USDc. Já vinculadas ao preço do Real são BRZ, MBRL e cREAL.\n<br>\n\n* **O que é o BRZ?**\n [BRZ](https://www.brztoken.io/) é a sigla para Brazilian Digital Token. É um tipo de criptomoeda que tem valor pareado com o Real (BRL). Ou seja, 1 BRZ tem valor igual a 1 BRL."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "De quem estou comprando?",
|
"title": "De quem estou comprando?",
|
||||||
"content": "Vendedores fazem as ofertas e indicam a chave-Pix que irão receber o valor transacionado. Essa oferta fica travada no smart contract até que as transações sejam concluídas e você possa fazer o saque dos seus tokens. \n\n<br>\n\n* **Onde encontrar comprovante do Pix?**\n Quando você faz um Pix um comprovante é gerado automaticamente para o pagador e para o recebedor. Esse comprovante pode ser acessado no momento da transação via app/site do banco ou pelo seu extrato bancário convencional.\n* **Para onde vão os tokens que eu comprei?**\n Os tokens comprados terão como destino a carteira que você conectou ao p2pix. \n Nota: Lembre-se sempre de configurar a sua carteira na rede correta em que quer receber seus tokens. "
|
"content": "Vendedores fazem as ofertas e indicam a chave-Pix que irão receber o valor transacionado. Essa oferta fica travada no smart contract até que as transações sejam concluídas e você possa fazer o saque dos seus tokens. \n\n<br>\n\n* **Onde encontrar comprovante do Pix?**\n Quando você faz um Pix um comprovante é gerado automaticamente para o pagador e para o recebedor. Esse comprovante pode ser acessado no momento da transação via app/site do banco ou pelo seu extrato bancário convencional.\n* **Para onde vão os tokens que eu comprei?**\n Os tokens comprados terão como destino a carteira que você conectou ao p2pix. \n Nota: Lembre-se sempre de configurar a sua carteira na rede correta em que quer receber seus tokens. "
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "3. Vender tokens",
|
"name": "3. Vender tokens",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"title": "Como vender?",
|
"title": "Como vender?",
|
||||||
"content": "1. Enviar tokens;\n2. Indique sua chave-Pix;\n3. Criar [whitelist](https://academy.binance.com/en/glossary/whitelist) onde quer receber isso;\n4. Withdraw (saque) para remoção dos tokens;\n5. Lock (trava de 24h) para esperar transações antes dos saques.\n"
|
"content": "1. Enviar tokens;\n2. Indique sua chave-Pix;\n3. Criar [whitelist](https://academy.binance.com/en/glossary/whitelist) onde quer receber isso;\n4. Withdraw (saque) para remoção dos tokens;\n5. Lock (trava de 24h) para esperar transações antes dos saques.\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "O que é aprovar tokens?",
|
"title": "O que é aprovar tokens?",
|
||||||
"content": "Aprovações de tokens funcionam como permissões de sua carteira a um determinado [dApp](https://ethereum.org/en/developers/docs/dapps/) para movimentar os tokens. Ao aprovar tokens, você permite que nosso contrato inteligente acesse e execute as transações na sua carteira web3."
|
"content": "Aprovações de tokens funcionam como permissões de sua carteira a um determinado [dApp](https://ethereum.org/en/developers/docs/dapps/) para movimentar os tokens. Ao aprovar tokens, você permite que nosso contrato inteligente acesse e execute as transações na sua carteira web3."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "O que é o lock na rede?",
|
"title": "O que é o lock na rede?",
|
||||||
"content": "\"Travamento\" na rede é uma forma de especificar um tempo para retirada dos ativos digitais. O contrato inteligente do p2pix 'trava' os tokens enviados para a rede por 24 horas. Só depois de transcorridas essas 24h que o saque dos tokens estará liberado. Isso ocorre para garantir uma transação segura para os vendedores."
|
"content": "\"Travamento\" na rede é uma forma de especificar um tempo para retirada dos ativos digitais. O contrato inteligente do p2pix 'trava' os tokens enviados para a rede por 24 horas. Só depois de transcorridas essas 24h que o saque dos tokens estará liberado. Isso ocorre para garantir uma transação segura para os vendedores."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Como retirar a oferta?",
|
"title": "Como retirar a oferta?",
|
||||||
"content": "Caso você desista da sua oferta, você pode invalidar sua ordem de venda e impedir um novo ‘lock’ na rede."
|
"content": "Caso você desista da sua oferta, você pode invalidar sua ordem de venda e impedir um novo ‘lock’ na rede."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2139,4 +2139,4 @@
|
|||||||
"ISPB": "02318507",
|
"ISPB": "02318507",
|
||||||
"longName": "BANCO KEB HANA DO BRASIL S.A."
|
"longName": "BANCO KEB HANA DO BRASIL S.A."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import type { TokenEnum } from "@/model/NetworkEnum";
|
import type { TokenEnum } from '@/model/NetworkEnum';
|
||||||
|
import { Networks } from '@/config/networks';
|
||||||
export const imagesPath = import.meta.glob<string>("@/assets/*.{png,svg}", {
|
|
||||||
eager: true,
|
|
||||||
query: "?url",
|
|
||||||
import: "default",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getNetworkImage = (networkName: string): string => {
|
export const getNetworkImage = (networkName: string): string => {
|
||||||
const path = Object.keys(imagesPath).find((key) =>
|
const normalizedName = networkName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||||
key.endsWith(`${networkName.toLowerCase()}.svg`)
|
return new URL(`../assets/networks/${normalizedName}.svg`, import.meta.url)
|
||||||
);
|
.href;
|
||||||
return path ? imagesPath[path] : "";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTokenImage = (tokenName: TokenEnum): string => {
|
export const getTokenImage = (tokenName: TokenEnum): string => {
|
||||||
const path = Object.keys(imagesPath).find((key) =>
|
return new URL(
|
||||||
key.endsWith(`${tokenName.toLowerCase()}.svg`)
|
`../assets/tokens/${tokenName.toLowerCase()}.svg`,
|
||||||
);
|
import.meta.url,
|
||||||
return path ? imagesPath[path] : "";
|
).href;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||||
|
import type { Address } from 'viem';
|
||||||
|
|
||||||
const verifyNetworkLiquidity = (
|
const verifyNetworkLiquidity = (
|
||||||
tokenValue: number,
|
tokenValue: number,
|
||||||
walletAddress: string,
|
walletAddress: Address,
|
||||||
validDepositList: ValidDeposit[]
|
validDepositList: ValidDeposit[],
|
||||||
): ValidDeposit[] => {
|
): ValidDeposit[] => {
|
||||||
const filteredDepositList = validDepositList
|
const filteredDepositList = validDepositList
|
||||||
.filter((element) => {
|
.filter((element) => {
|
||||||
const remaining = element.remaining;
|
const remaining = element.remaining;
|
||||||
if (
|
if (
|
||||||
tokenValue!! <= remaining &&
|
tokenValue! <= remaining &&
|
||||||
tokenValue!! != 0 &&
|
tokenValue! != 0 &&
|
||||||
element.seller !== walletAddress
|
element.seller !== walletAddress
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
@@ -24,14 +25,14 @@ const verifyNetworkLiquidity = (
|
|||||||
const uniqueNetworkDeposits = filteredDepositList.reduce(
|
const uniqueNetworkDeposits = filteredDepositList.reduce(
|
||||||
(acc: ValidDeposit[], current) => {
|
(acc: ValidDeposit[], current) => {
|
||||||
const existingNetwork = acc.find(
|
const existingNetwork = acc.find(
|
||||||
(deposit) => deposit.network === current.network
|
(deposit) => deposit.network === current.network,
|
||||||
);
|
);
|
||||||
if (!existingNetwork) {
|
if (!existingNetwork) {
|
||||||
acc.push(current);
|
acc.push(current);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
return uniqueNetworkDeposits;
|
return uniqueNetworkDeposits;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
export const pixFormatValidation = (pixKey: string): boolean => {
|
|
||||||
const cpf = /(^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$)/g;
|
|
||||||
const cnpj = /(^\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2}$)/g;
|
|
||||||
const telefone = /(^[0-9]{2})?(\s|-)?(9?[0-9]{4})-?([0-9]{4}$)/g;
|
|
||||||
|
|
||||||
if (pixKey.match(cpf) || pixKey.match(cnpj) || pixKey.match(telefone)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const postProcessKey = (pixKey: string): string => {
|
export const postProcessKey = (pixKey: string): string => {
|
||||||
pixKey = pixKey.replace(/[-.()/]/g, "");
|
pixKey = pixKey.replace(/[-.()/]/g, '');
|
||||||
return pixKey;
|
return pixKey;
|
||||||
};
|
};
|
||||||
|
|||||||