amashukov / toncenter-client-php
Typed toncenter v2 HTTP client for The Open Network — {ok, result} envelope unwrapping, retry-aware via any PSR-18 client, typed Value Objects (masterchain info, account state, run-get-method stack reader, transactions). Pluggable wallet RPC adapter.
Package info
github.com/AndreyMashukov/toncenter-client-php
pkg:composer/amashukov/toncenter-client-php
Requires
- php: >=8.3
- ext-gmp: *
- amashukov/ton-cell-php: ^0.1.0
- amashukov/ton-wallet-php: ^0.1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/log: ^3.0
Requires (Dev)
- amashukov/http-client-php: ^0.1.0
- amashukov/rector-php-rules: ^0.4
- friendsofphp/php-cs-fixer: ^3.50
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- rector/rector: ^2.0
Suggests
- amashukov/http-client-php: PSR-18 cURL client + retry/header-injection middleware ready to wire the X-Api-Key + 5xx/429 retry pipeline this client expects.
Provides
None
Conflicts
None
Replaces
None
README
Typed PHP toncenter v2 client for The Open Network — PSR-18 transport, {ok, result} envelope unwrapping, retry-aware, typed Value Objects.
A typed PHP client for the toncenter v2 HTTP API on The Open Network (TON). It speaks any PSR-18 HTTP client, unwraps the {ok, result} envelope into typed exceptions in one place, and returns Value Objects instead of loose arrays — masterchain info, account state, a run-get-method stack reader, and typed transactions. TON balances exceed PHP_INT_MAX past ~9,223 TON, so every nano-TON value comes back as a decimal string, never a lossy float. Includes ToncenterWalletRpc, a drop-in WalletRpcInterface adapter for ton-wallet-php.
Features
{ok, result}envelope unwrapping in one place — a non-200status,ok: false, or a missingresultbecomes aTonRpcException; business code never touches the envelope. Toncenter sends its errors with a matching HTTP status and a{"ok": false, "error": …, "code": …}body, so a non-200response is read for that body: the exception carries toncenter'scodeanderror(code0when the body is not toncenter JSON, e.g. a proxy error page). That is howtryLocateResultTxturns toncenter's404"transaction was not found" intonull.- Typed Value Objects — masterchain info, account state, run-get-method stack reader, and a five-state transaction status (
Pending,Success,ComputePhaseFailed,ActionPhaseFailed,Aborted) derived from the TVM compute / action phases. - Transaction phases from the transaction BOC — toncenter v2 answers
getTransactionswithout adescription, so a naive reader sees every transaction as successful. When the row has nodescription, the client parses the TL-BTransactionin the row'sdatafield: credit, compute and action phases,aborted, whether a bounce message was sent, anddestroyedland inTonTransaction::$description(TonTransactionDescription) and drive the status.getTypedTransactions,getTypedTransactionandtryLocateResultTxrefuse a row withoutdatawith aTonRpcException, so a transaction read through the client never comes back without its phases: a caller that sees$description === nullon such a transaction can treat it as a bug, not as "no bounce". - Decimal-string big numbers — balances and gas values are returned as decimal strings, safe past
PHP_INT_MAX. - Transport-agnostic — bring your own PSR-18 client and PSR-17 factories. The retry policy and the
X-Api-Keyrate-limit header are middleware concerns, not baked into the client. - 542 / 429 retry decider — toncenter emits transient
542("no workers available") and429; the recommended retry status set is documented so a retry middleware can target exactly those. - Wallet RPC adapter —
ToncenterWalletRpcimplementsAmashukov\TonWallet\WalletRpcInterface(getSeqno+sendBoc) so aton-wallet-phpwallet broadcasts through toncenter with no glue code.
Why amashukov/toncenter-client-php
The common alternative is hand-rolled Guzzle calls scattered across a service layer, each re-implementing envelope checks, bigint parsing, and 542/429 retry logic. This package centralizes all of that behind a typed surface with PHPStan level 9, returns Value Objects instead of arrays, and stays transport-agnostic so retry / API-key concerns live in your PSR-18 pipeline.
Installation
composer require amashukov/toncenter-client-php
Usage
Any PSR-18 client and PSR-17 factory pair works. The example below wires
amashukov/http-client-php
(cURL transport + retry / header-injection middleware) with
nyholm/psr7 for the PSR-7 messages:
use Amashukov\HttpClient\CurlClient; use Amashukov\HttpClient\Middleware\HeaderInjectionMiddleware; use Amashukov\HttpClient\Middleware\RetryMiddleware; use Amashukov\HttpClient\Pipeline; use Amashukov\Toncenter\ToncenterClient; use Nyholm\Psr7\Factory\Psr17Factory; $psr17 = new Psr17Factory(); $http = new Pipeline( new CurlClient($psr17, $psr17, timeoutSeconds: 60), [ // X-Api-Key lifts the rate limit from 1 RPS to 10 RPS (optional). new HeaderInjectionMiddleware(['X-Api-Key' => $apiKey]), // Toncenter emits transient 542 ("no workers available") + 429; retry those. new RetryMiddleware(maxAttempts: 3, retryStatusCodes: [429, 500, 502, 503, 504, 542]), ], ); $client = new ToncenterClient($http, $psr17, $psr17); $info = $client->getMasterchainInfo(); // TonMasterchainInfo $balance = $client->getBalance('EQ...'); // decimal-string nano-TON $account = $client->getAddressInformation('EQ...'); if ($account->isActive()) { // ... }
Logging
Pass any PSR-3 logger (Psr\Log\LoggerInterface, e.g. Monolog or a framework's logger) as the last constructor argument; without one the client logs nothing.
$client = new ToncenterClient($http, $psr17, $psr17, ToncenterClient::DEFAULT_BASE_URL, $logger);
debug— every request (method,path) and every response (status).error— every failure that reaches the caller as aTonRpcException: transport error, non-200status,ok: false, invalid JSON, missingresult. The context carriesexception,method,pathand toncenter'scode.- An answer the client handles itself is not an error: toncenter's
404"transaction was not found" fromtryLocateResultTxis logged atdebug, and the method returnsnull.
Reading get-method results
$result = $client->runMethod('EQ...', 'get_wallet_data'); if ($result->isOk()) { $balance = $result->stack->readBigInt(); // numeric-string $ownerSlice = $result->stack->readSliceBoc(); // base64 BOC }
The stack reader is a cursor over a TON TupleItem sum-type
(TonTupleItemInt / Cell / Slice / Builder / Null / Tuple) and
decodes both decimal and 0x-prefixed bigints via GMP.
Following a message to its result
$result = $client->tryLocateResultTx($sourceAddress, $destinationAddress, $createdLt); if (null !== $result && null !== $result->description) { $credited = $result->description->credited; // nano-TON credited, null without a credit phase $bounced = $result->description->bounced; // a bounce message was sent back $executed = $result->isStatusSuccess(); // compute and action phases succeeded }
tryLocateResultTx asks toncenter for the transaction in which the destination
processed one internal message, so it does not page through the destination's
history and answers the same for a quiet wallet and for an exchange address with
thousands of transactions a minute. It returns null while the transaction is not
known yet.
Broadcasting a wallet transfer
use Amashukov\Toncenter\ToncenterWalletRpc; $rpc = new ToncenterWalletRpc($client); $seqno = $rpc->getSeqno($walletRawAddress); // 0 if the wallet is not yet deployed $rpc->sendBoc($signedTransferBocBase64);
API
| Method | Returns |
|---|---|
getMasterchainInfo() |
TonMasterchainInfo |
getBalance(string $address) |
string (decimal nano-TON) |
getAddressInformation(string $address) |
TonAccountInfo |
isContractDeployed(string $address) |
bool |
getTypedTransactions(string $address, array $opts = []) |
list<TonTransaction> |
getTypedTransaction(string $address, string $lt, string $hash) |
TonTransaction |
tryLocateResultTx(string $source, string $destination, string $createdLt) |
?TonTransaction |
runMethod(string $address, string $method, array $stack = []) |
TonRunMethodResult |
sendBoc(string $bocBase64) |
TonSendBocResult |
Requirements
- PHP 8.3+
ext-gmp(bigint decoding of get-method stack values)- A PSR-18 client + PSR-17 request / stream factories
- Optional: a PSR-3 logger
Related packages
| Package | Role |
|---|---|
| amashukov/ton-wallet-php | Wallet v4r2 + WalletRpcInterface this client adapts (dependency) |
| amashukov/http-client-php | PSR-18 cURL client + retry / header-injection middleware |
| amashukov/ton-cell-php | TLB Cell / Builder / BOC layer |
| amashukov/ton-php | TON meta-package |
| amashukov/blockchain-context-bundle | Symfony bundle wiring the whole family |
Quality
- PHPStan level 9, clean.
- php-cs-fixer
@PER-CSruleset. - GitHub Actions CI on every push.
License
MIT — see LICENSE.
Strict evidence for undeployed accounts
TonAccountInfo::isProvenUninitialized() is suitable for deciding that a contract has no deployed token state. It requires an explicit uninitialized, uninit or nonexist wire state, an unsigned decimal-string native balance, and absent/empty code and data. Missing, unknown or malformed fields never qualify. Native TON may still be present at an undeployed address; this predicate does not mean its native balance is zero. The legacy state normalization and isUninitialized() remain compatible, including their defaults; use the strict predicate for absence decisions. Direct constructor calls have no strict wire evidence by default.
Estimate message fees
$estimate = $client->estimateFee($walletAddress, $unsignedBodyBoc); $sourceGasNanotons = $estimate->sourceFees->gasFee;
estimateFee calls the v2 /estimateFee endpoint without sending a transaction.
Pass the base64 message body, not the complete external-message BOC. Optional
initCodeBoc and initDataBoc support undeployed accounts. ignoreSignature
defaults to true; pass false to require a valid signature during estimation.
TonFeeEstimate preserves sourceFees and the ordered destinationFees list.
Each TonFeeBreakdown exposes inForwardFee, storageFee, gasFee and
forwardFee as nonnegative decimal strings in nanotons, including explicit zero.
Missing, negative, fractional or malformed components raise TonRpcException;
large JSON integers are preserved without floating-point conversion. Existing
transport/envelope errors and logging apply to estimation as to other methods.
This is an estimate, not a receipt or a guaranteed final charge. The provider may return no destination fees; that does not establish zero downstream cost or full trace coverage. Applications must establish which message executions are covered before using the estimate to price a multi-contract operation.