☰
PHP多链钱包后端实战:ETH/BSC/TRON离线签名与转账
2026/10/14 2:31:12 网站建设 项目流程

简介:这是一套面向Web3钱包DApp开发者的全栈源码,基于Laravel与uni-app实现,覆盖ETH、BSC、TRON三条链的常见链上交互场景。前端部分包含查询余额、转账、Token转账、授权转账与登录签名等操作;后端则提供创建钱包、查询手续费Token与Token余额、转账手续费Token、转账Token、授权、取消授权及授权转账等接口逻辑,适合需要快速搭建多链钱包Demo或学习链上签名与授权机制的开发者参考。资源包共71个文件,以43个PHP后端文件为核心,辅以少量JS、CSS、JSON配置及环境文件,整体约80KB,结构紧凑,便于按模块阅读与二次开发。目前已有259人学习下载,可作为理解Laravel结合Web3多链交互、梳理授权与转账流程的实践素材。

1. 多链钱包后端:为什么 PHP 也能扛住 ETH、BSC、TRON 的签名与转账

很多做 uni-app 的团队,前端页面早就跑通了,卡在后端:用户要查余额、要签名、要转账、要授权,还得同时支持 ETH、BSC、TRON 三条链。第一反应往往是「PHP 干不了这个,得上 Node 或者 Go」。我一开始也这么想,直到把一套基于 Laravel 的多链服务真正跑在生产上,才发现关键根本不在语言,而在你有没有把「离线签名」和「节点交互」这两件事拆干净。

这套方案的核心思路是:Laravel 只负责业务编排、密钥管理、交易组装和落库,真正的链上交互全部交给 RPC 节点,签名全部在服务端离线完成,私钥永远不出服务器。ETH 和 BSC 共用一套 EVM 逻辑,TRON 单独走一套,前端 uni-app 只调你自己的接口,不碰任何私钥。适合谁?适合已经有 PHP 团队、想快速给 App 加上多链钱包能力、又不想重构成微服务的中小项目。下面我把选型、依赖、代码和踩过的坑一次讲清楚。

2. 环境与依赖选型:Laravel 侧到底要装什么

2.1 三条链的 SDK 选择与理由

ETH 和 BSC 都是 EVM 兼容链,交易结构、ABI 编码、签名算法(secp256k1 + keccak256)完全一致,唯一区别是 chainId 和 RPC 地址。所以这两条链可以共用一套代码,只需要在配置里区分。PHP 侧最成熟的方案是web3p/web3.php配合web3p/ethereum-tx,前者负责 RPC 调用和 ABI 编解码,后者负责离线组装和签名交易。

TRON 就不一样了。它的地址格式是 Base58Check,签名用的是 secp256k1 但哈希方式不同,交易结构里还有 refBlockBytes、expiration 这些 EVM 没有的字段。PHP 侧我一般用iexbase/tron-api,它封装了 TRX 转账、TRC20 转账、授权和资源查询。注意这个库对 TRC20 的授权转账(transferFrom)支持需要自己补一层 ABI 编码,后面会讲。

链用途推荐库关键依赖
ETH余额/签名/转账/授权web3p/web3.php + web3p/ethereum-txgmp、secp256k1
BSC同上同 ETH,仅改 chainId 和 RPC同上
TRON余额/签名/转账/授权iexbase/tron-apigmp、secp256k1

提示:secp256k1这个 PHP 扩展必须装,否则签名会退化成纯 PHP 实现,速度慢十倍以上,高并发下直接拖垮接口。

2.2 安装命令与节点配置

# 安装 EVM 相关依赖 composer require web3p/web3.php web3p/ethereum-tx # 安装 TRON 依赖 composer require iexbase/tron-api # 确认 secp256k1 扩展已加载 php -m | grep secp256k1

装完之后在config/chains.php里把三条链的配置集中管理,不要散落在代码里:

<?php // config/chains.php return [ 'eth' => [ 'rpc' => env('ETH_RPC', 'https://你的ETH节点'), 'chain_id'=> 1, 'explorer'=> 'https://etherscan.io', ], 'bsc' => [ 'rpc' => env('BSC_RPC', 'https://你的BSC节点'), 'chain_id'=> 56, 'explorer'=> 'https://bscscan.com', ], 'tron' => [ 'rpc' => env('TRON_RPC', 'https://你的TRON节点'), 'api_key' => env('TRON_API_KEY', ''), ], ];

这里的chain_id是签名时写进交易体的,ETH 是 1,BSC 是 56,写错了节点会直接拒绝广播,报「invalid chain id」。RPC 地址建议用自建节点或者付费的稳定节点服务,公共节点在高峰期经常超时,这是血泪经验。

