Compare commits
111
Commits
buy-refactor
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d6dc80f55 | ||
|
|
cea50bb22c | ||
|
|
a9d370ce9e | ||
|
|
fe8cedf048 | ||
|
|
c47968333f | ||
|
|
7887f50b1a | ||
|
|
e182347f5e | ||
|
|
9f04c09704 | ||
|
|
36cd57b59e | ||
|
|
0bc4e4ccf3 | ||
|
|
00390ab0c3 | ||
|
|
5dc630acdf | ||
|
|
9828a44cf8 | ||
|
|
616302ffbe | ||
|
|
cee0cc1296 | ||
|
|
72b9b64da8 | ||
|
|
e2a03786e1 | ||
|
|
918e6fecde | ||
|
|
0cdc69d4da | ||
|
|
ee783ea727 | ||
|
|
0761f74e1a | ||
|
|
289910df95 | ||
|
|
c7a285c4a1 | ||
|
|
c9ca178b24 | ||
|
|
555eac9e7c | ||
|
|
10569db3b1 | ||
|
|
2e2d2ff6d7 | ||
|
|
e7bf32b6c3 | ||
|
|
4a09a323bd | ||
|
|
6c5c487874 | ||
|
|
4e65ab7ff0 | ||
|
|
7c17e940da | ||
|
|
bdaffbd889 | ||
|
|
9c948d7da4 | ||
|
|
af897e7dd4 | ||
|
|
663a0bce46 | ||
|
|
4469ccb30a | ||
|
|
98c6e04a16 | ||
|
|
d63cb8c6d3 | ||
|
|
c481d9d0a5 | ||
|
|
63f5ee017b | ||
|
|
dacbeac019 | ||
|
|
18efb7543e | ||
|
|
6caf34b579 | ||
|
|
ec1053c660 | ||
|
|
46be71046a | ||
|
|
ebe03eb439 | ||
|
|
abeef0bd85 | ||
|
|
ea5773c7d0 | ||
|
|
95c3692bcb | ||
|
|
7cda8d5573 | ||
|
|
6cfe478177 | ||
|
|
f31fa15887 | ||
|
|
ad5b0a3a93 | ||
|
|
6979ba0402 | ||
|
|
43b955296a | ||
|
|
1cf9898e2d | ||
|
|
9c8ba43339 | ||
|
|
b655a3c4b6 | ||
|
|
42016d0101 | ||
|
|
674948120c | ||
|
|
3c8e9c0262 | ||
|
|
d686fca363 | ||
|
|
183fd698a9 | ||
|
|
100aab6b42 | ||
|
|
9a506acfa6 | ||
|
|
d603753654 | ||
|
|
b4f5134156 | ||
|
|
290e339f0c | ||
|
|
4c721e4431 | ||
|
|
1adef2dbb8 | ||
|
|
bf75cd766a | ||
|
|
d3eae76f91 | ||
|
|
a4163a2ba6 | ||
|
|
fad52d79d2 | ||
|
|
f64ea2ddf1 | ||
|
|
e67c8fcc77 | ||
|
|
ac670235cd | ||
|
|
38201bb254 | ||
|
|
fece86e305 | ||
|
|
b27b07fe47 | ||
|
|
d33d7f8538 | ||
|
|
57714fac9b | ||
|
|
9eee78fa91 | ||
|
|
4b4ade2bfa | ||
|
|
364cdd3b60 | ||
|
|
799f7cfe09 | ||
|
|
2117638305 | ||
|
|
a3e3f0506c | ||
|
|
976c48ac4b | ||
|
|
7bcf5d90c2 | ||
|
|
358ae7410f | ||
|
|
a906fa136d | ||
|
|
7ec73e8c6f | ||
|
|
84afed78fb | ||
|
|
2b6be86b2e | ||
|
|
fdc03068f2 | ||
|
|
c58e91e073 | ||
|
|
13c0fcc681 | ||
|
|
5c1d560d0c | ||
|
|
a24ee193d4 | ||
|
|
9b325ac917 | ||
|
|
c3d770f713 | ||
|
|
3ef1694217 | ||
|
|
2b707e81c2 | ||
|
|
f6a9ab854c | ||
|
|
474af2fbfc | ||
|
|
4af059f6b7 | ||
|
|
23163be99d | ||
|
|
b956c8ec2b | ||
|
|
1d429f039a |
No files matched your search
@@ -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",
|
||||
},
|
||||
};
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,9 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
vendor/
|
||||
.dagrobin
|
||||
.claude
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
@@ -29,3 +32,6 @@ coverage
|
||||
.vercel
|
||||
|
||||
.env
|
||||
|
||||
# Codegen output (regenerated by `bun run wagmi:gen`, runs on prestart)
|
||||
src/blockchain/abi.ts
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "p2pix-smart-contracts"]
|
||||
path = p2pix-smart-contracts
|
||||
url = https://git.p2pix.co/doiim/p2pix-smart-contracts
|
||||
+3
-1
@@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
+6
-6
@@ -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"]
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
+3
-3
@@ -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'
|
||||
@@ -1 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
@@ -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 },
|
||||
);
|
||||
@@ -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"]
|
||||
}
|
||||
Submodule
+1
Submodule p2pix-smart-contracts added at c4db98ae00.
+49
-46
@@ -1,57 +1,60 @@
|
||||
{
|
||||
"name": "p2pix-front-end",
|
||||
"version": "0.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite --host=0.0.0.0 --port 3000",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"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
-2
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
+19
-26
@@ -1,34 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import TopBar from "@/components/TopBar/TopBar.vue";
|
||||
import SpinnerComponent from "@/components/SpinnerComponent.vue";
|
||||
import ToasterComponent from "@/components/ToasterComponent.vue";
|
||||
import { init, useOnboard } from "@web3-onboard/vue";
|
||||
import injectedModule from "@web3-onboard/injected-wallets";
|
||||
import { Networks } from "./model/Networks";
|
||||
import { NetworkEnum } from "./model/NetworkEnum";
|
||||
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();
|
||||
const targetNetwork = ref(NetworkEnum.sepolia);
|
||||
const targetNetwork = ref(DEFAULT_NETWORK);
|
||||
|
||||
const web3Onboard = init({
|
||||
wallets: [injected],
|
||||
chains: [
|
||||
{
|
||||
id: Networks[NetworkEnum.sepolia].chainId,
|
||||
token: "ETH",
|
||||
label: "Sepolia",
|
||||
rpcUrl: import.meta.env.VITE_SEPOLIA_API_URL,
|
||||
},
|
||||
{
|
||||
id: Networks[NetworkEnum.rootstock].chainId,
|
||||
token: "tRBTC",
|
||||
label: "Rootstock Testnet",
|
||||
rpcUrl: import.meta.env.VITE_ROOTSTOCK_API_URL,
|
||||
},
|
||||
],
|
||||
chains: Object.values(Networks).map((network) => ({
|
||||
id: `0x${network.id.toString(16)}`,
|
||||
token: network.nativeCurrency.symbol,
|
||||
label: network.name,
|
||||
rpcUrl: network.rpcUrls.default.http[0],
|
||||
})),
|
||||
connect: {
|
||||
autoConnectLastWallet: true,
|
||||
},
|
||||
@@ -41,7 +33,7 @@ if (!connectedWallet) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 sm:p-4 md:p-8">
|
||||
<main class="p-3 sm:p-4 md:p-8">
|
||||
<TopBar />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
@@ -62,5 +54,6 @@ if (!connectedWallet) {
|
||||
</template>
|
||||
</RouterView>
|
||||
<ToasterComponent :targetNetwork="targetNetwork" />
|
||||
</div>
|
||||
<VersionFooter />
|
||||
</main>
|
||||
</template>
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
@import './base.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "./base.css" layer(base);
|
||||
@import "tailwindcss";
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="375"
|
||||
height="416"
|
||||
viewBox="0 0 375 416"
|
||||
sodipodi:docname="rootstock-testnet.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.7788462"
|
||||
inkscape:cx="187.48108"
|
||||
inkscape:cy="215.3081"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="993"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="g1"
|
||||
style="fill:#b3b3b3">
|
||||
<image
|
||||
width="375"
|
||||
height="416"
|
||||
preserveAspectRatio="none"
|
||||
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXcAAAGgCAYAAABR+ydpAAAAAXNSR0IArs4c6QAAIABJREFUeF7s nXmYVMXVxt9ze2YAxRVZAtO3B8E1atxiTGLc97jvRo1rRJnuBlRi1ORzsmhUDEt3D4rRaNS4JUZj NMYtUaNGE3fjhgvTtwdcEEUFhJnue76negYEBKaXu9W9p54nf+Sx6pz3/KrmcPveqlMEaUJAYwLM IExtSaDR3grMm4AxCkRxML4GA4PBWA/AQACNy4VZAmERGPMBzAPT+wB3AjwLoJkAXsPH1kxqQ1Fj NCI94gQo4vFL+JoR4MnNA9BkfAfMuwH0bQA7AljfhTAWA/wiYPwbjCdQLD1K53R+7IIfMSkEXCEg yd0VrGLUSQI8JfE1NNqHwTYOAfHuAPo7ab9CWzbA/wFwLwzjz9Saf73CcdJNCPhCQJK7L9jFaV8E uH3wQNj9jwbRSWDsBsDoa4zH//0VADejSDfRhPx7HvsWd0KgTwKS3PtEJB28JMDZls0BToFYJfV1 vPRdo68iwPcAsSylOh6t0YYMEwKOE5Dk7jhSMVgLAc607AzDvhCMgwDouS4Jz8HGZfjY+jO1wa6F g4wRAk4R0POPyKnoxY7vBDiX2B42LgHx/r6LcU7AKyD+GSULf3HOpFgSAtURkOReHS/p7RABvqp5 BIrGZQBO0PZJvS8WapcNYzyNs57rq6v8dyHgNAFJ7k4TFXtrJMBtaMCGiXEgbuvdfx52YjaYZqBE F9KEDrWvXpoQ8ISAJHdPMIsTRYCzzVuDjOvB2CGCROaAcRalrb9GMHYJ2QcCktx9gB41l9wGA4MS 5wD8KwD9ohb/ivHydTAWj6fWuQuizUGid5uAJHe3CUfcPs8YvhG6Gm4GsF/EUSwf/huAfQylOtVe eWlCwBUCktxdwSpGe17DxFVpgDsBMoXIVwgsAvMZlC7cKmyEgBsEJLm7QVVsgrPm0QBuALCW4Fgt AQbRr/FR/meyL15WidMEJLk7TVTsgTOJc0E8KbRbHB2fY74F89Y5ldpe63LctBiMLAFJ7pGdeucD L5ffzZlq7/qPnbceeosPoKl4BI2Zsyj0kUqAnhCQ5O4J5vA76Uns8QxAyfBH61aE9BgWL/4+Tfxg oVsexG50CEhyj85cuxopZ8wpIIx31UkkjPOj6OYD6ZzOLyIRrgTpGgFJ7q6hjY5hzprqtOnF0YnY 9UjvQ9Pgw2nMc92uexIHoSUgyT20U+tNYJwzzwRjhjfeouSFrqdU/rQoRSyxOktAkruzPCNljTPm 3iDcD6AhUoF7FSzhQkpav/bKnfgJFwFJ7uGaT8+i4UxcXUT9XwAbeOY0eo5sMA6TejTRm3gnIpbk 7gTFiNkoX1LdSP8G6BsRC92PcOeDeUdKF97xw7n41JeAJHd958435Zw1rwJwlm8Couf4Wcwb+F05 5BS9ia8nYknu9dCL4FjOxg8BSG4Y8n7uL6OUdYH3bsWjrgQkues6cz7o7q3w+CqAIT64j7rLEtjY hdIdT0cdhMRfGQFJ7pVxkl7lKo/mTQBOFBi+EXgd8wZuK69nfOOvlWNJ7lpNl39iOWfuBcbD/ikQ zz0E+KeUKlwiNIRAXwQkufdFSP47yveeDjJfArCl4PCdwCIYxc2pdU7BdyUiINAEJLkHenqCIY5z iVYw54KhRlSA8AdKWvJ6TJbCGglIcpcFskYC3D54IOwBao+1fEQNzlphGMb21NrxYnAkiZKgEZDk HrQZCZgezpo/ASBH4AM2LwDupZR1cPBkiaKgEJDkHpSZCKAOnjR0bfTr1wHCRgGUF3VJ8vQe9RXQ R/yS3GWBrJYAZ8w0CNMEUVAJ0G2Uyh8fVHWiy18Cktz95R9Y79wGA4PMmQBGBVakCCvCKG4sO2dk IayKgCR3WRerJMCZxPdBfK/gCToBuoRS+Z8GXaXo856AJHfvmWvhkTPm3SAcqoXYaIt8D/Msk9pQ jDYGiX5lApLcZU18hQBfNWoIit2dABoFjwYEiL5PyfzfNFAqEj0kIMndQ9i6uJJDS7rM1FKdfAul Cifoplr0uktAkru7fLW0zlnzMQC7aik+iqIJn2NtYwid2rE4iuFLzKsmIMldVsYKBDgzbDCo6T0A MUGjEQGmgyidv08jxSLVZQKS3F0GrJt5zpkngXGjbrpFL02nVL5VOAiBpQQkuctaWOnJ3bwRhJME i3YE3qaUtYl2qkWwawQkubuGVk/DnDVVKdlmPdVHXLVRNOVAU8TXwHLhS3KXtbCMAE9paUGDPUuQ aEqA+QeULtyqqXqR7TABSe4OA9XZHGfjxwJ0m84xRFz7NEpZ4yPOQMLvJSDJXZbCl0/uWfNyAD8W JNoS+BelLNnCqu30OStckruzPLW2xtn43wA6QOsgoi1+PqWsDaKNQKJfSkCSu6yF5Z/cOwAkBInG BIo0nCbk1TkFaREnIMk94gtgaficGd0P1LUIgCFINCZAvBslC49rHIFId4iAJHeHQOpuptDWPLp5 kPGW7nFEXj/hh5S0boo8BwEASe6yCMoEONO8G8h4VHBoT+ACSlmXaR+FBFA3AUnudSMMhwHOmceA cXs4oolwFIyplLYmRJiAhN5LQJK7LIUygYWT42et1UhXCQ69CXQV+cZ+Ewon6x2FqHeCgCR3JyiG wAZnEhNBfEUIQol4CHwXpQpHRByChA/IO3dZBT0EOGteDKBNeGhP4AFKWftrH4UEUDcBeXKvG2E4 DHAu/kswyUXLmk/n3IWlfw75yew9NQ9D5DtAQJK7AxDDYIJziUvAfGEYYolyDK990P3Y13/13u5R ZiCx9xCQ5C4roUyg7cD12y4+YF31akaa3gTktYze8+eYeknujqHU2xBnzZ8A+LXeUYh6APdQyjpU SAgBSe6yBsoEOJNIgjgrOPQm0FXEH/pNsE7UOwpR7wQBSe5OUAyBjQWTm3+wdqPxhxCEEvEQOEep QiriECR8eecua2ApAc6Z+4DxoBDRnADhYkpav9A8CpHvAAF5cncAYhhMvPcL8+vDNsD/whBLlGNY tMT+0drndV4bZQYSew8BSe6yEnreubcPHgh7wOeCQ3MChH0paT2keRQi3wECktwdgBgWE5w13wcw NCzxRDKOGEbRWOvdSMYuQa9AQJK7LIhlBDibeBTg3QSJtgQWY6g1kI5BSdsIRLhjBCS5O4ZSf0Oc iedA1Kp/JJGN4HlKWTtENnoJXJ7cZQ2smgBnEqeB+DrhoysBvo5ShTN0VS+6nSUgT+7O8tTaGmeb twaMl7UOIsriCWMoaV0TZQQS+5cEJLnLavjynXsbDAwy5wFYX7BoSICwFSWtVzVULpJdICDJ3QWo OpvkrPkXAIfoHENEtX+IpDWMCBzR+CXslQhIcpclsQIBzprq6HpGsOhGgG6lVP4HuqkWve4RkOTu HlstLXMmPgpEb2spPtqiT6KUdXO0EUj0yxOQ5C7r4SsEOGu+AmArQaMNgSK67aF0TufH2igWoa4T 0D65s/oIuOGI4YAxEkAzDBoK5g0BWhfAWgCaeihyEYzFIPochPlg/hDAHCCWBzfMovTbS1ynrYkD zprqLlW5uEOT+QLwIKWs/fSR665SzozuByqOBEoJAMNBNASM9cG8Dgj9AWroVdAFYBHAn4HoY4Df h43ZgD0LH8+eQ22w3VXqrnWtkjvP2KERxQ+3hU07g7A9GNsA2Lw3iddDSp3o6wDoFRC/AOZn0NT4 NI1599N6jOo6lqeO3Ayx0hu66o+cbsIplLR+H7m41SPbjI3XQ1f3ziD6Fpi2A3hrAC0AYnXyWATg DRBeBuN5GPw0Goa8SGOe667TrmfDA5/cOTdiU7DxfbCxL4i/B2Btj+iohP8CgIfAfD+GFZ6K0rFu zsb/DdDOHrEWN7UTWIBFA4bT+W9Gougb34EY3o9/B0QHANgHwHYOJPJK6S8E079A9oMg+z5Kzp5Z 6UA/+gUyufck9Jj68n8MgC38APNVnzQXxH+Gbd+KVOfjYd9yxtnE6QBL6dhgLL7Vq2BcS2nrR0GX WY8+ZhCyzbvCMI4H0xEAD67HnoNjXwdwB6h0SxATfWCSO09uHoAmQyXzM8H4joMT4IapWQBfh1LD tTR+1gduOPDbJs8Yvha6GjoBbOC3FvG/BgJEO1Ay/3wYGfHUkUMRK54B0OkA1De14DbCU7D5tyjy 7XRO5xdBEOp7cuerRg1BqSsFm84CYaMgQKlCwxIQbkWJf0PjCqG76IIz5mUgnF8FD+nqLYHHKWWF roonT4tvhRidC8bxAPp5i7ROb4yPYPDViDVl6ex31KYN35pvyZ3bW4bBtn9SflIHBvhGwBnHDPBd ALdRqlNtIwxF42x8OECqNrhef2ChoF9BEISDKWndW0FPLbr01DaiNoAOD8FFQurp/RoYxmXU2qHu SfC8eZ7cyzf+cP/zwTTBw4+jXoFVW6dugd1wEY171/LKqZt+OGteBeAsN32I7ZoIvICktUMYvv1w +/A47IZLAajvbEZNNII7aCGIp4AWX06tcxd4KdOz5F7+KJJLHA/wpPLe03C3RQBdhoE0iU7tWKxz qJwZ0QyKvQWo/cHSgkOAD6VU4Z7g6KleCV/f0h8LeCLA6he8OpMS5jYHoIlI5m/16h9kT5I7Tzc3 RhHXgLBXmGfvq7HRTLine truncated
|
||||
id="image1"
|
||||
style="fill:#b3b3b3;fill-opacity:1" />
|
||||
<path
|
||||
style="fill:#b3b3b3;fill-opacity:1;stroke-width:0.562162"
|
||||
d="m 180.45405,414.20796 c -3.24648,-0.30209 -5.98702,-0.78137 -6.09008,-1.06509 -0.10307,-0.28371 -1.05172,-0.51584 -2.10811,-0.51584 -1.0564,0 -1.92072,-0.25298 -1.92072,-0.56217 0,-0.30918 -0.49102,-0.56216 -1.09115,-0.56216 -0.60013,0 -1.24749,-0.25297 -1.43858,-0.56216 -0.19109,-0.30919 -0.73698,-0.56216 -1.21309,-0.56216 -0.86347,0 -4.71858,-1.62979 -5.25178,-2.22026 -0.15459,-0.17119 -1.04,-0.69032 -1.96757,-1.15361 -0.92756,-0.46329 -2.02077,-1.15856 -2.42934,-1.54505 -0.40857,-0.38649 -0.93006,-0.7027 -1.15887,-0.7027 -0.2288,0 -0.9064,-0.45036 -1.50579,-1.0008 -0.59938,-0.55043 -2.10167,-1.75206 -3.33843,-2.67027 -2.81359,-2.0889 -2.91728,-2.19207 -5.63322,-5.60461 -1.23038,-1.54594 -2.38259,-2.9373 -2.56047,-3.09189 -0.17788,-0.1546 -0.80161,-1.10324 -1.38607,-2.10811 -0.58446,-1.00486 -1.26108,-1.82703 -1.50361,-1.82703 -0.24252,0 -0.44095,-0.37946 -0.44095,-0.84324 0,-0.46378 -0.25298,-0.84324 -0.56217,-0.84324 -0.30919,0 -0.56216,-0.32826 -0.56216,-0.72946 0,-0.40121 -0.8854,-2.45841 -1.96757,-4.57156 -1.08216,-2.11315 -1.96756,-4.31462 -1.96756,-4.89216 0,-0.57753 -0.22814,-1.05007 -0.50697,-1.05007 -0.27883,0 -0.68647,-1.58108 -0.90586,-3.51351 -0.21939,-1.93243 -0.60971,-3.77556 -0.86737,-4.09584 -0.25766,-0.32028 -0.58471,-4.11487 -0.72676,-8.43243 -0.27925,-8.48778 -0.753,-12.60228 -1.54792,-13.44362 -0.27752,-0.29373 -0.50458,-1.06753 -0.50458,-1.71956 0,-0.65202 -0.88541,-2.65586 -1.96757,-4.45297 -1.08216,-1.79711 -1.96757,-3.39307 -1.96757,-3.54658 0,-0.58109 -4.32266,-4.51095 -6.85994,-6.23658 -1.45405,-0.98891 -3.41986,-1.95325 -4.36846,-2.14296 -0.9486,-0.18972 -1.86087,-0.56525 -2.02729,-0.8345 -0.1664,-0.26925 -1.41107,-0.48955 -2.76593,-0.48955 -1.35486,0 -2.61972,-0.25298 -2.81081,-0.56217 -0.19109,-0.30919 -1.32947,-0.56216 -2.52973,-0.56216 -1.20026,0 -2.33864,0.25297 -2.52973,0.56216 -0.19109,0.30919 -1.597372,0.56217 -3.12507,0.56217 -1.527699,0 -2.777632,0.25297 -2.777632,0.56216 0,0.30919 -0.56919,0.57629 -1.264865,0.59357 -1.578366,0.0392 -5.727263,1.69778 -7.032115,2.81119 -0.933847,0.79684 -2.717503,1.69734 -6.740858,3.40317 -1.004865,0.42606 -1.827027,0.96585 -1.827027,1.19957 0,0.23371 -0.632433,0.42493 -1.405406,0.42493 -0.772973,0 -1.405405,0.19509 -1.405405,0.43354 0,0.48369 -5.660945,1.79529 -9.556757,2.21424 -1.391351,0.14962 -2.770054,0.50049 -3.063784,0.7797 -0.293729,0.27921 -3.089081,0.50766 -6.211892,0.50766 -3.12281,0 -5.918162,-0.22845 -6.211891,-0.50766 -0.29373,-0.27921 -1.672433,-0.64473 -3.063784,-0.81225 -4.44802,-0.53556 -9.556757,-1.69027 -9.556757,-2.16009 0,-0.25033 -0.491016,-0.45514 -1.091147,-0.45514 -0.600131,0 -1.247493,-0.25297 -1.438583,-0.56216 -0.191089,-0.30919 -0.838451,-0.56216 -1.438582,-0.56216 -0.600131,0 -1.091147,-0.23288 -1.091147,-0.5175 0,-0.28462 -0.724734,-0.67667 -1.610519,-0.87122 -0.885786,-0.19456 -2.140073,-0.84709 -2.787305,-1.45007 -0.647233,-0.603 -1.440337,-1.09635 -1.762455,-1.09635 -0.322117,0 -0.585667,-0.21237 -0.585667,-0.47192 0,-0.25956 -0.56919,-0.73745 -1.264865,-1.06198 C 22.08292,327.58353 11.243243,316.97 11.243243,315.39421 c 0,-0.2767 -0.316216,-0.84507 -0.702702,-1.26306 -1.3845193,-1.49734 -4.0555867,-6.51003 -5.6289285,-10.56358 -0.3000203,-0.77298 -0.76895,-1.84468 -1.0420656,-2.38156 -0.2731156,-0.53689 -0.4965739,-1.46446 -0.4965739,-2.06127 0,-0.5968 -0.2301341,-1.16942 -0.5114091,-1.27249 -0.2812749,-0.10306 -0.7889857,-2.97009 -1.1282463,-6.37117 -0.3392605,-3.40108 -0.86804536,-6.26811 -1.17507742,-6.37117 -0.74875885,-0.25134 -0.71696408,-3.88223 0.0361941,-4.13328 0.3269389,-0.10898 0.86215012,-2.97601 1.18935832,-6.37117 0.3272082,-3.39517 0.8186317,-6.25735 1.0920522,-6.36041 0.2734205,-0.10307 0.4971282,-0.67569 0.4971282,-1.2725 0,-0.5968 0.2317062,-1.52437 0.5149026,-2.06126 0.2831965,-0.53689 0.8817713,-1.86156 1.3301664,-2.94372 2.0206548,-4.87667 2.3903001,-5.62162 2.7894582,-5.62162 0.2337128,0 0.4249322,-0.24806 0.4249322,-0.55125 0,-2.18603 13.1652956,-17.43794 15.0522526,-17.43794 0.327027,0 0.678918,-0.18973 0.781982,-0.42162 0.269511,-0.6064 4.833396,-3.51352 5.515875,-3.51352 0.315818,0 0.574215,-0.25297 0.574215,-0.56216 0,-0.30919 0.254213,-0.56216 0.564918,-0.56216 0.310706,0 1.875888,-0.63244 3.478184,-1.40541 1.602295,-0.77297 3.371866,-1.4054 3.932378,-1.4054 0.560513,0 1.019114,-0.25298 1.019114,-0.56217 0,-0.30918 0.611352,-0.56216 1.358559,-0.56216 0.747207,0 1.442883,-0.22315 1.545946,-0.49589 0.179311,-0.47452 11.287869,-1.91815 14.803603,-1.92383 3.312547,-0.005 14.063842,1.39797 14.588109,1.90412 0.293729,0.28358 0.99284,0.5156 1.553574,0.5156 0.560734,0 1.458788,0.23171 1.995676,0.5149 0.536887,0.2832 1.988047,0.94893 3.224804,1.4794 5.015301,2.15116 8.372404,3.9387 9.715713,5.17327 0.420526,0.38649 0.990946,0.7027 1.267603,0.7027 1.55602,0 12.207422,10.87828 13.623322,13.91352 0.32453,0.69567 0.80242,1.26486 1.06198,1.26486 0.25955,0 0.47192,0.37946 0.47192,0.84324 0,0.46379 0.20083,0.84325 0.44629,0.84325 0.50146,0 4.61317,8.42979 4.61317,9.45788 0,0.3Line truncated
|
||||
id="path2" />
|
||||
<path
|
||||
style="fill:#b3b3b3;fill-opacity:1;stroke-width:0.562162"
|
||||
d="m 181.85946,113.07866 c -0.30919,-0.29842 -1.82703,-0.66518 -3.37297,-0.81502 -3.43346,-0.33279 -8.0885,-1.37307 -9.40859,-2.10257 -0.53689,-0.2967 -1.35905,-0.53945 -1.82703,-0.53945 -0.46798,0 -0.85087,-0.25297 -0.85087,-0.56216 0,-0.30919 -0.4427,-0.57021 -0.98378,-0.58004 -0.54108,-0.01 -1.47484,-0.3893 -2.07502,-0.84324 -0.60017,-0.45395 -1.54882,-0.82537 -2.10811,-0.82537 -0.55928,0 -1.01687,-0.25297 -1.01687,-0.56216 0,-0.30919 -0.37946,-0.56216 -0.84325,-0.56216 -0.46378,0 -0.84324,-0.22229 -0.84324,-0.49396 0,-0.27168 -0.75892,-0.80832 -1.68649,-1.19253 -0.92756,-0.38421 -1.68648,-0.9038 -1.68648,-1.15464 0,-0.25084 -0.47595,-0.60714 -1.05767,-0.79177 -1.04027,-0.33017 -10.74774,-9.594144 -10.74774,-10.25672 0,-0.186665 -0.63243,-1.168555 -1.4054,-2.181976 -0.77298,-1.013421 -1.40541,-2.112431 -1.40541,-2.442251 0,-0.329821 -0.25297,-0.59967 -0.56216,-0.59967 -0.30919,0 -0.56216,-0.379459 -0.56216,-0.843243 0,-0.463784 -0.20429,-0.843244 -0.45398,-0.843244 -0.24969,0 -0.91772,-1.075135 -1.48452,-2.389189 -0.56679,-1.314054 -1.50088,-3.385351 -2.07575,-4.602888 -0.57487,-1.217536 -1.04521,-2.672131 -1.04521,-3.232432 0,-0.560302 -0.21168,-1.018734 -0.47039,-1.018734 -0.90563,0 -1.92543,-8.783587 -1.92543,-16.583784 0,-7.800195 1.0198,-16.583783 1.92543,-16.583783 0.25871,0 0.47039,-0.632433 0.47039,-1.405406 0,-0.772973 0.25297,-1.405405 0.56216,-1.405405 0.30919,0 0.56216,-0.332115 0.56216,-0.738033 0,-0.405918 0.63243,-2.049002 1.40541,-3.651298 0.77297,-1.602295 1.4054,-3.167478 1.4054,-3.478183 0,-0.310705 0.25297,-0.564919 0.56216,-0.564919 0.30919,0 0.56217,-0.379459 0.56217,-0.843243 0,-0.463784 0.22228,-0.843243 0.49396,-0.843243 0.27167,0 0.79796,-0.733931 1.16952,-1.630958 0.37156,-0.897027 1.28766,-2.205988 2.03577,-2.908803 0.74811,-0.702816 1.36021,-1.472715 1.36021,-1.710889 0,-0.579165 5.40291,-5.88675 7.87027,-7.73141 1.08216,-0.809052 2.09405,-1.609838 2.24864,-1.779524 0.1546,-0.169687 1.48225,-1.056532 2.95035,-1.9707657 1.4681,-0.9142348 2.66972,-1.8519747 2.67027,-2.0838666 5.5e-4,-0.2318919 0.35996,-0.4216216 0.79869,-0.4216216 0.43873,0 1.64035,-0.4222619 2.67027,-0.9383599 3.41216,-1.7098496 3.8182,-1.8724509 4.67577,-1.8724509 0.46798,0 0.85087,-0.252973 0.85087,-0.5621622 0,-0.3091892 0.70833,-0.5621621 1.57405,-0.5621621 0.86573,0 1.81438,-0.2323009 2.10811,-0.5162242 0.29373,-0.2839232 3.18628,-0.7778909 6.42789,-1.0977061 3.2416,-0.319815 5.98214,-0.84643309 6.09009,-1.17026233 0.25569,-0.76708351 3.63845,-0.76316439 4.40672,0.005105 0.32664,0.32663724 3.22318,0.85503913 6.43676,1.17422643 3.21357,0.3191874 5.92719,0.8108577 6.03025,1.0926007 0.10307,0.281743 0.92523,0.51226 1.82703,0.51226 0.9018,0 1.63964,0.2529729 1.63964,0.5621621 0,0.3091892 0.63243,0.5621622 1.40541,0.5621622 0.77297,0 1.4054,0.252973 1.4054,0.5621622 0,0.3091891 0.4576,0.5621621 1.01688,0.5621621 0.55928,0 1.50793,0.3714138 2.10811,0.8253648 0.60017,0.4539505 1.53393,0.8334099 2.07501,0.8432433 0.54108,0.00983 0.98379,0.2708514 0.98379,0.5800406 0,0.3091892 0.26364,0.5621621 0.58589,0.5621621 0.32223,0 0.89142,0.2538382 1.26486,0.5640848 0.37343,0.3102466 1.99753,1.4462463 3.60909,2.5244413 2.66795,1.784963 11.40502,9.989899 11.40502,10.710396 0,0.166422 0.31622,0.680148 0.7027,1.141612 0.38649,0.461464 0.95568,1.146462 1.26487,1.522219 0.30919,0.375756 0.91884,1.374599 1.35478,2.219651 0.43594,0.845051 1.22876,2.004601 1.76182,2.576779 0.53307,0.572178 1.14514,1.841312 1.36016,2.820299 0.21502,0.978988 0.62383,1.779978 0.90845,1.779978 0.28462,0 0.51749,0.632432 0.51749,1.405405 0,0.772973 0.25297,1.405406 0.56216,1.405406 0.30919,0 0.56217,0.632432 0.56217,1.405405 0,0.772973 0.20804,1.405406 0.46231,1.405406 1.0378,0 2.0087,8.481909 1.99483,17.427027 -0.0164,10.608871 -0.72744,15.174358 -3.08489,19.808587 -0.27312,0.536888 -0.49658,1.35905 -0.49658,1.827027 0,0.467978 -0.24151,0.850872 -0.53669,0.850872 -0.29517,0 -0.70279,0.885405 -0.90581,1.967568 -0.20301,1.082162 -0.59393,1.967567 -0.86871,1.967567 -0.27478,0 -0.4996,0.37946 -0.4996,0.843243 0,0.463784 -0.20612,0.843244 -0.45804,0.843244 -0.25193,0 -0.8828,0.822162 -1.40194,1.827027 -0.51915,1.004865 -1.26167,2.309233 -1.65006,2.898598 -1.03291,1.56737 -10.98752,11.577076 -11.51336,11.577076 -0.24615,0 -1.0774,0.58355 -1.84721,1.29677 -0.76981,0.71322 -2.22182,1.66038 -3.22669,2.1048 -1.00486,0.44441 -1.82702,0.99924 -1.82702,1.23296 0,0.23371 -0.25422,0.42493 -0.56492,0.42493 -0.31071,0 -1.87589,0.63243 -3.47819,1.40541 -1.60229,0.77297 -3.24538,1.4054 -3.65129,1.4054 -0.40592,0 -0.73804,0.22923 -0.73804,0.50941 0,0.28018 -1.3281,0.68796 -2.95135,0.90619 -1.62324,0.21823 -3.20432,0.59542 -3.51351,0.83821 -1.28934,1.01241 -17.09178,2.06939 -17.98919,1.20323 z"
|
||||
id="path3" />
|
||||
<path
|
||||
style="fill:#666666;stroke-width:0.562162"
|
||||
d="m 180.45405,413.99003 c -6.22403,-1.10996 -8.7453,-1.77793 -12.76882,-3.38288 -5.7302,-2.28574 -11.13907,-5.4179 -15.90145,-9.20818 -8.66889,-6.8994 -16.7896,-20.20407 -18.8811,-30.93411 -0.45201,-2.31892 -1.06539,-8.1573 -1.36306,-12.97418 -0.59065,-9.55763 -1.58544,-13.70964 -4.50899,-18.81948 -5.0696,-8.86073 -17.09257,-14.52382 -26.85868,-12.65104 -5.87418,1.12646 -8.888105,2.23304 -15.847628,5.81856 -7.069554,3.6422 -11.402048,5.22317 -15.902668,5.80306 -1.61137,0.20761 -3.540514,0.60403 -4.286986,0.88094 -1.851734,0.68689 -11.887876,0.64911 -14.506578,-0.0546 -1.168964,-0.31413 -4.056797,-0.9368 -6.417407,-1.3837 C 32.426281,335.04275 17.400009,324.52345 10.962666,314.50885 8.4439099,310.59041 4.8041036,303.0256 3.602451,299.21171 2.534914,295.82349 0.92277292,285.99962 0.95001844,283.04864 0.97805015,280.0125 2.4941036,270.75763 3.5407895,267.23305 8.0380406,252.08917 21.318097,237.58338 36.259459,231.4945 c 6.299621,-2.5672 8.137954,-3.03016 15.227297,-3.83477 6.253205,-0.70972 14.008271,-0.11281 20.188918,1.55393 4.229106,1.14047 12.437523,4.91252 16.864864,7.75 6.321339,4.05132 14.666852,13.72424 18.477562,21.4165 4.13659,8.35009 5.11221,12.53661 6.07623,26.07389 0.89327,12.54384 3.18272,18.62922 9.0236,23.98489 5.79912,5.31738 10.11216,6.93402 18.49922,6.93402 8.87757,0 14.58387,-2.09628 24.85704,-9.13154 4.96445,-3.39975 9.43764,-9.73655 11.37208,-16.10986 1.19117,-3.92448 1.01574,-11.0092 -0.37771,-15.25387 -2.2181,-6.75665 -7.65246,-13.04332 -15.38748,-17.80079 -12.4599,-7.66356 -20.02818,-16.08572 -25.30389,-28.15877 -2.71515,-6.21341 -2.91624,-7.21809 -3.66326,-18.30189 -0.85213,-12.64365 -2.59343,-19.23699 -6.44609,-24.40783 -2.99835,-4.02424 -4.9325,-5.81837 -8.24493,-7.64809 -4.64133,-2.56377 -7.1492,-3.14887 -13.53407,-3.15755 -5.307698,-0.007 -6.571225,0.1935 -10.760547,1.70937 -2.610681,0.94466 -5.975795,2.50869 -7.478033,3.47563 -2.651505,1.70668 -5.8401,3.0384 -13.954849,5.82827 -3.598383,1.23713 -4.781785,1.34553 -14.616216,1.33887 -11.608725,-0.008 -12.8086,-0.22283 -21.534948,-3.85793 -6.67657,-2.78123 -10.756926,-5.57156 -16.898695,-11.5561 -4.89564,-4.77031 -11.3374441,-12.93714 -11.3374441,-14.37345 0,-0.22358 -0.732432,-2.02259 -1.6276264,-3.99778 C 4.7852868,151.99444 3.7461949,149.24 3.3713883,147.84864 2.4720363,144.51008 0.98484203,135.15452 0.95857764,132.67027 0.92981349,129.94956 2.6316553,119.43779 3.5979626,116.36756 c 1.2951409,-4.11502 4.6635656,-11.05132 7.1808944,-14.78699 4.55578,-6.760688 14.244166,-15.635706 19.85898,-18.19176 0.927568,-0.42226 2.065946,-1.003634 2.52973,-1.291941 1.771166,-1.101031 8.288847,-3.325512 11.416313,-3.896381 1.795729,-0.327781 3.502902,-0.743021 3.793717,-0.922755 0.290816,-0.179734 4.077984,-0.330058 8.41593,-0.334053 6.737053,-0.0062 8.76607,0.215086 13.91439,1.517544 9.905056,2.505852 17.44306,6.823574 25.328694,14.508122 4.762759,4.641309 8.705499,10.005004 11.008679,14.976184 0.70187,1.5149 1.52426,3.08094 1.82755,3.48009 1.69511,2.23091 3.33159,10.69168 4.38097,22.65005 0.43411,4.94703 1.02483,9.90839 1.31269,11.02525 1.80454,7.0012 7.60249,14.22787 13.90386,17.33003 4.36018,2.1465 10.11128,3.11296 15.5805,2.61825 3.91289,-0.35394 10.64775,-2.55763 12.25111,-4.00866 0.50098,-0.45338 1.23137,-0.82433 1.62308,-0.82433 0.39171,0 1.38368,-0.47407 2.20438,-1.05349 5.41891,-3.82578 18.51411,-7.41793 27.14153,-7.4452 9.55647,-0.0302 16.61857,1.98821 29.5547,8.44702 9.96061,4.97318 14.62803,5.91306 22.75405,4.58201 7.10966,-1.16458 14.41868,-6.49562 18.21646,-13.28668 2.38432,-4.26358 3.00278,-6.98455 3.69575,-16.25987 1.11836,-14.96903 3.94942,-24.32008 10.26112,-33.89263 2.75945,-4.18508 10.26982,-11.755795 14.78892,-14.90775 4.40093,-3.06954 11.75671,-6.451152 16.18236,-7.439386 2.16432,-0.483287 4.73874,-1.128067 5.72094,-1.432843 2.46207,-0.763989 16.13182,-0.689945 19.5713,0.106011 6.45189,1.493074 9.23971,2.416036 13.65604,4.52111 6.72727,3.206596 10.75923,6.172279 16.36873,12.039923 6.67555,6.982745 10.40698,13.263815 13.50647,22.735295 0.92418,2.82411 2.50821,12.76488 2.50821,15.74054 0,2.69735 -1.54232,12.65333 -2.3949,15.45946 -1.18795,3.90997 -4.55461,11.02377 -7.02272,14.83908 -5.12548,7.92316 -17.01415,18.01529 -24.19163,20.53591 -0.95924,0.33687 -2.04776,0.80736 -2.41894,1.04554 -0.37117,0.23817 -2.64487,1.05828 -5.05264,1.82247 -4.0202,1.27594 -5.25237,1.38943 -15.08541,1.38943 -9.83086,0 -11.02824,-0.11023 -14.62303,-1.34612 -8.33237,-2.86469 -11.7202,-4.22348 -13.13119,-5.26665 -2.29613,-1.69757 -5.0914,-3.07224 -9.05448,-4.45283 -4.54508,-1.58334 -14.50344,-1.81431 -18.48236,-0.42866 -7.20223,2.50816 -12.57869,7.28915 -15.77066,14.02399 -2.62313,5.53461 -3.26936,8.82163 -3.99588,20.32464 -0.35137,5.56314 -0.97275,10.39108 -1.62352,12.61425 -2.202,7.52244 -6.96849,16.01503 -12.34435,21.99428 -3.08677,3.43323 -10.2969,9.06014 -15.64689,12.21111 -4.92527,2.90083 -10.29949,8.47654 -13.33386,13.83379 l -2.21373,3.90841 0.003,7.87027 0.003,7.87027 2.11683,3.93514 c 2.90954,5.40876 4.29161,7.1042 8.65494,10.617Line truncated
|
||||
id="path1" />
|
||||
<path
|
||||
style="fill:#999999;stroke-width:0.562162"
|
||||
d="m 181.56218,112.80934 c -0.1635,-0.17358 -2.0608,-0.59047 -4.21622,-0.92642 -10.04121,-1.56505 -18.71078,-5.95429 -27.29462,-13.818776 -10.02676,-9.186477 -16.28485,-20.965084 -17.45508,-32.852874 -0.99809,-10.139159 0.25779,-23.676788 2.5294,-27.265325 0.39144,-0.618378 1.30906,-2.617602 2.03915,-4.442719 2.83182,-7.079115 10.83219,-16.556533 18.74217,-22.202395 5.42215,-3.8701346 4.74121,-3.4891096 10.49302,-5.8714466 5.32464,-2.2054109 5.92351,-2.3648698 13.49189,-3.592407 6.81719,-1.10570114 8.60429,-1.10611466 15.45946,-0.00358 6.45256,1.037785 10.58969,2.210356 14.56495,4.1280847 1.53527,0.7406425 2.99613,1.3466229 3.24633,1.3466229 0.85617,0 8.60683,5.787337 12.84856,9.593889 3.57184,3.205389 11.21137,13.06757 11.2247,14.490417 0.002,0.203361 0.40222,1.128666 0.88959,2.056234 3.84884,7.325131 5.07332,14.888375 4.55655,28.144355 -0.25433,6.52378 -0.57697,8.735848 -1.84489,12.648649 -0.84516,2.608173 -1.87549,5.141507 -2.2896,5.629638 -0.41413,0.488131 -0.75295,1.229409 -0.75295,1.647292 0,1.225682 -4.46938,7.830205 -8.13508,12.02141 -3.38488,3.870144 -10.28753,9.764311 -13.86428,11.838691 -1.04089,0.60368 -3.28388,1.67891 -4.98442,2.3894 -1.70055,0.71049 -3.59784,1.54552 -4.21622,1.85563 -3.07547,1.5423 -10.56989,2.91875 -17.42703,3.20073 -4.01946,0.16528 -7.44188,0.15849 -7.60538,-0.0151 z"
|
||||
id="path4" />
|
||||
<path
|
||||
style="fill:#666666;stroke-width:0.562162"
|
||||
d="m 176.22224,412.93597 c -14.27631,-3.31448 -27.3642,-12.3061 -34.92525,-23.9943 -6.17372,-9.54359 -8.14478,-15.99326 -9.47275,-30.99658 -1.02089,-11.5339 -2.1083,-15.67794 -5.41225,-20.62565 -4.63901,-6.94696 -13.77624,-11.82756 -22.14302,-11.82756 -5.228643,0 -11.162073,1.77986 -20.350928,6.1047 -9.122306,4.2935 -10.163976,4.66906 -16.739665,6.03525 -8.752618,1.81847 -20.689239,0.77528 -29.698317,-2.59545 -4.182604,-1.56491 -11.444871,-6.00341 -16.262927,-9.93947 -5.985462,-4.88975 -9.59184,-9.60177 -13.6809846,-17.8753 -5.9214348,-11.98078 -7.36746076,-22.64405 -4.761285,-35.11063 1.4170213,-6.77829 2.6046715,-9.86343 6.0987717,-15.84269 7.5446749,-12.91078 20.7421299,-23.1781 34.6939309,-26.9911 6.495658,-1.77525 20.47665,-1.78015 26.935005,-0.009 4.98703,1.36732 14.624108,5.79127 18.650999,8.56184 6.287199,4.32571 14.35862,14.12776 18.24499,22.15697 2.97921,6.15505 4.25865,11.66321 5.29158,22.781 0.95297,10.25711 1.99576,15.17513 4.11103,19.38848 1.96522,3.91449 7.14288,8.78989 11.73731,11.05211 3.55735,1.75158 4.2843,1.89758 10.36201,2.08108 7.92121,0.23915 11.37518,-0.41845 17.33455,-3.30034 9.6533,-4.66822 15.31578,-9.81539 19.0009,-17.27171 l 2.12622,-4.30212 v -7.36642 c 0,-7.36297 -9.7e-4,-7.36839 -2.08226,-11.57958 -2.76167,-5.58787 -7.05727,-9.90078 -15.68542,-15.74866 -10.85825,-7.35937 -17.15186,-14.18235 -22.03721,-23.89083 -3.41797,-6.79239 -4.18005,-9.88096 -5.18178,-21.00079 -0.51352,-5.70028 -1.40997,-12.22648 -1.99213,-14.50268 -2.21718,-8.6691 -7.24969,-15.26576 -14.28345,-18.72289 -3.74537,-1.84087 -4.24652,-1.93645 -11.14588,-2.12564 -6.703985,-0.18385 -7.534793,-0.088 -11.537758,1.33056 -2.372465,0.84076 -6.10751,2.51184 -8.3001,3.7135 -2.192595,1.20166 -6.871465,3.21395 -10.397492,4.47176 l -6.410953,2.28692 -10.683594,-0.003 c -12.043232,-0.003 -13.933205,-0.30792 -21.105535,-3.40008 -9.744277,-4.20099 -15.579406,-8.76272 -23.621249,-18.4664 C 9.4391111,161.23789 8.1112645,159.06339 6.2033497,154.4486 1.6199165,143.36237 0.48469696,132.49449 2.754034,121.42702 6.2409887,104.42128 20.046648,87.66215 35.862502,81.235593 c 7.3796,-2.9986 12.197624,-3.879341 21.196956,-3.87483 7.414553,0.0037 9.10516,0.188099 14.005533,1.527482 16.109982,4.403224 29.576349,15.998483 37.183489,32.016945 2.33258,4.91176 3.18232,9.10033 4.73272,23.32869 0.77072,7.07306 1.57639,11.85022 2.33117,13.82261 1.6117,4.2116 6.38278,10.2872 10.03663,12.78087 4.24972,2.90031 7.81611,3.98719 14.0968,4.29607 7.17379,0.3528 10.57855,-0.57434 21.05149,-5.73247 15.50896,-7.63847 28.18539,-9.27043 41.16495,-5.29958 2.09441,0.64074 8.14041,3.15256 13.43555,5.58182 5.29514,2.42925 10.79266,4.71021 12.21672,5.06879 1.48221,0.37322 5.05715,0.55814 8.36166,0.43252 5.01313,-0.19058 6.25314,-0.44667 9.42653,-1.94674 5.04326,-2.38399 8.97516,-5.6629 11.72041,-9.77395 3.06149,-4.58462 3.85153,-7.48151 4.89508,-17.94918 1.74016,-17.45527 4.93995,-27.04452 12.14919,-36.409125 2.77726,-3.607582 9.49702,-10.042349 13.01413,-12.462183 4.19904,-2.889014 11.48797,-6.146221 17.24958,-7.708319 4.62118,-1.252906 6.4795,-1.448546 13.81896,-1.454838 10.00342,-0.0086 14.78652,1.008991 23.1989,4.935384 6.85139,3.197815 11.28136,6.411166 16.52942,11.989887 8.84982,9.407414 13.15322,18.154884 15.15861,30.812734 l 1.18064,7.45209 -1.18393,7.47285 c -2.24428,14.16571 -7.97048,24.40936 -19.16944,34.29238 -4.99867,4.41129 -10.00303,7.47472 -16.36696,10.01906 -5.25051,2.09918 -6.61702,2.40613 -12.36757,2.77797 -8.31257,0.5375 -17.50599,0.10355 -21.08108,-0.99508 -5.50519,-1.69175 -11.5705,-4.20674 -14.72219,-6.10459 -1.75959,-1.05958 -4.54675,-2.43066 -6.19368,-3.04686 -4.06672,-1.52155 -11.87994,-2.32516 -16.67002,-1.71454 -10.33861,1.31792 -18.98919,9.62295 -21.9788,21.10085 -0.46274,1.77658 -1.23569,7.92509 -1.71767,13.66336 -0.92011,10.95445 -1.97167,15.38628 -5.13647,21.64781 -5.29555,10.47717 -11.49434,17.02946 -23.52711,24.86883 -6.84116,4.45701 -11.22354,9.03158 -14.82935,15.47963 -1.72816,3.09036 -1.75444,3.25352 -1.75444,10.89545 v 7.7581 l 2.38919,4.52092 c 3.87383,7.33023 9.55449,12.15437 20.51096,17.41837 l 4.92688,2.3671 h 8.99459 c 8.79686,0 9.06875,-0.0361 12.36757,-1.64296 4.42572,-2.15575 9.73568,-7.55071 12.04126,-12.23401 1.87742,-3.81359 2.35922,-6.23753 3.68562,-18.54238 1.49665,-13.88433 2.70001,-18.00054 8.10734,-27.732 1.88311,-3.38901 4.103,-6.10681 9.00804,-11.02855 6.23752,-6.25875 6.80228,-6.67485 13.53612,-9.97314 8.19,-4.01154 13.83656,-5.59508 21.64324,-6.06973 16.15952,-0.9825 30.09441,4.36787 41.93321,16.10047 5.21649,5.16969 8.20568,9.255 11.36357,15.53057 2.86456,5.69262 4.06002,9.67806 5.19046,17.30392 1.05443,7.11315 0.80678,11.03163 -1.21182,19.1743 -4.07479,16.43699 -15.48282,30.20125 -31.20513,37.65026 -15.2814,7.24011 -32.28907,6.51076 -50.04289,-2.14602 -4.12966,-2.01362 -9.40069,-4.18529 -11.7134,-4.82593 -15.89832,-4.40396 -30.93823,4.63609 -34.53755,20.75953 -0.44864,2.00973 -1.22301,7.95459 -1.72083,13.21081 -1.07546,11.35532 -1.81775,14.59129 -4.81345,20.98395 -6.57831,14.03776 -19.65376,25.3Line truncated
|
||||
id="path5" />
|
||||
<path
|
||||
style="fill:#999999;stroke-width:0.562162"
|
||||
d="m 175.95675,111.37992 c -10.99475,-2.4062 -19.5687,-7.28188 -27.93576,-15.886043 -4.54217,-4.670887 -6.59161,-7.312268 -8.96287,-11.551679 -4.76714,-8.522828 -6.38785,-15.247462 -6.38785,-26.504321 0,-10.107303 1.03301,-15.975432 3.88128,-22.048044 3.19391,-6.809532 6.14903,-11.127545 11.25563,-16.446696 10.16277,-10.5857799 19.59958,-15.2165005 35.20757,-17.2766401 4.34916,-0.5740577 5.98163,-0.5321512 11.21046,0.2877807 9.28788,1.4564284 17.94097,4.52445 22.73943,8.0624424 10.14221,7.478042 16.67077,14.732556 20.58689,22.876092 3.83142,7.967391 4.30153,10.302845 4.59599,22.832765 0.2327,9.902058 0.13742,11.516425 -0.92904,15.74054 -4.57937,18.138285 -20.91198,34.480333 -39.36116,39.383903 -6.28332,1.67003 -19.45914,1.9396 -25.90057,0.5299 z"
|
||||
id="path6" />
|
||||
<path
|
||||
style="fill:#999999;stroke-width:0.562162"
|
||||
d="m 176.51891,412.99193 c -18.014,-4.24378 -31.74984,-15.32437 -39.6549,-31.98928 -2.72624,-5.7473 -4.02486,-11.61565 -5.00659,-22.62428 -0.46874,-5.25622 -1.24485,-11.05566 -1.72469,-12.88765 -2.4271,-9.26643 -9.98203,-16.60913 -19.69123,-19.13806 -7.084,-1.84514 -13.016383,-0.78153 -24.14961,4.32977 -10.192826,4.67956 -12.230957,5.44159 -17.989189,6.72593 -9.382453,2.0927 -21.351551,1.17161 -30.822641,-2.37198 C 30.118977,332.28225 19.571198,324.6698 14.264732,318.28163 9.7058942,312.79348 4.2793903,301.48601 2.7519433,294.29188 c -2.08046687,-9.7988 -1.1538709,-21.85184 2.3671206,-30.7912 3.163004,-8.03047 10.3760521,-17.54477 17.8574991,-23.55474 12.396451,-9.95828 26.681773,-13.87478 42.555106,-11.667 7.572211,1.0532 18.566647,5.76775 25.356936,10.87337 5.815905,4.37298 12.825085,13.22132 16.519665,20.85433 3.0022,6.20254 4.17011,11.21153 5.28233,22.65531 1.04612,10.7636 1.86762,14.64281 4.0584,19.16421 1.98717,4.10118 7.20896,9.12687 11.86197,11.41654 3.74376,1.84224 3.78961,1.84918 12.20048,1.84747 7.8435,-0.002 8.69112,-0.10633 11.94472,-1.476 10.92484,-4.59902 18.58151,-11.00805 22.41058,-18.75884 l 2.19541,-4.44393 v -7.33543 c 0,-7.17305 -0.0415,-7.42033 -1.87497,-11.17032 -2.61838,-5.35538 -7.7954,-10.61122 -15.97007,-16.21323 -10.97394,-7.52032 -17.06914,-14.14372 -21.95985,-23.86285 -3.40786,-6.7723 -4.2398,-10.0683 -5.14589,-20.3871 -0.46284,-5.27092 -1.22284,-11.42717 -1.6889,-13.68056 -1.86747,-9.0292 -7.37139,-16.58353 -14.71888,-20.20221 -3.63574,-1.79063 -4.18354,-1.89385 -11.04957,-2.08213 -6.698666,-0.1837 -7.536119,-0.0874 -11.512326,1.32421 -2.358478,0.83728 -6.323374,2.61502 -8.810874,3.95055 -2.487495,1.33553 -7.17894,3.34803 -10.425427,4.47223 l -5.902702,2.04401 -10.44097,0.01 c -5.742535,0.005 -11.434427,-0.17419 -12.64865,-0.39915 C 39.772586,185.87135 30.31374,181.46615 24.978535,177.4556 21.088086,174.5311 14.265586,167.55892 10.790813,162.95662 5.6611499,156.16243 1.5674794,142.61878 1.5515758,132.38919 1.5310317,119.17464 7.1018281,105.23305 16.379544,95.280423 c 7.916371,-8.492254 16.144723,-13.564837 26.62586,-16.41421 4.965169,-1.349817 6.606302,-1.526699 14.054054,-1.514751 7.41045,0.01189 9.11375,0.199517 14.054054,1.548147 15.963448,4.357787 29.343418,15.773565 36.860788,31.449601 2.60623,5.43479 3.40957,9.28416 5.01027,24.00754 1.13476,10.43765 1.29598,11.17735 3.36949,15.45946 4.86771,10.05261 13.48718,15.45946 24.64506,15.45946 5.8759,0 9.79055,-1.18531 19.46332,-5.89324 9.02871,-4.39446 14.37826,-6.06535 22.55732,-7.04558 9.80689,-1.17533 17.90288,0.68764 32.56942,7.49455 11.43901,5.30897 12.65703,5.63467 19.94839,5.33426 5.68031,-0.23404 6.44452,-0.4059 10.31806,-2.3203 6.76303,-3.34248 12.63096,-10.03964 14.37123,-16.40212 0.33827,-1.23676 1.00748,-6.16973 1.48713,-10.96216 1.28513,-12.84042 2.879,-19.71099 6.2065,-26.75374 4.75376,-10.061457 14.73847,-20.44785 24.1985,-25.171999 9.82331,-4.905565 14.70285,-6.053815 25.7837,-6.067415 10.05543,-0.01234 15.08347,1.04753 23.24626,4.900131 6.9187,3.265423 11.30059,6.46105 16.66762,12.155355 5.63486,5.978478 9.24298,11.514008 11.76033,18.042548 8.58449,22.26322 2.5079,46.15789 -15.69583,61.71983 -5.64299,4.82406 -10.39679,7.74025 -16.58378,10.17319 -5.19466,2.04273 -6.67818,2.37217 -12.36757,2.74637 -8.28969,0.54522 -17.49599,0.1138 -21.08108,-0.9879 -5.60872,-1.72357 -11.64854,-4.24236 -15.3265,-6.39164 -4.83842,-2.82741 -7.99401,-3.85101 -13.62079,-4.41824 -6.50447,-0.65572 -11.70717,0.0477 -16.00628,2.16417 -6.7127,3.30465 -12.49629,10.8072 -14.55731,18.88393 -0.4734,1.85514 -1.258,7.97645 -1.74355,13.60293 -0.96229,11.15049 -1.96397,15.3348 -5.22193,21.81329 -5.31831,10.57554 -10.96502,16.4776 -24.31283,25.41228 -5.22637,3.49841 -11.05373,9.60147 -13.71987,14.36896 l -2.02067,3.61329 v 7.82986 7.82986 l 2.55257,4.48368 c 4.27078,7.50178 9.85506,12.25727 20.25132,17.24575 l 5.00138,2.39983 7.66548,0.21051 c 5.86771,0.16115 8.39252,-8.4e-4 10.76555,-0.6907 6.20318,-1.80331 13.56349,-8.9854 16.06778,-15.6787 0.66615,-1.78045 1.47339,-6.4977 2.02909,-11.85744 1.75433,-16.92035 3.02528,-21.91842 7.80999,-30.71313 2.40967,-4.42918 4.05669,-6.50398 9.65144,-12.15824 6.53778,-6.60733 6.96277,-6.92374 13.77297,-10.25423 8.53865,-4.17578 13.97019,-5.64138 22.48648,-6.06755 16.00921,-0.80112 29.31643,4.41189 41.08997,16.09672 10.36692,10.2888 15.65635,21.68311 17.10052,36.83735 0.66921,7.02217 -2.07387,19.05964 -6.22777,27.32937 -6.74057,13.41937 -20.98171,24.94045 -35.35765,28.60433 -14.22242,3.62475 -26.10025,2.06617 -42.26883,-5.54642 -12.81611,-6.03416 -18.62818,-6.95771 -27.04782,-4.29795 -7.30396,2.30732 -12.43433,6.72357 -15.92438,13.70778 -2.18148,4.36553 -3.10358,8.83379 -4.1616,20.16617 -1.05237,11.27183 -1.76206,14.35742 -4.78674,20.81191 -6.6811,14.25711 -19.4581,25.32512 -34.83137,30.17248 -4.15237,1.30929 -5.95061,1.53154 -14.05406,1.73703 -6.40822,0.1625 -10.25209,0.01 -12.38432,-0.49276 z"
|
||||
id="path7" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 79 KiB |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File diff suppressed because it is too large.
Load diff
@@ -1,70 +0,0 @@
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { NetworkEnum, TokenEnum } from "@/model/NetworkEnum";
|
||||
import { createPublicClient, http, type Address } from "viem";
|
||||
import { sepolia, rootstock } from "viem/chains";
|
||||
|
||||
const Tokens: { [key in NetworkEnum]: { [key in TokenEnum]: Address } } = {
|
||||
[NetworkEnum.sepolia]: {
|
||||
BRZ: "0x3eBE67A2C7bdB2081CBd34ba3281E90377462289",
|
||||
// BRX: "0x3eBE67A2C7bdB2081CBd34ba3281E90377462289",
|
||||
},
|
||||
[NetworkEnum.rootstock]: {
|
||||
BRZ: "0xfE841c74250e57640390f46d914C88d22C51e82e",
|
||||
// BRX: "0xfE841c74250e57640390f46d914C88d22C51e82e",
|
||||
},
|
||||
};
|
||||
|
||||
export const getTokenByAddress = (address: Address) => {
|
||||
const user = useUser();
|
||||
const networksTokens = Tokens[user.networkName.value];
|
||||
for (const [token, tokenAddress] of Object.entries(networksTokens)) {
|
||||
if (tokenAddress.toLowerCase() === address.toLowerCase()) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getTokenAddress = (
|
||||
token: TokenEnum,
|
||||
network?: NetworkEnum
|
||||
): Address => {
|
||||
const user = useUser();
|
||||
return Tokens[network ? network : user.networkName.value][
|
||||
token
|
||||
];
|
||||
};
|
||||
|
||||
export const getP2PixAddress = (network?: NetworkEnum): Address => {
|
||||
const user = useUser();
|
||||
const possibleP2PixAddresses: { [key in NetworkEnum]: Address } = {
|
||||
[NetworkEnum.sepolia]: "0xb7cD135F5eFD9760981e02E2a898790b688939fe",
|
||||
[NetworkEnum.rootstock]: "0x98ba35eb14b38D6Aa709338283af3e922476dE34",
|
||||
};
|
||||
|
||||
return possibleP2PixAddresses[
|
||||
network ? network : user.networkName.value
|
||||
];
|
||||
};
|
||||
|
||||
export const getProviderUrl = (network?: NetworkEnum): string => {
|
||||
const user = useUser();
|
||||
const possibleProvidersUrls: { [key in NetworkEnum]: string } = {
|
||||
[NetworkEnum.sepolia]: import.meta.env.VITE_SEPOLIA_API_URL,
|
||||
[NetworkEnum.rootstock]: import.meta.env.VITE_RSK_API_URL,
|
||||
};
|
||||
|
||||
return possibleProvidersUrls[network || user.networkName.value];
|
||||
};
|
||||
|
||||
export const getProviderByNetwork = (network: NetworkEnum) => {
|
||||
const chain = network === NetworkEnum.sepolia ? sepolia : rootstock;
|
||||
return createPublicClient({
|
||||
chain,
|
||||
transport: http(getProviderUrl(network)),
|
||||
});
|
||||
};
|
||||
|
||||
export const isPossibleNetwork = (networkChain: NetworkEnum): boolean => {
|
||||
return Number(networkChain) in NetworkEnum;
|
||||
};
|
||||
@@ -1,34 +1,23 @@
|
||||
import { getContract } from "./provider";
|
||||
import { getTokenAddress } from "./addresses";
|
||||
import {
|
||||
bytesToHex,
|
||||
encodeAbiParameters,
|
||||
keccak256,
|
||||
parseAbiParameters,
|
||||
parseEther,
|
||||
stringToBytes,
|
||||
stringToHex,
|
||||
toBytes,
|
||||
type Address,
|
||||
} from "viem";
|
||||
import type { TokenEnum } from "@/model/NetworkEnum";
|
||||
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,
|
||||
});
|
||||
@@ -36,58 +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: TokenEnum
|
||||
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 tokenAddress = getTokenAddress(token);
|
||||
|
||||
const { request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "withdraw",
|
||||
args: [tokenAddress, parseEther(amount), []],
|
||||
account
|
||||
functionName: 'withdraw',
|
||||
args: [token, parseEther(amount), []],
|
||||
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,
|
||||
pixtarget: string,
|
||||
signature: string
|
||||
): Promise<any> => {
|
||||
pixTimestamp: `0x${string}` & { lenght: 34 },
|
||||
signature: `0x${string}`,
|
||||
): Promise<TransactionReceipt> => {
|
||||
const { address, abi, wallet, client, account } = await getContract();
|
||||
|
||||
console.log("Releasing lock", { lockID, pixtarget, signature });
|
||||
if (!wallet) {
|
||||
throw new Error("Wallet not connected");
|
||||
throw new Error('Wallet not connected');
|
||||
}
|
||||
|
||||
// Convert pixtarget to bytes32
|
||||
const pixTimestamp = keccak256(stringToHex(pixtarget, { size: 32 }) );
|
||||
|
||||
const { request } = await client.simulateContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "release",
|
||||
args: [BigInt(lockID), pixTimestamp, stringToHex(signature)],
|
||||
account
|
||||
functionName: 'release',
|
||||
args: [BigInt(lockID), pixTimestamp, signature],
|
||||
account,
|
||||
});
|
||||
|
||||
const hash = await wallet.writeContract(request);
|
||||
|
||||
+36
-38
@@ -1,14 +1,13 @@
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { formatEther, toHex, stringToHex } from "viem";
|
||||
import type { PublicClient, Address } from "viem";
|
||||
|
||||
import { getContract } from "./provider";
|
||||
import { getP2PixAddress, getTokenAddress } from "./addresses";
|
||||
import { p2PixAbi } from "./abi"
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import { getNetworkSubgraphURL, NetworkEnum, TokenEnum } from "@/model/NetworkEnum";
|
||||
import type { UnreleasedLock } from "@/model/UnreleasedLock";
|
||||
import type { LockStatus } from "@/model/LockStatus"
|
||||
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();
|
||||
@@ -16,12 +15,10 @@ const getNetworksLiquidity = async (): Promise<void> => {
|
||||
|
||||
const depositLists: ValidDeposit[][] = [];
|
||||
|
||||
for (const network of Object.values(NetworkEnum).filter(
|
||||
(v) => !isNaN(Number(v))
|
||||
)) {
|
||||
for (const network of Object.values(Networks)) {
|
||||
const deposits = await getValidDeposits(
|
||||
getTokenAddress(user.selectedToken.value),
|
||||
Number(network)
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
network,
|
||||
);
|
||||
if (deposits) depositLists.push(deposits);
|
||||
}
|
||||
@@ -32,38 +29,38 @@ const getNetworksLiquidity = async (): Promise<void> => {
|
||||
};
|
||||
|
||||
const getParticipantID = async (
|
||||
seller: string,
|
||||
token: string
|
||||
seller: 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: NetworkEnum,
|
||||
contractInfo?: { client: PublicClient; address: Address }
|
||||
network: NetworkConfig,
|
||||
contractInfo?: { client: PublicClient; address: Address },
|
||||
): Promise<ValidDeposit[]> => {
|
||||
let client: PublicClient, abi;
|
||||
|
||||
@@ -74,9 +71,6 @@ const getValidDeposits = async (
|
||||
({ abi, client } = await getContract(true));
|
||||
}
|
||||
|
||||
// TODO: Remove this once we have a subgraph for rootstock
|
||||
if (network === NetworkEnum.rootstock) return [];
|
||||
|
||||
const body = {
|
||||
query: `
|
||||
{
|
||||
@@ -90,23 +84,27 @@ const getValidDeposits = async (
|
||||
`,
|
||||
};
|
||||
|
||||
const depositLogs = await fetch(getNetworkSubgraphURL(network), {
|
||||
method: "POST",
|
||||
const depositLogs = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// remove doubles from sellers list
|
||||
const depositData = await depositLogs.json();
|
||||
if (!depositData.data) {
|
||||
console.error('Error fetching deposit logs');
|
||||
return [];
|
||||
}
|
||||
const depositAddeds = depositData.data.depositAddeds;
|
||||
const uniqueSellers = depositAddeds.reduce(
|
||||
(acc: Record<Address, boolean>, deposit: any) => {
|
||||
acc[deposit.seller] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<Address, boolean>
|
||||
{} as Record<Address, boolean>,
|
||||
);
|
||||
|
||||
if (!contractInfo) {
|
||||
@@ -119,9 +117,9 @@ const getValidDeposits = async (
|
||||
const sellersList = Object.keys(uniqueSellers) as Address[];
|
||||
// Use multicall to batch all getBalance requests
|
||||
const balanceCalls = sellersList.map((seller) => ({
|
||||
address: getP2PixAddress(network),
|
||||
address: (network.contracts?.p2pix as ChainContract).address,
|
||||
abi,
|
||||
functionName: "getBalance",
|
||||
functionName: 'getBalance',
|
||||
args: [seller, token],
|
||||
}));
|
||||
|
||||
@@ -140,7 +138,7 @@ const getValidDeposits = async (
|
||||
remaining: Number(formatEther(mappedBalance.result as bigint)),
|
||||
seller,
|
||||
network,
|
||||
participantID: "",
|
||||
participantID: '',
|
||||
};
|
||||
depositList[seller + token] = validDeposit;
|
||||
}
|
||||
@@ -149,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],
|
||||
});
|
||||
|
||||
|
||||
+20
-18
@@ -1,6 +1,5 @@
|
||||
import { p2PixAbi } from "./abi";
|
||||
import { updateWalletStatus } from "./wallet";
|
||||
import { getProviderUrl, getP2PixAddress } from "./addresses";
|
||||
import { p2PixAbi } from './abi';
|
||||
import { updateWalletStatus } from './wallet';
|
||||
import {
|
||||
createPublicClient,
|
||||
createWalletClient,
|
||||
@@ -8,20 +7,22 @@ import {
|
||||
http,
|
||||
PublicClient,
|
||||
WalletClient,
|
||||
} from "viem";
|
||||
import { sepolia, rootstock } from "viem/chains";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
} 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 = getProviderUrl();
|
||||
return createPublicClient({
|
||||
chain:
|
||||
Number(user.networkName.value) === sepolia.id ? sepolia : rootstock,
|
||||
transport: http(rpcUrl),
|
||||
});
|
||||
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),
|
||||
});
|
||||
};
|
||||
|
||||
const getWalletClient = (): WalletClient | null => {
|
||||
@@ -30,12 +31,14 @@ const getWalletClient = (): WalletClient | null => {
|
||||
|
||||
const getContract = async (onlyRpcProvider = false) => {
|
||||
const client = getPublicClient();
|
||||
const address = getP2PixAddress();
|
||||
const user = useUser();
|
||||
const address = (user.network.value.contracts?.p2pix as ChainContract)
|
||||
.address;
|
||||
const abi = p2PixAbi;
|
||||
const wallet = onlyRpcProvider ? null : getWalletClient();
|
||||
|
||||
if (!client) {
|
||||
throw new Error("Public client not initialized");
|
||||
throw new Error('Public client not initialized');
|
||||
}
|
||||
|
||||
const [account] = wallet ? await wallet.getAddresses() : [null];
|
||||
@@ -45,10 +48,9 @@ const getContract = async (onlyRpcProvider = false) => {
|
||||
|
||||
const connectProvider = async (p: any): Promise<void> => {
|
||||
const user = useUser();
|
||||
const chain =
|
||||
Number(user.networkName.value) === sepolia.id ? sepolia : rootstock;
|
||||
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,12 +1,15 @@
|
||||
import { getContract, getPublicClient, getWalletClient } from "./provider";
|
||||
import { getTokenAddress, getP2PixAddress } from "./addresses";
|
||||
import { parseEther, toHex } from "viem";
|
||||
import { sepolia, rootstock } from "viem/chains";
|
||||
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 { mockTokenAbi } from "./abi";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { createParticipant } from "@/utils/bbPay";
|
||||
import type { Participant } from "@/utils/bbPay";
|
||||
const getP2PixAddress = (): Address => {
|
||||
const user = useUser();
|
||||
return (user.network.value.contracts?.p2pix as ChainContract).address;
|
||||
};
|
||||
|
||||
const approveTokens = async (participant: Participant): Promise<any> => {
|
||||
const user = useUser();
|
||||
@@ -14,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 = getTokenAddress(user.selectedToken.value);
|
||||
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.networkId.value === sepolia.id ? sepolia : rootstock;
|
||||
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,
|
||||
@@ -55,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();
|
||||
@@ -63,17 +67,17 @@ 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.networkId.value === sepolia.id ? sepolia : rootstock;
|
||||
const chain = user.network.value;
|
||||
const hash = await walletClient.writeContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "deposit",
|
||||
functionName: 'deposit',
|
||||
args: [
|
||||
user.networkId.value + "-" + sellerId.id,
|
||||
toHex("", { size: 32 }),
|
||||
getTokenAddress(user.selectedToken.value),
|
||||
user.network.value.id + '-' + sellerId.id,
|
||||
toHex('', { size: 32 }),
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
parseEther(user.seller.value.offer.toString()),
|
||||
true,
|
||||
],
|
||||
|
||||
+55
-135
@@ -1,16 +1,14 @@
|
||||
import { formatEther, hexToString, 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 { getTokenAddress } from "./addresses";
|
||||
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 { getNetworkSubgraphURL } from "@/model/NetworkEnum";
|
||||
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();
|
||||
@@ -19,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;
|
||||
}
|
||||
|
||||
@@ -32,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(
|
||||
getTokenAddress(user.selectedToken.value),
|
||||
user.networkName.value
|
||||
user.network.value.tokens[user.selectedToken.value].address,
|
||||
user.network.value,
|
||||
);
|
||||
if (walletDeposits) {
|
||||
return walletDeposits
|
||||
@@ -52,22 +50,22 @@ 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();
|
||||
|
||||
// Get the current network for the subgraph URL
|
||||
const network = user.networkName.value;
|
||||
const network = user.network.value;
|
||||
|
||||
// Query subgraph for all relevant transactions
|
||||
const subgraphQuery = {
|
||||
@@ -110,10 +108,10 @@ export const listAllTransactionByWalletAddress = async (
|
||||
`,
|
||||
};
|
||||
|
||||
const response = await fetch(getNetworkSubgraphURL(network), {
|
||||
method: "POST",
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
@@ -128,10 +126,11 @@ export const listAllTransactionByWalletAddress = async (
|
||||
transactions.push({
|
||||
token: deposit.token,
|
||||
blockNumber: parseInt(deposit.blockNumber),
|
||||
blockTimestamp: parseInt(deposit.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(deposit.amount))),
|
||||
seller: deposit.seller,
|
||||
buyer: "",
|
||||
event: "DepositAdded",
|
||||
buyer: '',
|
||||
event: 'DepositAdded',
|
||||
lockStatus: undefined,
|
||||
transactionHash: deposit.transactionHash,
|
||||
});
|
||||
@@ -147,10 +146,11 @@ export const listAllTransactionByWalletAddress = async (
|
||||
transactions.push({
|
||||
token: lock.token,
|
||||
blockNumber: parseInt(lock.blockNumber),
|
||||
blockTimestamp: parseInt(lock.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(lock.amount))),
|
||||
seller: lock.seller,
|
||||
buyer: lock.buyer,
|
||||
event: "LockAdded",
|
||||
event: 'LockAdded',
|
||||
lockStatus: lockStatus,
|
||||
transactionHash: lock.transactionHash,
|
||||
transactionID: lock.lockID.toString(),
|
||||
@@ -164,10 +164,11 @@ export const listAllTransactionByWalletAddress = async (
|
||||
transactions.push({
|
||||
token: undefined, // Subgraph doesn't provide token in this event, we could enhance this later
|
||||
blockNumber: parseInt(release.blockNumber),
|
||||
blockTimestamp: parseInt(release.blockTimestamp),
|
||||
amount: -1, // Amount not available in this event
|
||||
seller: "",
|
||||
seller: '',
|
||||
buyer: release.buyer,
|
||||
event: "LockReleased",
|
||||
event: 'LockReleased',
|
||||
lockStatus: undefined,
|
||||
transactionHash: release.transactionHash,
|
||||
transactionID: release.lockId.toString(),
|
||||
@@ -181,10 +182,11 @@ export const listAllTransactionByWalletAddress = async (
|
||||
transactions.push({
|
||||
token: withdrawal.token,
|
||||
blockNumber: parseInt(withdrawal.blockNumber),
|
||||
blockTimestamp: parseInt(withdrawal.blockTimestamp),
|
||||
amount: parseFloat(formatEther(BigInt(withdrawal.amount))),
|
||||
seller: withdrawal.seller,
|
||||
buyer: "",
|
||||
event: "DepositWithdrawn",
|
||||
buyer: '',
|
||||
event: 'DepositWithdrawn',
|
||||
lockStatus: undefined,
|
||||
transactionHash: withdrawal.transactionHash,
|
||||
});
|
||||
@@ -195,75 +197,9 @@ 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.networkName.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(getNetworkSubgraphURL(network), {
|
||||
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.networkName.value;
|
||||
const network = user.network.value;
|
||||
|
||||
// Query subgraph for lock added transactions
|
||||
const subgraphQuery = {
|
||||
@@ -284,10 +220,10 @@ const listLockTransactionByWalletAddress = async (walletAddress: Address) => {
|
||||
|
||||
try {
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(getNetworkSubgraphURL(network), {
|
||||
method: "POST",
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
@@ -307,12 +243,12 @@ 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),
|
||||
seller: lock.seller,
|
||||
token: lock.token,
|
||||
token: undefined, // Token not available in LockAdded subgraph event
|
||||
amount: BigInt(lock.amount),
|
||||
},
|
||||
// Add other necessary fields to match the original format
|
||||
@@ -320,19 +256,19 @@ 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);
|
||||
}
|
||||
};
|
||||
|
||||
const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
const user = useUser();
|
||||
const network = user.networkName.value;
|
||||
const network = user.network.value;
|
||||
|
||||
// Query subgraph for lock added transactions where seller matches
|
||||
const subgraphQuery = {
|
||||
@@ -342,7 +278,6 @@ const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockTimestamp
|
||||
blockNumber
|
||||
@@ -354,10 +289,10 @@ const listLockTransactionBySellerAddress = async (sellerAddress: Address) => {
|
||||
|
||||
try {
|
||||
// Fetch data from subgraph
|
||||
const response = await fetch(getNetworkSubgraphURL(network), {
|
||||
method: "POST",
|
||||
const response = await fetch(network.subgraphUrls[0], {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(subgraphQuery),
|
||||
});
|
||||
@@ -377,12 +312,12 @@ 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),
|
||||
seller: lock.seller,
|
||||
token: lock.token,
|
||||
token: undefined, // Token not available in LockAdded subgraph event
|
||||
amount: BigInt(lock.amount),
|
||||
},
|
||||
// Add other necessary fields to match the original format
|
||||
@@ -390,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);
|
||||
@@ -411,15 +346,15 @@ 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)
|
||||
@@ -427,7 +362,7 @@ export const checkUnreleasedLock = async (
|
||||
};
|
||||
|
||||
export const getActiveLockAmount = async (
|
||||
walletAddress: Address
|
||||
walletAddress: Address,
|
||||
): Promise<number> => {
|
||||
const { address, abi, client } = await getContract(true);
|
||||
const lockSeller = await listLockTransactionBySellerAddress(walletAddress);
|
||||
@@ -436,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],
|
||||
});
|
||||
|
||||
@@ -447,9 +382,9 @@ export const getActiveLockAmount = async (
|
||||
client.readContract({
|
||||
address: address,
|
||||
abi,
|
||||
functionName: "mapLocks",
|
||||
functionName: 'mapLocks',
|
||||
args: [BigInt(id)],
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const mapLocksResults = await client.multicall({
|
||||
@@ -463,18 +398,3 @@ export const getActiveLockAmount = async (
|
||||
return total;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
export const getSellerParticipantId = async (
|
||||
sellerAddress: Address,
|
||||
tokenAddress: Address
|
||||
): Promise<string> => {
|
||||
const { address, abi, client } = await getContract();
|
||||
|
||||
const participantId = await client.readContract({
|
||||
address,
|
||||
abi,
|
||||
functionName: "getPixTarget",
|
||||
args: [sellerAddress, tokenAddress],
|
||||
});
|
||||
return hexToString(participantId);
|
||||
};
|
||||
+21
-19
@@ -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/CustomButton/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 "../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.selectedToken.value);
|
||||
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>
|
||||
+157
-57
@@ -1,17 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import SpinnerComponent from "@/components/SpinnerComponent.vue";
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { verifyNetworkLiquidity } from "@/utils/networkLiquidity";
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import { getTokenImage } from "@/utils/imagesPath";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
|
||||
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();
|
||||
@@ -19,7 +21,7 @@ const selectTokenToggle = ref<boolean>(false);
|
||||
|
||||
const {
|
||||
walletAddress,
|
||||
networkName,
|
||||
network,
|
||||
selectedToken,
|
||||
depositsValidList,
|
||||
loadingNetworkLiquidity,
|
||||
@@ -33,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> => {
|
||||
@@ -51,11 +144,11 @@ const connectAccount = async (): Promise<void> => {
|
||||
|
||||
const emitConfirmButton = async (): Promise<void> => {
|
||||
const deposit = selectedDeposits.value?.find(
|
||||
(d) => d.network === Number(networkName.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
|
||||
@@ -71,6 +164,7 @@ const handleInputEvent = (event: any): void => {
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
checkReputationLimit(tokenValue.value);
|
||||
verifyLiquidity();
|
||||
};
|
||||
|
||||
@@ -90,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 === Number(networkName.value)
|
||||
);
|
||||
hasLiquidity.value = !!selDeposits.find((d) => d.network === network.value);
|
||||
enableOrDisableConfirmButton();
|
||||
};
|
||||
|
||||
@@ -110,7 +201,7 @@ const enableOrDisableConfirmButton = (): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedDeposits.value.find((d) => d.network === networkName.value)) {
|
||||
if (!selectedDeposits.value.find((d) => d.network === network.value)) {
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +209,7 @@ const enableOrDisableConfirmButton = (): void => {
|
||||
enableConfirmButton.value = true;
|
||||
};
|
||||
|
||||
watch(networkName, (): void => {
|
||||
watch(network, (): void => {
|
||||
verifyLiquidity();
|
||||
enableOrDisableConfirmButton();
|
||||
});
|
||||
@@ -127,6 +218,13 @@ watch(walletAddress, (): void => {
|
||||
verifyLiquidity();
|
||||
});
|
||||
|
||||
const availableNetworks = computed(() => {
|
||||
if (!selectedDeposits.value) return [];
|
||||
return Object.values(Networks).filter((network) =>
|
||||
selectedDeposits.value?.some((d) => d.network.id === network.id),
|
||||
);
|
||||
});
|
||||
|
||||
// Add form submission handler
|
||||
const handleSubmit = async (e: Event): Promise<void> => {
|
||||
e.preventDefault();
|
||||
@@ -153,14 +251,12 @@ 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"
|
||||
name="tokenAmount"
|
||||
class="border-none outline-none text-lg text-gray-900"
|
||||
class="border-none outline-none text-lg text-gray-900 sm:flex-1 max-w-[60%]"
|
||||
v-bind:class="{
|
||||
'font-semibold': tokenValue != undefined,
|
||||
'text-xl': tokenValue != undefined,
|
||||
@@ -170,7 +266,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
step=".01"
|
||||
required
|
||||
/>
|
||||
<div class="relative overflow-visible">
|
||||
<div class="relative overflow-visible ml-auto sm:ml-0">
|
||||
<button
|
||||
ref="tokenDropdownRef"
|
||||
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
|
||||
@@ -233,24 +329,12 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<img
|
||||
alt="Rootstock image"
|
||||
src="@/assets/rootstock.svg?url"
|
||||
v-for="network in availableNetworks"
|
||||
:key="network.id"
|
||||
:alt="`${network.name} image`"
|
||||
:src="getNetworkImage(network.name)"
|
||||
width="24"
|
||||
height="24"
|
||||
v-if="
|
||||
selectedDeposits &&
|
||||
selectedDeposits.find((d) => d.network == NetworkEnum.rootstock)
|
||||
"
|
||||
/>
|
||||
<img
|
||||
alt="Ethereum image"
|
||||
src="@/assets/ethereum.svg?url"
|
||||
width="24"
|
||||
height="24"
|
||||
v-if="
|
||||
selectedDeposits &&
|
||||
selectedDeposits.find((d) => d.network == NetworkEnum.sepolia)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,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"
|
||||
@@ -282,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"
|
||||
@@ -303,6 +401,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
v-if="walletAddress"
|
||||
type="submit"
|
||||
text="Confirmar Oferta"
|
||||
:isDisabled="!enableConfirmButton"
|
||||
/>
|
||||
<CustomButton
|
||||
v-else
|
||||
@@ -314,6 +413,7 @@ const handleSubmit = async (e: Event): Promise<void> => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
@@ -336,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;
|
||||
}
|
||||
|
||||
+85
-37
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import CustomButton from "@/components/CustomButton/CustomButton.vue";
|
||||
import CustomModal from "@/components//CustomModal/CustomModal.vue";
|
||||
import SpinnerComponent from "@/components/SpinnerComponent.vue";
|
||||
import { createSolicitation, getSolicitation, type Offer } from "@/utils/bbPay";
|
||||
import { getSellerParticipantId } from "@/blockchain/wallet";
|
||||
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,34 +15,36 @@ 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 pixTarget = 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);
|
||||
const copyFeedbackTimeout = ref<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Function to generate QR code SVG
|
||||
const generateQrCodeSvg = async (text: string) => {
|
||||
try {
|
||||
const svgString = await QRCode.toString(text, {
|
||||
type: "svg",
|
||||
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 () => {
|
||||
@@ -52,11 +54,11 @@ const checkSolicitationStatus = async () => {
|
||||
|
||||
try {
|
||||
const response = await getSolicitation(
|
||||
solicitationData.value.numeroSolicitacao
|
||||
solicitationData.value.numeroSolicitacao,
|
||||
);
|
||||
|
||||
if (response.signature) {
|
||||
pixTarget.value = response.pixTarget;
|
||||
pixTimestamp.value = response.pixTimestamp;
|
||||
releaseSignature.value = response.signature;
|
||||
// Stop polling when payment is confirmed
|
||||
if (pollingInterval.value) {
|
||||
@@ -65,7 +67,7 @@ const checkSolicitationStatus = async () => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking solicitation status:", error);
|
||||
console.error('Error checking solicitation status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,16 +82,35 @@ const startPolling = () => {
|
||||
pollingInterval.value = setInterval(checkSolicitationStatus, 10000);
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
if (!qrCode.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(qrCode.value);
|
||||
|
||||
if (copyFeedbackTimeout.value) {
|
||||
clearTimeout(copyFeedbackTimeout.value);
|
||||
}
|
||||
|
||||
copyFeedback.value = true;
|
||||
|
||||
copyFeedbackTimeout.value = setTimeout(() => {
|
||||
copyFeedback.value = false;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error copying to clipboard:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { tokenAddress, sellerAddress, amount } = await getUnreleasedLockById(
|
||||
BigInt(props.lockID)
|
||||
BigInt(props.lockID),
|
||||
);
|
||||
|
||||
const participantId = await getSellerParticipantId(
|
||||
sellerAddress,
|
||||
tokenAddress
|
||||
);
|
||||
const participantId = await getParticipantID(sellerAddress, tokenAddress);
|
||||
|
||||
const offer: Offer = {
|
||||
amount,
|
||||
@@ -109,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,6 +140,10 @@ onUnmounted(() => {
|
||||
clearInterval(pollingInterval.value);
|
||||
pollingInterval.value = null;
|
||||
}
|
||||
if (copyFeedbackTimeout.value) {
|
||||
clearTimeout(copyFeedbackTimeout.value);
|
||||
copyFeedbackTimeout.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -156,13 +181,24 @@ onUnmounted(() => {
|
||||
{{ qrCode }}
|
||||
</span>
|
||||
</div>
|
||||
<img
|
||||
alt="Copy PIX code"
|
||||
src="@/assets/copyPix.svg?url"
|
||||
width="16"
|
||||
height="16"
|
||||
class="pt-2 lg:mb-5 cursor-pointer"
|
||||
/>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<img
|
||||
alt="Copy PIX code"
|
||||
src="@/assets/copyPix.svg?url"
|
||||
width="16"
|
||||
height="16"
|
||||
class="pt-2 cursor-pointer hover:opacity-70 transition-opacity"
|
||||
@click="copyToClipboard"
|
||||
/>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback"
|
||||
class="text-xs text-emerald-500 font-semibold"
|
||||
>
|
||||
Código copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
:is-disabled="releaseSignature === ''"
|
||||
@@ -170,7 +206,7 @@ onUnmounted(() => {
|
||||
releaseSignature ? 'Enviar para a rede' : 'Validando pagamento...'
|
||||
"
|
||||
@button-clicked="
|
||||
emit('pixValidated', { pixTarget, signature: releaseSignature })
|
||||
emit('pixValidated', { pixTimestamp, signature: releaseSignature })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@@ -183,6 +219,7 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
@@ -231,13 +268,24 @@ 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;
|
||||
}
|
||||
|
||||
/* Fade transition for copy feedback */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
text: String,
|
||||
isDisabled: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["buttonClicked"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
@click="emit('buttonClicked')"
|
||||
v-bind:class="{ 'opacity-70': props.isDisabled }"
|
||||
:disabled="props.isDisabled ? props.isDisabled : false"
|
||||
>
|
||||
{{ props.text }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.button {
|
||||
@apply rounded-lg w-full text-base font-semibold text-gray-900 p-4 bg-amber-400;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
changeType?: 'positive' | 'negative' | 'neutral';
|
||||
icon?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
changeType: 'neutral',
|
||||
loading: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="analytics-card">
|
||||
<div class="analytics-content">
|
||||
<div v-if="loading" class="analytics-value">
|
||||
<div class="animate-pulse bg-gray-300 h-8 w-16 rounded"></div>
|
||||
</div>
|
||||
<div v-else class="analytics-value">{{ value }}</div>
|
||||
<div class="analytics-title">{{ title }}</div>
|
||||
<div
|
||||
v-if="change && !loading"
|
||||
class="analytics-change"
|
||||
:class="`change-${changeType}`"
|
||||
>
|
||||
{{ change }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="icon && !loading" class="analytics-icon">
|
||||
<img :src="icon" :alt="`${title} icon`" class="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.analytics-card {
|
||||
@apply bg-white rounded-lg border border-gray-200 p-6 flex items-center justify-between shadow-lg;
|
||||
}
|
||||
|
||||
.analytics-content {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.analytics-value {
|
||||
@apply text-2xl font-bold text-amber-400 mb-1 break-words overflow-hidden;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.analytics-title {
|
||||
@apply text-sm text-gray-900 mb-1;
|
||||
}
|
||||
|
||||
.analytics-change {
|
||||
@apply text-xs font-medium;
|
||||
}
|
||||
|
||||
.change-positive {
|
||||
@apply text-green-600;
|
||||
}
|
||||
|
||||
.change-negative {
|
||||
@apply text-red-600;
|
||||
}
|
||||
|
||||
.change-neutral {
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
.analytics-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
type: 'deposit' | 'lock' | 'release' | 'return';
|
||||
timestamp: string;
|
||||
seller?: string;
|
||||
buyer?: string | null;
|
||||
amount: string;
|
||||
token: string;
|
||||
blockNumber: string;
|
||||
transactionHash: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
transactions: Transaction[];
|
||||
networkExplorerUrl: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const copyFeedback = ref<{ [key: string]: boolean }>({});
|
||||
const copyFeedbackTimeout = ref<{ [key: string]: NodeJS.Timeout | null }>({});
|
||||
|
||||
const getTransactionTypeInfo = (type: string) => {
|
||||
const typeMap = {
|
||||
deposit: { label: 'Depósito', status: 'completed' as const },
|
||||
lock: { label: 'Bloqueio', status: 'open' as const },
|
||||
release: { label: 'Liberação', status: 'completed' as const },
|
||||
return: { label: 'Retorno', status: 'expired' as const },
|
||||
};
|
||||
return (
|
||||
typeMap[type as keyof typeof typeMap] || {
|
||||
label: type,
|
||||
status: 'pending' as const,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getTransactionTypeColor = (type: string) => {
|
||||
const colorMap = {
|
||||
deposit: 'text-emerald-600',
|
||||
lock: 'text-amber-600',
|
||||
release: 'text-emerald-600',
|
||||
return: 'text-gray-600',
|
||||
};
|
||||
return colorMap[type as keyof typeof colorMap] || 'text-gray-600';
|
||||
};
|
||||
|
||||
const formatAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
const formatAmount = (amount: string, decimals: number = 18): string => {
|
||||
const num = parseFloat(amount) / Math.pow(10, decimals);
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const getExplorerUrl = (txHash: string) => {
|
||||
return `${props.networkExplorerUrl}/tx/${txHash}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (address: string, key: string) => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(address);
|
||||
|
||||
if (copyFeedbackTimeout.value[key]) {
|
||||
clearTimeout(copyFeedbackTimeout.value[key]!);
|
||||
}
|
||||
|
||||
copyFeedback.value[key] = true;
|
||||
|
||||
copyFeedbackTimeout.value[key] = setTimeout(() => {
|
||||
copyFeedback.value[key] = false;
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Error copying to clipboard:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="hidden lg:block overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">
|
||||
Horário
|
||||
</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Tipo</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">
|
||||
Participantes
|
||||
</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Valor</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Bloco</th>
|
||||
<th class="text-left py-3 px-4 text-gray-700 font-medium">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
class="border-b border-gray-100 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<td class="py-4 px-4">
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ transaction.timestamp }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<span
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ getTransactionTypeInfo(transaction.type).label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<div class="space-y-1">
|
||||
<div v-if="transaction.seller" class="text-sm">
|
||||
<span class="text-gray-600">Vendedor: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.seller,
|
||||
`seller-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.seller) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`seller-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="transaction.buyer" class="text-sm">
|
||||
<span class="text-gray-600">Comprador: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.buyer,
|
||||
`buyer-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.buyer) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`buyer-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<div class="text-sm font-semibold text-emerald-600">
|
||||
{{ formatAmount(transaction.amount, 18) }} BRZ
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<div class="text-sm text-gray-600 font-mono">
|
||||
#{{ transaction.blockNumber }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-4 px-4">
|
||||
<a
|
||||
:href="getExplorerUrl(transaction.transactionHash)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors"
|
||||
>
|
||||
Explorador
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div class="lg:hidden space-y-4">
|
||||
<div
|
||||
v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
class="bg-gray-50 rounded-lg p-4 border border-gray-200"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span
|
||||
:class="getTransactionTypeColor(transaction.type)"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ getTransactionTypeInfo(transaction.type).label }}
|
||||
</span>
|
||||
<div class="text-sm text-gray-600">{{ transaction.timestamp }}</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
<div v-if="transaction.seller" class="text-sm">
|
||||
<span class="text-gray-600">Vendedor: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
transaction.seller,
|
||||
`seller-${transaction.id}`,
|
||||
)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.seller) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`seller-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="transaction.buyer" class="text-sm">
|
||||
<span class="text-gray-600">Comprador: </span>
|
||||
<div class="relative inline-block">
|
||||
<span
|
||||
class="text-gray-900 font-mono cursor-pointer hover:text-amber-500 transition-colors"
|
||||
@click="
|
||||
copyToClipboard(transaction.buyer, `buyer-${transaction.id}`)
|
||||
"
|
||||
title="Copiar"
|
||||
>
|
||||
{{ formatAddress(transaction.buyer) }}
|
||||
</span>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="copyFeedback[`buyer-${transaction.id}`]"
|
||||
class="absolute -top-6 left-1/2 transform -translate-x-1/2 text-xs text-emerald-500 font-semibold bg-white px-2 py-1 rounded shadow-sm whitespace-nowrap z-10"
|
||||
>
|
||||
Copiado!
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-600">Valor: </span>
|
||||
<span class="font-semibold text-emerald-600"
|
||||
>{{ formatAmount(transaction.amount, 18) }} BRZ</span
|
||||
>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-600">Bloco: </span>
|
||||
<span class="text-gray-900 font-mono"
|
||||
>#{{ transaction.blockNumber }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="getExplorerUrl(transaction.transactionHash)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center px-3 py-1 bg-amber-400 text-gray-900 rounded-lg text-sm font-medium hover:bg-amber-500 transition-colors"
|
||||
>
|
||||
Ver no Explorador
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="transactions.length === 0" class="text-center py-12">
|
||||
<div class="text-gray-500 text-lg mb-2">📭</div>
|
||||
<p class="text-gray-600">Nenhuma transação encontrada</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { decimalCount } from '@/utils/decimalCount';
|
||||
import { useFloating, arrow, offset, flip, shift } from '@floating-ui/vue';
|
||||
import IconButton from '../ui/IconButton.vue';
|
||||
import withdrawIcon from '@/assets/withdraw.svg?url';
|
||||
|
||||
const props = defineProps<{
|
||||
validDeposits: ValidDeposit[];
|
||||
activeLockAmount: number;
|
||||
selectedToken: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
withdraw: [amount: string];
|
||||
}>();
|
||||
|
||||
const withdrawAmount = ref<string>('');
|
||||
const isCollapsibleOpen = ref<boolean>(false);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const validWithdrawAmount = ref<boolean>(true);
|
||||
const enableConfirmButton = ref<boolean>(false);
|
||||
const showInfoTooltip = ref<boolean>(false);
|
||||
const floatingArrow = ref(null);
|
||||
|
||||
const reference = ref<HTMLElement | null>(null);
|
||||
const floating = ref<HTMLElement | null>(null);
|
||||
const infoText = ref<HTMLElement | null>(null);
|
||||
|
||||
const remaining = computed(() => {
|
||||
if (props.validDeposits.length > 0) {
|
||||
const deposit = props.validDeposits[0];
|
||||
return deposit ? deposit.remaining : 0;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
if (decimalCount(String(value)) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
if (value > remaining.value) {
|
||||
validWithdrawAmount.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validWithdrawAmount.value = true;
|
||||
enableConfirmButton.value = true;
|
||||
};
|
||||
|
||||
const callWithdraw = () => {
|
||||
if (enableConfirmButton.value && withdrawAmount.value) {
|
||||
emit('withdraw', withdrawAmount.value);
|
||||
// Reset form after withdraw
|
||||
withdrawAmount.value = '';
|
||||
isCollapsibleOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openWithdrawForm = () => {
|
||||
isCollapsibleOpen.value = true;
|
||||
};
|
||||
|
||||
const cancelWithdraw = () => {
|
||||
isCollapsibleOpen.value = false;
|
||||
withdrawAmount.value = '';
|
||||
validDecimals.value = true;
|
||||
validWithdrawAmount.value = true;
|
||||
enableConfirmButton.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useFloating(reference, floating, {
|
||||
placement: 'right',
|
||||
middleware: [
|
||||
offset(10),
|
||||
flip(),
|
||||
shift(),
|
||||
arrow({ element: floatingArrow }),
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full bg-white p-4 sm:p-6 rounded-lg">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm leading-5 font-medium text-gray-600">
|
||||
Saldo disponível
|
||||
</p>
|
||||
<p class="text-xl leading-7 font-semibold text-gray-900">
|
||||
{{ remaining }} {{ selectedToken }}
|
||||
</p>
|
||||
<div class="flex gap-2 w-32 sm:w-56" v-if="activeLockAmount != 0">
|
||||
<span class="text-xs font-normal text-gray-400" ref="infoText">
|
||||
{{ `com ${activeLockAmount.toFixed(2)} ${selectedToken} em lock` }}
|
||||
</span>
|
||||
<div
|
||||
class="absolute mt-[2px] md-view"
|
||||
:style="{ left: `${(infoText?.clientWidth ?? 108) + 4}px` }"
|
||||
>
|
||||
<img
|
||||
alt="info image"
|
||||
src="@/assets/info.svg?url"
|
||||
aria-describedby="tooltip"
|
||||
ref="reference"
|
||||
@mouseover="showInfoTooltip = true"
|
||||
@mouseout="showInfoTooltip = false"
|
||||
/>
|
||||
<div
|
||||
role="tooltip"
|
||||
ref="floating"
|
||||
class="w-56 z-50 tooltip md-view"
|
||||
v-if="showInfoTooltip"
|
||||
>
|
||||
Valor "em lock" significa que a quantia está aguardando
|
||||
confirmação de compra e só estará disponível para saque caso a
|
||||
transação expire.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!isCollapsibleOpen" class="flex justify-end items-center">
|
||||
<IconButton
|
||||
text="Sacar"
|
||||
:icon="withdrawIcon"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:full-width="false"
|
||||
@click="openWithdrawForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-5">
|
||||
<div v-show="isCollapsibleOpen" class="py-2 w-100">
|
||||
<p class="text-sm leading-5 font-medium">Valor do saque</p>
|
||||
<input
|
||||
type="number"
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="0"
|
||||
class="text-2xl text-gray-900 w-full outline-none"
|
||||
v-model="withdrawAmount"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center" v-if="!validDecimals">
|
||||
<span class="text-red-500 font-normal text-sm">
|
||||
Por favor utilize no máximo 2 casas decimais
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-center" v-else-if="!validWithdrawAmount">
|
||||
<span class="text-red-500 font-normal text-sm">
|
||||
Saldo insuficiente
|
||||
</span>
|
||||
</div>
|
||||
<hr v-show="isCollapsibleOpen" class="pb-3" />
|
||||
<div v-show="isCollapsibleOpen" class="flex justify-between items-center">
|
||||
<h1
|
||||
@click="cancelWithdraw"
|
||||
class="text-black font-medium cursor-pointer hover:text-gray-600 transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</h1>
|
||||
|
||||
<IconButton
|
||||
text="Sacar"
|
||||
:icon="withdrawIcon"
|
||||
variant="outline"
|
||||
size="md"
|
||||
:full-width="false"
|
||||
:disabled="!enableConfirmButton"
|
||||
@click="callWithdraw"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
p {
|
||||
@apply text-gray-900;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.md-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { WalletTransaction } from "@/model/WalletTransaction";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import SpinnerComponent from "../SpinnerComponent.vue";
|
||||
import { decimalCount } from "@/utils/decimalCount";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { useFloating, arrow, offset, flip, shift } from "@floating-ui/vue";
|
||||
import type { ValidDeposit } from '@/model/ValidDeposit';
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { ref, watch } from 'vue';
|
||||
import SpinnerComponent from '../ui/SpinnerComponent.vue';
|
||||
import BalanceCard from './BalanceCard.vue';
|
||||
import TransactionCard from './TransactionCard.vue';
|
||||
|
||||
const user = useUser();
|
||||
|
||||
@@ -18,84 +16,14 @@ const props = defineProps<{
|
||||
activeLockAmount: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["depositWithdrawn"]);
|
||||
const emit = defineEmits(['depositWithdrawn']);
|
||||
|
||||
const { loadingWalletTransactions } = user;
|
||||
|
||||
const remaining = ref<number>(0);
|
||||
const itemsToShow = ref<WalletTransaction[]>([]);
|
||||
const withdrawAmount = ref<string>("");
|
||||
const withdrawButtonOpacity = ref<number>(0.6);
|
||||
const withdrawButtonCursor = ref<string>("not-allowed");
|
||||
const isCollapsibleOpen = ref<boolean>(false);
|
||||
const validDecimals = ref<boolean>(true);
|
||||
const validWithdrawAmount = ref<boolean>(true);
|
||||
const enableConfirmButton = ref<boolean>(false);
|
||||
const showInfoTooltip = ref<boolean>(false);
|
||||
const floatingArrow = ref(null);
|
||||
|
||||
const reference = ref<HTMLElement | null>(null);
|
||||
const floating = ref<HTMLElement | null>(null);
|
||||
const infoText = ref<HTMLElement | null>(null);
|
||||
|
||||
// Debounce methods
|
||||
const handleInputEvent = (event: any): void => {
|
||||
const { value } = event.target;
|
||||
|
||||
if (decimalCount(String(value)) > 2) {
|
||||
validDecimals.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validDecimals.value = true;
|
||||
|
||||
if (value > remaining.value) {
|
||||
validWithdrawAmount.value = false;
|
||||
enableConfirmButton.value = false;
|
||||
return;
|
||||
}
|
||||
validWithdrawAmount.value = true;
|
||||
enableConfirmButton.value = true;
|
||||
};
|
||||
|
||||
const callWithdraw = () => {
|
||||
emit("depositWithdrawn", withdrawAmount.value);
|
||||
};
|
||||
|
||||
watch(enableConfirmButton, (): void => {
|
||||
if (!enableConfirmButton.value) {
|
||||
withdrawButtonOpacity.value = 0.7;
|
||||
withdrawButtonCursor.value = "not-allowed";
|
||||
} else {
|
||||
withdrawButtonOpacity.value = 1;
|
||||
withdrawButtonCursor.value = "pointer";
|
||||
}
|
||||
});
|
||||
|
||||
watch(withdrawAmount, (): void => {
|
||||
if (!withdrawAmount.value || !enableConfirmButton.value) {
|
||||
withdrawButtonOpacity.value = 0.7;
|
||||
withdrawButtonCursor.value = "not-allowed";
|
||||
} else {
|
||||
withdrawButtonOpacity.value = 1;
|
||||
withdrawButtonCursor.value = "pointer";
|
||||
}
|
||||
});
|
||||
|
||||
const getRemaining = (): number => {
|
||||
if (props.validDeposits.length > 0) {
|
||||
// in this release only the BRL token is being used.
|
||||
const deposit = props.validDeposits[0];
|
||||
remaining.value = deposit ? deposit.remaining : 0;
|
||||
return deposit ? deposit.remaining : 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getExplorer = (): string => {
|
||||
return user.networkName.value == NetworkEnum.sepolia
|
||||
? "Etherscan"
|
||||
: "Polygonscan";
|
||||
const callWithdraw = (amount: string) => {
|
||||
emit('depositWithdrawn', amount);
|
||||
};
|
||||
|
||||
const showInitialItems = (): void => {
|
||||
@@ -103,46 +31,18 @@ const showInitialItems = (): void => {
|
||||
};
|
||||
|
||||
const openEtherscanUrl = (transactionHash: string): void => {
|
||||
const networkUrl =
|
||||
user.networkName.value == NetworkEnum.sepolia
|
||||
? "sepolia.etherscan.io"
|
||||
: "mumbai.polygonscan.com";
|
||||
const networkUrl = user.network.value.blockExplorers?.default.url;
|
||||
const url = `https://${networkUrl}/tx/${transactionHash}`;
|
||||
window.open(url, "_blank");
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const loadMore = (): void => {
|
||||
const itemsShowing = itemsToShow.value.length;
|
||||
itemsToShow.value?.push(
|
||||
...props.walletTransactions.slice(itemsShowing, itemsShowing + 3)
|
||||
...props.walletTransactions.slice(itemsShowing, itemsShowing + 3),
|
||||
);
|
||||
};
|
||||
|
||||
const getEventName = (event: string | undefined): string => {
|
||||
if (!event) return "Desconhecido";
|
||||
|
||||
const possibleEventName: { [key: string]: string } = {
|
||||
DepositAdded: "Oferta",
|
||||
LockAdded: "Reserva",
|
||||
LockReleased: "Compra",
|
||||
DepositWithdrawn: "Retirada",
|
||||
};
|
||||
|
||||
return possibleEventName[event];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useFloating(reference, floating, {
|
||||
placement: "right",
|
||||
middleware: [
|
||||
offset(10),
|
||||
flip(),
|
||||
shift(),
|
||||
arrow({ element: floatingArrow }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// watch props changes
|
||||
watch(props, async (): Promise<void> => {
|
||||
const itemsToShowQty = itemsToShow.value.length;
|
||||
@@ -167,179 +67,23 @@ showInitialItems();
|
||||
<SpinnerComponent width="8" height="8"></SpinnerComponent>
|
||||
</div>
|
||||
<div class="main-container max-w-md" v-else>
|
||||
<div
|
||||
class="w-full bg-white p-4 sm:p-6 rounded-lg"
|
||||
<BalanceCard
|
||||
v-if="props.validDeposits.length > 0"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-sm leading-5 font-medium text-gray-600">
|
||||
Saldo disponível
|
||||
</p>
|
||||
<p class="text-xl leading-7 font-semibold text-gray-900">
|
||||
{{ getRemaining() }} {{ user.selectedToken.value }}
|
||||
</p>
|
||||
<div class="flex gap-2 w-32 sm:w-56" v-if="activeLockAmount != 0">
|
||||
<span class="text-xs font-normal text-gray-400" ref="infoText">{{
|
||||
`com ${activeLockAmount.toFixed(2)} ${
|
||||
user.selectedToken.value
|
||||
} em lock`
|
||||
}}</span>
|
||||
<div
|
||||
class="absolute mt-[2px] md-view"
|
||||
:style="{ left: `${(infoText?.clientWidth ?? 108) + 4}px` }"
|
||||
>
|
||||
<img
|
||||
alt="info image"
|
||||
src="@/assets/info.svg?url"
|
||||
aria-describedby="tooltip"
|
||||
ref="reference"
|
||||
@mouseover="showInfoTooltip = true"
|
||||
@mouseout="showInfoTooltip = false"
|
||||
/>
|
||||
<div
|
||||
role="tooltip"
|
||||
ref="floating"
|
||||
class="w-56 z-50 tooltip md-view"
|
||||
v-if="showInfoTooltip"
|
||||
>
|
||||
Valor “em lock” significa que a quantia está aguardando
|
||||
confirmação de compra e só estará disponível para saque caso a
|
||||
transação expire.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!isCollapsibleOpen" class="flex justify-end items-center">
|
||||
<div
|
||||
class="flex gap-2 cursor-pointer items-center justify-self-center border-2 p-2 border-amber-300 rounded-md"
|
||||
@click="[(isCollapsibleOpen = true)]"
|
||||
>
|
||||
<img
|
||||
alt="Withdraw image"
|
||||
src="@/assets/withdraw.svg?url"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
<span class="last-release-info">Sacar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-5">
|
||||
<div v-show="isCollapsibleOpen" class="py-2 w-100">
|
||||
<p class="text-sm leading-5 font-medium">Valor do saque</p>
|
||||
<input
|
||||
type="number"
|
||||
name=""
|
||||
@input="debounce(handleInputEvent, 500)($event)"
|
||||
placeholder="0"
|
||||
class="text-2xl text-gray-900 w-full outline-none"
|
||||
v-model="withdrawAmount"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center" v-if="!validDecimals">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize no máximo 2 casas decimais</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-center" v-else-if="!validWithdrawAmount">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Saldo insuficiente</span
|
||||
>
|
||||
</div>
|
||||
<hr v-show="isCollapsibleOpen" class="pb-3" />
|
||||
<div
|
||||
v-show="isCollapsibleOpen"
|
||||
class="flex justify-between items-center"
|
||||
>
|
||||
<h1
|
||||
@click="[(isCollapsibleOpen = false)]"
|
||||
class="text-black font-medium cursor-pointer"
|
||||
>
|
||||
Cancelar
|
||||
</h1>
|
||||
:valid-deposits="props.validDeposits"
|
||||
:active-lock-amount="activeLockAmount"
|
||||
:selected-token="user.selectedToken.value"
|
||||
@withdraw="callWithdraw"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="withdraw-button flex gap-2 items-center justify-self-center border-2 p-2 border-amber-300 rounded-md"
|
||||
@click="callWithdraw"
|
||||
>
|
||||
<img
|
||||
alt="Withdraw image"
|
||||
src="@/assets/withdraw.svg?url"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
<span class="last-release-info">Sacar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="w-full bg-white p-4 sm:p-6 rounded-lg"
|
||||
<TransactionCard
|
||||
v-for="item in itemsToShow"
|
||||
:key="item.blockNumber"
|
||||
>
|
||||
<div class="item-container">
|
||||
<div class="flex flex-col self-start">
|
||||
<span class="text-xs sm:text-sm leading-5 font-medium text-gray-600">
|
||||
{{ getEventName(item.event) }}
|
||||
</span>
|
||||
<span
|
||||
class="text-xl sm:text-xl leading-7 font-semibold text-gray-900"
|
||||
>
|
||||
{{ item.amount }}
|
||||
<!-- {{ getTokenByAddress(item.token) }} -->
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div
|
||||
class="bg-amber-300 status-text"
|
||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
|
||||
>
|
||||
Em Aberto
|
||||
</div>
|
||||
<div
|
||||
class="bg-[#94A3B8] status-text"
|
||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 2"
|
||||
>
|
||||
Expirado
|
||||
</div>
|
||||
<div
|
||||
class="bg-emerald-300 status-text"
|
||||
v-if="
|
||||
(getEventName(item.event) == 'Reserva' && item.lockStatus == 3) ||
|
||||
getEventName(item.event) != 'Reserva'
|
||||
"
|
||||
>
|
||||
Finalizado
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-2 cursor-pointer items-center justify-self-center w-full"
|
||||
@click="openEtherscanUrl(item.transactionHash)"
|
||||
v-if="getEventName(item.event) != 'Reserva' || item.lockStatus != 1"
|
||||
>
|
||||
<span class="last-release-info">{{ getExplorer() }}</span>
|
||||
<img
|
||||
alt="Redirect image"
|
||||
src="@/assets/redirect.svg?url"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-2 justify-self-center w-full"
|
||||
v-if="getEventName(item.event) == 'Reserva' && item.lockStatus == 1"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'home',
|
||||
force: true,
|
||||
state: { lockID: item.transactionID },
|
||||
}"
|
||||
class="router-button"
|
||||
>Continuar</RouterLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:selected-token="user.selectedToken.value"
|
||||
:transaction="item"
|
||||
:network-name="user.network.value.name"
|
||||
@open-explorer="openEtherscanUrl"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col justify-center items-center w-full mt-2 gap-2"
|
||||
v-if="
|
||||
@@ -349,14 +93,14 @@ showInitialItems();
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="text-white font-semibold"
|
||||
class="text-white font-semibold border-2 border-amber-300 rounded-lg px-4 py-2 hover:bg-amber-300/10 transition-colors cursor-pointer"
|
||||
@click="loadMore()"
|
||||
>
|
||||
Carregar mais
|
||||
</button>
|
||||
<span class="text-gray-300">
|
||||
({{ itemsToShow.length }} de {{ props.walletTransactions.length }}
|
||||
transações )
|
||||
<span class="text-gray-300 text-sm">
|
||||
{{ itemsToShow.length }} de {{ props.walletTransactions.length }}
|
||||
transações
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -367,63 +111,5 @@ showInitialItems();
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-gray-900;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
@apply text-xs sm:text-base font-medium text-gray-900 rounded-lg text-center mb-2 px-2 py-1 mt-4;
|
||||
}
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
@apply grid grid-cols-4 grid-flow-row items-center px-8 py-6 gap-4 rounded-lg shadow-md shadow-gray-600 backdrop-blur-md mt-10 w-auto;
|
||||
}
|
||||
|
||||
.last-release-info {
|
||||
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply bg-white text-gray-900 font-medium text-xs md:text-base px-3 py-2 rounded border-2 border-emerald-500 left-5 top-[-3rem];
|
||||
}
|
||||
|
||||
.router-button {
|
||||
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
|
||||
}
|
||||
|
||||
.withdraw-button {
|
||||
opacity: v-bind(withdrawButtonOpacity);
|
||||
cursor: v-bind(withdrawButtonCursor);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.md-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* Minimal styles - most styles moved to child components */
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import type { WalletTransaction } from '@/model/WalletTransaction';
|
||||
import { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { computed } from 'vue';
|
||||
import StatusBadge, { type StatusType } from '../ui/StatusBadge.vue';
|
||||
import { Networks } from '@/config/networks';
|
||||
|
||||
const props = defineProps<{
|
||||
transaction: WalletTransaction;
|
||||
networkName: keyof typeof Networks;
|
||||
selectedToken: TokenEnum;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
openExplorer: [transactionHash: string];
|
||||
}>();
|
||||
|
||||
const eventName = computed(() => {
|
||||
if (!props.transaction.event) return 'Desconhecido';
|
||||
|
||||
const possibleEventName: { [key: string]: string } = {
|
||||
DepositAdded: 'Oferta',
|
||||
LockAdded: 'Reserva',
|
||||
LockReleased: 'Compra',
|
||||
DepositWithdrawn: 'Retirada',
|
||||
};
|
||||
|
||||
return possibleEventName[props.transaction.event] || 'Desconhecido';
|
||||
});
|
||||
|
||||
const explorerName = computed(() => {
|
||||
return Networks[(props.networkName as string).toLowerCase()].blockExplorers
|
||||
?.default.name;
|
||||
});
|
||||
|
||||
const statusType = computed((): StatusType => {
|
||||
if (eventName.value === 'Reserva') {
|
||||
switch (props.transaction.lockStatus) {
|
||||
case 1:
|
||||
return 'open';
|
||||
case 2:
|
||||
return 'expired';
|
||||
case 3:
|
||||
return 'completed';
|
||||
default:
|
||||
return 'completed';
|
||||
}
|
||||
}
|
||||
return 'completed';
|
||||
});
|
||||
|
||||
const showExplorerLink = computed(() => {
|
||||
return eventName.value !== 'Reserva' || props.transaction.lockStatus !== 1;
|
||||
});
|
||||
|
||||
const showContinueButton = computed(() => {
|
||||
return eventName.value === 'Reserva' && props.transaction.lockStatus === 1;
|
||||
});
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
if (!props.transaction.blockTimestamp) return '';
|
||||
|
||||
const timestamp = props.transaction.blockTimestamp;
|
||||
const date = new Date(timestamp * 1000);
|
||||
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
});
|
||||
|
||||
const handleExplorerClick = () => {
|
||||
emit('openExplorer', props.transaction.transactionHash);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full bg-white p-4 sm:p-6 rounded-lg">
|
||||
<div class="item-container">
|
||||
<div class="flex flex-col self-start">
|
||||
<span class="text-xs sm:text-sm leading-5 font-medium text-gray-600">
|
||||
{{ eventName }}
|
||||
</span>
|
||||
<span class="text-xl sm:text-xl leading-7 font-semibold text-gray-900">
|
||||
{{ transaction.amount }} {{ selectedToken }}
|
||||
</span>
|
||||
<span
|
||||
v-if="formattedDate"
|
||||
class="text-xs sm:text-sm leading-5 font-normal text-gray-500 mt-1"
|
||||
>
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="mb-2 mt-4">
|
||||
<StatusBadge :status="statusType" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showExplorerLink"
|
||||
class="flex gap-2 cursor-pointer items-center justify-self-center w-full"
|
||||
@click="handleExplorerClick"
|
||||
>
|
||||
<span class="last-release-info">{{ explorerName }}</span>
|
||||
<img
|
||||
alt="Redirect image"
|
||||
src="@/assets/redirect.svg?url"
|
||||
class="w-3 h-3 sm:w-4 sm:h-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showContinueButton"
|
||||
class="flex gap-2 justify-self-center w-full"
|
||||
>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'home',
|
||||
force: true,
|
||||
state: { lockID: transaction.transactionID },
|
||||
}"
|
||||
class="router-button"
|
||||
>
|
||||
Continuar
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.item-container {
|
||||
@apply flex justify-between items-center;
|
||||
}
|
||||
|
||||
.last-release-info {
|
||||
@apply font-medium text-xs sm:text-sm text-gray-900 justify-self-center;
|
||||
}
|
||||
|
||||
.router-button {
|
||||
@apply rounded-lg border-amber-300 border-2 px-3 py-2 text-gray-900 font-semibold sm:text-base text-xs hover:bg-transparent w-full text-center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import CustomButton from "@/components/CustomButton/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> => {
|
||||
@@ -71,16 +71,16 @@ const handleSubmit = (e: Event): void => {
|
||||
|
||||
const data: Participant = {
|
||||
offer: offer.value,
|
||||
chainID: user.networkId.value,
|
||||
chainID: user.network.value.id,
|
||||
identification: processedIdentification,
|
||||
bankIspb: selectedBank.value?.ISPB,
|
||||
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/CustomButton/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/polygon.svg?url"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
<img
|
||||
alt="Ethereum image"
|
||||
src="@/assets/ethereum.svg?url"
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex pt-2 justify-center" v-if="!validDecimals">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Por favor utilize no máximo 2 casas decimais</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex pt-2 justify-center" v-else-if="!hasLiquidity">
|
||||
<span class="text-red-500 font-normal text-sm"
|
||||
>Atualmente não há liquidez nas redes para sua demanda</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomButton
|
||||
v-if="walletAddress"
|
||||
:text="'Conectar carteira'"
|
||||
@buttonClicked="emit('tokenBuy')"
|
||||
/>
|
||||
<CustomButton
|
||||
v-if="!walletAddress"
|
||||
:text="'Conectar carteira'"
|
||||
@buttonClicked="connectAccount()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-divide {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
}
|
||||
.bottom-position {
|
||||
top: -20px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
input[type="number"]::-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/CustomButton/CustomButton.vue";
|
||||
import CustomButton from '@/components/ui/CustomButton.vue';
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["sendNetwork"]);
|
||||
const emit = defineEmits(['sendNetwork']);
|
||||
|
||||
// props and store references
|
||||
const props = defineProps({
|
||||
@@ -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>
|
||||
+278
-155
@@ -1,22 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
import { getNetworkImage } from "@/utils/imagesPath";
|
||||
import { Networks } from "@/model/Networks";
|
||||
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 { 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";
|
||||
interface MenuOption {
|
||||
label: string;
|
||||
route?: string;
|
||||
action?: () => void;
|
||||
showInDesktop?: boolean;
|
||||
showInMobile?: boolean;
|
||||
isDynamic?: boolean;
|
||||
dynamicLabel?: () => string;
|
||||
dynamicRoute?: () => string;
|
||||
showVersion?: boolean;
|
||||
}
|
||||
|
||||
// Use the new composable
|
||||
const user = useUser();
|
||||
const { walletAddress, sellerView, networkId } = user;
|
||||
const { walletAddress, sellerView, network } = user;
|
||||
|
||||
const menuOpenToggle = ref<boolean>(false);
|
||||
const infoMenuOpenToggle = ref<boolean>(false);
|
||||
@@ -35,42 +47,39 @@ 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());
|
||||
});
|
||||
|
||||
watch(connectedChain, (newVal: any) => {
|
||||
// Check if connected chain is valid, otherwise default to Sepolia (NetworkEnum.SEPOLIA)
|
||||
// Check if connected chain is valid, otherwise default to Sepolia
|
||||
if (
|
||||
!newVal ||
|
||||
!Object.values(Networks).some(
|
||||
(network) => Number(network.chainId) === 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.setNetworkId(user.networkId.value);
|
||||
user.setNetwork(DEFAULT_NETWORK);
|
||||
return;
|
||||
}
|
||||
user.setNetworkId(newVal?.id);
|
||||
user.setNetworkById(newVal?.id);
|
||||
});
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -78,16 +87,24 @@ const closeMenu = (): void => {
|
||||
menuOpenToggle.value = false;
|
||||
};
|
||||
|
||||
const networkChange = async (network: NetworkEnum): Promise<void> => {
|
||||
const networkChange = async (network: NetworkConfig): Promise<void> => {
|
||||
currencyMenuOpenToggle.value = false;
|
||||
try {
|
||||
await setChain({
|
||||
chainId: Networks[network].chainId,
|
||||
wallet: connectedWallet.value?.label || "",
|
||||
});
|
||||
user.setNetworkId(network);
|
||||
} catch (error) {
|
||||
console.log("Error changing network", error);
|
||||
|
||||
// If wallet is connected, try to change chain in wallet
|
||||
if (connectedWallet.value) {
|
||||
const chainId = network.id.toString(16);
|
||||
try {
|
||||
await setChain({
|
||||
chainId: `0x${chainId}`,
|
||||
wallet: connectedWallet.value.label,
|
||||
});
|
||||
user.setNetwork(network);
|
||||
} catch (error) {
|
||||
console.log('Error changing network', error);
|
||||
}
|
||||
} else {
|
||||
// If no wallet connected, just update the network state
|
||||
user.setNetwork(network);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,11 +119,83 @@ onClickOutside(currencyRef, () => {
|
||||
onClickOutside(infoMenuRef, () => {
|
||||
infoMenuOpenToggle.value = false;
|
||||
});
|
||||
|
||||
const infoMenuOptions: MenuOption[] = [
|
||||
{
|
||||
label: 'Explorar Transações',
|
||||
route: '/explore',
|
||||
showInDesktop: true,
|
||||
showInMobile: false,
|
||||
},
|
||||
{
|
||||
label: 'Perguntas frequentes',
|
||||
route: '/faq',
|
||||
showInDesktop: true,
|
||||
showInMobile: false,
|
||||
},
|
||||
{
|
||||
label: 'Versões',
|
||||
route: '/versions',
|
||||
showInDesktop: true,
|
||||
showInMobile: false,
|
||||
},
|
||||
];
|
||||
|
||||
const walletMenuOptions: MenuOption[] = [
|
||||
{
|
||||
label: 'Quero vender',
|
||||
isDynamic: true,
|
||||
dynamicLabel: () => (sellerView.value ? 'Quero comprar' : 'Quero vender'),
|
||||
dynamicRoute: () => (sellerView.value ? '/' : '/seller'),
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: 'Explorar Transações',
|
||||
route: '/explore',
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: 'Gerenciar Ofertas',
|
||||
route: '/manage_bids',
|
||||
showInDesktop: true,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: 'Perguntas frequentes',
|
||||
route: '/faq',
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: 'Versões',
|
||||
route: '/versions',
|
||||
showInDesktop: false,
|
||||
showInMobile: true,
|
||||
},
|
||||
{
|
||||
label: 'Desconectar',
|
||||
route: '/',
|
||||
action: disconnectUser,
|
||||
showInDesktop: true,
|
||||
showInMobile: true,
|
||||
},
|
||||
];
|
||||
|
||||
const handleMenuOptionClick = (option: MenuOption): void => {
|
||||
if (!option.action) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="z-20">
|
||||
<RouterLink :to="'/'" class="default-button">
|
||||
<RouterLink
|
||||
:to="'/'"
|
||||
class="default-button flex items-center md:h-auto md:py-2 h-10 py-0"
|
||||
>
|
||||
<img
|
||||
alt="P2Pix logo"
|
||||
class="logo hidden md:inline-block"
|
||||
@@ -116,7 +205,7 @@ onClickOutside(infoMenuRef, () => {
|
||||
/>
|
||||
<img
|
||||
alt="P2Pix logo"
|
||||
class="logo inline-block md:hidden w-10/12"
|
||||
class="logo inline-block md:hidden h-10"
|
||||
width="40"
|
||||
height="40"
|
||||
src="@/assets/logo2.svg?url"
|
||||
@@ -128,13 +217,11 @@ onClickOutside(infoMenuRef, () => {
|
||||
<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"
|
||||
@@ -154,26 +241,45 @@ onClickOutside(infoMenuRef, () => {
|
||||
>
|
||||
<div class="mt-2">
|
||||
<div class="bg-white rounded-md z-10 -left-36 w-52">
|
||||
<div class="menu-button gap-2 px-4 rounded-md cursor-pointer">
|
||||
<span
|
||||
class="text-gray-900 py-4 text-end font-semibold text-sm"
|
||||
>
|
||||
Documentação
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
<RouterLink
|
||||
:to="'/faq'"
|
||||
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
|
||||
<template
|
||||
v-for="(option, index) in infoMenuOptions.filter(
|
||||
(opt) => opt.showInDesktop,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
<span
|
||||
class="text-gray-900 py-4 text-end font-semibold text-sm whitespace-nowrap"
|
||||
<RouterLink
|
||||
v-if="option.route"
|
||||
:to="option.route"
|
||||
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
|
||||
>
|
||||
Perguntas frequentes
|
||||
</span>
|
||||
</RouterLink>
|
||||
<span
|
||||
class="text-gray-900 py-4 text-end font-semibold text-sm whitespace-nowrap"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
<div
|
||||
v-else
|
||||
class="menu-button gap-2 px-4 rounded-md cursor-pointer"
|
||||
>
|
||||
<span
|
||||
class="text-gray-900 py-4 text-end font-semibold text-sm"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
index <
|
||||
infoMenuOptions.filter((opt) => opt.showInDesktop)
|
||||
.length -
|
||||
1
|
||||
"
|
||||
class="w-full flex justify-center"
|
||||
>
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
@@ -219,46 +325,28 @@ onClickOutside(infoMenuRef, () => {
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<RouterLink
|
||||
:to="'/faq'"
|
||||
v-if="!walletAddress"
|
||||
class="default-button inline-block md:hidden"
|
||||
>
|
||||
FAQ
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
:to="sellerView ? '/' : '/seller'"
|
||||
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>
|
||||
<RouterLink
|
||||
:to="sellerView ? '/' : '/seller'"
|
||||
v-if="!walletAddress"
|
||||
class="default-button sm:whitespace-normal whitespace-nowrap inline-block md:hidden w-40 sm:w-44 md:w-36"
|
||||
>
|
||||
<div class="topbar-text topbar-link text-center mx-auto inline-block">
|
||||
{{ sellerView ? "Quero comprar" : "Quero vender" }}
|
||||
</div>
|
||||
</RouterLink>
|
||||
<div class="flex flex-col relative" v-if="walletAddress">
|
||||
<div class="flex flex-col relative">
|
||||
<div
|
||||
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"
|
||||
:src="getNetworkImage(NetworkEnum[user.networkName.value])"
|
||||
:src="getNetworkImage(network.name)"
|
||||
height="24"
|
||||
width="24"
|
||||
/>
|
||||
@@ -266,11 +354,7 @@ onClickOutside(infoMenuRef, () => {
|
||||
class="default-text hidden sm:inline-block text-gray-50 group-hover:text-gray-900 transition-all duration-500 ease-in-out whitespace-nowrap text-ellipsis overflow-hidden"
|
||||
:class="{ '!text-gray-900': currencyMenuOpenToggle }"
|
||||
>
|
||||
{{
|
||||
Networks[user.networkName.value]
|
||||
? Networks[user.networkName.value].chainName
|
||||
: "Invalid Chain"
|
||||
}}
|
||||
{{ user.network.value.name || 'Invalid Chain' }}
|
||||
</span>
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out mt-1"
|
||||
@@ -292,20 +376,20 @@ onClickOutside(infoMenuRef, () => {
|
||||
class="mt-2 bg-white rounded-md border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
||||
>
|
||||
<div
|
||||
v-for="(chainData, network) in Networks"
|
||||
:key="network"
|
||||
v-for="network in Networks"
|
||||
:key="network.id"
|
||||
class="menu-button p-4 gap-2 cursor-pointer hover:bg-gray-200 flex items-center !justify-start whitespace-nowrap transition-colors duration-150 ease-in-out"
|
||||
@click="networkChange(network)"
|
||||
>
|
||||
<img
|
||||
:alt="chainData.chainName + ' image'"
|
||||
:alt="network.name + ' image'"
|
||||
width="20"
|
||||
height="20"
|
||||
:src="getNetworkImage(NetworkEnum[network])"
|
||||
:src="getNetworkImage(network.name)"
|
||||
class="mr-2 ml-1"
|
||||
/>
|
||||
<span class="text-gray-900 font-semibold text-sm">
|
||||
{{ chainData.chainName }}
|
||||
{{ network.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full flex justify-center">
|
||||
@@ -318,7 +402,7 @@ onClickOutside(infoMenuRef, () => {
|
||||
<button
|
||||
type="button"
|
||||
v-if="!walletAddress"
|
||||
class="border-amber-500 border-2 rounded default-button hidden md:inline-block"
|
||||
class="border-amber-500 border-2 sm:rounded !rounded-lg default-button hidden md:inline-block"
|
||||
@click="connnectWallet()"
|
||||
>
|
||||
Conectar carteira
|
||||
@@ -326,7 +410,7 @@ onClickOutside(infoMenuRef, () => {
|
||||
<button
|
||||
type="button"
|
||||
v-if="!walletAddress"
|
||||
class="border-amber-500 border-2 rounded default-button inline-block md:hidden"
|
||||
class="border-amber-500 border-2 sm:rounded !rounded-lg default-button inline-block md:hidden h-10"
|
||||
@click="connnectWallet()"
|
||||
>
|
||||
Conectar
|
||||
@@ -337,13 +421,11 @@ onClickOutside(infoMenuRef, () => {
|
||||
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
|
||||
@@ -372,23 +454,40 @@ onClickOutside(infoMenuRef, () => {
|
||||
<div
|
||||
class="bg-white rounded-md z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
|
||||
>
|
||||
<RouterLink
|
||||
to="/manage_bids"
|
||||
class="redirect_button menu-button"
|
||||
@click="closeMenu()"
|
||||
<template
|
||||
v-for="(option, index) in walletMenuOptions.filter(
|
||||
(opt) => opt.showInDesktop,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
Gerenciar Ofertas
|
||||
</RouterLink>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="redirect_button menu-button"
|
||||
@click="disconnectUser"
|
||||
>
|
||||
Desconectar
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="option.route && !option.action"
|
||||
:to="option.route"
|
||||
class="redirect_button menu-button"
|
||||
@click="closeMenu()"
|
||||
>
|
||||
{{ option.label }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-else-if="option.route && option.action"
|
||||
:to="option.route"
|
||||
class="redirect_button menu-button"
|
||||
@click="option.action"
|
||||
>
|
||||
{{ option.label }}
|
||||
</RouterLink>
|
||||
<div
|
||||
v-if="
|
||||
index <
|
||||
walletMenuOptions.filter((opt) => opt.showInDesktop)
|
||||
.length -
|
||||
1
|
||||
"
|
||||
class="w-full flex justify-center"
|
||||
>
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,32 +499,49 @@ onClickOutside(infoMenuRef, () => {
|
||||
v-show="menuOpenToggle"
|
||||
class="mobile-menu fixed w-4/5 text-gray-900 inline-block md:hidden"
|
||||
>
|
||||
<div class="pl-4 mt-2 h-full">
|
||||
<div class="pl-4 h-full">
|
||||
<div class="bg-white rounded-md z-10 h-full">
|
||||
<div class="menu-button" @click="closeMenu()">
|
||||
<RouterLink
|
||||
:to="sellerView ? '/' : '/seller'"
|
||||
class="redirect_button mt-2"
|
||||
<template
|
||||
v-for="(option, index) in walletMenuOptions.filter(
|
||||
(opt) => opt.showInMobile,
|
||||
)"
|
||||
:key="index"
|
||||
>
|
||||
<div class="menu-button" @click="handleMenuOptionClick(option)">
|
||||
<RouterLink
|
||||
v-if="option.isDynamic"
|
||||
:to="option.dynamicRoute ? option.dynamicRoute() : '/'"
|
||||
class="redirect_button"
|
||||
:class="{ 'mt-2': index === 0 }"
|
||||
>
|
||||
{{ option.dynamicLabel ? option.dynamicLabel() : option.label }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-else-if="option.route && !option.action"
|
||||
:to="option.route"
|
||||
class="redirect_button"
|
||||
>
|
||||
{{ option.label }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-else-if="option.route && option.action"
|
||||
:to="option.route"
|
||||
class="redirect_button"
|
||||
@click.stop="option.action"
|
||||
>
|
||||
{{ option.label }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
index <
|
||||
walletMenuOptions.filter((opt) => opt.showInMobile).length - 1
|
||||
"
|
||||
class="w-full flex justify-center"
|
||||
>
|
||||
{{ sellerView ? "Quero comprar" : "Quero vender" }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
<div class="menu-button" @click="closeMenu()">
|
||||
<RouterLink to="/manage_bids" class="redirect_button">
|
||||
Gerenciar Ofertas
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
<div class="menu-button" @click="disconnectUser">
|
||||
<RouterLink to="/" class="redirect_button">
|
||||
Desconectar
|
||||
</RouterLink>
|
||||
</div>
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full flex justify-center">
|
||||
<hr class="w-4/5" />
|
||||
</div>
|
||||
@@ -459,22 +575,22 @@ onClickOutside(infoMenuRef, () => {
|
||||
v-show="currencyMenuOpenToggle"
|
||||
class="mobile-menu fixed w-4/5 text-gray-900 inline-block md:hidden"
|
||||
>
|
||||
<div class="pl-4 mt-2 h-full">
|
||||
<div class="pl-4 h-full">
|
||||
<div class="bg-white rounded-md z-10 h-full">
|
||||
<div
|
||||
v-for="(chainData, network) in Networks"
|
||||
:key="network"
|
||||
v-for="network in Networks"
|
||||
:key="network.id"
|
||||
class="menu-button gap-2 sm:px-4 rounded-md cursor-pointer py-2 px-4"
|
||||
@click="networkChange(network)"
|
||||
>
|
||||
<img
|
||||
:alt="chainData.chainName + 'image'"
|
||||
:alt="network.name + 'image'"
|
||||
width="20"
|
||||
height="20"
|
||||
:src="getNetworkImage(NetworkEnum[network])"
|
||||
:src="getNetworkImage(network.name)"
|
||||
/>
|
||||
<span class="text-gray-900 py-4 text-end font-bold text-sm">
|
||||
{{ chainData.chainName }}
|
||||
{{ network.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -484,6 +600,7 @@ onClickOutside(infoMenuRef, () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
header {
|
||||
@apply flex flex-row justify-between w-full items-center;
|
||||
}
|
||||
@@ -524,8 +641,14 @@ a:hover {
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
margin-top: 1400px;
|
||||
bottom: 0px;
|
||||
height: auto;
|
||||
top: 60px;
|
||||
right: 10px;
|
||||
left: auto;
|
||||
max-height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
text: string;
|
||||
isDisabled?: boolean;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
icon?: string;
|
||||
iconPosition?: 'left' | 'right';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
isDisabled: false,
|
||||
variant: 'primary',
|
||||
size: 'xl',
|
||||
iconPosition: 'left',
|
||||
fullWidth: true,
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['buttonClicked']);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.isDisabled && !props.loading) {
|
||||
emit('buttonClicked');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'button',
|
||||
`variant-${variant}`,
|
||||
`size-${size}`,
|
||||
{ 'is-disabled': isDisabled || loading, 'full-width': fullWidth },
|
||||
]"
|
||||
:disabled="isDisabled || loading"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span v-if="loading" class="loader"></span>
|
||||
<template v-else>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'left'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
<span class="button-text">{{ text }}</span>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'right'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.button {
|
||||
@apply rounded-lg font-semibold transition-all duration-200 cursor-pointer flex items-center justify-center gap-2;
|
||||
}
|
||||
|
||||
.button:hover:not(.is-disabled) {
|
||||
@apply transform scale-[1.02];
|
||||
}
|
||||
|
||||
.button.is-disabled {
|
||||
@apply opacity-70 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.button.full-width {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* Variantes */
|
||||
.variant-primary {
|
||||
@apply bg-amber-400 text-gray-900 border-2 border-amber-400;
|
||||
}
|
||||
|
||||
.variant-primary:hover:not(.is-disabled) {
|
||||
@apply bg-amber-500 border-amber-500;
|
||||
}
|
||||
|
||||
.variant-secondary {
|
||||
@apply bg-gray-200 text-gray-900 border-2 border-gray-300;
|
||||
}
|
||||
|
||||
.variant-secondary:hover:not(.is-disabled) {
|
||||
@apply bg-gray-300 border-gray-400;
|
||||
}
|
||||
|
||||
.variant-outline {
|
||||
@apply bg-transparent text-gray-900 border-2 border-amber-400;
|
||||
}
|
||||
|
||||
.variant-outline:hover:not(.is-disabled) {
|
||||
@apply bg-amber-400/10;
|
||||
}
|
||||
|
||||
.variant-ghost {
|
||||
@apply bg-transparent text-gray-900 border-2 border-transparent;
|
||||
}
|
||||
|
||||
.variant-ghost:hover:not(.is-disabled) {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Tamanhos */
|
||||
.size-sm {
|
||||
@apply px-2 py-1 text-xs;
|
||||
}
|
||||
|
||||
.size-sm .button-icon {
|
||||
@apply w-3 h-3;
|
||||
}
|
||||
|
||||
.size-md {
|
||||
@apply px-3 py-2 text-sm;
|
||||
}
|
||||
|
||||
.size-md .button-icon {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
.size-lg {
|
||||
@apply px-4 py-3 text-base;
|
||||
}
|
||||
|
||||
.size-lg .button-icon {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
.size-xl {
|
||||
@apply p-4 text-base;
|
||||
}
|
||||
|
||||
.size-xl .button-icon {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
/* Loader animation */
|
||||
.loader {
|
||||
@apply w-5 h-5 border-2 border-gray-900 border-t-transparent rounded-full animate-spin;
|
||||
}
|
||||
</style>
|
||||
@@ -1,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>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
type FormCardPadding = 'sm' | 'md' | 'lg';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
padding?: FormCardPadding;
|
||||
fullWidth?: boolean;
|
||||
noBorder?: boolean;
|
||||
}>(),
|
||||
{
|
||||
padding: 'md',
|
||||
fullWidth: true,
|
||||
noBorder: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'form-card',
|
||||
`padding-${padding}`,
|
||||
{ 'full-width': fullWidth, 'no-border': noBorder },
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.form-card {
|
||||
@apply flex flex-col bg-white rounded-lg;
|
||||
}
|
||||
|
||||
.form-card:not(.no-border) {
|
||||
@apply border-y;
|
||||
}
|
||||
|
||||
.form-card.full-width {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.padding-sm {
|
||||
@apply px-4 py-3;
|
||||
}
|
||||
|
||||
.padding-md {
|
||||
@apply sm:px-10 px-6 py-5;
|
||||
}
|
||||
|
||||
.padding-lg {
|
||||
@apply px-12 py-8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
type IconButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
type IconButtonSize = 'sm' | 'md' | 'lg';
|
||||
type IconPosition = 'left' | 'right';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
text: string;
|
||||
icon?: string;
|
||||
variant?: IconButtonVariant;
|
||||
size?: IconButtonSize;
|
||||
iconPosition?: IconPosition;
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'md',
|
||||
iconPosition: 'left',
|
||||
disabled: false,
|
||||
fullWidth: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled) {
|
||||
emit('click');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'icon-button',
|
||||
`variant-${variant}`,
|
||||
`size-${size}`,
|
||||
{ 'is-disabled': disabled, 'full-width': fullWidth },
|
||||
]"
|
||||
:disabled="disabled"
|
||||
@click="handleClick"
|
||||
>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'left'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
<span class="button-text">{{ text }}</span>
|
||||
<img
|
||||
v-if="icon && iconPosition === 'right'"
|
||||
:src="icon"
|
||||
:alt="`${text} icon`"
|
||||
class="button-icon"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.icon-button {
|
||||
@apply flex items-center justify-center gap-2 font-medium rounded-lg transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.icon-button:hover:not(.is-disabled) {
|
||||
@apply transform scale-[1.02];
|
||||
}
|
||||
|
||||
.icon-button.is-disabled {
|
||||
@apply opacity-60 cursor-not-allowed;
|
||||
}
|
||||
|
||||
.icon-button.full-width {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
/* Variantes */
|
||||
.variant-primary {
|
||||
@apply bg-amber-400 text-gray-900 border-2 border-amber-400;
|
||||
}
|
||||
|
||||
.variant-primary:hover:not(.is-disabled) {
|
||||
@apply bg-amber-500 border-amber-500;
|
||||
}
|
||||
|
||||
.variant-secondary {
|
||||
@apply bg-gray-200 text-gray-900 border-2 border-gray-300;
|
||||
}
|
||||
|
||||
.variant-secondary:hover:not(.is-disabled) {
|
||||
@apply bg-gray-300 border-gray-400;
|
||||
}
|
||||
|
||||
.variant-outline {
|
||||
@apply bg-transparent text-gray-900 border-2 border-amber-300;
|
||||
}
|
||||
|
||||
.variant-outline:hover:not(.is-disabled) {
|
||||
@apply bg-amber-300/10;
|
||||
}
|
||||
|
||||
.variant-ghost {
|
||||
@apply bg-transparent text-gray-900 border-2 border-transparent;
|
||||
}
|
||||
|
||||
.variant-ghost:hover:not(.is-disabled) {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
|
||||
/* Tamanhos */
|
||||
.size-sm {
|
||||
@apply px-2 py-1 text-xs;
|
||||
}
|
||||
|
||||
.size-sm .button-icon {
|
||||
@apply w-3 h-3;
|
||||
}
|
||||
|
||||
.size-md {
|
||||
@apply px-3 py-2 text-sm;
|
||||
}
|
||||
|
||||
.size-md .button-icon {
|
||||
@apply w-4 h-4;
|
||||
}
|
||||
|
||||
.size-lg {
|
||||
@apply px-4 py-3 text-base;
|
||||
}
|
||||
|
||||
.size-lg .button-icon {
|
||||
@apply w-5 h-5;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
</style>
|
||||
+6
-7
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
export type StatusType = 'open' | 'expired' | 'completed' | 'pending';
|
||||
|
||||
const props = defineProps<{
|
||||
status: StatusType;
|
||||
customText?: string;
|
||||
}>();
|
||||
|
||||
const statusConfig = computed(() => {
|
||||
const configs: Record<StatusType, { text: string; color: string }> = {
|
||||
open: {
|
||||
text: 'Em Aberto',
|
||||
color: 'bg-amber-300',
|
||||
},
|
||||
expired: {
|
||||
text: 'Expirado',
|
||||
color: 'bg-[#94A3B8]',
|
||||
},
|
||||
completed: {
|
||||
text: 'Finalizado',
|
||||
color: 'bg-emerald-300',
|
||||
},
|
||||
pending: {
|
||||
text: 'Pendente',
|
||||
color: 'bg-gray-300',
|
||||
},
|
||||
};
|
||||
|
||||
return configs[props.status];
|
||||
});
|
||||
|
||||
const displayText = computed(() => {
|
||||
return props.customText || statusConfig.value.text;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[statusConfig.color, 'status-badge']">
|
||||
{{ displayText }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.status-badge {
|
||||
@apply text-xs sm:text-base font-medium text-gray-900 rounded-lg text-center px-2 py-1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useOnboard } from "@web3-onboard/vue";
|
||||
import { Networks } from "../model/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();
|
||||
const { networkId, networkName } = user;
|
||||
const { network } = user;
|
||||
|
||||
const isWrongNetwork = ref(false);
|
||||
const targetNetworkName = computed(() => Networks[networkName.value].chainName);
|
||||
const targetNetworkName = computed(() => network.value.name);
|
||||
|
||||
const checkNetwork = () => {
|
||||
if (connectedWallet.value) {
|
||||
const chainId = connectedWallet.value.chains[0].id;
|
||||
isWrongNetwork.value = Number(chainId) !== networkId.value;
|
||||
isWrongNetwork.value = Number(chainId) !== network.value.id;
|
||||
} else {
|
||||
isWrongNetwork.value = false; // No wallet connected yet
|
||||
}
|
||||
@@ -23,23 +23,24 @@ const checkNetwork = () => {
|
||||
const switchNetwork = async () => {
|
||||
try {
|
||||
if (connectedWallet.value && connectedWallet.value.provider) {
|
||||
const chainId = network.value.id.toString(16);
|
||||
await connectedWallet.value.provider.request({
|
||||
method: "wallet_switchEthereumChain",
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [
|
||||
{
|
||||
chainId: Networks[networkName.value].chainId,
|
||||
chainId: `0x${chainId}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to switch network:", error);
|
||||
console.error('Failed to switch network:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(checkNetwork);
|
||||
watch(connectedWallet, checkNetwork);
|
||||
watch(networkId, checkNetwork, { immediate: true });
|
||||
watch(network, checkNetwork, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -65,7 +66,9 @@ watch(networkId, 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,
|
||||
@@ -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>
|
||||
@@ -0,0 +1,468 @@
|
||||
import { NetworkConfig } from '@/model/NetworkEnum';
|
||||
import { ref, computed, type Ref } from 'vue';
|
||||
import { sepolia, rootstock, rootstockTestnet } from 'viem/chains';
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
type: 'deposit' | 'lock' | 'release' | 'return';
|
||||
timestamp: string;
|
||||
blockTimestamp: string;
|
||||
seller?: string;
|
||||
buyer?: string | null;
|
||||
amount: string;
|
||||
token: string;
|
||||
blockNumber: string;
|
||||
transactionHash: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsData {
|
||||
totalVolume: string;
|
||||
totalTransactions: string;
|
||||
totalLocks: string;
|
||||
totalDeposits: string;
|
||||
totalReleases: string;
|
||||
}
|
||||
|
||||
export function useGraphQL(network: Ref<NetworkConfig>) {
|
||||
const searchAddress = ref('');
|
||||
const selectedType = ref('all');
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const analyticsLoading = ref(false);
|
||||
|
||||
const transactionsData = ref<Transaction[]>([]);
|
||||
const analyticsData = ref<AnalyticsData>({
|
||||
totalVolume: '0',
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0',
|
||||
});
|
||||
|
||||
const executeQuery = async (query: string, variables: any = {}) => {
|
||||
const url = network.value.subgraphUrls[0]; // TODO: try all available URLs
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(data.errors[0]?.message || 'GraphQL error');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (err) {
|
||||
console.error('GraphQL query error:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAllActivity = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const query = `
|
||||
query GetAllActivity($first: Int = 50) {
|
||||
depositAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
depositWithdrawns(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockAddeds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReleaseds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReturneds(first: $first, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await executeQuery(query, { first: 50 });
|
||||
transactionsData.value = processActivityData(data);
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error ? err.message : 'Failed to fetch transactions';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserActivity = async (userAddress: string) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const query = `
|
||||
query GetUserActivity($userAddress: String!, $first: Int = 50) {
|
||||
depositAddeds(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
depositWithdrawns(first: $first, where: { seller: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
seller
|
||||
token
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockAddeds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockID
|
||||
seller
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReleaseds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
amount
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
lockReturneds(first: $first, where: { buyer: $userAddress }, orderBy: "blockTimestamp", orderDirection: "desc") {
|
||||
id
|
||||
buyer
|
||||
lockId
|
||||
blockNumber
|
||||
blockTimestamp
|
||||
transactionHash
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await executeQuery(query, { userAddress, first: 50 });
|
||||
transactionsData.value = processActivityData(data);
|
||||
} catch (err) {
|
||||
error.value =
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to fetch user transactions';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearData = () => {
|
||||
transactionsData.value = [];
|
||||
analyticsData.value = {
|
||||
totalVolume: '0',
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0',
|
||||
};
|
||||
};
|
||||
|
||||
const fetchAnalytics = async () => {
|
||||
analyticsLoading.value = true;
|
||||
|
||||
const query = `
|
||||
query GetAnalytics {
|
||||
depositAddeds(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
depositWithdrawns(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
lockAddeds(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
lockReleaseds(first: 1000) {
|
||||
amount
|
||||
blockTimestamp
|
||||
}
|
||||
lockReturneds(first: 1000) {
|
||||
blockTimestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await executeQuery(query);
|
||||
analyticsData.value = processAnalyticsData(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch analytics:', err);
|
||||
} finally {
|
||||
analyticsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const processActivityData = (data: any): Transaction[] => {
|
||||
if (!data) return [];
|
||||
|
||||
const activities: Transaction[] = [];
|
||||
|
||||
if (data.depositAddeds) {
|
||||
data.depositAddeds.forEach((deposit: any) => {
|
||||
activities.push({
|
||||
id: deposit.id,
|
||||
blockNumber: deposit.blockNumber,
|
||||
blockTimestamp: deposit.blockTimestamp,
|
||||
transactionHash: deposit.transactionHash,
|
||||
type: 'deposit',
|
||||
seller: deposit.seller,
|
||||
buyer: undefined,
|
||||
amount: deposit.amount,
|
||||
token: deposit.token,
|
||||
timestamp: formatTimestamp(deposit.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.depositWithdrawns) {
|
||||
data.depositWithdrawns.forEach((withdrawal: any) => {
|
||||
activities.push({
|
||||
id: withdrawal.id,
|
||||
blockNumber: withdrawal.blockNumber,
|
||||
blockTimestamp: withdrawal.blockTimestamp,
|
||||
transactionHash: withdrawal.transactionHash,
|
||||
type: 'deposit', // Treat as deposit withdrawal
|
||||
seller: withdrawal.seller,
|
||||
buyer: undefined,
|
||||
amount: withdrawal.amount,
|
||||
token: withdrawal.token,
|
||||
timestamp: formatTimestamp(withdrawal.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockAddeds) {
|
||||
data.lockAddeds.forEach((lock: any) => {
|
||||
activities.push({
|
||||
id: lock.id,
|
||||
blockNumber: lock.blockNumber,
|
||||
blockTimestamp: lock.blockTimestamp,
|
||||
transactionHash: lock.transactionHash,
|
||||
type: 'lock',
|
||||
seller: lock.seller,
|
||||
buyer: lock.buyer,
|
||||
amount: lock.amount,
|
||||
token: lock.token,
|
||||
timestamp: formatTimestamp(lock.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReleaseds) {
|
||||
data.lockReleaseds.forEach((release: any) => {
|
||||
activities.push({
|
||||
id: release.id,
|
||||
blockNumber: release.blockNumber,
|
||||
blockTimestamp: release.blockTimestamp,
|
||||
transactionHash: release.transactionHash,
|
||||
type: 'release',
|
||||
seller: undefined, // Release doesn't have seller info
|
||||
buyer: release.buyer,
|
||||
amount: release.amount,
|
||||
token: 'BRZ', // Default token
|
||||
timestamp: formatTimestamp(release.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReturneds) {
|
||||
data.lockReturneds.forEach((returnTx: any) => {
|
||||
activities.push({
|
||||
id: returnTx.id,
|
||||
blockNumber: returnTx.blockNumber,
|
||||
blockTimestamp: returnTx.blockTimestamp,
|
||||
transactionHash: returnTx.transactionHash,
|
||||
type: 'return',
|
||||
seller: undefined, // Return doesn't have seller info
|
||||
buyer: returnTx.buyer,
|
||||
amount: '0', // Return doesn't have amount
|
||||
token: 'BRZ', // Default token
|
||||
timestamp: formatTimestamp(returnTx.blockTimestamp),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return activities.sort(
|
||||
(a, b) => parseInt(b.blockTimestamp) - parseInt(a.blockTimestamp),
|
||||
);
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string): string => {
|
||||
const now = Date.now() / 1000;
|
||||
const diff = now - parseInt(timestamp);
|
||||
|
||||
if (diff < 60) return 'Just now';
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
|
||||
return `${Math.floor(diff / 86400)} days ago`;
|
||||
};
|
||||
|
||||
const formatAmount = (amount: string): string => {
|
||||
const num = parseFloat(amount);
|
||||
if (num >= 1000000000000000)
|
||||
return `${(num / 1000000000000000).toFixed(1)}Q`;
|
||||
if (num >= 1000000000000) return `${(num / 1000000000000).toFixed(1)}T`;
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(1)}B`;
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
|
||||
if (num < 1) return num.toFixed(4);
|
||||
return num.toFixed(2);
|
||||
};
|
||||
|
||||
const processAnalyticsData = (data: any): AnalyticsData => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalVolume: '0',
|
||||
totalTransactions: '0',
|
||||
totalLocks: '0',
|
||||
totalDeposits: '0',
|
||||
totalReleases: '0',
|
||||
};
|
||||
}
|
||||
|
||||
let totalVolume = 0;
|
||||
let totalTransactions = 0;
|
||||
let totalLocks = 0;
|
||||
let totalDeposits = 0;
|
||||
let totalReleases = 0;
|
||||
|
||||
if (data.depositAddeds) {
|
||||
data.depositAddeds.forEach((deposit: any) => {
|
||||
totalVolume += parseFloat(deposit.amount || '0');
|
||||
totalTransactions++;
|
||||
totalDeposits++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.depositWithdrawns) {
|
||||
data.depositWithdrawns.forEach((withdrawal: any) => {
|
||||
totalVolume += parseFloat(withdrawal.amount || '0');
|
||||
totalTransactions++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockAddeds) {
|
||||
data.lockAddeds.forEach((lock: any) => {
|
||||
totalVolume += parseFloat(lock.amount || '0');
|
||||
totalTransactions++;
|
||||
totalLocks++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReleaseds) {
|
||||
data.lockReleaseds.forEach((release: any) => {
|
||||
totalVolume += parseFloat(release.amount || '0');
|
||||
totalTransactions++;
|
||||
totalReleases++;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.lockReturneds) {
|
||||
data.lockReturneds.forEach((returnTx: any) => {
|
||||
totalTransactions++;
|
||||
});
|
||||
}
|
||||
|
||||
const result = {
|
||||
totalVolume: formatAmount(totalVolume.toString()),
|
||||
totalTransactions: totalTransactions.toString(),
|
||||
totalLocks: totalLocks.toString(),
|
||||
totalDeposits: totalDeposits.toString(),
|
||||
totalReleases: totalReleases.toString(),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const filteredTransactions = computed(() => {
|
||||
let filtered = transactionsData.value;
|
||||
|
||||
if (selectedType.value !== 'all') {
|
||||
filtered = filtered.filter((tx) => tx.type === selectedType.value);
|
||||
}
|
||||
|
||||
if (searchAddress.value) {
|
||||
const searchLower = searchAddress.value.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(tx) =>
|
||||
tx.seller?.toLowerCase().includes(searchLower) ||
|
||||
tx.buyer?.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
return {
|
||||
searchAddress,
|
||||
selectedType,
|
||||
transactions: filteredTransactions,
|
||||
analytics: analyticsData,
|
||||
loading,
|
||||
error,
|
||||
analyticsLoading,
|
||||
fetchAllActivity,
|
||||
fetchUserActivity,
|
||||
fetchAnalytics,
|
||||
clearData,
|
||||
};
|
||||
}
|
||||
+36
-17
@@ -1,22 +1,21 @@
|
||||
import { ref } from "vue";
|
||||
import { NetworkEnum, TokenEnum } from "../model/NetworkEnum";
|
||||
import type { ValidDeposit } from "@/model/ValidDeposit";
|
||||
import type { Participant } from "../utils/bbPay";
|
||||
import { NetworkById } from "@/model/Networks";
|
||||
import type { Address } from "viem"
|
||||
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 networkId = ref(11155111);
|
||||
const networkName = ref(NetworkEnum.sepolia);
|
||||
const selectedToken = ref(TokenEnum.BRZ);
|
||||
const balance = ref('');
|
||||
const network = ref(DEFAULT_NETWORK);
|
||||
const selectedToken = ref<TokenEnum>(TokenEnum.BRZ);
|
||||
const loadingLock = ref(false);
|
||||
const sellerView = ref(false);
|
||||
const depositsValidList = ref<ValidDeposit[]>([]);
|
||||
const loadingWalletTransactions = ref(false);
|
||||
const loadingNetworkLiquidity = ref(false);
|
||||
const seller = ref<Participant>({} as Participant);
|
||||
const sellerId = ref("");
|
||||
const sellerId = ref('');
|
||||
|
||||
export function useUser() {
|
||||
// Actions become regular functions
|
||||
@@ -32,9 +31,29 @@ export function useUser() {
|
||||
selectedToken.value = token;
|
||||
};
|
||||
|
||||
const setNetworkId = (network: string | number) => {
|
||||
networkName.value = NetworkById(network) || NetworkEnum.sepolia;
|
||||
networkId.value = Number(network);
|
||||
const setNetwork = (chain: NetworkConfig) => {
|
||||
network.value = chain;
|
||||
};
|
||||
|
||||
const setNetworkById = (id: string | number) => {
|
||||
let chainId: number;
|
||||
|
||||
if (typeof id === 'string') {
|
||||
// Parse hex string or number string to number
|
||||
if (id.startsWith('0x')) {
|
||||
chainId = parseInt(id, 16);
|
||||
} else {
|
||||
chainId = parseInt(id, 10);
|
||||
}
|
||||
} else {
|
||||
chainId = id;
|
||||
}
|
||||
|
||||
// Find network by chain ID
|
||||
const chain = Object.values(Networks).find((n) => n.id === chainId);
|
||||
if (chain) {
|
||||
network.value = chain;
|
||||
}
|
||||
};
|
||||
|
||||
const setLoadingLock = (isLoading: boolean) => {
|
||||
@@ -76,8 +95,7 @@ export function useUser() {
|
||||
// State
|
||||
walletAddress,
|
||||
balance,
|
||||
networkId,
|
||||
networkName,
|
||||
network,
|
||||
selectedToken,
|
||||
loadingLock,
|
||||
sellerView,
|
||||
@@ -91,7 +109,8 @@ export function useUser() {
|
||||
setWalletAddress,
|
||||
setBalance,
|
||||
setSelectedToken,
|
||||
setNetworkId,
|
||||
setNetwork,
|
||||
setNetworkById,
|
||||
setLoadingLock,
|
||||
setSellerView,
|
||||
setDepositsValidList,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { mainnet, sepolia, rootstock, rootstockTestnet } from 'viem/chains';
|
||||
import { NetworkConfig } from '@/model/NetworkEnum';
|
||||
// TODO: import addresses from p2pix-smart-contracts deployments
|
||||
|
||||
export const Networks: { [key: string]: NetworkConfig } = {
|
||||
mainnet: {
|
||||
...mainnet,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_MAINNET_API_URL] } },
|
||||
contracts: {
|
||||
...mainnet.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_MAINNET_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_MAINNET_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_MAINNET_SUBGRAPH_URL],
|
||||
},
|
||||
rootstock: {
|
||||
...rootstock,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL] } },
|
||||
contracts: {
|
||||
...rootstock.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL],
|
||||
},
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const NetworksTestnet: { [key: string]: NetworkConfig } = {
|
||||
sepolia: {
|
||||
...sepolia,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_SEPOLIA_API_URL] } },
|
||||
contracts: {
|
||||
...sepolia.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_SEPOLIA_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_SEPOLIA_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL],
|
||||
},
|
||||
rootstockTestnet: {
|
||||
...rootstockTestnet,
|
||||
rpcUrls: { default: { http: [import.meta.env.VITE_RSK_API_URL] } },
|
||||
contracts: {
|
||||
...rootstockTestnet.contracts,
|
||||
p2pix: { address: import.meta.env.VITE_RSK_P2PIX_ADDRESS },
|
||||
},
|
||||
tokens: {
|
||||
BRZ: { address: import.meta.env.VITE_RSK_TOKEN_ADDRESS },
|
||||
},
|
||||
subgraphUrls: [import.meta.env.VITE_RSK_SUBGRAPH_URL],
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_NETWORK = Networks.mainnet;
|
||||
+6
-6
@@ -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');
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface AppVersion {
|
||||
tag: string;
|
||||
ipfsHash: string;
|
||||
releaseDate: string;
|
||||
description?: string;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface Bank {
|
||||
COMPE: string;
|
||||
ISPB: string;
|
||||
longName: string;
|
||||
}
|
||||
+2
-2
@@ -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,18 +1,10 @@
|
||||
export enum NetworkEnum {
|
||||
sepolia = 11155111,
|
||||
rootstock = 31,
|
||||
}
|
||||
|
||||
export const getNetworkSubgraphURL = (network: NetworkEnum) => {
|
||||
const networkMap: Record<NetworkEnum, string> = {
|
||||
[NetworkEnum.sepolia]: import.meta.env.VITE_SEPOLIA_SUBGRAPH_URL || "",
|
||||
[NetworkEnum.rootstock]: import.meta.env.VITE_RSK_SUBGRAPH_URL || "",
|
||||
};
|
||||
|
||||
return networkMap[network] || "";
|
||||
};
|
||||
import type { Chain, ChainContract } from 'viem';
|
||||
|
||||
export enum TokenEnum {
|
||||
BRZ = "BRZ",
|
||||
BRZ = 'BRZ',
|
||||
// BRX = 'BRX'
|
||||
}
|
||||
export type NetworkConfig = Chain & {
|
||||
tokens: Record<TokenEnum, ChainContract>;
|
||||
subgraphUrls: string[];
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { NetworkEnum } from "@/model/NetworkEnum";
|
||||
|
||||
export const NetworkById = (
|
||||
chainId: string | number
|
||||
): NetworkEnum | undefined => {
|
||||
const normalizedChainId =
|
||||
typeof chainId === "number" ? chainId : Number(chainId);
|
||||
|
||||
for (const [network, details] of Object.entries(Networks)) {
|
||||
if (Number(details.chainId) === normalizedChainId) {
|
||||
return network as unknown as NetworkEnum;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const Networks = {
|
||||
[NetworkEnum.sepolia]: {
|
||||
chainId: "0xAA36A7",
|
||||
chainName: "Sepolia Testnet",
|
||||
},
|
||||
[NetworkEnum.rootstock]: {
|
||||
chainId: "0x1F",
|
||||
chainName: "Rootstock Testnet",
|
||||
rpcUrls: ["https://public-node.testnet.rsk.co/"],
|
||||
iconUrls: [
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAPOSURBVHgBxVhNUhpBFH6vGdxp4S4LoSYnEE8gnEA4AbpMJUQ4gXgCRJK1egLxBOIJJCdwJElVllMu49Cd1z04zD/dY1H5qihmut/M93VPv59uhHdAXFaPAaEDgA2/BaeA4hq/zG+gIBAKQoyr9yshid4Jdn+2oQAYFIC4rA2zxUhgS3yrDqEAjGdIDD/YYG09aRl7L7vYd10wgPkMlcoNfdvtFhjCXJBAeyO2S5gLQuFo25bEIxjCCt8oN2Z46I+Mu4A4SbjwojQBi1+BDl5LP+JNYlhtQRmPsjjQN1ILldwY7JTXOuD9bWL/jxO8dFy7oL9TyMcIu/PeSghxlLduQUA9jwPXiAk98HLw5jFiaFfAEjRLImPR0qi7z+2VmArZ7zzqcDAS01ljCKqf7QSjxb7jKkIhTohu6rOCq64RjsNiFEo7x7ocSNMvlddhPWb0CQ6gAAw4HKZpKGFDcWhzSEG6kbQCm4dLbi9m+XlpBTHea2D31zTSNtxrAGMNdcP5FPuxfhlKdCHgASUJxcd7zUcobkAPXvkzWGyf7uVCt2M2DtkMljaHSxu92WWLAz8OjWsD+juD/4tzcpqBSh3yQrmwoNFFMZNuDB7bJRsp/hzMMQqeT+NQ96KtNEBK+SG+23XgHgUyy8FPjpPozy3M4sZwh1/nLRMOK26Mn50Z5IHjA6XkBugJSn1XHkeBbK8dJsxsl0jMEOUpm0o9+gkX+7+TI0E+0x6Hsk0ijyNYQ/4OAqWn2aF+5cLxEoRq6idqtyEPtFhp/XyMNI2p9ADFUc/iYL5h7YzEXEEyptj04mvVHxkGP4F8MS4sWDsqRr4DbyGZRiIcqCKtpRMYeTMcpVVAFewqMVPSjUkMVQTBp6BPVKeiTqN65E0qP1AvIArWC98qcQsms39oDeBEtoXFKFgLbQ76ZKiXiRH2E01UF9Go+kGDh32/LWHZAD2OQ7mGdLO4ndrqWaHZyNyD6XJUWEq6yIQqReOweCe49ivD2DNUIutjJgXpHwyUtyPbY/IMWehfBA0IZxQSQoW9rKXL+ltq0oKqYC+RB6yLKys4xEw/Idde5R02cTGOcgh1LSNnid+nihIqcN0tr48MhL89L2uoG+Dqv5Px/IwqAhkqnEi296M1OyLPqVCgdKhcuKNjlUnQL4X78cRk1E1JlMkBME1sFE0gRrRJZGs3iT44bRZP5z0wQJHzIZMMbpztN1t+FDhsMBe0YNfatimHDetgLGiZGkYapqPwYt6YIAWPDYI9fSrETfjkwwSFT2EVrV/USY+r+/GGNp2I7zoW/gdR9aOdZ/lPGgAAAABJRU5ErkJggg==",
|
||||
],
|
||||
nativeCurrency: {
|
||||
name: "tRBTC",
|
||||
symbol: "tRBTC",
|
||||
decimals: 18,
|
||||
},
|
||||
blockExplorerUrls: ["https://explorer.testnet.rootstock.io/"],
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export type Pix = {
|
||||
pixKey: string;
|
||||
merchantCity?: string;
|
||||
merchantName?: string;
|
||||
value?: number;
|
||||
transactionId?: string;
|
||||
message?: string;
|
||||
cep?: string;
|
||||
currency?: number;
|
||||
countryCode?: string;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Address } from "viem";
|
||||
import { Address } from 'viem';
|
||||
|
||||
export type UnreleasedLock = {
|
||||
lockID: bigint;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NetworkEnum } from "./NetworkEnum";
|
||||
import type { Address } from "viem";
|
||||
import type { Address } from 'viem';
|
||||
import type { NetworkConfig } from '@/model/NetworkEnum';
|
||||
|
||||
export type ValidDeposit = {
|
||||
token: Address;
|
||||
@@ -7,6 +7,6 @@ export type ValidDeposit = {
|
||||
remaining: number;
|
||||
seller: Address;
|
||||
participantID: string;
|
||||
network: NetworkEnum;
|
||||
network: NetworkConfig;
|
||||
open?: boolean;
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
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;
|
||||
blockNumber: number;
|
||||
blockTimestamp?: number;
|
||||
amount: number;
|
||||
seller: string;
|
||||
buyer: string;
|
||||
|
||||
+35
-16
@@ -1,38 +1,57 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import FaqView from "@/views/FaqView.vue";
|
||||
import ManageBidsView from "@/views/ManageBidsView.vue";
|
||||
import SellerView from "@/views/SellerView.vue";
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createWebHashHistory,
|
||||
} from 'vue-router';
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import FaqView from '@/views/FaqView.vue';
|
||||
import ManageBidsView from '@/views/ManageBidsView.vue';
|
||||
import SellerView from '@/views/SellerView.vue';
|
||||
import ExploreView from '@/views/ExploreView.vue';
|
||||
import VersionsView from '@/views/VersionsView.vue';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history:
|
||||
import.meta.env.MODE === 'production' && import.meta.env.BASE_URL === './'
|
||||
? createWebHashHistory()
|
||||
: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/:lockID",
|
||||
name: "redirect buy",
|
||||
path: '/:lockID',
|
||||
name: 'redirect buy',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/seller",
|
||||
name: "seller",
|
||||
path: '/seller',
|
||||
name: 'seller',
|
||||
component: SellerView,
|
||||
},
|
||||
{
|
||||
path: "/manage_bids",
|
||||
name: "manage bids",
|
||||
path: '/manage_bids',
|
||||
name: 'manage bids',
|
||||
component: ManageBidsView,
|
||||
},
|
||||
{
|
||||
path: "/faq",
|
||||
name: "faq",
|
||||
path: '/faq',
|
||||
name: 'faq',
|
||||
component: FaqView,
|
||||
},
|
||||
{
|
||||
path: '/explore',
|
||||
name: 'explore',
|
||||
component: ExploreView,
|
||||
},
|
||||
{
|
||||
path: '/versions',
|
||||
name: 'versions',
|
||||
component: VersionsView,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -1,5 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
const component: DefineComponent;
|
||||
export default component;
|
||||
}
|
||||
+2
-2
@@ -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';
|
||||
},
|
||||
};
|
||||
+12
-10
@@ -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,27 +49,29 @@ 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: 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: any = await response.json();
|
||||
const obj = await response.json();
|
||||
|
||||
return {
|
||||
pixTarget: obj.pixTarget,
|
||||
pixTimestamp: obj.pixTimestamp,
|
||||
signature: obj.signature,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
+9
-24
@@ -1,30 +1,15 @@
|
||||
import type { TokenEnum } from "@/model/NetworkEnum";
|
||||
|
||||
export const imagesPath = import.meta.glob<string>("@/assets/*.{png,svg}", {
|
||||
eager: true,
|
||||
query: "?url",
|
||||
import: "default",
|
||||
});
|
||||
import type { TokenEnum } from '@/model/NetworkEnum';
|
||||
import { Networks } from '@/config/networks';
|
||||
|
||||
export const getNetworkImage = (networkName: string): string => {
|
||||
try {
|
||||
const path = Object.keys(imagesPath).find((key) =>
|
||||
key.endsWith(`${networkName.toLowerCase()}.svg`)
|
||||
);
|
||||
return path ? imagesPath[path] : "";
|
||||
} catch (error) {
|
||||
console.error("Error fetching network image");
|
||||
const path = Object.keys(imagesPath).find((key) =>
|
||||
key.endsWith(`invalidIcon.svg`)
|
||||
);
|
||||
return path ? imagesPath[path] : "";
|
||||
return "";
|
||||
}
|
||||
const normalizedName = networkName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
return new URL(`../assets/networks/${normalizedName}.svg`, import.meta.url)
|
||||
.href;
|
||||
};
|
||||
|
||||
export const getTokenImage = (tokenName: TokenEnum): string => {
|
||||
const path = Object.keys(imagesPath).find((key) =>
|
||||
key.endsWith(`${tokenName.toLowerCase()}.svg`)
|
||||
);
|
||||
return path ? imagesPath[path] : "";
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
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.0.0',
|
||||
ipfsHash: 'bafybeiagfqnxnb5zdrks6dicfm7kxjdtzzzzm2ouluxgdseg2hrrotayzi',
|
||||
releaseDate: '2023-01-28',
|
||||
description: 'Initial release',
|
||||
},
|
||||
];
|
||||
|
||||
export function getLatestVersion(): AppVersion | null {
|
||||
return appVersions.length > 0 ? appVersions[0] : null;
|
||||
}
|
||||
|
||||
export function getIpfsUrl(ipfsHash: string): string {
|
||||
return `https://${ipfsHash}.ipfs.dweb.link`;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { useUser } from '@/composables/useUser';
|
||||
import { useGraphQL } from '@/composables/useGraphQL';
|
||||
import FormCard from '@/components/ui/FormCard.vue';
|
||||
import LoadingComponent from '@/components/ui/LoadingComponent.vue';
|
||||
import AnalyticsCard from '@/components/Explorer/AnalyticsCard.vue';
|
||||
import TransactionTable from '@/components/Explorer/TransactionTable.vue';
|
||||
|
||||
const user = useUser();
|
||||
const { network } = user;
|
||||
|
||||
const {
|
||||
searchAddress,
|
||||
selectedType,
|
||||
transactions,
|
||||
analytics,
|
||||
loading,
|
||||
error,
|
||||
analyticsLoading,
|
||||
fetchAllActivity,
|
||||
fetchUserActivity,
|
||||
fetchAnalytics,
|
||||
clearData,
|
||||
} = useGraphQL(network);
|
||||
|
||||
const transactionTypes = [
|
||||
{ key: 'all', label: 'Todas' },
|
||||
{ key: 'deposit', label: 'Depósitos' },
|
||||
{ key: 'lock', label: 'Bloqueios' },
|
||||
{ key: 'release', label: 'Liberações' },
|
||||
{ key: 'return', label: 'Retornos' },
|
||||
];
|
||||
|
||||
const handleTypeFilter = (type: string) => {
|
||||
selectedType.value = type;
|
||||
};
|
||||
|
||||
watch(searchAddress, async (newAddress) => {
|
||||
if (newAddress.trim()) {
|
||||
await fetchUserActivity(newAddress.trim());
|
||||
} else {
|
||||
await fetchAllActivity();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
network,
|
||||
async () => {
|
||||
clearData();
|
||||
await Promise.all([fetchAllActivity(), fetchAnalytics()]);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
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"
|
||||
:loading="analyticsLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<FormCard padding="lg" class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<!-- Search Input -->
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="searchAddress"
|
||||
type="text"
|
||||
placeholder="Buscar por endereço de carteira..."
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Type Filters -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="type in transactionTypes"
|
||||
:key="type.key"
|
||||
@click="handleTypeFilter(type.key)"
|
||||
:class="[
|
||||
'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',
|
||||
]"
|
||||
>
|
||||
{{ type.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FormCard>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-12">
|
||||
<LoadingComponent title="Carregando transações..." />
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="text-center py-12">
|
||||
<div class="text-red-600 text-lg mb-2">⚠️</div>
|
||||
<p class="text-red-600 mb-2">Erro ao carregar transações</p>
|
||||
<p class="text-gray-600 text-sm">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<TransactionTable
|
||||
:transactions="transactions"
|
||||
:network-explorer-url="network.blockExplorers?.default.url || ''"
|
||||
/>
|
||||
</FormCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
</style>
|
||||
+61
-34
@@ -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,25 +23,27 @@ 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 text-5xl max-w-[50rem]"
|
||||
<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 text-base max-w-[40rem]"
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between w-10/12 mt-20">
|
||||
<div>
|
||||
<h1 class="text-3xl text-white font-bold">Sumário</h1>
|
||||
<div class="faq-container">
|
||||
<div class="sumario-section">
|
||||
<h1 class="sumario-title">Sumário</h1>
|
||||
<h3
|
||||
:class="index == selectedSection ? 'selected-sumario' : 'sumario'"
|
||||
v-for="(f, index) in faq"
|
||||
@@ -52,7 +54,7 @@ const openItem = (index: number) => {
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="w-4/6">
|
||||
<div class="content-section">
|
||||
<div
|
||||
v-for="(item, index) in faq[selectedSection].items"
|
||||
v-bind:key="item.title"
|
||||
@@ -61,16 +63,16 @@ const openItem = (index: number) => {
|
||||
<img
|
||||
alt="plus"
|
||||
src="@/assets/plus.svg?url"
|
||||
class="mr-3"
|
||||
class="icon"
|
||||
v-if="!item.isOpen"
|
||||
/>
|
||||
<img
|
||||
alt="plus"
|
||||
alt="minus"
|
||||
src="@/assets/minus.svg?url"
|
||||
class="mr-3"
|
||||
class="icon"
|
||||
v-if="item.isOpen"
|
||||
/>
|
||||
<h4 class="text-white">{{ item.title }}</h4>
|
||||
<h4 class="item-title">{{ item.title }}</h4>
|
||||
</div>
|
||||
<div class="content" v-if="item.isOpen" v-html="item.content"></div>
|
||||
<div class="hr"></div>
|
||||
@@ -81,23 +83,53 @@ 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;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4 mb-8 sm:mb-12;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.faq-container {
|
||||
@apply flex flex-col sm:flex-row sm:justify-between w-full sm:w-10/12 max-w-7xl gap-8 sm:gap-0 mt-8 sm:mt-20;
|
||||
}
|
||||
|
||||
.sumario-section {
|
||||
@apply w-full sm:w-auto sm:min-w-[200px];
|
||||
}
|
||||
|
||||
.sumario-title {
|
||||
@apply text-xl sm:text-3xl text-white font-bold mb-4 sm:mb-0;
|
||||
}
|
||||
|
||||
.sumario {
|
||||
margin-top: 24px;
|
||||
cursor: pointer;
|
||||
@apply text-white mt-6 sm:mt-6 cursor-pointer text-sm sm:text-base;
|
||||
}
|
||||
|
||||
.selected-sumario {
|
||||
font-weight: bolder;
|
||||
margin-top: 24px;
|
||||
cursor: pointer;
|
||||
@apply text-white font-bold mt-6 sm:mt-6 cursor-pointer text-sm sm:text-base;
|
||||
}
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16;
|
||||
|
||||
.content-section {
|
||||
@apply w-full sm:w-4/6;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply mr-3 flex-shrink-0 w-5 h-5 sm:w-6 sm:h-6;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
@apply text-white font-semibold text-sm sm:text-base;
|
||||
}
|
||||
|
||||
div.content {
|
||||
padding-top: 24px;
|
||||
color: white;
|
||||
@apply pt-6 text-white text-sm sm:text-base;
|
||||
}
|
||||
|
||||
.content :deep(ul) {
|
||||
@@ -112,9 +144,12 @@ div.content {
|
||||
@apply list-disc m-1 p-1;
|
||||
}
|
||||
|
||||
.content :deep(p) {
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
.hr {
|
||||
border: 1px solid #1f2937;
|
||||
margin: 24px 0;
|
||||
@apply border border-gray-700 my-6;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -125,12 +160,4 @@ h2,
|
||||
h4 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
</style>
|
||||
+28
-29
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import SearchComponent from "@/components/SearchComponent.vue";
|
||||
import LoadingComponent from "@/components/LoadingComponent/LoadingComponent.vue";
|
||||
import BuyConfirmedComponent from "@/components/BuyConfirmedComponent/BuyConfirmedComponent.vue";
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import QrCodeComponent from "@/components/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/CustomAlert/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,
|
||||
@@ -24,12 +24,12 @@ const user = useUser();
|
||||
user.setSellerView(false);
|
||||
|
||||
// States
|
||||
const { loadingLock, walletAddress, networkName } = user;
|
||||
const { loadingLock, walletAddress, network } = user;
|
||||
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;
|
||||
@@ -59,27 +59,26 @@ const confirmBuyClick = async (
|
||||
}
|
||||
};
|
||||
|
||||
const releaseTransaction = async ({
|
||||
pixTarget,
|
||||
signature,
|
||||
}: {
|
||||
pixTarget: string;
|
||||
signature: string;
|
||||
const releaseTransaction = async (params: {
|
||||
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), pixTarget, signature);
|
||||
await release.wait();
|
||||
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);
|
||||
@@ -107,7 +106,7 @@ if (paramLockID) {
|
||||
await checkForUnreleasedLocks();
|
||||
});
|
||||
|
||||
watch(networkName, async () => {
|
||||
watch(network, async () => {
|
||||
if (walletAddress.value) await checkForUnreleasedLocks();
|
||||
});
|
||||
}
|
||||
@@ -115,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,22 +1,22 @@
|
||||
<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/LoadingComponent/LoadingComponent.vue";
|
||||
import CustomAlert from "@/components/CustomAlert/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, networkName, selectedToken } = user;
|
||||
const { walletAddress, network, selectedToken } = user;
|
||||
const loadingWithdraw = ref<boolean>(false);
|
||||
const showAlert = ref<boolean>(false);
|
||||
|
||||
@@ -29,17 +29,20 @@ const callWithdraw = async (amount: string) => {
|
||||
loadingWithdraw.value = true;
|
||||
let withdraw;
|
||||
try {
|
||||
withdraw = await withdrawDeposit(amount, selectedToken.value);
|
||||
withdraw = await withdrawDeposit(
|
||||
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;
|
||||
}
|
||||
@@ -49,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);
|
||||
@@ -70,7 +73,7 @@ const getWalletTransactions = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
if (!walletAddress.value) {
|
||||
router.push({ name: "home" });
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
await getWalletTransactions();
|
||||
});
|
||||
@@ -79,7 +82,7 @@ watch(walletAddress, async () => {
|
||||
await getWalletTransactions();
|
||||
});
|
||||
|
||||
watch(networkName, async () => {
|
||||
watch(network, async () => {
|
||||
await getWalletTransactions();
|
||||
});
|
||||
</script>
|
||||
@@ -116,6 +119,7 @@ watch(networkName, 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/LoadingComponent/LoadingComponent.vue";
|
||||
import { useUser } from "@/composables/useUser";
|
||||
import { approveTokens, addDeposit } from "@/blockchain/sellerMethods";
|
||||
import CustomAlert from "@/components/CustomAlert/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,
|
||||
@@ -69,7 +69,7 @@ const sendNetwork = async () => {
|
||||
/>
|
||||
<div v-if="flowStep == Step.Network">
|
||||
<SendNetwork
|
||||
:sellerId="user.sellerId.value"
|
||||
:sellerId="String(user.sellerId.value)"
|
||||
:offer="Number(user.seller.value.offer)"
|
||||
:selected-token="user.selectedToken.value"
|
||||
v-if="!loading"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
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(),
|
||||
);
|
||||
latestVersion.value = getLatestVersion();
|
||||
});
|
||||
|
||||
const openIpfsVersion = (ipfsHash: string) => {
|
||||
const url = getIpfsUrl(ipfsHash);
|
||||
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',
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="text-container">
|
||||
<span class="text font-extrabold text-5xl max-w-[50rem]">
|
||||
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.
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="versions-container">
|
||||
<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">
|
||||
Atual
|
||||
</span>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<p class="version-date">
|
||||
<span class="label">Data de lançamento:</span>
|
||||
{{ formatDate(version.releaseDate) }}
|
||||
</p>
|
||||
<p v-if="version.description" class="version-description">
|
||||
{{ version.description }}
|
||||
</p>
|
||||
<div class="version-actions">
|
||||
<button
|
||||
v-if="currentVersion !== version.tag"
|
||||
@click="openIpfsVersion(version.ipfsHash)"
|
||||
class="ipfs-button"
|
||||
>
|
||||
Abrir no IPFS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="versions.length === 0" class="empty-state">
|
||||
<p class="text-gray-400">Nenhuma versão cadastrada ainda.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
.page {
|
||||
@apply flex flex-col items-center justify-center w-full mt-16 px-4;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@apply flex flex-col items-center justify-center gap-4 mb-12;
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply text-white text-center;
|
||||
}
|
||||
|
||||
.versions-container {
|
||||
@apply w-full max-w-4xl space-y-4;
|
||||
}
|
||||
|
||||
.version-card {
|
||||
@apply bg-gray-800 rounded-lg p-6 border border-gray-700 hover:border-amber-500 transition-colors;
|
||||
}
|
||||
|
||||
.version-header {
|
||||
@apply flex items-center justify-between mb-4;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@apply text-2xl font-bold text-white;
|
||||
}
|
||||
|
||||
.current-badge {
|
||||
@apply px-3 py-1 bg-amber-500 text-gray-900 text-xs font-semibold rounded-full;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
@apply space-y-3;
|
||||
}
|
||||
|
||||
.version-date {
|
||||
@apply text-gray-300 text-sm;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply text-gray-400 font-medium;
|
||||
}
|
||||
|
||||
.version-description {
|
||||
@apply text-gray-300 text-sm;
|
||||
}
|
||||
|
||||
.version-actions {
|
||||
@apply flex justify-center sm:justify-start items-center gap-4 pt-2 w-full;
|
||||
}
|
||||
|
||||
.ipfs-button {
|
||||
@apply px-4 py-2 bg-amber-500 text-gray-900 font-semibold rounded hover:bg-amber-600 transition-colors text-sm;
|
||||
}
|
||||
|
||||
.ipfs-hash {
|
||||
@apply text-gray-400 text-xs font-mono break-all;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@apply text-center py-12;
|
||||
}
|
||||
</style>
|
||||
+2
-2
@@ -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"
|
||||
]
|
||||
|
||||
+2
-2
@@ -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
-12
@@ -1,32 +1,58 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
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 sh(cmd: string): string {
|
||||
try {
|
||||
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: './',
|
||||
build: {
|
||||
target: "esnext",
|
||||
target: 'esnext',
|
||||
},
|
||||
define: {
|
||||
__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),
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
+6
-5
@@ -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