【PHP】基于币安链,一个完整的USDT转账示例

发布于:2025-05-10 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

1.先查询余额是否充足

2.动态获取nonce

3.动态获取gasPrice

4.动态获取estimateGas

5.发送交易

6.完整示例


环境要求:

php7.4+

依赖:

"web3p/ethereum-tx": "^0.4.3",
"web3p/web3.php": "dev-master"

参考文档:

Geth Proxy | BscScan

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); // 转换为标准单位
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到