
簡介這套基于uniapp與Laravel框架的源碼包面向區塊鏈DApp開發者與PHP/前端工程師解決ETH、BSC、TRON多鏈錢包中余額查詢、轉賬、代幣授權及簽名驗證等常見交互難題。前端部分完整覆蓋錢包DApp常用操作后端提供創建錢包、查詢手續費代幣與Token余額、轉賬、授權及授權轉賬等接口前后端聯調邏輯清晰。壓縮包共71個文件以PHP后端源碼為主輔以JavaScript、CSS及環境配置等文件整體僅80KB結構精簡、便于快速移植或二次開發。目前已有259人在線學習與下載適合希望快速擁有多鏈錢包實操代碼的中高級開發者參考使用。通過這套資源可直接獲取各鏈接口調用的設計思路與工具函數節省從零摸索合約與RPC交互的時間并依據自身業務擴展鏈種或支付場景。 做區塊鏈錢包后端這件事圈里一直有個偏見覺得PHP干不了這活。我做過好幾個多鏈錢包項目從ETH到BSC再到TRON后端核心邏輯全是用Laravel寫的前端App用uniapp打包跑得一直很穩。這篇就把整個實現思路、關鍵代碼和踩坑記錄完整拆出來給有同樣需求的兄弟一個參考。這套東西能干什么簡單說就是通過PHP后端統一封裝三條鏈的JSON-RPC和HTTP接口實現查余額、離線簽名、轉賬、代幣授權approve和代付歸集transferFrom然后通過RESTful接口供uniapp調用。適合錢包類App、DApp后臺、交易所歸集系統、以及任何需要在服務端管私鑰和做鏈上操作的項目。前后端通信、私鑰管理、跨鏈差異處理這些核心問題都會在下面展開講。1. 項目整體思路PHP后端如何統一封裝三條鏈1.1 為什么選PHPLaravel而不是Node或Go先回答很多人會問的問題公鏈交互不都是JS和Go的天下嗎確實以太坊生態里Node有ethers.jsGo有go-ethereum文檔多、例子多。但如果是傳統的PHP技術棧團隊或者項目本身就用Laravel做業務后臺再為了一個錢包模塊引入一整套Node服務維護成本是雙份的。PHP做鏈交互最大的優勢是能直接復用你已有的Laravel業務代碼用戶體系、訂單表、提現審核流程、管理后臺全在一個項目里搞定。我不需要單獨部署一個簽名服務不需要跨服務調接口直接用Laravel的Command和Service就可以把鏈上操作和業務邏輯串起來。打個比方Node方案像是為了切個水果專門買一套德國刀具PHP方案則是用你廚房里那把已經很順手的刀換了個磨刀石。實際開發中PHP生態里也有現成的庫ETH和BSC走web3p/web3.phpTRON走iexbase/tron-api兩者都基于JSON-RPC或官方HTTP接口封裝穩定性和社區活躍度都夠用。配合Laravel的隊列、緩存、日志體系整個鏈路非常順。1.2 三條鏈的技術差異與統一抽象ETH和BSC屬于EVM系底層機制幾乎一樣余額通過eth_getBalance查代幣通過調用合約的balanceOf方法查轉賬走eth_sendRawTransactiongas費模型一致。TRON則是一個獨立體系查詢和廣播走的是TronGrid的HTTP接口交易需要先構建、再簽名、最后廣播資源模型是帶寬和能量而不是gas。這三條鏈有個共同的底層邏輯服務端只需要管私鑰簽名和交易廣播鏈上狀態查詢本質就是HTTP請求。所以我做了一個ChainService抽象層每個鏈一個驅動對外暴露統一的方法getBalance、getTokenBalance、buildTransaction、signTransaction、broadcastTransaction。上層業務只管調$chainService-transfer($from, $to, $amount)不需要關心底下是EVM還是TRON。這個抽象層的設計是整項目的基石。如果一開始不做這層封裝后面每加一條鏈業務代碼就要跟著改一遍。做了抽象之后業務代碼一次寫完后面接新鏈只加一個驅動文件即可。2. 環境準備與依賴選型2.1 用到的核心庫與選型理由Laravel版本用的是10.xPHP 8.1這兩個依賴層是這樣分的web3p/web3.phpETH/BSC的JSON-RPC客戶端支持eth、net、personal等模塊web3p/ethereum-tx配合web3.php做離線交易簽名不用依賴節點私鑰iexbase/tron-apiTRON節點接口封裝支持余額、合約調用、交易廣播guzzlehttp/guzzleLaravel自帶的HTTP客戶端TRON接口請求走它選web3p/ethereum-tx而不是節點自帶簽名的原因很現實生產環境里節點往往是第三方提供的比如BSC公共RPC如果你把私鑰發給節點去簽名私鑰就暴露給節點服務商了。離線簽名可以做到“私鑰永不離開你的服務器”這一點對錢包類項目是底線要求。TRON那邊有個細節iexbase/tron-api默認的TronGrid地址是官方主網但國內網絡訪問TronGrid偶爾不穩定建議在.env里配置自建的FullNode地址或者可用的公共節點。這個后面在配置部分細說。2.2 Laravel工程與配置要點依賴裝好之后我把所有鏈相關配置都放進了.env和config/chain.php// config/chain.php return [ eth [ rpc_url env(ETH_RPC_URL, https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY), chain_id 1, ], bsc [ rpc_url env(BSC_RPC_URL, https://bsc-dataseed.binance.org), chain_id 56, ], tron [ full_node env(TRON_FULL_NODE, https://api.trongrid.io), solidity_node env(TRON_SOLIDITY_NODE, https://api.trongrid.io), event_server env(TRON_EVENT_SERVER, https://api.trongrid.io), ], contracts [ // 你業務里用到的USDT合約地址 usdt_eth 0xdAC17F958D2ee523a2206206994597C13D831ec7, usdt_bsc 0x55d398326f99059fF775485246999027B3197955, usdt_tron TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf, ], ];幾個關鍵點私鑰不要直接寫在config文件里。我單獨建了一個PrivateKeyManager類從.env讀取并且線上環境用KMS或者外部密鑰托管服務來做二次保護。另外RPC節點的選擇直接影響穩定性ETH和BSC建議至少配置兩個節點做故障切換TRON的FullNode和SolidityNode盡量分開配——查詢余額走SolidityNode已確認數據廣播交易和查最新狀態走FullNode。3. 核心功能實現余額、轉賬、簽名3.1 ETH/BSC余額與轉賬實現EVM鏈的余額查詢分兩種原生幣和ERC20代幣。原生幣直接用eth_getBalance這個最簡單use Web3\Web3; use Web3\Providers\HttpProvider; use Web3\RequestManagers\HttpRequestManager; $web3 new Web3(new HttpProvider(new HttpRequestManager($rpcUrl))); $web3-eth-getBalance($address, latest, function ($err, $balance) { // 返回的是Wei需要除以10^18才是ETH $ethBalance $balance-toString() / 1e18; });ERC20代幣余額不能直接查節點需要調用合約的balanceOf方法。web3.php里通過Contract類調合約方法但需要合約ABI。我偷懶的寫法是直接用eth_call手動構造calldata這樣不用在服務端維護復雜的ABIJSON// balanceOf(address) 的方法簽名哈希 $methodHash 0x70a08231; $data $methodHash . str_pad(substr($contractAddress, 2), 64, 0, STR_PAD_LEFT); $web3-eth-call([ to $contractAddress, data $data, ], latest, function ($err, $result) { // $result 是十六進制字符串轉十進制就是余額 });然后看轉賬。ETH轉賬的核心是構造一個交易對象設置from、to、value、gasLimit、gasPrice、nonce用私鑰簽名后廣播。web3p/ethereum-tx封裝了這一套use Web3p\EthereumTx\Transaction; $nonce $this-getNonce($fromAddress); // eth_getTransactionCount用pending參數 $gasPrice $this-getGasPrice(); // eth_gasPrice $tx new Transaction([ nonce 0x . dechex($nonce), from $fromAddress, to $toAddress, value 0x . dechex($amountWei), gas 0x5208, // 21000普通轉賬固定值 gasPrice 0x . dechex($gasPrice), chainId $chainId, ]); $signed 0x . $tx-sign($privateKey); $web3-eth-sendRawTransaction($signed, function ($err, $txHash) { ... });這里有個新手特別容易踩的坑nonce必須用pending狀態查詢。默認的latest只返回已確認交易數量如果你連續發起兩筆轉賬第二筆拿到的nonce會和第一筆一樣導致交易被拒絕或者頂掉。更穩妥的做法是把nonce緩存在Redis里每發一筆遞增同時監聽鏈上確認結果做校準。ERC20代幣轉賬比原生幣多一步調用合約的transfer方法。calldata由方法哈希0xa9059cbb加接收地址加金額拼接而成其他流程和ETH轉賬一樣。gasLimit不能寫21000需要先估gaseth_estimateGas或者直接給一個經驗值比如USDT轉賬給60000這樣能省一次RPC請求。3.2 TRON余額與轉賬實現TRON的原生幣TRX余額走/wallet/getaccount接口傳地址返回賬戶信息里面有balance字段。TRC20代幣比如USDT余額則要調用合約的balanceOfiexbase/tron-api封裝了合約調用方法use IEXBase\TronAPI\Tron; $tron new Tron($fullNode, $privateKey); $balance $tron-getBalance($address); // TRX余額 // TRC20合約余額 $contract $tron-contract($usdtContractAddress); $result $contract-balanceOf($address);TRON轉賬有個和EVM區別較大的地方交易需要兩步走。第一步調用/wallet/createtransaction創建交易返回一個包含txID和raw_data的對象第二步把raw_data用私鑰做SECP256K1簽名得到signature第三步把txID、raw_data、signature拼起來廣播到/wallet/broadcasttransaction。iexbase/tron-api的send方法已經把這三步封裝好了$tron-send($toAddress, $amount); // 轉TRX但TRC20轉賬需要走合約的transfer方法tron-api的合約模塊對參數的處理比較死板我實際是直接用Guzzle手動拼的請求// 1. 創建交易 $response Http::post($fullNode . /wallet/triggerconstantcontract, [ owner_address $fromAddressHex, contract_address $usdtContractHex, function_selector transfer(address,uint256), parameter $toAddressHex . str_pad(dechex($amount), 64, 0, STR_PAD_LEFT), visible true, ]); // 2. 對raw_data做簽名 // 3. 廣播TRON轉賬還有一個很多人不注意的機制帶寬資源。賬戶每天有免費帶寬額度但轉賬消耗的帶寬超過免費額度時會燃燒少量TRX作為手續費。如果目標賬戶是首次激活沒有任何交易轉賬需要額外支付激活費用。實測做批量歸集時如果大量地址從來沒被激活過這一塊費用預算要提前算好。3.3 交易簽名、nonce與gas的細節簽名這塊是整個系統安全性的命門單獨拎出來說三個細節。第一簽名計算過程必須獨立成類不要散落在各種Service里。我封裝了TransactionSigner輸入是privateKey rawTransaction輸出是signedHex任何鏈的驅動都走這一個入口。好處是方便審計也方便后續接HSM硬件簽名模塊——只要替換這個類內部實現上層無感知。第二EIP-1559之后ETH主網gas模型變了但BSC和Polygon這些鏈還在用傳統gasPrice。所以我做了一個gasPrice策略先嘗試eth_feeHistory判斷是否支持EIP-1559支持就構造maxPriorityFeePerGas maxFeePerGas不支持就退回gasPrice。如果寫死一種模式換個鏈就容易翻車。第三離線簽名后一定先解碼驗證再廣播。我在廣播前會把簽名的交易eth_call一次驗證from地址是否和預期一致。這個環節救過我一次測試環境私鑰配置錯了如果不是先驗證那筆測試幣就直接打給未知地址了。注意私鑰絕不能出現在日志里。Laravel的日志可能會記錄異常棧如果異常消息里帶了簽名后的tx數據或者私鑰那就完蛋。我在ExceptionHandler里對包含privateKey字段的上下文做了強制過濾。4. 授權與授權轉賬ERC20/TRC20的高級玩法4.1 approve授權怎么調授權approve是ERC20代幣最核心的機制之一代幣持有人允許某個地址通常是合約或歸集賬戶代替自己轉移一定額度的代幣。場景很典型DApp需要用戶把代幣存入合約交易所需要從用戶地址歸集代幣都是靠approvetransferFrom組合實現的。approve的調用方式和transfer幾乎一樣區別只在方法哈希和參數。approve(address spender, uint256 amount)的方法簽名哈希是0x095ea7b3calldata由0x095ea7b3 spender地址(補零到32字節) amount(補零到32字節)拼接$methodHash 0x095ea7b3; $data $methodHash . str_pad(substr($spenderAddress, 2), 64, 0, STR_PAD_LEFT) . str_pad(dechex($amount), 64, 0, STR_PAD_LEFT);然后走和轉賬一樣的簽名、廣播流程。TRON的TRC20也有同樣的approve標準用法一致就是走TRON的合約調用和廣播流程。4.2 transferFrom授權轉賬授權轉賬transferFrom是歸集系統的核心。邏輯是用戶先approve給平臺一個歸集地址比如0xCollector平臺隨后調用transferFrom(userAddress, collectorAddress, amount)把用戶代幣劃轉到歸集地址。這樣用戶不需要把私鑰交給平臺平臺只能移動用戶授權范圍內且授權給該地址的額度。transferFrom(address from, address to, uint256 amount)的方法哈希是0x23b872dd$data 0x23b872dd . str_pad(substr($fromAddress, 2), 64, 0, STR_PAD_LEFT) . str_pad(substr($toAddress, 2), 64, 0, STR_PAD_LEFT) . str_pad(dechex($amount), 64, 0, STR_PAD_LEFT);注意transferFrom的調用方msg.sender必須是之前被approve的那個地址。也就是說發起這筆交易的私鑰對應的地址必須等于approve里的spender參數否則合約會拋ERC20: insufficient allowance。4.3 授權前檢查與額度設計實際業務里我強烈建議加一道“授權前檢查”邏輯調用allowance(owner, spender)看一下當前已授權額度只有額度不夠時才需要再次approve。因為有些代幣合約對重復approve到非零值有特殊處理比如USDT老合約要求先approve到0再approve新值盲目重復授權會失敗。授權額度還有一個坑很多項目圖方便直接approve一個非常大的值比如uint256最大值這是有風險的。如果spender地址被攻擊理論上能把用戶所有代幣轉走。業界更穩妥的做法是每次按需授權用完把額度重置為0或者至少限制在本次交易的金額上限。分布式系統和用戶量大時每筆都授權會增加鏈上開銷這里需要一個折中判斷。我個人實踐下來適合項目初期的方案是授權一個“單用戶最大可歸集額度”比如10000 USDT每次歸集完檢查剩余額度低于下一次預估歸集額時再觸發補充授權。這樣既不會頻繁上鏈也能控制風險敞口。5. Uniapp接入與Laravel接口聯調5.1 API接口設計uniapp在這個架構里只是“殼”真正的鏈上操作全在Laravel后端。為什么不直接在uniapp里調鏈因為私鑰不能放前端。App端一旦逆向私鑰和助記詞就是白送的。所以接口設計遵循一個原則前端永遠只碰地址后端永遠不發光私鑰。典型接口POST /api/wallet/create服務端生成新地址返回地址和助記詞助記詞只展示一次GET /api/wallet/balance?chainethaddress0x...查余額POST /api/wallet/transfer發起轉賬參數是鏈、from地址、to地址、金額POST /api/wallet/approve發起授權POST /api/wallet/collect發起授權轉賬歸集uniapp端請求Regul只要保證在微信小程序或App里都能通就行。我的經驗是不要用axios直接用uni.request否則在微信小程序里要額外處理adapter兼容純屬給自己加活。// uniapp 請求示例 uni.request({ url: https://api.yoursite.com/api/wallet/balance, method: GET, data: { chain: bsc, address: 0x... }, header: { Authorization: Bearer token }, success: (res) { // 渲染到頁面 } });轉賬這類需要確認的操作前端流程是用戶點“提現”→調POST /api/wallet/transfer→后端返回txHash或錯誤信息→前端輪詢GET /api/wallet/tx-status?txHash...等交易確認后展示成功狀態。5.2 CORS錯誤排查實錄結合搜索熱詞聯調階段遇到最多的就是CORS。這一步非常折磨人尤其你是從瀏覽器頁面聯調H5模式時控制臺報Access-Control-Allow-Origin錯誤第一反應往往是后端配置不對但其實很多時候問題出在預檢請求沒有處理好。Laravel的CORS配置在config/cors.php10.x默認是允許所有來源。如果你自定義過paths一定要把api/*加進去不然接口全被攔。一個我印象極深的坑Laravel默認CORS配置不覆蓋storage目錄下的靜態文件。當時有個需求是App里預覽PDF對賬單PDF放在storage里結果H5模式一預覽就報CORS錯誤。排查半天才發現是storage的靜態文件托管沒走cors中間件。解決辦法是在public/storage的Nginx配置里手動加上跨域頭location /storage/ { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization; }另一個常見坑是OPTIONS預檢請求被Laravel的CSRF中間件攔截。Laravel的web中間件組默認帶CSRF驗證如果你的API路由誤用了web中間件POST接口會先收到一個OPTIONS預檢請求然后直接403。解決辦法API路由必須走api中間件組并且在app/Http/Middleware/VerifyCsrfToken.php的$except里排除api/*路徑。6. 常見問題與排查技巧實錄6.1 高頻問題速查表我把這段時間被問得最多的問題整理成了一張表基本覆蓋了這套系統90%的線上問題癥狀可能原因排查方法轉賬一直pending最終失敗gasPrice設置過低用eth_gasPrice動態獲取別寫死連續第二筆交易報nonce too lownonce用了latest狀態改pending狀態或Redis自增鏈上校準ERC20轉賬報insufficient fundsgasLimit不足或鏈上余額不足先用estimateGas估算檢查代幣余額TRX轉賬扣了資源費且多次失敗帶寬/能量不足質押TRX獲取資源或預留手續費approve后transferFrom失敗spender地址不匹配核對approve的spender和簽名地址是否一致uniapp請求H5報CORS錯誤cors配置未覆蓋api路徑檢查config/cors.php路徑檢查Nginx頭TRON合約調用返回CONTRACT_VALIDATE_ERROR參數編碼錯誤確認地址是否轉成hex格式金額是否補零到64位私鑰環境變量多個鏈共用一個鏈的key覆蓋另一個用ETH_PRIVATE_KEY、BSC_PRIVATE_KEY、TRON_PRIVATE_KEY分開配6.2 我踩過的幾個最痛的坑第一TRON地址格式問題。TRON有base58格式T開頭的地址和hex格式41開頭的地址API接口里owner_address、contract_address、parameter里的地址要求格式各不一樣。triggerconstantcontract接口的visible參數控制地址格式最容易出錯的場景是把base58地址塞進了parameter字段——這里必須是hex格式而且不用帶41前綴拼32字節時直接補零。第二測試環境和主網環境切換。如果測試用BSC測試網gas模型和主網不完全一致測試網經常出現一次性成功、上主網就pending的情況。后來我強制要求所有鏈的驅動必須在構造時注入chainId并且根據chainId自動切換地址格式、gas策略、瀏覽器掃描鏈接從源頭杜絕測試配置帶到生產。第三記錄交易哈希要留足上下文。交易哈希本身沒有業務含義我在交易表里同時記錄了chain、from、to、amount、tokenType、txHash、status。排查問題時拿到一個哈希能立刻反查是哪筆業務避免去區塊瀏覽器一個個翻。第四做ERC20歸集時還要注意代幣精度。USDT是6位精度ETH是18位精度轉賬金額如果統一按整數處理USDT的金額會差幾個數量級。我在TokenService里維護了一張精度表所有金額計算先從字符串轉成最小單位整數再拼calldata運算全程用bcmath擴展避免浮點精度丟失。最后再分享一個實用小技巧所有鏈的RPC請求我都會在最外層包一層超時控制。ETH的RPC節點偶爾會無響應HttpRequestManager默認超時時間太短容易導致大批轉賬同時超時我統一調成了30秒并且加了重試機制——重試時如果交易已經廣播成功sendRawTransaction會返回已經存在或者交易已替代的錯誤這時候直接查交易狀態即可不要盲目重發。這套PHPuniapp的多鏈錢包方案目前支撐著我在生產環境里的多個歸集和代付業務累計處理過上萬筆鏈上交易。如果你也在用PHP做錢包后端按照這個思路去做能少走很多彎路。搜索熱詞里出現的CORS和storage文件問題實際就是聯調階段的“絆腳石”提前在Nginx和中間件層面處理好能把后面App、H5、小程序三端聯調的時間壓縮一大半。本文還有配套的精品資源點擊獲取