Skip to content

Instantly share code, notes, and snippets.

@BusinessDuck
Created December 29, 2024 19:23
Show Gist options
  • Save BusinessDuck/667e3e9832b80dc512aaa0189762aecc to your computer and use it in GitHub Desktop.
Save BusinessDuck/667e3e9832b80dc512aaa0189762aecc to your computer and use it in GitHub Desktop.
Php example
<?php
namespace App;
use Web3\Web3;
use Web3\Providers\HttpProvider;
use Web3\RequestManagers\RequestManager;
use Web3\Contract;
use Web3\Utils;
use Web3\Validators\AddressValidator;
// библиотека для подписи транзакций
use EthereumTx\Transaction as EthTx;
/**
* Класс LotteryV2 — реализация методов лотереи PancakeSwap
*/
class LotteryV2
{
/** @var Web3 */
protected $web3;
/** @var Contract */
protected $contract;
/** @var string Приватный ключ для подписания транзакций (0x...) */
protected $privateKey;
/** @var string Адрес (from), который соответствует приватному ключу */
protected $fromAddress;
/** @var string Адрес контракта лотереи */
protected $lotteryAddress;
/** @var int Chain ID (BSC Mainnet = 56) */
protected $chainId = 56;
/** @var int Лимит газа по умолчанию при отправке транзакций */
protected $gas = 3000000;
/** @var string Цена газа (Wei) */
protected $gasPriceWei;
/**
* Конструктор.
*
* @param string $lotteryAddress Адрес контракта лотереи
* @param string $rpcUrl RPC, например https://bsc-dataseed.binance.org/
* @param string $privateKey Ваш приватный ключ (0x...)
* @param string $fromAddress Адрес (0x...), которому принадлежит privateKey
* @param string $lotteryAbiJsonPath Путь к JSON-файлу, содержащему ABI контракта
* @param int $gas Лимит газа
* @param int $gasPriceGwei Цена газа в Gwei
*/
public function __construct(
string $lotteryAddress,
string $rpcUrl,
string $privateKey,
string $fromAddress,
string $lotteryAbiJsonPath,
int $gas = 3000000,
int $gasPriceGwei = 5
) {
// Инициализация Web3
$this->web3 = new Web3(new HttpProvider(new RequestManager($rpcUrl, 30)));
// Валидация и приведение адресов к Checksum
if (!AddressValidator::validate($lotteryAddress)) {
throw new \InvalidArgumentException('Lottery contract address is invalid.');
}
$this->lotteryAddress = Utils::toChecksumAddress($lotteryAddress);
$this->privateKey = $privateKey;
$this->fromAddress = Utils::toChecksumAddress($fromAddress);
$this->gas = $gas;
$this->gasPriceWei = Utils::toWei("{$gasPriceGwei}", 'gwei');
// Чтение ABI
if (!file_exists($lotteryAbiJsonPath)) {
throw new \RuntimeException("ABI file not found: {$lotteryAbiJsonPath}");
}
$abi = file_get_contents($lotteryAbiJsonPath);
// Создаём экземпляр контракта
$this->contract = new Contract($this->web3->provider, $abi);
$this->contract->at($this->lotteryAddress);
}
/**
* -----------------------------
* Методы чтения (без газа)
* -----------------------------
*/
/**
* Получить текущий ID лотереи (currentLotteryId).
*/
public function getCurrentLotteryId()
{
return $this->callContractFunction('currentLotteryId');
}
/**
* Получить данные о лотерее (viewLottery).
*
* @param int $lotteryId
* @return array|mixed
*/
public function getLottery(int $lotteryId)
{
// В Python: get_lottery
return $this->callContractFunction('viewLottery', $lotteryId);
}
/**
* Посмотреть номера и статус (viewNumbersAndStatusesForTicketIds).
*
* @param array|int $ticketIds Можно один ID или несколько
* @return mixed
*/
public function viewNumbersAndStatusesForTicketIds($ticketIds)
{
// Если пользователь передал single ID, заворачиваем в массив
if (!is_array($ticketIds)) {
$ticketIds = [$ticketIds];
}
return $this->callContractFunction('viewNumbersAndStatusesForTicketIds', $ticketIds);
}
/**
* Посмотреть предполагаемые/рассчитанные награды (если есть метод в контракте, например viewRewardsForTicketIds).
* В некоторых версиях лотереи этот метод может отсутствовать.
*
* @param array|int $ticketIds
* @return mixed
*/
public function viewRewardsForTicketIds($ticketIds)
{
// Метод встречается в некоторых форках или в старых версиях. Убедитесь, что он есть в ABI.
if (!is_array($ticketIds)) {
$ticketIds = [$ticketIds];
}
return $this->callContractFunction('viewRewardsForTicketIds', $ticketIds);
}
/**
* Посмотреть, сколько будет стоить покупка N билетов (calculateTotalPriceForBulkTickets).
*
* @param int $lotteryId
* @param int $ticketQuantity
* @return mixed
*/
public function calculateTotalPriceForBulkTickets(int $lotteryId, int $ticketQuantity)
{
// Некоторые версии контракта используют другое название метода.
// В Python: calculate_total_price_for_bulk_tickets
return $this->callContractFunction(
'calculateTotalPriceForBulkTickets',
$lotteryId,
$ticketQuantity
);
}
/**
* Аналог: batchCalculateTotalPriceForBulkTickets или calculateTotalPriceForMultipleBulkTickets.
* Может отсутствовать в реальном контракте.
*
* @param int $lotteryId
* @param array $ticketQuantities
* @return mixed
*/
public function batchCalculateTotalPriceForBulkTickets(int $lotteryId, array $ticketQuantities)
{
// В Python: batch_calculate_total_price_for_bulk_tickets / calculate_total_price_for_multiple_bulk_tickets
// Имейте в виду: реальный контракт может по-другому называть этот метод.
return $this->callContractFunction(
'batchCalculateTotalPriceForBulkTickets',
$lotteryId,
$ticketQuantities
);
}
/**
* Пример просмотра пользовательской информации (viewUserInfoForLotteryId),
* если такой метод существует в контракте. Некоторые версии лотереи его не имеют.
*
* @param string $userAddress
* @param int $lotteryId
* @param int $cursor
* @param int $size
* @return mixed
*/
public function viewUserInfoForLotteryId(string $userAddress, int $lotteryId, int $cursor, int $size)
{
return $this->callContractFunction(
'viewUserInfoForLotteryId',
$userAddress,
$lotteryId,
$cursor,
$size
);
}
/**
* -----------------------------
* Методы записи (требуют газа)
* -----------------------------
*/
/**
* Покупка билетов (buyTickets).
*
* @param int $lotteryId
* @param array $ticketNumbers Пример: [111111, 222222, ...] или правильный формат для контракта
* @return string Хэш транзакции
*/
public function buyTickets(int $lotteryId, array $ticketNumbers)
{
// В Python: buy_tickets
$fnData = [
'name' => 'buyTickets',
'params' => [$lotteryId, $ticketNumbers]
];
return $this->sendTransaction($fnData);
}
/**
* Клейм наград (claimTickets/claimRewards).
*
* @param int $lotteryId
* @param array $ticketIds
* @return string Хэш транзакции
*/
public function claimTickets(int $lotteryId, array $ticketIds)
{
// В Python: claim_tickets
$fnData = [
'name' => 'claimTickets',
'params' => [$lotteryId, $ticketIds]
];
return $this->sendTransaction($fnData);
}
/**
* ----------------------------------------------------------------
* Обёртки для вызовов контракта (call / sendRawTransaction / подпись)
* ----------------------------------------------------------------
*/
/**
* Универсальный метод для чтения (call).
*
* @param string $functionName
* @param mixed ...$params
* @return mixed
*/
protected function callContractFunction(string $functionName, ...$params)
{
$resultData = null;
$errorData = null;
$this->contract->call($functionName, $params, function ($err, $result) use (&$resultData, &$errorData) {
if ($err !== null) {
$errorData = $err->getMessage();
} else {
$resultData = $result;
}
});
if ($errorData !== null) {
throw new \RuntimeException("Call error [{$functionName}]: " . $errorData);
}
// Если метод возвращает единственное значение, зачастую оно лежит в [0].
if (is_array($resultData) && count($resultData) === 1) {
return $resultData[0];
}
return $resultData;
}
/**
* Универсальный метод для отправки транзакций (изменяющих состояние).
*
* @param array $functionData ['name' => ..., 'params' => [...]]
* @return string Хэш транзакции (tx hash)
*/
protected function sendTransaction(array $functionData)
{
$functionName = $functionData['name'];
$params = $functionData['params'] ?? [];
// Получаем nonce для fromAddress
$nonce = $this->getNonce($this->fromAddress);
// Собираем data для контракта (байткод вызова)
$data = $this->getFunctionData($functionName, $params);
// Формируем структуру транзакции
$txData = [
'nonce' => Utils::toHex($nonce, true),
'gasPrice' => Utils::toHex($this->gasPriceWei, true),
'gasLimit' => Utils::toHex($this->gas, true),
'to' => $this->lotteryAddress,
'value' => '0x0', // обычно 0, если не шлём BNB
'data' => $data,
'chainId' => $this->chainId,
];
// Подписываем
$signedTx = $this->signTransaction($txData, $this->privateKey);
// Отправляем raw-транзакцию
return $this->sendRawTransaction($signedTx);
}
/**
* Получить nonce (число транзакций) для указанного адреса.
*
* @param string $address
* @return int
*/
protected function getNonce(string $address)
{
$nonce = 0;
$errorData = null;
$this->web3->eth->getTransactionCount($address, 'pending', function ($err, $res) use (&$nonce, &$errorData) {
if ($err !== null) {
$errorData = $err->getMessage();
} else {
// $res — это BigNumber
$nonce = Utils::toString($res);
}
});
if ($errorData !== null) {
throw new \RuntimeException("Error getTransactionCount: " . $errorData);
}
return (int) $nonce;
}
/**
* Собрать data для вызова функции контракта.
*
* @param string $functionName
* @param array $params
* @return string
*/
protected function getFunctionData(string $functionName, array $params)
{
$data = null;
$this->contract->at($this->lotteryAddress)->getData($functionName, $params, function ($err, $res) use (&$data) {
if ($err !== null) {
throw new \RuntimeException("getData error: " . $err->getMessage());
}
$data = $res;
});
if (!$data) {
throw new \RuntimeException("Unable to get data for function {$functionName}");
}
return $data;
}
/**
* Подписывает транзакцию (EC-Schnorr / ECDSA) с помощью kornrunner/ethereum-tx.
*
* @param array $txData
* @param string $privateKey
* @return string Signed tx (hex)
*/
protected function signTransaction(array $txData, string $privateKey)
{
// Преобразуем поля к формату, который понимает EthTx
// (nonce, gasPrice, gasLimit, to, value, data, chainId)
$ethTx = new EthTx([
'nonce' => Utils::stripZero($txData['nonce']),
'gasPrice' => Utils::stripZero($txData['gasPrice']),
'gasLimit' => Utils::stripZero($txData['gasLimit']),
'to' => $txData['to'],
'value' => Utils::stripZero($txData['value']),
'data' => Utils::stripZero($txData['data']),
'chainId' => $txData['chainId'],
]);
// Подпись
$signed = $ethTx->sign($privateKey);
// Возвращаем в виде 0x...hex
return '0x' . bin2hex($signed);
}
/**
* Отправить сырую (подписанную) транзакцию в сеть.
*
* @param string $signedTx
* @return string txHash
*/
protected function sendRawTransaction(string $signedTx)
{
$txHash = null;
$errorData = null;
$this->web3->eth->sendRawTransaction($signedTx, function ($err, $hash) use (&$txHash, &$errorData) {
if ($err !== null) {
$errorData = $err->getMessage();
} else {
$txHash = $hash;
}
});
if ($errorData !== null) {
throw new \RuntimeException("sendRawTransaction error: " . $errorData);
}
return $txHash;
}
}
// Пример использования
<?php
require 'vendor/autoload.php';
use App\LotteryV2;
// RPC нода BSC Mainnet
$rpcUrl = 'https://bsc-dataseed.binance.org/';
// Адрес контракта лотереи (пример; возьмите реальный из PancakeSwap Docs / BscScan)
$lotteryAddress = '0x1234567890ABCDEF1234567890ABCDEF12345678';
// Ваш приватный ключ (никому не показывайте!)
$privateKey = '0x...';
// Ваш адрес (кошелёк), которому соответствует $privateKey
$fromAddress = '0x...';
// Путь к JSON-файлу с ABI (скопированным с BscScan)
$lotteryAbiPath = __DIR__ . '/lottery_abi_v2.json';
// Инициализируем
$lottery = new LotteryV2(
$lotteryAddress,
$rpcUrl,
$privateKey,
$fromAddress,
$lotteryAbiPath,
3000000, // gas limit
5 // gas price в Gwei
);
// 1. Получаем текущий ID лотереи
$currentId = $lottery->getCurrentLotteryId();
echo "Current lottery ID: {$currentId}\n";
// 2. Данные о лотерее
$lotteryInfo = $lottery->getLottery($currentId);
print_r($lotteryInfo);
// 3. Посмотреть номера и статусы билетов
$ticketData = $lottery->viewNumbersAndStatusesForTicketIds([100001, 100002]);
print_r($ticketData);
// 4. (Опционально) посмотреть предполагаемые награды (если метод есть в контракте)
/// $rewards = $lottery->viewRewardsForTicketIds([100001, 100002]);
// print_r($rewards);
// 5. Купить билеты (если лотерея активна и у вас есть CAKE для покупки — нужно ещё разрешение "approve" на Spender!)
/*
$txHashBuy = $lottery->buyTickets((int)$currentId, [111111, 222222]);
echo "buyTickets txHash: {$txHashBuy}\n";
*/
// 6. Клейм билеты
/*
$txHashClaim = $lottery->claimTickets((int)$currentId, [100001, 100002]);
echo "claimTickets txHash: {$txHashClaim}\n";
*/
// И так далее по аналогии.
Важные моменты
approve() на токен CAKE
Перед покупкой билетов в реальном контракте PancakeSwap Lottery обычно нужно «разрешить» (approve) смарт-контракту лотереи списывать определённое количество CAKE. Это делается в токен-контракте CAKE вызовом approve(lotteryAddress, amount). Если у вас нет такого шага, buyTickets может завершиться ошибкой «ERC20: transfer amount exceeds allowance».
Дополнительные методы
В Python-версиях (lotteryv2.py, lottery.py, utils.py) есть вспомогательные функции, например «подсчитать потенциальные матчи билетов», «просмотр информации юзера» и т.п. В реальном контракте этих методов может не быть — иногда они просто вызывают multicall (агрегатор нескольких view-функций). Добавляйте только те, что реально объявлены в ABI.
Формат билетов
В PancakeSwap Lottery (v2) билеты представляют собой 6-значные номера (каждая цифра в диапазоне 0–9) или иной формат. Убедитесь, что передаёте корректный массив (тип uint256[]).
Gas Limit и Gas Price
Подбираются опытным путём или с помощью Gas Tracker. Для BNB Smart Chain обычно 5–10 Gwei достаточно, но при повышенной нагрузке может быть больше.
Проверка баланса BNB
Если на кошельке недостаточно BNB для газа, транзакция не пройдёт.
Проверка баланса CAKE
Для покупки билетов (buyTickets) у вас должен быть достаточный баланс CAKE (или другого токена, если это форк).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment