Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
02536c0
feat(appkit): add sign message
heyllog Apr 20, 2026
fd0e740
feat(appkit): draft gasless
heyllog Apr 20, 2026
7df94cd
feat(appkit): use loadStateInit
heyllog Apr 20, 2026
960555a
feat(gasless): move types to models
heyllog Apr 20, 2026
6f87338
feat(gasless): fixes after merge
heyllog May 24, 2026
a8edbf8
feat(gasless): polishing
heyllog May 25, 2026
72ecb8c
feat(gasless): delete ton-api dependency
heyllog May 25, 2026
e4498f4
feat(gasless): rename provider methods
heyllog May 25, 2026
bfd48c7
feat(gasless): add error mapping
heyllog May 25, 2026
4c56c13
feat(gasless): rework network management in tonapi provider
heyllog May 25, 2026
0fa3908
feat(gasless): return external boc + normalized hash
heyllog May 25, 2026
641a547
feat(gasless): rename jetton to asset
heyllog May 25, 2026
1d72552
feat(gasless): add metadata
heyllog May 25, 2026
b51fa81
feat(gasless): replace gasless config with suppported assets
heyllog May 25, 2026
f0409b1
feat(gasless): polishing
heyllog May 25, 2026
912e0a2
feat(gasless): add docs
heyllog May 25, 2026
0a971c1
feat(gasless): delete links from doc
heyllog May 25, 2026
75d8b64
feat(gasless): reinstall deps
heyllog May 25, 2026
4b5b940
feat(gasless): get radix back
heyllog May 25, 2026
a57bd1a
Merge branch 'main' into feat/TON-803-gasless-on-main
heyllog May 27, 2026
935144c
feat(gasless): polishing
heyllog May 27, 2026
b677a58
feat(gasless): add error for missing jetton wallet
heyllog May 27, 2026
16daa49
feat(gasless): polishing
heyllog May 28, 2026
1f5dbdc
feat(gasless): rework minter ui
heyllog May 28, 2026
c1b87ac
feat(gasless): update UI
heyllog May 28, 2026
7358cb1
feat(gasless): hide gasless option for TON
heyllog May 28, 2026
0de3202
feat(gasless): excess to relayer + delete gasless ton
heyllog May 28, 2026
74c4587
feat(appkit): small ui improvements
heyllog May 28, 2026
ae0d89e
feat(appkit): add checkSignMessageSupport
heyllog May 28, 2026
321ab58
feat(appkit): pass networks from hooks
heyllog May 28, 2026
465ca9b
feat(appkit): save token icons locally
heyllog May 28, 2026
5d5bc65
Merge branch main into feat/TON-803-gasless-on-main
heyllog May 29, 2026
3e112ae
feat(gasless): add switch
heyllog Jun 1, 2026
05048b3
feat(gasless): extend mint store
heyllog Jun 1, 2026
bacddd9
feat(gasless): add confirm and settings modals for mint
heyllog Jun 1, 2026
025328e
feat(gasless): rework mint hooks
heyllog Jun 1, 2026
76cc340
feat(gasless): rework mint hooks
heyllog Jun 3, 2026
34057be
feat(gasless): use LowBalanceModal in mint
heyllog Jun 3, 2026
4b37eec
feat(gasless): turn off gasless if not available
heyllog Jun 4, 2026
5b327bc
feat(gasless): rework code
heyllog Jun 5, 2026
e548044
feat(gasless): add invalide after error and add readable error for in…
heyllog Jun 5, 2026
11927b4
feat(gasless): add gasless send in demo-wallet
heyllog Jun 8, 2026
a6bbb05
feat(gasless): add toast
heyllog Jun 8, 2026
4d30b3a
Merge branch 'main' into feat/TON-803-gasless-on-main
TrueCarry Jun 8, 2026
c5b8e6f
Merge branch 'main' into feat/TON-803-gasless-on-main
TrueCarry Jun 8, 2026
bf64fc9
feat(gasless): add registerProvider to TonWalletKit class
TrueCarry Jun 8, 2026
fe65de7
feat(gasless): add util to check if wallet supports sign message
heyllog Jun 8, 2026
4ffa168
feat(new-ui): add quote retries
heyllog Jun 8, 2026
368150f
feat(gasless): toast walletkit missing error
heyllog Jun 8, 2026
93bdbf0
Merge branch 'feat/TON-803-gasless-on-main' of github.com:ton-connect…
heyllog Jun 8, 2026
ea41429
feat(gasless): polishing
heyllog Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/gasless-transactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@ton/walletkit': patch
'@ton/appkit': patch
'@ton/appkit-react': patch
---

