Compare commits
71 Commits
19395403b4
...
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 | ||
|
|
290e339f0c | ||
|
|
1adef2dbb8 | ||
|
|
d3eae76f91 | ||
|
|
f64ea2ddf1 |
@@ -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
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
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
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
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
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
8
.gitignore
vendored
@@ -13,6 +13,9 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
vendor/
|
||||
.dagrobin
|
||||
.claude
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@@ -28,4 +31,7 @@ coverage
|
||||
*.sw?
|
||||
.vercel
|
||||
|
||||
.env
|
||||
.env
|
||||
|
||||
# Codegen output (regenerated by `bun run wagmi:gen`, runs on prestart)
|
||||
src/blockchain/abi.ts
|
||||
3
.gitmodules
vendored
Normal file
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
12
Dockerfile
@@ -1,10 +1,10 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
FROM oven/bun:latest
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn
|
||||
COPY ./ ./
|
||||
COPY package.json bun.lock ./
|
||||
COPY vendor ./vendor
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["yarn", "start"]
|
||||
CMD ["bun", "run", "start"]
|
||||
|
||||
52
README.md
52
README.md
@@ -3,13 +3,6 @@
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<center>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=liftlearning_P2Pix-Front-End)
|
||||
[](https://sonarcloud.io/summary/new_code?id=liftlearning_P2Pix-Front-End)
|
||||
|
||||
</center>
|
||||
|
||||
This application aims to create a democratic and secure solution for the purchase and sale of ERC20 tokens, through the PIX, integrating the functionalities of smart contracts (smart contracts) of the blockchain with a receipt by digital signature. Allowing the integration of national financial system transactions to public blockchains, dispensing with custody through intermediaries.
|
||||
|
||||
# Table of Contents
|
||||
@@ -59,47 +52,54 @@ See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Dependencies
|
||||
|
||||
### API
|
||||
For full operation of the application, it is necessary to correctly configure the variable that points to the api in the .env file, in the repository there is an .env.example file, just rename it to just .env and modify the variable `VITE_API_URL`. The api can be run locally see [https://github.com/liftlearning/Pix-Explorer-Back-End](https://github.com/liftlearning/Pix-Explorer-Back-End), or it can be pointed to just her staging address: [https://p2pix-block-explorer-api-staging.vercel.app/](https://p2pix-block-explorer-api-staging.vercel.app/)
|
||||
### API + RPC
|
||||
|
||||
### Alchemy Keys
|
||||
In the .env file, set `VITE_GOERLI_API_URL=https://eth-goerli.g.alchemy.com/v2/Zu9m4b2U_EzVU_zd-vgZDOleY8OF1DNP` and `VITE_MUMBAI_API_URL=https://polygon-mumbai.g.alchemy.com/v2/ZANeCqfj6VsXGpOH6gWAP6SIVIgD9Pwv`
|
||||
Copy `.env.example` to `.env` and set the per-network variables:
|
||||
|
||||
You can also replace it with your own Alchemy Keys if you have one.
|
||||
| Var | Purpose |
|
||||
|---|---|
|
||||
| `VITE_APP_API_URL` | zkPix middleware base URL (default `http://localhost:3001`) |
|
||||
| `VITE_SEPOLIA_API_URL`, `VITE_MAINNET_API_URL`, `VITE_RSK_API_URL` | RPC endpoints per network (Alchemy, Infura, public RPC) |
|
||||
| `VITE_SEPOLIA_TOKEN_ADDRESS`, `VITE_MAINNET_TOKEN_ADDRESS`, `VITE_RSK_TOKEN_ADDRESS` | BRZ token address per network |
|
||||
| `VITE_SEPOLIA_SUBGRAPH_URL`, `VITE_MAINNET_SUBGRAPH_URL`, `VITE_RSK_SUBGRAPH_URL` | The Graph subgraph endpoints |
|
||||
|
||||
## Build Setup
|
||||
|
||||
The application can be tested by its trial version [https://p2pix-staging.vercel.app/](https://p2pix-staging.vercel.app/), the only requirement is to be running the smart contract of local way. To run the application locally, there are two different ways:
|
||||
|
||||
### Run with yarn
|
||||
```sh
|
||||
# Clone the repo
|
||||
git clone https://github.com/liftlearning/P2Pix-Front-End
|
||||
cd P2Pix-Front-End
|
||||
### Run with bun
|
||||
|
||||
# Install dependencies with yarn
|
||||
yarn install
|
||||
```sh
|
||||
# Pull the smart-contracts submodule (skip if you cloned with --recurse-submodules)
|
||||
git submodule update --init
|
||||
|
||||
# Install front-end dependencies
|
||||
bun install
|
||||
|
||||
# One-time bootstrap of the smart-contracts submodule (needed before wagmi:gen)
|
||||
cd p2pix-smart-contracts && bun install && cd ..
|
||||
|
||||
# Generate ABI bindings from the submodule (run again whenever contracts change)
|
||||
bun run wagmi:gen
|
||||
|
||||
# Type-Check, Compile and Minify for Production
|
||||
yarn build
|
||||
bun run build
|
||||
|
||||
# Compile and Hot-Reload for Development (port 3000)
|
||||
yarn start
|
||||
bun start
|
||||
|
||||
# Lint with [ESLint](https://eslint.org/)
|
||||
yarn lint
|
||||
bun run lint
|
||||
```
|
||||
### Run with docker-compose
|
||||
|
||||
```sh
|
||||
# Clone the repo
|
||||
git clone https://github.com/liftlearning/P2Pix-Front-End
|
||||
cd P2Pix-Front-End
|
||||
# Pull the smart-contracts submodule (skip if you cloned with --recurse-submodules)
|
||||
git submodule update --init
|
||||
|
||||
#1. Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/);
|
||||
#2. Install [Docker Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
# Run docker-compose up command
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
},
|
||||
@@ -10,8 +10,8 @@ module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { node: "current" } }],
|
||||
"@babel/preset-typescript",
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,5 @@ services:
|
||||
container_name: p2pix_frontend
|
||||
build:
|
||||
context: .
|
||||
volumes:
|
||||
- '.:/app'
|
||||
ports:
|
||||
- '3000:3000'
|
||||
31
eslint.config.js
Normal file
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
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
1
p2pix-smart-contracts
Submodule
Submodule p2pix-smart-contracts added at c4db98ae00
12285
package-lock.json
generated
12285
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
95
package.json
95
package.json
@@ -1,57 +1,60 @@
|
||||
{
|
||||
"name": "p2pix-front-end",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite --host=0.0.0.0 --port 3000",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "bun run type-check && bun run build-only",
|
||||
"build-only": "vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --skipLibCheck --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore --fix",
|
||||
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,vue,json}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,vue,json}\"",
|
||||
"wagmi:gen": "wagmi generate",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/vue": "^0.2.1",
|
||||
"@headlessui/vue": "^1.7.3",
|
||||
"@heroicons/vue": "^2.0.12",
|
||||
"@vueuse/core": "^9.12.0",
|
||||
"@web3-onboard/injected-wallets": "^2.11.2",
|
||||
"@web3-onboard/vue": "^2.9.0",
|
||||
"alchemy-sdk": "^2.3.0",
|
||||
"axios": "^1.2.1",
|
||||
"marked": "^4.2.12",
|
||||
"qrcode": "^1.5.1",
|
||||
"viem": "^2.31.3",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.2.41",
|
||||
"vue-markdown": "^2.2.4",
|
||||
"vue-router": "^4.1.5"
|
||||
"@floating-ui/vue": "^1.1.11",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"@web3-onboard/injected-wallets": "^2.11.3",
|
||||
"@web3-onboard/vue": "^2.10.0",
|
||||
"marked": "^18.0.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"viem": "^2.48.8",
|
||||
"vite-svg-loader": "^5.1.1",
|
||||
"vue": "^3.5.33",
|
||||
"vue-router": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/crc": "^3.8.0",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^16.11.68",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/vue-markdown": "^2.2.1",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"@wagmi/cli": "^2.3.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.18",
|
||||
"prettier": "^2.7.1",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^3.1.8",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"@wagmi/cli": "^2.10.0",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.9.0",
|
||||
"happy-dom": "^20.9.0",
|
||||
"postcss": "^8.5.8",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.10",
|
||||
"vitest": "^4.1.5",
|
||||
"vue-tsc": "^3.2.7"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
18
src/App.vue
18
src/App.vue
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import TopBar from "@/components/TopBar/TopBar.vue";
|
||||
import SpinnerComponent from "@/components/ui/SpinnerComponent.vue";
|
||||
import ToasterComponent from "@/components/ui/ToasterComponent.vue";
|
||||
import { init, useOnboard } from "@web3-onboard/vue";
|
||||
import injectedModule from "@web3-onboard/injected-wallets";
|
||||
import { Networks, DEFAULT_NETWORK } from "@/config/networks";
|
||||
import { ref } from "vue";
|
||||
import { useRoute } from 'vue-router';
|
||||
import TopBar from '@/components/TopBar/TopBar.vue';
|
||||
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||
import ToasterComponent from '@/components/ui/ToasterComponent.vue';
|
||||
import VersionFooter from '@/components/ui/VersionFooter.vue';
|
||||
import { init, useOnboard } from '@web3-onboard/vue';
|
||||
import injectedModule from '@web3-onboard/injected-wallets';
|
||||
import { Networks, DEFAULT_NETWORK } from '@/config/networks';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const route = useRoute();
|
||||
const injected = injectedModule();
|
||||
@@ -53,5 +54,6 @@ if (!connectedWallet) {
|
||||
</template>
|
||||
</RouterView>
|
||||
<ToasterComponent :targetNetwork="targetNetwork" />
|
||||
<VersionFooter />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@import './base.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "./base.css" layer(base);
|
||||
@import "tailwindcss";
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,23 @@
|
||||
import { getContract } from "./provider";
|
||||
import { ChainContract } from "viem";
|
||||
import {
|
||||
parseEther,
|
||||
type Address,
|
||||
type TransactionReceipt,
|
||||
} from "viem";
|
||||
import { getContract } from './provider';
|
||||
import { ChainContract } from 'viem';
|
||||
import { parseEther, type Address, type TransactionReceipt } from 'viem';
|
||||
|
||||
export const addLock = async (
|
||||
sellerAddress: Address,
|
||||
tokenAddress: Address,
|
||||
amount: number
|
||||
amount: number,
|
||||
): Promise<bigint> => {
|
||||
const { address, abi, wallet, client, account } = await getContract();
|
||||
const parsedAmount = parseEther(amount.toString());
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error("Wallet not connected");
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
const { result, request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "lock",
|
||||
functionName: 'lock',
|
||||
args: [sellerAddress, tokenAddress, parsedAmount, [], []],
|
||||
account,
|
||||
});
|
||||
@@ -29,52 +25,52 @@ export const addLock = async (
|
||||
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||
|
||||
if (!receipt.status)
|
||||
throw new Error("Transaction failed: " + receipt.transactionHash);
|
||||
throw new Error('Transaction failed: ' + receipt.transactionHash);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const withdrawDeposit = async (
|
||||
amount: string,
|
||||
token: Address
|
||||
token: Address,
|
||||
): Promise<boolean> => {
|
||||
const { address, abi, wallet, client, account } = await getContract();
|
||||
|
||||
if (!wallet) {
|
||||
throw new Error("Wallet not connected");
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
const { request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "withdraw",
|
||||
functionName: 'withdraw',
|
||||
args: [token, parseEther(amount), []],
|
||||
account
|
||||
account,
|
||||
});
|
||||
|
||||
const hash = await wallet.writeContract(request);
|
||||
const receipt = await client.waitForTransactionReceipt({ hash });
|
||||
|
||||
return receipt.status === "success";
|
||||
return receipt.status === 'success';
|
||||
};
|
||||
|
||||
export const releaseLock = async (
|
||||
lockID: bigint,
|
||||
pixTimestamp: `0x${string}`&{lenght:34},
|
||||
signature: `0x${string}`
|
||||
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");
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
const { request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "release",
|
||||
functionName: 'release',
|
||||
args: [BigInt(lockID), pixTimestamp, signature],
|
||||
account
|
||||
account,
|
||||
});
|
||||
|
||||
const hash = await wallet.writeContract(request);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { formatEther, toHex, stringToHex } from "viem";
|
||||
import type { PublicClient, Address } from "viem";
|
||||
import { Networks } from "@/config/networks";
|
||||
import { getContract } from "./provider";
|
||||
import { p2PixAbi } from "./abi"
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { NetworkConfig } from "@/model/NetworkEnum";
|
||||
import type { UnreleasedLock } from "@/model/UnreleasedLock";
|
||||
import { ChainContract } from "viem";
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { formatEther, toHex, stringToHex } from 'viem';
|
||||
import type { PublicClient, Address } from 'viem';
|
||||
import { Networks } from '@/config/networks';
|
||||
import { getContract } from './provider';
|
||||
import { p2PixAbi } from './abi';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import type { UnreleasedLock } from '@/model/UnreleasedLock';
|
||||
import { ChainContract } from 'viem';
|
||||
|
||||
const getNetworksLiquidity = async (): Promise<void> => {
|
||||
const user = useUser();
|
||||
@@ -18,7 +18,7 @@ const getNetworksLiquidity = async (): Promise<void> => {
|
||||
for (const network of Object.values(Networks)) {
|
||||
const deposits = await getValidDeposits(
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
network
|
||||
network,
|
||||
);
|
||||
if (deposits) depositLists.push(deposits);
|
||||
}
|
||||
@@ -30,37 +30,37 @@ const getNetworksLiquidity = async (): Promise<void> => {
|
||||
|
||||
const getParticipantID = async (
|
||||
seller: Address,
|
||||
token: Address
|
||||
token: Address,
|
||||
): Promise<string> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
|
||||
const participantIDHex = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getPixTarget",
|
||||
functionName: 'getPixTarget',
|
||||
args: [seller, token],
|
||||
});
|
||||
|
||||
// Remove '0x' prefix and convert hex to UTF-8 string
|
||||
const hexString =
|
||||
typeof participantIDHex === "string"
|
||||
typeof participantIDHex === 'string'
|
||||
? participantIDHex
|
||||
: toHex(participantIDHex as bigint);
|
||||
if (!hexString) throw new Error("Participant ID not found");
|
||||
if (!hexString) throw new Error('Participant ID not found');
|
||||
const bytes = new Uint8Array(
|
||||
hexString
|
||||
.slice(2)
|
||||
.match(/.{1,2}/g)!
|
||||
.map((byte: string) => parseInt(byte, 16))
|
||||
.map((byte: string) => parseInt(byte, 16)),
|
||||
);
|
||||
// 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 (
|
||||
token: Address,
|
||||
network: NetworkConfig,
|
||||
contractInfo?: { client: PublicClient; address: Address }
|
||||
contractInfo?: { client: PublicClient; address: Address },
|
||||
): Promise<ValidDeposit[]> => {
|
||||
let client: PublicClient, abi;
|
||||
|
||||
@@ -84,10 +84,10 @@ const getValidDeposits = async (
|
||||
`,
|
||||
};
|
||||
|
||||
const depositLogs = await fetch( network.subgraphUrls[0], {
|
||||
method: "POST",
|
||||
const depositLogs = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
@@ -95,7 +95,7 @@ const getValidDeposits = async (
|
||||
// remove doubles from sellers list
|
||||
const depositData = await depositLogs.json();
|
||||
if (!depositData.data) {
|
||||
console.error("Error fetching deposit logs");
|
||||
console.error('Error fetching deposit logs');
|
||||
return [];
|
||||
}
|
||||
const depositAddeds = depositData.data.depositAddeds;
|
||||
@@ -104,7 +104,7 @@ const getValidDeposits = async (
|
||||
acc[deposit.seller] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<Address, boolean>
|
||||
{} as Record<Address, boolean>,
|
||||
);
|
||||
|
||||
if (!contractInfo) {
|
||||
@@ -119,7 +119,7 @@ const getValidDeposits = async (
|
||||
const balanceCalls = sellersList.map((seller) => ({
|
||||
address: (network.contracts?.p2pix as ChainContract).address,
|
||||
abi,
|
||||
functionName: "getBalance",
|
||||
functionName: 'getBalance',
|
||||
args: [seller, token],
|
||||
}));
|
||||
|
||||
@@ -138,7 +138,7 @@ const getValidDeposits = async (
|
||||
remaining: Number(formatEther(mappedBalance.result as bigint)),
|
||||
seller,
|
||||
network,
|
||||
participantID: "",
|
||||
participantID: '',
|
||||
};
|
||||
depositList[seller + token] = validDeposit;
|
||||
}
|
||||
@@ -147,14 +147,14 @@ const getValidDeposits = async (
|
||||
};
|
||||
|
||||
const getUnreleasedLockById = async (
|
||||
lockID: bigint
|
||||
lockID: bigint,
|
||||
): Promise<UnreleasedLock> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
|
||||
const [ , , , amount, token, seller ] = await client.readContract({
|
||||
const [, , , amount, token, seller] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "mapLocks",
|
||||
functionName: 'mapLocks',
|
||||
args: [lockID],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { p2PixAbi } from "./abi";
|
||||
import { updateWalletStatus } from "./wallet";
|
||||
import { p2PixAbi } from './abi';
|
||||
import { updateWalletStatus } from './wallet';
|
||||
import {
|
||||
createPublicClient,
|
||||
createWalletClient,
|
||||
@@ -7,22 +7,22 @@ import {
|
||||
http,
|
||||
PublicClient,
|
||||
WalletClient,
|
||||
} from "viem";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import type { NetworkConfig } from "@/model/NetworkEnum";
|
||||
import type { ChainContract } from "viem";
|
||||
} from 'viem';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import type { ChainContract } from 'viem';
|
||||
|
||||
let walletClient: WalletClient | null = null;
|
||||
|
||||
const getPublicClient = (): PublicClient => {
|
||||
const user = useUser();
|
||||
const rpcUrl = (user.network.value as NetworkConfig).rpcUrls.default.http[0];
|
||||
const chain = user.network.value;
|
||||
const user = useUser();
|
||||
const rpcUrl = (user.network.value as NetworkConfig).rpcUrls.default.http[0];
|
||||
const chain = user.network.value;
|
||||
|
||||
return createPublicClient({
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
});
|
||||
return createPublicClient({
|
||||
chain,
|
||||
transport: http(rpcUrl),
|
||||
});
|
||||
};
|
||||
|
||||
const getWalletClient = (): WalletClient | null => {
|
||||
@@ -32,12 +32,13 @@ const getWalletClient = (): WalletClient | null => {
|
||||
const getContract = async (onlyRpcProvider = false) => {
|
||||
const client = getPublicClient();
|
||||
const user = useUser();
|
||||
const address = (user.network.value.contracts?.p2pix as ChainContract).address;
|
||||
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");
|
||||
throw new Error('Public client not initialized');
|
||||
}
|
||||
|
||||
const [account] = wallet ? await wallet.getAddresses() : [null];
|
||||
@@ -49,7 +50,7 @@ const connectProvider = async (p: any): Promise<void> => {
|
||||
const user = useUser();
|
||||
const chain = user.network.value;
|
||||
|
||||
const [account] = await p!.request({ method: "eth_requestAccounts" });
|
||||
const [account] = await p!.request({ method: 'eth_requestAccounts' });
|
||||
|
||||
walletClient = createWalletClient({
|
||||
account,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getContract, getPublicClient, getWalletClient } from "./provider";
|
||||
import { parseEther, toHex, ChainContract } from "viem";
|
||||
import { mockTokenAbi } from "./abi";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { createParticipant } from "@/utils/bbPay";
|
||||
import type { Participant } from "@/utils/bbPay";
|
||||
import type { Address } from "viem";
|
||||
import { getContract, getPublicClient, getWalletClient } from './provider';
|
||||
import { parseEther, toHex, ChainContract } from 'viem';
|
||||
import { mockTokenAbi } from './abi';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { createParticipant } from '@/utils/bbPay';
|
||||
import type { Participant } from '@/utils/bbPay';
|
||||
import type { Address } from 'viem';
|
||||
|
||||
const getP2PixAddress = (): Address => {
|
||||
const user = useUser();
|
||||
const user = useUser();
|
||||
return (user.network.value.contracts?.p2pix as ChainContract).address;
|
||||
};
|
||||
|
||||
@@ -17,30 +17,31 @@ const approveTokens = async (participant: Participant): Promise<any> => {
|
||||
const walletClient = getWalletClient();
|
||||
|
||||
if (!publicClient || !walletClient) {
|
||||
throw new Error("Clients not initialized");
|
||||
throw new Error('Clients not initialized');
|
||||
}
|
||||
|
||||
user.setSeller(participant);
|
||||
const [account] = await walletClient.getAddresses();
|
||||
|
||||
// Get token address
|
||||
const tokenAddress = user.network.value.tokens[user.selectedToken.value].address;
|
||||
const tokenAddress =
|
||||
user.network.value.tokens[user.selectedToken.value].address;
|
||||
|
||||
// Check if the token is already approved
|
||||
const allowance = await publicClient.readContract({
|
||||
address: tokenAddress,
|
||||
abi: mockTokenAbi,
|
||||
functionName: "allowance",
|
||||
functionName: 'allowance',
|
||||
args: [account, getP2PixAddress()],
|
||||
});
|
||||
|
||||
if ( allowance < parseEther(participant.offer.toString()) ) {
|
||||
if (allowance < parseEther(participant.offer.toString())) {
|
||||
// Approve tokens
|
||||
const chain = user.network.value;
|
||||
const hash = await walletClient.writeContract({
|
||||
address: tokenAddress,
|
||||
abi: mockTokenAbi,
|
||||
functionName: "approve",
|
||||
functionName: 'approve',
|
||||
args: [getP2PixAddress(), parseEther(participant.offer.toString())],
|
||||
account,
|
||||
chain,
|
||||
@@ -58,7 +59,7 @@ const addDeposit = async (): Promise<any> => {
|
||||
const user = useUser();
|
||||
|
||||
if (!walletClient) {
|
||||
throw new Error("Wallet client not initialized");
|
||||
throw new Error('Wallet client not initialized');
|
||||
}
|
||||
|
||||
const [account] = await walletClient.getAddresses();
|
||||
@@ -66,16 +67,16 @@ const addDeposit = async (): Promise<any> => {
|
||||
const sellerId = await createParticipant(user.seller.value);
|
||||
user.setSellerId(sellerId.id);
|
||||
if (!sellerId.id) {
|
||||
throw new Error("Failed to create participant");
|
||||
throw new Error('Failed to create participant');
|
||||
}
|
||||
const chain = user.network.value;
|
||||
const hash = await walletClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "deposit",
|
||||
functionName: 'deposit',
|
||||
args: [
|
||||
user.network.value.id + "-" + sellerId.id,
|
||||
toHex("", { size: 32 }),
|
||||
user.network.value.id + '-' + sellerId.id,
|
||||
toHex('', { size: 32 }),
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
parseEther(user.seller.value.offer.toString()),
|
||||
true,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { formatEther, type Address } from "viem";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { formatEther, type Address } from 'viem';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
|
||||
import { getPublicClient, getWalletClient, getContract } from "./provider";
|
||||
import { getPublicClient, getWalletClient, getContract } from './provider';
|
||||
|
||||
import { getValidDeposits, getUnreleasedLockById } from "./events";
|
||||
import { getValidDeposits, getUnreleasedLockById } from './events';
|
||||
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
||||
import type { UnreleasedLock } from "@/model/UnreleasedLock";
|
||||
import { LockStatus } from "@/model/LockStatus";
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import type { UnreleasedLock } from '@/model/UnreleasedLock';
|
||||
import { LockStatus } from '@/model/LockStatus';
|
||||
|
||||
export const updateWalletStatus = async (): Promise<void> => {
|
||||
const user = useUser();
|
||||
@@ -17,7 +17,7 @@ export const updateWalletStatus = async (): Promise<void> => {
|
||||
const walletClient = getWalletClient();
|
||||
|
||||
if (!publicClient || !walletClient) {
|
||||
console.error("Client not initialized");
|
||||
console.error('Client not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ export const updateWalletStatus = async (): Promise<void> => {
|
||||
};
|
||||
|
||||
export const listValidDepositTransactionsByWalletAddress = async (
|
||||
walletAddress: Address
|
||||
walletAddress: Address,
|
||||
): Promise<ValidDeposit[]> => {
|
||||
const user = useUser();
|
||||
const walletDeposits = await getValidDeposits(
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
user.network.value
|
||||
user.network.value,
|
||||
);
|
||||
if (walletDeposits) {
|
||||
return walletDeposits
|
||||
@@ -50,17 +50,17 @@ export const listValidDepositTransactionsByWalletAddress = async (
|
||||
|
||||
const getLockStatus = async (id: bigint): Promise<LockStatus> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
const [ sortedIDs , status ] = await client.readContract({
|
||||
const [sortedIDs, status] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getLocksStatus",
|
||||
functionName: 'getLocksStatus',
|
||||
args: [[id]],
|
||||
});
|
||||
return status[0];
|
||||
};
|
||||
|
||||
export const listAllTransactionByWalletAddress = async (
|
||||
walletAddress: Address
|
||||
walletAddress: Address,
|
||||
): Promise<WalletTransaction[]> => {
|
||||
const user = useUser();
|
||||
|
||||
@@ -109,9 +109,9 @@ export const listAllTransactionByWalletAddress = async (
|
||||
};
|
||||
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
@@ -129,8 +129,8 @@ export const listAllTransactionByWalletAddress = async (
|
||||
blockTimestamp: parseInt(deposit.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(deposit.amount))),
|
||||
seller: deposit.seller,
|
||||
buyer: "",
|
||||
event: "DepositAdded",
|
||||
buyer: '',
|
||||
event: 'DepositAdded',
|
||||
lockStatus: undefined,
|
||||
transactionHash: deposit.transactionHash,
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export const listAllTransactionByWalletAddress = async (
|
||||
amount: parseFloat(formatEther(BigInt(lock.amount))),
|
||||
seller: lock.seller,
|
||||
buyer: lock.buyer,
|
||||
event: "LockAdded",
|
||||
event: 'LockAdded',
|
||||
lockStatus: lockStatus,
|
||||
transactionHash: lock.transactionHash,
|
||||
transactionID: lock.lockID.toString(),
|
||||
@@ -166,9 +166,9 @@ export const listAllTransactionByWalletAddress = async (
|
||||
blockNumber: parseInt(release.blockNumber),
|
||||
blockTimestamp: parseInt(release.blockTimestamp),
|
||||
amount: -1, // Amount not available in this event
|
||||
seller: "",
|
||||
seller: '',
|
||||
buyer: release.buyer,
|
||||
event: "LockReleased",
|
||||
event: 'LockReleased',
|
||||
lockStatus: undefined,
|
||||
transactionHash: release.transactionHash,
|
||||
transactionID: release.lockId.toString(),
|
||||
@@ -185,8 +185,8 @@ export const listAllTransactionByWalletAddress = async (
|
||||
blockTimestamp: parseInt(withdrawal.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(withdrawal.amount))),
|
||||
seller: withdrawal.seller,
|
||||
buyer: "",
|
||||
event: "DepositWithdrawn",
|
||||
buyer: '',
|
||||
event: 'DepositWithdrawn',
|
||||
lockStatus: undefined,
|
||||
transactionHash: withdrawal.transactionHash,
|
||||
});
|
||||
@@ -197,72 +197,6 @@ export const listAllTransactionByWalletAddress = async (
|
||||
return transactions.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||
};
|
||||
|
||||
// get wallet's release transactions
|
||||
export const listReleaseTransactionByWalletAddress = async (
|
||||
walletAddress: Address
|
||||
) => {
|
||||
const user = useUser();
|
||||
const network = user.network.value;
|
||||
|
||||
// Query subgraph for release transactions
|
||||
const subgraphQuery = {
|
||||
query: `
|
||||
{
|
||||
lockReleaseds(where: {buyer: "${walletAddress.toLowerCase()}"}) {
|
||||
buyer
|
||||
lockId
|
||||
e2eId
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Process the subgraph response into the same format as the previous implementation
|
||||
if (!data.data?.lockReleaseds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Transform the subgraph data to match the event log decode format
|
||||
return data.data.lockReleaseds
|
||||
.sort((a: any, b: any) => {
|
||||
return parseInt(b.blockNumber) - parseInt(a.blockNumber);
|
||||
})
|
||||
.map((release: any) => {
|
||||
try {
|
||||
// Create a structure similar to the decoded event log
|
||||
return {
|
||||
eventName: "LockReleased",
|
||||
args: {
|
||||
buyer: release.buyer,
|
||||
lockID: BigInt(release.lockId),
|
||||
e2eId: release.e2eId,
|
||||
},
|
||||
// Add any other necessary fields to match the original return format
|
||||
blockNumber: BigInt(release.blockNumber),
|
||||
transactionHash: release.transactionHash,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing subgraph data", error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((decoded: any) => decoded !== null);
|
||||
};
|
||||
|
||||
const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||
const user = useUser();
|
||||
const network = user.network.value;
|
||||
@@ -287,9 +221,9 @@ const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||
try {
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
@@ -309,7 +243,7 @@ const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||
try {
|
||||
// Create a structure similar to the decoded event log
|
||||
return {
|
||||
eventName: "LockAdded",
|
||||
eventName: 'LockAdded',
|
||||
args: {
|
||||
buyer: lock.buyer,
|
||||
lockID: BigInt(lock.lockID),
|
||||
@@ -322,13 +256,13 @@ const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||
transactionHash: lock.transactionHash,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing subgraph data", error);
|
||||
console.error('Error processing subgraph data', error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((decoded: any) => decoded !== null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching from subgraph:", error);
|
||||
console.error('Error fetching from subgraph:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -356,9 +290,9 @@ const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
try {
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
@@ -378,7 +312,7 @@ const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
try {
|
||||
// Create a structure similar to the decoded event log
|
||||
return {
|
||||
eventName: "LockAdded",
|
||||
eventName: 'LockAdded',
|
||||
args: {
|
||||
buyer: lock.buyer,
|
||||
lockID: BigInt(lock.lockID),
|
||||
@@ -391,19 +325,19 @@ const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
transactionHash: lock.transactionHash,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error processing subgraph data", error);
|
||||
console.error('Error processing subgraph data', error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((decoded: any) => decoded !== null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching from subgraph:", error);
|
||||
console.error('Error fetching from subgraph:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const checkUnreleasedLock = async (
|
||||
walletAddress: Address
|
||||
walletAddress: Address,
|
||||
): Promise<UnreleasedLock | undefined> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
const addedLocks = await listLockTransactionByWalletAddress(walletAddress);
|
||||
@@ -412,23 +346,23 @@ export const checkUnreleasedLock = async (
|
||||
|
||||
const lockIds = addedLocks.map((lock: any) => lock.args.lockID);
|
||||
|
||||
const [ sortedIDs, status ] = await client.readContract({
|
||||
const [sortedIDs, status] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getLocksStatus",
|
||||
functionName: 'getLocksStatus',
|
||||
args: [lockIds],
|
||||
});
|
||||
|
||||
const unreleasedLockId = status.findIndex(
|
||||
(status: LockStatus) => status == LockStatus.Active
|
||||
(status: LockStatus) => status == LockStatus.Active,
|
||||
);
|
||||
|
||||
|
||||
if (unreleasedLockId !== -1)
|
||||
return getUnreleasedLockById(sortedIDs[unreleasedLockId]);
|
||||
};
|
||||
|
||||
export const getActiveLockAmount = async (
|
||||
walletAddress: Address
|
||||
walletAddress: Address,
|
||||
): Promise<number> => {
|
||||
const { address, abi, client } = await getContract(true);
|
||||
const lockSeller = await listLockTransactionBySellerAddress(walletAddress);
|
||||
@@ -437,10 +371,10 @@ export const getActiveLockAmount = async (
|
||||
|
||||
const lockIds = lockSeller.map((lock: any) => lock.args.lockID);
|
||||
|
||||
const [ sortedIDs, status ] = await client.readContract({
|
||||
const [sortedIDs, status] = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getLocksStatus",
|
||||
functionName: 'getLocksStatus',
|
||||
args: [lockIds],
|
||||
});
|
||||
|
||||
@@ -448,9 +382,9 @@ export const getActiveLockAmount = async (
|
||||
client.readContract({
|
||||
address: address,
|
||||
abi,
|
||||
functionName: "mapLocks",
|
||||
functionName: 'mapLocks',
|
||||
args: [BigInt(id)],
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const mapLocksResults = await client.multicall({
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { withdrawDeposit } from "@/blockchain/buyerMethods";
|
||||
import { withdrawDeposit } from '@/blockchain/buyerMethods';
|
||||
import {
|
||||
getActiveLockAmount,
|
||||
listAllTransactionByWalletAddress,
|
||||
listValidDepositTransactionsByWalletAddress,
|
||||
} from "@/blockchain/wallet";
|
||||
import CustomButton from "@/components/ui/CustomButton.vue";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
|
||||
} from '@/blockchain/wallet';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import ListingComponent from '@/components/ListingComponent/ListingComponent.vue';
|
||||
|
||||
// props
|
||||
const props = defineProps<{
|
||||
@@ -31,11 +31,11 @@ const getWalletTransactions = async () => {
|
||||
user.setLoadingWalletTransactions(true);
|
||||
if (walletAddress.value) {
|
||||
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
|
||||
walletAddress.value
|
||||
walletAddress.value,
|
||||
);
|
||||
|
||||
const allUserTransactions = await listAllTransactionByWalletAddress(
|
||||
walletAddress.value
|
||||
walletAddress.value,
|
||||
);
|
||||
|
||||
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
|
||||
@@ -53,19 +53,22 @@ const getWalletTransactions = async () => {
|
||||
const callWithdraw = async (amount: string) => {
|
||||
if (amount) {
|
||||
user.setLoadingWalletTransactions(true);
|
||||
const withdraw = await withdrawDeposit(amount, user.network.value.tokens[user.selectedToken.value].address);
|
||||
const withdraw = await withdrawDeposit(
|
||||
amount,
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
);
|
||||
if (withdraw) {
|
||||
console.log("Saque realizado!");
|
||||
console.log('Saque realizado!');
|
||||
await getWalletTransactions();
|
||||
} else {
|
||||
console.log("Não foi possível realizar o saque!");
|
||||
console.log('Não foi possível realizar o saque!');
|
||||
}
|
||||
user.setLoadingWalletTransactions(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["makeAnotherTransaction"]);
|
||||
const emit = defineEmits(['makeAnotherTransaction']);
|
||||
|
||||
// observer
|
||||
watch(props, async (): Promise<void> => {
|
||||
@@ -86,9 +89,7 @@ onMounted(async () => {
|
||||
</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 flex-col w-full bg-white px-10 py-5 rounded-lg">
|
||||
<div>
|
||||
<p>Tokens recebidos</p>
|
||||
<p class="text-2xl text-gray-900">
|
||||
@@ -129,6 +130,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
@@ -149,8 +151,8 @@ p {
|
||||
@apply font-medium text-base text-gray-900;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import SpinnerComponent from "@/components/ui/SpinnerComponent.vue";
|
||||
import CustomButton from "@/components/ui/CustomButton.vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { verifyNetworkLiquidity } from "@/utils/networkLiquidity";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import { getTokenImage, getNetworkImage } from "@/utils/imagesPath";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import { Networks } from "@/config/networks";
|
||||
import { TokenEnum } from "@/model/NetworkEnum";
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import SpinnerComponent from '@/components/ui/SpinnerComponent.vue';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { verifyNetworkLiquidity } from '@/utils/networkLiquidity';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import { decimalCount } from '@/utils/decimalCount';
|
||||
import { getTokenImage, getNetworkImage } from '@/utils/imagesPath';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { Networks } from '@/config/networks';
|
||||
import { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { getContract } from '@/blockchain/provider';
|
||||
import { reputationAbi } from '@/blockchain/abi';
|
||||
import { type Address } from 'viem';
|
||||
|
||||
// Store reference
|
||||
const user = useUser();
|
||||
@@ -32,15 +35,106 @@ const tokenValue = ref<number>(0);
|
||||
const enableConfirmButton = ref<boolean>(false);
|
||||
const hasLiquidity = ref<boolean>(true);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const identification = ref<string>("");
|
||||
const identification = ref<string>('');
|
||||
const selectedDeposits = ref<ValidDeposit[]>();
|
||||
const reputationLimit = ref<number | null>(null);
|
||||
const exceedsReputationLimit = ref<boolean>(false);
|
||||
|
||||
import ChevronDown from "@/assets/chevronDown.svg";
|
||||
import { useOnboard } from "@web3-onboard/vue";
|
||||
import { getParticipantID } from "@/blockchain/events";
|
||||
import ChevronDown from '@/assets/chevronDown.svg';
|
||||
import { useOnboard } from '@web3-onboard/vue';
|
||||
import { getParticipantID } from '@/blockchain/events';
|
||||
|
||||
// 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
|
||||
const connectAccount = async (): Promise<void> => {
|
||||
@@ -50,11 +144,11 @@ const connectAccount = async (): Promise<void> => {
|
||||
|
||||
const emitConfirmButton = async (): Promise<void> => {
|
||||
const deposit = selectedDeposits.value?.find(
|
||||
(d) => d.network === network.value
|
||||
(d) => d.network === network.value,
|
||||
);
|
||||
if (!deposit) return;
|
||||
deposit.participantID = await getParticipantID(deposit.seller, deposit.token);
|
||||
emit("tokenBuy", deposit, tokenValue.value);
|
||||
emit('tokenBuy', deposit, tokenValue.value);
|
||||
};
|
||||
|
||||
// Debounce methods
|
||||
@@ -70,6 +164,7 @@ const handleInputEvent = (event: any): void => {
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
checkReputationLimit(tokenValue.value);
|
||||
verifyLiquidity();
|
||||
};
|
||||
|
||||
@@ -89,17 +184,14 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
// Verify if there is a valid deposit to buy
|
||||
const verifyLiquidity = (): void => {
|
||||
enableConfirmButton.value = false;
|
||||
if (!walletAddress.value)
|
||||
return;
|
||||
if (!walletAddress.value) return;
|
||||
const selDeposits = verifyNetworkLiquidity(
|
||||
tokenValue.value,
|
||||
walletAddress.value,
|
||||
depositsValidList.value
|
||||
depositsValidList.value,
|
||||
);
|
||||
selectedDeposits.value = selDeposits;
|
||||
hasLiquidity.value = !!selDeposits.find(
|
||||
(d) => d.network === network.value
|
||||
);
|
||||
hasLiquidity.value = !!selDeposits.find((d) => d.network === network.value);
|
||||
enableOrDisableConfirmButton();
|
||||
};
|
||||
|
||||
@@ -129,7 +221,7 @@ watch(walletAddress, (): void => {
|
||||
const availableNetworks = computed(() => {
|
||||
if (!selectedDeposits.value) return [];
|
||||
return Object.values(Networks).filter((network) =>
|
||||
selectedDeposits.value?.some((d) => d.network.id === network.id)
|
||||
selectedDeposits.value?.some((d) => d.network.id === network.id),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -159,9 +251,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
</div>
|
||||
<form class="main-container" @submit="handleSubmit">
|
||||
<div class="backdrop-blur -z-10 w-full h-full"></div>
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg">
|
||||
<div class="flex justify-between sm:w-full items-center">
|
||||
<input
|
||||
type="number"
|
||||
@@ -268,7 +358,10 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-else-if="
|
||||
!hasLiquidity && !loadingNetworkLiquidity && tokenValue > 0
|
||||
!hasLiquidity &&
|
||||
!loadingNetworkLiquidity &&
|
||||
tokenValue > 0 &&
|
||||
!exceedsReputationLimit
|
||||
"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
@@ -276,11 +369,22 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
demanda</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex justify-center"
|
||||
v-if="
|
||||
exceedsReputationLimit &&
|
||||
!loadingNetworkLiquidity &&
|
||||
reputationLimit !== null
|
||||
"
|
||||
>
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>O valor excede o limite permitido pela sua reputação. Limite
|
||||
máximo: {{ reputationLimit }} {{ selectedToken }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<input
|
||||
type="text"
|
||||
v-model="identification"
|
||||
@@ -297,6 +401,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
v-if="walletAddress"
|
||||
type="submit"
|
||||
text="Confirmar Oferta"
|
||||
:isDisabled="!enableConfirmButton"
|
||||
/>
|
||||
<CustomButton
|
||||
v-else
|
||||
@@ -308,6 +413,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
@@ -330,12 +436,12 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<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";
|
||||
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 {
|
||||
@@ -15,11 +15,11 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const qrCode = ref<string>("");
|
||||
const qrCodeSvg = ref<string>("");
|
||||
const qrCode = ref<string>('');
|
||||
const qrCodeSvg = ref<string>('');
|
||||
const showWarnModal = ref<boolean>(true);
|
||||
const pixTimestamp = ref<string>("");
|
||||
const releaseSignature = ref<string>("");
|
||||
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);
|
||||
@@ -29,22 +29,22 @@ const copyFeedbackTimeout = ref<NodeJS.Timeout | null>(null);
|
||||
const generateQrCodeSvg = async (text: string) => {
|
||||
try {
|
||||
const svgString = await QRCode.toString(text, {
|
||||
type: "svg",
|
||||
type: 'svg',
|
||||
width: 192, // 48 * 4 for better quality
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: "#000000",
|
||||
light: "#FFFFFF",
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
qrCodeSvg.value = svgString;
|
||||
} catch (error) {
|
||||
console.error("Error generating QR code SVG:", error);
|
||||
console.error('Error generating QR code SVG:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["pixValidated"]);
|
||||
const emit = defineEmits(['pixValidated']);
|
||||
|
||||
// Function to check solicitation status
|
||||
const checkSolicitationStatus = async () => {
|
||||
@@ -54,7 +54,7 @@ const checkSolicitationStatus = async () => {
|
||||
|
||||
try {
|
||||
const response = await getSolicitation(
|
||||
solicitationData.value.numeroSolicitacao
|
||||
solicitationData.value.numeroSolicitacao,
|
||||
);
|
||||
|
||||
if (response.signature) {
|
||||
@@ -67,7 +67,7 @@ const checkSolicitationStatus = async () => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking solicitation status:", error);
|
||||
console.error('Error checking solicitation status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,7 +82,6 @@ const startPolling = () => {
|
||||
pollingInterval.value = setInterval(checkSolicitationStatus, 10000);
|
||||
};
|
||||
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (!qrCode.value) {
|
||||
return;
|
||||
@@ -101,20 +100,17 @@ const copyToClipboard = async () => {
|
||||
copyFeedback.value = false;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error copying to clipboard:", error);
|
||||
console.error('Error copying to clipboard:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { tokenAddress, sellerAddress, amount } = await getUnreleasedLockById(
|
||||
BigInt(props.lockID)
|
||||
BigInt(props.lockID),
|
||||
);
|
||||
|
||||
const participantId = await getParticipantID(
|
||||
sellerAddress,
|
||||
tokenAddress
|
||||
);
|
||||
const participantId = await getParticipantID(sellerAddress, tokenAddress);
|
||||
|
||||
const offer: Offer = {
|
||||
amount,
|
||||
@@ -134,7 +130,7 @@ onMounted(async () => {
|
||||
// Start polling for solicitation status
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
console.error("Error creating solicitation:", error);
|
||||
console.error('Error creating solicitation:', error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -223,6 +219,7 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
@@ -271,13 +268,13 @@ h2 {
|
||||
@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"] {
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
changeType: 'neutral',
|
||||
loading: false
|
||||
loading: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
</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}`">
|
||||
<div
|
||||
v-if="change && !loading"
|
||||
class="analytics-change"
|
||||
:class="`change-${changeType}`"
|
||||
>
|
||||
{{ change }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,6 +37,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
</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;
|
||||
}
|
||||
|
||||
@@ -28,17 +28,22 @@ const getTransactionTypeInfo = (type: string) => {
|
||||
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: { label: 'Retorno', status: 'expired' as const },
|
||||
};
|
||||
return typeMap[type as keyof typeof typeMap] || { label: type, status: 'pending' 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',
|
||||
lock: 'text-amber-600',
|
||||
release: 'text-emerald-600',
|
||||
return: 'text-gray-600'
|
||||
return: 'text-gray-600',
|
||||
};
|
||||
return colorMap[type as keyof typeof colorMap] || 'text-gray-600';
|
||||
};
|
||||
@@ -85,25 +90,31 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
<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">
|
||||
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">
|
||||
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"
|
||||
<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>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ transaction.timestamp }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<span
|
||||
<span
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
@@ -115,9 +126,14 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
<div v-if="transaction.seller" class="text-sm">
|
||||
<span class="text-gray-600">Vendedor: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="copyToClipboard(transaction.seller, `seller-${transaction.id}`)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.seller,
|
||||
`seller-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.seller) }}
|
||||
@@ -135,9 +151,14 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
<div v-if="transaction.buyer" class="text-sm">
|
||||
<span class="text-gray-600">Comprador: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.buyer,
|
||||
`buyer-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.buyer) }}
|
||||
@@ -181,13 +202,13 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="lg:hidden space-y-4">
|
||||
<div
|
||||
v-for="transaction in transactions"
|
||||
<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
|
||||
<span
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
@@ -195,14 +216,19 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
</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
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="copyToClipboard(transaction.seller, `seller-${transaction.id}`)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.seller,
|
||||
`seller-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.seller) }}
|
||||
@@ -220,9 +246,11 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
<div v-if="transaction.buyer" class="text-sm">
|
||||
<span class="text-gray-600">Comprador: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)"
|
||||
@click="
|
||||
copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.buyer) }}
|
||||
@@ -239,14 +267,18 @@ const copyToClipboard = async (address: string, key: string) => {
|
||||
</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>
|
||||
<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>
|
||||
<span class="text-gray-900 font-mono"
|
||||
>#{{ transaction.blockNumber }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<a
|
||||
:href="getExplorerUrl(transaction.transactionHash)"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<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";
|
||||
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[];
|
||||
@@ -17,7 +17,7 @@ const emit = defineEmits<{
|
||||
withdraw: [amount: string];
|
||||
}>();
|
||||
|
||||
const withdrawAmount = ref<string>("");
|
||||
const withdrawAmount = ref<string>('');
|
||||
const isCollapsibleOpen = ref<boolean>(false);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const validWithdrawAmount = ref<boolean>(true);
|
||||
@@ -58,9 +58,9 @@ const handleInputEvent = (event: any): void => {
|
||||
|
||||
const callWithdraw = () => {
|
||||
if (enableConfirmButton.value && withdrawAmount.value) {
|
||||
emit("withdraw", withdrawAmount.value);
|
||||
emit('withdraw', withdrawAmount.value);
|
||||
// Reset form after withdraw
|
||||
withdrawAmount.value = "";
|
||||
withdrawAmount.value = '';
|
||||
isCollapsibleOpen.value = false;
|
||||
}
|
||||
};
|
||||
@@ -71,7 +71,7 @@ const openWithdrawForm = () => {
|
||||
|
||||
const cancelWithdraw = () => {
|
||||
isCollapsibleOpen.value = false;
|
||||
withdrawAmount.value = "";
|
||||
withdrawAmount.value = '';
|
||||
validDecimals.value = true;
|
||||
validWithdrawAmount.value = true;
|
||||
enableConfirmButton.value = false;
|
||||
@@ -79,7 +79,7 @@ const cancelWithdraw = () => {
|
||||
|
||||
onMounted(() => {
|
||||
useFloating(reference, floating, {
|
||||
placement: "right",
|
||||
placement: 'right',
|
||||
middleware: [
|
||||
offset(10),
|
||||
flip(),
|
||||
@@ -162,10 +162,7 @@ onMounted(() => {
|
||||
</span>
|
||||
</div>
|
||||
<hr v-show="isCollapsibleOpen" class="pb-3" />
|
||||
<div
|
||||
v-show="isCollapsibleOpen"
|
||||
class="flex justify-between items-center"
|
||||
>
|
||||
<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"
|
||||
@@ -188,6 +185,7 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
p {
|
||||
@apply text-gray-900;
|
||||
}
|
||||
@@ -196,13 +194,13 @@ p {
|
||||
@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"] {
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@@ -212,4 +210,3 @@ input[type="number"]::-webkit-outer-spin-button {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { ref, watch } from "vue";
|
||||
import SpinnerComponent from "../ui/SpinnerComponent.vue";
|
||||
import BalanceCard from "./BalanceCard.vue";
|
||||
import TransactionCard from "./TransactionCard.vue";
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { ref, watch } from 'vue';
|
||||
import SpinnerComponent from '../ui/SpinnerComponent.vue';
|
||||
import BalanceCard from './BalanceCard.vue';
|
||||
import TransactionCard from './TransactionCard.vue';
|
||||
|
||||
const user = useUser();
|
||||
|
||||
@@ -16,14 +16,14 @@ const props = defineProps<{
|
||||
activeLockAmount: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["depositWithdrawn"]);
|
||||
const emit = defineEmits(['depositWithdrawn']);
|
||||
|
||||
const { loadingWalletTransactions } = user;
|
||||
|
||||
const itemsToShow = ref<WalletTransaction[]>([]);
|
||||
|
||||
const callWithdraw = (amount: string) => {
|
||||
emit("depositWithdrawn", amount);
|
||||
emit('depositWithdrawn', amount);
|
||||
};
|
||||
|
||||
const showInitialItems = (): void => {
|
||||
@@ -33,13 +33,13 @@ const showInitialItems = (): void => {
|
||||
const openEtherscanUrl = (transactionHash: string): void => {
|
||||
const networkUrl = user.network.value.blockExplorers?.default.url;
|
||||
const url = `https://${networkUrl}/tx/${transactionHash}`;
|
||||
window.open(url, "_blank");
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const loadMore = (): void => {
|
||||
const itemsShowing = itemsToShow.value.length;
|
||||
itemsToShow.value?.push(
|
||||
...props.walletTransactions.slice(itemsShowing, itemsShowing + 3)
|
||||
...props.walletTransactions.slice(itemsShowing, itemsShowing + 3),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<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";
|
||||
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;
|
||||
@@ -16,63 +16,64 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const eventName = computed(() => {
|
||||
if (!props.transaction.event) return "Desconhecido";
|
||||
if (!props.transaction.event) return 'Desconhecido';
|
||||
|
||||
const possibleEventName: { [key: string]: string } = {
|
||||
DepositAdded: "Oferta",
|
||||
LockAdded: "Reserva",
|
||||
LockReleased: "Compra",
|
||||
DepositWithdrawn: "Retirada",
|
||||
DepositAdded: 'Oferta',
|
||||
LockAdded: 'Reserva',
|
||||
LockReleased: 'Compra',
|
||||
DepositWithdrawn: 'Retirada',
|
||||
};
|
||||
|
||||
return possibleEventName[props.transaction.event] || "Desconhecido";
|
||||
return possibleEventName[props.transaction.event] || 'Desconhecido';
|
||||
});
|
||||
|
||||
const explorerName = computed(() => {
|
||||
return Networks[(props.networkName as string).toLowerCase()].blockExplorers?.default.name;
|
||||
return Networks[(props.networkName as string).toLowerCase()].blockExplorers
|
||||
?.default.name;
|
||||
});
|
||||
|
||||
const statusType = computed((): StatusType => {
|
||||
if (eventName.value === "Reserva") {
|
||||
if (eventName.value === 'Reserva') {
|
||||
switch (props.transaction.lockStatus) {
|
||||
case 1:
|
||||
return "open";
|
||||
return 'open';
|
||||
case 2:
|
||||
return "expired";
|
||||
return 'expired';
|
||||
case 3:
|
||||
return "completed";
|
||||
return 'completed';
|
||||
default:
|
||||
return "completed";
|
||||
return 'completed';
|
||||
}
|
||||
}
|
||||
return "completed";
|
||||
return 'completed';
|
||||
});
|
||||
|
||||
const showExplorerLink = computed(() => {
|
||||
return eventName.value !== "Reserva" || props.transaction.lockStatus !== 1;
|
||||
return eventName.value !== 'Reserva' || props.transaction.lockStatus !== 1;
|
||||
});
|
||||
|
||||
const showContinueButton = computed(() => {
|
||||
return eventName.value === "Reserva" && props.transaction.lockStatus === 1;
|
||||
return eventName.value === 'Reserva' && props.transaction.lockStatus === 1;
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
if (!props.transaction.blockTimestamp) return "";
|
||||
|
||||
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 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");
|
||||
|
||||
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);
|
||||
emit('openExplorer', props.transaction.transactionHash);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -130,6 +131,7 @@ const handleExplorerClick = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.item-container {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
@@ -142,4 +144,3 @@ const handleExplorerClick = () => {
|
||||
@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,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import CustomButton from "@/components/ui/CustomButton.vue";
|
||||
import { postProcessKey } from "@/utils/pixKeyFormat";
|
||||
import { TokenEnum } from "@/model/NetworkEnum";
|
||||
import { getTokenImage } from "@/utils/imagesPath";
|
||||
import { useOnboard } from "@web3-onboard/vue";
|
||||
import ChevronDown from "@/assets/chevron.svg";
|
||||
import { ref, computed } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
import { postProcessKey } from '@/utils/pixKeyFormat';
|
||||
import { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { getTokenImage } from '@/utils/imagesPath';
|
||||
import { useOnboard } from '@web3-onboard/vue';
|
||||
import ChevronDown from '@/assets/chevron.svg';
|
||||
|
||||
// Import the bank list
|
||||
import bankList from "@/utils/files/isbpList.json";
|
||||
import type { Participant } from "@/utils/bbPay";
|
||||
import bankList from '@/utils/files/isbpList.json';
|
||||
import type { Participant } from '@/utils/bbPay';
|
||||
|
||||
// Define Bank interface
|
||||
interface Bank {
|
||||
@@ -26,17 +26,17 @@ const formRef = ref<HTMLFormElement | null>(null);
|
||||
const user = useUser();
|
||||
const { walletAddress, selectedToken } = user;
|
||||
|
||||
const offer = ref<string>("");
|
||||
const identification = ref<string>("");
|
||||
const account = ref<string>("");
|
||||
const branch = ref<string>("");
|
||||
const accountType = ref<string>("");
|
||||
const offer = ref<string>('');
|
||||
const identification = ref<string>('');
|
||||
const account = ref<string>('');
|
||||
const branch = ref<string>('');
|
||||
const accountType = ref<string>('');
|
||||
const selectTokenToggle = ref<boolean>(false);
|
||||
const savingsVariation = ref<string>("");
|
||||
const savingsVariation = ref<string>('');
|
||||
const errors = ref<{ [key: string]: string }>({});
|
||||
|
||||
// Bank selection
|
||||
const bankSearchQuery = ref<string>("");
|
||||
const bankSearchQuery = ref<string>('');
|
||||
const showBankList = ref<boolean>(false);
|
||||
const selectedBank = ref<Bank | null>(null);
|
||||
|
||||
@@ -44,7 +44,7 @@ const filteredBanks = computed(() => {
|
||||
if (!bankSearchQuery.value) return [];
|
||||
return bankList
|
||||
.filter((bank) =>
|
||||
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase())
|
||||
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase()),
|
||||
)
|
||||
.slice(0, 5);
|
||||
});
|
||||
@@ -56,7 +56,7 @@ const handleBankSelect = (bank: Bank) => {
|
||||
};
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["approveTokens"]);
|
||||
const emit = defineEmits(['approveTokens']);
|
||||
|
||||
// Methods
|
||||
const connectAccount = async (): Promise<void> => {
|
||||
@@ -77,10 +77,10 @@ const handleSubmit = (e: Event): void => {
|
||||
accountType: accountType.value,
|
||||
account: account.value,
|
||||
branch: branch.value,
|
||||
savingsVariation: savingsVariation.value || "",
|
||||
savingsVariation: savingsVariation.value || '',
|
||||
};
|
||||
|
||||
emit("approveTokens", data);
|
||||
emit('approveTokens', data);
|
||||
};
|
||||
|
||||
// Token selection
|
||||
@@ -113,7 +113,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
<form ref="formRef" @submit="handleSubmit" class="main-container">
|
||||
<!-- Offer input -->
|
||||
<div
|
||||
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10 gap-4"
|
||||
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg gap-4"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@@ -185,9 +185,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- CPF or CNPJ input -->
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<input
|
||||
type="text"
|
||||
v-model="identification"
|
||||
@@ -200,9 +198,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
/>
|
||||
</div>
|
||||
<!-- Bank selection -->
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -235,9 +231,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Account and Branch inputs -->
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
@@ -262,9 +256,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
</div>
|
||||
</div>
|
||||
<!-- Account Type Selection -->
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
@@ -287,7 +279,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
v-if="accountType === '2'"
|
||||
type="text"
|
||||
v-model="savingsVariation"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
|
||||
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full bg-white sm:px-10 px-6 py-4 rounded-lg"
|
||||
:class="{ 'text-xl font-medium': savingsVariation }"
|
||||
placeholder="Variação da poupança"
|
||||
required
|
||||
@@ -305,6 +297,7 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
@@ -327,13 +320,13 @@ const handleSelectedToken = (token: TokenEnum): void => {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import CustomButton from "@/components/ui/CustomButton.vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import { getTokenImage } from "@/utils/imagesPath";
|
||||
import { useOnboard } from "@web3-onboard/vue";
|
||||
|
||||
// Store
|
||||
const user = useUser();
|
||||
const { walletAddress } = user;
|
||||
|
||||
// Reactive state
|
||||
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(user.selectedToken.value)"
|
||||
/>
|
||||
<span class="text-gray-900 text-lg w-fit" id="token">{{
|
||||
user.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/networks/polygon.svg?url"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
<img
|
||||
alt="Ethereum image"
|
||||
src="@/assets/networks/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"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import CustomButton from "@/components/ui/CustomButton.vue";
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["sendNetwork"]);
|
||||
const emit = defineEmits(['sendNetwork']);
|
||||
|
||||
// props and store references
|
||||
const props = defineProps({
|
||||
@@ -27,9 +27,7 @@ const props = defineProps({
|
||||
>
|
||||
</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 flex-col w-full bg-white px-10 py-5 rounded-lg">
|
||||
<div>
|
||||
<p>Tokens ofertados</p>
|
||||
<p class="text-2xl text-gray-900">
|
||||
@@ -59,6 +57,7 @@ const props = defineProps({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
@@ -79,8 +78,8 @@ p {
|
||||
@apply font-medium text-base;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import { getNetworkImage } from "@/utils/imagesPath";
|
||||
import { Networks } from "@/config/networks";
|
||||
import { useOnboard } from "@web3-onboard/vue";
|
||||
import { ref, watch } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { getNetworkImage } from '@/utils/imagesPath';
|
||||
import { Networks } from '@/config/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';
|
||||
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';
|
||||
|
||||
interface MenuOption {
|
||||
label: string;
|
||||
@@ -47,7 +47,7 @@ const connnectWallet = async (): Promise<void> => {
|
||||
|
||||
watch(connectedWallet, async (newVal: any) => {
|
||||
connectProvider(newVal.provider);
|
||||
const addresses = await newVal.provider.request({ method: "eth_accounts" });
|
||||
const addresses = await newVal.provider.request({ method: 'eth_accounts' });
|
||||
user.setWalletAddress(addresses.shift());
|
||||
});
|
||||
|
||||
@@ -55,12 +55,10 @@ watch(connectedChain, (newVal: any) => {
|
||||
// Check if connected chain is valid, otherwise default to Sepolia
|
||||
if (
|
||||
!newVal ||
|
||||
!Object.values(Networks).some(
|
||||
(network) => network.id === Number(newVal.id)
|
||||
)
|
||||
!Object.values(Networks).some((network) => network.id === Number(newVal.id))
|
||||
) {
|
||||
console.log(
|
||||
"Invalid or unsupported network detected, defaulting to Sepolia"
|
||||
'Invalid or unsupported network detected, defaulting to Sepolia',
|
||||
);
|
||||
user.setNetwork(DEFAULT_NETWORK);
|
||||
return;
|
||||
@@ -69,20 +67,19 @@ watch(connectedChain, (newVal: any) => {
|
||||
});
|
||||
|
||||
const formatWalletAddress = (): string => {
|
||||
if (!walletAddress.value)
|
||||
throw new Error("Wallet not connected");
|
||||
if (!walletAddress.value) throw new Error('Wallet not connected');
|
||||
const walletAddressLength = walletAddress.value.length;
|
||||
const initialText = walletAddress.value.substring(0, 5);
|
||||
const finalText = walletAddress.value.substring(
|
||||
walletAddressLength - 4,
|
||||
walletAddressLength
|
||||
walletAddressLength,
|
||||
);
|
||||
return `${initialText}...${finalText}`;
|
||||
};
|
||||
|
||||
const disconnectUser = async (): Promise<void> => {
|
||||
user.setWalletAddress(null);
|
||||
await disconnectWallet({ label: connectedWallet.value?.label || "" });
|
||||
await disconnectWallet({ label: connectedWallet.value?.label || '' });
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
@@ -92,10 +89,10 @@ const closeMenu = (): void => {
|
||||
|
||||
const networkChange = async (network: NetworkConfig): Promise<void> => {
|
||||
currencyMenuOpenToggle.value = false;
|
||||
|
||||
|
||||
// If wallet is connected, try to change chain in wallet
|
||||
if (connectedWallet.value) {
|
||||
const chainId = network.id.toString(16)
|
||||
const chainId = network.id.toString(16);
|
||||
try {
|
||||
await setChain({
|
||||
chainId: `0x${chainId}`,
|
||||
@@ -103,7 +100,7 @@ const networkChange = async (network: NetworkConfig): Promise<void> => {
|
||||
});
|
||||
user.setNetwork(network);
|
||||
} catch (error) {
|
||||
console.log("Error changing network", error);
|
||||
console.log('Error changing network', error);
|
||||
}
|
||||
} else {
|
||||
// If no wallet connected, just update the network state
|
||||
@@ -125,20 +122,20 @@ onClickOutside(infoMenuRef, () => {
|
||||
|
||||
const infoMenuOptions: MenuOption[] = [
|
||||
{
|
||||
label: "Explorar Transações",
|
||||
route: "/explore",
|
||||
label: 'Explorar Transações',
|
||||
route: '/explore',
|
||||
showInDesktop: true,
|
||||
showInMobile: false,
|
||||
},
|
||||
{
|
||||
label: "Perguntas frequentes",
|
||||
route: "/faq",
|
||||
label: 'Perguntas frequentes',
|
||||
route: '/faq',
|
||||
showInDesktop: true,
|
||||
showInMobile: false,
|
||||
},
|
||||
{
|
||||
label: "Versões",
|
||||
route: "/versions",
|
||||
label: 'Versões',
|
||||
route: '/versions',
|
||||
showInDesktop: true,
|
||||
showInMobile: false,
|
||||
},
|
||||
@@ -146,40 +143,40 @@ const infoMenuOptions: MenuOption[] = [
|
||||
|
||||
const walletMenuOptions: MenuOption[] = [
|
||||
{
|
||||
label: "Quero vender",
|
||||
label: 'Quero vender',
|
||||
isDynamic: true,
|
||||
dynamicLabel: () => (sellerView.value ? "Quero comprar" : "Quero vender"),
|
||||
dynamicRoute: () => (sellerView.value ? "/" : "/seller"),
|
||||
dynamicLabel: () => (sellerView.value ? 'Quero comprar' : 'Quero vender'),
|
||||
dynamicRoute: () => (sellerView.value ? '/' : '/seller'),
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: "Explorar Transações",
|
||||
route: "/explore",
|
||||
label: 'Explorar Transações',
|
||||
route: '/explore',
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: "Gerenciar Ofertas",
|
||||
route: "/manage_bids",
|
||||
label: 'Gerenciar Ofertas',
|
||||
route: '/manage_bids',
|
||||
showInDesktop: true,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: "Perguntas frequentes",
|
||||
route: "/faq",
|
||||
label: 'Perguntas frequentes',
|
||||
route: '/faq',
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: "Versões",
|
||||
route: "/versions",
|
||||
label: 'Versões',
|
||||
route: '/versions',
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: "Desconectar",
|
||||
route: "/",
|
||||
label: 'Desconectar',
|
||||
route: '/',
|
||||
action: disconnectUser,
|
||||
showInDesktop: true,
|
||||
showInMobile: true,
|
||||
@@ -195,7 +192,10 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
|
||||
<template>
|
||||
<header class="z-20">
|
||||
<RouterLink :to="'/'" class="default-button flex items-center md:h-auto md:py-2 h-10 py-0">
|
||||
<RouterLink
|
||||
:to="'/'"
|
||||
class="default-button flex items-center md:h-auto md:py-2 h-10 py-0"
|
||||
>
|
||||
<img
|
||||
alt="P2Pix logo"
|
||||
class="logo hidden md:inline-block"
|
||||
@@ -217,13 +217,11 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
<button
|
||||
ref="infoMenuRef"
|
||||
class="default-button hidden md:inline-block cursor-pointer"
|
||||
@click="
|
||||
[
|
||||
(infoMenuOpenToggle = !infoMenuOpenToggle),
|
||||
(menuOpenToggle = false),
|
||||
(currencyMenuOpenToggle = false),
|
||||
]
|
||||
"
|
||||
@click="[
|
||||
(infoMenuOpenToggle = !infoMenuOpenToggle),
|
||||
(menuOpenToggle = false),
|
||||
(currencyMenuOpenToggle = false),
|
||||
]"
|
||||
>
|
||||
<h1
|
||||
class="topbar-text topbar-link"
|
||||
@@ -245,7 +243,7 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
<div class="bg-white rounded-md z-10 -left-36 w-52">
|
||||
<template
|
||||
v-for="(option, index) in infoMenuOptions.filter(
|
||||
(opt) => opt.showInDesktop
|
||||
(opt) => opt.showInDesktop,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
@@ -273,7 +271,8 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
<div
|
||||
v-if="
|
||||
index <
|
||||
infoMenuOptions.filter((opt) => opt.showInDesktop).length -
|
||||
infoMenuOptions.filter((opt) => opt.showInDesktop)
|
||||
.length -
|
||||
1
|
||||
"
|
||||
class="w-full flex justify-center"
|
||||
@@ -331,7 +330,7 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
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">
|
||||
{{ sellerView ? "Quero comprar" : "Quero vender" }}
|
||||
{{ sellerView ? 'Quero comprar' : 'Quero vender' }}
|
||||
</div>
|
||||
</RouterLink>
|
||||
<div class="flex flex-col relative">
|
||||
@@ -339,13 +338,11 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
ref="currencyRef"
|
||||
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 }"
|
||||
@click="
|
||||
[
|
||||
(currencyMenuOpenToggle = !currencyMenuOpenToggle),
|
||||
(menuOpenToggle = false),
|
||||
(infoMenuOpenToggle = false),
|
||||
]
|
||||
"
|
||||
@click="[
|
||||
(currencyMenuOpenToggle = !currencyMenuOpenToggle),
|
||||
(menuOpenToggle = false),
|
||||
(infoMenuOpenToggle = false),
|
||||
]"
|
||||
>
|
||||
<img
|
||||
alt="Choosed network image"
|
||||
@@ -357,9 +354,7 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
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 }"
|
||||
>
|
||||
{{
|
||||
user.network.value.name || "Invalid Chain"
|
||||
}}
|
||||
{{ user.network.value.name || 'Invalid Chain' }}
|
||||
</span>
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out mt-1"
|
||||
@@ -426,13 +421,11 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
ref="walletAddressRef"
|
||||
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 }"
|
||||
@click="
|
||||
[
|
||||
(menuOpenToggle = !menuOpenToggle),
|
||||
(currencyMenuOpenToggle = false),
|
||||
(infoMenuOpenToggle = false),
|
||||
]
|
||||
"
|
||||
@click="[
|
||||
(menuOpenToggle = !menuOpenToggle),
|
||||
(currencyMenuOpenToggle = false),
|
||||
(infoMenuOpenToggle = false),
|
||||
]"
|
||||
>
|
||||
<img alt="Account image" src="@/assets/account.svg?url" />
|
||||
<span
|
||||
@@ -463,7 +456,7 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
>
|
||||
<template
|
||||
v-for="(option, index) in walletMenuOptions.filter(
|
||||
(opt) => opt.showInDesktop
|
||||
(opt) => opt.showInDesktop,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
@@ -510,7 +503,7 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
<div class="bg-white rounded-md z-10 h-full">
|
||||
<template
|
||||
v-for="(option, index) in walletMenuOptions.filter(
|
||||
(opt) => opt.showInMobile
|
||||
(opt) => opt.showInMobile,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
@@ -607,6 +600,7 @@ const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
header {
|
||||
@apply flex flex-row justify-between w-full items-center;
|
||||
}
|
||||
@@ -653,6 +647,8 @@ a:hover {
|
||||
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);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { TokenEnum } from "@/model/NetworkEnum";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import TokenSelector from "./TokenSelector.vue";
|
||||
import ErrorMessage from "./ErrorMessage.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: number;
|
||||
selectedToken: TokenEnum;
|
||||
placeholder?: string;
|
||||
showTokenSelector?: boolean;
|
||||
showConversion?: boolean;
|
||||
conversionRate?: number;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}>(),
|
||||
{
|
||||
placeholder: "0",
|
||||
showTokenSelector: true,
|
||||
showConversion: true,
|
||||
conversionRate: 1,
|
||||
minValue: 0,
|
||||
disabled: false,
|
||||
required: false,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: number];
|
||||
"update:selectedToken": [token: TokenEnum];
|
||||
error: [message: string | null];
|
||||
valid: [isValid: boolean];
|
||||
}>();
|
||||
|
||||
const inputValue = ref<string>(String(props.modelValue || ""));
|
||||
const validDecimals = ref(true);
|
||||
const validRange = ref(true);
|
||||
|
||||
const convertedValue = computed(() => {
|
||||
return (props.modelValue * props.conversionRate).toFixed(2);
|
||||
});
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
if (!validDecimals.value) {
|
||||
return "Por favor utilize no máximo 2 casas decimais";
|
||||
}
|
||||
if (!validRange.value) {
|
||||
if (props.minValue && props.modelValue < props.minValue) {
|
||||
return `Valor mínimo: ${props.minValue}`;
|
||||
}
|
||||
if (props.maxValue && props.modelValue > props.maxValue) {
|
||||
return `Valor máximo: ${props.maxValue}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const isValid = computed(() => {
|
||||
return validDecimals.value && validRange.value;
|
||||
});
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const value = target.value;
|
||||
inputValue.value = value;
|
||||
|
||||
const numValue = Number(value);
|
||||
|
||||
// Validar decimais
|
||||
if (decimalCount(value) > 2) {
|
||||
validDecimals.value = false;
|
||||
emit("error", "Por favor utilize no máximo 2 casas decimais");
|
||||
emit("valid", false);
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
// Validar range
|
||||
if (props.minValue !== undefined && numValue < props.minValue) {
|
||||
validRange.value = false;
|
||||
emit("error", `Valor mínimo: ${props.minValue}`);
|
||||
emit("valid", false);
|
||||
return;
|
||||
}
|
||||
if (props.maxValue !== undefined && numValue > props.maxValue) {
|
||||
validRange.value = false;
|
||||
emit("error", `Valor máximo: ${props.maxValue}`);
|
||||
emit("valid", false);
|
||||
return;
|
||||
}
|
||||
validRange.value = true;
|
||||
|
||||
emit("update:modelValue", numValue);
|
||||
emit("error", null);
|
||||
emit("valid", true);
|
||||
};
|
||||
|
||||
const debouncedHandleInput = debounce(handleInput, 500);
|
||||
|
||||
const handleTokenChange = (token: TokenEnum) => {
|
||||
emit("update:selectedToken", token);
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal !== Number(inputValue.value)) {
|
||||
inputValue.value = String(newVal || "");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="amount-input-container">
|
||||
<div class="input-row">
|
||||
<input
|
||||
type="number"
|
||||
:value="inputValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
class="amount-input"
|
||||
:class="{
|
||||
'font-semibold text-xl': modelValue > 0,
|
||||
'has-error': !isValid,
|
||||
}"
|
||||
step="0.01"
|
||||
@input="debouncedHandleInput"
|
||||
/>
|
||||
|
||||
<TokenSelector
|
||||
v-if="showTokenSelector"
|
||||
:model-value="selectedToken"
|
||||
:disabled="disabled"
|
||||
size="md"
|
||||
@update:model-value="handleTokenChange"
|
||||
/>
|
||||
|
||||
<div v-else class="token-display">
|
||||
{{ selectedToken }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="info-row">
|
||||
<p v-if="showConversion" class="conversion-text">
|
||||
~ R$ {{ convertedValue }}
|
||||
</p>
|
||||
<slot name="extra-info"></slot>
|
||||
</div>
|
||||
|
||||
<ErrorMessage
|
||||
v-if="errorMessage"
|
||||
:message="errorMessage"
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.amount-input-container {
|
||||
@apply flex flex-col w-full gap-2;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
@apply flex justify-between items-center w-full gap-4;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
@apply border-none outline-none text-lg text-gray-900 flex-1 bg-transparent;
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.amount-input::-webkit-inner-spin-button,
|
||||
.amount-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.amount-input:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.amount-input.has-error {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
.token-display {
|
||||
@apply flex items-center px-3 py-2 bg-gray-300 rounded-3xl min-w-fit text-gray-900 font-medium;
|
||||
}
|
||||
|
||||
.divider {
|
||||
@apply w-full border-b border-gray-300 my-2;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
|
||||
.conversion-text {
|
||||
@apply text-gray-500 font-normal text-sm;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import bankList from "@/utils/files/isbpList.json";
|
||||
|
||||
export interface Bank {
|
||||
ISPB: string;
|
||||
longName: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string | null;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
placeholder: "Busque e selecione seu banco",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
change: [bank: Bank];
|
||||
}>();
|
||||
|
||||
const bankItems = computed(() => {
|
||||
return bankList.map((bank) => ({
|
||||
value: bank.ISPB,
|
||||
label: bank.longName,
|
||||
bank: bank,
|
||||
}));
|
||||
});
|
||||
|
||||
const selectedItem = computed(() => {
|
||||
if (!props.modelValue) return null;
|
||||
return bankItems.value.find((item) => item.value === props.modelValue);
|
||||
});
|
||||
|
||||
const searchQuery = computed({
|
||||
get: () => selectedItem.value?.label || "",
|
||||
set: (value: string) => {
|
||||
// Handled by input
|
||||
},
|
||||
});
|
||||
|
||||
const filteredBanks = computed(() => {
|
||||
if (!searchQuery.value) return [];
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return bankList
|
||||
.filter((bank) => bank.longName.toLowerCase().includes(query))
|
||||
.slice(0, 10);
|
||||
});
|
||||
|
||||
const showBankList = computed(() => {
|
||||
return filteredBanks.value.length > 0 && searchQuery.value.length > 0;
|
||||
});
|
||||
|
||||
const selectBank = (bank: Bank) => {
|
||||
emit("update:modelValue", bank.ISPB);
|
||||
emit("change", bank);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bank-selector">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
class="bank-input"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<transition name="dropdown-fade">
|
||||
<div v-if="showBankList" class="bank-list">
|
||||
<div
|
||||
v-for="bank in filteredBanks"
|
||||
:key="bank.ISPB"
|
||||
class="bank-item"
|
||||
@click="selectBank(bank)"
|
||||
>
|
||||
<span class="bank-name">{{ bank.longName }}</span>
|
||||
<span class="bank-ispb">{{ bank.ISPB }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bank-selector {
|
||||
@apply relative w-full;
|
||||
}
|
||||
|
||||
.bank-input {
|
||||
@apply w-full px-4 py-3 border-none outline-none rounded-lg bg-white text-gray-900 text-sm;
|
||||
}
|
||||
|
||||
.bank-input:focus {
|
||||
@apply ring-2 ring-indigo-800;
|
||||
}
|
||||
|
||||
.bank-input:disabled {
|
||||
@apply opacity-50 cursor-not-allowed bg-gray-100;
|
||||
}
|
||||
|
||||
.bank-list {
|
||||
@apply absolute top-full left-0 right-0 mt-2 bg-white rounded-lg border border-gray-300 shadow-lg z-50 max-h-64 overflow-y-auto;
|
||||
}
|
||||
|
||||
.bank-item {
|
||||
@apply flex justify-between items-center px-4 py-3 cursor-pointer hover:bg-gray-100 transition-colors;
|
||||
}
|
||||
|
||||
.bank-name {
|
||||
@apply text-gray-900 font-medium text-sm flex-1;
|
||||
}
|
||||
|
||||
.bank-ispb {
|
||||
@apply text-gray-500 text-xs ml-2;
|
||||
}
|
||||
|
||||
/* Animação */
|
||||
.dropdown-fade-enter-active,
|
||||
.dropdown-fade-leave-active {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
.dropdown-fade-enter-from,
|
||||
.dropdown-fade-leave-to {
|
||||
@apply opacity-0 -translate-y-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref } from 'vue';
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const alertText = ref<string>("");
|
||||
const alertPaddingLeft = ref<string>("18rem");
|
||||
const alertText = ref<string>('');
|
||||
const alertPaddingLeft = ref<string>('18rem');
|
||||
|
||||
if (props.type === "sell") {
|
||||
alertPaddingLeft.value = "30%";
|
||||
} else if (props.type === "buy") {
|
||||
alertPaddingLeft.value = "30%";
|
||||
} else if (props.type === "withdraw") {
|
||||
alertPaddingLeft.value = "40%";
|
||||
} else if (props.type === "redirect") {
|
||||
alertPaddingLeft.value = "35%";
|
||||
if (props.type === 'sell') {
|
||||
alertPaddingLeft.value = '30%';
|
||||
} else if (props.type === 'buy') {
|
||||
alertPaddingLeft.value = '30%';
|
||||
} else if (props.type === 'withdraw') {
|
||||
alertPaddingLeft.value = '40%';
|
||||
} else if (props.type === 'redirect') {
|
||||
alertPaddingLeft.value = '35%';
|
||||
}
|
||||
|
||||
switch (props.type) {
|
||||
case "buy":
|
||||
case 'buy':
|
||||
alertText.value =
|
||||
"Tudo certo! Os tokens já foram retirados da oferta e estão disponíveis na sua carteira.";
|
||||
'Tudo certo! Os tokens já foram retirados da oferta e estão disponíveis na sua carteira.';
|
||||
break;
|
||||
case "sell":
|
||||
case 'sell':
|
||||
alertText.value =
|
||||
"Tudo certo! Os tokens já foram reservados e sua oferta está disponível.";
|
||||
'Tudo certo! Os tokens já foram reservados e sua oferta está disponível.';
|
||||
break;
|
||||
case "redirect":
|
||||
alertText.value = "Existe uma compra em aberto. Continuar?";
|
||||
case 'redirect':
|
||||
alertText.value = 'Existe uma compra em aberto. Continuar?';
|
||||
break;
|
||||
case "withdraw":
|
||||
alertText.value = "Tudo certo! Saque realizado com sucesso!";
|
||||
case 'withdraw':
|
||||
alertText.value = 'Tudo certo! Saque realizado com sucesso!';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
||||
export type ButtonSize = "sm" | "md" | "lg" | "xl";
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -9,25 +9,25 @@ const props = withDefaults(
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
icon?: string;
|
||||
iconPosition?: "left" | "right";
|
||||
iconPosition?: 'left' | 'right';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
isDisabled: false,
|
||||
variant: "primary",
|
||||
size: "xl",
|
||||
iconPosition: "left",
|
||||
variant: 'primary',
|
||||
size: 'xl',
|
||||
iconPosition: 'left',
|
||||
fullWidth: true,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(["buttonClicked"]);
|
||||
const emit = defineEmits(['buttonClicked']);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.isDisabled && !props.loading) {
|
||||
emit("buttonClicked");
|
||||
emit('buttonClicked');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -64,6 +64,7 @@ const handleClick = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.button {
|
||||
@apply rounded-lg font-semibold transition-all duration-200 cursor-pointer flex items-center justify-center gap-2;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isRedirectModal: Boolean,
|
||||
});
|
||||
|
||||
const modalColor = ref<string>("white");
|
||||
const modalHeight = ref<string>("250px");
|
||||
const pFontSize = ref<string>("16px");
|
||||
const modalColor = ref<string>('white');
|
||||
const modalHeight = ref<string>('250px');
|
||||
const pFontSize = ref<string>('16px');
|
||||
|
||||
if (props.isRedirectModal) {
|
||||
modalColor.value = "rgba(251, 191, 36, 1)";
|
||||
modalHeight.value = "150px";
|
||||
pFontSize.value = "20px";
|
||||
modalColor.value = 'rgba(251, 191, 36, 1)';
|
||||
modalHeight.value = '150px';
|
||||
pFontSize.value = '20px';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import { ref, computed } from "vue";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import ChevronDown from "@/assets/chevronDown.svg";
|
||||
|
||||
export interface DropdownItem<T = any> {
|
||||
value: T;
|
||||
label: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: DropdownItem<T>[];
|
||||
modelValue: T;
|
||||
placeholder?: string;
|
||||
searchable?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
showIcon?: boolean;
|
||||
}>(),
|
||||
{
|
||||
placeholder: "Selecione...",
|
||||
searchable: false,
|
||||
disabled: false,
|
||||
size: "md",
|
||||
showIcon: true,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: T];
|
||||
change: [value: T];
|
||||
}>();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const searchQuery = ref("");
|
||||
const dropdownRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const selectedItem = computed(() => {
|
||||
return props.items.find((item) => item.value === props.modelValue);
|
||||
});
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!props.searchable || !searchQuery.value) {
|
||||
return props.items;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return props.items.filter((item) =>
|
||||
item.label.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const toggleDropdown = () => {
|
||||
if (!props.disabled) {
|
||||
isOpen.value = !isOpen.value;
|
||||
if (!isOpen.value) {
|
||||
searchQuery.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = (item: DropdownItem<T>) => {
|
||||
if (!item.disabled) {
|
||||
emit("update:modelValue", item.value);
|
||||
emit("change", item.value);
|
||||
isOpen.value = false;
|
||||
searchQuery.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
onClickOutside(dropdownRef, () => {
|
||||
isOpen.value = false;
|
||||
searchQuery.value = "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="dropdownRef" class="dropdown-container">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'dropdown-trigger',
|
||||
`size-${size}`,
|
||||
{ disabled: disabled, open: isOpen },
|
||||
]"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<img
|
||||
v-if="selectedItem?.icon && showIcon"
|
||||
:src="selectedItem.icon"
|
||||
:alt="selectedItem.label"
|
||||
class="item-icon"
|
||||
/>
|
||||
<span class="selected-text">
|
||||
{{ selectedItem?.label || placeholder }}
|
||||
</span>
|
||||
<ChevronDown
|
||||
class="chevron"
|
||||
:class="{ rotated: isOpen }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<transition name="dropdown-fade">
|
||||
<div v-if="isOpen" class="dropdown-menu">
|
||||
<input
|
||||
v-if="searchable"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Buscar..."
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<div class="items-container">
|
||||
<div
|
||||
v-for="item in filteredItems"
|
||||
:key="String(item.value)"
|
||||
:class="[
|
||||
'dropdown-item',
|
||||
{
|
||||
selected: item.value === modelValue,
|
||||
disabled: item.disabled,
|
||||
},
|
||||
]"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
<img
|
||||
v-if="item.icon && showIcon"
|
||||
:src="item.icon"
|
||||
:alt="item.label"
|
||||
class="item-icon"
|
||||
/>
|
||||
<span class="item-label">{{ item.label }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredItems.length === 0" class="no-results">
|
||||
Nenhum resultado encontrado
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dropdown-container {
|
||||
@apply relative inline-block;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
@apply flex items-center gap-2 bg-gray-300 hover:bg-gray-200 rounded-3xl transition-colors cursor-pointer border-none outline-none;
|
||||
}
|
||||
|
||||
.dropdown-trigger:focus {
|
||||
@apply outline-2 outline-indigo-800;
|
||||
}
|
||||
|
||||
.dropdown-trigger.disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.dropdown-trigger.disabled:hover {
|
||||
@apply bg-gray-300;
|
||||
}
|
||||
|
||||
.size-sm {
|
||||
@apply px-2 py-1 text-sm;
|
||||
}
|
||||
|
||||
.size-md {
|
||||
@apply px-3 py-2 text-base;
|
||||
}
|
||||
|
||||
.size-lg {
|
||||
@apply px-4 py-3 text-lg;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
@apply sm:w-fit w-4 flex-shrink-0;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
@apply text-gray-900 font-medium min-w-fit;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
@apply transition-transform duration-300 invert pr-1;
|
||||
}
|
||||
|
||||
.chevron.rotated {
|
||||
@apply rotate-180;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply absolute right-0 mt-2 bg-white rounded-xl border border-gray-300 shadow-md z-50 min-w-max w-full;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@apply w-full px-4 py-3 border-b border-gray-200 outline-none text-gray-900;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
@apply border-indigo-800;
|
||||
}
|
||||
|
||||
.items-container {
|
||||
@apply max-h-64 overflow-y-auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@apply flex items-center gap-2 px-4 py-4 cursor-pointer hover:bg-gray-300 transition-colors text-gray-900 font-semibold text-sm;
|
||||
}
|
||||
|
||||
.dropdown-item.selected {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled:hover {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
@apply text-end;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
@apply px-4 py-6 text-center text-gray-500 text-sm;
|
||||
}
|
||||
|
||||
/* Animação */
|
||||
.dropdown-fade-enter-active,
|
||||
.dropdown-fade-leave-active {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
.dropdown-fade-enter-from,
|
||||
.dropdown-fade-leave-to {
|
||||
@apply opacity-0 -translate-y-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
export type ErrorType = "error" | "warning" | "info";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
message: string;
|
||||
type?: ErrorType;
|
||||
centered?: boolean;
|
||||
icon?: boolean;
|
||||
}>(),
|
||||
{
|
||||
type: "error",
|
||||
centered: true,
|
||||
icon: false,
|
||||
}
|
||||
);
|
||||
|
||||
const colorClasses = {
|
||||
error: "text-red-500",
|
||||
warning: "text-amber-500",
|
||||
info: "text-blue-500",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['error-message-container', { centered: centered }]">
|
||||
<div :class="['error-message', colorClasses[type]]">
|
||||
<span v-if="icon" class="icon">⚠</span>
|
||||
<span class="message">{{ message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error-message-container {
|
||||
@apply flex w-full;
|
||||
}
|
||||
|
||||
.error-message-container.centered {
|
||||
@apply justify-center;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply font-normal text-sm flex items-center gap-2;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
.message {
|
||||
@apply leading-tight;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
export type FormCardPadding = "sm" | "md" | "lg";
|
||||
type FormCardPadding = 'sm' | 'md' | 'lg';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -8,10 +8,10 @@ const props = withDefaults(
|
||||
noBorder?: boolean;
|
||||
}>(),
|
||||
{
|
||||
padding: "md",
|
||||
padding: 'md',
|
||||
fullWidth: true,
|
||||
noBorder: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -28,6 +28,7 @@ const props = withDefaults(
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.form-card {
|
||||
@apply flex flex-col bg-white rounded-lg;
|
||||
}
|
||||
@@ -52,4 +53,3 @@ const props = withDefaults(
|
||||
@apply px-12 py-8;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
export type IconButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
||||
export type IconButtonSize = "sm" | "md" | "lg";
|
||||
export type IconPosition = "left" | "right";
|
||||
type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
type IconButtonSize = 'sm' | 'md' | 'lg';
|
||||
type IconPosition = 'left' | 'right';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -14,12 +14,12 @@ const props = withDefaults(
|
||||
fullWidth?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: "outline",
|
||||
size: "md",
|
||||
iconPosition: "left",
|
||||
variant: 'outline',
|
||||
size: 'md',
|
||||
iconPosition: 'left',
|
||||
disabled: false,
|
||||
fullWidth: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -28,7 +28,7 @@ const emit = defineEmits<{
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled) {
|
||||
emit("click");
|
||||
emit('click');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -62,6 +62,7 @@ const handleClick = () => {
|
||||
</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;
|
||||
}
|
||||
@@ -144,4 +145,3 @@ const handleClick = () => {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
text: string;
|
||||
placement?: "top" | "bottom" | "left" | "right";
|
||||
iconSrc?: string;
|
||||
showOnHover?: boolean;
|
||||
}>(),
|
||||
{
|
||||
placement: "right",
|
||||
iconSrc: "",
|
||||
showOnHover: true,
|
||||
}
|
||||
);
|
||||
|
||||
const showTooltip = ref<boolean>(false);
|
||||
const reference = ref<HTMLElement | null>(null);
|
||||
const floating = ref<HTMLElement | null>(null);
|
||||
const floatingArrow = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
useFloating(reference, floating, {
|
||||
placement: props.placement,
|
||||
middleware: [offset(10), flip(), shift(), arrow({ element: floatingArrow })],
|
||||
});
|
||||
});
|
||||
|
||||
const handleMouseOver = () => {
|
||||
if (props.showOnHover) {
|
||||
showTooltip.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseOut = () => {
|
||||
if (props.showOnHover) {
|
||||
showTooltip.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTooltip = () => {
|
||||
if (!props.showOnHover) {
|
||||
showTooltip.value = !showTooltip.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="info-tooltip-container">
|
||||
<img
|
||||
:src="iconSrc || '/src/assets/info.svg'"
|
||||
alt="info icon"
|
||||
class="info-icon"
|
||||
ref="reference"
|
||||
@mouseover="handleMouseOver"
|
||||
@mouseout="handleMouseOut"
|
||||
@click="toggleTooltip"
|
||||
/>
|
||||
<div
|
||||
v-if="showTooltip"
|
||||
role="tooltip"
|
||||
ref="floating"
|
||||
class="tooltip-content"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.info-tooltip-container {
|
||||
@apply relative inline-block;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
@apply cursor-pointer transition-opacity hover:opacity-70;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
@apply bg-white text-gray-900 font-medium text-xs md:text-sm px-3 py-2 rounded border-2 border-emerald-500 z-50 max-w-xs shadow-lg;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.tooltip-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,13 +12,11 @@ const props = defineProps({
|
||||
<span
|
||||
class="text font-bold sm:text-3xl text-2xl sm:max-w-[29rem] max-w-[20rem]"
|
||||
>
|
||||
{{ props.title ? props.title : "Confirme em sua carteira" }}
|
||||
{{ props.title ? props.title : 'Confirme em sua carteira' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="main-container max-w-md">
|
||||
<div
|
||||
class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg border-y-10"
|
||||
>
|
||||
<div class="flex flex-col w-full bg-white sm:px-10 px-4 py-5 rounded-lg">
|
||||
<div
|
||||
class="flex flex-col text-center justify-center w-full items-center p-2 px-3 rounded-3xl lg:min-w-fit gap-1"
|
||||
>
|
||||
@@ -38,6 +36,7 @@ const props = defineProps({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
@@ -60,12 +59,12 @@ const props = defineProps({
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import SpinnerComponent from "./SpinnerComponent.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
message?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
centered?: boolean;
|
||||
inline?: boolean;
|
||||
}>(),
|
||||
{
|
||||
message: "Carregando...",
|
||||
size: "md",
|
||||
centered: true,
|
||||
inline: false,
|
||||
}
|
||||
);
|
||||
|
||||
const sizeMap = {
|
||||
sm: { spinner: "4", text: "text-sm" },
|
||||
md: { spinner: "6", text: "text-base" },
|
||||
lg: { spinner: "8", text: "text-lg" },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'loading-state',
|
||||
{ centered: centered, inline: inline },
|
||||
]"
|
||||
>
|
||||
<span v-if="message" :class="['loading-message', sizeMap[size].text]">
|
||||
{{ message }}
|
||||
</span>
|
||||
<SpinnerComponent
|
||||
:width="sizeMap[size].spinner"
|
||||
:height="sizeMap[size].spinner"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.loading-state {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
||||
.loading-state.centered {
|
||||
@apply justify-center;
|
||||
}
|
||||
|
||||
.loading-state.inline {
|
||||
@apply inline-flex;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
@apply text-gray-900 font-normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { getNetworkImage } from "@/utils/imagesPath";
|
||||
import type { NetworkConfig } from "@/model/NetworkEnum";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
networks: NetworkConfig[];
|
||||
size?: "sm" | "md" | "lg";
|
||||
showLabel?: boolean;
|
||||
}>(),
|
||||
{
|
||||
size: "md",
|
||||
showLabel: false,
|
||||
}
|
||||
);
|
||||
|
||||
const sizeMap = {
|
||||
sm: 16,
|
||||
md: 24,
|
||||
lg: 32,
|
||||
};
|
||||
|
||||
const networkData = computed(() => {
|
||||
return props.networks.map((network) => ({
|
||||
network,
|
||||
image: getNetworkImage(network.name),
|
||||
name: network.name,
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="network-badges">
|
||||
<div
|
||||
v-for="data in networkData"
|
||||
:key="data.network.id"
|
||||
class="network-badge"
|
||||
:title="data.name"
|
||||
>
|
||||
<img
|
||||
:alt="`${data.name} logo`"
|
||||
:src="data.image"
|
||||
:width="sizeMap[size]"
|
||||
:height="sizeMap[size]"
|
||||
class="network-icon"
|
||||
/>
|
||||
<span v-if="showLabel" class="network-label">
|
||||
{{ data.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.network-badges {
|
||||
@apply flex gap-2 items-center;
|
||||
}
|
||||
|
||||
.network-badge {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.network-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
|
||||
.network-label {
|
||||
@apply text-sm font-medium text-gray-900;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { Networks } from "@/config/networks";
|
||||
import type { NetworkConfig } from "@/model/NetworkEnum";
|
||||
import { getNetworkImage } from "@/utils/imagesPath";
|
||||
import Dropdown, { type DropdownItem } from "./Dropdown.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: NetworkConfig;
|
||||
disabled?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
availableNetworks?: NetworkConfig[];
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
size: "md",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: NetworkConfig];
|
||||
change: [value: NetworkConfig];
|
||||
}>();
|
||||
|
||||
const networkItems = computed((): DropdownItem<NetworkConfig>[] => {
|
||||
return Object.values(Networks).map((network) => ({
|
||||
value: network,
|
||||
label: network.name,
|
||||
icon: getNetworkImage(network.name),
|
||||
}));
|
||||
});
|
||||
|
||||
const handleChange = (value: NetworkConfig) => {
|
||||
emit("update:modelValue", value);
|
||||
emit("change", value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown
|
||||
:model-value="modelValue"
|
||||
:items="networkItems"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:show-icon="true"
|
||||
@update:model-value="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
export type HeaderSize = "sm" | "md" | "lg";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
size?: HeaderSize;
|
||||
centered?: boolean;
|
||||
}>(),
|
||||
{
|
||||
size: "lg",
|
||||
centered: true,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['page-header', `size-${size}`, { centered: centered }]"
|
||||
>
|
||||
<h1 class="title text-white font-extrabold">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="subtitle" class="subtitle text-white font-medium">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.page-header.centered {
|
||||
@apply items-center justify-center text-center;
|
||||
}
|
||||
|
||||
/* Tamanhos */
|
||||
.size-sm .title {
|
||||
@apply sm:text-2xl text-xl sm:max-w-[20rem] max-w-[16rem];
|
||||
}
|
||||
|
||||
.size-sm .subtitle {
|
||||
@apply sm:text-sm text-xs sm:max-w-[18rem] max-w-[14rem];
|
||||
}
|
||||
|
||||
.size-md .title {
|
||||
@apply sm:text-4xl text-2xl sm:max-w-[28rem] max-w-[22rem];
|
||||
}
|
||||
|
||||
.size-md .subtitle {
|
||||
@apply sm:text-base text-sm sm:max-w-[26rem] max-w-[20rem];
|
||||
}
|
||||
|
||||
.size-lg .title {
|
||||
@apply sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem];
|
||||
}
|
||||
|
||||
.size-lg .subtitle {
|
||||
@apply sm:text-base text-sm sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,10 +10,10 @@ const getCustomClass = () => {
|
||||
return [
|
||||
`w-${props.width}`,
|
||||
`h-${props.height}`,
|
||||
`fill-white`,
|
||||
"text-gray-200",
|
||||
"animate-spin",
|
||||
"dark:text-gray-600",
|
||||
'fill-white',
|
||||
'text-gray-200',
|
||||
'animate-spin',
|
||||
'dark:text-gray-600',
|
||||
];
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed } from 'vue';
|
||||
|
||||
export type StatusType = "open" | "expired" | "completed" | "pending";
|
||||
export type StatusType = 'open' | 'expired' | 'completed' | 'pending';
|
||||
|
||||
const props = defineProps<{
|
||||
status: StatusType;
|
||||
@@ -11,20 +11,20 @@ const props = defineProps<{
|
||||
const statusConfig = computed(() => {
|
||||
const configs: Record<StatusType, { text: string; color: string }> = {
|
||||
open: {
|
||||
text: "Em Aberto",
|
||||
color: "bg-amber-300",
|
||||
text: 'Em Aberto',
|
||||
color: 'bg-amber-300',
|
||||
},
|
||||
expired: {
|
||||
text: "Expirado",
|
||||
color: "bg-[#94A3B8]",
|
||||
text: 'Expirado',
|
||||
color: 'bg-[#94A3B8]',
|
||||
},
|
||||
completed: {
|
||||
text: "Finalizado",
|
||||
color: "bg-emerald-300",
|
||||
text: 'Finalizado',
|
||||
color: 'bg-emerald-300',
|
||||
},
|
||||
pending: {
|
||||
text: "Pendente",
|
||||
color: "bg-gray-300",
|
||||
text: 'Pendente',
|
||||
color: 'bg-gray-300',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,8 +43,8 @@ const displayText = computed(() => {
|
||||
</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>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<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";
|
||||
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();
|
||||
@@ -23,9 +23,9 @@ const checkNetwork = () => {
|
||||
const switchNetwork = async () => {
|
||||
try {
|
||||
if (connectedWallet.value && connectedWallet.value.provider) {
|
||||
let chainId = network.value.id.toString(16);
|
||||
const chainId = network.value.id.toString(16);
|
||||
await connectedWallet.value.provider.request({
|
||||
method: "wallet_switchEthereumChain",
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [
|
||||
{
|
||||
chainId: `0x${chainId}`,
|
||||
@@ -34,7 +34,7 @@ const switchNetwork = async () => {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to switch network:", error);
|
||||
console.error('Failed to switch network:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +66,9 @@ watch(network, checkNetwork, { immediate: true });
|
||||
<style scoped>
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { TokenEnum } from "@/model/NetworkEnum";
|
||||
import { getTokenImage } from "@/utils/imagesPath";
|
||||
import Dropdown, { type DropdownItem } from "./Dropdown.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: TokenEnum;
|
||||
disabled?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
size: "md",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: TokenEnum];
|
||||
change: [value: TokenEnum];
|
||||
}>();
|
||||
|
||||
const tokenItems = computed((): DropdownItem<TokenEnum>[] => {
|
||||
return Object.values(TokenEnum).map((token) => ({
|
||||
value: token,
|
||||
label: token,
|
||||
icon: getTokenImage(token),
|
||||
}));
|
||||
});
|
||||
|
||||
const handleChange = (value: TokenEnum) => {
|
||||
emit("update:modelValue", value);
|
||||
emit("change", value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown
|
||||
:model-value="modelValue"
|
||||
:items="tokenItems"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:show-icon="true"
|
||||
@update:model-value="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
14
src/components/ui/VersionFooter.vue
Normal file
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>
|
||||
@@ -1,152 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import CustomButton from "./CustomButton.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
walletAddress: string | null;
|
||||
variant?: "primary" | "secondary" | "outline";
|
||||
showMenu?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: "primary",
|
||||
showMenu: true,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
connect: [];
|
||||
disconnect: [];
|
||||
viewTransactions: [];
|
||||
}>();
|
||||
|
||||
const menuOpen = ref(false);
|
||||
const menuRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const isConnected = computed(() => {
|
||||
return !!props.walletAddress;
|
||||
});
|
||||
|
||||
const formattedAddress = computed(() => {
|
||||
if (!props.walletAddress) return "";
|
||||
|
||||
const address = props.walletAddress;
|
||||
const length = address.length;
|
||||
const start = address.substring(0, 5);
|
||||
const end = address.substring(length - 4, length);
|
||||
|
||||
return `${start}...${end}`;
|
||||
});
|
||||
|
||||
const handleConnect = () => {
|
||||
emit("connect");
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
menuOpen.value = false;
|
||||
emit("disconnect");
|
||||
};
|
||||
|
||||
const handleViewTransactions = () => {
|
||||
menuOpen.value = false;
|
||||
emit("viewTransactions");
|
||||
};
|
||||
|
||||
const toggleMenu = () => {
|
||||
if (isConnected.value && props.showMenu) {
|
||||
menuOpen.value = !menuOpen.value;
|
||||
}
|
||||
};
|
||||
|
||||
onClickOutside(menuRef, () => {
|
||||
menuOpen.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wallet-connect-container">
|
||||
<CustomButton
|
||||
v-if="!isConnected"
|
||||
text="Conectar carteira"
|
||||
:variant="variant"
|
||||
@button-clicked="handleConnect"
|
||||
/>
|
||||
|
||||
<div v-else ref="menuRef" class="wallet-connected">
|
||||
<button
|
||||
type="button"
|
||||
class="wallet-button"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<span class="wallet-address">{{ formattedAddress }}</span>
|
||||
<div class="wallet-indicator"></div>
|
||||
</button>
|
||||
|
||||
<transition name="menu-fade">
|
||||
<div v-if="menuOpen && showMenu" class="wallet-menu">
|
||||
<button
|
||||
type="button"
|
||||
class="menu-item"
|
||||
@click="handleViewTransactions"
|
||||
>
|
||||
<span>Ver transações</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-item disconnect"
|
||||
@click="handleDisconnect"
|
||||
>
|
||||
<span>Desconectar</span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wallet-connect-container {
|
||||
@apply relative inline-block;
|
||||
}
|
||||
|
||||
.wallet-connected {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.wallet-button {
|
||||
@apply flex items-center gap-3 px-4 py-2 bg-white border-2 border-amber-400 rounded-lg hover:bg-amber-50 transition-colors cursor-pointer;
|
||||
}
|
||||
|
||||
.wallet-address {
|
||||
@apply text-gray-900 font-semibold text-sm;
|
||||
}
|
||||
|
||||
.wallet-indicator {
|
||||
@apply w-2 h-2 bg-emerald-500 rounded-full;
|
||||
}
|
||||
|
||||
.wallet-menu {
|
||||
@apply absolute top-full right-0 mt-2 bg-white rounded-lg border border-gray-300 shadow-lg z-50 min-w-[200px] overflow-hidden;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@apply w-full px-4 py-3 text-left text-gray-900 font-medium text-sm hover:bg-gray-100 transition-colors cursor-pointer border-none;
|
||||
}
|
||||
|
||||
.menu-item.disconnect {
|
||||
@apply text-red-500 hover:bg-red-50;
|
||||
}
|
||||
|
||||
/* Animação */
|
||||
.menu-fade-enter-active,
|
||||
.menu-fade-leave-active {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
.menu-fade-enter-from,
|
||||
.menu-fade-leave-to {
|
||||
@apply opacity-0 -translate-y-2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import { isTestnetEnvironment } from '@/config/networks';
|
||||
import { sepolia, rootstock, rootstockTestnet } from "viem/chains";
|
||||
import { sepolia, rootstock, rootstockTestnet } from 'viem/chains';
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
@@ -30,32 +29,19 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
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'
|
||||
totalReleases: '0',
|
||||
});
|
||||
|
||||
const getGraphQLUrl = () => {
|
||||
const rskNetworkName = isTestnetEnvironment() ? rootstockTestnet.name : rootstock.name;
|
||||
|
||||
switch (network.value.name) {
|
||||
case sepolia.name:
|
||||
return import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL!;
|
||||
case rskNetworkName:
|
||||
return import.meta.env.VITE_RSK_SUBGRAPH_URL!;
|
||||
default:
|
||||
throw new Error(`Unsupported network: ${network.value.name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const executeQuery = async (query: string, variables: any = {}) => {
|
||||
const url = getGraphQLUrl();
|
||||
|
||||
const url = network.value.subgraphUrls[0]; // TODO: try all available URLs
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -73,7 +59,7 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(data.errors[0]?.message || 'GraphQL error');
|
||||
}
|
||||
@@ -143,7 +129,8 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
const data = await executeQuery(query, { first: 50 });
|
||||
transactionsData.value = processActivityData(data);
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch transactions';
|
||||
error.value =
|
||||
err instanceof Error ? err.message : 'Failed to fetch transactions';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -207,7 +194,10 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
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';
|
||||
error.value =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to fetch user transactions';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -220,7 +210,7 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0'
|
||||
totalReleases: '0',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -263,9 +253,9 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
|
||||
const processActivityData = (data: any): Transaction[] => {
|
||||
if (!data) return [];
|
||||
|
||||
|
||||
const activities: Transaction[] = [];
|
||||
|
||||
|
||||
if (data.depositAddeds) {
|
||||
data.depositAddeds.forEach((deposit: any) => {
|
||||
activities.push({
|
||||
@@ -278,11 +268,11 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
buyer: undefined,
|
||||
amount: deposit.amount,
|
||||
token: deposit.token,
|
||||
timestamp: formatTimestamp(deposit.blockTimestamp)
|
||||
timestamp: formatTimestamp(deposit.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (data.depositWithdrawns) {
|
||||
data.depositWithdrawns.forEach((withdrawal: any) => {
|
||||
activities.push({
|
||||
@@ -295,11 +285,11 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
buyer: undefined,
|
||||
amount: withdrawal.amount,
|
||||
token: withdrawal.token,
|
||||
timestamp: formatTimestamp(withdrawal.blockTimestamp)
|
||||
timestamp: formatTimestamp(withdrawal.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (data.lockAddeds) {
|
||||
data.lockAddeds.forEach((lock: any) => {
|
||||
activities.push({
|
||||
@@ -312,11 +302,11 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
buyer: lock.buyer,
|
||||
amount: lock.amount,
|
||||
token: lock.token,
|
||||
timestamp: formatTimestamp(lock.blockTimestamp)
|
||||
timestamp: formatTimestamp(lock.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (data.lockReleaseds) {
|
||||
data.lockReleaseds.forEach((release: any) => {
|
||||
activities.push({
|
||||
@@ -329,11 +319,11 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
buyer: release.buyer,
|
||||
amount: release.amount,
|
||||
token: 'BRZ', // Default token
|
||||
timestamp: formatTimestamp(release.blockTimestamp)
|
||||
timestamp: formatTimestamp(release.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (data.lockReturneds) {
|
||||
data.lockReturneds.forEach((returnTx: any) => {
|
||||
activities.push({
|
||||
@@ -346,18 +336,20 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
buyer: returnTx.buyer,
|
||||
amount: '0', // Return doesn't have amount
|
||||
token: 'BRZ', // Default token
|
||||
timestamp: formatTimestamp(returnTx.blockTimestamp)
|
||||
timestamp: formatTimestamp(returnTx.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return activities.sort((a, b) => parseInt(b.blockTimestamp) - parseInt(a.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`;
|
||||
@@ -366,7 +358,8 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
|
||||
const formatAmount = (amount: string): string => {
|
||||
const num = parseFloat(amount);
|
||||
if (num >= 1000000000000000) return `${(num / 1000000000000000).toFixed(1)}Q`;
|
||||
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`;
|
||||
@@ -382,7 +375,7 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0'
|
||||
totalReleases: '0',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -423,7 +416,6 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (data.lockReturneds) {
|
||||
data.lockReturneds.forEach((returnTx: any) => {
|
||||
totalTransactions++;
|
||||
@@ -435,27 +427,28 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
totalTransactions: totalTransactions.toString(),
|
||||
totalLocks: totalLocks.toString(),
|
||||
totalDeposits: totalDeposits.toString(),
|
||||
totalReleases: totalReleases.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);
|
||||
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)
|
||||
filtered = filtered.filter(
|
||||
(tx) =>
|
||||
tx.seller?.toLowerCase().includes(searchLower) ||
|
||||
tx.buyer?.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
@@ -470,6 +463,6 @@ export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
fetchAllActivity,
|
||||
fetchUserActivity,
|
||||
fetchAnalytics,
|
||||
clearData
|
||||
clearData,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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"
|
||||
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 balance = ref('');
|
||||
const network = ref(DEFAULT_NETWORK);
|
||||
const selectedToken = ref<TokenEnum>(TokenEnum.BRZ);
|
||||
const loadingLock = ref(false);
|
||||
@@ -15,7 +15,7 @@ const depositsValidList = ref<ValidDeposit[]>([]);
|
||||
const loadingWalletTransactions = ref(false);
|
||||
const loadingNetworkLiquidity = ref(false);
|
||||
const seller = ref<Participant>({} as Participant);
|
||||
const sellerId = ref("");
|
||||
const sellerId = ref('');
|
||||
|
||||
export function useUser() {
|
||||
// Actions become regular functions
|
||||
@@ -50,7 +50,7 @@ export function useUser() {
|
||||
}
|
||||
|
||||
// Find network by chain ID
|
||||
const chain = Object.values(Networks).find(n => n.id === chainId);
|
||||
const chain = Object.values(Networks).find((n) => n.id === chainId);
|
||||
if (chain) {
|
||||
network.value = chain;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,60 @@
|
||||
import { sepolia, rootstock, rootstockTestnet } from "viem/chains";
|
||||
import { NetworkConfig } from "@/model/NetworkEnum"
|
||||
import { mainnet, sepolia, rootstock, rootstockTestnet } from 'viem/chains';
|
||||
import { NetworkConfig } from '@/model/NetworkEnum';
|
||||
// TODO: import addresses from p2pix-smart-contracts deployments
|
||||
|
||||
export const isTestnetEnvironment = () => {
|
||||
return import.meta.env.VITE_ENVIRONMENT === 'testnet' ||
|
||||
import.meta.env.NODE_ENV === 'development' ||
|
||||
import.meta.env.MODE === 'development';
|
||||
};
|
||||
|
||||
export const Networks: {[key:string]: NetworkConfig} = {
|
||||
sepolia: { ...sepolia,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_SEPOLIA_API_URL]}},
|
||||
contracts: { ...sepolia.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_SEPOLIA_P2PIX_ADDRESS } },
|
||||
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_SEPOLIA_TOKEN_ADDRESS } },
|
||||
subgraphUrls: [import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL]
|
||||
BRZ: { address: import.meta.env.VITE_MAINNET_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_MAINNET_SUBGRAPH_URL],
|
||||
},
|
||||
rootstock: { ...(isTestnetEnvironment() ? rootstockTestnet : rootstock),
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL]}},
|
||||
contracts: { ...(isTestnetEnvironment() ? rootstockTestnet.contracts : rootstock.contracts),
|
||||
p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS } },
|
||||
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]
|
||||
BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL],
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_NETWORK = Networks.sepolia;
|
||||
/** @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;
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@@ -1,12 +1,12 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
import "./assets/main.css";
|
||||
import "./assets/transitions.css";
|
||||
import './assets/main.css';
|
||||
import './assets/transitions.css';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
||||
app.mount('#app');
|
||||
|
||||
@@ -4,5 +4,3 @@ export interface AppVersion {
|
||||
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 Section = {
|
||||
type Section = {
|
||||
name: string;
|
||||
items: Question[];
|
||||
};
|
||||
|
||||
export type Question = {
|
||||
type Question = {
|
||||
title: string;
|
||||
content: string;
|
||||
isOpen?: boolean;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Address } from "viem";
|
||||
import type { Address } from 'viem';
|
||||
|
||||
export enum LockStatus { // from DataTypes.sol
|
||||
export enum LockStatus {
|
||||
// from DataTypes.sol
|
||||
Inexistent = 0, // Uninitialized Lock
|
||||
Active = 1, // Valid Lock
|
||||
Expired = 2, // Expired Lock
|
||||
Released = 3 // Already released Lock
|
||||
Active = 1, // Valid Lock
|
||||
Expired = 2, // Expired Lock
|
||||
Released = 3, // Already released Lock
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Chain, ChainContract } from "viem";
|
||||
import type { Chain, ChainContract } from 'viem';
|
||||
|
||||
export enum TokenEnum {
|
||||
BRZ = 'BRZ',
|
||||
// BRX = 'BRX'
|
||||
}
|
||||
export type NetworkConfig = Chain & {
|
||||
tokens: Record<TokenEnum, ChainContract>,
|
||||
subgraphUrls: string[]
|
||||
tokens: Record<TokenEnum, ChainContract>;
|
||||
subgraphUrls: string[];
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export type Pix = {
|
||||
pixKey: string;
|
||||
merchantCity?: string;
|
||||
merchantName?: string;
|
||||
value?: number;
|
||||
transactionId?: string;
|
||||
message?: string;
|
||||
cep?: string;
|
||||
currency?: number;
|
||||
countryCode?: string;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Address } from "viem";
|
||||
import { Address } from 'viem';
|
||||
|
||||
export type UnreleasedLock = {
|
||||
lockID: bigint;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Address } from "viem";
|
||||
import type { NetworkConfig } from "@/model/NetworkEnum";
|
||||
import type { Address } from 'viem';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
|
||||
export type ValidDeposit = {
|
||||
token: Address;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LockStatus } from "@/model/LockStatus"
|
||||
import type { Address } from "viem"
|
||||
import type { LockStatus } from '@/model/LockStatus';
|
||||
import type { Address } from 'viem';
|
||||
|
||||
export type WalletTransaction = {
|
||||
token?: Address;
|
||||
|
||||
@@ -1,50 +1,55 @@
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import FaqView from "@/views/FaqView.vue";
|
||||
import ManageBidsView from "@/views/ManageBidsView.vue";
|
||||
import SellerView from "@/views/SellerView.vue";
|
||||
import ExploreView from "@/views/ExploreView.vue";
|
||||
import VersionsView from "@/views/VersionsView.vue";
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
} from 'vue-router';
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import FaqView from '@/views/FaqView.vue';
|
||||
import ManageBidsView from '@/views/ManageBidsView.vue';
|
||||
import SellerView from '@/views/SellerView.vue';
|
||||
import ExploreView from '@/views/ExploreView.vue';
|
||||
import VersionsView from '@/views/VersionsView.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: import.meta.env.MODE === 'production' && import.meta.env.BASE_URL === './'
|
||||
? createWebHashHistory()
|
||||
: createWebHistory(import.meta.env.BASE_URL),
|
||||
history:
|
||||
import.meta.env.MODE === 'production' && import.meta.env.BASE_URL === './'
|
||||
? createWebHashHistory()
|
||||
: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/:lockID",
|
||||
name: "redirect buy",
|
||||
path: '/:lockID',
|
||||
name: 'redirect buy',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/seller",
|
||||
name: "seller",
|
||||
path: '/seller',
|
||||
name: 'seller',
|
||||
component: SellerView,
|
||||
},
|
||||
{
|
||||
path: "/manage_bids",
|
||||
name: "manage bids",
|
||||
path: '/manage_bids',
|
||||
name: 'manage bids',
|
||||
component: ManageBidsView,
|
||||
},
|
||||
{
|
||||
path: "/faq",
|
||||
name: "faq",
|
||||
path: '/faq',
|
||||
name: 'faq',
|
||||
component: FaqView,
|
||||
},
|
||||
{
|
||||
path: "/explore",
|
||||
name: "explore",
|
||||
path: '/explore',
|
||||
name: 'explore',
|
||||
component: ExploreView,
|
||||
},
|
||||
{
|
||||
path: "/versions",
|
||||
name: "versions",
|
||||
path: '/versions',
|
||||
name: 'versions',
|
||||
component: VersionsView,
|
||||
},
|
||||
],
|
||||
|
||||
4
src/shims-vue.d.ts
vendored
4
src/shims-vue.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
const component: DefineComponent;
|
||||
export default component;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module.exports = {
|
||||
process() {
|
||||
return {
|
||||
code: `module.exports = {};`,
|
||||
code: 'module.exports = {};',
|
||||
};
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return "svgTransform";
|
||||
return 'svgTransform';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface Participant {
|
||||
savingsVariation?: string;
|
||||
}
|
||||
|
||||
export interface ParticipantWithID extends Participant {
|
||||
interface ParticipantWithID extends Participant {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ export interface Offer {
|
||||
|
||||
export const createParticipant = async (participant: Participant) => {
|
||||
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/register`, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chainID: participant.chainID,
|
||||
@@ -49,21 +49,23 @@ export const createParticipant = async (participant: Participant) => {
|
||||
|
||||
export const createSolicitation = async (offer: Offer) => {
|
||||
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/request`, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
amount: offer.amount,
|
||||
pixTarget: offer.sellerId.split("-").pop(),
|
||||
pixTarget: offer.sellerId.split('-').pop(),
|
||||
}),
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getSolicitation = async (id: bigint): Promise<{pixTimestamp: `0x${string}`, signature: `0x${string}`}> => {
|
||||
export const getSolicitation = async (
|
||||
id: bigint,
|
||||
): Promise<{ pixTimestamp: `0x${string}`; signature: `0x${string}` }> => {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_APP_API_URL}/release/${id}`
|
||||
`${import.meta.env.VITE_APP_API_URL}/release/${id}`,
|
||||
);
|
||||
|
||||
const obj = await response.json();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const decimalCount = (numStr: string): number => {
|
||||
if (numStr.includes(".")) {
|
||||
return numStr.split(".")[1].length;
|
||||
if (numStr.includes('.')) {
|
||||
return numStr.split('.')[1].length;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
[
|
||||
{
|
||||
"name": "1. Como Começar",
|
||||
"items": [
|
||||
{
|
||||
"title": "O que é uma carteira (wallet)?",
|
||||
"content": "Cripto-wallet é um software que armazena criptomoedas e criptoativos de forma segura. Existem também dispositivos físicos chamados de cold/hard wallets. Com a carteira-cripto você consegue comprar e vender tokens sem a necessidade de uma corretora (exchange), por exemplo. Cripto-ativos não ficam de fato armazenados nessa carteira, mas sim as informações que possibilitam acessá-los numa blockchain, como suas chaves pública e privada.\n<br>\n\n* **O que é um endereço de carteira (wallet address)?**\n Sequência alfanumérica que permite que você envie e receba suas criptomoedas em segurança. Por exemplo: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy.\n\n* **Como conectar sua carteira ao p2pix?**\n Clique em 'Conectar Carteira'. Escolha qual wallet ou ponte (bridge) utilizará para se conectar (MetaMask ou WalletConnect). Ao clicar, sua carteira se abrirá. Caso seja seu 1º acesso, certifique-se de que permitiu sua carteira a se conectar com nossa plataforma. \n\n* **Como instalar MetaMask no Google Chrome?** \n MetaMask é uma web3 wallet que pode ser adicionada como extensão no seu browser (navegador). Para instalar esse plugin (add-on) no Google Chrome, basta você acessar a Chrome Web Store e pesquisar “MetaMask”. Certifique-se de que é a versão legítima da carteira pelo número de downloads (bastante alto, pois é a maior cold wallet da web3), então é só clicar em “Adicionar ao Chrome”. Depois da instalação ser concluída, basta seguir o passo-a-passo proposto na interface da MetaMask para a criação da conta, login e senha. O caminho para a instalação em outros browsers como Firefox, Opera, entre outros navegadores, é o mesmo. \n\n<br>\nNota: Não esqueça de guardar sua seed phrase em um local seguro! Sem ela é impossível recuperar o acesso à sua carteira (caso perca sua senha) e também aos seus cripto ativos, eles serão perdidos - literalmente - para sempre.\n"
|
||||
},
|
||||
{
|
||||
"title": "O que é uma rede (network)?",
|
||||
"content": "Criptomoedas são transacionadas em redes de computadores, nas blockchains é que ficam armazenadas as informações sobre suas transações, não em um servidor. Existem [diversas redes no ecossistema cripto](https://www.gemini.com/cryptopedia/blockchain-technology-explained), como a rede do Bitcoin, da Ethereum, para citar as duas mais conhecidas. \n<br>\n\n* **Como selecionar a rede?**\nVocê deve ficar atento a rede que está selecionada em sua carteira. Na MetaMask, escolha a você escolhe a rede que vai operar clicando na primeira caixa ao lado do seu avatar:\n\nCaso você copie um endereço na sua MetaMask estando selecionada a Rede Ethereum Mainnet e queira operar na Polygon, seus cripto ativos serão perdidos. \n\n* **O que é taxa de rede (gas fee)?**\nCusto de transação do blockchain. Você paga essa taxa para a rede que vai remunerar os validadores. É uma forma de incentivar os validadores a dar continuidade nos seus serviços. "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. Comprar tokens",
|
||||
"items": [
|
||||
{
|
||||
"title": "Como comprar?",
|
||||
"content": "1. Conectar wallet (MetaMask ou WalletConnect)\n2. Digitar amount (quantidade);\n3. Solicitar token (criptomoeda);\n4. Identificar vendedor (da lista de pessoas);\n * Escanear QRCode (ou acrescenta chave manualmente);\n * Colar e2eID (identificação end-to-end) a.k.a. código de identificação (API Pix / comprovante bancário);\n5. Receber na carteira (wallet conectada."
|
||||
},
|
||||
{
|
||||
"title": "O que é uma stablecoin?",
|
||||
"content": "É uma criptomoeda com lastro em moeda fiduciária (por exemplo, Real brasileiro ou Dólar americando). Cada stablecoin gerada por um projeto emissor tem a mesma quantidade em reserva da moeda fiduciária em seu caixa. Alguns exemplos de stablecoins pareadas com dólar americano são USDt, DAI, USDc. Já vinculadas ao preço do Real são BRZ, MBRL e cREAL.\n<br>\n\n* **O que é o BRZ?**\n [BRZ](https://www.brztoken.io/) é a sigla para Brazilian Digital Token. É um tipo de criptomoeda que tem valor pareado com o Real (BRL). Ou seja, 1 BRZ tem valor igual a 1 BRL."
|
||||
},
|
||||
{
|
||||
"title": "De quem estou comprando?",
|
||||
"content": "Vendedores fazem as ofertas e indicam a chave-Pix que irão receber o valor transacionado. Essa oferta fica travada no smart contract até que as transações sejam concluídas e você possa fazer o saque dos seus tokens. \n\n<br>\n\n* **Onde encontrar comprovante do Pix?**\n Quando você faz um Pix um comprovante é gerado automaticamente para o pagador e para o recebedor. Esse comprovante pode ser acessado no momento da transação via app/site do banco ou pelo seu extrato bancário convencional.\n* **Para onde vão os tokens que eu comprei?**\n Os tokens comprados terão como destino a carteira que você conectou ao p2pix. \n Nota: Lembre-se sempre de configurar a sua carteira na rede correta em que quer receber seus tokens. "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. Vender tokens",
|
||||
"items": [
|
||||
{
|
||||
"title": "Como vender?",
|
||||
"content": "1. Enviar tokens;\n2. Indique sua chave-Pix;\n3. Criar [whitelist](https://academy.binance.com/en/glossary/whitelist) onde quer receber isso;\n4. Withdraw (saque) para remoção dos tokens;\n5. Lock (trava de 24h) para esperar transações antes dos saques.\n"
|
||||
},
|
||||
{
|
||||
"title": "O que é aprovar tokens?",
|
||||
"content": "Aprovações de tokens funcionam como permissões de sua carteira a um determinado [dApp](https://ethereum.org/en/developers/docs/dapps/) para movimentar os tokens. Ao aprovar tokens, você permite que nosso contrato inteligente acesse e execute as transações na sua carteira web3."
|
||||
},
|
||||
{
|
||||
"title": "O que é o lock na rede?",
|
||||
"content": "\"Travamento\" na rede é uma forma de especificar um tempo para retirada dos ativos digitais. O contrato inteligente do p2pix 'trava' os tokens enviados para a rede por 24 horas. Só depois de transcorridas essas 24h que o saque dos tokens estará liberado. Isso ocorre para garantir uma transação segura para os vendedores."
|
||||
},
|
||||
{
|
||||
"title": "Como retirar a oferta?",
|
||||
"content": "Caso você desista da sua oferta, você pode invalidar sua ordem de venda e impedir um novo ‘lock’ na rede."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"name": "1. Como Começar",
|
||||
"items": [
|
||||
{
|
||||
"title": "O que é uma carteira (wallet)?",
|
||||
"content": "Cripto-wallet é um software que armazena criptomoedas e criptoativos de forma segura. Existem também dispositivos físicos chamados de cold/hard wallets. Com a carteira-cripto você consegue comprar e vender tokens sem a necessidade de uma corretora (exchange), por exemplo. Cripto-ativos não ficam de fato armazenados nessa carteira, mas sim as informações que possibilitam acessá-los numa blockchain, como suas chaves pública e privada.\n<br>\n\n* **O que é um endereço de carteira (wallet address)?**\n Sequência alfanumérica que permite que você envie e receba suas criptomoedas em segurança. Por exemplo: 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy.\n\n* **Como conectar sua carteira ao p2pix?**\n Clique em 'Conectar Carteira'. Escolha qual wallet ou ponte (bridge) utilizará para se conectar (MetaMask ou WalletConnect). Ao clicar, sua carteira se abrirá. Caso seja seu 1º acesso, certifique-se de que permitiu sua carteira a se conectar com nossa plataforma. \n\n* **Como instalar MetaMask no Google Chrome?** \n MetaMask é uma web3 wallet que pode ser adicionada como extensão no seu browser (navegador). Para instalar esse plugin (add-on) no Google Chrome, basta você acessar a Chrome Web Store e pesquisar “MetaMask”. Certifique-se de que é a versão legítima da carteira pelo número de downloads (bastante alto, pois é a maior cold wallet da web3), então é só clicar em “Adicionar ao Chrome”. Depois da instalação ser concluída, basta seguir o passo-a-passo proposto na interface da MetaMask para a criação da conta, login e senha. O caminho para a instalação em outros browsers como Firefox, Opera, entre outros navegadores, é o mesmo. \n\n<br>\nNota: Não esqueça de guardar sua seed phrase em um local seguro! Sem ela é impossível recuperar o acesso à sua carteira (caso perca sua senha) e também aos seus cripto ativos, eles serão perdidos - literalmente - para sempre.\n"
|
||||
},
|
||||
{
|
||||
"title": "O que é uma rede (network)?",
|
||||
"content": "Criptomoedas são transacionadas em redes de computadores, nas blockchains é que ficam armazenadas as informações sobre suas transações, não em um servidor. Existem [diversas redes no ecossistema cripto](https://www.gemini.com/cryptopedia/blockchain-technology-explained), como a rede do Bitcoin, da Ethereum, para citar as duas mais conhecidas. \n<br>\n\n* **Como selecionar a rede?**\nVocê deve ficar atento a rede que está selecionada em sua carteira. Na MetaMask, escolha a você escolhe a rede que vai operar clicando na primeira caixa ao lado do seu avatar:\n\nCaso você copie um endereço na sua MetaMask estando selecionada a Rede Ethereum Mainnet e queira operar na Polygon, seus cripto ativos serão perdidos. \n\n* **O que é taxa de rede (gas fee)?**\nCusto de transação do blockchain. Você paga essa taxa para a rede que vai remunerar os validadores. É uma forma de incentivar os validadores a dar continuidade nos seus serviços. "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. Comprar tokens",
|
||||
"items": [
|
||||
{
|
||||
"title": "Como comprar?",
|
||||
"content": "1. Conectar wallet (MetaMask ou WalletConnect)\n2. Digitar amount (quantidade);\n3. Solicitar token (criptomoeda);\n4. Identificar vendedor (da lista de pessoas);\n * Escanear QRCode (ou acrescenta chave manualmente);\n * Colar e2eID (identificação end-to-end) a.k.a. código de identificação (API Pix / comprovante bancário);\n5. Receber na carteira (wallet conectada."
|
||||
},
|
||||
{
|
||||
"title": "O que é uma stablecoin?",
|
||||
"content": "É uma criptomoeda com lastro em moeda fiduciária (por exemplo, Real brasileiro ou Dólar americando). Cada stablecoin gerada por um projeto emissor tem a mesma quantidade em reserva da moeda fiduciária em seu caixa. Alguns exemplos de stablecoins pareadas com dólar americano são USDt, DAI, USDc. Já vinculadas ao preço do Real são BRZ, MBRL e cREAL.\n<br>\n\n* **O que é o BRZ?**\n [BRZ](https://www.brztoken.io/) é a sigla para Brazilian Digital Token. É um tipo de criptomoeda que tem valor pareado com o Real (BRL). Ou seja, 1 BRZ tem valor igual a 1 BRL."
|
||||
},
|
||||
{
|
||||
"title": "De quem estou comprando?",
|
||||
"content": "Vendedores fazem as ofertas e indicam a chave-Pix que irão receber o valor transacionado. Essa oferta fica travada no smart contract até que as transações sejam concluídas e você possa fazer o saque dos seus tokens. \n\n<br>\n\n* **Onde encontrar comprovante do Pix?**\n Quando você faz um Pix um comprovante é gerado automaticamente para o pagador e para o recebedor. Esse comprovante pode ser acessado no momento da transação via app/site do banco ou pelo seu extrato bancário convencional.\n* **Para onde vão os tokens que eu comprei?**\n Os tokens comprados terão como destino a carteira que você conectou ao p2pix. \n Nota: Lembre-se sempre de configurar a sua carteira na rede correta em que quer receber seus tokens. "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. Vender tokens",
|
||||
"items": [
|
||||
{
|
||||
"title": "Como vender?",
|
||||
"content": "1. Enviar tokens;\n2. Indique sua chave-Pix;\n3. Criar [whitelist](https://academy.binance.com/en/glossary/whitelist) onde quer receber isso;\n4. Withdraw (saque) para remoção dos tokens;\n5. Lock (trava de 24h) para esperar transações antes dos saques.\n"
|
||||
},
|
||||
{
|
||||
"title": "O que é aprovar tokens?",
|
||||
"content": "Aprovações de tokens funcionam como permissões de sua carteira a um determinado [dApp](https://ethereum.org/en/developers/docs/dapps/) para movimentar os tokens. Ao aprovar tokens, você permite que nosso contrato inteligente acesse e execute as transações na sua carteira web3."
|
||||
},
|
||||
{
|
||||
"title": "O que é o lock na rede?",
|
||||
"content": "\"Travamento\" na rede é uma forma de especificar um tempo para retirada dos ativos digitais. O contrato inteligente do p2pix 'trava' os tokens enviados para a rede por 24 horas. Só depois de transcorridas essas 24h que o saque dos tokens estará liberado. Isso ocorre para garantir uma transação segura para os vendedores."
|
||||
},
|
||||
{
|
||||
"title": "Como retirar a oferta?",
|
||||
"content": "Caso você desista da sua oferta, você pode invalidar sua ordem de venda e impedir um novo ‘lock’ na rede."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2139,4 +2139,4 @@
|
||||
"ISPB": "02318507",
|
||||
"longName": "BANCO KEB HANA DO BRASIL S.A."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { TokenEnum } from "@/model/NetworkEnum";
|
||||
import { Networks } from "@/config/networks";
|
||||
import type { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { Networks } from '@/config/networks';
|
||||
|
||||
export const getNetworkImage = (networkName: string): string => {
|
||||
const normalizedName = networkName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
return new URL(`../assets/networks/${normalizedName}.svg`, import.meta.url).href;
|
||||
return new URL(`../assets/networks/${normalizedName}.svg`, import.meta.url)
|
||||
.href;
|
||||
};
|
||||
|
||||
export const getTokenImage = (tokenName: TokenEnum): string => {
|
||||
return new URL(`../assets/tokens/${tokenName.toLowerCase()}.svg`, import.meta.url).href;
|
||||
return new URL(
|
||||
`../assets/tokens/${tokenName.toLowerCase()}.svg`,
|
||||
import.meta.url,
|
||||
).href;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { Address } from "viem";
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { Address } from 'viem';
|
||||
|
||||
const verifyNetworkLiquidity = (
|
||||
tokenValue: number,
|
||||
walletAddress: Address,
|
||||
validDepositList: ValidDeposit[]
|
||||
validDepositList: ValidDeposit[],
|
||||
): ValidDeposit[] => {
|
||||
const filteredDepositList = validDepositList
|
||||
.filter((element) => {
|
||||
const remaining = element.remaining;
|
||||
if (
|
||||
tokenValue!! <= remaining &&
|
||||
tokenValue!! != 0 &&
|
||||
tokenValue! <= remaining &&
|
||||
tokenValue! != 0 &&
|
||||
element.seller !== walletAddress
|
||||
) {
|
||||
return true;
|
||||
@@ -25,14 +25,14 @@ const verifyNetworkLiquidity = (
|
||||
const uniqueNetworkDeposits = filteredDepositList.reduce(
|
||||
(acc: ValidDeposit[], current) => {
|
||||
const existingNetwork = acc.find(
|
||||
(deposit) => deposit.network === current.network
|
||||
(deposit) => deposit.network === current.network,
|
||||
);
|
||||
if (!existingNetwork) {
|
||||
acc.push(current);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
[],
|
||||
);
|
||||
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 => {
|
||||
pixKey = pixKey.replace(/[-.()/]/g, "");
|
||||
pixKey = pixKey.replace(/[-.()/]/g, '');
|
||||
return pixKey;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { AppVersion } from "@/model/AppVersion";
|
||||
import type { AppVersion } from '@/model/AppVersion';
|
||||
|
||||
export const appVersions: AppVersion[] = [
|
||||
{
|
||||
tag: "1.1.0",
|
||||
ipfsHash: "bafybeiaffdxrxoex3qh7kirnkkufnvpafb4gmkt7mjxufnnpbrq6tmqoha",
|
||||
releaseDate: "2025-11-06",
|
||||
description: "Explorer and versioning features added"
|
||||
tag: '1.1.0',
|
||||
ipfsHash: 'bafybeiaffdxrxoex3qh7kirnkkufnvpafb4gmkt7mjxufnnpbrq6tmqoha',
|
||||
releaseDate: '2025-11-06',
|
||||
description: 'Explorer and versioning features added',
|
||||
},
|
||||
{
|
||||
tag: "1.0.0",
|
||||
ipfsHash: "bafybeiagfqnxnb5zdrks6dicfm7kxjdtzzzzm2ouluxgdseg2hrrotayzi",
|
||||
releaseDate: "2023-01-28",
|
||||
description: "Initial release"
|
||||
tag: '1.0.0',
|
||||
ipfsHash: 'bafybeiagfqnxnb5zdrks6dicfm7kxjdtzzzzm2ouluxgdseg2hrrotayzi',
|
||||
releaseDate: '2023-01-28',
|
||||
description: 'Initial release',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -19,12 +19,6 @@ export function getLatestVersion(): AppVersion | null {
|
||||
return appVersions.length > 0 ? appVersions[0] : null;
|
||||
}
|
||||
|
||||
export function getVersionByTag(tag: string): AppVersion | null {
|
||||
return appVersions.find((v) => v.tag === tag) || null;
|
||||
}
|
||||
|
||||
export function getIpfsUrl(ipfsHash: string): string {
|
||||
return `https://${ipfsHash}.ipfs.dweb.link`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const {
|
||||
fetchAllActivity,
|
||||
fetchUserActivity,
|
||||
fetchAnalytics,
|
||||
clearData
|
||||
clearData,
|
||||
} = useGraphQL(network);
|
||||
|
||||
const transactionTypes = [
|
||||
@@ -29,7 +29,7 @@ const transactionTypes = [
|
||||
{ key: 'deposit', label: 'Depósitos' },
|
||||
{ key: 'lock', label: 'Bloqueios' },
|
||||
{ key: 'release', label: 'Liberações' },
|
||||
{ key: 'return', label: 'Retornos' }
|
||||
{ key: 'return', label: 'Retornos' },
|
||||
];
|
||||
|
||||
const handleTypeFilter = (type: string) => {
|
||||
@@ -44,51 +44,48 @@ watch(searchAddress, async (newAddress) => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(network, async () => {
|
||||
clearData();
|
||||
await Promise.all([
|
||||
fetchAllActivity(),
|
||||
fetchAnalytics()
|
||||
]);
|
||||
}, { deep: true });
|
||||
watch(
|
||||
network,
|
||||
async () => {
|
||||
clearData();
|
||||
await Promise.all([fetchAllActivity(), fetchAnalytics()]);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
fetchAllActivity(),
|
||||
fetchAnalytics()
|
||||
]);
|
||||
await Promise.all([fetchAllActivity(), fetchAnalytics()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
|
||||
<AnalyticsCard
|
||||
title="Volume Total"
|
||||
:value="analytics.totalVolume"
|
||||
:loading="analyticsLoading"
|
||||
/>
|
||||
|
||||
|
||||
<AnalyticsCard
|
||||
title="Total de Transações"
|
||||
:value="analytics.totalTransactions"
|
||||
:loading="analyticsLoading"
|
||||
/>
|
||||
|
||||
|
||||
<AnalyticsCard
|
||||
title="Total de Bloqueios"
|
||||
:value="analytics.totalLocks"
|
||||
:loading="analyticsLoading"
|
||||
/>
|
||||
|
||||
|
||||
<AnalyticsCard
|
||||
title="Total de Depósitos"
|
||||
:value="analytics.totalDeposits"
|
||||
:loading="analyticsLoading"
|
||||
/>
|
||||
|
||||
|
||||
<AnalyticsCard
|
||||
title="Total de Liberações"
|
||||
:value="analytics.totalReleases"
|
||||
@@ -119,7 +116,7 @@ onMounted(async () => {
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
selectedType === type.key
|
||||
? 'bg-amber-400 text-gray-900'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200',
|
||||
]"
|
||||
>
|
||||
{{ type.label }}
|
||||
@@ -143,11 +140,15 @@ onMounted(async () => {
|
||||
<!-- Transactions Table -->
|
||||
<FormCard v-else padding="lg">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">Transações Recentes</h2>
|
||||
<p class="text-gray-600">{{ transactions.length }} transações encontradas</p>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-2">
|
||||
Transações Recentes
|
||||
</h2>
|
||||
<p class="text-gray-600">
|
||||
{{ transactions.length }} transações encontradas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TransactionTable
|
||||
<TransactionTable
|
||||
:transactions="transactions"
|
||||
:network-explorer-url="network.blockExplorers?.default.url || ''"
|
||||
/>
|
||||
@@ -160,4 +161,4 @@ onMounted(async () => {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { Faq } from "@/model/Faq";
|
||||
import { ref } from "vue";
|
||||
import { marked } from "marked";
|
||||
import faqContent from "@/utils/files/faqContent.json";
|
||||
import type { Faq } from '@/model/Faq';
|
||||
import { ref } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import faqContent from '@/utils/files/faqContent.json';
|
||||
|
||||
const faq = ref<Faq>(faqContent);
|
||||
|
||||
@@ -23,17 +23,19 @@ const openItem = (index: number) => {
|
||||
|
||||
faq.value[selectedSection.value].items[index].content = marked(
|
||||
faq.value[selectedSection.value].items[index].content
|
||||
);
|
||||
) as string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="text-container">
|
||||
<span class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[50rem] max-w-[90%]"
|
||||
<span
|
||||
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[50rem] max-w-[90%]"
|
||||
>Perguntas Frequentes
|
||||
</span>
|
||||
<span class="text font-medium sm:text-base text-sm sm:max-w-[40rem] max-w-[90%]"
|
||||
<span
|
||||
class="text font-medium sm:text-base text-sm sm:max-w-[40rem] max-w-[90%]"
|
||||
>Não conseguiu uma resposta para sua dúvida? Acesse a comunidade do
|
||||
Discord para falar diretamente conosco.</span
|
||||
>
|
||||
@@ -81,6 +83,7 @@ const openItem = (index: number) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-8 sm:mt-16 px-4;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import SearchComponent from "@/components/BuyerSteps/BuyerSearchComponent.vue";
|
||||
import LoadingComponent from "@/components/ui/LoadingComponent.vue";
|
||||
import BuyConfirmedComponent from "@/components/BuyerSteps/BuyConfirmedComponent.vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import QrCodeComponent from "@/components/BuyerSteps/QrCodeComponent.vue";
|
||||
import { addLock, releaseLock } from "@/blockchain/buyerMethods";
|
||||
import { updateWalletStatus, checkUnreleasedLock } from "@/blockchain/wallet";
|
||||
import { getNetworksLiquidity } from "@/blockchain/events";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import { getUnreleasedLockById } from "@/blockchain/events";
|
||||
import CustomAlert from "@/components/ui/CustomAlert.vue";
|
||||
import { getSolicitation } from "@/utils/bbPay";
|
||||
import type { Address } from "viem";
|
||||
import SearchComponent from '@/components/BuyerSteps/BuyerSearchComponent.vue';
|
||||
import LoadingComponent from '@/components/ui/LoadingComponent.vue';
|
||||
import BuyConfirmedComponent from '@/components/BuyerSteps/BuyConfirmedComponent.vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import QrCodeComponent from '@/components/BuyerSteps/QrCodeComponent.vue';
|
||||
import { addLock, releaseLock } from '@/blockchain/buyerMethods';
|
||||
import { updateWalletStatus, checkUnreleasedLock } from '@/blockchain/wallet';
|
||||
import { getNetworksLiquidity } from '@/blockchain/events';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import { getUnreleasedLockById } from '@/blockchain/events';
|
||||
import CustomAlert from '@/components/ui/CustomAlert.vue';
|
||||
import { getSolicitation } from '@/utils/bbPay';
|
||||
import type { Address } from 'viem';
|
||||
|
||||
enum Step {
|
||||
Search,
|
||||
@@ -29,7 +29,7 @@ const flowStep = ref<Step>(Step.Search);
|
||||
const participantID = ref<string>();
|
||||
const sellerAddress = ref<Address>();
|
||||
const tokenAmount = ref<number>();
|
||||
const lockID = ref<string>("");
|
||||
const lockID = ref<string>('');
|
||||
const loadingRelease = ref<boolean>(false);
|
||||
const showModal = ref<boolean>(false);
|
||||
const showBuyAlert = ref<boolean>(false);
|
||||
@@ -37,7 +37,7 @@ const paramLockID = window.history.state?.lockID;
|
||||
|
||||
const confirmBuyClick = async (
|
||||
selectedDeposit: ValidDeposit,
|
||||
tokenValue: number
|
||||
tokenValue: number,
|
||||
) => {
|
||||
participantID.value = selectedDeposit.participantID;
|
||||
tokenAmount.value = tokenValue;
|
||||
@@ -60,22 +60,25 @@ const confirmBuyClick = async (
|
||||
};
|
||||
|
||||
const releaseTransaction = async (params: {
|
||||
pixTimestamp: `0x${string}`&{lenght:34},
|
||||
signature: `0x${string}`,
|
||||
pixTimestamp: `0x${string}` & { lenght: 34 };
|
||||
signature: `0x${string}`;
|
||||
}) => {
|
||||
flowStep.value = Step.List;
|
||||
showBuyAlert.value = true;
|
||||
loadingRelease.value = true;
|
||||
|
||||
const release = await releaseLock(BigInt(lockID.value), params.pixTimestamp, params.signature);
|
||||
const release = await releaseLock(
|
||||
BigInt(lockID.value),
|
||||
params.pixTimestamp,
|
||||
params.signature,
|
||||
);
|
||||
|
||||
await updateWalletStatus();
|
||||
loadingRelease.value = false;
|
||||
};
|
||||
|
||||
const checkForUnreleasedLocks = async (): Promise<void> => {
|
||||
if (!walletAddress.value)
|
||||
throw new Error("Wallet not connected");
|
||||
if (!walletAddress.value) throw new Error('Wallet not connected');
|
||||
const lock = await checkUnreleasedLock(walletAddress.value);
|
||||
if (lock) {
|
||||
lockID.value = String(lock.lockID);
|
||||
@@ -111,7 +114,7 @@ if (paramLockID) {
|
||||
onMounted(async () => {
|
||||
await getNetworksLiquidity();
|
||||
if (walletAddress.value && !paramLockID) await checkForUnreleasedLocks();
|
||||
window.history.state.lockID = "";
|
||||
window.history.state.lockID = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import ListingComponent from "@/components/ListingComponent/ListingComponent.vue";
|
||||
import LoadingComponent from "@/components/ui/LoadingComponent.vue";
|
||||
import CustomAlert from "@/components/ui/CustomAlert.vue";
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import ListingComponent from '@/components/ListingComponent/ListingComponent.vue';
|
||||
import LoadingComponent from '@/components/ui/LoadingComponent.vue';
|
||||
import CustomAlert from '@/components/ui/CustomAlert.vue';
|
||||
import {
|
||||
listValidDepositTransactionsByWalletAddress,
|
||||
listAllTransactionByWalletAddress,
|
||||
getActiveLockAmount,
|
||||
} from "@/blockchain/wallet";
|
||||
import { withdrawDeposit } from "@/blockchain/buyerMethods";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
||||
} from '@/blockchain/wallet';
|
||||
import { withdrawDeposit } from '@/blockchain/buyerMethods';
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
|
||||
import router from "@/router/index";
|
||||
import router from '@/router/index';
|
||||
|
||||
const user = useUser();
|
||||
const { walletAddress, network, selectedToken } = user;
|
||||
@@ -30,18 +30,19 @@ const callWithdraw = async (amount: string) => {
|
||||
let withdraw;
|
||||
try {
|
||||
withdraw = await withdrawDeposit(
|
||||
amount,
|
||||
network.value.tokens[selectedToken.value].address);
|
||||
amount,
|
||||
network.value.tokens[selectedToken.value].address,
|
||||
);
|
||||
} catch {
|
||||
loadingWithdraw.value = false;
|
||||
}
|
||||
|
||||
if (withdraw) {
|
||||
console.log("Saque realizado!");
|
||||
console.log('Saque realizado!');
|
||||
await getWalletTransactions();
|
||||
showAlert.value = true;
|
||||
} else {
|
||||
console.log("Não foi possível realizar o saque!");
|
||||
console.log('Não foi possível realizar o saque!');
|
||||
}
|
||||
loadingWithdraw.value = false;
|
||||
}
|
||||
@@ -51,11 +52,11 @@ const getWalletTransactions = async () => {
|
||||
user.setLoadingWalletTransactions(true);
|
||||
if (walletAddress.value) {
|
||||
const walletDeposits = await listValidDepositTransactionsByWalletAddress(
|
||||
walletAddress.value
|
||||
walletAddress.value,
|
||||
);
|
||||
|
||||
const allUserTransactions = await listAllTransactionByWalletAddress(
|
||||
walletAddress.value
|
||||
walletAddress.value,
|
||||
);
|
||||
|
||||
activeLockAmount.value = await getActiveLockAmount(walletAddress.value);
|
||||
@@ -72,7 +73,7 @@ const getWalletTransactions = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
if (!walletAddress.value) {
|
||||
router.push({ name: "home" });
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
await getWalletTransactions();
|
||||
});
|
||||
@@ -118,6 +119,7 @@ watch(network, async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center gap-10 mt-20 w-full;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref } from 'vue';
|
||||
|
||||
import SellerComponent from "@/components/SellerSteps/SellerComponent.vue";
|
||||
import SendNetwork from "@/components/SellerSteps/SendNetwork.vue";
|
||||
import LoadingComponent from "@/components/ui/LoadingComponent.vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { approveTokens, addDeposit } from "@/blockchain/sellerMethods";
|
||||
import CustomAlert from "@/components/ui/CustomAlert.vue";
|
||||
import type { Participant } from "@/utils/bbPay";
|
||||
import SellerComponent from '@/components/SellerSteps/SellerComponent.vue';
|
||||
import SendNetwork from '@/components/SellerSteps/SendNetwork.vue';
|
||||
import LoadingComponent from '@/components/ui/LoadingComponent.vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { approveTokens, addDeposit } from '@/blockchain/sellerMethods';
|
||||
import CustomAlert from '@/components/ui/CustomAlert.vue';
|
||||
import type { Participant } from '@/utils/bbPay';
|
||||
|
||||
enum Step {
|
||||
Search,
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { appVersions, getIpfsUrl, getLatestVersion } from "@/utils/versions";
|
||||
import type { AppVersion } from "@/model/AppVersion";
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { appVersions, getIpfsUrl, getLatestVersion } from '@/utils/versions';
|
||||
import type { AppVersion } from '@/model/AppVersion';
|
||||
|
||||
const versions = ref<AppVersion[]>([]);
|
||||
const latestVersion = ref<AppVersion | null>(null);
|
||||
const currentVersion = __APP_VERSION__;
|
||||
|
||||
onMounted(() => {
|
||||
versions.value = [...appVersions].sort((a, b) =>
|
||||
new Date(b.releaseDate).getTime() - new Date(a.releaseDate).getTime()
|
||||
versions.value = [...appVersions].sort(
|
||||
(a, b) =>
|
||||
new Date(b.releaseDate).getTime() - new Date(a.releaseDate).getTime(),
|
||||
);
|
||||
latestVersion.value = getLatestVersion();
|
||||
});
|
||||
|
||||
const openIpfsVersion = (ipfsHash: string) => {
|
||||
const url = getIpfsUrl(ipfsHash);
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("pt-BR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
return date.toLocaleDateString('pt-BR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -36,28 +37,22 @@ const formatDate = (dateString: string): string => {
|
||||
Versões do P2Pix
|
||||
</span>
|
||||
<span class="text font-medium text-base max-w-[40rem]">
|
||||
Visualize todas as versões do P2Pix. Cada versão está
|
||||
disponível no IPFS para acesso permanente e descentralizado.
|
||||
Visualize todas as versões do P2Pix. Cada versão está disponível no IPFS
|
||||
para acesso permanente e descentralizado.
|
||||
</span>
|
||||
<div v-if="currentVersion" class="mt-4">
|
||||
<span class="text-gray-400 text-sm">
|
||||
Versão atual: <span class="font-semibold text-white">{{ currentVersion }}</span>
|
||||
Versão atual:
|
||||
<span class="font-semibold text-white">{{ currentVersion }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="versions-container">
|
||||
<div
|
||||
v-for="version in versions"
|
||||
:key="version.tag"
|
||||
class="version-card"
|
||||
>
|
||||
<div v-for="version in versions" :key="version.tag" class="version-card">
|
||||
<div class="version-header">
|
||||
<h3 class="version-tag">{{ version.tag }}</h3>
|
||||
<span
|
||||
v-if="version.tag === currentVersion"
|
||||
class="current-badge"
|
||||
>
|
||||
<span v-if="version.tag === currentVersion" class="current-badge">
|
||||
Atual
|
||||
</span>
|
||||
</div>
|
||||
@@ -89,6 +84,7 @@ const formatDate = (dateString: string): string => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16 px-4;
|
||||
}
|
||||
@@ -153,5 +149,3 @@ const formatDate = (dateString: string): string => {
|
||||
@apply text-center py-12;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.vue"],
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', './src/**/*.vue'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"cypress.config.*",
|
||||
@@ -7,6 +7,7 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
@@ -7,7 +7,6 @@
|
||||
"scripts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
@@ -21,6 +20,7 @@
|
||||
"skipLibCheck": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
@@ -38,4 +38,4 @@
|
||||
"node_modules",
|
||||
"./node_modules"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { execSync } from "node:child_process";
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import svgLoader from "vite-svg-loader";
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
|
||||
function getGitTag(): string {
|
||||
function sh(cmd: string): string {
|
||||
try {
|
||||
const tags = execSync("git tag --sort=-version:refname").toString().trim().split("\n");
|
||||
return tags.length > 0 ? tags[0] : "unknown";
|
||||
} catch (fallbackError) {
|
||||
return "";
|
||||
return execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] })
|
||||
.toString()
|
||||
.trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getAppVersion(): string {
|
||||
const tag = sh('git tag --sort=-version:refname').split('\n')[0] || '';
|
||||
const shortSha = sh('git rev-parse --short HEAD');
|
||||
const tagSha = tag ? sh(`git rev-list -n 1 ${tag}`) : '';
|
||||
const headSha = sh('git rev-parse HEAD');
|
||||
if (tag && tagSha === headSha) return tag;
|
||||
if (tag && shortSha) return `${tag}+${shortSha}`;
|
||||
return shortSha || 'dev';
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
base: './',
|
||||
build: {
|
||||
target: "esnext",
|
||||
target: 'esnext',
|
||||
},
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(getGitTag()),
|
||||
__APP_VERSION__: JSON.stringify(getAppVersion()),
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: "esnext",
|
||||
target: 'esnext',
|
||||
define: {
|
||||
global: "globalThis",
|
||||
global: 'globalThis',
|
||||
},
|
||||
supported: {
|
||||
bigint: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [vue(), vueJsx(), svgLoader()],
|
||||
plugins: [vue(), vueJsx(), tailwindcss(), svgLoader()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
"viem/errors": fileURLToPath(
|
||||
new URL("./node_modules/viem/errors", import.meta.url)
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'viem/errors': fileURLToPath(
|
||||
new URL('./node_modules/viem/errors', import.meta.url),
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
40
vitest.config.ts
Normal file
40
vitest.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import viteConfig from './vite.config';
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
globals: true,
|
||||
include: ['tests/**/*.{test,spec}.ts'],
|
||||
exclude: [
|
||||
'p2pix-smart-contracts/**',
|
||||
'vendor/**',
|
||||
'**/node_modules/**',
|
||||
],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html', 'lcov'],
|
||||
include: ['src/**/*.{ts,vue}'],
|
||||
exclude: [
|
||||
'src/main.ts',
|
||||
'src/router/**',
|
||||
'src/assets/**',
|
||||
'src/generated.ts',
|
||||
'src/subgraph/generated.ts',
|
||||
'**/*.d.ts',
|
||||
'**/__mocks__/**',
|
||||
'p2pix-smart-contracts/**',
|
||||
'vendor/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineConfig } from '@wagmi/cli'
|
||||
import { hardhat } from '@wagmi/cli/plugins'
|
||||
import { defineConfig } from '@wagmi/cli';
|
||||
import { hardhat } from '@wagmi/cli/plugins';
|
||||
|
||||
export default defineConfig({
|
||||
out: 'src/blockchain/abi.ts',
|
||||
contracts: [],
|
||||
plugins: [
|
||||
hardhat({
|
||||
project: '../p2pix-smart-contracts',
|
||||
}),],
|
||||
})
|
||||
project: 'p2pix-smart-contracts',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user