新增:充值逻辑
This commit is contained in:
parent
0a355db7b0
commit
0d221d0a35
@ -1,7 +1,7 @@
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
|
||||
/** 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖 */
|
||||
const BASE_URL =
|
||||
export const BASE_URL =
|
||||
(import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL ??
|
||||
'https://api.xtrader.vip'
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { get } from './request'
|
||||
import { get, post } from './request'
|
||||
|
||||
export interface DepositAddressData {
|
||||
address?: string
|
||||
@ -30,6 +30,17 @@ function pickAddress(d: DepositAddressData | undefined): string | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
export interface CollectDepositParams {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export async function collectDeposit(
|
||||
data: CollectDepositParams,
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<{ code: number; msg?: string }> {
|
||||
return post<{ code: number; msg?: string }>('/wallet/collect', data, config)
|
||||
}
|
||||
|
||||
export async function getDepositAddress(
|
||||
params: GetDepositAddressParams,
|
||||
config?: { headers?: Record<string, string> },
|
||||
|
||||
@ -75,15 +75,36 @@
|
||||
<div class="address-box">
|
||||
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
|
||||
<div class="address-row">
|
||||
<code class="address-code">{{ depositAddressShort }}</code>
|
||||
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
||||
<code class="address-code">{{ depositAddress }}</code>
|
||||
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="qr-wrap">
|
||||
<img :src="qrCodeUrl" alt="QR Code" class="qr-img" width="120" height="120" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="check-deposit-section mt-4">
|
||||
<v-btn
|
||||
block
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="isPolling && checkStatus === 'checking' && nextCheckCountdown <= 0"
|
||||
:disabled="isPolling"
|
||||
@click="startDepositCheck"
|
||||
>
|
||||
{{ isPolling ? t('deposit.checking') : t('deposit.checkStatus') }}
|
||||
</v-btn>
|
||||
<div v-if="isPolling || checkStatus !== 'idle'" class="check-status-msg mt-2 text-center text-caption">
|
||||
<template v-if="checkStatus === 'checking'">
|
||||
<span v-if="nextCheckCountdown > 0">{{ t('deposit.nextCheckIn', { seconds: nextCheckCountdown }) }}</span>
|
||||
<span v-else>{{ t('deposit.checkingNow') }}</span>
|
||||
</template>
|
||||
<span v-else-if="checkStatus === 'success'" class="text-success">{{ t('deposit.checkSuccess') }}</span>
|
||||
<span v-else-if="checkStatus === 'timeout'" class="text-warning">{{ t('deposit.checkTimeout') }}</span>
|
||||
<span v-else-if="checkStatus === 'error'" class="text-error">{{ checkMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Connect Exchange:连接钱包后显示地址 -->
|
||||
@ -128,7 +149,7 @@
|
||||
<div class="address-box">
|
||||
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
|
||||
<div class="address-row">
|
||||
<code class="address-code">{{ depositAddressShort }}</code>
|
||||
<code class="address-code">{{ depositAddress }}</code>
|
||||
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
||||
</v-btn>
|
||||
@ -145,10 +166,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getDepositAddress } from '@/api/wallet'
|
||||
import { getDepositAddress, collectDeposit } from '@/api/wallet'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ethers } from 'ethers'
|
||||
import { USDC_ADDRESS_BY_CHAIN } from '@/api/pmset'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = withDefaults(
|
||||
@ -178,10 +201,191 @@ const networks = [
|
||||
{ id: 'optimism', label: 'Optimism' },
|
||||
]
|
||||
|
||||
const depositAddressShort = computed(() => {
|
||||
const a = depositAddress.value
|
||||
return a ? `${a.slice(0, 6)}...${a.slice(-4)}` : ''
|
||||
})
|
||||
const RPC_URLS: Record<string, string> = {
|
||||
ethereum: 'https://ethereum-rpc.publicnode.com',
|
||||
polygon: 'https://polygon-bor-rpc.publicnode.com',
|
||||
arbitrum: 'https://arbitrum-one-rpc.publicnode.com',
|
||||
optimism: 'https://optimism-rpc.publicnode.com',
|
||||
}
|
||||
|
||||
const CHAIN_IDS: Record<string, number> = {
|
||||
ethereum: 1,
|
||||
polygon: 137,
|
||||
arbitrum: 42161,
|
||||
optimism: 10,
|
||||
}
|
||||
|
||||
const isPolling = ref(false)
|
||||
const checkStatus = ref<'idle' | 'checking' | 'success' | 'timeout' | 'error'>('idle')
|
||||
const checkMessage = ref('')
|
||||
const nextCheckTime = ref(0)
|
||||
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
const POLL_INTERVAL = 60000 // 60 seconds
|
||||
const MAX_POLL_TIME = 30 * 60 * 1000 // 30 minutes
|
||||
let initialBalance = BigInt(0)
|
||||
let startTime = 0
|
||||
|
||||
const nextCheckCountdown = ref(0)
|
||||
|
||||
function stopDepositCheck() {
|
||||
isPolling.value = false
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
countdownTimer = null
|
||||
}
|
||||
checkStatus.value = 'idle'
|
||||
checkMessage.value = ''
|
||||
}
|
||||
|
||||
async function startDepositCheck() {
|
||||
if (!depositAddress.value || !selectedNetwork.value) return
|
||||
|
||||
stopDepositCheck()
|
||||
isPolling.value = true
|
||||
checkStatus.value = 'checking'
|
||||
checkMessage.value = t('deposit.checkingStatus')
|
||||
startTime = Date.now()
|
||||
initialBalance = BigInt(0) // Reset initial balance assumption or fetch it first?
|
||||
// We should probably fetch initial balance first, but for now assuming user deposits > 0 into a fresh-ish account or just checking for > 0 if it's a unique deposit address.
|
||||
// Ideally we check current balance and see if it increases.
|
||||
// For simplicity based on prompt "check for balance change", let's fetch current first.
|
||||
|
||||
await checkBalance(true)
|
||||
}
|
||||
|
||||
async function checkBalance(isFirst = false) {
|
||||
if (!isPolling.value) return
|
||||
|
||||
try {
|
||||
const rpcUrl = RPC_URLS[selectedNetwork.value]
|
||||
const usdcAddr = USDC_ADDRESS_BY_CHAIN[selectedNetwork.value]
|
||||
|
||||
if (!rpcUrl || !usdcAddr) {
|
||||
checkStatus.value = 'error'
|
||||
checkMessage.value = 'Network configuration error'
|
||||
isPolling.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl, CHAIN_IDS[selectedNetwork.value], {
|
||||
staticNetwork: true,
|
||||
})
|
||||
|
||||
let targetAddr = depositAddress.value
|
||||
try {
|
||||
targetAddr = ethers.getAddress(targetAddr)
|
||||
} catch (e) {
|
||||
console.warn('Invalid address format', targetAddr)
|
||||
}
|
||||
|
||||
// Prepare contract calls
|
||||
// For Polygon, check both Bridged USDC (USDC.e) and Native USDC
|
||||
const contractsToCheck: string[] = [usdcAddr]
|
||||
if (selectedNetwork.value === 'polygon') {
|
||||
// Add Polygon Native USDC
|
||||
contractsToCheck.push('0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359')
|
||||
}
|
||||
|
||||
let totalBalance = BigInt(0)
|
||||
|
||||
for (const addr of contractsToCheck) {
|
||||
try {
|
||||
const contract = new ethers.Contract(addr, ['function balanceOf(address) view returns (uint256)'], provider)
|
||||
const bal = await contract.balanceOf(targetAddr)
|
||||
totalBalance += bal
|
||||
} catch (err) {
|
||||
console.warn(`Failed to check balance for token ${addr}`, err)
|
||||
}
|
||||
}
|
||||
|
||||
const balance = totalBalance
|
||||
|
||||
if (isFirst) {
|
||||
initialBalance = balance
|
||||
// If first check, we just record baseline.
|
||||
// Wait, if user already deposited before clicking check?
|
||||
// If balance > 0 on first check, maybe we should just try to collect?
|
||||
// Let's assume if balance > 0 we try to collect.
|
||||
if (balance > BigInt(0)) {
|
||||
await triggerCollect()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (balance > initialBalance || (initialBalance === BigInt(0) && balance > BigInt(0))) {
|
||||
await triggerCollect()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Continue polling
|
||||
if (Date.now() - startTime > MAX_POLL_TIME) {
|
||||
checkStatus.value = 'timeout'
|
||||
isPolling.value = false
|
||||
return
|
||||
}
|
||||
|
||||
scheduleNextPoll()
|
||||
|
||||
} catch (e) {
|
||||
console.error('Balance check failed:', e)
|
||||
// Don't stop polling on transient error, just retry
|
||||
scheduleNextPoll()
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerCollect() {
|
||||
try {
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
if (!uid) throw new Error('User not logged in')
|
||||
|
||||
const res = await collectDeposit({ userId: Number(uid) }, { headers: userStore.getAuthHeaders() || {} })
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
checkStatus.value = 'success'
|
||||
isPolling.value = false
|
||||
// Optionally refresh balance
|
||||
userStore.fetchUsdcBalance()
|
||||
} else {
|
||||
// If collection failed but balance is there, maybe retry or show error?
|
||||
// Show error but keep polling? No, better stop to avoid spamming if backend is broken.
|
||||
checkStatus.value = 'error'
|
||||
checkMessage.value = res.msg || 'Collection failed'
|
||||
isPolling.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Collect request error:', e)
|
||||
checkStatus.value = 'error'
|
||||
checkMessage.value = e instanceof Error ? e.message : 'Collection request failed'
|
||||
isPolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextPoll() {
|
||||
if (!isPolling.value) return
|
||||
|
||||
nextCheckTime.value = Date.now() + POLL_INTERVAL
|
||||
checkStatus.value = 'checking' // Waiting
|
||||
|
||||
if (countdownTimer) clearInterval(countdownTimer)
|
||||
updateCountdown()
|
||||
countdownTimer = setInterval(updateCountdown, 1000)
|
||||
|
||||
pollTimer = setTimeout(() => {
|
||||
checkBalance()
|
||||
}, POLL_INTERVAL)
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
const diff = Math.max(0, Math.ceil((nextCheckTime.value - Date.now()) / 1000))
|
||||
nextCheckCountdown.value = diff
|
||||
if (diff <= 0 && countdownTimer) {
|
||||
clearInterval(countdownTimer)
|
||||
}
|
||||
}
|
||||
|
||||
const qrCodeUrl = computed(() => {
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(depositAddress.value)}`
|
||||
@ -268,6 +472,7 @@ watch(
|
||||
depositAddress.value = ''
|
||||
loadError.value = ''
|
||||
loading.value = false
|
||||
stopDepositCheck()
|
||||
} else {
|
||||
if (step.value === 'crypto') fetchAddress()
|
||||
}
|
||||
@ -277,6 +482,10 @@ watch(
|
||||
watch(selectedNetwork, () => {
|
||||
if (step.value === 'crypto') fetchAddress()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopDepositCheck()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -408,6 +617,7 @@ watch(selectedNetwork, () => {
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
.qr-wrap {
|
||||
margin-top: 16px;
|
||||
|
||||
@ -204,7 +204,15 @@
|
||||
"coinbaseComingSoon": "Coinbase Wallet (Coming soon)",
|
||||
"walletConnectComingSoon": "WalletConnect (Coming soon)",
|
||||
"close": "Close",
|
||||
"installMetaMask": "Please install MetaMask or another Web3 wallet."
|
||||
"installMetaMask": "Please install MetaMask or another Web3 wallet.",
|
||||
"checkStatus": "Check Deposit Status",
|
||||
"checking": "Checking...",
|
||||
"checkingStatus": "Checking deposit status...",
|
||||
"nextCheckIn": "Next check in: {seconds}s",
|
||||
"checkingNow": "Checking now...",
|
||||
"checkSuccess": "Deposit detected! Processing...",
|
||||
"checkTimeout": "No deposit detected. Please try again later.",
|
||||
"checkError": "Check failed"
|
||||
},
|
||||
"withdraw": {
|
||||
"title": "Withdraw",
|
||||
|
||||
@ -204,7 +204,15 @@
|
||||
"coinbaseComingSoon": "Coinbase Wallet(即将推出)",
|
||||
"walletConnectComingSoon": "WalletConnect(即将推出)",
|
||||
"close": "关闭",
|
||||
"installMetaMask": "请安装 MetaMask 或其他 Web3 钱包。"
|
||||
"installMetaMask": "请安装 MetaMask 或其他 Web3 钱包。",
|
||||
"checkStatus": "检查充值状态",
|
||||
"checking": "正在检测...",
|
||||
"checkingStatus": "正在检测充值状态...",
|
||||
"nextCheckIn": "下次检测: {seconds}秒",
|
||||
"checkingNow": "正在检测...",
|
||||
"checkSuccess": "检测到充值!已提交处理。",
|
||||
"checkTimeout": "未检测到充值。请稍候再试。",
|
||||
"checkError": "检测失败"
|
||||
},
|
||||
"withdraw": {
|
||||
"title": "提现",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
||||
import { getUserWsUrl } from '@/api/request'
|
||||
import { getUserWsUrl, BASE_URL } from '@/api/request'
|
||||
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
|
||||
|
||||
export interface UserInfo {
|
||||
@ -63,7 +63,15 @@ export const useUserStore = defineStore('user', () => {
|
||||
const user = ref<UserInfo | null>(stored?.user ?? null)
|
||||
|
||||
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
||||
const avatarUrl = computed(() => user.value?.headerImg ?? '')
|
||||
const avatarUrl = computed(() => {
|
||||
const img = user.value?.headerImg
|
||||
if (!img) return ''
|
||||
if (img.startsWith('http') || img.startsWith('data:')) return img
|
||||
// 处理相对路径,拼接 BASE_URL
|
||||
const baseUrl = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
|
||||
const path = img.startsWith('/') ? img : `/${img}`
|
||||
return `${baseUrl}${path}`
|
||||
})
|
||||
/** 钱包余额显示,如 "0.00",可从接口或 UserSocket 推送更新 */
|
||||
const balance = ref<string>('0.00')
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user