目录
环境要求:
php7.4+
依赖:
"web3p/ethereum-tx": "^0.4.3", "web3p/web3.php": "dev-master"
参考文档:
1.先查询余额是否充足
// 方法一:web3查询
$rpcUrl = 'https://bsc-dataseed.binance.org/';
// 初始化Web3实例
$web3 = new Web3(new HttpProvider($rpcUrl));
// USDT合约ABI(简化版)
$usdtAbi = '[{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}]';
// 转账前余额查询
$currentBalance = $this->getUsdtBalance($web3, $usdtAbi, $contractAddress, $fromAddress);
if ($currentBalance < $amount) {
throw new Exception("账户余额不足");
}
// 查询余额函数
public function getUsdtBalance($web3, $usdtAbi, $contractAddress, $address) {
$contract = new Contract($web3->provider, $usdtAbi);
$balance = 0;
$contract->at($contractAddress)->call('balanceOf', $address, function ($err, $result) use (&$balance) {
if ($err !== null) {
throw new Exception("余额查询失败: " . $err->getMessage());
}
$balance = $result[0]->toString();
});
return $balance / pow(10, 18); // 转换为标准单位
}
// 方法二:根据币安官方文档https://docs.bscscan.com/api-endpoints/tokens
/**
* 获取账户代币余额
*/
public function getBalance($contract,$address){
$query = [
'module' => 'account',
'action' => 'tokenbalance',
'contractaddress' => $contract,
'address'=>$address,
'tag'=>'latest',
'apikey' => $this->apikey,
];
$data = $this->get($query);
return $data['result'];
}
/**
* 请求处理
*/
private function get($query) {
$client = new Client();
$response = $client->get($this->url."api", [
'query' => $query
]);
if ($response->getStatusCode() != 200) {
throw new Exception('Web3 接口请求异常');
}
return json_decode($response->getBody()->getContents(), true);
}
2.动态获取nonce
// 方法一:官方文档
/**
* 获取nonce
* 参考文档:https://docs.bscscan.com/api-endpoints/geth-parity-proxy#eth_gettransactioncount
*/
private function getNonce($address) {
$query = [
'module' => 'proxy',
'action' => 'eth_getTransactionCount',
'address' => $address,
'tag' => 'pending',
'apikey' => $this->apikey,
];
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 getNonce 错误:'.$data['error']['code'].$data['error']['message']);
}
}
// 方法二:web3
$web3->eth->getTransactionCount($fromAddress, 'latest', function ($err, $nonceResult) use (&$nonce) {
if ($err) throw new Exception($err->getMessage());
$nonce = hexdec($nonceResult->toString());
});
3.动态获取gasPrice
// 方法一:web3
$web3->eth->gasPrice(function ($err, $gasPriceResult) use (&$gasPrice) {
if ($err) throw new Exception($err->getMessage());
$gasPrice = $gasPriceResult->toString();
});
// 方法二:官方文档
/**
* 获取gasPrice
*/
private function gasPrice() {
$query = [
'module' => 'proxy',
'action' => 'eth_gasPrice',
'apikey' => $this->apikey,
];
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 gasPrice 错误:'.$data['error']['code'].$data['error']['message']);
}
}
4.动态获取estimateGas
/**
* 获取estimateGas
* https://docs.bscscan.com/api-endpoints/geth-parity-proxy#eth_estimategas
* @param $txParams
*/
private function estimateGas($txParams) {
$query = array_merge($txParams,[
'module' => 'proxy',
'action' => 'eth_estimateGas',
'apikey' => $this->apikey,
]);
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 estimateGas 错误:'.$data['error']['code'].$data['error']['message']);
}
}
5.发送交易
/**
* https://docs.bscscan.com/api-endpoints/geth-parity-proxy#eth_gettransactioncount
* @throws Exception
*/
private function sendRawTransaction($hex) {
$query = [
'module' => 'proxy',
'action' => 'eth_sendRawTransaction',
'hex' => $hex,
'apikey' => $this->apikey,
];
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 send 错误:'.$data['error']['code'].$data['error']['message']);
}
}
6.完整示例
<?php
namespace app\common\library;
use Exception;
use GuzzleHttp\Client;
use Web3\Contract;
use Web3\Providers\HttpProvider;
use Web3\Utils;
use Web3\Web3;
use Web3p\EthereumTx\Transaction;
class Transfer
{
public $apikey = "xxxx";
public $url = "https://api.bscscan.com/";
public $chainId = 56;
public function send($amount, $toAddress)
{
$contractAddress = '0x55d398326f99059ff775485246999027b3197955'; // usdt的合约地址
$fromAddress = '';
$fromAddressKey = '';
if(empty($fromAddress) || empty($fromAddressKey)){
throw new Exception('请先配置from_address和from_address_key');
}
$rpcUrl = 'https://bsc-dataseed.binance.org/';
// 初始化Web3实例
$web3 = new Web3(new HttpProvider($rpcUrl));
// USDT合约ABI(简化版)
$usdtAbi = '[{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}]';
// 转账前余额查询
$currentBalance = $this->getUsdtBalance($web3, $usdtAbi, $contractAddress, $fromAddress);
if ($currentBalance < $amount) {
throw new Exception("账户余额不足");
}
// 将金额转换为字符串,并移除科学计数法
$amountWei = (int)bcmul($amount, pow(10, 18)); // 转换为wei单位
$nonce = $this->getNonce($fromAddress);
$gasPrice = $this->gasPrice();
// 正确编码data字段(transfer方法签名+参数)
$toParam = str_pad(substr($toAddress, 2), 64, '0', STR_PAD_LEFT); // 地址参数编码
$amountHex = str_pad(Utils::toHex($amountWei), 64, '0', STR_PAD_LEFT); // 32字节金额
$data = [
'0xa9059cbb',
$toParam,
$amountHex,
];
// 构建交易参数
$txParams = [
'from' => $fromAddress,
'to' => $contractAddress,
'value' => '0x0', // ERC20转账必须为0
'gasPrice' => $gasPrice,
'data' => implode('', $data),
];
$gasLimit = $this->estimateGas($txParams);
$txData = [
'nonce' => $nonce,
'from' => strtolower($fromAddress),
'to' => strtolower($contractAddress),
'gas' => $gasLimit, // Gas Limit(合约交互需更高gas)
'gasPrice' => $gasPrice, // 5 Gwei
'chainId' => $this->chainId,
'value' => '0x0',
'data' => implode('', $data)
];
$transaction = new Transaction($txData);
$hex = $transaction->sign($fromAddressKey);
return $this->sendRawTransaction('0x' . $hex);
}
/**
* @throws Exception
*/
private function sendRawTransaction($hex) {
$query = [
'module' => 'proxy',
'action' => 'eth_sendRawTransaction',
'hex' => $hex,
'apikey' => $this->apikey,
];
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 send 错误:'.$data['error']['code'].$data['error']['message']);
}
}
/**
* 获取nonce
*/
private function getNonce($address) {
$query = [
'module' => 'proxy',
'action' => 'eth_getTransactionCount',
'address' => $address,
'tag' => 'pending',
'apikey' => $this->apikey,
];
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 getNonce 错误:'.$data['error']['code'].$data['error']['message']);
}
}
/**
* 获取gasPrice
*/
private function gasPrice() {
$query = [
'module' => 'proxy',
'action' => 'eth_gasPrice',
'apikey' => $this->apikey,
];
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 gasPrice 错误:'.$data['error']['code'].$data['error']['message']);
}
}
/**
* 获取estimateGas
* @param $txParams
*/
private function estimateGas($txParams) {
$query = array_merge($txParams,[
'module' => 'proxy',
'action' => 'eth_estimateGas',
'apikey' => $this->apikey,
]);
$data = $this->get($query);
if(isset($data['result'])){
return $data['result'];
}else{
throw new Exception('Web3 estimateGas 错误:'.$data['error']['code'].$data['error']['message']);
}
}
/**
* 请求处理
*/
private function get($query) {
$client = new Client();
$response = $client->get($this->url."api", [
'query' => $query
]);
if ($response->getStatusCode() != 200) {
throw new Exception('Web3 接口请求异常');
}
return json_decode($response->getBody()->getContents(), true);
}
// 查询余额函数
public function getUsdtBalance($web3, $usdtAbi, $contractAddress, $address) {
$contract = new Contract($web3->provider, $usdtAbi);
$balance = 0;
$contract->at($contractAddress)->call('balanceOf', $address, function ($err, $result) use (&$balance) {
if ($err !== null) {
throw new Exception("余额查询失败: " . $err->getMessage());
}
$balance = $result[0]->toString();
});
return $balance / pow(10, 18); // 转换为标准单位
}
}