2.3 私钥管理与加密存储

私钥绝对不能明文存数据库。我一般用 Laravel 的encrypt()配合一个独立的WALLET_KEY环境变量做双层加密,数据库里存密文,解密只在签名的那一瞬间发生。

<?php namespace App\Services; use Illuminate\Support\Facades\Crypt; class KeyVault { // 加密私钥后入库 public function store(string $privateKey): string { // 先用应用 key 加密,再用独立的钱包 key 做一次 HMAC 校验 $encrypted = Crypt::encryptString($privateKey); return $encrypted; } // 签名前解密,用完立即 unset public function retrieve(string $cipher): string { return Crypt::decryptString($cipher); } }

逻辑说明:Crypt::encryptString用的是 Laravel 的APP_KEY,AES-256-CBC。参数上唯一要注意的是APP_KEY一旦泄露,所有私钥等于裸奔,所以生产环境这个 key 必须和代码仓库分离。解密出来的私钥在签名函数里用完立刻unset($privateKey),不要留在内存里等 GC。

3. ETH 与 BSC:余额查询、离线签名、转账与授权

3.1 余额查询:原生币和 ERC20 分开处理

原生币余额用eth_getBalance,ERC20 余额要调合约的balanceOf。很多人第一次写会把两者混在一起,结果 ERC20 查出来是 0。

<?php use Web3\Web3; use Web3\Contract; class EvmBalance { protected Web3 $web3; public function __construct(string $rpc) { $this->web3 = new Web3($rpc); } // 查原生币余额,返回的是 wei,需要除以 1e18 public function native(string $address): string { $balance = '0'; $this->web3->eth->getBalance($address, function ($err, $result) use (&$balance) { if ($err !== null) { throw new \Exception('查询失败: ' . $err->getMessage()); } $balance = $result->toString(); }); return $balance; } // 查 ERC20 余额,decimals 一般是 18,但 USDT 是 6 public function erc20(string $token, string $address, int $decimals = 18): string { $contract = new Contract($this->web3->provider, [ 'balanceOf' => [ 'inputs' => [['name' => 'owner', 'type' => 'address']], 'outputs' => [['name' => 'balance', 'type' => 'uint256']], ], ]); $contract->at($token); $balance = '0'; $contract->call('balanceOf', $address, function ($err, $result) use (&$balance) { if ($err !== null) { throw new \Exception('合约调用失败: ' . $err->getMessage()); } $balance = $result[0]->toString(); }); return $balance; } }

逻辑说明:getBalance返回的是十六进制字符串,web3p会自动转成十进制。ERC20 的decimals一定要从合约读或者写死在配置里,USDT 在 ETH 上是 6 位,在 BSC 上也是 6 位,如果你按 18 位去格式化,余额会少 12 个数量级,这个坑我见过不止一次。

3.2 离线签名:nonce、gas 和 chainId 三个必调参数

签名是整套方案里最容易翻车的地方。EVM 交易签名需要 nonce、gasPrice、gasLimit、to、value、data、chainId 七个字段,少一个或者错一个,广播就会失败。

<?php use Web3p\EthereumTx\Transaction; class EvmSigner { // 组装并签名一笔 ETH/BSC 转账 public function signTransfer( string $privateKey, string $to, string $valueWei, int $nonce, int $chainId, string $gasPrice = '5', // 单位 gwei string $gasLimit = '21000' ): string { $tx = new Transaction([ 'nonce' => '0x' . dechex($nonce), 'gasPrice' => '0x' . dechex((int)($gasPrice * 1e9)), 'gasLimit' => '0x' . dechex((int)$gasLimit), 'to' => $to, 'value' => '0x' . dechex((int)$valueWei), 'data' => '', 'chainId' => $chainId, ]); // sign 返回的是已签名的原始交易十六进制串 return '0x' . $tx->sign($privateKey); } }

逻辑说明:nonce必须从链上eth_getTransactionCount拿,而且要用pending标签,否则连续转账时第二笔会因为 nonce 重复被拒。gasPrice单位是 gwei,乘以 1e9 转成 wei 再转十六进制。gasLimit原生币转账固定 21000,ERC20 转账一般 60000 起步,授权操作要 80000 以上。chainId写错会报「invalid sender」,这是最常见的签名失败原因。

3.3 ERC20 授权与授权转账:approve 和 transferFrom

授权是 DeFi 场景的核心。用户先approve给某个合约额度,然后合约再调transferFrom把币转走。这两步的 data 字段需要 ABI 编码。

<?php use Web3\Contract; class Erc20Approve { // 生成 approve 的 data public function approveData(string $spender, string $amountWei): string { $contract = new Contract(new \Web3\Providers\HttpProvider( new \Web3\RequestManagers\HttpRequestManager('https://你的节点') ), [ 'approve' => [ 'inputs' => [ ['name' => 'spender', 'type' => 'address'], ['name' => 'value', 'type' => 'uint256'], ], 'outputs' => [['name' => '', 'type' => 'bool']], ], ]); // getData 只编码,不发送 return $contract->getData('approve', $spender, $amountWei); } }

逻辑说明:getData返回的是0x095ea7b3开头的 calldata,前面是方法选择器,后面是参数。把这个 data 塞进上一节的signTransfer的data字段,to填代币合约地址,value填 0,就是一笔授权交易。授权转账则是合约侧调transferFrom,你的后端只需要构造这个 calldata 并签名,逻辑一样。注意授权额度不要随便给2^256-1,一旦合约有漏洞,用户的币会被全部转走,这是行业里反复出现的教训。

4. TRON:地址格式、资源模型和 TRC20 转账的差异

4.1 TRON 地址与 EVM 地址的转换

TRON 地址是T开头的 Base58Check 编码,底层其实是和 EVM 一样的 20 字节。两者可以互转,但格式不对节点会直接报错。

<?php use IEXBase\TronAPI\Tron; class TronAddress { // EVM 的 0x 地址转 TRON 的 T 地址 public function evmToTron(string $evmAddress): string { // 去掉 0x,前面补 41(TRON 主网前缀) $hex = '41' . substr($evmAddress, 2); return $this->base58CheckEncode($hex); } // TRON 的 T 地址转 EVM 的 0x 地址 public function tronToEvm(string $tronAddress): string { $hex = $this->base58CheckDecode($tronAddress); // 去掉前缀 41,补 0x return '0x' . substr($hex, 2); } }

逻辑说明:TRON 主网地址前缀是0x41,测试网是0xa0。Base58Check 编码里带了 4 字节校验和,手写容易出错,建议直接用iexbase/tron-api自带的Tron::addressToHex和Tron::hexToAddress。这个转换在跨链场景里特别重要,用户从 ETH 复制过来的地址直接填到 TRON 转账里,币就丢了。

4.2 带宽与能量:TRON 转账为什么需要「资源」

TRON 和 EVM 最大的区别是资源模型。TRX 转账消耗带宽(Bandwidth),TRC20 转账消耗能量(Energy)。账户每天有免费带宽,但 TRC20 转账的能量消耗很大,没有能量就要烧 TRX 抵扣。

<?php use IEXBase\TronAPI\Tron; class TronTransfer { protected Tron $tron; public function __construct(string $rpc, string $apiKey) { $this->tron = new Tron($rpc, $apiKey); } // TRX 转账 public function sendTrx(string $from, string $to, float $amount): string { $this->tron->setPrivateKey($from); $result = $this->tron->send($to, $amount); return $result['txid']; } // TRC20 转账,需要合约地址 public function sendTrc20(string $from, string $to, string $contract, float $amount, int $decimals = 6): string { $this->tron->setPrivateKey($from); $result = $this->tron->getContract($contract)->transfer($to, $amount * pow(10, $decimals)); return $result['txid']; } }

逻辑说明:send和transfer内部会自动处理 refBlock、expiration 和签名。decimals对 TRC20 尤其重要,USDT-TRC20 是 6 位,写错金额会差几个数量级。如果账户没有能量,transfer会失败并提示「energy not enough」,这时候要么租能量,要么在账户里留够 TRX 让它烧。

4.3 TRC20 授权转账的 ABI 补全

iexbase/tron-api对approve和transferFrom的封装不完整,需要自己拼 calldata。方法和 EVM 一样,用approve(address,uint256)的选择器0x095ea7b3。

<?php class TronApprove { // 手动构造 TRC20 approve 的 calldata public function approveData(string $spender, string $amount): string { // 方法选择器 $methodId = '095ea7b3'; // 地址去掉 T 前缀转成 32 字节 $spenderHex = str_pad(substr($this->tronToHex($spender), 2), 64, '0', STR_PAD_LEFT); // 金额转成 32 字节十六进制 $amountHex = str_pad(dechex($amount), 64, '0', STR_PAD_LEFT); return '0x' . $methodId . $spenderHex . $amountHex; } }

逻辑说明:TRON 的 calldata 编码和 EVM 完全一致,都是 4 字节选择器加 32 字节对齐参数。区别在于 TRON 的交易体里要额外填contract_address和fee_limit。fee_limit是你能接受的最大 TRX 燃烧量,设太小交易会失败,设太大又怕被恶意合约吃掉,一般 TRC20 转账设 100 TRX 以内。

5. 避坑与排查:多链钱包后端最常见的 5 个翻车点

5.1 nonce 重复导致连续转账失败

现象:用户快速点两次转账,第一笔成功,第二笔报「replacement transaction underpriced」或者一直 pending。原因:两笔交易用了同一个 nonce,节点认为你在替换交易。解决:nonce 从eth_getTransactionCount用pending标签拿,并且在服务端对同一地址加锁,保证串行发交易。

5.2 gasPrice 设太低交易卡住

现象:交易广播出去了,但几个小时都不上链。原因:gasPrice 低于当前网络均价,矿工不打包。解决:动态获取eth_gasPrice,然后乘以 1.2 作为缓冲。BSC 的 gasPrice 波动比 ETH 小,但也不能写死 5 gwei。

5.3 TRON 能量不足导致 TRC20 转账失败

现象:TRX 转账正常,TRC20 转账报「energy not enough」。原因:TRC20 合约调用消耗能量,账户没有足够能量也没有足够 TRX 抵扣。解决:转账前先查账户能量和 TRX 余额,不够就提示用户或者走能量租赁。这个在测试网经常被忽略,因为测试网能量免费。

5.4 地址格式混用导致币丢失

现象:用户把 ETH 地址填到 TRON 转账里,交易成功但币不见了。原因:TRON 和 EVM 地址底层都是 20 字节,但格式不同,节点不会帮你校验。解决:后端在构造交易前强制校验地址格式,ETH/BSC 必须是0x开头 42 位,TRON 必须是T开头 34 位,不匹配直接拒绝。

5.5 私钥解密后未及时清理

现象:服务器内存里长期存在明文私钥,被内存 dump 工具抓到。原因:解密后的私钥变量没有及时unset,PHP 的 GC 不会立刻回收。解决:签名函数里用完立即unset,并且把签名逻辑封装成独立进程,签完就退出,减少私钥在内存里的停留时间。

6. 进阶:把签名服务拆成独立进程与批量转账技巧

走到生产环境,你会发现把签名逻辑放在 Laravel 的 HTTP 请求里是个隐患:私钥在 Web 进程内存里,一旦有漏洞就是灾难。我现在的做法是把签名拆成一个独立的 CLI 进程,Laravel 通过队列把待签交易丢过去,签名进程签完把原始交易串回传,再由另一个进程广播。

# 启动签名 worker php artisan wallet:sign-worker --chain=eth # 启动广播 worker php artisan wallet:broadcast-worker --chain=eth

批量转账是另一个高频需求,比如给几百个用户发空投。逐个签名逐个广播太慢,我一般用「同一 nonce 批量签名 + 顺序广播」的方式:先拿一个起始 nonce,然后对每笔交易 nonce 递增,全部签好之后按顺序广播。这样能把签名和广播解耦,签名可以并行,广播必须串行。

<?php class BatchTransfer { // 批量签名,返回原始交易数组 public function signBatch(string $privateKey, array $transfers, int $startNonce, int $chainId): array { $signed = []; foreach ($transfers as $i => $t) { $signed[] = $this->signer->signTransfer( $privateKey, $t['to'], $t['value'], $startNonce + $i, // nonce 递增 $chainId ); } return $signed; } }

逻辑说明:startNonce从链上拿一次就行,后面的 nonce 全部本地递增。广播的时候必须按数组顺序发,因为 nonce 小的必须先上链,否则后面的会卡在 mempool 里。这个技巧在空投场景下能把几百笔转账的耗时从十几分钟压到一两分钟。

验证方法上,我习惯在测试网先跑一遍完整流程:查余额、签名、广播、确认上链、再查一次余额。ETH 用 Sepolia,BSC 用测试网,TRON 用 Nile 测试网。测试网的币不值钱,但流程和主网完全一致,能提前发现 90% 的问题。

最后说个我自己的习惯:任何涉及私钥的代码,我都不在本地 IDE 里直接跑,而是丢到隔离的测试环境,用测试私钥跑通再换生产。私钥这东西没有后悔药,一次泄露就是全部归零。这套方案值不值得做?如果你已经有 PHP 团队,答案是值得,因为省掉了一次重构;如果你从零开始,也可以先按这个结构跑通,再决定要不要换语言。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询