ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP+Laravel多链钱包后端开发实战:ETH/BSC/TRON与uniapp接入

PHP+Laravel多链钱包后端开发实战:ETH/BSC/TRON与uniapp接入 简介这套基于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、小程序三端联调的时间压缩一大半。本文还有配套的精品资源点击获取
返回列表