Compare commits

...

7 Commits

Author SHA1 Message Date
hueso
3227e3209c fixed type check errors 2025-03-04 18:46:10 -03:00
hueso
54cff28ba0 removed residual mumbai tests 2025-03-04 18:38:14 -03:00
hueso
d5f9c8f6fa updated to typescript v5 (for compatibility with web3-onboard) 2025-03-04 18:35:58 -03:00
Filipe Soccol
0f17a67e00 Correct bugs and adjust Buyer form. 2024-12-03 16:18:10 -03:00
Filipe Soccol
c90f468d3c Finished refactoring for Sellet flow. 2024-12-02 12:17:47 -03:00
Filipe Soccol
c4dae86b5f Cleaned code and prepared for API communications. 2024-12-01 11:50:08 -03:00
Filipe Soccol
92f6cb4d35 Added Sellet from with all required fields. 2024-11-27 09:55:38 -03:00
23 changed files with 2864 additions and 523 deletions

View File

@ -59,9 +59,9 @@
"postcss": "^8.4.18",
"prettier": "^2.7.1",
"tailwindcss": "^3.2.1",
"typescript": "~4.7.4",
"typescript": "~5.8.2",
"vite": "^3.1.8",
"vitest": "^0.28.1",
"vue-tsc": "^1.0.8"
"vue-tsc": "^2.2.8"
}
}

View File

@ -19,3 +19,14 @@
opacity: 0;
transform: translateY(15px);
}
.resize-enter-active,
.resize-leave-active {
max-height: 100px;
transition: all 0.3s ease;
}
.resize-enter-from,
.resize-leave-to {
max-height: 0px;
}

View File

@ -32,14 +32,6 @@ describe("addresses.ts functions", () => {
);
});
it("getTokenAddress Polygon", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.polygon);
expect(getTokenAddress(TokenEnum.BRZ)).toBe(
"0xC86042E9F2977C62Da8c9dDF7F9c40fde4796A29"
);
});
it("getTokenAddress Rootstock", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.rootstock);
@ -48,7 +40,6 @@ describe("addresses.ts functions", () => {
);
});
it("getTokenAddress Default", () => {
expect(getTokenAddress(TokenEnum.BRZ)).toBe(
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
@ -63,14 +54,6 @@ describe("addresses.ts functions", () => {
);
});
it("getP2PixAddress Polygon", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.polygon);
expect(getP2PixAddress()).toBe(
"0x4A2886EAEc931e04297ed336Cc55c4eb7C75BA00"
);
});
it("getP2PixAddress Rootstock", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.rootstock);
@ -91,12 +74,6 @@ describe("addresses.ts functions", () => {
expect(getProviderUrl()).toBe(import.meta.env.VITE_GOERLI_API_URL);
});
it("getProviderUrl Polygon", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.polygon);
expect(getProviderUrl()).toBe(import.meta.env.VITE_MUMBAI_API_URL);
});
it("getProviderUrl Rootstock", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.rootstock);
@ -110,12 +87,12 @@ describe("addresses.ts functions", () => {
it("isPossibleNetwork Returns", () => {
const etherStore = useEtherStore();
etherStore.setNetworkId(NetworkEnum.sepolia);
expect(isPossibleNetwork(0x5)).toBe(true);
expect(isPossibleNetwork(5)).toBe(true);
expect(isPossibleNetwork(0x13881)).toBe(true);
expect(isPossibleNetwork(80001)).toBe(true);
expect(isPossibleNetwork(0x5 as NetworkEnum)).toBe(true);
expect(isPossibleNetwork(5 as NetworkEnum)).toBe(true);
expect(isPossibleNetwork(0x13881 as NetworkEnum)).toBe(true);
expect(isPossibleNetwork(80001 as NetworkEnum)).toBe(true);
expect(isPossibleNetwork(NaN)).toBe(false);
expect(isPossibleNetwork(0x55)).toBe(false);
expect(isPossibleNetwork(NaN as NetworkEnum)).toBe(false);
expect(isPossibleNetwork(0x55 as NetworkEnum)).toBe(false);
});
});

View File

