资讯动态

PHP+Laravel+Uniapp多链钱包源码:ETH/BSC/TRON余额查询、签名、转账与授权转账实现

发布时间:2026/10/14 2:02:35 来源:尧图企业网站定制
简介这是一套面向Web3去中心化应用开发者的全栈源码基于Laravel后端与uniapp前端覆盖ETH、BSC、TRON三条主流公链的常见钱包交互场景。前端部分实现查询余额、转账、Token转账、授权转账与登录签名等DApp常用操作后端则包含创建钱包、查询手续费Token与Token余额、转账手续费Token、转账Token、授权Token、取消授权及授权转账等接口逻辑适合有一定PHP与区块链基础、希望快速搭建多链钱包Demo或二次开发的开发者参考。资源包共71个文件以43个PHP源码为核心辅以少量JS、CSS、JSON配置及env、artisan等Laravel工程文件整体约80KB结构紧凑、便于按模块阅读。目前已有259人学习下载可帮助读者理解多链余额查询、签名与授权转账的完整实现思路并作为项目脚手架快速落地。1. 从一笔「余额查不到」的工单说起这套源码到底解决什么问题去年底接了个外包咨询某公司做的是一个多链钱包的运营后台前端用 uniapp 打包成 App 和小程序后端是 Laravel。上线两周后客服炸了用户充值 ETH 到账了后台余额还是 0TRON 上的 USDT 转账明明广播成功订单状态却卡在「待确认」。我拉下代码一看问题不在业务逻辑而在链交互层——余额查询用的是全节点eth_getBalance轮询TRON 那边干脆没做资源抵押判断签名环节私钥还在服务端明文过了一遍日志。这套「php uniapp laravel 实现 ETH、BSC、TRON 查询余额、签名、转账、授权、授权转账」的源码解决的正是这类多链钱包后端最脏最累的那一层把三条链的 RPC 调用、离线签名、交易构造、授权approve与授权转账transferFrom统一成 Laravel 里可复用的服务类前端 uniapp 只负责拿地址、展示余额、发起请求。适合谁做钱包 App、交易所充值提现后台、DApp 运营工具的后端同学尤其是被 TRON 的带宽能量和 ETH 的 nonce 管理折磨过的人。它不是什么开箱即用的商业系统而是一套能让你少走两个月弯路的链交互骨架。2. 三条链的 RPC 与签名差异为什么不能一套代码硬套2.1 ETH 与 BSC 的 EVM 同构但 nonce 和 gas 策略要分开ETH 和 BSC 都是 EVM 兼容链eth_getBalance、eth_sendRawTransaction、eth_call这些方法名完全一样所以很多人图省事把 RPC 地址做成配置项就以为搞定了。实际翻车点在于BSC 出块约 3 秒ETH 约 12 秒nonce 的获取时机和 pending 交易的处理策略完全不同。BSC 上如果连续发两笔转账第二笔的 nonce 必须等第一笔进 mempool 后才能拿到否则会报nonce too low。常见做法是封装一个EvmService把链 ID、RPC 端点、gas 倍率作为构造参数。下面是我一般会用的余额查询与 nonce 获取写法?php // app/Services/EvmService.php namespace App\Services; use Illuminate\Support\Facades\Http; class EvmService { protected string $rpc; protected int $chainId; public function __construct(string $rpc, int $chainId) { $this-rpc $rpc; $this-chainId $chainId; } // 统一 RPC 请求避免每个方法重复写 curl protected function call(string $method, array $params []) { $res Http::post($this-rpc, [ jsonrpc 2.0, id 1, method $method, params $params, ])-json(); if (isset($res[error])) { throw new \Exception($method . failed: . $res[error][message]); } return $res[result]; } // 查询原生币余额返回 wei 的十六进制 public function getBalance(string $address): string { return $this-call(eth_getBalance, [$address, latest]); } // 查询 ERC20/BEP20 余额data 为 balanceOf(address) 的编码 public function getTokenBalance(string $token, string $address): string { $data 0x70a08231 . str_pad(substr($address, 2), 64, 0, STR_PAD_LEFT); return $this-call(eth_call, [ [to $token, data $data], latest, ]); } // 获取 pending nonce注意用 pending 而不是 latest public function getNonce(string $address): string { return $this-call(eth_getTransactionCount, [$address, pending]); } }逻辑说明call方法把 JSON-RPC 的请求体固定下来避免每个业务方法都写一遍 HTTP 调用。getTokenBalance里的0x70a08231是balanceOf(address)的函数选择器后面补 64 位十六进制地址这是 ERC20 标准里最常被写错的地方——地址要去掉0x再左补零。getNonce必须用pending用latest会导致连续转账时第二笔 nonce 重复。参数说明$rpc传 ETH 或 BSC 的节点地址$chainId用于签名时的 EIP-155 防重放ETH 主网是 1BSC 是 56。gas 倍率建议 ETH 用 1.2BSC 用 1.1因为 BSC 的 gas 波动小但出块快倍率太高反而浪费。2.2 TRON 的账户模型与资源抵押和 EVM 完全是两回事TRON 没有 nonce交易靠ref_block_bytes和expiration防重放没有 gas转账消耗的是带宽Bandwidth和能量Energy。如果账户没有抵押 TRX 获取资源每笔 TRX 转账要烧掉约 0.1 TRXUSDT 转账要烧掉十几到几十 TRX 不等。很多源码在这里偷懒直接让用户账户里留 TRX 当手续费结果用户把 TRX 转走后 USDT 就转不动了。TRON 的余额查询走的是 TronGrid 的 HTTP API不是 JSON-RPC。签名用的是 secp256k1但交易构造的 protobuf 结构必须严格按 Tron 的格式来。下面是一个查询 TRX 和 TRC20 余额的封装?php // app/Services/TronService.php namespace App\Services; use Illuminate\Support\Facades\Http; class TronService { protected string $apiKey; protected string $baseUrl https://api.trongrid.io; public function __construct(string $apiKey) { $this-apiKey $apiKey; } // 查询 TRX 余额返回 sun1 TRX 1e6 sun public function getTrxBalance(string $address): int { $res Http::withHeaders([TRON-PRO-API-KEY $this-apiKey]) -get($this-baseUrl . /v1/accounts/ . $address) -json(); // 账户未激活时 data 为空数组余额按 0 处理 return $res[data][0][balance] ?? 0; } // 查询 TRC20 余额需要调用合约的 balanceOf public function getTrc20Balance(string $contract, string $address): int { $res Http::withHeaders([TRON-PRO-API-KEY $this-apiKey]) -post($this-baseUrl . /wallet/triggerconstantcontract, [ owner_address $address, contract_address $contract, function_selector balanceOf(address), parameter str_pad(substr($address, 2), 64, 0, STR_PAD_LEFT), visible true, ]) -json(); // 返回的是十六进制字符串需要转成十进制 $hex $res[constant_result][0] ?? 0; return hexdec($hex); } }逻辑说明TRON 的账户查询接口在账户未激活时返回空data必须用?? 0兜底否则会抛数组越界。TRC20 余额查询用triggerconstantcontract模拟调用visible: true表示地址用 Base58 格式而不是 hex这个参数漏了会一直返回空结果。hexdec转换时要注意大数溢出PHP 的 int 在 64 位系统上够用但如果是 32 位环境需要换bcmath。参数说明$apiKey是 TronGrid 的 API Key免费版有频率限制生产环境建议自建节点或买付费套餐。$contract是 TRC20 合约地址USDT 主网合约是TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t。2.3 离线签名私钥永远不要出现在日志和异常里三条链的签名库不一样。ETH/BSC 用web3p/ethereum-tx或kornrunner/ethereum-offline-signerTRON 用iexbase/tron-api或自己实现 protobuf 编码。不管用哪个有一条铁律私钥只在签名函数内部解密签名完立即销毁变量异常堆栈里绝对不能带私钥。?php // 以 ETH 转账签名为例 use Web3p\EthereumTx\Transaction; public function signEthTransfer(string $privateKey, string $to, string $valueWei, string $nonce, string $gasPrice, int $chainId): string { $tx new Transaction([ nonce $nonce, gasPrice $gasPrice, gasLimit 0x5208, // 21000原生币转账固定值 to $to, value $valueWei, chainId $chainId, data , ]); // sign 内部会做 EIP-155 处理返回 0x 开头的 raw transaction $signed $tx-sign($privateKey); // 主动清理敏感变量虽然 PHP 的 GC 不保证立即回收但能减少暴露面 unset($privateKey); return 0x . $signed; }逻辑说明gasLimit对原生币转账固定 21000写多了浪费、写少了失败。chainId必须传否则签出来的交易在 ETH 和 BSC 之间可以重放。unset($privateKey)是心理安慰大于实际作用真正要做的是在框架层面禁止把$privateKey写进Log::info或异常上下文。参数说明$valueWei是转账金额的 wei 值前端传过来通常是字符串不要用浮点数转换会丢精度。$gasPrice从eth_gasPrice拿再乘以倍率。3. 授权与授权转账approve 和 transferFrom 的完整链路3.1 approve 的本质是修改 allowance 映射授权approve在链上就是调用代币合约的approve(spender, amount)把allowance[owner][spender]设成你指定的值。很多钱包源码在这里只写了一个「授权」按钮没告诉用户授权额度可以改结果用户授权了无限额度2^256-1后面想撤销还得再发一笔交易把额度改成 0。// 构造 approve 的 data public function buildApproveData(string $spender, string $amount): string { // approve(address,uint256) 选择器 $selector 0x095ea7b3; $spenderPadded str_pad(substr($spender, 2), 64, 0, STR_PAD_LEFT); // amount 转成 64 位十六进制注意大数用 bcmath 或 gmp $amountPadded str_pad(gmp_strval(gmp_init($amount), 16), 64, 0, STR_PAD_LEFT); return $selector . $spenderPadded . $amountPadded; }逻辑说明0x095ea7b3是approve(address,uint256)的函数选择器两个参数各占 32 字节。amount如果传2^256-1就是无限授权传具体数值就是限额授权。用gmp而不是dechex是因为dechex在超过 PHP_INT_MAX 时会返回错误结果。参数说明$spender是被授权方地址通常是你的业务合约或中转地址。$amount是最小单位的数值USDT 是 6 位小数ETH 上的 ERC20 大多是 18 位。3.2 transferFrom 需要先检查 allowance 和余额授权转账transferFrom不是直接转而是合约检查allowance[from][msg.sender] amount且balanceOf[from] amount两个条件都满足才执行。常见翻车是用户授权了 100 USDT但余额只有 50转账直接 revert错误信息还不明显。// 查询 allowance public function getAllowance(string $token, string $owner, string $spender): string { // allowance(address,address) 选择器 0xdd62ed3e $data 0xdd62ed3e . str_pad(substr($owner, 2), 64, 0, STR_PAD_LEFT) . str_pad(substr($spender, 2), 64, 0, STR_PAD_LEFT); return $this-call(eth_call, [[to $token, data $data], latest]); }逻辑说明0xdd62ed3e是allowance(address,address)的选择器。返回的是十六进制字符串业务层要转成十进制再和转账金额比较。如果 allowance 不足要么让用户重新 approve要么用increaseAllowance部分代币支持。参数说明$owner是代币持有者$spender是调用 transferFrom 的地址通常是你的业务合约。TRON 上的 TRC20 授权逻辑一样但接口调用走 TronGrid 的triggerconstantcontract。3.3 TRON 的授权转账要额外算能量消耗TRON 的 TRC20 transferFrom 消耗的能量比普通 transfer 更高因为要多读一次 allowance 映射。如果账户能量不足会烧掉更多 TRX。我一般会在发起前先调estimateEnergy预估不够就提示用户抵押 TRX 或直接扣手续费。// TRON 能量预估 public function estimateEnergy(string $contract, string $from, string $to, int $amount): int { $res Http::withHeaders([TRON-PRO-API-KEY $this-apiKey]) -post($this-baseUrl . /wallet/triggerconstantcontract, [ owner_address $from, contract_address $contract, function_selector transferFrom(address,address,uint256), parameter str_pad(substr($from, 2), 64, 0, STR_PAD_LEFT) . str_pad(substr($to, 2), 64, 0, STR_PAD_LEFT) . str_pad(dechex($amount), 64, 0, STR_PAD_LEFT), visible true, ]) -json(); return $res[energy_used] ?? 0; }逻辑说明triggerconstantcontract模拟执行不消耗真实资源返回的energy_used就是预估消耗。如果账户能量低于这个值差额会按 TRX 燃烧费率约 420 sun/能量。dechex($amount)在金额超过 2^31 时会溢出生产环境要用gmp_strval(gmp_init($amount), 16)。参数说明$from是代币持有者$to是接收方$amount是最小单位。TRON 的 USDT 是 6 位小数所以 1 USDT 传 1000000。4. 避坑与排查那些让我半夜爬起来改代码的瞬间4.1 现象ETH 转账一直 pendingnonce 越积越多原因用了latest拿 nonce前一笔还没打包就发了下一笔两笔 nonce 相同后一笔被节点拒绝但前端显示已提交。解决统一用pending拿 nonce并在数据库里维护一个nonce_lock表同一地址的转账串行化。如果已经卡住用相同 nonce 发一笔 0 ETH 的自转账覆盖gasPrice 提高 20%。4.2 现象TRON 转账报「Account resource insufficient」原因账户没有抵押 TRX 获取能量或者能量被其他交易消耗完了。解决在发起转账前调estimateEnergy能量不足时要么让用户抵押要么在业务层扣一笔 TRX 作为手续费补贴。注意 TRON 的能量是 24 小时线性恢复的不是实时回满。4.3 现象BSC 上 approve 成功但 transferFrom 一直失败原因BSC 的部分代币合约尤其是土狗币不标准allowance返回值不是标准 uint256或者transferFrom有额外的黑名单/税率逻辑。解决先用eth_call模拟 transferFrom看 revert 原因如果是非标准代币在业务层加白名单只支持主流币种。4.4 现象签名后的 raw transaction 广播返回「invalid sender」原因chainId 传错或者签名库的 EIP-155 处理有 bug。ETH 主网 chainId 是 1BSC 是 56测试网分别是 5 和 97。解决在签名前打印 chainId 确认用web3p/ethereum-tx的话注意版本老版本对 EIP-155 支持不完整。4.5 现象TRC20 余额查询返回 0但链上明明有币原因triggerconstantcontract的visible参数没传true地址被当成 hex 格式解析查不到。或者合约地址写错USDT 主网合约是TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t不是那个TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t之外的任何变体。解决统一用 Base58 地址visible固定true合约地址做成配置项并加校验。5. 进阶把三条链的签名服务做成可插拔的 Laravel 包5.1 用接口隔离链差异业务层不感知具体链这套源码如果直接写死三个 Service后面加 SOL、TON 会越来越乱。我一般会定义一个ChainSignerInterface把getBalance、signTransfer、broadcast、estimateFee四个方法抽象出来每条链一个实现类通过 Laravel 的服务容器按链名解析。?php // app/Contracts/ChainSignerInterface.php namespace App\Contracts; interface ChainSignerInterface { public function getBalance(string $address, string $token ): string; public function signTransfer(string $privateKey, string $to, string $amount, array $options []): string; public function broadcast(string $signedTx): string; public function estimateFee(string $from, string $to, string $amount, string $token ): array; }逻辑说明$token为空表示原生币传合约地址表示代币。$options放链特有的参数比如 ETH 的 gasPrice、TRON 的 memo。estimateFee返回预估手续费和资源消耗前端用来展示。参数说明实现类在AppServiceProvider里绑定比如$this-app-bind(chain.eth, EvmSigner::class)业务层用app(chain. . $chain)拿实例。5.2 用队列和幂等键防止重复广播链上交易最怕重复广播。同一笔提现请求如果因为网络超时重试了三次可能发出三笔交易。我一般会在广播前生成一个idempotency_key存 Redis 并设 10 分钟过期广播成功后把 txHash 写回数据库。如果 Redis 里已有 key 且状态是broadcasting直接返回「处理中」不重复发。// 广播前的幂等检查 public function broadcastWithIdempotency(string $key, string $signedTx, ChainSignerInterface $signer): string { $lock Cache::lock(broadcast: . $key, 600); if (!$lock-get()) { throw new \Exception(duplicate broadcast request); } try { $txHash $signer-broadcast($signedTx); Cache::put(txhash: . $key, $txHash, 3600); return $txHash; } finally { $lock-release(); } }逻辑说明Cache::lock是 Laravel 的原子锁基于 Redis 的 SET NX 实现。拿到锁才广播广播完释放。如果广播失败锁也会释放允许重试。txhash缓存 1 小时方便前端轮询。参数说明$key建议用「链名 用户 ID 业务订单号」拼接保证全局唯一。锁的过期时间 600 秒要大于广播的超时时间否则锁提前释放会导致重复广播。5.3 验证方法用测试网跑通全链路再上主网这套源码的验证顺序我建议是先 ETH Sepolia 测试网跑通余额查询和转账再 BSC Testnet 跑 approve 和 transferFrom最后 TRON Nile 测试网跑能量预估和 TRC20 转账。每条链至少跑 10 笔连续交易观察 nonce 递增、能量消耗、gas 波动是否符合预期。链测试网chainId水龙头ETHSepolia11155111官方 faucetBSCTestnet97官方 faucetTRONNile-官方 faucet提示测试网的 RPC 节点不稳定建议用多个端点做故障转移代码里把 RPC 列表做成数组失败自动切换下一个。从那以后我每次接多链钱包的活都强制先跑一遍测试网的连续转账确认 nonce 和能量逻辑没问题再碰主网。这套源码的价值不在于它写了多少行而在于它把三条链最容易翻车的地方都踩过一遍你拿过去改改配置就能用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