From 02536c0d1dfeb73bf22d73b593cdf3e4360cbd22 Mon Sep 17 00:00:00 2001 From: VK Date: Mon, 20 Apr 2026 16:09:59 +0400 Subject: [PATCH 01/46] feat(appkit): add sign message --- .../actions/transaction/sign-message.ts | 27 ++++++++ .../actions/transaction/transaction.test.ts | 16 +++++ .../hooks/transaction/transaction.test.tsx | 63 ++++++++++++++++++ .../hooks/transaction/use-sign-message.tsx | 42 ++++++++++++ packages/appkit-react/docs/hooks.md | 35 ++++++++++ .../transaction/hooks/use-sign-message.ts | 50 ++++++++++++++ .../src/features/transaction/index.ts | 1 + packages/appkit/docs/actions.md | 19 ++++++ packages/appkit/src/actions/index.ts | 6 ++ .../src/actions/transaction/sign-message.ts | 40 +++++++++++ .../adapters/ton-connect-wallet-adapter.ts | 42 ++++++++---- packages/appkit/src/queries/index.ts | 12 ++++ .../src/queries/transaction/sign-message.ts | 66 +++++++++++++++++++ packages/appkit/src/types/signing.ts | 12 ++++ packages/appkit/src/types/wallet.ts | 20 +++++- template/packages/appkit-react/docs/hooks.md | 6 ++ template/packages/appkit/docs/actions.md | 6 ++ 17 files changed, 449 insertions(+), 14 deletions(-) create mode 100644 demo/examples/src/appkit/actions/transaction/sign-message.ts create mode 100644 demo/examples/src/appkit/hooks/transaction/use-sign-message.tsx create mode 100644 packages/appkit-react/src/features/transaction/hooks/use-sign-message.ts create mode 100644 packages/appkit/src/actions/transaction/sign-message.ts create mode 100644 packages/appkit/src/queries/transaction/sign-message.ts diff --git a/demo/examples/src/appkit/actions/transaction/sign-message.ts b/demo/examples/src/appkit/actions/transaction/sign-message.ts new file mode 100644 index 000000000..cb60b48c0 --- /dev/null +++ b/demo/examples/src/appkit/actions/transaction/sign-message.ts @@ -0,0 +1,27 @@ +/** + * 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 { AppKit } from '@ton/appkit'; +import { signMessage } from '@ton/appkit'; + +export const signMessageExample = async (appKit: AppKit) => { + // SAMPLE_START: SIGN_MESSAGE + const result = await signMessage(appKit, { + messages: [ + { + address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + amount: '100000000', // 0.1 TON in nanotons + }, + ], + }); + + // result.internalBoc is a signed internal message BoC (base64) + // that can be relayed on-chain by a third party (e.g. a gasless relayer). + console.log('Signed Message:', result); + // SAMPLE_END: SIGN_MESSAGE +}; diff --git a/demo/examples/src/appkit/actions/transaction/transaction.test.ts b/demo/examples/src/appkit/actions/transaction/transaction.test.ts index 482dd02a3..d5b24a0f7 100644 --- a/demo/examples/src/appkit/actions/transaction/transaction.test.ts +++ b/demo/examples/src/appkit/actions/transaction/transaction.test.ts @@ -12,6 +12,7 @@ import { Network } from '@ton/walletkit'; import type { WalletInterface } from '@ton/appkit'; import { sendTransactionExample } from './send-transaction'; +import { signMessageExample } from './sign-message'; import { transferTonExample } from './transfer-ton'; import { createTransferTonTransactionExample } from './create-transfer-ton-transaction'; @@ -19,6 +20,7 @@ describe('Transaction Actions Examples', () => { let appKit: AppKit; let consoleSpy: ReturnType; let mockSendTransaction: ReturnType; + let mockSignMessage: ReturnType; beforeEach(() => { vi.clearAllMocks(); @@ -31,6 +33,7 @@ describe('Transaction Actions Examples', () => { }); mockSendTransaction = vi.fn(); + mockSignMessage = vi.fn(); }); afterEach(() => { @@ -43,6 +46,7 @@ describe('Transaction Actions Examples', () => { getWalletId: () => 'mock-wallet-id', getNetwork: () => Network.mainnet(), sendTransaction: mockSendTransaction, + signMessage: mockSignMessage, } as unknown as WalletInterface; appKit.walletsManager.setWallets([mockWallet]); @@ -61,6 +65,18 @@ describe('Transaction Actions Examples', () => { }); }); + describe('signMessageExample', () => { + it('should log signed message result', async () => { + setupMockWallet(); + mockSignMessage.mockResolvedValue({ internalBoc: 'mock-internal-boc' }); + + await signMessageExample(appKit); + + expect(mockSignMessage).toHaveBeenCalled(); + expect(consoleSpy).toHaveBeenCalledWith('Signed Message:', { internalBoc: 'mock-internal-boc' }); + }); + }); + describe('transferTonExample', () => { it('should log transfer result', async () => { setupMockWallet(); diff --git a/demo/examples/src/appkit/hooks/transaction/transaction.test.tsx b/demo/examples/src/appkit/hooks/transaction/transaction.test.tsx index 277ab008e..7d6aee5ef 100644 --- a/demo/examples/src/appkit/hooks/transaction/transaction.test.tsx +++ b/demo/examples/src/appkit/hooks/transaction/transaction.test.tsx @@ -14,6 +14,7 @@ import { Network } from '@ton/walletkit'; import { createWrapper } from '../../../__tests__/test-utils'; import { UseSendTransactionExample } from './use-send-transaction'; +import { UseSignMessageExample } from './use-sign-message'; import { UseTransferTonExample } from './use-transfer-ton'; import { UseWatchTransactionsByAddressExample } from './use-watch-transactions-by-address'; import { UseWatchTransactionsExample } from './use-watch-transactions'; @@ -21,14 +22,17 @@ import { UseWatchTransactionsExample } from './use-watch-transactions'; describe('Transaction Hooks Examples', () => { let mockAppKit: any; let mockSendTransaction: any; + let mockSignMessage: any; const mockBoc = 'te6cckEBAQEAAgAAAEysuc0='; + const mockInternalBoc = 'te6cckEBAQEAAgAAAEysuc1='; const mockNetwork = Network.mainnet(); const mockWallet = { getAddress: () => 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', getNetwork: () => mockNetwork, sendTransaction: vi.fn(), + signMessage: vi.fn(), }; beforeEach(() => { @@ -38,6 +42,9 @@ describe('Transaction Hooks Examples', () => { mockSendTransaction = vi.fn().mockResolvedValue({ boc: mockBoc }); mockWallet.sendTransaction = mockSendTransaction; + mockSignMessage = vi.fn().mockResolvedValue({ internalBoc: mockInternalBoc }); + mockWallet.signMessage = mockSignMessage; + mockAppKit = { getDefaultNetwork: vi.fn(), connectors: [], @@ -116,6 +123,62 @@ describe('Transaction Hooks Examples', () => { }); }); + describe('UseSignMessageExample', () => { + it('should render sign button initially', () => { + render(, { wrapper: createWrapper(mockAppKit) }); + expect(screen.getByText('Sign Message')).toBeDefined(); + }); + + it('should call signMessage on button click', async () => { + render(, { wrapper: createWrapper(mockAppKit) }); + + const button = screen.getByText('Sign Message'); + act(() => { + button.click(); + }); + + await waitFor(() => { + expect(mockSignMessage).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + amount: '100000000', + address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + }), + ]), + }), + ); + }); + }); + + it('should display internal BOC on success', async () => { + render(, { wrapper: createWrapper(mockAppKit) }); + + const button = screen.getByText('Sign Message'); + act(() => { + button.click(); + }); + + await waitFor(() => { + expect(screen.getByText(`Internal BOC: ${mockInternalBoc}`)).toBeDefined(); + }); + }); + + it('should display error on failure', async () => { + mockSignMessage.mockRejectedValue(new Error('User rejected')); + render(, { wrapper: createWrapper(mockAppKit) }); + + const button = screen.getByText('Sign Message'); + act(() => { + button.click(); + }); + + await waitFor(() => { + expect(screen.getByText('Error: User rejected')).toBeDefined(); + }); + }); + }); + describe('UseTransferTonExample', () => { it('should render transfer button initially', () => { render(, { wrapper: createWrapper(mockAppKit) }); diff --git a/demo/examples/src/appkit/hooks/transaction/use-sign-message.tsx b/demo/examples/src/appkit/hooks/transaction/use-sign-message.tsx new file mode 100644 index 000000000..7f1448428 --- /dev/null +++ b/demo/examples/src/appkit/hooks/transaction/use-sign-message.tsx @@ -0,0 +1,42 @@ +/** + * 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 { useSignMessage } from '@ton/appkit-react'; + +export const UseSignMessageExample = () => { + // SAMPLE_START: USE_SIGN_MESSAGE + const { mutate: signMessage, isPending, error, data } = useSignMessage(); + + const handleSign = () => { + signMessage({ + validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes + messages: [ + { + address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + amount: '100000000', // 0.1 TON in nanotons + }, + ], + }); + }; + + return ( +
+ + {error &&
Error: {error.message}
} + {data && ( +
+

Message Signed!

+

Internal BOC: {data.internalBoc}

+
+ )} +
+ ); + // SAMPLE_END: USE_SIGN_MESSAGE +}; diff --git a/packages/appkit-react/docs/hooks.md b/packages/appkit-react/docs/hooks.md index 06fa62048..d7bd4ebcd 100644 --- a/packages/appkit-react/docs/hooks.md +++ b/packages/appkit-react/docs/hooks.md @@ -876,6 +876,41 @@ return ( ); ``` +### `useSignMessage` + +Hook to sign a transaction-shaped request without broadcasting it. Returns a signed internal-message BoC that can be relayed on-chain by a third party (e.g. a gasless relayer). Requires wallet support for the `SignMessage` feature. + +```tsx +const { mutate: signMessage, isPending, error, data } = useSignMessage(); + +const handleSign = () => { + signMessage({ + validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes + messages: [ + { + address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + amount: '100000000', // 0.1 TON in nanotons + }, + ], + }); +}; + +return ( +
+ + {error &&
Error: {error.message}
} + {data && ( +
+

Message Signed!

+

Internal BOC: {data.internalBoc}

+
+ )} +
+); +``` + ### `useTransferTon` Hook to simplify transferring TON to another address. diff --git a/packages/appkit-react/src/features/transaction/hooks/use-sign-message.ts b/packages/appkit-react/src/features/transaction/hooks/use-sign-message.ts new file mode 100644 index 000000000..70d44d72b --- /dev/null +++ b/packages/appkit-react/src/features/transaction/hooks/use-sign-message.ts @@ -0,0 +1,50 @@ +/** + * 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. + * + */ + +'use client'; + +import type { MutateFunction, MutateOptions } from '@tanstack/react-query'; +import type { + SignMessageData, + SignMessageErrorType, + SignMessageOptions, + SignMessageVariables, +} from '@ton/appkit/queries'; +import { signMessageMutationOptions } from '@ton/appkit/queries'; + +import { useMutation } from '../../../libs/query'; +import type { UseMutationReturnType } from '../../../libs/query'; +import { useAppKit } from '../../settings'; + +export type UseSignMessageParameters = SignMessageOptions; + +export type UseSignMessageReturnType = UseMutationReturnType< + SignMessageData, + SignMessageErrorType, + SignMessageVariables, + context, + ( + variables: SignMessageVariables, + options?: MutateOptions, + ) => void, + MutateFunction +>; + +/** + * Hook to sign a transaction-shaped request without broadcasting it. + * + * Returns a signed internal-message BoC that a third party can relay on-chain + * (e.g. a gasless relayer). + */ +export const useSignMessage = ( + parameters: UseSignMessageParameters = {}, +): UseSignMessageReturnType => { + const appKit = useAppKit(); + + return useMutation(signMessageMutationOptions(appKit, parameters)); +}; diff --git a/packages/appkit-react/src/features/transaction/index.ts b/packages/appkit-react/src/features/transaction/index.ts index 5b49ca36b..d5415eca8 100644 --- a/packages/appkit-react/src/features/transaction/index.ts +++ b/packages/appkit-react/src/features/transaction/index.ts @@ -7,6 +7,7 @@ */ export * from './hooks/use-send-transaction'; +export * from './hooks/use-sign-message'; export * from './hooks/use-transfer-ton'; export * from './hooks/use-transaction-status'; export * from './hooks/use-watch-transactions-by-address'; diff --git a/packages/appkit/docs/actions.md b/packages/appkit/docs/actions.md index 949067e83..26aa13cd5 100644 --- a/packages/appkit/docs/actions.md +++ b/packages/appkit/docs/actions.md @@ -664,6 +664,25 @@ const result = await sendTransaction(appKit, { console.log('Transaction Result:', result); ``` + +### `signMessage` + +Ask the connected wallet to sign a transaction-shaped request without broadcasting it. Returns a signed internal-message BoC that can be relayed on-chain by a third party (e.g. a gasless relayer). Requires wallet support for the `SignMessage` feature. + +```ts +const result = await signMessage(appKit, { + messages: [ + { + address: 'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', + amount: '100000000', // 0.1 TON in nanotons + }, + ], +}); + +// result.internalBoc is a signed internal message BoC (base64) +// that can be relayed on-chain by a third party (e.g. a gasless relayer). +console.log('Signed Message:', result); +``` ### `transferTon` diff --git a/packages/appkit/src/actions/index.ts b/packages/appkit/src/actions/index.ts index e9c38eb36..e090f69d8 100644 --- a/packages/appkit/src/actions/index.ts +++ b/packages/appkit/src/actions/index.ts @@ -184,6 +184,12 @@ export { type SendTransactionParameters, type SendTransactionReturnType, } from './transaction/send-transaction'; +export { + signMessage, + type SignMessageParameters, + type SignMessageReturnType, + type SignMessageErrorType, +} from './transaction/sign-message'; export { transferTon, type TransferTonParameters, type TransferTonReturnType } from './transaction/transfer-ton'; export { getTransactionStatus, diff --git a/packages/appkit/src/actions/transaction/sign-message.ts b/packages/appkit/src/actions/transaction/sign-message.ts new file mode 100644 index 000000000..57aac6b7b --- /dev/null +++ b/packages/appkit/src/actions/transaction/sign-message.ts @@ -0,0 +1,40 @@ +/** + * 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 { TransactionRequest } from '../../types/transaction'; +import type { SignMessageResponse } from '../../types/signing'; +import type { AppKit } from '../../core/app-kit'; +import { getSelectedWallet } from '../wallets/get-selected-wallet'; + +export type SignMessageParameters = TransactionRequest; + +export type SignMessageReturnType = SignMessageResponse; + +export type SignMessageErrorType = Error; + +/** + * Ask the connected wallet to sign a transaction-shaped request without broadcasting it. + * + * Returns a signed internal-message BoC that can be relayed on-chain by a third party + * (e.g. a gasless relayer). Unlike sendTransaction, the message is NOT submitted to the + * network by the wallet. + * + * Requires the wallet to support the SignMessage feature. + */ +export const signMessage = async ( + appKit: AppKit, + parameters: SignMessageParameters, +): Promise => { + const wallet = getSelectedWallet(appKit); + + if (!wallet) { + throw new Error('Wallet not connected'); + } + + return wallet.signMessage(parameters); +}; diff --git a/packages/appkit/src/connectors/tonconnect/adapters/ton-connect-wallet-adapter.ts b/packages/appkit/src/connectors/tonconnect/adapters/ton-connect-wallet-adapter.ts index d7db5c564..3f748cdc9 100644 --- a/packages/appkit/src/connectors/tonconnect/adapters/ton-connect-wallet-adapter.ts +++ b/packages/appkit/src/connectors/tonconnect/adapters/ton-connect-wallet-adapter.ts @@ -9,7 +9,7 @@ import { Address } from '@ton/core'; import type { Wallet as TonConnectWallet } from '@tonconnect/sdk'; import type { SignDataPayload as TonConnectSignDataPayload } from '@tonconnect/sdk'; -import type { SendTransactionResponse, UserFriendlyAddress, Hex } from '@ton/walletkit'; +import type { Feature, SendTransactionResponse, UserFriendlyAddress, Hex } from '@ton/walletkit'; import { asHex, createWalletId, getNormalizedExtMessageHash } from '@ton/walletkit'; import type { TonConnectUI } from '@tonconnect/ui'; @@ -17,7 +17,7 @@ import type { TransactionRequest } from '../../../types/transaction'; import type { Base64String } from '../../../types/primitives'; import { getValidUntil } from '../utils/transaction'; import type { WalletInterface } from '../../../types/wallet'; -import type { SignDataRequest, SignDataResponse } from '../../../types/signing'; +import type { SignDataRequest, SignDataResponse, SignMessageResponse } from '../../../types/signing'; import { Network } from '../../../types/network'; /** @@ -73,21 +73,16 @@ export class TonConnectWalletAdapter implements WalletInterface { return createWalletId(this.getNetwork(), this.getAddress()); } + getSupportedFeatures(): Feature[] | undefined { + return this.tonConnectWallet.device?.features; + } + // ========================================== // Signing / Transactions // ========================================== async sendTransaction(request: TransactionRequest): Promise { - const transaction = { - validUntil: request.validUntil || getValidUntil(), - messages: request.messages.map((msg) => ({ - address: msg.address, - amount: String(msg.amount), - payload: msg.payload, - stateInit: msg.stateInit, - })), - network: request.network?.chainId ?? this.tonConnectWallet.account?.chain, - }; + const transaction = this.mapTransactionRequest(request); const result = await this.tonConnectUI.sendTransaction(transaction); const { hash, boc: normalizedBoc } = getNormalizedExtMessageHash(result.boc); @@ -99,6 +94,16 @@ export class TonConnectWalletAdapter implements WalletInterface { }; } + async signMessage(request: TransactionRequest): Promise { + const message = this.mapTransactionRequest(request); + + const result = await this.tonConnectUI.signMessage(message); + + return { + internalBoc: result.internalBoc as Base64String, + }; + } + async signData(payload: SignDataRequest): Promise { const result = await this.tonConnectUI.signData(this.mapSignDataRequest(payload)); @@ -115,6 +120,19 @@ export class TonConnectWalletAdapter implements WalletInterface { // Private helpers // ========================================== + private mapTransactionRequest(request: TransactionRequest) { + return { + validUntil: request.validUntil || getValidUntil(), + messages: request.messages.map((msg) => ({ + address: msg.address, + amount: String(msg.amount), + payload: msg.payload, + stateInit: msg.stateInit, + })), + network: request.network?.chainId ?? this.tonConnectWallet.account?.chain, + }; + } + private mapSignDataRequest(request: SignDataRequest): TonConnectSignDataPayload { const chainId = request.network?.chainId ?? this.getNetwork().chainId; diff --git a/packages/appkit/src/queries/index.ts b/packages/appkit/src/queries/index.ts index da7e6f0b9..90e3d77fe 100644 --- a/packages/appkit/src/queries/index.ts +++ b/packages/appkit/src/queries/index.ts @@ -220,6 +220,18 @@ export { type SendTransactionParameters, type SendTransactionReturnType, } from './transaction/send-transaction'; +export { + signMessageMutationOptions, + type SignMessageData, + type SignMessageErrorType, + type SignMessageMutate, + type SignMessageMutateAsync, + type SignMessageMutationOptions, + type SignMessageOptions, + type SignMessageVariables, + type SignMessageParameters, + type SignMessageReturnType, +} from './transaction/sign-message'; export { getTransactionStatusQueryOptions, type GetTransactionStatusData, diff --git a/packages/appkit/src/queries/transaction/sign-message.ts b/packages/appkit/src/queries/transaction/sign-message.ts new file mode 100644 index 000000000..f8bb8cf87 --- /dev/null +++ b/packages/appkit/src/queries/transaction/sign-message.ts @@ -0,0 +1,66 @@ +/** + * 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 { MutateOptions, MutationOptions } from '@tanstack/query-core'; + +import { signMessage } from '../../actions/transaction/sign-message'; +import type { + SignMessageErrorType, + SignMessageParameters, + SignMessageReturnType, +} from '../../actions/transaction/sign-message'; +import type { AppKit } from '../../core/app-kit'; +import type { MutationParameter } from '../../types/query'; +import type { Compute } from '../../types/utils'; + +export type { SignMessageErrorType, SignMessageParameters, SignMessageReturnType }; + +export type SignMessageOptions = MutationParameter< + SignMessageData, + SignMessageErrorType, + SignMessageVariables, + context +>; + +export const signMessageMutationOptions = ( + appKit: AppKit, + options: SignMessageOptions = {}, +): SignMessageMutationOptions => { + return { + ...options.mutation, + mutationFn(variables) { + return signMessage(appKit, variables); + }, + mutationKey: ['signMessage'], + }; +}; + +export type SignMessageMutationOptions = MutationOptions< + SignMessageData, + SignMessageErrorType, + SignMessageVariables, + context +>; + +export type SignMessageData = Compute; + +export type SignMessageVariables = SignMessageParameters; + +export type SignMessageMutate = ( + variables: SignMessageVariables, + options?: + | Compute, context>> + | undefined, +) => void; + +export type SignMessageMutateAsync = ( + variables: SignMessageVariables, + options?: + | Compute, context>> + | undefined, +) => Promise; diff --git a/packages/appkit/src/types/signing.ts b/packages/appkit/src/types/signing.ts index c97fc2403..e0d43fc79 100644 --- a/packages/appkit/src/types/signing.ts +++ b/packages/appkit/src/types/signing.ts @@ -76,3 +76,15 @@ export interface SignDataResponse { /** Original payload that was signed */ payload: SignDataRequest; } + +/** + * SignMessage Response - returned from wallet. + * + * Wallet signs a transaction-shaped request with the internal message opcode + * (instead of external), so the resulting BoC can be relayed on-chain by a + * third party (e.g. a gasless relayer) rather than broadcast directly. + */ +export interface SignMessageResponse { + /** Signed internal message BoC (base64) ready to be relayed */ + internalBoc: Base64String; +} diff --git a/packages/appkit/src/types/wallet.ts b/packages/appkit/src/types/wallet.ts index 81f42030f..b6255415a 100644 --- a/packages/appkit/src/types/wallet.ts +++ b/packages/appkit/src/types/wallet.ts @@ -6,10 +6,10 @@ * */ -import type { SendTransactionResponse, Hex, UserFriendlyAddress } from '@ton/walletkit'; +import type { Feature, SendTransactionResponse, Hex, UserFriendlyAddress } from '@ton/walletkit'; import type { TransactionRequest } from './transaction'; -import type { SignDataRequest, SignDataResponse } from './signing'; +import type { SignDataRequest, SignDataResponse, SignMessageResponse } from './signing'; import type { Network } from './network'; /** @@ -37,6 +37,13 @@ export interface WalletInterface { /** Get unique wallet identifier */ getWalletId(): string; + /** + * Features supported by the underlying wallet (e.g. SendTransaction, SignData, SignMessage). + * Returns undefined when the connector cannot report capabilities. + * Callers should gracefully degrade when a feature is missing. + */ + getSupportedFeatures(): Feature[] | undefined; + // ========================================== // Actions requiring wallet signature // ========================================== @@ -46,4 +53,13 @@ export interface WalletInterface { /** Sign arbitrary data using TonConnect signData */ signData(payload: SignDataRequest): Promise; + + /** + * Sign a transaction-shaped request without broadcasting it. + * The wallet returns a signed internal-message BoC that a third party can relay + * on-chain (e.g. a gasless relayer). + * + * Requires the wallet to support the SignMessage feature (see getSupportedFeatures). + */ + signMessage(request: TransactionRequest): Promise; } diff --git a/template/packages/appkit-react/docs/hooks.md b/template/packages/appkit-react/docs/hooks.md index 4699b2361..8f07ba845 100644 --- a/template/packages/appkit-react/docs/hooks.md +++ b/template/packages/appkit-react/docs/hooks.md @@ -246,6 +246,12 @@ Hook to send a transaction to the blockchain. %%demo/examples/src/appkit/hooks/transaction#USE_SEND_TRANSACTION%% +### `useSignMessage` + +Hook to sign a transaction-shaped request without broadcasting it. Returns a signed internal-message BoC that can be relayed on-chain by a third party (e.g. a gasless relayer). Requires wallet support for the `SignMessage` feature. + +%%demo/examples/src/appkit/hooks/transaction#USE_SIGN_MESSAGE%% + ### `useTransferTon` Hook to simplify transferring TON to another address. diff --git a/template/packages/appkit/docs/actions.md b/template/packages/appkit/docs/actions.md index 78fa64331..abdd49261 100644 --- a/template/packages/appkit/docs/actions.md +++ b/template/packages/appkit/docs/actions.md @@ -331,6 +331,12 @@ Create a TON transfer transaction request without sending it. Send a transaction to the blockchain. %%demo/examples/src/appkit/actions/transaction#SEND_TRANSACTION%% + +### `signMessage` + +Ask the connected wallet to sign a transaction-shaped request without broadcasting it. Returns a signed internal-message BoC that can be relayed on-chain by a third party (e.g. a gasless relayer). Requires wallet support for the `SignMessage` feature. + +%%demo/examples/src/appkit/actions/transaction#SIGN_MESSAGE%% ### `transferTon` From fd0e7405745d64ec4fbe993ecb247b71f944e6de Mon Sep 17 00:00:00 2001 From: VK Date: Mon, 20 Apr 2026 18:01:47 +0400 Subject: [PATCH 02/46] feat(appkit): draft gasless --- .../layout/app-router/app-router.tsx | 4 +- .../core/components/layout/layout/layout.tsx | 20 +- .../appkit-minter/src/core/configs/app-kit.ts | 7 + apps/appkit-minter/src/pages/gasless-page.tsx | 153 ++++++++++++ apps/appkit-minter/src/pages/index.ts | 2 + .../src/pages/sign-message-page.tsx | 20 ++ .../gasless/hooks/use-estimate-gasless.ts | 45 ++++ .../gasless/hooks/use-gasless-config.ts | 31 +++ .../hooks/use-send-gasless-transaction.ts | 55 ++++ .../src/features/gasless/index.ts | 23 ++ packages/appkit-react/src/index.ts | 1 + packages/appkit/package.json | 16 ++ .../src/actions/gasless/estimate-gasless.ts | 53 ++++ .../src/actions/gasless/get-gasless-config.ts | 32 +++ .../gasless/send-gasless-transaction.ts | 84 +++++++ packages/appkit/src/actions/index.ts | 20 ++ .../create-transfer-ton-transaction.ts | 7 +- .../src/core/app-kit/services/app-kit.ts | 7 + packages/appkit/src/gasless/index.ts | 20 ++ packages/appkit/src/gasless/tonapi/index.ts | 9 + packages/appkit/src/index.ts | 1 + .../src/queries/gasless/estimate-gasless.ts | 60 +++++ .../src/queries/gasless/get-gasless-config.ts | 58 +++++ .../gasless/send-gasless-transaction.ts | 70 ++++++ packages/appkit/src/queries/index.ts | 26 ++ packages/appkit/src/types/transaction.ts | 5 +- packages/appkit/src/utils/index.ts | 2 +- packages/walletkit/package.json | 19 +- .../src/api/interfaces/DefiProvider.ts | 2 +- .../src/api/interfaces/GaslessAPI.ts | 77 ++++++ .../walletkit/src/api/interfaces/index.ts | 3 +- .../src/defi/gasless/GaslessManager.ts | 85 +++++++ .../src/defi/gasless/GaslessProvider.ts | 42 ++++ packages/walletkit/src/defi/gasless/errors.ts | 21 ++ packages/walletkit/src/defi/gasless/index.ts | 19 ++ .../gasless/tonapi/TonApiGaslessProvider.ts | 234 ++++++++++++++++++ .../src/defi/gasless/tonapi/index.ts | 10 + packages/walletkit/src/defi/gasless/types.ts | 85 +++++++ packages/walletkit/src/index.ts | 9 + pnpm-lock.yaml | 20 ++ 40 files changed, 1446 insertions(+), 11 deletions(-) create mode 100644 apps/appkit-minter/src/pages/gasless-page.tsx create mode 100644 apps/appkit-minter/src/pages/sign-message-page.tsx create mode 100644 packages/appkit-react/src/features/gasless/hooks/use-estimate-gasless.ts create mode 100644 packages/appkit-react/src/features/gasless/hooks/use-gasless-config.ts create mode 100644 packages/appkit-react/src/features/gasless/hooks/use-send-gasless-transaction.ts create mode 100644 packages/appkit-react/src/features/gasless/index.ts create mode 100644 packages/appkit/src/actions/gasless/estimate-gasless.ts create mode 100644 packages/appkit/src/actions/gasless/get-gasless-config.ts create mode 100644 packages/appkit/src/actions/gasless/send-gasless-transaction.ts create mode 100644 packages/appkit/src/gasless/index.ts create mode 100644 packages/appkit/src/gasless/tonapi/index.ts create mode 100644 packages/appkit/src/queries/gasless/estimate-gasless.ts create mode 100644 packages/appkit/src/queries/gasless/get-gasless-config.ts create mode 100644 packages/appkit/src/queries/gasless/send-gasless-transaction.ts create mode 100644 packages/walletkit/src/api/interfaces/GaslessAPI.ts create mode 100644 packages/walletkit/src/defi/gasless/GaslessManager.ts create mode 100644 packages/walletkit/src/defi/gasless/GaslessProvider.ts create mode 100644 packages/walletkit/src/defi/gasless/errors.ts create mode 100644 packages/walletkit/src/defi/gasless/index.ts create mode 100644 packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts create mode 100644 packages/walletkit/src/defi/gasless/tonapi/index.ts create mode 100644 packages/walletkit/src/defi/gasless/types.ts diff --git a/apps/appkit-minter/src/core/components/layout/app-router/app-router.tsx b/apps/appkit-minter/src/core/components/layout/app-router/app-router.tsx index a10a7b60e..0046c7fd7 100644 --- a/apps/appkit-minter/src/core/components/layout/app-router/app-router.tsx +++ b/apps/appkit-minter/src/core/components/layout/app-router/app-router.tsx @@ -11,7 +11,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { useWatchBalance, useWatchTransactions, useWatchJettons, useBalance } from '@ton/appkit-react'; import { toast } from 'sonner'; -import { JettonsPage, MinterPage, NftsPage, StakingPage, SwapPage } from '@/pages'; +import { GaslessPage, JettonsPage, MinterPage, NftsPage, SignMessagePage, StakingPage, SwapPage } from '@/pages'; export const AppRouter: React.FC = () => { // Set balance refetch interval to 20 seconds @@ -57,6 +57,8 @@ export const AppRouter: React.FC = () => { } /> } /> } /> + } /> + } /> } /> diff --git a/apps/appkit-minter/src/core/components/layout/layout/layout.tsx b/apps/appkit-minter/src/core/components/layout/layout/layout.tsx index 7bfb7c0f4..fc2c314d6 100644 --- a/apps/appkit-minter/src/core/components/layout/layout/layout.tsx +++ b/apps/appkit-minter/src/core/components/layout/layout/layout.tsx @@ -7,7 +7,18 @@ */ import { TonConnectButton, useAddress } from '@ton/appkit-react'; -import { ArrowLeftRight, BookOpen, Coins, ExternalLink, Github, ImageIcon, Sparkles, Wallet } from 'lucide-react'; +import { + ArrowLeftRight, + BookOpen, + Coins, + ExternalLink, + Github, + ImageIcon, + PenLine, + Sparkles, + Wallet, + Zap, +} from 'lucide-react'; import { Link, NavLink } from 'react-router-dom'; import type { ComponentType, FC, ReactNode } from 'react'; @@ -58,6 +69,13 @@ const NAV_GROUPS: readonly { label?: string; links: readonly NavGroupLink[] }[] { to: '/staking', label: 'Staking', icon: Coins }, ], }, + { + label: 'Wallet', + links: [ + { to: '/sign', label: 'Sign Message', icon: PenLine }, + { to: '/gasless', label: 'Gasless', icon: Zap }, + ], + }, ]; const EXTERNAL_LINKS: readonly { href: string; label: string; icon: ComponentType<{ className?: string }> }[] = [ diff --git a/apps/appkit-minter/src/core/configs/app-kit.ts b/apps/appkit-minter/src/core/configs/app-kit.ts index 6a5eb67c5..cd5903d2a 100644 --- a/apps/appkit-minter/src/core/configs/app-kit.ts +++ b/apps/appkit-minter/src/core/configs/app-kit.ts @@ -17,6 +17,8 @@ import { import { createDeDustProvider } from '@ton/appkit/swap/dedust'; import { createOmnistonProvider } from '@ton/appkit/swap/omniston'; import { createTonstakersProvider } from '@ton/appkit/staking/tonstakers'; +import { TonApiGaslessProvider } from '@ton/appkit/gasless/tonapi'; +import { TonApiClient } from '@ton-api/client'; import { ENV_TON_API_KEY_TESTNET, ENV_TON_API_KEY_MAINNET } from '@/core/configs/env'; @@ -35,6 +37,10 @@ const tetraApiClient = new ApiClientTonApi({ endpoint: 'https://tetra.tonapi.io', }); +const mainnetTonApi = new TonApiClient({ + baseUrl: 'https://tonapi.io', +}); + export const appKit = new AppKit({ networks: { [Network.mainnet().chainId]: { apiClient: mainnetApiClient }, @@ -54,5 +60,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 }), + new TonApiGaslessProvider({ client: mainnetTonApi }), ], }); diff --git a/apps/appkit-minter/src/pages/gasless-page.tsx b/apps/appkit-minter/src/pages/gasless-page.tsx new file mode 100644 index 000000000..be2093b4d --- /dev/null +++ b/apps/appkit-minter/src/pages/gasless-page.tsx @@ -0,0 +1,153 @@ +/** + * 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 { useState, useMemo, useEffect } from 'react'; +import type { FC } from 'react'; +import { + useGaslessConfig, + useSendGaslessTransaction, + useAddress, + useJettonBalanceByAddress, + useJettonWalletAddress, +} from '@ton/appkit-react'; +import type { Base64String } from '@ton/appkit-react'; +import { parseUnits, createJettonTransferPayload } from '@ton/appkit'; +import { toast } from 'sonner'; + +import { Card, Layout } from '@/core/components'; + +const USDT_MASTER_MAINNET = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'; + +export const GaslessPage: FC = () => { + const address = useAddress(); + const { data: gaslessConfig, isLoading: isConfigLoading } = useGaslessConfig(); + const { mutateAsync: sendGasless, isPending: isSending } = useSendGaslessTransaction(); + + const [amount, setAmount] = useState('0.1'); + const [recipient, setRecipient] = useState(''); + + const { data: usdtBalance } = useJettonBalanceByAddress({ + jettonAddress: USDT_MASTER_MAINNET, + ownerAddress: address, + jettonDecimals: 6, + }); + + const { data: usdtWalletAddress } = useJettonWalletAddress({ + jettonAddress: USDT_MASTER_MAINNET, + ownerAddress: address, + }); + + // Set own address as default recipient + useEffect(() => { + if (address && !recipient) { + setRecipient(address); + } + }, [address, recipient]); + + const isUsdtSupported = useMemo(() => { + return gaslessConfig?.gasJettons.some((j) => j.masterId === USDT_MASTER_MAINNET); + }, [gaslessConfig]); + + const handleSend = async () => { + if (!address || !usdtWalletAddress) { + toast.error(!address ? 'Wallet not connected' : 'Could not resolve USDT wallet address'); + return; + } + + try { + const payload = createJettonTransferPayload({ + amount: parseUnits(amount, 6), // USDT has 6 decimals + destination: recipient, + responseDestination: address, + }); + + await sendGasless({ + feeJettonMaster: USDT_MASTER_MAINNET, + messages: [ + { + address: usdtWalletAddress, + amount: parseUnits('0.06', 9).toString(), + payload: payload.toBoc().toString('base64') as Base64String, + }, + ], + }); + + toast.success('Gasless transaction submitted!'); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Gasless error:', error); + toast.error(error instanceof Error ? error.message : 'Failed to send gasless transaction'); + } + }; + + return ( + + +
+

Send USDT (Gasless)

+

+ Send USDT without having TON for gas. The fee will be paid in USDT. +

+
+ + {!address && ( +
+ Please connect your wallet first. +
+ )} + + {address && ( +
+
+ +
+ {usdtBalance ? Number(usdtBalance).toFixed(2) : '0.00'} USDT +
+
+ +
+ + setRecipient(e.target.value)} + placeholder="Address" + /> +
+ +
+ + setAmount(e.target.value)} + /> +
+ + + + {!isConfigLoading && !isUsdtSupported && ( +

+ Relayer does not support USDT for gas fees on this network. +

+ )} +
+ )} +
+
+ ); +}; diff --git a/apps/appkit-minter/src/pages/index.ts b/apps/appkit-minter/src/pages/index.ts index 09a13a93d..ad60f596a 100644 --- a/apps/appkit-minter/src/pages/index.ts +++ b/apps/appkit-minter/src/pages/index.ts @@ -11,3 +11,5 @@ export { JettonsPage } from './jettons-page'; export { NftsPage } from './nfts-page'; export { SwapPage } from './swap-page'; export { StakingPage } from './staking-page'; +export { SignMessagePage } from './sign-message-page'; +export { GaslessPage } from './gasless-page'; diff --git a/apps/appkit-minter/src/pages/sign-message-page.tsx b/apps/appkit-minter/src/pages/sign-message-page.tsx new file mode 100644 index 000000000..fef84a09a --- /dev/null +++ b/apps/appkit-minter/src/pages/sign-message-page.tsx @@ -0,0 +1,20 @@ +/** + * 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 React from 'react'; + +import { Layout } from '@/core/components'; +import { SignMessageCard } from '@/features/signing'; + +export const SignMessagePage: React.FC = () => { + return ( + + + + ); +}; diff --git a/packages/appkit-react/src/features/gasless/hooks/use-estimate-gasless.ts b/packages/appkit-react/src/features/gasless/hooks/use-estimate-gasless.ts new file mode 100644 index 000000000..649bac03e --- /dev/null +++ b/packages/appkit-react/src/features/gasless/hooks/use-estimate-gasless.ts @@ -0,0 +1,45 @@ +/** + * 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 { MutateFunction, MutateOptions } from '@tanstack/react-query'; +import { estimateGaslessMutationOptions } from '@ton/appkit/queries'; +import type { + EstimateGaslessData, + EstimateGaslessErrorType, + EstimateGaslessMutationConfig, + EstimateGaslessVariables, +} from '@ton/appkit/queries'; + +import { useAppKit } from '../../settings'; +import { useMutation } from '../../../libs/query'; +import type { UseMutationReturnType } from '../../../libs/query'; + +export type UseEstimateGaslessParameters = EstimateGaslessMutationConfig; + +export type UseEstimateGaslessReturnType = UseMutationReturnType< + EstimateGaslessData, + EstimateGaslessErrorType, + EstimateGaslessVariables, + context, + ( + variables: EstimateGaslessVariables, + options?: MutateOptions, + ) => void, + MutateFunction +>; + +/** + * Hook to estimate gasless transaction. + */ +export const useEstimateGasless = ( + parameters: UseEstimateGaslessParameters = {}, +): UseEstimateGaslessReturnType => { + const appKit = useAppKit(); + + return useMutation(estimateGaslessMutationOptions(appKit, parameters)); +}; diff --git a/packages/appkit-react/src/features/gasless/hooks/use-gasless-config.ts b/packages/appkit-react/src/features/gasless/hooks/use-gasless-config.ts new file mode 100644 index 000000000..d62731e16 --- /dev/null +++ b/packages/appkit-react/src/features/gasless/hooks/use-gasless-config.ts @@ -0,0 +1,31 @@ +/** + * 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 { getGaslessConfigQueryOptions } from '@ton/appkit/queries'; +import type { GetGaslessConfigData, GetGaslessConfigErrorType, GetGaslessConfigQueryConfig } from '@ton/appkit/queries'; + +import { useAppKit } from '../../settings'; +import { useQuery } from '../../../libs/query'; +import type { UseQueryReturnType } from '../../../libs/query'; + +export type UseGaslessConfigParameters = GetGaslessConfigQueryConfig; +export type UseGaslessConfigReturnType = UseQueryReturnType< + selectData, + GetGaslessConfigErrorType +>; + +/** + * Hook to get gasless relayer configuration. + */ +export const useGaslessConfig = ( + parameters: UseGaslessConfigParameters = {}, +): UseGaslessConfigReturnType => { + const appKit = useAppKit(); + + return useQuery(getGaslessConfigQueryOptions(appKit, parameters)); +}; diff --git a/packages/appkit-react/src/features/gasless/hooks/use-send-gasless-transaction.ts b/packages/appkit-react/src/features/gasless/hooks/use-send-gasless-transaction.ts new file mode 100644 index 000000000..53f0dd5eb --- /dev/null +++ b/packages/appkit-react/src/features/gasless/hooks/use-send-gasless-transaction.ts @@ -0,0 +1,55 @@ +/** + * 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 { MutateFunction, MutateOptions } from '@tanstack/react-query'; +import { sendGaslessTransactionMutationOptions } from '@ton/appkit/queries'; +import type { + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionMutationConfig, + SendGaslessTransactionVariables, +} from '@ton/appkit/queries'; + +import { useAppKit } from '../../settings'; +import { useMutation } from '../../../libs/query'; +import type { UseMutationReturnType } from '../../../libs/query'; + +export type UseSendGaslessTransactionParameters = SendGaslessTransactionMutationConfig; + +export type UseSendGaslessTransactionReturnType = UseMutationReturnType< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context, + ( + variables: SendGaslessTransactionVariables, + options?: MutateOptions< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context + >, + ) => void, + MutateFunction< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context + > +>; + +/** + * Hook to send gasless transaction. + */ +export const useSendGaslessTransaction = ( + parameters: UseSendGaslessTransactionParameters = {}, +): UseSendGaslessTransactionReturnType => { + const appKit = useAppKit(); + + return useMutation(sendGaslessTransactionMutationOptions(appKit, parameters)); +}; diff --git a/packages/appkit-react/src/features/gasless/index.ts b/packages/appkit-react/src/features/gasless/index.ts new file mode 100644 index 000000000..4ffc4f984 --- /dev/null +++ b/packages/appkit-react/src/features/gasless/index.ts @@ -0,0 +1,23 @@ +/** + * 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. + * + */ + +export { + useGaslessConfig, + type UseGaslessConfigParameters, + type UseGaslessConfigReturnType, +} from './hooks/use-gasless-config'; +export { + useEstimateGasless, + type UseEstimateGaslessParameters, + type UseEstimateGaslessReturnType, +} from './hooks/use-estimate-gasless'; +export { + useSendGaslessTransaction, + type UseSendGaslessTransactionParameters, + type UseSendGaslessTransactionReturnType, +} from './hooks/use-send-gasless-transaction'; diff --git a/packages/appkit-react/src/index.ts b/packages/appkit-react/src/index.ts index 2ae249e19..a82b9c3ef 100644 --- a/packages/appkit-react/src/index.ts +++ b/packages/appkit-react/src/index.ts @@ -39,5 +39,6 @@ export * from './features/settings'; export * from './features/swap'; export * from './features/signing'; export * from './features/staking'; +export * from './features/gasless'; export * from './types/appkit-ui-token'; diff --git a/packages/appkit/package.json b/packages/appkit/package.json index a797c1150..c8af9fe5b 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -60,6 +60,16 @@ "types": "./dist/cjs/staking/tonstakers/index.d.ts", "default": "./dist/cjs/staking/tonstakers/index.js" } + }, + "./gasless/tonapi": { + "import": { + "types": "./dist/esm/gasless/tonapi/index.d.ts", + "default": "./dist/esm/gasless/tonapi/index.js" + }, + "require": { + "types": "./dist/cjs/gasless/tonapi/index.d.ts", + "default": "./dist/cjs/gasless/tonapi/index.js" + } } }, "typesVersions": { @@ -75,6 +85,9 @@ ], "staking/tonstakers": [ "./dist/esm/staking/tonstakers/index.d.ts" + ], + "gasless/tonapi": [ + "./dist/esm/gasless/tonapi/index.d.ts" ] } }, @@ -106,6 +119,9 @@ "@ston-fi/omniston-sdk": { "optional": true }, + "@ton-api/client": { + "optional": true + }, "@tanstack/query-core": { "optional": true } diff --git a/packages/appkit/src/actions/gasless/estimate-gasless.ts b/packages/appkit/src/actions/gasless/estimate-gasless.ts new file mode 100644 index 000000000..9a4afb297 --- /dev/null +++ b/packages/appkit/src/actions/gasless/estimate-gasless.ts @@ -0,0 +1,53 @@ +/** + * 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 { GaslessEstimateResult } from '@ton/walletkit'; + +import type { TransactionRequestMessage } from '../../types/transaction'; +import type { AppKit } from '../../core/app-kit'; +import { getSelectedWallet } from '../wallets/get-selected-wallet'; + +export interface EstimateGaslessParameters { + /** Master address of the jetton used to pay the relayer's fee */ + feeJettonMaster: string; + /** User's messages to include in the gasless transaction */ + messages: TransactionRequestMessage[]; + /** Gasless provider id. Uses the default provider when omitted. */ + providerId?: string; +} + +export type EstimateGaslessReturnType = Promise; + +export type EstimateGaslessErrorType = Error; + +/** + * Ask the relayer to estimate a gasless transaction. + * + * Returns relayer-wrapped messages (ready to be signed via `signMessage`), the + * commission charged in the fee jetton, and the bundle validity window. + */ +export const estimateGasless = async ( + appKit: AppKit, + parameters: EstimateGaslessParameters, +): EstimateGaslessReturnType => { + const wallet = getSelectedWallet(appKit); + + if (!wallet) { + throw new Error('Wallet not connected'); + } + + return appKit.gaslessManager.estimate( + { + feeJettonMaster: parameters.feeJettonMaster, + walletAddress: wallet.getAddress(), + walletPublicKey: wallet.getPublicKey(), + messages: parameters.messages, + }, + parameters.providerId, + ); +}; diff --git a/packages/appkit/src/actions/gasless/get-gasless-config.ts b/packages/appkit/src/actions/gasless/get-gasless-config.ts new file mode 100644 index 000000000..89053c162 --- /dev/null +++ b/packages/appkit/src/actions/gasless/get-gasless-config.ts @@ -0,0 +1,32 @@ +/** + * 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 { GaslessConfig } from '@ton/walletkit'; + +import type { AppKit } from '../../core/app-kit'; + +export interface GetGaslessConfigOptions { + /** Gasless provider id. Uses the default provider when omitted. */ + providerId?: string; +} + +export type GetGaslessConfigReturnType = Promise; + +export type GetGaslessConfigErrorType = Error; + +/** + * Fetch gasless relayer configuration. + * + * Returns the relay address and jettons accepted by the relayer as fee payment. + */ +export const getGaslessConfig = async ( + appKit: AppKit, + options: GetGaslessConfigOptions = {}, +): GetGaslessConfigReturnType => { + return appKit.gaslessManager.getConfig(options.providerId); +}; diff --git a/packages/appkit/src/actions/gasless/send-gasless-transaction.ts b/packages/appkit/src/actions/gasless/send-gasless-transaction.ts new file mode 100644 index 000000000..0e20b478f --- /dev/null +++ b/packages/appkit/src/actions/gasless/send-gasless-transaction.ts @@ -0,0 +1,84 @@ +/** + * 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 { GaslessSendResult } from '@ton/walletkit'; + +import type { Base64String } from '../../types/primitives'; +import type { TransactionRequestMessage } from '../../types/transaction'; +import type { AppKit } from '../../core/app-kit'; +import { getSelectedWallet } from '../wallets/get-selected-wallet'; + +export interface SendGaslessTransactionParameters { + /** Master address of the jetton used to pay the relayer's fee */ + feeJettonMaster: string; + /** User's messages to include in the gasless transaction */ + messages: TransactionRequestMessage[]; + /** Gasless provider id. Uses the default provider when omitted. */ + providerId?: string; +} + +export interface SendGaslessTransactionReturnType { + /** Signed internal BoC that was submitted to the relayer */ + internalBoc: Base64String; + /** Relayer commission in fee-jetton nanounits */ + commission: bigint; + /** Provider-specific relayer response */ + relayerResponse: GaslessSendResult; +} + +export type SendGaslessTransactionErrorType = Error; + +/** + * Execute a full gasless transaction flow: + * 1. estimate (with the relayer) + * 2. sign the relayer-wrapped messages via the wallet's `signMessage` + * 3. submit the signed BoC to the relayer + * + * Requires the wallet to support the SignMessage feature. + */ +export const sendGaslessTransaction = async ( + appKit: AppKit, + parameters: SendGaslessTransactionParameters, +): Promise => { + const wallet = getSelectedWallet(appKit); + + if (!wallet) { + throw new Error('Wallet not connected'); + } + + const walletPublicKey = wallet.getPublicKey(); + + const estimate = await appKit.gaslessManager.estimate( + { + feeJettonMaster: parameters.feeJettonMaster, + walletAddress: wallet.getAddress(), + walletPublicKey, + messages: parameters.messages, + }, + parameters.providerId, + ); + + const { internalBoc } = await wallet.signMessage({ + messages: estimate.messages, + validUntil: estimate.validUntil, + }); + + const relayerResponse = await appKit.gaslessManager.send( + { + walletPublicKey, + internalBoc, + }, + parameters.providerId, + ); + + return { + internalBoc, + commission: estimate.commission, + relayerResponse, + }; +}; diff --git a/packages/appkit/src/actions/index.ts b/packages/appkit/src/actions/index.ts index e090f69d8..7d2368841 100644 --- a/packages/appkit/src/actions/index.ts +++ b/packages/appkit/src/actions/index.ts @@ -108,6 +108,26 @@ export { transferNft, type TransferNftParameters, type TransferNftReturnType } f // Providers export { registerProvider, type RegisterProviderOptions } from './providers/register-provider'; +// Gasless +export { + getGaslessConfig, + type GetGaslessConfigOptions, + type GetGaslessConfigReturnType, + type GetGaslessConfigErrorType, +} from './gasless/get-gasless-config'; +export { + estimateGasless, + type EstimateGaslessParameters, + type EstimateGaslessReturnType, + type EstimateGaslessErrorType, +} from './gasless/estimate-gasless'; +export { + sendGaslessTransaction, + type SendGaslessTransactionParameters, + type SendGaslessTransactionReturnType, + type SendGaslessTransactionErrorType, +} from './gasless/send-gasless-transaction'; + // Signing export { signText, type SignTextParameters, type SignTextReturnType } from './signing/sign-text'; export { signBinary, type SignBinaryParameters, type SignBinaryReturnType } from './signing/sign-binary'; diff --git a/packages/appkit/src/actions/transaction/create-transfer-ton-transaction.ts b/packages/appkit/src/actions/transaction/create-transfer-ton-transaction.ts index bf84ad836..b27d2b882 100644 --- a/packages/appkit/src/actions/transaction/create-transfer-ton-transaction.ts +++ b/packages/appkit/src/actions/transaction/create-transfer-ton-transaction.ts @@ -9,6 +9,7 @@ import { createCommentPayloadBase64, parseUnits } from '@ton/walletkit'; import type { TransactionRequest, TransactionRequestMessage } from '../../types/transaction'; +import type { Base64String } from '../../types/primitives'; import type { AppKit } from '../../core/app-kit'; import { getSelectedWallet } from '../wallets/get-selected-wallet'; @@ -45,14 +46,14 @@ export const createTransferTonTransaction = ( const message: TransactionRequestMessage = { address: recipientAddress, amount: parseUnits(amount, 9).toString(), - stateInit, + stateInit: stateInit as Base64String, }; // Payload takes priority, otherwise use comment if (payload) { - message.payload = payload; + message.payload = payload as Base64String; } else if (comment) { - message.payload = createCommentPayloadBase64(comment); + message.payload = createCommentPayloadBase64(comment) as Base64String; } return { diff --git a/packages/appkit/src/core/app-kit/services/app-kit.ts b/packages/appkit/src/core/app-kit/services/app-kit.ts index fcb685a37..8c0048ee0 100644 --- a/packages/appkit/src/core/app-kit/services/app-kit.ts +++ b/packages/appkit/src/core/app-kit/services/app-kit.ts @@ -12,6 +12,8 @@ import type { ProviderInput, SwapProviderInterface, StakingProviderInterface, St import type { AppKitConfig } from '../types/config'; import { CONNECTOR_EVENTS, WALLETS_EVENTS } from '../constants/events'; import { StakingManager } from '../../../staking'; +import { GaslessManager } from '../../../gasless'; +import type { GaslessProviderInterface } from '../../../gasless'; import type { Connector, ConnectorFactoryContext, ConnectorInput } from '../../../types/connector'; import { EventEmitter } from '../../emitter'; import type { AppKitEmitter, AppKitEvents } from '../types/events'; @@ -33,6 +35,7 @@ export class AppKit { readonly walletsManager: WalletsManager; readonly swapManager: SwapManager; readonly stakingManager: StakingManager; + readonly gaslessManager: GaslessManager; readonly networkManager: AppKitNetworkManager; readonly streamingManager: StreamingManager; @@ -56,6 +59,7 @@ export class AppKit { this.swapManager = new SwapManager(() => this.createFactoryContext()); this.stakingManager = new StakingManager(() => this.createFactoryContext()); + this.gaslessManager = new GaslessManager(() => this.createFactoryContext()); this.streamingManager = new StreamingManager(() => this.createFactoryContext()); if (config.connectors) { @@ -126,6 +130,9 @@ export class AppKit { case 'streaming': this.streamingManager.registerProvider(provider as StreamingProvider); break; + case 'gasless': + this.gaslessManager.registerProvider(provider as GaslessProviderInterface); + break; default: throw new Error('Unknown provider type'); } diff --git a/packages/appkit/src/gasless/index.ts b/packages/appkit/src/gasless/index.ts new file mode 100644 index 000000000..7c771ffb3 --- /dev/null +++ b/packages/appkit/src/gasless/index.ts @@ -0,0 +1,20 @@ +/** + * 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. + * + */ + +export { GaslessProvider, GaslessError, GaslessManager } from '@ton/walletkit'; + +export type { + GaslessAPI, + GaslessProviderInterface, + GaslessConfig, + GaslessGasJetton, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from '@ton/walletkit'; diff --git a/packages/appkit/src/gasless/tonapi/index.ts b/packages/appkit/src/gasless/tonapi/index.ts new file mode 100644 index 000000000..29bf67892 --- /dev/null +++ b/packages/appkit/src/gasless/tonapi/index.ts @@ -0,0 +1,9 @@ +/** + * 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. + * + */ + +export * from '@ton/walletkit/gasless/tonapi'; diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index b851df432..855cc962d 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -35,6 +35,7 @@ export * from './connectors/tonconnect'; export * from './swap'; export * from './staking'; +export * from './gasless'; // Actions export * from './actions'; diff --git a/packages/appkit/src/queries/gasless/estimate-gasless.ts b/packages/appkit/src/queries/gasless/estimate-gasless.ts new file mode 100644 index 000000000..2ba317097 --- /dev/null +++ b/packages/appkit/src/queries/gasless/estimate-gasless.ts @@ -0,0 +1,60 @@ +/** + * 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 { MutationOptions } from '@tanstack/query-core'; + +import { estimateGasless } from '../../actions/gasless/estimate-gasless'; +import type { + EstimateGaslessErrorType, + EstimateGaslessParameters, + EstimateGaslessReturnType, +} from '../../actions/gasless/estimate-gasless'; +import type { AppKit } from '../../core/app-kit'; +import type { MutationParameter } from '../../types/query'; +import type { Compute } from '../../types/utils'; + +export type { EstimateGaslessErrorType }; + +export type EstimateGaslessMutationConfig = MutationParameter< + EstimateGaslessData, + EstimateGaslessErrorType, + EstimateGaslessVariables, + context +>; + +export const estimateGaslessMutationOptions = ( + appKit: AppKit, + config: EstimateGaslessMutationConfig = {}, +): EstimateGaslessMutationOptions => { + return { + ...config.mutation, + mutationFn: (variables: EstimateGaslessVariables) => estimateGasless(appKit, variables), + mutationKey: ['estimateGasless'], + }; +}; + +export type EstimateGaslessVariables = Compute; + +export type EstimateGaslessData = Compute>; + +export type EstimateGaslessMutate = ( + variables: EstimateGaslessVariables, + options?: MutationOptions, +) => void; + +export type EstimateGaslessMutateAsync = ( + variables: EstimateGaslessVariables, + options?: MutationOptions, +) => Promise; + +export type EstimateGaslessMutationOptions = MutationOptions< + EstimateGaslessData, + EstimateGaslessErrorType, + EstimateGaslessVariables, + context +>; diff --git a/packages/appkit/src/queries/gasless/get-gasless-config.ts b/packages/appkit/src/queries/gasless/get-gasless-config.ts new file mode 100644 index 000000000..815d4af38 --- /dev/null +++ b/packages/appkit/src/queries/gasless/get-gasless-config.ts @@ -0,0 +1,58 @@ +/** + * 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 { getGaslessConfig } from '../../actions/gasless/get-gasless-config'; +import type { + GetGaslessConfigErrorType, + GetGaslessConfigOptions, + GetGaslessConfigReturnType, +} from '../../actions/gasless/get-gasless-config'; +import type { AppKit } from '../../core/app-kit'; +import type { QueryOptions, QueryParameter } from '../../types/query'; +import type { Compute, ExactPartial } from '../../types/utils'; +import { filterQueryOptions } from '../../utils'; + +export type { GetGaslessConfigErrorType }; + +export type GetGaslessConfigQueryConfig = Compute< + ExactPartial +> & + QueryParameter; + +export const getGaslessConfigQueryOptions = ( + appKit: AppKit, + options: GetGaslessConfigQueryConfig = {}, +): GetGaslessConfigQueryOptions => { + return { + ...options.query, + queryFn: async (context) => { + const [, parameters] = context.queryKey as [string, GetGaslessConfigOptions]; + return getGaslessConfig(appKit, parameters); + }, + queryKey: getGaslessConfigQueryKey(options), + }; +}; + +export type GetGaslessConfigQueryFnData = Compute>; + +export type GetGaslessConfigData = GetGaslessConfigQueryFnData; + +export const getGaslessConfigQueryKey = ( + options: Compute> = {}, +): GetGaslessConfigQueryKey => { + return ['gaslessConfig', filterQueryOptions(options as unknown as Record)] as const; +}; + +export type GetGaslessConfigQueryKey = readonly ['gaslessConfig', Compute>]; + +export type GetGaslessConfigQueryOptions = QueryOptions< + GetGaslessConfigQueryFnData, + GetGaslessConfigErrorType, + selectData, + GetGaslessConfigQueryKey +>; diff --git a/packages/appkit/src/queries/gasless/send-gasless-transaction.ts b/packages/appkit/src/queries/gasless/send-gasless-transaction.ts new file mode 100644 index 000000000..01832bf1d --- /dev/null +++ b/packages/appkit/src/queries/gasless/send-gasless-transaction.ts @@ -0,0 +1,70 @@ +/** + * 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 { MutationOptions } from '@tanstack/query-core'; + +import { sendGaslessTransaction } from '../../actions/gasless/send-gasless-transaction'; +import type { + SendGaslessTransactionErrorType, + SendGaslessTransactionParameters, + SendGaslessTransactionReturnType, +} from '../../actions/gasless/send-gasless-transaction'; +import type { AppKit } from '../../core/app-kit'; +import type { MutationParameter } from '../../types/query'; +import type { Compute } from '../../types/utils'; + +export type { SendGaslessTransactionErrorType }; + +export type SendGaslessTransactionMutationConfig = MutationParameter< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context +>; + +export const sendGaslessTransactionMutationOptions = ( + appKit: AppKit, + config: SendGaslessTransactionMutationConfig = {}, +): SendGaslessTransactionMutationOptions => { + return { + ...config.mutation, + mutationFn: (variables: SendGaslessTransactionVariables) => sendGaslessTransaction(appKit, variables), + mutationKey: ['sendGaslessTransaction'], + }; +}; + +export type SendGaslessTransactionVariables = Compute; + +export type SendGaslessTransactionData = Compute>; + +export type SendGaslessTransactionMutate = ( + variables: SendGaslessTransactionVariables, + options?: MutationOptions< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context + >, +) => void; + +export type SendGaslessTransactionMutateAsync = ( + variables: SendGaslessTransactionVariables, + options?: MutationOptions< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context + >, +) => Promise; + +export type SendGaslessTransactionMutationOptions = MutationOptions< + SendGaslessTransactionData, + SendGaslessTransactionErrorType, + SendGaslessTransactionVariables, + context +>; diff --git a/packages/appkit/src/queries/index.ts b/packages/appkit/src/queries/index.ts index 90e3d77fe..0468f2c6d 100644 --- a/packages/appkit/src/queries/index.ts +++ b/packages/appkit/src/queries/index.ts @@ -165,6 +165,32 @@ export { type BuildSwapTransactionVariables, } from './swap/build-swap-transaction'; +// Gasless +export { + getGaslessConfigQueryOptions, + type GetGaslessConfigQueryConfig, + type GetGaslessConfigData, + type GetGaslessConfigErrorType, +} from './gasless/get-gasless-config'; +export { + estimateGaslessMutationOptions, + type EstimateGaslessMutationConfig, + type EstimateGaslessData, + type EstimateGaslessErrorType, + type EstimateGaslessMutate, + type EstimateGaslessMutateAsync, + type EstimateGaslessVariables, +} from './gasless/estimate-gasless'; +export { + sendGaslessTransactionMutationOptions, + type SendGaslessTransactionMutationConfig, + type SendGaslessTransactionData, + type SendGaslessTransactionErrorType, + type SendGaslessTransactionMutate, + type SendGaslessTransactionMutateAsync, + type SendGaslessTransactionVariables, +} from './gasless/send-gasless-transaction'; + // Staking export { getStakingQuoteQueryOptions, diff --git a/packages/appkit/src/types/transaction.ts b/packages/appkit/src/types/transaction.ts index 0255b717b..d5bab0f82 100644 --- a/packages/appkit/src/types/transaction.ts +++ b/packages/appkit/src/types/transaction.ts @@ -9,6 +9,7 @@ import type { ExtraCurrencies, TokenAmount } from '@ton/walletkit'; import type { Network } from './network'; +import type { Base64String } from './primitives'; export type { TransactionStatus } from '@ton/walletkit'; @@ -56,10 +57,10 @@ export interface TransactionRequestMessage { /** * Initial state for deploying a new contract, encoded in Base64 */ - stateInit?: string; + stateInit?: Base64String; /** * Message payload data encoded in Base64 */ - payload?: string; + payload?: Base64String; } diff --git a/packages/appkit/src/utils/index.ts b/packages/appkit/src/utils/index.ts index 369be1302..97cf50e67 100644 --- a/packages/appkit/src/utils/index.ts +++ b/packages/appkit/src/utils/index.ts @@ -6,7 +6,7 @@ * */ -export { formatUnits, parseUnits, compareAddress } from '@ton/walletkit'; +export { formatUnits, parseUnits, compareAddress, createJettonTransferPayload } from '@ton/walletkit'; export * from './address/is-valid-address'; export * from './address/to-bounceble-address'; diff --git a/packages/walletkit/package.json b/packages/walletkit/package.json index a3b513474..49fa6ef5e 100644 --- a/packages/walletkit/package.json +++ b/packages/walletkit/package.json @@ -60,6 +60,16 @@ "types": "./dist/cjs/defi/staking/tonstakers/index.d.ts", "default": "./dist/cjs/defi/staking/tonstakers/index.js" } + }, + "./gasless/tonapi": { + "import": { + "types": "./dist/esm/defi/gasless/tonapi/index.d.ts", + "default": "./dist/esm/defi/gasless/tonapi/index.js" + }, + "require": { + "types": "./dist/cjs/defi/gasless/tonapi/index.d.ts", + "default": "./dist/cjs/defi/gasless/tonapi/index.js" + } } }, "typesVersions": { @@ -75,6 +85,9 @@ ], "staking/tonstakers": [ "./dist/cjs/defi/staking/tonstakers/index.d.ts" + ], + "gasless/tonapi": [ + "./dist/cjs/defi/gasless/tonapi/index.d.ts" ] } }, @@ -123,10 +136,14 @@ "peerDependenciesMeta": { "@ston-fi/omniston-sdk": { "optional": true + }, + "@ton-api/client": { + "optional": true } }, "optionalDependencies": { - "@ston-fi/omniston-sdk": "^0.7.9" + "@ston-fi/omniston-sdk": "^0.7.9", + "@ton-api/client": "^0.4.0" }, "devDependencies": { "@jest/globals": "^30.2.0", diff --git a/packages/walletkit/src/api/interfaces/DefiProvider.ts b/packages/walletkit/src/api/interfaces/DefiProvider.ts index 33661ca41..a3eb7ec49 100644 --- a/packages/walletkit/src/api/interfaces/DefiProvider.ts +++ b/packages/walletkit/src/api/interfaces/DefiProvider.ts @@ -11,7 +11,7 @@ import type { Network, BaseProvider } from '../models'; /** * Type of provider */ -export type DefiProviderType = 'swap' | 'staking'; +export type DefiProviderType = 'swap' | 'staking' | 'gasless'; /** * Base interface for all DeFi providers diff --git a/packages/walletkit/src/api/interfaces/GaslessAPI.ts b/packages/walletkit/src/api/interfaces/GaslessAPI.ts new file mode 100644 index 000000000..3e59b0890 --- /dev/null +++ b/packages/walletkit/src/api/interfaces/GaslessAPI.ts @@ -0,0 +1,77 @@ +/** + * 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 { + GaslessConfig, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from '../../defi/gasless/types'; +import type { DefiManagerAPI } from './DefiManagerAPI'; +import type { DefiProvider } from './DefiProvider'; + +/** + * Gasless API interface exposed by GaslessManager. + * + * Gasless lets a dApp submit on-chain transactions without the user paying TON + * for gas: a relayer co-signs and covers the gas, taking a jetton fee in return. + */ +export interface GaslessAPI extends DefiManagerAPI { + /** + * Fetch relayer configuration (supported jettons and relay address). + * @param providerId Provider identifier (optional, uses default if not specified) + */ + getConfig(providerId?: string): Promise; + + /** + * Estimate fees and obtain relayer-wrapped messages for signing. + * + * Pass the returned `messages` to `wallet.signMessage` to obtain a signed + * internal-message BoC, then submit it via `send`. + * + * @param params Estimation parameters (wallet identity, fee jetton, messages) + * @param providerId Provider identifier (optional, uses default if not specified) + */ + estimate(params: GaslessEstimateParams, providerId?: string): Promise; + + /** + * Submit a signed transaction BoC to the relayer for on-chain execution. + * + * @param params Signed message and wallet public key + * @param providerId Provider identifier (optional, uses default if not specified) + */ + send(params: GaslessSendParams, providerId?: string): Promise; +} + +/** + * Interface that all gasless providers must implement. + */ +export interface GaslessProviderInterface extends DefiProvider { + readonly type: 'gasless'; + + /** + * Unique identifier for the provider + */ + readonly providerId: string; + + /** + * Fetch relayer configuration (supported jettons and relay address). + */ + getConfig(): Promise; + + /** + * Estimate fees and return relayer-wrapped messages for signing. + */ + estimate(params: GaslessEstimateParams): Promise; + + /** + * Submit a signed transaction BoC to the relayer. + */ + send(params: GaslessSendParams): Promise; +} diff --git a/packages/walletkit/src/api/interfaces/index.ts b/packages/walletkit/src/api/interfaces/index.ts index cdaca3627..aee441303 100644 --- a/packages/walletkit/src/api/interfaces/index.ts +++ b/packages/walletkit/src/api/interfaces/index.ts @@ -12,9 +12,10 @@ export type { WalletSigner, ISigner } from './WalletSigner'; // Defi interfaces export type { DefiManagerAPI } from './DefiManagerAPI'; -export type { DefiProvider } from './DefiProvider'; +export type { DefiProvider, DefiProviderType } from './DefiProvider'; export type { SwapAPI, SwapProviderInterface } from './SwapAPI'; export type { StakingAPI, StakingProviderInterface } from './StakingAPI'; +export type { GaslessAPI, GaslessProviderInterface } from './GaslessAPI'; export type { TONConnectSessionManager } from './TONConnectSessionManager'; diff --git a/packages/walletkit/src/defi/gasless/GaslessManager.ts b/packages/walletkit/src/defi/gasless/GaslessManager.ts new file mode 100644 index 000000000..e57838838 --- /dev/null +++ b/packages/walletkit/src/defi/gasless/GaslessManager.ts @@ -0,0 +1,85 @@ +/** + * 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 { GaslessAPI, GaslessProviderInterface } from '../../api/interfaces'; +import { globalLogger } from '../../core/Logger'; +import type { ProviderFactoryContext } from '../../types/factory'; +import { DefiManager } from '../DefiManager'; +import { GaslessError } from './errors'; +import type { + GaslessConfig, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from './types'; + +const log = globalLogger.createChild('GaslessManager'); + +/** + * GaslessManager — manages gasless relay providers and delegates gasless operations. + * + * Allows registration of multiple gasless providers and provides a unified API. + * Providers can be switched dynamically. + */ +export class GaslessManager extends DefiManager implements GaslessAPI { + constructor(createFactoryContext: () => ProviderFactoryContext) { + super(createFactoryContext); + } + + /** + * Fetch relayer configuration (supported jettons and relay address). + */ + async getConfig(providerId?: string): Promise { + log.debug('Getting gasless config', { providerId: providerId ?? this.defaultProviderId }); + + try { + return await this.getProvider(providerId ?? this.defaultProviderId).getConfig(); + } catch (error) { + log.error('Failed to get gasless config', { error }); + throw error; + } + } + + /** + * Estimate fees and obtain relayer-wrapped messages for signing. + */ + async estimate(params: GaslessEstimateParams, providerId?: string): Promise { + log.debug('Estimating gasless transaction', { + walletAddress: params.walletAddress, + feeJettonMaster: params.feeJettonMaster, + messagesCount: params.messages.length, + providerId: providerId ?? this.defaultProviderId, + }); + + try { + return await this.getProvider(providerId ?? this.defaultProviderId).estimate(params); + } catch (error) { + log.error('Failed to estimate gasless transaction', { error, params }); + throw error; + } + } + + /** + * Submit a signed transaction BoC to the relayer. + */ + async send(params: GaslessSendParams, providerId?: string): Promise { + log.debug('Sending gasless transaction', { providerId: providerId ?? this.defaultProviderId }); + + try { + return await this.getProvider(providerId ?? this.defaultProviderId).send(params); + } catch (error) { + log.error('Failed to send gasless transaction', { error }); + throw error; + } + } + + protected createError(message: string, code: string, details?: unknown): GaslessError { + return new GaslessError(message, code, details); + } +} diff --git a/packages/walletkit/src/defi/gasless/GaslessProvider.ts b/packages/walletkit/src/defi/gasless/GaslessProvider.ts new file mode 100644 index 000000000..0d0f3071b --- /dev/null +++ b/packages/walletkit/src/defi/gasless/GaslessProvider.ts @@ -0,0 +1,42 @@ +/** + * 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 { GaslessProviderInterface } from '../../api/interfaces'; +import type { + GaslessConfig, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from './types'; + +/** + * Abstract base class for gasless relay providers. + * + * Concrete providers (e.g. TonApiGaslessProvider) implement the three methods + * below against a specific relayer backend. + * + * @example + * ```typescript + * class MyGaslessProvider extends GaslessProvider { + * readonly providerId = 'my-relayer'; + * + * async getConfig(): Promise { ... } + * async estimate(params): Promise { ... } + * async send(params): Promise { ... } + * } + * ``` + */ +export abstract class GaslessProvider implements GaslessProviderInterface { + readonly type = 'gasless'; + abstract readonly providerId: string; + + abstract getConfig(): Promise; + abstract estimate(params: GaslessEstimateParams): Promise; + abstract send(params: GaslessSendParams): Promise; +} diff --git a/packages/walletkit/src/defi/gasless/errors.ts b/packages/walletkit/src/defi/gasless/errors.ts new file mode 100644 index 000000000..a0374cb5c --- /dev/null +++ b/packages/walletkit/src/defi/gasless/errors.ts @@ -0,0 +1,21 @@ +/** + * 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 { DefiManagerError } from '../errors'; + +export class GaslessError extends DefiManagerError { + static readonly UNSUPPORTED_FEE_JETTON = 'UNSUPPORTED_FEE_JETTON'; + static readonly ESTIMATE_FAILED = 'ESTIMATE_FAILED'; + static readonly SEND_FAILED = 'SEND_FAILED'; + static readonly CONFIG_FAILED = 'CONFIG_FAILED'; + + constructor(message: string, code: string, details?: unknown) { + super(message, code, details); + this.name = 'GaslessError'; + } +} diff --git a/packages/walletkit/src/defi/gasless/index.ts b/packages/walletkit/src/defi/gasless/index.ts new file mode 100644 index 000000000..ef5702477 --- /dev/null +++ b/packages/walletkit/src/defi/gasless/index.ts @@ -0,0 +1,19 @@ +/** + * 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. + * + */ + +export { GaslessProvider } from './GaslessProvider'; +export { GaslessManager } from './GaslessManager'; +export { GaslessError } from './errors'; +export type { + GaslessConfig, + GaslessGasJetton, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from './types'; diff --git a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts new file mode 100644 index 000000000..8cbc4283c --- /dev/null +++ b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts @@ -0,0 +1,234 @@ +/** + * 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 { + Address, + beginCell, + Cell, + external, + internal, + loadMessageRelaxed, + storeMessage, + storeMessageRelaxed, +} from '@ton/core'; +import type { TonApiClient } from '@ton-api/client'; + +import type { Base64String } from '../../../api/models/core/Primitives'; +import { globalLogger } from '../../../core/Logger'; +import { CallForSuccess } from '../../../utils/retry'; +import { GaslessError } from '../errors'; +import { GaslessProvider } from '../GaslessProvider'; +import type { + GaslessConfig, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from '../types'; + +const log = globalLogger.createChild('TonApiGaslessProvider'); + +/** + * Configuration for TonApiGaslessProvider. + */ +export interface TonApiGaslessProviderConfig { + /** Pre-configured TonApi client (brings its own baseUrl / API key). */ + client: TonApiClient; + /** Optional provider id override. Defaults to 'tonapi'. */ + providerId?: string; + /** Number of send retries on transient errors. Defaults to 5. */ + sendRetries?: number; + /** Delay between send retries in ms. Defaults to 2000. */ + sendRetryDelayMs?: number; +} + +/** + * Gasless provider implementation backed by TonApi (@ton-api/client). + * + * Follows the public gasless flow documented at + * https://docs.tonapi.io/tonapi/rest-api/gasless. + * + * @example + * ```typescript + * import { TonApiClient } from '@ton-api/client'; + * import { TonApiGaslessProvider } from '@ton/walletkit/gasless/tonapi'; + * + * const provider = new TonApiGaslessProvider({ + * client: new TonApiClient({ baseUrl: 'https://tonapi.io' }), + * }); + * + * kit.registerProvider(provider); + * ``` + */ +export class TonApiGaslessProvider extends GaslessProvider { + readonly providerId: string; + + private readonly client: TonApiClient; + private readonly sendRetries: number; + private readonly sendRetryDelayMs: number; + + constructor(config: TonApiGaslessProviderConfig) { + super(); + this.client = config.client; + this.providerId = config.providerId ?? 'tonapi'; + this.sendRetries = config.sendRetries ?? 5; + this.sendRetryDelayMs = config.sendRetryDelayMs ?? 2000; + } + + async getConfig(): Promise { + try { + const cfg = await this.client.gasless.gaslessConfig(); + return { + relayAddress: cfg.relayAddress.toString({ bounceable: true }), + gasJettons: cfg.gasJettons.map((jetton) => ({ + masterId: jetton.masterId.toString({ bounceable: true }), + })), + }; + } catch (error) { + log.error('Failed to fetch gasless config', { error }); + throw new GaslessError( + error instanceof Error ? error.message : 'Failed to fetch gasless config', + GaslessError.CONFIG_FAILED, + error, + ); + } + } + + async estimate(params: GaslessEstimateParams): Promise { + const feeJettonMaster = Address.parse(params.feeJettonMaster); + const walletAddress = Address.parse(params.walletAddress); + const walletPublicKey = stripHexPrefix(params.walletPublicKey); + + const messagesBoc = params.messages.map((message) => ({ + boc: buildInternalMessageCell(message), + })); + + try { + const result = await this.client.gasless.gaslessEstimate(feeJettonMaster, { + walletAddress, + walletPublicKey, + messages: messagesBoc, + }); + + return { + messages: result.messages.map((message) => ({ + address: message.address.toString({ bounceable: true }), + amount: message.amount, + payload: message.payload ? cellToBase64(message.payload) : undefined, + stateInit: message.stateInit ? cellToBase64(message.stateInit) : undefined, + })), + commission: result.commission, + validUntil: result.validUntil, + relayAddress: result.relayAddress.toString({ bounceable: true }), + from: result.from.toString({ bounceable: true }), + }; + } catch (error) { + log.error('Failed to estimate gasless transaction', { error, params }); + throw new GaslessError( + error instanceof Error ? error.message : 'Failed to estimate gasless transaction', + GaslessError.ESTIMATE_FAILED, + error, + ); + } + } + + async send(params: GaslessSendParams): Promise { + const walletPublicKey = stripHexPrefix(params.walletPublicKey); + const externalBoc = internalBocToExternalMessageBoc(params.internalBoc); + + try { + const result = await CallForSuccess( + () => + this.client.gasless.gaslessSend({ + walletPublicKey, + boc: externalBoc, + }), + this.sendRetries, + this.sendRetryDelayMs, + ); + + return (result ?? {}) as GaslessSendResult; + } catch (error) { + log.error('Failed to send gasless transaction', { error }); + throw new GaslessError( + error instanceof Error ? error.message : 'Failed to send gasless transaction', + GaslessError.SEND_FAILED, + error, + ); + } + } +} + +function stripHexPrefix(value: string): string { + return value.startsWith('0x') ? value.slice(2) : value; +} + +function cellToBase64(cell: Cell): Base64String { + return cell.toBoc().toString('base64') as Base64String; +} + +function buildInternalMessageCell(message: { + address: string; + amount: string | bigint; + payload?: string; + stateInit?: string; +}): Cell { + const to = Address.parse(message.address); + const value = BigInt(message.amount); + const body = message.payload ? Cell.fromBase64(message.payload) : beginCell().endCell(); + const init = message.stateInit ? parseStateInit(message.stateInit) : undefined; + + return beginCell() + .storeWritable( + storeMessageRelaxed( + internal({ + to, + value, + bounce: true, + body, + init, + }), + ), + ) + .endCell(); +} + +function parseStateInit(base64: string) { + const cell = Cell.fromBase64(base64); + const slice = cell.beginParse(); + const maybeCodeBit = slice.loadBit(); + const code = maybeCodeBit ? slice.loadRef() : undefined; + const maybeDataBit = slice.loadBit(); + const data = maybeDataBit ? slice.loadRef() : undefined; + return { code, data }; +} + +/** + * Convert an internal-message BoC (what `wallet.signMessage` returns) into an + * external message BoC that the relayer accepts for broadcast. + */ +function internalBocToExternalMessageBoc(internalBoc: Base64String): Cell { + const parsed = Cell.fromBase64(internalBoc); + const { info, body, init } = loadMessageRelaxed(parsed.beginParse()); + + if (info.type !== 'internal') { + throw new GaslessError('Signed message must be an internal message', GaslessError.SEND_FAILED); + } + + return beginCell() + .storeWritable( + storeMessage( + external({ + to: info.dest, + init: init ?? undefined, + body, + }), + ), + ) + .endCell(); +} diff --git a/packages/walletkit/src/defi/gasless/tonapi/index.ts b/packages/walletkit/src/defi/gasless/tonapi/index.ts new file mode 100644 index 000000000..5b50dd0c6 --- /dev/null +++ b/packages/walletkit/src/defi/gasless/tonapi/index.ts @@ -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. + * + */ + +export { TonApiGaslessProvider } from './TonApiGaslessProvider'; +export type { TonApiGaslessProviderConfig } from './TonApiGaslessProvider'; diff --git a/packages/walletkit/src/defi/gasless/types.ts b/packages/walletkit/src/defi/gasless/types.ts new file mode 100644 index 000000000..de7c956fd --- /dev/null +++ b/packages/walletkit/src/defi/gasless/types.ts @@ -0,0 +1,85 @@ +/** + * 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 { Base64String, Hex, UserFriendlyAddress } from '../../api/models/core/Primitives'; +import type { TransactionRequestMessage } from '../../api/models/transactions/TransactionRequest'; + +/** + * Relayer configuration for gasless transactions. + * + * Reports which jettons the relayer accepts as fee payment and the address + * where relayer commission is routed. + */ +export interface GaslessConfig { + /** Address where the relayer expects to receive the commission */ + relayAddress: UserFriendlyAddress; + /** Jettons supported by the relayer for paying the fee */ + gasJettons: GaslessGasJetton[]; +} + +/** + * A jetton accepted by the relayer as a fee payment option. + */ +export interface GaslessGasJetton { + /** Jetton master address */ + masterId: UserFriendlyAddress; +} + +/** + * Parameters to estimate a gasless transaction. + * + * The relayer wraps the caller's messages with commission-collection logic and + * returns a new set of messages that the wallet should sign via `signMessage`. + */ +export interface GaslessEstimateParams { + /** Master address of the jetton used to pay the relayer's fee */ + feeJettonMaster: UserFriendlyAddress; + /** Sender wallet address */ + walletAddress: UserFriendlyAddress; + /** Sender wallet public key */ + walletPublicKey: Hex; + /** Messages that the caller wants to include in the transaction */ + messages: TransactionRequestMessage[]; +} + +/** + * Result of gasless estimation. + * + * Contains relayer-wrapped messages that should be passed to `wallet.signMessage` + * in place of the caller's original messages, together with the commission the + * relayer will deduct and the timestamp after which the estimate expires. + */ +export interface GaslessEstimateResult { + /** Relayer-wrapped messages ready to be signed */ + messages: TransactionRequestMessage[]; + /** Relayer commission in fee-jetton nanounits */ + commission: bigint; + /** Unix timestamp after which the bundle becomes invalid for relay */ + validUntil: number; + /** Address of the relayer that produced this estimate */ + relayAddress: UserFriendlyAddress; + /** Sender wallet address echoed by the relayer */ + from: UserFriendlyAddress; +} + +/** + * Parameters to submit a signed gasless transaction to the relayer. + */ +export interface GaslessSendParams { + /** Sender wallet public key */ + walletPublicKey: Hex; + /** Signed internal-message BoC obtained from `wallet.signMessage` */ + internalBoc: Base64String; +} + +/** + * Result of submitting a signed gasless transaction to the relayer. + * + * Fields are provider-specific; callers should not rely on a particular shape. + */ +export type GaslessSendResult = Record; diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index f9c899ef3..0f79ad429 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -22,6 +22,15 @@ export { JettonsManager } from './core/JettonsManager'; export { DefiError, DefiErrorCode } from './defi/errors'; export { SwapManager, SwapProvider, SwapError, SwapErrorCode } from './defi/swap'; export { StakingManager, StakingProvider, StakingError, StakingErrorCode } from './defi/staking'; +export { GaslessManager, GaslessProvider, GaslessError } from './defi/gasless'; +export type { + GaslessConfig, + GaslessGasJetton, + GaslessEstimateParams, + GaslessEstimateResult, + GaslessSendParams, + GaslessSendResult, +} from './defi/gasless'; export { EventEmitter } from './core/EventEmitter'; export type { EventListener, EventPayload, KitEvent } from './core/EventEmitter'; export type { SharedKitEvents } from './types/emitter'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d6a1e1be..9e5c9292f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -878,6 +878,9 @@ importers: '@ston-fi/omniston-sdk': specifier: ^0.7.9 version: 0.7.9(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@ton-api/client': + specifier: ^0.4.0 + version: 0.4.0(@ton/core@0.63.1(@ton/crypto@3.3.0)) packages/walletkit-android-bridge: dependencies: @@ -4030,6 +4033,11 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@ton-api/client@0.4.0': + resolution: {integrity: sha512-k3d6RzNWRDEZpVa9Gig2kHqp74tGvaK/imkjb08RIGxn+wKC7Yv5g98GnXEHf3srRkjtRCG0Nnjx8EsasOMJkw==} + peerDependencies: + '@ton/core': '>=0.59.0' + '@ton-community/ton-ledger@7.3.0': resolution: {integrity: sha512-eG4KqQaQoUgdVzedUlt8ZkwHDw7JiXnPqZOO+1fUKISwjU2OdiRIeo+TB0yKK3X3fm/0hgQbFBEZAbGSCKvzTw==} peerDependencies: @@ -5345,6 +5353,9 @@ packages: core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + core-js-pure@3.49.0: + resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -13991,6 +14002,12 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@ton-api/client@0.4.0(@ton/core@0.63.1(@ton/crypto@3.3.0))': + dependencies: + '@ton/core': 0.63.1(@ton/crypto@3.3.0) + core-js-pure: 3.49.0 + optional: true + '@ton-community/ton-ledger@7.3.0(@ton/core@0.63.1(@ton/crypto@3.3.0))': dependencies: '@ledgerhq/hw-transport': 6.35.0 @@ -15619,6 +15636,9 @@ snapshots: dependencies: browserslist: 4.28.1 + core-js-pure@3.49.0: + optional: true + core-util-is@1.0.3: {} cors@2.8.6: From 7df94cd7daa744671f49935b72b646ff4416d6af Mon Sep 17 00:00:00 2001 From: VK Date: Mon, 20 Apr 2026 21:20:43 +0400 Subject: [PATCH 03/46] feat(appkit): use loadStateInit --- .../gasless/tonapi/TonApiGaslessProvider.ts | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts index 8cbc4283c..525c2d42a 100644 --- a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts +++ b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts @@ -13,12 +13,13 @@ import { external, internal, loadMessageRelaxed, + loadStateInit, storeMessage, storeMessageRelaxed, } from '@ton/core'; import type { TonApiClient } from '@ton-api/client'; -import type { Base64String } from '../../../api/models/core/Primitives'; +import type { Base64String, TransactionRequestMessage } from '../../../api/models'; import { globalLogger } from '../../../core/Logger'; import { CallForSuccess } from '../../../utils/retry'; import { GaslessError } from '../errors'; @@ -172,16 +173,11 @@ function cellToBase64(cell: Cell): Base64String { return cell.toBoc().toString('base64') as Base64String; } -function buildInternalMessageCell(message: { - address: string; - amount: string | bigint; - payload?: string; - stateInit?: string; -}): Cell { +function buildInternalMessageCell(message: TransactionRequestMessage): Cell { const to = Address.parse(message.address); const value = BigInt(message.amount); const body = message.payload ? Cell.fromBase64(message.payload) : beginCell().endCell(); - const init = message.stateInit ? parseStateInit(message.stateInit) : undefined; + const init = message.stateInit ? loadStateInit(Cell.fromBase64(message.stateInit).beginParse()) : undefined; return beginCell() .storeWritable( @@ -189,6 +185,10 @@ function buildInternalMessageCell(message: { internal({ to, value, + // Jetton transfers (the primary gasless use case) require bounce=true; + // TransactionRequestMessage does not carry a bounce flag, so we default + // to true. Callers needing non-bounceable messages should handle that + // outside the gasless flow. bounce: true, body, init, @@ -198,16 +198,6 @@ function buildInternalMessageCell(message: { .endCell(); } -function parseStateInit(base64: string) { - const cell = Cell.fromBase64(base64); - const slice = cell.beginParse(); - const maybeCodeBit = slice.loadBit(); - const code = maybeCodeBit ? slice.loadRef() : undefined; - const maybeDataBit = slice.loadBit(); - const data = maybeDataBit ? slice.loadRef() : undefined; - return { code, data }; -} - /** * Convert an internal-message BoC (what `wallet.signMessage` returns) into an * external message BoC that the relayer accepts for broadcast. From 960555a6dca28b8aca0685e781ed663119f73c3b Mon Sep 17 00:00:00 2001 From: VK Date: Mon, 20 Apr 2026 22:20:41 +0400 Subject: [PATCH 04/46] feat(gasless): move types to models --- apps/appkit-minter/src/pages/gasless-page.tsx | 4 +- .../src/actions/gasless/estimate-gasless.ts | 2 +- .../gasless/send-gasless-transaction.ts | 13 +-- packages/appkit/src/gasless/index.ts | 1 - .../src/api/interfaces/GaslessAPI.ts | 12 +-- .../src/api/models/gasless/GaslessConfig.ts | 23 +++++ .../models/gasless/GaslessEstimateParams.ts | 27 ++++++ .../models/gasless/GaslessEstimateResult.ts | 31 ++++++ .../api/models/gasless/GaslessGasJetton.ts | 17 ++++ .../api/models/gasless/GaslessSendParams.ts | 19 ++++ packages/walletkit/src/api/models/index.ts | 7 ++ .../src/defi/gasless/GaslessManager.ts | 12 +-- .../src/defi/gasless/GaslessProvider.ts | 12 +-- packages/walletkit/src/defi/gasless/index.ts | 8 -- .../gasless/tonapi/TonApiGaslessProvider.ts | 95 +++---------------- .../src/defi/gasless/tonapi/utils.ts | 80 ++++++++++++++++ packages/walletkit/src/defi/gasless/types.ts | 85 ----------------- packages/walletkit/src/index.ts | 8 -- 18 files changed, 233 insertions(+), 223 deletions(-) create mode 100644 packages/walletkit/src/api/models/gasless/GaslessConfig.ts create mode 100644 packages/walletkit/src/api/models/gasless/GaslessEstimateParams.ts create mode 100644 packages/walletkit/src/api/models/gasless/GaslessEstimateResult.ts create mode 100644 packages/walletkit/src/api/models/gasless/GaslessGasJetton.ts create mode 100644 packages/walletkit/src/api/models/gasless/GaslessSendParams.ts create mode 100644 packages/walletkit/src/defi/gasless/tonapi/utils.ts delete mode 100644 packages/walletkit/src/defi/gasless/types.ts diff --git a/apps/appkit-minter/src/pages/gasless-page.tsx b/apps/appkit-minter/src/pages/gasless-page.tsx index be2093b4d..26d33a95c 100644 --- a/apps/appkit-minter/src/pages/gasless-page.tsx +++ b/apps/appkit-minter/src/pages/gasless-page.tsx @@ -16,7 +16,7 @@ import { useJettonWalletAddress, } from '@ton/appkit-react'; import type { Base64String } from '@ton/appkit-react'; -import { parseUnits, createJettonTransferPayload } from '@ton/appkit'; +import { parseUnits, createJettonTransferPayload, compareAddress } from '@ton/appkit'; import { toast } from 'sonner'; import { Card, Layout } from '@/core/components'; @@ -50,7 +50,7 @@ export const GaslessPage: FC = () => { }, [address, recipient]); const isUsdtSupported = useMemo(() => { - return gaslessConfig?.gasJettons.some((j) => j.masterId === USDT_MASTER_MAINNET); + return gaslessConfig?.supportedGasJettons.some((j) => compareAddress(j.jettonMaster, USDT_MASTER_MAINNET)); }, [gaslessConfig]); const handleSend = async () => { diff --git a/packages/appkit/src/actions/gasless/estimate-gasless.ts b/packages/appkit/src/actions/gasless/estimate-gasless.ts index 9a4afb297..114b4000b 100644 --- a/packages/appkit/src/actions/gasless/estimate-gasless.ts +++ b/packages/appkit/src/actions/gasless/estimate-gasless.ts @@ -29,7 +29,7 @@ export type EstimateGaslessErrorType = Error; * Ask the relayer to estimate a gasless transaction. * * Returns relayer-wrapped messages (ready to be signed via `signMessage`), the - * commission charged in the fee jetton, and the bundle validity window. + * fee charged in the fee jetton, and the bundle validity window. */ export const estimateGasless = async ( appKit: AppKit, diff --git a/packages/appkit/src/actions/gasless/send-gasless-transaction.ts b/packages/appkit/src/actions/gasless/send-gasless-transaction.ts index 0e20b478f..3a82dcf9f 100644 --- a/packages/appkit/src/actions/gasless/send-gasless-transaction.ts +++ b/packages/appkit/src/actions/gasless/send-gasless-transaction.ts @@ -6,7 +6,7 @@ * */ -import type { GaslessSendResult } from '@ton/walletkit'; +import type { TokenAmount } from '@ton/walletkit'; import type { Base64String } from '../../types/primitives'; import type { TransactionRequestMessage } from '../../types/transaction'; @@ -25,10 +25,8 @@ export interface SendGaslessTransactionParameters { export interface SendGaslessTransactionReturnType { /** Signed internal BoC that was submitted to the relayer */ internalBoc: Base64String; - /** Relayer commission in fee-jetton nanounits */ - commission: bigint; - /** Provider-specific relayer response */ - relayerResponse: GaslessSendResult; + /** Relayer fee in fee-jetton nanounits */ + fee: TokenAmount; } export type SendGaslessTransactionErrorType = Error; @@ -68,7 +66,7 @@ export const sendGaslessTransaction = async ( validUntil: estimate.validUntil, }); - const relayerResponse = await appKit.gaslessManager.send( + await appKit.gaslessManager.send( { walletPublicKey, internalBoc, @@ -78,7 +76,6 @@ export const sendGaslessTransaction = async ( return { internalBoc, - commission: estimate.commission, - relayerResponse, + fee: estimate.fee, }; }; diff --git a/packages/appkit/src/gasless/index.ts b/packages/appkit/src/gasless/index.ts index 7c771ffb3..5aa5053dc 100644 --- a/packages/appkit/src/gasless/index.ts +++ b/packages/appkit/src/gasless/index.ts @@ -16,5 +16,4 @@ export type { GaslessEstimateParams, GaslessEstimateResult, GaslessSendParams, - GaslessSendResult, } from '@ton/walletkit'; diff --git a/packages/walletkit/src/api/interfaces/GaslessAPI.ts b/packages/walletkit/src/api/interfaces/GaslessAPI.ts index 3e59b0890..1b2f027e5 100644 --- a/packages/walletkit/src/api/interfaces/GaslessAPI.ts +++ b/packages/walletkit/src/api/interfaces/GaslessAPI.ts @@ -6,13 +6,7 @@ * */ -import type { - GaslessConfig, - GaslessEstimateParams, - GaslessEstimateResult, - GaslessSendParams, - GaslessSendResult, -} from '../../defi/gasless/types'; +import type { GaslessConfig, GaslessEstimateParams, GaslessEstimateResult, GaslessSendParams } from '../models'; import type { DefiManagerAPI } from './DefiManagerAPI'; import type { DefiProvider } from './DefiProvider'; @@ -46,7 +40,7 @@ export interface GaslessAPI extends DefiManagerAPI { * @param params Signed message and wallet public key * @param providerId Provider identifier (optional, uses default if not specified) */ - send(params: GaslessSendParams, providerId?: string): Promise; + send(params: GaslessSendParams, providerId?: string): Promise; } /** @@ -73,5 +67,5 @@ export interface GaslessProviderInterface extends DefiProvider { /** * Submit a signed transaction BoC to the relayer. */ - send(params: GaslessSendParams): Promise; + send(params: GaslessSendParams): Promise; } diff --git a/packages/walletkit/src/api/models/gasless/GaslessConfig.ts b/packages/walletkit/src/api/models/gasless/GaslessConfig.ts new file mode 100644 index 000000000..c61be999a --- /dev/null +++ b/packages/walletkit/src/api/models/gasless/GaslessConfig.ts @@ -0,0 +1,23 @@ +/** + * 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 { UserFriendlyAddress } from '../core/Primitives'; +import type { GaslessGasJetton } from './GaslessGasJetton'; + +/** + * Relayer configuration for gasless transactions. + * + * Reports which jettons the relayer accepts as fee payment and the address + * where the relayer fee is routed. + */ +export interface GaslessConfig { + /** Address where the relayer expects to receive the fee */ + relayAddress: UserFriendlyAddress; + /** Jettons supported by the relayer for paying the fee */ + supportedGasJettons: GaslessGasJetton[]; +} diff --git a/packages/walletkit/src/api/models/gasless/GaslessEstimateParams.ts b/packages/walletkit/src/api/models/gasless/GaslessEstimateParams.ts new file mode 100644 index 000000000..de1298172 --- /dev/null +++ b/packages/walletkit/src/api/models/gasless/GaslessEstimateParams.ts @@ -0,0 +1,27 @@ +/** + * 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 { Hex, UserFriendlyAddress } from '../core/Primitives'; +import type { TransactionRequestMessage } from '../transactions/TransactionRequest'; + +/** + * Parameters to estimate a gasless transaction. + * + * The relayer wraps the caller's messages with fee-collection logic and + * returns a new set of messages that the wallet should sign via `signMessage`. + */ +export interface GaslessEstimateParams { + /** Master address of the jetton used to pay the relayer's fee */ + feeJettonMaster: UserFriendlyAddress; + /** Sender wallet address */ + walletAddress: UserFriendlyAddress; + /** Sender wallet public key */ + walletPublicKey: Hex; + /** Messages that the caller wants to include in the transaction */ + messages: TransactionRequestMessage[]; +} diff --git a/packages/walletkit/src/api/models/gasless/GaslessEstimateResult.ts b/packages/walletkit/src/api/models/gasless/GaslessEstimateResult.ts new file mode 100644 index 000000000..2ac48d670 --- /dev/null +++ b/packages/walletkit/src/api/models/gasless/GaslessEstimateResult.ts @@ -0,0 +1,31 @@ +/** + * 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 { UserFriendlyAddress } from '../core/Primitives'; +import type { TokenAmount } from '../core/TokenAmount'; +import type { TransactionRequestMessage } from '../transactions/TransactionRequest'; + +/** + * Result of gasless estimation. + * + * Contains relayer-wrapped messages that should be passed to `wallet.signMessage` + * in place of the caller's original messages, together with the fee the relayer + * will deduct and the timestamp after which the estimate expires. + */ +export interface GaslessEstimateResult { + /** Relayer-wrapped messages ready to be signed */ + messages: TransactionRequestMessage[]; + /** Relayer fee in fee-jetton nanounits */ + fee: TokenAmount; + /** Unix timestamp after which the bundle becomes invalid for relay */ + validUntil: number; + /** Address of the relayer that produced this estimate */ + relayAddress: UserFriendlyAddress; + /** Sender wallet address echoed by the relayer */ + from: UserFriendlyAddress; +} diff --git a/packages/walletkit/src/api/models/gasless/GaslessGasJetton.ts b/packages/walletkit/src/api/models/gasless/GaslessGasJetton.ts new file mode 100644 index 000000000..d8cb80417 --- /dev/null +++ b/packages/walletkit/src/api/models/gasless/GaslessGasJetton.ts @@ -0,0 +1,17 @@ +/** + * 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 { UserFriendlyAddress } from '../core/Primitives'; + +/** + * A jetton accepted by the relayer as a fee payment option. + */ +export interface GaslessGasJetton { + /** Jetton master address */ + jettonMaster: UserFriendlyAddress; +} diff --git a/packages/walletkit/src/api/models/gasless/GaslessSendParams.ts b/packages/walletkit/src/api/models/gasless/GaslessSendParams.ts new file mode 100644 index 000000000..829a2ef90 --- /dev/null +++ b/packages/walletkit/src/api/models/gasless/GaslessSendParams.ts @@ -0,0 +1,19 @@ +/** + * 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 { Base64String, Hex } from '../core/Primitives'; + +/** + * Parameters to submit a signed gasless transaction to the relayer. + */ +export interface GaslessSendParams { + /** Sender wallet public key */ + walletPublicKey: Hex; + /** Signed internal-message BoC obtained from `wallet.signMessage` */ + internalBoc: Base64String; +} diff --git a/packages/walletkit/src/api/models/index.ts b/packages/walletkit/src/api/models/index.ts index f6e946734..fbc6750a0 100644 --- a/packages/walletkit/src/api/models/index.ts +++ b/packages/walletkit/src/api/models/index.ts @@ -137,6 +137,13 @@ export type { StakingQuoteParams } from './staking/StakingQuoteParams'; export type { UnstakeModes } from './staking/UnstakeMode'; export { UnstakeMode } from './staking/UnstakeMode'; +// Gasless models +export type { GaslessConfig } from './gasless/GaslessConfig'; +export type { GaslessGasJetton } from './gasless/GaslessGasJetton'; +export type { GaslessEstimateParams } from './gasless/GaslessEstimateParams'; +export type { GaslessEstimateResult } from './gasless/GaslessEstimateResult'; +export type { GaslessSendParams } from './gasless/GaslessSendParams'; + // Transaction models export * from './transactions/Transaction'; export type { TransactionAddressMetadata, TransactionAddressMetadataEntry } from './transactions/TransactionMetadata'; diff --git a/packages/walletkit/src/defi/gasless/GaslessManager.ts b/packages/walletkit/src/defi/gasless/GaslessManager.ts index e57838838..1ffc7e1c8 100644 --- a/packages/walletkit/src/defi/gasless/GaslessManager.ts +++ b/packages/walletkit/src/defi/gasless/GaslessManager.ts @@ -7,17 +7,11 @@ */ import type { GaslessAPI, GaslessProviderInterface } from '../../api/interfaces'; +import type { GaslessConfig, GaslessEstimateParams, GaslessEstimateResult, GaslessSendParams } from '../../api/models'; import { globalLogger } from '../../core/Logger'; import type { ProviderFactoryContext } from '../../types/factory'; import { DefiManager } from '../DefiManager'; import { GaslessError } from './errors'; -import type { - GaslessConfig, - GaslessEstimateParams, - GaslessEstimateResult, - GaslessSendParams, - GaslessSendResult, -} from './types'; const log = globalLogger.createChild('GaslessManager'); @@ -68,11 +62,11 @@ export class GaslessManager extends DefiManager implem /** * Submit a signed transaction BoC to the relayer. */ - async send(params: GaslessSendParams, providerId?: string): Promise { + async send(params: GaslessSendParams, providerId?: string): Promise { log.debug('Sending gasless transaction', { providerId: providerId ?? this.defaultProviderId }); try { - return await this.getProvider(providerId ?? this.defaultProviderId).send(params); + await this.getProvider(providerId ?? this.defaultProviderId).send(params); } catch (error) { log.error('Failed to send gasless transaction', { error }); throw error; diff --git a/packages/walletkit/src/defi/gasless/GaslessProvider.ts b/packages/walletkit/src/defi/gasless/GaslessProvider.ts index 0d0f3071b..74ea8056a 100644 --- a/packages/walletkit/src/defi/gasless/GaslessProvider.ts +++ b/packages/walletkit/src/defi/gasless/GaslessProvider.ts @@ -7,13 +7,7 @@ */ import type { GaslessProviderInterface } from '../../api/interfaces'; -import type { - GaslessConfig, - GaslessEstimateParams, - GaslessEstimateResult, - GaslessSendParams, - GaslessSendResult, -} from './types'; +import type { GaslessConfig, GaslessEstimateParams, GaslessEstimateResult, GaslessSendParams } from '../../api/models'; /** * Abstract base class for gasless relay providers. @@ -28,7 +22,7 @@ import type { * * async getConfig(): Promise { ... } * async estimate(params): Promise { ... } - * async send(params): Promise { ... } + * async send(params): Promise { ... } * } * ``` */ @@ -38,5 +32,5 @@ export abstract class GaslessProvider implements GaslessProviderInterface { abstract getConfig(): Promise; abstract estimate(params: GaslessEstimateParams): Promise; - abstract send(params: GaslessSendParams): Promise; + abstract send(params: GaslessSendParams): Promise; } diff --git a/packages/walletkit/src/defi/gasless/index.ts b/packages/walletkit/src/defi/gasless/index.ts index ef5702477..00e27cc3c 100644 --- a/packages/walletkit/src/defi/gasless/index.ts +++ b/packages/walletkit/src/defi/gasless/index.ts @@ -9,11 +9,3 @@ export { GaslessProvider } from './GaslessProvider'; export { GaslessManager } from './GaslessManager'; export { GaslessError } from './errors'; -export type { - GaslessConfig, - GaslessGasJetton, - GaslessEstimateParams, - GaslessEstimateResult, - GaslessSendParams, - GaslessSendResult, -} from './types'; diff --git a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts index 525c2d42a..705208ee4 100644 --- a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts +++ b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts @@ -6,31 +6,20 @@ * */ -import { - Address, - beginCell, - Cell, - external, - internal, - loadMessageRelaxed, - loadStateInit, - storeMessage, - storeMessageRelaxed, -} from '@ton/core'; +import { Address } from '@ton/core'; import type { TonApiClient } from '@ton-api/client'; -import type { Base64String, TransactionRequestMessage } from '../../../api/models'; -import { globalLogger } from '../../../core/Logger'; -import { CallForSuccess } from '../../../utils/retry'; -import { GaslessError } from '../errors'; -import { GaslessProvider } from '../GaslessProvider'; import type { GaslessConfig, GaslessEstimateParams, GaslessEstimateResult, GaslessSendParams, - GaslessSendResult, -} from '../types'; +} from '../../../api/models'; +import { globalLogger } from '../../../core/Logger'; +import { CallForSuccess } from '../../../utils/retry'; +import { GaslessError } from '../errors'; +import { GaslessProvider } from '../GaslessProvider'; +import { buildInternalMessageCell, cellToBase64, internalBocToExternalMessageBoc, stripHexPrefix } from './utils'; const log = globalLogger.createChild('TonApiGaslessProvider'); @@ -86,8 +75,8 @@ export class TonApiGaslessProvider extends GaslessProvider { const cfg = await this.client.gasless.gaslessConfig(); return { relayAddress: cfg.relayAddress.toString({ bounceable: true }), - gasJettons: cfg.gasJettons.map((jetton) => ({ - masterId: jetton.masterId.toString({ bounceable: true }), + supportedGasJettons: cfg.gasJettons.map((jetton) => ({ + jettonMaster: jetton.masterId.toString({ bounceable: true }), })), }; } catch (error) { @@ -123,7 +112,7 @@ export class TonApiGaslessProvider extends GaslessProvider { payload: message.payload ? cellToBase64(message.payload) : undefined, stateInit: message.stateInit ? cellToBase64(message.stateInit) : undefined, })), - commission: result.commission, + fee: result.commission.toString(), validUntil: result.validUntil, relayAddress: result.relayAddress.toString({ bounceable: true }), from: result.from.toString({ bounceable: true }), @@ -138,12 +127,12 @@ export class TonApiGaslessProvider extends GaslessProvider { } } - async send(params: GaslessSendParams): Promise { + async send(params: GaslessSendParams): Promise { const walletPublicKey = stripHexPrefix(params.walletPublicKey); const externalBoc = internalBocToExternalMessageBoc(params.internalBoc); try { - const result = await CallForSuccess( + await CallForSuccess( () => this.client.gasless.gaslessSend({ walletPublicKey, @@ -152,8 +141,6 @@ export class TonApiGaslessProvider extends GaslessProvider { this.sendRetries, this.sendRetryDelayMs, ); - - return (result ?? {}) as GaslessSendResult; } catch (error) { log.error('Failed to send gasless transaction', { error }); throw new GaslessError( @@ -164,61 +151,3 @@ export class TonApiGaslessProvider extends GaslessProvider { } } } - -function stripHexPrefix(value: string): string { - return value.startsWith('0x') ? value.slice(2) : value; -} - -function cellToBase64(cell: Cell): Base64String { - return cell.toBoc().toString('base64') as Base64String; -} - -function buildInternalMessageCell(message: TransactionRequestMessage): Cell { - const to = Address.parse(message.address); - const value = BigInt(message.amount); - const body = message.payload ? Cell.fromBase64(message.payload) : beginCell().endCell(); - const init = message.stateInit ? loadStateInit(Cell.fromBase64(message.stateInit).beginParse()) : undefined; - - return beginCell() - .storeWritable( - storeMessageRelaxed( - internal({ - to, - value, - // Jetton transfers (the primary gasless use case) require bounce=true; - // TransactionRequestMessage does not carry a bounce flag, so we default - // to true. Callers needing non-bounceable messages should handle that - // outside the gasless flow. - bounce: true, - body, - init, - }), - ), - ) - .endCell(); -} - -/** - * Convert an internal-message BoC (what `wallet.signMessage` returns) into an - * external message BoC that the relayer accepts for broadcast. - */ -function internalBocToExternalMessageBoc(internalBoc: Base64String): Cell { - const parsed = Cell.fromBase64(internalBoc); - const { info, body, init } = loadMessageRelaxed(parsed.beginParse()); - - if (info.type !== 'internal') { - throw new GaslessError('Signed message must be an internal message', GaslessError.SEND_FAILED); - } - - return beginCell() - .storeWritable( - storeMessage( - external({ - to: info.dest, - init: init ?? undefined, - body, - }), - ), - ) - .endCell(); -} diff --git a/packages/walletkit/src/defi/gasless/tonapi/utils.ts b/packages/walletkit/src/defi/gasless/tonapi/utils.ts new file mode 100644 index 000000000..1f389cfe7 --- /dev/null +++ b/packages/walletkit/src/defi/gasless/tonapi/utils.ts @@ -0,0 +1,80 @@ +/** + * 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 { + Address, + beginCell, + Cell, + external, + internal, + loadMessageRelaxed, + loadStateInit, + storeMessage, + storeMessageRelaxed, +} from '@ton/core'; + +import type { Base64String, TransactionRequestMessage } from '../../../api/models'; +import { GaslessError } from '../errors'; + +export const stripHexPrefix = (value: string): string => { + return value.startsWith('0x') ? value.slice(2) : value; +}; + +export const cellToBase64 = (cell: Cell): Base64String => { + return cell.toBoc().toString('base64') as Base64String; +}; + +export const buildInternalMessageCell = (message: TransactionRequestMessage): Cell => { + const to = Address.parse(message.address); + const value = BigInt(message.amount); + const body = message.payload ? Cell.fromBase64(message.payload) : beginCell().endCell(); + const init = message.stateInit ? loadStateInit(Cell.fromBase64(message.stateInit).beginParse()) : undefined; + + return beginCell() + .storeWritable( + storeMessageRelaxed( + internal({ + to, + value, + // Jetton transfers (the primary gasless use case) require bounce=true; + // TransactionRequestMessage does not carry a bounce flag, so we default + // to true. Callers needing non-bounceable messages should handle that + // outside the gasless flow. + bounce: true, + body, + init, + }), + ), + ) + .endCell(); +}; + +/** + * Convert an internal-message BoC (what `wallet.signMessage` returns) into an + * external message BoC that the relayer accepts for broadcast. + */ +export const internalBocToExternalMessageBoc = (internalBoc: Base64String): Cell => { + const parsed = Cell.fromBase64(internalBoc); + const { info, body, init } = loadMessageRelaxed(parsed.beginParse()); + + if (info.type !== 'internal') { + throw new GaslessError('Signed message must be an internal message', GaslessError.SEND_FAILED); + } + + return beginCell() + .storeWritable( + storeMessage( + external({ + to: info.dest, + init: init ?? undefined, + body, + }), + ), + ) + .endCell(); +}; diff --git a/packages/walletkit/src/defi/gasless/types.ts b/packages/walletkit/src/defi/gasless/types.ts deleted file mode 100644 index de7c956fd..000000000 --- a/packages/walletkit/src/defi/gasless/types.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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 { Base64String, Hex, UserFriendlyAddress } from '../../api/models/core/Primitives'; -import type { TransactionRequestMessage } from '../../api/models/transactions/TransactionRequest'; - -/** - * Relayer configuration for gasless transactions. - * - * Reports which jettons the relayer accepts as fee payment and the address - * where relayer commission is routed. - */ -export interface GaslessConfig { - /** Address where the relayer expects to receive the commission */ - relayAddress: UserFriendlyAddress; - /** Jettons supported by the relayer for paying the fee */ - gasJettons: GaslessGasJetton[]; -} - -/** - * A jetton accepted by the relayer as a fee payment option. - */ -export interface GaslessGasJetton { - /** Jetton master address */ - masterId: UserFriendlyAddress; -} - -/** - * Parameters to estimate a gasless transaction. - * - * The relayer wraps the caller's messages with commission-collection logic and - * returns a new set of messages that the wallet should sign via `signMessage`. - */ -export interface GaslessEstimateParams { - /** Master address of the jetton used to pay the relayer's fee */ - feeJettonMaster: UserFriendlyAddress; - /** Sender wallet address */ - walletAddress: UserFriendlyAddress; - /** Sender wallet public key */ - walletPublicKey: Hex; - /** Messages that the caller wants to include in the transaction */ - messages: TransactionRequestMessage[]; -} - -/** - * Result of gasless estimation. - * - * Contains relayer-wrapped messages that should be passed to `wallet.signMessage` - * in place of the caller's original messages, together with the commission the - * relayer will deduct and the timestamp after which the estimate expires. - */ -export interface GaslessEstimateResult { - /** Relayer-wrapped messages ready to be signed */ - messages: TransactionRequestMessage[]; - /** Relayer commission in fee-jetton nanounits */ - commission: bigint; - /** Unix timestamp after which the bundle becomes invalid for relay */ - validUntil: number; - /** Address of the relayer that produced this estimate */ - relayAddress: UserFriendlyAddress; - /** Sender wallet address echoed by the relayer */ - from: UserFriendlyAddress; -} - -/** - * Parameters to submit a signed gasless transaction to the relayer. - */ -export interface GaslessSendParams { - /** Sender wallet public key */ - walletPublicKey: Hex; - /** Signed internal-message BoC obtained from `wallet.signMessage` */ - internalBoc: Base64String; -} - -/** - * Result of submitting a signed gasless transaction to the relayer. - * - * Fields are provider-specific; callers should not rely on a particular shape. - */ -export type GaslessSendResult = Record; diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index 0f79ad429..963a8c7db 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -23,14 +23,6 @@ export { DefiError, DefiErrorCode } from './defi/errors'; export { SwapManager, SwapProvider, SwapError, SwapErrorCode } from './defi/swap'; export { StakingManager, StakingProvider, StakingError, StakingErrorCode } from './defi/staking'; export { GaslessManager, GaslessProvider, GaslessError } from './defi/gasless'; -export type { - GaslessConfig, - GaslessGasJetton, - GaslessEstimateParams, - GaslessEstimateResult, - GaslessSendParams, - GaslessSendResult, -} from './defi/gasless'; export { EventEmitter } from './core/EventEmitter'; export type { EventListener, EventPayload, KitEvent } from './core/EventEmitter'; export type { SharedKitEvents } from './types/emitter'; From 6f873388dcbbaface3cd85de42b369b40946bb63 Mon Sep 17 00:00:00 2001 From: VK Date: Sun, 24 May 2026 22:40:28 +0400 Subject: [PATCH 05/46] feat(gasless): fixes after merge --- apps/appkit-minter/src/pages/gasless-page.tsx | 6 +- .../src/pages/sign-message-page.tsx | 79 ++++++++++++++++++- packages/appkit/src/types/provider.ts | 13 ++- .../src/defi/gasless/GaslessProvider.ts | 9 ++- packages/walletkit/src/defi/gasless/errors.ts | 19 +++-- .../gasless/tonapi/TonApiGaslessProvider.ts | 5 ++ 6 files changed, 115 insertions(+), 16 deletions(-) diff --git a/apps/appkit-minter/src/pages/gasless-page.tsx b/apps/appkit-minter/src/pages/gasless-page.tsx index 26d33a95c..daa4a7b54 100644 --- a/apps/appkit-minter/src/pages/gasless-page.tsx +++ b/apps/appkit-minter/src/pages/gasless-page.tsx @@ -19,7 +19,7 @@ import type { Base64String } from '@ton/appkit-react'; import { parseUnits, createJettonTransferPayload, compareAddress } from '@ton/appkit'; import { toast } from 'sonner'; -import { Card, Layout } from '@/core/components'; +import { Layout } from '@/core/components'; const USDT_MASTER_MAINNET = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'; @@ -87,7 +87,7 @@ export const GaslessPage: FC = () => { return ( - +

