index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <template>
  2. <div class="container">
  3. <div class="content">
  4. <div class="card-box">
  5. <div class="card-title">选择币种</div>
  6. <div class="card-input" @click="selectPop(1)">
  7. <van-image width="30px" height="30px" round :src="selecctList.logo"/>
  8. <div class="card-text">{{selecctList.name}}</div>
  9. <svg-icon style="width: 16px; height: 16px;" name="down" />
  10. </div>
  11. </div>
  12. <div class="card-box">
  13. <div class="card-title">收货地址
  14. <svg-icon style="width: 16px; height: 16px;" name="address" @click="goToAddress"/>
  15. </div>
  16. <div class="card-input" style="height:100%">
  17. <van-field
  18. type="textarea"
  19. v-model="walletAddress"
  20. placeholder="请输入收货地址"
  21. rows="1"
  22. :autosize="true"
  23. >
  24. <template #right-icon>
  25. <svg-icon style="width: 16px; height: 16px;" name="sm" />
  26. </template>
  27. </van-field>
  28. </div>
  29. </div>
  30. <div class="card-box">
  31. <div class="card-title">
  32. <div>转账数量</div>
  33. <div>可用:{{selecctList.balance}} {{ selecctList.name }}</div>
  34. </div>
  35. <div class="card-input">
  36. <van-field v-model="unitNum" placeholder="0.00" />
  37. <div class="card-input-right">
  38. <div>{{ selecctList.name }}</div>
  39. <div class="line"></div>
  40. <div class="all" @click="unitNum = selecctList.balance">全部</div>
  41. </div>
  42. </div>
  43. </div>
  44. <!-- <div class="card-box">
  45. <div class="card-title">网络费用</div>
  46. <div class="card-input" style="flex-direction: column;align-items: self-start;padding: 8px 17px;">
  47. <div>18.34344398867676764566000ACC</div>
  48. <div class="price">STT0.01</div>
  49. </div>
  50. </div> -->
  51. </div>
  52. <van-button class="footer-btn" type="primary" size="large" @click="selectPop(2)" :disabled="isDisabled">确认</van-button>
  53. <van-popup v-model:show="showWallet" position="bottom" round style="height:400px">
  54. <div class="pop-content" style="height:400px">
  55. <div class="pop-title">
  56. <svg-icon style="width: 24px; height: 24px;" name="left-arrow" @click="showWallet = false"/>
  57. <div class="title">{{selectType == 1?'选择币种':'交易详情'}}</div>
  58. </div>
  59. <div class="list-ul" v-if="selectType == 1">
  60. <div class="list-li" v-for="item in hotTokensList" @click="changeList(item)" :key="item.name">
  61. <div class="list-li-lf">
  62. <van-image width="42px" height="42px" round :src="item.logo"/>
  63. <div style="margin-left: 12px;">{{item.name}}</div>
  64. </div>
  65. <div class="list-li-ri">
  66. <div>{{item.balance}}</div>
  67. <div class="list-li-ri-num">${{item.money}}</div>
  68. </div>
  69. </div>
  70. </div>
  71. <div class="pop-detail" v-if="selectType == 2">
  72. <!-- <div class="pop-detail-title">-{{gasFee}} ACC</div> -->
  73. <div class="pop-detail-cell">
  74. <div class="cell-label">付款地址:</div>
  75. <div class="cell-text">{{walletStore.account}}</div>
  76. </div>
  77. <div class="pop-detail-cell">
  78. <div class="cell-label">收款地址:</div>
  79. <div class="cell-text">{{walletAddress}}</div>
  80. </div>
  81. <div class="pop-detail-cell">
  82. <div class="cell-label">矿工费:</div>
  83. <div class="cell-text">{{ gasFee }} ACC</div>
  84. </div>
  85. <div class="pop-btn">
  86. <van-button class="btn" type="primary" size="large" color="#4765DD" @click="confirm" :disabled="!gasFee || gasFee === '0.0000'">确认</van-button>
  87. </div>
  88. </div>
  89. </div>
  90. </van-popup>
  91. <van-popup v-model:show="showPassWord" :style="{ borderRadius:'25px' }">
  92. <div class="pop-content-password">
  93. <div class="pop-title-password">請輸入密碼</div>
  94. <van-field v-model="passWord" class="pop-input" type="password"/>
  95. <div class="pop-btn-password">
  96. <van-button type="default" class="btn-password cancel" @click="cancel">取消</van-button>
  97. <van-button type="default" class="btn-password confirm" @click="popConfirm" :disabled="isDisabledConfirm">確定</van-button>
  98. </div>
  99. </div>
  100. </van-popup>
  101. </div>
  102. </template>
  103. <script setup>
  104. import { cryptoDecode } from "@/utils/crypto.js"
  105. import { useRouter,useRoute } from 'vue-router'
  106. import { useWalletStore } from "@/stores/modules/walletStore";
  107. import Web3 from "web3";
  108. import pubData from "@/utils/pub.json";
  109. import { showToast } from 'vant';
  110. import { showNotify } from 'vant';
  111. const router = useRouter();
  112. const route = useRoute();
  113. const walletStore = useWalletStore();
  114. const web3 = new Web3(walletStore.rpcUrl);
  115. const walletAddress = ref(route.query.address || '');
  116. const unitNum = ref('')
  117. const showWallet = ref(false)
  118. const showPassWord = ref(false)
  119. const selectType = ref('');
  120. let hotTokensList = walletStore.tokenList;
  121. let selecctList = walletStore.tokenList[0];
  122. const passWord = ref('')
  123. const gasFee = ref('');
  124. const isDisabledConfirm = ref(false)
  125. const isDisabled = computed(() => {
  126. const num = parseFloat(unitNum.value);
  127. return (
  128. !walletAddress.value || // 钱包地址为空
  129. !unitNum.value || // 输入为空
  130. isNaN(num) || num <= 0 || num >= selecctList.balance // 非法数值
  131. );
  132. });
  133. const goToAddress = () => {
  134. router.push('transferAddressManagement')
  135. }
  136. const selectPop = (type) => {
  137. showWallet.value = true;
  138. selectType.value = type;
  139. }
  140. watch(() => selectType.value, (val) => {
  141. if (val === 2) {
  142. estimateGasFee();
  143. }
  144. });
  145. const estimateGasFee = async () => {
  146. try {
  147. const from = walletStore.account;
  148. const to = walletAddress.value;
  149. const amount = unitNum.value;
  150. if (!from || !to || !amount) return;
  151. const isACC = selecctList.name === 'ACC';
  152. let tx;
  153. if (isACC) {
  154. // ① 直接转账
  155. tx = {
  156. from,
  157. to,
  158. value: web3.utils.toWei(amount.toString(), 'ether'),
  159. };
  160. } else {
  161. // ② ERC20 代币转账
  162. const contract = new web3.eth.Contract(pubData, selecctList.address);
  163. const value = web3.utils.toWei(amount.toString(), 'ether'); // 注意小数位
  164. tx = {
  165. from,
  166. to: selecctList.address,
  167. data: contract.methods.transfer(to, value).encodeABI(),
  168. };
  169. }
  170. const gasPrice = await web3.eth.getGasPrice(); // 单位 Wei
  171. const gasLimit = await web3.eth.estimateGas(tx); // 单位 Gas
  172. const feeWei = BigInt(gasPrice) * BigInt(gasLimit);
  173. gasFee.value = Number(web3.utils.fromWei(feeWei.toString(), 'ether')).toFixed(8);
  174. } catch (err) {
  175. console.error('预估矿工费失败:', err);
  176. gasFee.value = '0.0000';
  177. }
  178. };
  179. const changeList = (item) => {
  180. selecctList = item;
  181. showWallet.value = false;
  182. unitNum.value = ''
  183. }
  184. // 确认
  185. const confirm = async () => {
  186. showWallet.value = false;
  187. showPassWord.value = true;
  188. };
  189. // 密码取消
  190. const cancel = () => {
  191. passWord.value = '';
  192. showPassWord.value = false;
  193. }
  194. // 密码确认
  195. const popConfirm = () => {
  196. if(passWord.value != cryptoDecode(walletStore.accountPassword)){
  197. showNotify({ type: 'warning', message: '请输入正确的密码' });
  198. }else{
  199. getData();
  200. }
  201. }
  202. // 请求链
  203. const getData = async () => {
  204. isDisabledConfirm.value = true;
  205. try {
  206. let receipt;
  207. if (selecctList.name === 'ACC') {
  208. // ETH 转账
  209. receipt = await sendETH(
  210. walletStore.privateKey,
  211. walletAddress.value,
  212. unitNum.value
  213. );
  214. } else {
  215. // ERC‑20 代币转账
  216. receipt = await sendToken(
  217. walletStore.privateKey,
  218. selecctList.address,
  219. walletAddress.value,
  220. unitNum.value
  221. );
  222. }
  223. showToast('转账成功');
  224. router.push('/wallet');
  225. } catch (err) {
  226. showToast('转账失败');
  227. } finally {
  228. showWallet.value = false;
  229. isDisabledConfirm.value = false;
  230. }
  231. }
  232. // ========= ETH 转账 =========
  233. const sendETH = async (privateKey, toAddress, amountInEther) => {
  234. const account = web3.eth.accounts.privateKeyToAccount(privateKey);
  235. const from = account.address;
  236. const nonce = await web3.eth.getTransactionCount(from, 'latest');
  237. const gasPrice = await web3.eth.getGasPrice();
  238. const tx = {
  239. from,
  240. to: toAddress,
  241. value: web3.utils.toWei(amountInEther.toString(), 'ether'),
  242. gas: 21000,
  243. gasPrice,
  244. nonce,
  245. chainId: await web3.eth.getChainId(),
  246. };
  247. const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  248. return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  249. };
  250. // ========= ERC‑20 代币转账 =========
  251. const sendToken = async (privateKey, tokenAddress, toAddress, amount) => {
  252. const account = web3.eth.accounts.privateKeyToAccount(privateKey);
  253. const from = account.address;
  254. const contract = new web3.eth.Contract(pubData, tokenAddress);
  255. const nonce = await web3.eth.getTransactionCount(from, 'latest');
  256. const gasPrice = await web3.eth.getGasPrice();
  257. const value = web3.utils.toWei(amount.toString(), 'ether');
  258. const txData = contract.methods.transfer(toAddress, value).encodeABI();
  259. const tx = {
  260. from,
  261. to: tokenAddress,
  262. data: txData,
  263. gas: 100000,
  264. gasPrice,
  265. nonce,
  266. chainId: await web3.eth.getChainId(),
  267. };
  268. const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
  269. return await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  270. };
  271. </script>
  272. <style lang="less" scoped>
  273. .container{
  274. display: flex;
  275. flex-direction: column;
  276. height: 86vh;
  277. padding: 17px 17px 33px;
  278. box-sizing: border-box;
  279. .content{
  280. flex: 1;
  281. .card-box{
  282. margin-bottom: 25px;
  283. .card-title{
  284. font-family: PingFang SC, PingFang SC;
  285. font-weight: 400;
  286. font-size: 12px;
  287. color: #8D8D8D;
  288. margin-bottom: 6px;
  289. display: flex;
  290. align-items: center;
  291. justify-content: space-between;
  292. }
  293. .card-input{
  294. background: #F2F2F2;
  295. border-radius: 8px;
  296. border: 1px solid #D8D8D8;
  297. display: flex;
  298. align-items: center;
  299. padding: 18px 17px;
  300. height: 56px;
  301. box-sizing: border-box;
  302. font-family: PingFang SC, PingFang SC;
  303. font-weight: 400;
  304. font-size: 15px;
  305. color: #000000;
  306. .card-text{
  307. margin: 0 5px 0 8px;
  308. }
  309. .card-input-right{
  310. display: flex;
  311. align-items: center;
  312. flex-shrink: 0;
  313. font-family: PingFang SC, PingFang SC;
  314. font-weight: 400;
  315. font-size: 15px;
  316. color: #000000;
  317. margin-left: 17px;
  318. .line{
  319. height: 25px;
  320. border: 1px solid #D8D8D8;
  321. width: 0;
  322. margin: 0 17px;
  323. }
  324. .all{
  325. font-weight: 500;
  326. color: #4765DD;
  327. }
  328. }
  329. .price{
  330. font-size: 12px;
  331. color: #8D8D8D;
  332. margin-top: 1px;
  333. }
  334. :deep(.van-cell) {
  335. background:#F2F2F2 !important;
  336. padding: 0 !important;
  337. }
  338. :deep(.van-field__control) {
  339. font-family: PingFang SC, PingFang SC;
  340. font-weight: 400;
  341. font-size: 15px;
  342. color: #000000;
  343. }
  344. }
  345. }
  346. }
  347. .footer-btn{
  348. height: 40px !important;
  349. line-height: 40px !important;
  350. background: linear-gradient( 90deg, #4765DD 0%, #40A4FB 100%) !important;
  351. border-radius:28px;
  352. font-family: PingFang SC, PingFang SC;
  353. font-weight: 500;
  354. font-size: 15px;
  355. padding: 9px 0;
  356. box-sizing: border-box;
  357. color: #FFFFFF;
  358. }
  359. .pop-content{
  360. display: flex;
  361. flex-direction: column;
  362. .pop-title{
  363. padding: 17px;
  364. border-bottom: 1px solid #F2F2F2;
  365. display: flex;
  366. align-items: center;
  367. font-family: PingFang SC, PingFang SC;
  368. font-weight: 500;
  369. font-size: 17px;
  370. color: #000000;
  371. .title{
  372. flex: 1;
  373. display: flex;
  374. justify-content: center;
  375. }
  376. }
  377. .list-ul{
  378. flex: 1;
  379. display: flex;
  380. flex-direction: column;
  381. overflow: auto;
  382. margin: 16px;
  383. .list-li{
  384. display: flex;
  385. align-items: center;
  386. justify-content: space-between;
  387. font-family: PingFang SC, PingFang SC;
  388. font-weight: 500;
  389. margin-bottom: 16px;
  390. .list-li-lf{
  391. display: flex;
  392. align-items: center;
  393. font-size: 15px;
  394. color: #000000;
  395. }
  396. .list-li-ri{
  397. font-size: 15px;
  398. color: @font-color2;
  399. text-align: right;
  400. .list-li-ri-num{
  401. margin-top: 3px;
  402. font-weight: 400;
  403. font-size: 12px;
  404. color: #8D8D8D;
  405. }
  406. }
  407. }
  408. .list-li:last-child{
  409. margin-bottom: 0;
  410. }
  411. }
  412. .list-ul::-webkit-scrollbar{
  413. width: 0;
  414. }
  415. .pop-detail{
  416. padding: 17px 17px 0;
  417. .pop-detail-title{
  418. font-family: PingFang SC, PingFang SC;
  419. font-weight: 500;
  420. font-size: 17px;
  421. color: #000000;
  422. margin-bottom: 20px;
  423. text-align: center;
  424. }
  425. .pop-detail-cell{
  426. display: flex;
  427. font-family: PingFang SC, PingFang SC;
  428. font-weight: 400;
  429. font-size: 15px;
  430. color: #4F4F4F;
  431. padding: 10px 0;
  432. border-bottom: 1px solid #F2F2F2;
  433. .cell-label{
  434. width: 76px;
  435. flex-shrink: 0;
  436. }
  437. .cell-text{
  438. word-break: break-word;
  439. }
  440. }
  441. .pop-detail-cell:last-child{
  442. border-bottom: 0;
  443. }
  444. .pop-btn{
  445. margin-top: 53px;
  446. .btn{
  447. height: 40px;
  448. line-height: 40px;
  449. border-radius: 50px;
  450. font-family: PingFang SC, PingFang SC;
  451. font-weight: 500;
  452. font-size: 15px;
  453. color: #FFFFFF;
  454. }
  455. }
  456. }
  457. }
  458. }
  459. .pop-content-password{
  460. padding: 27px 35px 25px 34px;
  461. .pop-title-password{
  462. font-family: PingFang SC, PingFang SC;
  463. font-weight: 500;
  464. font-size: 17px;
  465. color: #000000;
  466. text-align: center;
  467. }
  468. .pop-input{
  469. background: #F2F2F2;
  470. border-radius: 8px;
  471. height: 40px;
  472. margin: 21px 0 31px;
  473. }
  474. .pop-btn-password{
  475. display: flex;
  476. justify-content: center;
  477. .btn-password{
  478. width: 83px;
  479. height: 29px;
  480. line-height: 29px;
  481. padding: 5px 0 !important;
  482. border-radius: 6px;
  483. font-family: PingFang SC, PingFang SC;
  484. font-weight: 400;
  485. font-size: 15px;
  486. box-sizing:border-box;
  487. }
  488. .cancel{
  489. margin-right: 17px !important;
  490. border: 1px solid #D8D8D8;
  491. color: #000 !important;
  492. }
  493. .confirm{
  494. background: @theme-color1;
  495. color: #FFF;
  496. font-weight: 500;
  497. }
  498. }
  499. }
  500. :deep(.van-popup--center) {
  501. margin: 0 40px !important;
  502. width: auto !important;
  503. }
  504. </style>