@ -1,28 +1,22 @@
import { getContract, getProvider } from "./provider";
import { getP2PixAddress, getTokenAddress } from "./addresses";
import { encodeBytes32String, Signature, Contract, parseEther } from "ethers";
import p2pix from "@/utils/smart_contract_files/P2PIX.json";
import {
solidityPackedKeccak256,
encodeBytes32String,
Signature,
Contract,
getBytes,
Wallet,
parseEther,
} from "ethers";
import type { TokenEnum } from "@/model/NetworkEnum";
import { createSolicitation } from "../utils/bbPay";
import type { Offer } from "../utils/bbPay";
const addLock = async (
seller: string,
sellerId: string,
token: string,
amount: number
): Promise<string> => {
const p2pContract = await getContract();
const lock = await p2pContract.lock(
seller,
sellerId,
token,
parseEther(String(amount)), // BigNumber
[],
@ -32,26 +26,29 @@ const addLock = async (
const lock_rec = await lock.wait();
const [t] = lock_rec.events;
return String(t.args.lockID);
const offer: Offer = {
amount,
lockId: String(t.args.lockID),
sellerId: sellerId,
};
const solicitation = await createSolicitation(offer);
return;
};
const releaseLock = async (
pixKey: string,
amount: number,
e2eId: string,
lockId: string
): Promise<any> => {
const mockBacenSigner = new Wallet(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
);
const releaseLock = async (solicitation: any): Promise<any> => {
// const mockBacenSigner = new Wallet(
// "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
// );
const messageToSign = solidityPackedKeccak256(
["bytes32", "uint256", "bytes32"],
[pixKey, parseEther(String(amount)), encodeBytes32String(e2eId)]
);
// const messageToSign = solidityPackedKeccak256(
// ["bytes32", "uint256", "bytes32"],
// [sellerId, parseEther(String(amount)), encodeBytes32String(signature)]
// );
// const messageHashBytes = getBytes(messageToSign);
// const flatSig = await mockBacenSigner.signMessage(messageHashBytes);
const messageHashBytes = getBytes(messageToSign);
const flatSig = await mockBacenSigner.signMessage(messageHashBytes);
const provider = getProvider();
const sig = Signature.from(flatSig);

View File

@ -5,12 +5,15 @@ import { encodeBytes32String, Contract, parseEther } from "ethers";
import mockToken from "../utils/smart_contract_files/MockToken.json";
import { useEtherStore } from "@/store/ether";
import { createParticipant } from "@/utils/bbPay";
import type { Participant } from "@/utils/bbPay";
const approveTokens = async (tokenQty: string): Promise<any> => {
const approveTokens = async (participant: Participant): Promise<any> => {
const provider = getProvider();
const signer = await provider?.getSigner();
const etherStore = useEtherStore();
etherStore.setSeller(participant);
const tokenContract = new Contract(
getTokenAddress(etherStore.selectedToken),
mockToken.abi,
@ -22,11 +25,11 @@ const approveTokens = async (tokenQty: string): Promise<any> => {
await signer?.getAddress(),
getP2PixAddress()
);
if (approved < parseEther(tokenQty)) {
if (approved < parseEther(participant.offer)) {
// Approve tokens
const apprv = await tokenContract.approve(
getP2PixAddress(),
parseEther(tokenQty)
parseEther(participant.offer)
);
await apprv.wait();
return true;
@ -34,15 +37,18 @@ const approveTokens = async (tokenQty: string): Promise<any> => {
return true;
};
const addDeposit = async (tokenQty: string, pixKey: string): Promise<any> => {
const addDeposit = async (): Promise<any> => {
const p2pContract = await getContract();
const etherStore = useEtherStore();
const sellerId = await createParticipant(etherStore.seller);
etherStore.setSellerId(sellerId.id);
const deposit = await p2pContract.deposit(
pixKey,
sellerId,
encodeBytes32String(""),
getTokenAddress(etherStore.selectedToken),
parseEther(tokenQty),
parseEther(etherStore.seller.offer),
true
);

View File

@ -26,8 +26,8 @@ export const updateWalletStatus = async (): Promise<void> => {
const provider = await getProvider();
const signer = await provider?.getSigner();
const { chainId } = await provider?.getNetwork();
const network = await provider?.getNetwork();
const chainId = network?.chainId;
if (!isPossibleNetwork(Number(chainId))) {
window.alert("Invalid chain!:" + chainId);
return;

View File

@ -1,75 +1,25 @@
<script setup lang="ts">
import { pix } from "@/utils/QrCodePix";
import { onMounted, onUnmounted, ref } from "vue";
import { debounce } from "@/utils/debounce";
import CustomButton from "@/components/CustomButton/CustomButton.vue";
import CustomModal from "@/components//CustomModal/CustomModal.vue";
import api from "@/services/index";
import QRCode from "qrcode";
// props and store references
const props = defineProps({
pixTarget: String,
tokenValue: Number,
sellerId: String,
amount: Number,
qrcode: String,
});
const windowSize = ref<number>(window.innerWidth);
const qrCode = ref<string>("");
const qrCodePayload = ref<string>("");
const isPixValid = ref<boolean>(false);
const isCodeInputEmpty = ref<boolean>(true);
const showWarnModal = ref<boolean>(true);
const e2eId = ref<string>("");
const releaseSignature = ref<string>("");
// Emits
const emit = defineEmits(["pixValidated"]);
const pixQrCode = pix({
pixKey: props.pixTarget ?? "",
value: props.tokenValue,
});
pixQrCode.base64QrCode().then((code: string) => {
qrCode.value = code;
});
qrCodePayload.value = pixQrCode.payload();
const handleInputEvent = async (event: any): Promise<void> => {
const { value } = event.target;
e2eId.value = value;
await validatePix();
};
const validatePix = async (): Promise<void> => {
if (e2eId.value == "") {
isPixValid.value = false;
isCodeInputEmpty.value = true;
return;
}
const sellerPixKey = props.pixTarget;
const transactionValue = props.tokenValue;
if (sellerPixKey && transactionValue) {
const body_req = {
e2e_id: e2eId.value,
pix_key: sellerPixKey,
pix_value: transactionValue,
};
isCodeInputEmpty.value = false;
try {
await api.post("validate_pix", body_req);
isPixValid.value = true;
} catch (error) {
isPixValid.value = false;
}
} else {
isCodeInputEmpty.value = false;
isPixValid.value = false;
}
};
onMounted(() => {
window.addEventListener(
"resize",
@ -94,7 +44,7 @@ onUnmounted(() => {
</span>
<span class="text font-medium lg:text-md text-sm max-w-[28rem]">
Após realizar o Pix no banco de sua preferência, clique no botão abaixo
para gerar a assinatura para liberação dos tokens.
para liberação dos tokens.
</span>
</div>
<div class="main-container max-w-md text-black">
@ -105,7 +55,7 @@ onUnmounted(() => {
<span class="text-center font-bold">Código pix</span>
<div class="break-words w-4/5">
<span class="text-center text-xs">
{{ qrCodePayload }}
{{ qrCode }}
</span>
</div>
<img
@ -115,17 +65,11 @@ onUnmounted(() => {
height="16"
class="pt-2 lg:mb-5 cursor-pointer"
/>
<span class="text-xs text-start hidden sm:inline-block">
<strong>ATENÇÃO!</strong> A transação será processada após inserir
o código de autenticação. Caso contrário não conseguiremos comprovar o
seu depósito e não será possível transferir os tokens para sua
carteira. Confira aqui como encontrar o código no comprovante.
</span>
</div>
<CustomButton
:is-disabled="isPixValid == false"
:text="'Enviar para a rede'"
@button-clicked="emit('pixValidated', e2eId)"
@button-clicked="emit('pixValidated', releaseSignature)"
/>
</div>
<CustomModal

View File

@ -34,6 +34,7 @@ const tokenValue = ref<number>(0);
const enableConfirmButton = ref<boolean>(false);
const hasLiquidity = ref<boolean>(true);
const validDecimals = ref<boolean>(true);
const identification = ref<string>("");
const selectedDeposits = ref<ValidDeposit[]>();
import ChevronDown from "@/assets/chevronDown.svg";
@ -104,9 +105,17 @@ const verifyLiquidity = (): void => {
};
const enableOrDisableConfirmButton = (): void => {
enableConfirmButton.value =
!!selectedDeposits.value &&
!!selectedDeposits.value.find((d) => d.network === networkName.value);
if (!selectedDeposits.value) {
enableConfirmButton.value = false;
return;
}
if (!selectedDeposits.value.find((d) => d.network === networkName.value)) {
enableConfirmButton.value = false;
return;
}
enableConfirmButton.value = true;
};
watch(networkName, (): void => {
@ -117,6 +126,18 @@ watch(networkName, (): void => {
watch(walletAddress, (): void => {
verifyLiquidity();
});
// Add form submission handler
const handleSubmit = async (e: Event): Promise<void> => {
e.preventDefault();
if (!enableConfirmButton.value) return;
if (walletAddress.value) {
await emitConfirmButton();
} else {
await connectAccount();
}
};
</script>
<template>
@ -132,7 +153,7 @@ watch(walletAddress, (): void => {
tokens após realizar o Pix</span
>
</div>
<div class="main-container">
<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"
@ -140,14 +161,16 @@ watch(walletAddress, (): void => {
<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"
v-bind:class="{
'font-semibold': tokenValue != undefined,
'text-xl': tokenValue != undefined,
}"
@input="debounce(handleInputEvent, 500)($event)"
placeholder="0 "
placeholder="0"
step=".01"
required
/>
<div class="relative overflow-visible">
<button
@ -260,18 +283,33 @@ watch(walletAddress, (): void => {
>
</div>
</div>
<CustomButton
v-if="!walletAddress"
:text="'Conectar carteira'"
@buttonClicked="connectAccount()"
/>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<input
type="text"
v-model="identification"
maxlength="14"
:pattern="'^\\d{11}$|^\\d{14}$'"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
placeholder="Digite seu CPF ou CNPJ (somente números)"
required
/>
</div>
<!-- Action buttons -->
<CustomButton
v-if="walletAddress"
:text="'Confirmar compra'"
:is-disabled="!enableConfirmButton"
@buttonClicked="emitConfirmButton()"
type="submit"
text="Confirmar Oferta"
/>
</div>
<CustomButton
v-else
text="Conectar carteira"
@buttonClicked="connectAccount()"
/>
</form>
</div>
</template>
@ -306,4 +344,10 @@ input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
.custom-button {
@apply w-full py-3 px-6 rounded-lg font-semibold text-white bg-indigo-600
hover:bg-indigo-700 disabled:bg-gray-400 disabled:cursor-not-allowed
transition-colors duration-200;
}
</style>

View File

@ -0,0 +1,360 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import CustomButton from "../CustomButton/CustomButton.vue";
import { pixFormatValidation, postProcessKey } from "@/utils/pixKeyFormat";
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { TokenEnum } from "@/model/NetworkEnum";
import { getTokenImage } from "@/utils/imagesPath";
import { useOnboard } from "@web3-onboard/vue";
import ChevronDown from "@/assets/chevron.svg";
// Import the bank list
import bankList from "@/utils/files/isbpList.json";
import type { Participant } from "@/utils/bbPay";
// Define Bank interface
interface Bank {
ISPB: string;
longName: string;
}
// html references
const tokenDropdownRef = ref<any>(null);
const formRef = ref<HTMLFormElement | null>(null);
// Reactive state
const etherStore = useEtherStore();
const { walletAddress, selectedToken } = storeToRefs(etherStore);
const fullName = 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 errors = ref<{ [key: string]: string }>({});
// Bank selection
const bankSearchQuery = ref<string>("");
const showBankList = ref<boolean>(false);
const selectedBank = ref<Bank | null>(null);
const filteredBanks = computed(() => {
if (!bankSearchQuery.value) return [];
return bankList
.filter((bank) =>
bank.longName.toLowerCase().includes(bankSearchQuery.value.toLowerCase())
)
.slice(0, 5);
});
const handleBankSelect = (bank: Bank) => {
selectedBank.value = bank;
bankSearchQuery.value = bank.longName;
showBankList.value = false;
};
// Emits
const emit = defineEmits(["approveTokens"]);
// Methods
const connectAccount = async (): Promise<void> => {
const { connectWallet } = useOnboard();
await connectWallet();
};
const handleSubmit = (e: Event): void => {
e.preventDefault();
const processedIdentification = postProcessKey(identification.value);
const data: Participant = {
offer: offer.value,
fullName: fullName.value,
identification: processedIdentification,
bankIspb: selectedBank.value?.ISPB,
accountType: accountType.value,
account: account.value,
branch: branch.value,
savingsVariation: savingsVariation.value || "",
};
emit("approveTokens", data);
};
// Token selection
const openTokenSelection = (): void => {
selectTokenToggle.value = true;
};
const handleSelectedToken = (token: TokenEnum): void => {
etherStore.setSelectedToken(token);
selectTokenToggle.value = false;
};
</script>
<template>
<div class="page w-full">
<div class="text-container">
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>
Venda cripto e receba em Pix
</span>
<span
class="text font-medium sm:text-base text-xs sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide"
>
Digite sua oferta, informe a chave Pix, selecione a rede, aprove o envio
da transação e confirme sua oferta.
</span>
</div>
<form ref="formRef" @submit="handleSubmit" class="main-container">
<!-- Offer input -->
<div
class="flex justify-between items-center w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10 gap-4"
>
<input
type="number"
v-model="offer"
class="border-none outline-none text-gray-900 sm:w-fit w-3/4 flex-grow"
:class="{
'!font-medium': offer !== undefined && offer !== '',
'text-xl': offer !== undefined && offer !== '',
}"
min="0.01"
max="999999999.99"
pattern="\d+(\.\d{0,2})?"
placeholder="Digite sua oferta (mínimo R$0,01)"
step=".01"
required
/>
<div class="relative overflow-visible">
<button
ref="tokenDropdownRef"
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
@click="openTokenSelection()"
>
<img
alt="Token image"
class="sm:w-fit w-4"
:src="getTokenImage(selectedToken)"
/>
<span
class="text-gray-900 sm:text-lg text-md font-medium"
id="token"
>
{{ selectedToken }}
</span>
<ChevronDown
class="text-gray-900 pr-4 sm:pr-0 transition-all duration-500 ease-in-out"
:class="{ 'scale-y-[-1]': selectTokenToggle }"
alt="Chevron Down"
/>
</button>
<transition name="dropdown">
<div
v-if="selectTokenToggle"
class="mt-2 text-gray-900 absolute right-0 z-50 w-full min-w-max"
>
<div
class="bg-white rounded-xl z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
>
<div
v-for="token in TokenEnum"
:key="token"
class="flex menu-button gap-2 px-4 cursor-pointer hover:bg-gray-300 transition-colors"
@click="handleSelectedToken(token)"
>
<img
:alt="token + ' logo'"
width="20"
height="20"
:src="getTokenImage(token)"
/>
<span
class="text-gray-900 py-4 text-end font-semibold text-sm"
>
{{ token }}
</span>
</div>
</div>
</div>
</transition>
</div>
</div>
<!-- Full name input -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<input
type="text"
v-model="fullName"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
maxlength="60"
:class="{ 'text-xl font-medium': fullName }"
placeholder="Digite seu nome completo"
required
/>
</div>
<!-- CPF or CNPJ input -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<input
type="text"
v-model="identification"
maxlength="14"
:pattern="'^\\d{11}$|^\\d{14}$'"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': identification }"
placeholder="Digite seu CPF ou CNPJ (somente números)"
required
/>
</div>
<!-- Bank selection -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<div class="relative">
<input
type="text"
v-model="bankSearchQuery"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': bankSearchQuery }"
placeholder="Buscar banco"
@focus="showBankList = true"
required
/>
<div
v-if="showBankList && filteredBanks.length > 0"
class="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-50 max-h-60 overflow-y-auto"
>
<div
v-for="bank in filteredBanks"
:key="bank.ISPB"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer transition-colors"
@click="handleBankSelect(bank)"
>
<div class="text-sm font-medium text-gray-900">
{{ bank.longName }}
</div>
<div class="text-xs text-gray-500">ISPB: {{ bank.ISPB }}</div>
</div>
</div>
</div>
<span v-if="errors.bank" class="text-red-500 text-sm mt-2">{{
errors.bank
}}</span>
</div>
<!-- Account and Branch inputs -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<div class="flex gap-4">
<div class="flex-1">
<input
type="text"
v-model="account"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': account }"
placeholder="Número da conta"
required
/>
</div>
<div class="flex-1">
<input
type="text"
v-model="branch"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
:class="{ 'text-xl font-medium': branch }"
placeholder="Agência"
required
/>
</div>
</div>
</div>
<!-- Account Type Selection -->
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
>
<div class="flex gap-4">
<div class="flex-1">
<select
v-model="accountType"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full"
required
>
<option value="" disabled selected>Tipo de conta</option>
<option value="1">Conta Corrente</option>
<option value="2">Conta Poupança</option>
<option value="3">Conta Salário</option>
<option value="4">Conta Pré-Paga</option>
</select>
</div>
</div>
</div>
<!-- Savings Account Variation -->
<Transition name="resize">
<input
v-if="accountType === '2'"
type="text"
v-model="savingsVariation"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-full bg-white sm:px-10 px-6 py-4 rounded-lg border-y-10"
:class="{ 'text-xl font-medium': savingsVariation }"
placeholder="Variação da poupança"
required
/>
</Transition>
<!-- Action buttons -->
<CustomButton v-if="walletAddress" type="submit" text="Aprovar tokens" />
<CustomButton
v-else
text="Conectar carteira"
@buttonClicked="connectAccount()"
/>
</form>
</div>
</template>
<style scoped>
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input {
@apply sm:text-lg text-sm;
}
</style>

View File

@ -6,7 +6,7 @@ const emit = defineEmits(["sendNetwork"]);
// props and store references
const props = defineProps({
pixKey: String,
sellerId: String,
offer: Number,
selectedToken: String,
});
@ -39,7 +39,7 @@ const props = defineProps({
<div class="my-3">
<p>Chave Pix</p>
<p class="text-xl text-gray-900 break-words">
{{ props.pixKey }}
{{ props.sellerId }}
</p>
</div>
<div class="mb-5">

View File

@ -1,263 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import CustomButton from "../CustomButton/CustomButton.vue";
import { debounce } from "@/utils/debounce";
import { decimalCount } from "@/utils/decimalCount";
import { pixFormatValidation, postProcessKey } from "@/utils/pixKeyFormat";
import { useEtherStore } from "@/store/ether";
import { storeToRefs } from "pinia";
import { connectProvider } from "@/blockchain/provider";
import { TokenEnum } from "@/model/NetworkEnum";
import { getTokenImage } from "@/utils/imagesPath";
import { onClickOutside } from "@vueuse/core";
import { useOnboard } from "@web3-onboard/vue";
import ChevronDown from "@/assets/chevron.svg";
// html references
const tokenDropdownRef = ref<any>(null);
// Reactive state
const etherStore = useEtherStore();
const { walletAddress, selectedToken } = storeToRefs(etherStore);
const offer = ref<string>("");
const pixKey = ref<string>("");
const selectTokenToggle = ref<boolean>(false);
const enableSelectButton = ref<boolean>(false);
const hasLiquidity = ref<boolean>(true);
const validDecimals = ref<boolean>(true);
const validPixFormat = ref<boolean>(true);
// Emits
const emit = defineEmits(["approveTokens"]);
// Blockchain methods
const connectAccount = async (): Promise<void> => {
const { connectWallet } = useOnboard();
await connectWallet();
};
// Debounce methods
const handleInputEvent = (event: any): void => {
const { value } = event.target;
offer.value = value;
if (decimalCount(offer.value) > 2) {
validDecimals.value = false;
enableSelectButton.value = false;
return;
}
validDecimals.value = true;
};
const handlePixKeyInputEvent = (event: any): void => {
const { value } = event.target;
pixKey.value = value;
if (pixFormatValidation(pixKey.value)) {
validPixFormat.value = true;
enableSelectButton.value = true;
return;
}
enableSelectButton.value = false;
validPixFormat.value = false;
};
const openTokenSelection = (): void => {
selectTokenToggle.value = true;
};
onClickOutside(tokenDropdownRef, () => {
selectTokenToggle.value = false;
});
const handleSelectedToken = (token: TokenEnum): void => {
etherStore.setSelectedToken(token);
selectTokenToggle.value = false;
};
const handleSellClick = async (
offer: string,
pixKey: string
): Promise<void> => {
const postProcessedPixKey = postProcessKey(pixKey);
emit("approveTokens", { offer, postProcessedPixKey });
};
</script>
<template>
<div class="page w-full">
<div class="text-container">
<span
class="text font-extrabold sm:text-5xl text-3xl sm:max-w-[29rem] max-w-[20rem]"
>Venda cripto e receba em Pix</span
>
<span
class="text font-medium sm:text-base text-xs sm:max-w-[28rem] max-w-[30rem] sm:tracking-normal tracking-wide"
>Digite sua oferta, informe a chave Pix, selecione a rede, aprove o
envio da transação e confirme sua oferta.</span
>
</div>
<div class="main-container">
<div class="backdrop-blur -z-10 w-full h-full"></div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-5 rounded-lg border-y-10"
>
<div class="flex justify-between items-center">
<input
type="number"
v-model="offer"
class="border-none outline-none text-gray-900 sm:w-fit w-3/4"
:class="{
'!font-medium': offer !== undefined && offer !== '',
'text-xl': offer !== undefined && offer !== '',
}"
@input="debounce(handleInputEvent, 500)($event)"
placeholder="Digite sua oferta"
step=".01"
/>
<div class="relative overflow-visible">
<button
ref="tokenDropdownRef"
class="flex flex-row items-center p-2 bg-gray-300 hover:bg-gray-200 focus:outline-indigo-800 focus:outline-2 rounded-3xl min-w-fit gap-2 transition-colors"
@click="openTokenSelection()"
>
<img
alt="Token image"
class="sm:w-fit w-4"
:src="getTokenImage(selectedToken)"
/>
<span
class="text-gray-900 sm:text-lg text-md font-medium"
id="token"
>
{{ selectedToken }}
</span>
<ChevronDown
class="text-gray-900 pr-4 sm:pr-0 transition-all duration-500 ease-in-out"
:class="{ 'scale-y-[-1]': selectTokenToggle }"
alt="Chevron Down"
/>
</button>
<transition name="dropdown">
<div
v-if="selectTokenToggle"
class="mt-2 text-gray-900 absolute right-0 z-50 w-full min-w-max"
>
<div
class="bg-white rounded-xl z-10 border border-gray-300 drop-shadow-md shadow-md overflow-clip"
>
<div
v-for="token in TokenEnum"
:key="token"
class="flex menu-button gap-2 px-4 cursor-pointer hover:bg-gray-300 transition-colors"
@click="handleSelectedToken(token)"
>
<img
:alt="token + ' logo'"
width="20"
height="20"
:src="getTokenImage(token)"
/>
<span
class="text-gray-900 py-4 text-end font-semibold text-sm"
>
{{ token }}
</span>
</div>
</div>
</div>
</transition>
</div>
</div>
<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 liquidez nas redes para sua demanda</span
>
</div>
</div>
<div
class="flex flex-col w-full bg-white sm:px-10 px-6 py-8 rounded-lg border-y-10"
>
<div class="flex justify-between w-full items-center">
<input
@input="debounce(handlePixKeyInputEvent, 500)($event)"
type="text"
v-model="pixKey"
class="border-none outline-none sm:text-lg text-sm text-gray-900 w-fit"
:class="{
'!font-medium': pixKey !== undefined && pixKey !== '',
'text-xl': pixKey !== undefined && pixKey !== '',
}"
placeholder="Digite a chave Pix"
/>
</div>
<div class="flex pt-2 justify-center" v-if="!validPixFormat">
<span class="text-red-500 font-normal text-sm"
>Por favor utilize telefone, CPF ou CNPJ</span
>
</div>
</div>
<CustomButton
v-if="walletAddress"
:text="'Aprovar tokens'"
:isDisabled="!validDecimals || !validPixFormat"
@buttonClicked="handleSellClick(offer, pixKey)"
/>
<CustomButton
v-if="!walletAddress"
:text="'Conectar carteira'"
@buttonClicked="connectAccount()"
/>
</div>
</div>
</template>
<style scoped>
.custom-divide {
width: 100%;
border-bottom: 1px solid #d1d5db;
}
.bottom-position {
top: -20px;
right: 50%;
transform: translateX(50%);
}
.page {
@apply flex flex-col items-center justify-center w-full mt-16;
}
.text-container {
@apply flex flex-col items-center justify-center gap-4;
}
.text {
@apply text-white text-center;
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}
input {
@apply sm:text-lg text-sm;
}
</style>

5
src/model/Bank.ts Normal file
View File

@ -0,0 +1,5 @@
export interface Bank {
COMPE: string;
ISPB: string;
longName: string;
}

View File

@ -1,7 +1,6 @@
import type { Event } from "ethers";
import { vi } from "vitest";
export const MockEvents: Event[] = [
export const MockEvents = [
{
blockNumber: 1,
blockHash: "0x8",

View File

@ -1,4 +1,5 @@
import type { ValidDeposit } from "../ValidDeposit";
import { NetworkEnum } from "@/model/NetworkEnum";
export const MockValidDeposits: ValidDeposit[] = [
{
@ -7,6 +8,7 @@ export const MockValidDeposits: ValidDeposit[] = [
remaining: 70,
seller: "mockedSellerAddress",
pixKey: "123456789",
network: NetworkEnum.sepolia,
},
{
blockNumber: 2,
@ -14,6 +16,7 @@ export const MockValidDeposits: ValidDeposit[] = [
remaining: 200,
seller: "mockedSellerAddress",
pixKey: "123456789",
network: NetworkEnum.sepolia,
},
{
blockNumber: 3,
@ -21,6 +24,7 @@ export const MockValidDeposits: ValidDeposit[] = [
remaining: 1250,
seller: "mockedSellerAddress",
pixKey: "123456789",
network: NetworkEnum.sepolia,
},
{
blockNumber: 4,
@ -28,6 +32,7 @@ export const MockValidDeposits: ValidDeposit[] = [
remaining: 4000,
seller: "mockedSellerAddress",
pixKey: "123456789",
network: NetworkEnum.sepolia,
},
{
blockNumber: 5,
@ -35,5 +40,6 @@ export const MockValidDeposits: ValidDeposit[] = [
remaining: 2000,
seller: "mockedSellerAddress",
pixKey: "123456789",
network: NetworkEnum.sepolia,
},
];

View File

@ -1,14 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import axios from "axios";
const defaultConfig = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
};
const api = axios.create({
...defaultConfig,
baseURL: import.meta.env.VITE_API_URL,
});
export default api;

View File

@ -1,5 +1,6 @@
import { NetworkEnum, TokenEnum } from "../model/NetworkEnum";
import type { ValidDeposit } from "@/model/ValidDeposit";
import type { Participant } from "../utils/bbPay";
import { defineStore } from "pinia";
export const useEtherStore = defineStore("ether", {
@ -13,6 +14,8 @@ export const useEtherStore = defineStore("ether", {
depositsValidList: [] as ValidDeposit[],
loadingWalletTransactions: false,
loadingNetworkLiquidity: false,
seller: {} as Participant,
sellerId: "",
}),
actions: {
setWalletAddress(walletAddress: string) {
@ -42,6 +45,12 @@ export const useEtherStore = defineStore("ether", {
setLoadingNetworkLiquidity(isLoadingNetworkLiquidity: boolean) {
this.loadingNetworkLiquidity = isLoadingNetworkLiquidity;
},
setSeller(seller: Participant) {
this.seller = seller;
},
setSellerId(sellerId: string) {
this.sellerId = sellerId;
},
},
getters: {
getValidDepositByWalletAddress: (state) => {

57
src/utils/bbPay.ts Normal file
View File

@ -0,0 +1,57 @@
export interface Participant {
offer: string;
fullName: string;
identification: string;
bankIspb?: string;
accountType: string;
account: string;
branch: string;
savingsVariation?: string;
}
export interface ParticipantWithID extends Participant {
id: string;
}
export interface Offer {
amount: number;
lockId: string;
sellerId: string;
}
// Specs for BB Pay Sandbox
// https://apoio.developers.bb.com.br/sandbox/spec/665797498bb48200130fc32c
export const createParticipant = async (participant: Participant) => {
const response = await fetch(`${process.env.VUE_APP_API_URL}/participants`, {
method: "PUT",
body: JSON.stringify(participant),
});
const data = await response.json();
return { ...participant, id: data.id } as ParticipantWithID;
};
export const createSolicitation = async (offer: Offer) => {
const response = await fetch(`${process.env.VUE_APP_API_URL}/solicitation`, {
method: "POST",
body: JSON.stringify(offer),
});
return response.json();
};
export const getSolicitation = async (id: string) => {
const response = await fetch(
`${process.env.VUE_APP_API_URL}/solicitation/${id}`
);
const obj: any = response.json();
return {
id: obj.numeroSolicitacao,
lockId: obj.codigoConciliacaoSolicitacao,
amount: obj.valorSolicitacao,
qrcode: obj.pix.textoQrCode,
status: obj.valorSomatorioPagamentosEfetivados >= obj.valorSolicitacao,
signature: obj.assinatura,
};
};

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ 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";
enum Step {
Search,
@ -59,18 +60,15 @@ const confirmBuyClick = async (
}
};
const releaseTransaction = async (e2eId: string) => {
const releaseTransaction = async (lockId: string) => {
flowStep.value = Step.List;
showBuyAlert.value = true;
loadingRelease.value = true;
if (lockID.value && tokenAmount.value && pixTarget.value) {
const release = await releaseLock(
pixTarget.value,
tokenAmount.value,
e2eId,
lockID.value
);
const solicitation = await getSolicitation(lockId);
if (solicitation.status) {
const release = await releaseLock(solicitation);
await release.wait();
await updateWalletStatus();
@ -140,8 +138,8 @@ onMounted(async () => {
/>
<div v-if="flowStep == Step.Buy">
<QrCodeComponent
:pixTarget="String(pixTarget)"
:tokenValue="tokenAmount"
:sellerId="String(pixTarget)"
:amount="tokenAmount"
@pix-validated="releaseTransaction"
v-if="!loadingLock"
/>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import WantSellComponent from "@/components/SellerSteps/WantSellComponent.vue";
import SellerComponent from "@/components/SellerSteps/SellerComponent.vue";
import SendNetwork from "@/components/SellerSteps/SendNetwork.vue";
import LoadingComponent from "@/components/LoadingComponent/LoadingComponent.vue";
import { approveTokens, addDeposit } from "@/blockchain/sellerMethods";
@ -7,6 +7,7 @@ import { approveTokens, addDeposit } from "@/blockchain/sellerMethods";
import { ref } from "vue";
import { useEtherStore } from "@/store/ether";
import CustomAlert from "@/components/CustomAlert/CustomAlert.vue";
import type { Participant } from "@/utils/bbPay";
enum Step {
Search,
@ -20,20 +21,13 @@ etherStore.setSellerView(true);
const flowStep = ref<Step>(Step.Sell);
const loading = ref<boolean>(false);
const offerValue = ref<string>("");
const pixKeyBuyer = ref<string>("");
const showAlert = ref<boolean>(false);
// Verificar tipagem
const approveOffer = async (args: {
offer: string;
postProcessedPixKey: string;
}) => {
const approveOffer = async (args: Participant) => {
loading.value = true;
try {
offerValue.value = args.offer;
pixKeyBuyer.value = args.postProcessedPixKey;
await approveTokens(args.offer);
await approveTokens(args);
flowStep.value = Step.Network;
loading.value = false;
} catch (err) {
@ -46,8 +40,8 @@ const approveOffer = async (args: {
const sendNetwork = async () => {
loading.value = true;
try {
if (offerValue.value && pixKeyBuyer.value) {
await addDeposit(String(offerValue.value), pixKeyBuyer.value);
if (etherStore.seller) {
await addDeposit();
flowStep.value = Step.Sell;
loading.value = false;
showAlert.value = true;
@ -63,7 +57,7 @@ const sendNetwork = async () => {
<template>
<div>
<div v-if="flowStep == Step.Sell">
<WantSellComponent v-if="!loading" @approve-tokens="approveOffer" />
<SellerComponent v-if="!loading" @approve-tokens="approveOffer" />
<LoadingComponent
v-if="loading"
:message="'A transação está sendo enviada para a rede.'"
@ -76,8 +70,8 @@ const sendNetwork = async () => {
/>
<div v-if="flowStep == Step.Network">
<SendNetwork
:pixKey="pixKeyBuyer"
:offer="Number(offerValue)"
:sellerId="etherStore.sellerId"
:offer="Number(etherStore.seller.offer)"
:selected-token="etherStore.selectedToken"
v-if="!loading"
@send-network="sendNetwork"

View File

@ -3,7 +3,8 @@
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
"src/**/*.vue",
"scripts"
],
"compilerOptions": {
"baseUrl": ".",
@ -24,6 +25,9 @@
"esnext",
"dom"
],
"preserveValueImports": false,
"importsNotUsedAsValues": "remove",
"verbatimModuleSyntax": true
},
"references": [
{

View File

@ -10,6 +10,17 @@ export default defineConfig({
build: {
target: "esnext",
},
optimizeDeps: {
esbuildOptions: {
target: "esnext",
define: {
global: "globalThis",
},
supported: {
bigint: true,
},
},
},
test: {
globals: true,
environment: "jsdom",

198
yarn.lock
View File

@ -242,11 +242,21 @@
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz"
@ -290,6 +300,13 @@
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz"
integrity sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==
"@babel/parser@^7.25.3":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5"
integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==
dependencies:
"@babel/types" "^7.26.9"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz"
@ -963,6 +980,14 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@babel/types@^7.26.9":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce"
integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
@ -1973,50 +1998,26 @@
picocolors "^1.0.0"
pretty-format "^27.5.1"
"@volar/language-core@1.0.9":
version "1.0.9"
resolved "https://registry.npmjs.org/@volar/language-core/-/language-core-1.0.9.tgz"
integrity sha512-5Fty3slLet6svXiJw2YxhYeo6c7wFdtILrql5bZymYLM+HbiZtJbryW1YnUEKAP7MO9Mbeh+TNH4Z0HFxHgIqw==
"@volar/language-core@2.4.11", "@volar/language-core@~2.4.11":
version "2.4.11"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.11.tgz#d95a9ec4f14fbdb41a6a64f9f321d11d23a5291c"
integrity sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==
dependencies:
"@volar/source-map" "1.0.9"
"@vue/reactivity" "^3.2.40"
muggle-string "^0.1.0"
"@volar/source-map" "2.4.11"
"@volar/source-map@1.0.9":
version "1.0.9"
resolved "https://registry.npmjs.org/@volar/source-map/-/source-map-1.0.9.tgz"
integrity sha512-fazB/vy5ZEJ3yKx4fabJyGNI3CBkdLkfEIRVu6+1P3VixK0Mn+eqyUIkLBrzGYaeFM3GybhCLCvsVdNz0Fu/CQ==
dependencies:
muggle-string "^0.1.0"
"@volar/source-map@2.4.11":
version "2.4.11"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.11.tgz#5876d4531508129724c2755e295db1df98bd5895"
integrity sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==
"@volar/typescript@1.0.9":
version "1.0.9"
resolved "https://registry.npmjs.org/@volar/typescript/-/typescript-1.0.9.tgz"
integrity sha512-dVziu+ShQUWuMukM6bvK2v2O446/gG6l1XkTh2vfkccw1IzjfbiP1TWQoNo1ipTfZOtu5YJGYAx+o5HNrGXWfQ==
"@volar/typescript@~2.4.11":
version "2.4.11"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.11.tgz#aafbfa413337654db211bf4d8fb6670c89f6fa57"
integrity sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==
dependencies:
"@volar/language-core" "1.0.9"
"@volar/vue-language-core@1.0.9":
version "1.0.9"
resolved "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.0.9.tgz"
integrity sha512-tofNoR8ShPFenHT1YVMuvoXtXWwoQE+fiXVqSmW0dSKZqEDjWQ3YeXSd0a6aqyKaIbvR7kWWGp34WbpQlwf9Ww==
dependencies:
"@volar/language-core" "1.0.9"
"@volar/source-map" "1.0.9"
"@vue/compiler-dom" "^3.2.40"
"@vue/compiler-sfc" "^3.2.40"
"@vue/reactivity" "^3.2.40"
"@vue/shared" "^3.2.40"
minimatch "^5.1.0"
vue-template-compiler "^2.7.10"
"@volar/vue-typescript@1.0.9":
version "1.0.9"
resolved "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.0.9.tgz"
integrity sha512-ZLe4y9YNbviACa7uAMCilzxA76gbbSlKfjspXBzk6fCobd8QCIig+VyDYcjANIlm2HhgSCX8jYTzhCKlegh4mw==
dependencies:
"@volar/typescript" "1.0.9"
"@volar/vue-language-core" "1.0.9"
"@volar/language-core" "2.4.11"
path-browserify "^1.0.1"
vscode-uri "^3.0.8"
"@vue/babel-helper-vue-transform-on@^1.0.2":
version "1.0.2"
@ -2048,7 +2049,18 @@
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-dom@3.2.41", "@vue/compiler-dom@^3.2.40":
"@vue/compiler-core@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/shared" "3.5.13"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.2.41":
version "3.2.41"
resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz"
integrity sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw==
@ -2056,6 +2068,14 @@
"@vue/compiler-core" "3.2.41"
"@vue/shared" "3.2.41"
"@vue/compiler-dom@^3.5.0":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
dependencies:
"@vue/compiler-core" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/compiler-sfc@2.7.14":
version "2.7.14"
resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz"
@ -2065,7 +2085,7 @@
postcss "^8.4.14"
source-map "^0.6.1"
"@vue/compiler-sfc@3.2.41", "@vue/compiler-sfc@^3.2.40":
"@vue/compiler-sfc@3.2.41":
version "3.2.41"
resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz"
integrity sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==
@ -2089,6 +2109,14 @@
"@vue/compiler-dom" "3.2.41"
"@vue/shared" "3.2.41"
"@vue/compiler-vue2@^2.7.16":
version "2.7.16"
resolved "https://registry.yarnpkg.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz#2ba837cbd3f1b33c2bc865fbe1a3b53fb611e249"
integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
"@vue/devtools-api@^6.4.4", "@vue/devtools-api@^6.4.5":
version "6.4.5"
resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.5.tgz"
@ -2111,6 +2139,20 @@
"@typescript-eslint/parser" "^5.0.0"
vue-eslint-parser "^9.0.0"
"@vue/language-core@2.2.8":
version "2.2.8"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.2.8.tgz#05befa390399fbd4409bc703ee0520b8ac1b7088"
integrity sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==
dependencies:
"@volar/language-core" "~2.4.11"
"@vue/compiler-dom" "^3.5.0"
"@vue/compiler-vue2" "^2.7.16"
"@vue/shared" "^3.5.0"
alien-signals "^1.0.3"
minimatch "^9.0.3"
muggle-string "^0.4.1"
path-browserify "^1.0.1"
"@vue/reactivity-transform@3.2.41":
version "3.2.41"
resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz"
@ -2122,7 +2164,7 @@
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity@3.2.41", "@vue/reactivity@^3.2.40":
"@vue/reactivity@3.2.41":
version "3.2.41"
resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.41.tgz"
integrity sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g==
@ -2154,11 +2196,16 @@
"@vue/compiler-ssr" "3.2.41"
"@vue/shared" "3.2.41"
"@vue/shared@3.2.41", "@vue/shared@^3.2.40":
"@vue/shared@3.2.41":
version "3.2.41"
resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.41.tgz"
integrity sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==
"@vue/shared@3.5.13", "@vue/shared@^3.5.0":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
"@vue/test-utils@^2.2.7":
version "2.2.7"
resolved "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.2.7.tgz"
@ -2376,6 +2423,11 @@ alchemy-sdk@^2.3.0:
sturdy-websocket "^0.2.1"
websocket "^1.0.34"
alien-signals@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/alien-signals/-/alien-signals-1.0.4.tgz#c696a5dc9963fc648ae97093411d7e74b0a81ac0"
integrity sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
@ -3125,7 +3177,7 @@ encode-utf8@^1.0.3:
resolved "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz"
integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==
entities@^4.2.0:
entities@^4.2.0, entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
@ -4615,10 +4667,10 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz"
integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==
minimatch@^9.0.3:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
@ -4652,10 +4704,10 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
muggle-string@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/muggle-string/-/muggle-string-0.1.0.tgz"
integrity sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg==
muggle-string@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
nanoid@^3.3.1:
version "3.3.7"
@ -4862,6 +4914,11 @@ parse5@^7.1.1:
dependencies:
entities "^4.4.0"
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@ -5374,7 +5431,7 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0"
source-map-js@^1.0.1:
source-map-js@^1.0.1, source-map-js@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@ -5757,10 +5814,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@~4.7.4:
version "4.7.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@~5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
uc.micro@^1.0.1:
version "1.0.6"
@ -6015,6 +6072,11 @@ vitest@^0.28.1:
vite-node "0.28.1"
why-is-node-running "^2.2.2"
vscode-uri@^3.0.8:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
vue-demi@*, vue-demi@^0.13.11:
version "0.13.11"
resolved "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz"
@ -6064,21 +6126,13 @@ vue-router@^4.1.5:
dependencies:
"@vue/devtools-api" "^6.4.5"
vue-template-compiler@^2.7.10:
version "2.7.13"
resolved "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.13.tgz"
integrity sha512-jYM6TClwDS9YqP48gYrtAtaOhRKkbYmbzE+Q51gX5YDr777n7tNI/IZk4QV4l/PjQPNh/FVa/E92sh/RqKMrog==
vue-tsc@^2.2.8:
version "2.2.8"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.2.8.tgz#7c8e1bd9333d25241a7f9988eedf08c65483158c"
integrity sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
vue-tsc@^1.0.8:
version "1.0.9"
resolved "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.0.9.tgz"
integrity sha512-vRmHD1K6DmBymNhoHjQy/aYKTRQNLGOu2/ESasChG9Vy113K6CdP0NlhR0bzgFJfv2eFB9Ez/9L5kIciUajBxQ==
dependencies:
"@volar/vue-language-core" "1.0.9"
"@volar/vue-typescript" "1.0.9"
"@volar/typescript" "~2.4.11"
"@vue/language-core" "2.2.8"
vue@^2.5.16:
version "2.7.14"