What happens
When <TonPayButton> is configured for a non-TON asset (e.g. asset="EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", USDT mainnet), the "Insufficient Funds" modal shows a balance 1000× smaller than reality — a wallet with 20 USDT displays as 0.02 USDT.
Root cause
fetchJettonBalance in packages/ui-react/src/hooks/useWalletBalance.ts reads:
const decimals = wallet.jetton?.decimals ?? 9;
But the toncenter v3 /api/v3/jetton/wallets response returns wallet.jetton as the master address string, not an object:
{
"jetton_wallets": [{
"balance": "20000000",
"jetton": "0:B113A994B5024A16719F69139328EB759596C38A25F59028B146FECDC3621DFE"
}],
"metadata": {
"0:B113...": {
"token_info": [{
"extra": { "decimals": "6" }
}]
}
}
}
So .decimals on a string is undefined → falls back to 9. USDT has 6 decimals → balance shown as raw / 10^9 instead of raw / 10^6.
Downstream consequence
Any merchant integrating USDT sees users wrongly routed to the "Top up with crypto" view. Users who pay via that QR flow bypass the handlePay callback — so the merchant's reference never gets attached, the transfer completes on-chain, but the order stays pending forever (pay.ton.org only indexes transfers that were built through the SDK handshake with a reference).
Suggested fix
Resolve decimals from metadata[masterAddr].token_info[0].extra.decimals (where toncenter v3 actually puts them), with a fallback for the legacy shape in case the response evolves:
let decimals = 9;
const masterAddr = typeof wallet.jetton === "string" ? wallet.jetton : null;
const fromMetadata = masterAddr
? data?.metadata?.[masterAddr]?.token_info?.[0]?.extra?.decimals
: undefined;
if (fromMetadata != null) {
const parsed = Number(fromMetadata);
if (Number.isFinite(parsed)) decimals = parsed;
} else if (typeof wallet.jetton?.decimals === "number") {
decimals = wallet.jetton.decimals;
}
Affects every jetton whose decimals ≠ 9. Version observed: @ton-pay/ui-react@0.3.2.
Happy to open a PR if useful.
What happens
When
<TonPayButton>is configured for a non-TON asset (e.g.asset="EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", USDT mainnet), the "Insufficient Funds" modal shows a balance 1000× smaller than reality — a wallet with 20 USDT displays as 0.02 USDT.Root cause
fetchJettonBalanceinpackages/ui-react/src/hooks/useWalletBalance.tsreads:But the toncenter v3
/api/v3/jetton/walletsresponse returnswallet.jettonas the master address string, not an object:{ "jetton_wallets": [{ "balance": "20000000", "jetton": "0:B113A994B5024A16719F69139328EB759596C38A25F59028B146FECDC3621DFE" }], "metadata": { "0:B113...": { "token_info": [{ "extra": { "decimals": "6" } }] } } }So
.decimalson a string isundefined→ falls back to9. USDT has 6 decimals → balance shown asraw / 10^9instead ofraw / 10^6.Downstream consequence
Any merchant integrating USDT sees users wrongly routed to the "Top up with crypto" view. Users who pay via that QR flow bypass the
handlePaycallback — so the merchant's reference never gets attached, the transfer completes on-chain, but the order stays pending forever (pay.ton.org only indexes transfers that were built through the SDK handshake with a reference).Suggested fix
Resolve decimals from
metadata[masterAddr].token_info[0].extra.decimals(where toncenter v3 actually puts them), with a fallback for the legacy shape in case the response evolves:Affects every jetton whose
decimals ≠ 9. Version observed:@ton-pay/ui-react@0.3.2.Happy to open a PR if useful.