Compare commits
No commits in common. "947ba83b2d8867bda82b4ce982d71da83b92072b" and "4dcb4a9d1179794f8adf6f535831f0315b284049" have entirely different histories.
947ba83b2d
...
4dcb4a9d11
@ -1,7 +1,7 @@
|
|||||||
import { i18n } from '@/plugins/i18n'
|
import { i18n } from '@/plugins/i18n'
|
||||||
|
|
||||||
/** 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖 */
|
/** 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖 */
|
||||||
export const BASE_URL =
|
const BASE_URL =
|
||||||
(import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL ??
|
(import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL ??
|
||||||
'https://api.xtrader.vip'
|
'https://api.xtrader.vip'
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { get, post } from './request'
|
import { get } from './request'
|
||||||
|
|
||||||
export interface DepositAddressData {
|
export interface DepositAddressData {
|
||||||
address?: string
|
address?: string
|
||||||
@ -30,17 +30,6 @@ 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(
|
export async function getDepositAddress(
|
||||||
params: GetDepositAddressParams,
|
params: GetDepositAddressParams,
|
||||||
config?: { headers?: Record<string, string> },
|
config?: { headers?: Record<string, string> },
|
||||||
|
|||||||
@ -75,7 +75,7 @@
|
|||||||
<div class="address-box">
|
<div class="address-box">
|
||||||
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
|
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
|
||||||
<div class="address-row">
|
<div class="address-row">
|
||||||
<code class="address-code">{{ depositAddress }}</code>
|
<code class="address-code">{{ depositAddressShort }}</code>
|
||||||
<v-btn size="small" variant="tonal" @click="copyAddress">
|
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||||
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -84,27 +84,6 @@
|
|||||||
<img :src="qrCodeUrl" alt="QR Code" class="qr-img" width="120" height="120" />
|
<img :src="qrCodeUrl" alt="QR Code" class="qr-img" width="120" height="120" />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</template>
|
||||||
|
|
||||||
<!-- Connect Exchange:连接钱包后显示地址 -->
|
<!-- Connect Exchange:连接钱包后显示地址 -->
|
||||||
@ -149,7 +128,7 @@
|
|||||||
<div class="address-box">
|
<div class="address-box">
|
||||||
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
|
<label class="address-label">{{ t('deposit.depositAddress') }}</label>
|
||||||
<div class="address-row">
|
<div class="address-row">
|
||||||
<code class="address-code">{{ depositAddress }}</code>
|
<code class="address-code">{{ depositAddressShort }}</code>
|
||||||
<v-btn size="small" variant="tonal" @click="copyAddress">
|
<v-btn size="small" variant="tonal" @click="copyAddress">
|
||||||
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
{{ copied ? t('deposit.copied') : t('deposit.copy') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -166,12 +145,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { getDepositAddress, collectDeposit } from '@/api/wallet'
|
import { getDepositAddress } from '@/api/wallet'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { ethers } from 'ethers'
|
|
||||||
import { USDC_ADDRESS_BY_CHAIN } from '@/api/pmset'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@ -201,191 +178,10 @@ const networks = [
|
|||||||
{ id: 'optimism', label: 'Optimism' },
|
{ id: 'optimism', label: 'Optimism' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const RPC_URLS: Record<string, string> = {
|
const depositAddressShort = computed(() => {
|
||||||
ethereum: 'https://ethereum-rpc.publicnode.com',
|
const a = depositAddress.value
|
||||||
polygon: 'https://polygon-bor-rpc.publicnode.com',
|
return a ? `${a.slice(0, 6)}...${a.slice(-4)}` : ''
|
||||||
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(() => {
|
const qrCodeUrl = computed(() => {
|
||||||
return `https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(depositAddress.value)}`
|
return `https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=${encodeURIComponent(depositAddress.value)}`
|
||||||
@ -472,7 +268,6 @@ watch(
|
|||||||
depositAddress.value = ''
|
depositAddress.value = ''
|
||||||
loadError.value = ''
|
loadError.value = ''
|
||||||
loading.value = false
|
loading.value = false
|
||||||
stopDepositCheck()
|
|
||||||
} else {
|
} else {
|
||||||
if (step.value === 'crypto') fetchAddress()
|
if (step.value === 'crypto') fetchAddress()
|
||||||
}
|
}
|
||||||
@ -482,10 +277,6 @@ watch(
|
|||||||
watch(selectedNetwork, () => {
|
watch(selectedNetwork, () => {
|
||||||
if (step.value === 'crypto') fetchAddress()
|
if (step.value === 'crypto') fetchAddress()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopDepositCheck()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -617,7 +408,6 @@ onUnmounted(() => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
word-break: break-all;
|
|
||||||
}
|
}
|
||||||
.qr-wrap {
|
.qr-wrap {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
@ -204,15 +204,7 @@
|
|||||||
"coinbaseComingSoon": "Coinbase Wallet (Coming soon)",
|
"coinbaseComingSoon": "Coinbase Wallet (Coming soon)",
|
||||||
"walletConnectComingSoon": "WalletConnect (Coming soon)",
|
"walletConnectComingSoon": "WalletConnect (Coming soon)",
|
||||||
"close": "Close",
|
"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": {
|
"withdraw": {
|
||||||
"title": "Withdraw",
|
"title": "Withdraw",
|
||||||
|
|||||||
@ -204,15 +204,7 @@
|
|||||||
"coinbaseComingSoon": "Coinbase Wallet(即将推出)",
|
"coinbaseComingSoon": "Coinbase Wallet(即将推出)",
|
||||||
"walletConnectComingSoon": "WalletConnect(即将推出)",
|
"walletConnectComingSoon": "WalletConnect(即将推出)",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"installMetaMask": "请安装 MetaMask 或其他 Web3 钱包。",
|
"installMetaMask": "请安装 MetaMask 或其他 Web3 钱包。"
|
||||||
"checkStatus": "检查充值状态",
|
|
||||||
"checking": "正在检测...",
|
|
||||||
"checkingStatus": "正在检测充值状态...",
|
|
||||||
"nextCheckIn": "下次检测: {seconds}秒",
|
|
||||||
"checkingNow": "正在检测...",
|
|
||||||
"checkSuccess": "检测到充值!已提交处理。",
|
|
||||||
"checkTimeout": "未检测到充值。请稍候再试。",
|
|
||||||
"checkError": "检测失败"
|
|
||||||
},
|
},
|
||||||
"withdraw": {
|
"withdraw": {
|
||||||
"title": "提现",
|
"title": "提现",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
import { getUsdcBalance, formatUsdcBalance, getUserInfo } from '@/api/user'
|
||||||
import { getUserWsUrl, BASE_URL } from '@/api/request'
|
import { getUserWsUrl } from '@/api/request'
|
||||||
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
|
import { UserSdk, type BalanceData, type PositionData } from '../../sdk/userSocket'
|
||||||
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
@ -63,15 +63,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const user = ref<UserInfo | null>(stored?.user ?? null)
|
const user = ref<UserInfo | null>(stored?.user ?? null)
|
||||||
|
|
||||||
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
const isLoggedIn = computed(() => !!token.value && !!user.value)
|
||||||
const avatarUrl = computed(() => {
|
const avatarUrl = computed(() => user.value?.headerImg ?? '')
|
||||||
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 推送更新 */
|
/** 钱包余额显示,如 "0.00",可从接口或 UserSocket 推送更新 */
|
||||||
const balance = ref<string>('0.00')
|
const balance = ref<string>('0.00')
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user