Added gasless transactions support. A relayer pays the TON gas; the user pays a fee in a relayer-accepted jetton (e.g. USDT). See [`@ton/appkit/docs/gasless.md`](https://github.com/ton-connect/kit/blob/main/packages/appkit/docs/gasless.md) for the regular-send → gasless-send migration guide.

- `@ton/walletkit`:
- `GaslessManager` and the `GaslessProvider` abstract base — parallel to `StakingManager` / `SwapManager`. Extend `GaslessProvider` to plug in your own relayer.
- `TonApiGaslessProvider` / `createTonApiGaslessProvider()` — gasless via the TonAPI relayer. Auto-discovers networks from the kit; per-chain `apiKey` / `endpoint` overrides supported.
- `GaslessError` with `GaslessErrorCode`: `UnsupportedFeeAsset`, `FeeAssetNotOwned`, `UnsupportedOperation`, `QuoteFailed`, `SendFailed`, `ConfigFailed`, `SignMessageNotSupported`, `TooManyMessages`, `QuoteExpired`, `WalletMismatch`.

- `@ton/appkit`:
- Actions: `getGaslessConfig`, `getGaslessQuote`, `getGaslessJettonTransferQuote`, `sendGaslessTransaction`, `getGaslessProviderMetadata`, plus provider management (`getGaslessManager`, `getGaslessProvider(s)`, `setDefaultGaslessProvider`, `watchGaslessProviders`).
- `getGaslessJettonTransferQuote` is the recommended entry point: takes `jettonAddress`/`recipientAddress`/`amount`/`feeAsset` and builds the transfer messages for you, routing the jetton `excess` back to the relayer. The two-step quote → `sendGaslessTransaction` flow is preserved.
- `sendGaslessTransaction` runs fail-fast guards before prompting the wallet — throws `GaslessError(QUOTE_EXPIRED)`, `WALLET_MISMATCH`, `SIGN_MESSAGE_NOT_SUPPORTED`, or `TOO_MANY_MESSAGES` so the user is not asked to sign a quote the relayer would reject.
- Quote queries are wallet- and network-bound: switching wallet or network refetches a fresh quote instead of serving one issued for the previous wallet.
- New `signMessage` action (the primitive gasless uses to sign the relayer-wrapped BoC). `TonConnectWalletAdapter` now implements `signMessage` and `getSupportedFeatures`.

- `@ton/appkit-react`:
- Hooks: `useGaslessConfig`, `useGaslessQuote`, `useGaslessJettonTransferQuote`, `useSendGaslessTransaction`, `useGaslessProviderMetadata`, `useGaslessProvider(s)`, `useSignMessage`. Quote hooks auto-refetch on wallet/network switch.
Binary file added apps/appkit-minter/public/tokens/gemston.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/ston.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/ton.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/appkit-minter/public/tokens/tston.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/usde.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/usdt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/utya.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/weth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/appkit-minter/public/tokens/xaut0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useWatchBalance, useWatchTransactions, useWatchJettons, useBalance } from '@ton/appkit-react';
import { middleEllipsis } from '@ton/appkit';
import { toast } from 'sonner';