Send USDT (Gasless)

@@ -147,7 +147,7 @@ export const GaslessPage: FC = () => { )}

)} - +
); }; diff --git a/apps/appkit-minter/src/pages/sign-message-page.tsx b/apps/appkit-minter/src/pages/sign-message-page.tsx index fef84a09a..c019853ee 100644 --- a/apps/appkit-minter/src/pages/sign-message-page.tsx +++ b/apps/appkit-minter/src/pages/sign-message-page.tsx @@ -6,15 +6,86 @@ * */ -import type React from 'react'; +import { useState } from 'react'; +import type { FC } from 'react'; +import { useSignText, useSelectedWallet } from '@ton/appkit-react'; +import { toast } from 'sonner'; import { Layout } from '@/core/components'; -import { SignMessageCard } from '@/features/signing'; -export const SignMessagePage: React.FC = () => { +export const SignMessagePage: FC = () => { + const [message, setMessage] = useState(''); + const [signature, setSignature] = useState(null); + + const [wallet] = useSelectedWallet(); + const { mutate: signText, isPending } = useSignText({ + mutation: { + onSuccess: (result) => { + setSignature(result.signature); + toast.success('Message signed successfully!'); + }, + onError: (error) => { + toast.error(`Signing failed: ${error.message}`); + }, + }, + }); + + const handleSign = () => { + if (!wallet || !message.trim()) { + toast.error('Please enter a message to sign'); + return; + } + + signText({ text: message }); + }; + + const handleCopySignature = () => { + if (signature) { + navigator.clipboard.writeText(signature); + toast.success('Signature copied to clipboard!'); + } + }; + return ( - +
+

Sign Message

+ +
+ +