import { JettonsPage, MinterPage, NftsPage, StakingPage, SwapPage } from '@/pages';
Expand All @@ -26,7 +27,7 @@ export const AppRouter: React.FC = () => {
onChange: (update) => {
if (update.traceHash) {
const hash = update.traceHash;
const shortHash = `${hash.slice(0, 6)}...${hash.slice(-4)}`;
const shortHash = middleEllipsis(hash);

if (update.status === 'invalidated') {
toast.error(`Transaction invalidated`, { id: hash, description: shortHash });
Expand Down
2 changes: 2 additions & 0 deletions apps/appkit-minter/src/core/configs/app-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { createDeDustProvider } from '@ton/appkit/swap/dedust';
import { createOmnistonProvider } from '@ton/appkit/swap/omniston';
import { createTonstakersProvider } from '@ton/appkit/staking/tonstakers';
import { createTonApiGaslessProvider } from '@ton/appkit/gasless/tonapi';

import { ENV_TON_API_KEY_TESTNET, ENV_TON_API_KEY_MAINNET, ENV_TONCONNECT_MANIFEST_URL } from '@/core/configs/env';

Expand Down Expand Up @@ -54,5 +55,6 @@ export const appKit = new AppKit({
createTonstakersProvider(),
createTonCenterStreamingProvider({ network: Network.mainnet(), apiKey: ENV_TON_API_KEY_MAINNET }),
createTonCenterStreamingProvider({ network: Network.testnet(), apiKey: ENV_TON_API_KEY_TESTNET }),
createTonApiGaslessProvider(),
],
});
10 changes: 10 additions & 0 deletions apps/appkit-minter/src/core/constants/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

/** USDT (Tether USD) jetton master on TON mainnet — bounceable form. */
export const USDT_MASTER_MAINNET = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs';
5 changes: 0 additions & 5 deletions apps/appkit-minter/src/core/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,4 @@ import { twMerge } from 'tailwind-merge';

export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

export const formatAddress = (address: string): string => {
if (address.length <= 10) return address;
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};

export const generateId = (): string => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { useEffect, useMemo } from 'react';
import type { FC } from 'react';
import { Input, useGaslessConfig, useJettonInfo } from '@ton/appkit-react';
import { asAddressFriendly, compareAddress, middleEllipsis } from '@ton/appkit';
import type { UserFriendlyAddress } from '@ton/appkit';

import { USDT_MASTER_MAINNET } from '../../../core/constants/tokens';

/**
* Renders one fee-asset `<option>`; shows the token's address until its jetton
* info loads, then the ticker. Each instance owns its own `useJettonInfo` query
* so labels resolve independently — React Query dedupes identical addresses.
*/
const FeeAssetOption: FC<{ address: UserFriendlyAddress }> = ({ address }) => {
const { data } = useJettonInfo({ address });
return <option value={address}>{data?.symbol || middleEllipsis(address)}</option>;
};

interface FeeAssetSelectProps {
value: UserFriendlyAddress | null;
onChange: (address: UserFriendlyAddress) => void;
disabled?: boolean;
}

/**
* Fee-asset picker for gasless transfers. Lists the relayer-accepted assets and
* preselects USDT (or the first asset) once they load. The native `<select>` is
* wrapped in `Input.Container size="s"` so the field background/border/focus
* state match the modal's other inputs.
*/
export const FeeAssetSelect: FC<FeeAssetSelectProps> = ({ value, onChange, disabled }) => {
const { data: config, isLoading } = useGaslessConfig();
const supportedAssets = useMemo(
() => config?.supportedAssets && [...config.supportedAssets].sort((a, b) => a.address.localeCompare(b.address)),
[config?.supportedAssets],
);

useEffect(() => {
if (!value && supportedAssets?.length) {
const preferred = supportedAssets.find((asset) => compareAddress(asset.address, USDT_MASTER_MAINNET));
onChange(preferred?.address ?? supportedAssets[0].address);
}
}, [value, supportedAssets, onChange]);

const isDisabled = disabled || isLoading || !supportedAssets?.length;

return (
<Input size="s" disabled={isDisabled}>
<Input.Header>
<Input.Title>Fee asset</Input.Title>
</Input.Header>
<Input.Field>
<select
className="flex-1 min-w-0 w-full bg-transparent border-none outline-none text-foreground p-0 cursor-pointer"
style={{
// Match `Input size="s"` typography exactly — Tailwind's
// text-sm (14px) differs from the design token (16px).
fontFamily: 'var(--ta-font-family)',
fontSize: 'var(--ta-input-s-size)',
fontWeight: 'var(--ta-input-s-weight)',
lineHeight: 'var(--ta-input-s-line-height)',
}}
value={value ?? ''}
onChange={(event) => onChange(asAddressFriendly(event.target.value))}
disabled={isDisabled}
>
{!value && (
<option value="" disabled>
Select fee asset
</option>
)}
{supportedAssets?.map((asset) => (
<FeeAssetOption key={asset.address} address={asset.address} />
))}
</select>
</Input.Field>
</Input>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { useMemo } from 'react';
import type { FC } from 'react';
import { useSelectedWallet } from '@ton/appkit-react';
import { getErrorMessage, supportsSignMessage } from '@ton/appkit';
import type { UserFriendlyAddress } from '@ton/appkit';

import { FeeAssetSelect } from './fee-asset-select';

interface GaslessControlsProps {
enabled: boolean;
onEnabledChange: (next: boolean) => void;
feeAsset: UserFriendlyAddress | null;
onFeeAssetChange: (next: UserFriendlyAddress) => void;
fee: string | null;
quoteError: unknown;
}

/**
* Self-contained gasless UI block for the transfer modal: the enable toggle
* (auto-disabled when the wallet lacks `SignMessage`), the fee-asset select,
* the formatted fee preview and the quote error. Controlled by the caller —
* gasless state lives in the modal so it can also gate the send button.
*/
export const GaslessControls: FC<GaslessControlsProps> = ({
enabled,
onEnabledChange,
feeAsset,
onFeeAssetChange,
fee,
quoteError,
}) => {
const [selectedWallet] = useSelectedWallet();
const hasSignMessage = useMemo(() => {
const features = selectedWallet?.getSupportedFeatures();
return features === undefined ? true : supportsSignMessage(features);
}, [selectedWallet]);

return (
<div className="space-y-2">
<label className="inline-flex items-center gap-2 text-sm text-foreground">
<input
type="checkbox"
checked={enabled}
disabled={!hasSignMessage}
onChange={(event) => onEnabledChange(event.target.checked)}
/>
<span>Gasless — pay the gas fee in another token</span>
</label>

{!hasSignMessage && (
<p className="text-xs text-tertiary-foreground">
Connected wallet does not support gasless (no SignMessage feature).
</p>
)}

{enabled && (
<>
<FeeAssetSelect value={feeAsset} onChange={onFeeAssetChange} />
{!quoteError && (
<p className="px-1 text-xs text-tertiary-foreground">Gas fee: {fee || 'Loading...'}</p>
)}
{quoteError && <p className="text-xs text-error">{getErrorMessage(quoteError)}</p>}
</>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) TonTech.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { FC } from 'react';
import { TonIconCircle } from '@ton/appkit-react';

import type { TokenInfo } from '../utils/get-token-summary';

interface TokenSummaryProps {
tokenType: 'TON' | 'JETTON';
info: TokenInfo;
}

/**
* Token avatar + available balance — the static header shown above the form in
* the transfer modal.
*/
export const TokenSummary: FC<TokenSummaryProps> = ({ tokenType, info }) => (
<div className="flex items-center space-x-3 mb-6">
<div className="w-10 h-10 bg-tertiary rounded-full flex items-center justify-center overflow-hidden">
{info.image ? (
<img src={info.image} alt={info.name} className="w-full h-full object-cover" />
) : tokenType === 'TON' ? (
<TonIconCircle size={40} />
) : (
<span className="text-sm font-bold text-tertiary-foreground">{info.symbol?.slice(0, 2)}</span>
)}
</div>
<div>
<p className="text-sm font-medium text-foreground">Available Balance</p>
<p className="text-xs text-tertiary-foreground">
{info.balance} {info.symbol}
</p>
</div>
</div>
);
Loading
Loading