diff --git a/demo/examples/src/appkit/actions/onramp/build-onramp-url.ts b/demo/examples/src/appkit/actions/onramp/build-onramp-url.ts new file mode 100644 index 000000000..063ead6e0 --- /dev/null +++ b/demo/examples/src/appkit/actions/onramp/build-onramp-url.ts @@ -0,0 +1,29 @@ +/** + * 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 { buildOnrampUrl, getOnrampQuotes } from '@ton/appkit/onramp'; + +export const buildOnrampUrlExample = async (appKit: AppKit) => { + // SAMPLE_START: BUILD_ONRAMP_URL + const quotes = await getOnrampQuotes(appKit, { + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + amount: '100', + }); + + const [quote] = quotes; + if (!quote) throw new Error('No onramp quotes available'); + + const url = await buildOnrampUrl(appKit, { + quote, + userAddress: 'UQ...wallet-address...', + }); + console.log('Onramp URL:', url); + // SAMPLE_END: BUILD_ONRAMP_URL +}; diff --git a/demo/examples/src/appkit/actions/onramp/get-onramp-quotes.ts b/demo/examples/src/appkit/actions/onramp/get-onramp-quotes.ts new file mode 100644 index 000000000..654a59d8e --- /dev/null +++ b/demo/examples/src/appkit/actions/onramp/get-onramp-quotes.ts @@ -0,0 +1,22 @@ +/** + * 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 { getOnrampQuotes } from '@ton/appkit/onramp'; + +export const getOnrampQuotesExample = async (appKit: AppKit) => { + // SAMPLE_START: GET_ONRAMP_QUOTES + const quotes = await getOnrampQuotes(appKit, { + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + amount: '100', + isFiatAmount: true, + }); + console.log('Onramp Quotes:', quotes); + // SAMPLE_END: GET_ONRAMP_QUOTES +}; diff --git a/packages/appkit-react/.storybook/app-kit.ts b/packages/appkit-react/.storybook/app-kit.ts index a875ed300..486cdd4ab 100644 --- a/packages/appkit-react/.storybook/app-kit.ts +++ b/packages/appkit-react/.storybook/app-kit.ts @@ -11,6 +11,7 @@ import { createOmnistonProvider } from '@ton/appkit/swap/omniston'; import { createDeDustProvider } from '@ton/appkit/swap/dedust'; import { createTonstakersProvider } from '@ton/appkit/staking/tonstakers'; import { createLayerswapProvider } from '@ton/appkit/crypto-onramp/layerswap'; +import { createAppkitOnrampProvider } from '@ton/appkit/onramp/appkit-onramp'; export const appKit = new AppKit({ networks: { @@ -40,5 +41,6 @@ export const appKit = new AppKit({ createDeDustProvider(), createTonstakersProvider(), createLayerswapProvider(), + createAppkitOnrampProvider({ apiKey: 'ak_test_66546d3cebb69dc4397570d65aad14dd' }), ], }); diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/index.ts new file mode 100644 index 000000000..d07d5150b --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/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 { OnrampAmountReversed } from './onramp-amount-reversed'; +export type { OnrampAmountReversedProps } from './onramp-amount-reversed'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css new file mode 100644 index 000000000..07bacc131 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.module.css @@ -0,0 +1,28 @@ +.container { + composes: bodySemibold from '../../../../styles/typography.module.css'; + + display: flex; + align-items: center; + justify-content: center; + cursor: text; + width: 100%; + overflow: hidden; + color: var(--ta-color-text-secondary); + gap: 8px; +} + +.changeDirection { + width: 14px; + height: 14px; + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; + outline: none; + color: var(--ta-color-text); + transition: opacity 0.2s ease-in-out; +} + +.changeDirection:hover { + opacity: 0.8; +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx new file mode 100644 index 000000000..25da63c55 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-amount-reversed/onramp-amount-reversed.tsx @@ -0,0 +1,18 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { AmountReversed } from '../../../../components/ui/amount-reversed'; +import type { AmountReversedProps } from '../../../../components/ui/amount-reversed'; + +export type OnrampAmountReversedProps = AmountReversedProps; + +export const OnrampAmountReversed: FC = ({ decimals, ...props }) => ( + +); diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-item/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/index.ts new file mode 100644 index 000000000..fe67367b2 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/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 { OnrampCurrencyItem } from './onramp-currency-item'; +export type { OnrampCurrencyItemProps } from './onramp-currency-item'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx new file mode 100644 index 000000000..be0e40347 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-item/onramp-currency-item.tsx @@ -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 { ComponentProps, FC } from 'react'; + +import { CurrencyItem } from '../../../../components/shared/currency-item'; +import type { OnrampCurrency } from '../../types'; + +export interface OnrampCurrencyItemProps extends ComponentProps { + currency: OnrampCurrency; +} + +export const OnrampCurrencyItem: FC = ({ currency, ...props }) => { + return ( + + + + + {currency.name} + + + {currency.code} + + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/index.ts new file mode 100644 index 000000000..58d97ad04 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/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 { OnrampCurrencySelectModal } from './onramp-currency-select-modal'; +export type { OnrampCurrencySelectModalProps } from './onramp-currency-select-modal'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx new file mode 100644 index 000000000..4d9d69913 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/onramp-currency-select-modal.tsx @@ -0,0 +1,86 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo, useState } from 'react'; +import type { FC } from 'react'; + +import { CurrencySelect } from '../../../../components/shared/currency-select-modal'; +import type { OnrampCurrency, CurrencySectionConfig } from '../../types'; +import { OnrampCurrencyItem } from '../onramp-currency-item'; +import { useI18n } from '../../../settings/hooks/use-i18n'; +import { filterCurrencies, groupCurrencySections } from './utils'; +import type { CurrencySection } from './utils'; + +export interface OnrampCurrencySelectModalProps { + open: boolean; + onClose: () => void; + currencies: OnrampCurrency[]; + currencySections?: CurrencySectionConfig[]; + onSelect: (currency: OnrampCurrency) => void; +} + +export const OnrampCurrencySelectModal: FC = ({ + open, + onClose, + currencies, + currencySections, + onSelect, +}) => { + const { t } = useI18n(); + const [search, setSearch] = useState(''); + + const displaySections = useMemo((): CurrencySection[] => { + if (search) { + return [{ title: '', currencies: filterCurrencies(currencies, search) }]; + } + if (currencySections) { + return groupCurrencySections(currencies, currencySections, t('tokenSelect.otherCurrencies')); + } + return [{ title: '', currencies }]; + }, [currencies, currencySections, search, t]); + + const isEmpty = displaySections.every((s) => s.currencies.length === 0); + + const handleSelect = (currency: OnrampCurrency) => () => { + onSelect(currency); + onClose(); + setSearch(''); + }; + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + onClose(); + setSearch(''); + } + }; + + return ( + + + + + {displaySections.map((section) => ( + + {section.title && {section.title}} + {section.currencies.map((currency) => ( + + ))} + + ))} + + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/utils.ts b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/utils.ts new file mode 100644 index 000000000..66057a46a --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-currency-select-modal/utils.ts @@ -0,0 +1,48 @@ +/** + * 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 { OnrampCurrency, CurrencySectionConfig } from '../../types'; + +export interface CurrencySection { + title: string; + currencies: OnrampCurrency[]; +} + +export const filterCurrencies = (currencies: OnrampCurrency[], search: string): OnrampCurrency[] => { + if (!search) return currencies; + const lower = search.toLowerCase(); + return currencies.filter((c) => c.name.toLowerCase().includes(lower) || c.code.toLowerCase().includes(lower)); +}; + +export const groupCurrencySections = ( + currencies: OnrampCurrency[], + sections: CurrencySectionConfig[], + otherTitle: string, +): CurrencySection[] => { + const currencyById = new Map(currencies.map((c) => [c.id, c])); + const assignedIds = new Set(); + + const result: CurrencySection[] = sections.map(({ title, ids }) => ({ + title, + currencies: ids.flatMap((id) => { + const c = currencyById.get(id); + if (c) { + assignedIds.add(id); + return [c]; + } + return []; + }), + })); + + const remaining = currencies.filter((c) => !assignedIds.has(c.id)); + if (remaining.length > 0) { + result.push({ title: otherTitle, currencies: remaining }); + } + + return result.filter((s) => s.currencies.length > 0); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-item/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/index.ts new file mode 100644 index 000000000..0213d7109 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/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 { OnrampProviderItem } from './onramp-provider-item'; +export type { OnrampProviderItemProps } from './onramp-provider-item'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css new file mode 100644 index 000000000..815d8f0bd --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.module.css @@ -0,0 +1,25 @@ +.container { + display: flex; + text-align: left; + align-items: center; + padding: 16px; + gap: 12px; + border: var(--ta-border-width-m) solid var(--ta-color-background-tertiary); + border-radius: var(--ta-border-radius-xl); + cursor: pointer; + background: transparent; +} + +.icon { + border-radius: var(--ta-border-radius-s); +} + +.name { + composes: bodySemibold from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); +} + +.methods { + composes: labelRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx new file mode 100644 index 000000000..6de9a4f49 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-item/onramp-provider-item.tsx @@ -0,0 +1,36 @@ +/** + * 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 { ComponentProps, FC } from 'react'; +import clsx from 'clsx'; + +import type { OnrampProvider } from '../../types'; +import { Logo } from '../../../../components/ui/logo'; +import styles from './onramp-provider-item.module.css'; + +export interface OnrampProviderItemProps extends ComponentProps<'button'> { + provider: OnrampProvider; +} + +export const OnrampProviderItem: FC = ({ provider, className, ...props }) => { + return ( + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/index.ts b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/index.ts new file mode 100644 index 000000000..2853976e1 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/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 { OnrampProviderSelect } from './onramp-provider-select'; +export type { OnrampProviderSelectProps } from './onramp-provider-select'; diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css new file mode 100644 index 000000000..bcac7f253 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.module.css @@ -0,0 +1,7 @@ +.list { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} diff --git a/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx new file mode 100644 index 000000000..7af5d8f72 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/components/onramp-provider-select/onramp-provider-select.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { Modal } from '../../../../components/ui/modal'; +import type { OnrampProvider } from '../../types'; +import styles from './onramp-provider-select.module.css'; +import { OnrampProviderItem } from '../onramp-provider-item'; +import { useI18n } from '../../../settings/hooks/use-i18n'; + +export interface OnrampProviderSelectProps { + open: boolean; + onClose: () => void; + providers: OnrampProvider[]; + onSelect: (provider: OnrampProvider) => void; +} + +export const OnrampProviderSelect: FC = ({ open, onClose, providers, onSelect }) => { + const { t } = useI18n(); + + const handleSelect = (provider: OnrampProvider) => () => { + onSelect(provider); + onClose(); + }; + + return ( + !isOpen && onClose()} title={t('onramp.checkout')}> +
+ {providers.map((provider) => ( + + ))} +
+
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/hooks/use-build-onramp-url.ts b/packages/appkit-react/src/features/onramp/hooks/use-build-onramp-url.ts new file mode 100644 index 000000000..9d45f6ec3 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/hooks/use-build-onramp-url.ts @@ -0,0 +1,44 @@ +/** + * 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 { UseMutationResult } from '@tanstack/react-query'; +import { buildOnrampUrlMutationOptions } from '@ton/appkit/queries'; +import type { + BuildOnrampUrlData, + BuildOnrampUrlErrorType, + BuildOnrampUrlMutationOptions, + BuildOnrampUrlVariables, +} from '@ton/appkit/queries'; + +import { useAppKit } from '../../settings'; +import { useMutation } from '../../../libs/query'; + +export type UseBuildOnrampUrlParameters = BuildOnrampUrlMutationOptions; + +export type UseBuildOnrampUrlReturnType = UseMutationResult< + BuildOnrampUrlData, + BuildOnrampUrlErrorType, + BuildOnrampUrlVariables, + context +>; + +/** + * Hook to build onramp URL + */ +export const useBuildOnrampUrl = ( + parameters: UseBuildOnrampUrlParameters = {}, +): UseBuildOnrampUrlReturnType => { + const appKit = useAppKit(); + + return useMutation({ + ...buildOnrampUrlMutationOptions(appKit), + ...parameters.mutation, + }); +}; diff --git a/packages/appkit-react/src/features/onramp/hooks/use-onramp-provider.ts b/packages/appkit-react/src/features/onramp/hooks/use-onramp-provider.ts new file mode 100644 index 000000000..4cd81af39 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/hooks/use-onramp-provider.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 { useSyncExternalStore, useCallback } from 'react'; +import { getOnrampProvider, watchOnrampProviders } from '@ton/appkit/onramp'; +import type { GetOnrampProviderOptions, GetOnrampProviderReturnType } from '@ton/appkit/onramp'; + +import { useAppKit } from '../../settings/hooks/use-app-kit'; + +export type UseOnrampProviderReturnType = GetOnrampProviderReturnType; + +/** + * Hook to get onramp provider + */ +export const useOnrampProvider = (options: GetOnrampProviderOptions = {}): UseOnrampProviderReturnType | undefined => { + const appKit = useAppKit(); + const { id } = options; + + const subscribe = useCallback( + (onChange: () => void) => { + return watchOnrampProviders(appKit, { onChange }); + }, + [appKit], + ); + + const getSnapshot = useCallback(() => { + try { + return getOnrampProvider(appKit, { id }); + } catch { + return undefined; + } + }, [appKit, id]); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +}; diff --git a/packages/appkit-react/src/features/onramp/hooks/use-onramp-providers.ts b/packages/appkit-react/src/features/onramp/hooks/use-onramp-providers.ts new file mode 100644 index 000000000..712ccd308 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/hooks/use-onramp-providers.ts @@ -0,0 +1,35 @@ +/** + * 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 { useSyncExternalStore, useCallback } from 'react'; +import { getOnrampProviders, watchOnrampProviders } from '@ton/appkit/onramp'; +import type { GetOnrampProvidersReturnType } from '@ton/appkit/onramp'; + +import { useAppKit } from '../../settings/hooks/use-app-kit'; + +export type UseOnrampProvidersReturnType = GetOnrampProvidersReturnType; + +/** + * Hook to get all registered onramp providers + */ +export const useOnrampProviders = (): UseOnrampProvidersReturnType => { + const appKit = useAppKit(); + + const subscribe = useCallback( + (onChange: () => void) => { + return watchOnrampProviders(appKit, { onChange }); + }, + [appKit], + ); + + const getSnapshot = useCallback(() => { + return getOnrampProviders(appKit); + }, [appKit]); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +}; diff --git a/packages/appkit-react/src/features/onramp/hooks/use-onramp-quotes.ts b/packages/appkit-react/src/features/onramp/hooks/use-onramp-quotes.ts new file mode 100644 index 000000000..55d414c07 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/hooks/use-onramp-quotes.ts @@ -0,0 +1,34 @@ +/** + * 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 { getOnrampQuotesQueryOptions } from '@ton/appkit/queries'; +import type { GetOnrampQuotesData, GetOnrampQuotesErrorType, GetOnrampQuotesQueryConfig } from '@ton/appkit/queries'; + +import { useAppKit } from '../../settings'; +import { useQuery } from '../../../libs/query'; +import type { UseQueryReturnType } from '../../../libs/query'; + +export type UseOnrampQuotesParameters = GetOnrampQuotesQueryConfig; + +export type UseOnrampQuotesReturnType = UseQueryReturnType< + selectData, + GetOnrampQuotesErrorType +>; + +/** + * Hook to get onramp quotes from all registered providers (results are flattened). + */ +export const useOnrampQuotes = ( + parameters: UseOnrampQuotesParameters = {}, +): UseOnrampQuotesReturnType => { + const appKit = useAppKit(); + + return useQuery(getOnrampQuotesQueryOptions(appKit, parameters)); +}; diff --git a/packages/appkit-react/src/features/onramp/index.ts b/packages/appkit-react/src/features/onramp/index.ts index 4f4545def..17993bf2b 100644 --- a/packages/appkit-react/src/features/onramp/index.ts +++ b/packages/appkit-react/src/features/onramp/index.ts @@ -6,9 +6,12 @@ * */ +// export * from './widgets/fiat-onramp/onramp-widget'; // fiat-onramp: not ready + export * from './widgets/crypto-onramp/crypto-onramp-widget'; export * from './widgets/crypto-onramp/crypto-onramp-widget-ui'; export * from './widgets/crypto-onramp/crypto-onramp-widget-provider'; +export * from './widgets/ton-pay-widget'; export { useCryptoOnrampProvider, type UseCryptoOnrampProviderReturnType } from './hooks/use-crypto-onramp-provider'; export { diff --git a/packages/appkit-react/src/features/onramp/mock-data/currencies.ts b/packages/appkit-react/src/features/onramp/mock-data/currencies.ts new file mode 100644 index 000000000..edb84bebf --- /dev/null +++ b/packages/appkit-react/src/features/onramp/mock-data/currencies.ts @@ -0,0 +1,33 @@ +/** + * 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 { OnrampCurrency } from '../types'; + +export const ONRAMP_CURRENCIES: OnrampCurrency[] = [ + { + id: 'eur', + code: 'EUR', + name: 'Euro', + symbol: '\u20AC', + logo: 'https://static.moonpay.com/widget/currencies/eur.svg', + }, + { + id: 'usd', + code: 'USD', + name: 'US Dollar', + symbol: '$', + logo: 'https://static.moonpay.com/widget/currencies/usd.svg', + }, + { + id: 'gbp', + code: 'GBP', + name: 'Pound Sterling', + symbol: '\u00A3', + logo: 'https://static.moonpay.com/widget/currencies/gbp.svg', + }, +]; diff --git a/packages/appkit-react/src/features/onramp/mock-data/providers.ts b/packages/appkit-react/src/features/onramp/mock-data/providers.ts new file mode 100644 index 000000000..41c037297 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/mock-data/providers.ts @@ -0,0 +1,36 @@ +/** + * 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 { OnrampProvider } from '../types'; + +export const ONRAMP_PROVIDERS: OnrampProvider[] = [ + { + id: 'moonpay', + name: 'MoonPay', + description: 'SEPA, PayPal, Debit Card and other options', + logo: 'https://images-serviceprovider.meld.io/MOONPAY/short_logo_light.png', + }, + { + id: 'transak', + name: 'Transak', + description: 'Debit Card, Apple Pay, Google Pay, SEPA', + logo: 'https://cdn.meld.io/images-serviceprovider/TRANSAK/short_logo_light.png', + }, + { + id: 'binance', + name: 'Binance', + description: 'Debit Card, Apple Pay, Binance Cash Balance and other options', + logo: 'https://cdn.meld.io/images-serviceprovider/BINANCECONNECT/short_logo_light.png', + }, + { + id: 'mercuryo', + name: 'Mercuryo', + description: 'Debit Card, Apple Pay, Google Pay, SEPA', + logo: 'https://static.mercuryo.io/logo/mercuryo-logo.svg', + }, +]; diff --git a/packages/appkit-react/src/features/onramp/types.ts b/packages/appkit-react/src/features/onramp/types.ts index 9ecac4d74..b43bb48d7 100644 --- a/packages/appkit-react/src/features/onramp/types.ts +++ b/packages/appkit-react/src/features/onramp/types.ts @@ -6,7 +6,76 @@ * */ +export interface OnrampCurrency { + id: string; + code: string; + name: string; + symbol?: string; + logo?: string; +} + +export interface OnrampProvider { + id: string; + name: string; + description?: string; + logo?: string; +} + +export interface CurrencySectionConfig { + title: string; + ids: string[]; +} + +export type AmountInputMode = 'token' | 'currency'; + export interface OnrampAmountPreset { amount: string; label: string; } + +export interface CryptoPaymentMethod { + id: string; + /** Token symbol, e.g. "USDC", "USDT" */ + symbol: string; + /** Token name, e.g. "USD Coin", "Tether" */ + name: string; + /** Human-readable network name, e.g. "Base", "BSC" */ + network: string; + /** Source chain id as string (decimal), e.g. "8453", "56" — passed as srcChainId to the onramp provider */ + networkId: string; + /** Number of decimals for the token */ + decimals: number; + /** Token contract address on the source network (empty string / zero address for native) */ + address: string; + logo?: string; + networkLogo?: string; +} + +export interface PaymentMethodSectionConfig { + title: string; + ids: string[]; +} + +/** + * Target token (what the user is buying on TON) in the crypto onramp widget. + * Kept separate from AppkitUIToken because `address` is the raw form expected + * by the onramp provider (e.g. "0x0000000000000000000000000000000000000000" + * for native TON, "EQCx..." for USDT jetton master). + */ +export interface CryptoOnrampToken { + id: string; + /** Token symbol, e.g. "TON", "USDT" */ + symbol: string; + /** Full token name, e.g. "Toncoin", "Tether" */ + name: string; + /** Number of decimals for the token */ + decimals: number; + /** Address as the onramp provider expects it */ + address: string; + logo?: string; +} + +export interface CryptoOnrampTokenSectionConfig { + title: string; + ids: string[]; +} diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/index.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/index.ts new file mode 100644 index 000000000..daafb19b9 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/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 { OnrampInfoBlock } from './onramp-info-block'; +export type { OnrampInfoBlockProps } from './onramp-info-block'; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/onramp-info-block.stories.tsx b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/onramp-info-block.stories.tsx new file mode 100644 index 000000000..558eee074 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/onramp-info-block.stories.tsx @@ -0,0 +1,54 @@ +/** + * 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 { Meta, StoryObj } from '@storybook/react-vite'; +import { Network } from '@ton/appkit'; + +import { OnrampInfoBlock } from './onramp-info-block'; +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; + +const TON_TOKEN: AppkitUIToken = { + id: 'ton', + symbol: 'TON', + name: 'Toncoin', + decimals: 9, + address: 'ton', + logo: 'https://asset.ston.fi/img/EQDQoc5M3Bh8eWFephi9bClhevelbZZvWhkqdo80XuY_0qXv', + network: Network.mainnet(), +}; + +const meta: Meta = { + title: 'Public/Features/Onramp/Internal/OnrampInfoBlock', + component: OnrampInfoBlock, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + selectedToken: TON_TOKEN, + selectedQuote: { + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + fiatAmount: '100', + cryptoAmount: '50.123456', + rate: '0.50123456', + providerId: 'appkit-onramp', + }, + isLoading: false, + }, +}; + +export const Loading: Story = { + args: { + selectedToken: TON_TOKEN, + selectedQuote: undefined, + isLoading: true, + }, +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/onramp-info-block.tsx b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/onramp-info-block.tsx new file mode 100644 index 000000000..52e428e71 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/onramp-info-block.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; +import type { OnrampQuote } from '@ton/appkit/onramp'; + +import { InfoBlock } from '../../../../../components/ui/info-block'; +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; +import { useConnectedWallets } from '../../../../wallets/hooks/use-connected-wallets'; +import { useI18n } from '../../../../settings/hooks/use-i18n'; +import { formatOnrampAmount } from '../../crypto-onramp/utils/format-onramp-amount'; +import { useOnrampBalance } from './use-onramp-balance'; + +export interface OnrampInfoBlockProps { + selectedToken: AppkitUIToken | null; + selectedQuote?: OnrampQuote; + isLoading: boolean; + className?: string; +} + +export const OnrampInfoBlock: FC = ({ selectedToken, selectedQuote, isLoading, className }) => { + const { t } = useI18n(); + + const wallets = useConnectedWallets(); + const activeWallet = wallets?.[0]; + const isWalletConnected = !!activeWallet; + const { targetBalance, isLoadingTargetBalance } = useOnrampBalance({ + selectedToken, + userAddress: activeWallet?.getAddress(), + }); + + return ( + + + {t('onramp.youGet')} + + {isLoading ? ( + + ) : ( + + {formatOnrampAmount(selectedQuote?.cryptoAmount, selectedToken?.decimals)}{' '} + {selectedToken?.symbol} + + )} + + + {isWalletConnected && ( + + {t('onramp.yourBalance')} + + {isLoadingTargetBalance ? ( + + ) : ( + + {formatOnrampAmount(targetBalance || '0', selectedToken?.decimals)} {selectedToken?.symbol} + + )} + + )} + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/use-onramp-balance.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/use-onramp-balance.ts new file mode 100644 index 000000000..75f5fb243 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-info-block/use-onramp-balance.ts @@ -0,0 +1,38 @@ +/** + * 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 { useBalance } from '../../../../balances/hooks/use-balance'; +import { useJettonBalanceByAddress } from '../../../../jettons/hooks/use-jetton-balance-by-address'; +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; + +const NATIVE_TON_TOKEN_ADDRESS = 'ton'; + +interface UseOnrampBalanceOptions { + selectedToken: AppkitUIToken | null; + userAddress: string | undefined; +} + +export const useOnrampBalance = ({ selectedToken, userAddress }: UseOnrampBalanceOptions) => { + const isNativeTonTarget = selectedToken?.address === NATIVE_TON_TOKEN_ADDRESS; + + const { data: nativeBalanceData, isLoading: isNativeBalanceLoading } = useBalance({ + query: { enabled: isNativeTonTarget && !!userAddress, refetchInterval: 5000 }, + }); + + const { data: jettonBalanceData, isLoading: isJettonBalanceLoading } = useJettonBalanceByAddress({ + jettonAddress: !isNativeTonTarget ? selectedToken?.address : undefined, + ownerAddress: userAddress, + jettonDecimals: selectedToken?.decimals, + query: { enabled: !isNativeTonTarget && !!selectedToken?.address && !!userAddress, refetchInterval: 5000 }, + }); + + return { + targetBalance: (isNativeTonTarget ? nativeBalanceData : jettonBalanceData) ?? '', + isLoadingTargetBalance: isNativeTonTarget ? isNativeBalanceLoading : isJettonBalanceLoading, + }; +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/index.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/index.ts new file mode 100644 index 000000000..5fc591d3d --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/index.ts @@ -0,0 +1,12 @@ +/** + * 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 { OnrampWidgetProvider } from './onramp-widget-provider'; +export type { OnrampProviderProps } from './onramp-widget-provider'; +export { OnrampContext, useOnrampContext } from './onramp-context'; +export type { OnrampContextType } from './onramp-context'; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/onramp-context.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/onramp-context.ts new file mode 100644 index 000000000..6fe8e668f --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/onramp-context.ts @@ -0,0 +1,106 @@ +/** + * 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 { createContext, useContext } from 'react'; +import type { OnrampQuote } from '@ton/appkit/onramp'; + +import type { TokenSectionConfig } from '../../../../../components/shared/token-select-modal'; +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; +import { ONRAMP_CURRENCIES } from '../../../mock-data/currencies'; +import { DEFAULT_ONRAMP_PRESETS } from '../../../constants'; +import type { + OnrampCurrency, + OnrampProvider as OnrampWidgetProviderType, + AmountInputMode, + OnrampAmountPreset, + CurrencySectionConfig, +} from '../../../types'; + +export interface OnrampContextType { + /** Full list of available tokens to buy */ + tokens: AppkitUIToken[]; + /** Optional section configs for grouping tokens in the selector */ + tokenSections?: TokenSectionConfig[]; + /** Currently selected token to buy */ + selectedToken: AppkitUIToken | null; + setSelectedToken: (token: AppkitUIToken) => void; + + /** Available fiat currencies */ + currencies: OnrampCurrency[]; + /** Optional section configs for grouping currencies */ + currencySections?: CurrencySectionConfig[]; + /** Currently selected fiat currency */ + selectedCurrency: OnrampCurrency; + setSelectedCurrency: (currency: OnrampCurrency) => void; + + /** Current amount input value */ + amount: string; + setAmount: (value: string) => void; + /** Whether user is entering token amount or fiat amount */ + amountInputMode: AmountInputMode; + setAmountInputMode: (mode: AmountInputMode) => void; + /** Converted amount in the opposite denomination (from the selected quote) */ + convertedAmount: string; + /** Preset amount values */ + presetAmounts: OnrampAmountPreset[]; + + /** Available payment providers (derived from received quotes) */ + providers: OnrampWidgetProviderType[]; + /** Currently selected payment provider */ + selectedProvider: OnrampWidgetProviderType | null; + setSelectedProvider: (provider: OnrampWidgetProviderType) => void; + /** Quote tied to the currently selected provider */ + selectedQuote?: OnrampQuote; + /** Whether registered providers support reversed (crypto-amount) quotes */ + isReversedAmountSupported: boolean; + + /** Validation/fetch error i18n key, null when everything is ok */ + error: string | null; + /** Whether quotes are being fetched */ + isLoadingQuote: boolean; + /** Whether the user can proceed (valid amount + quote available + provider selected) */ + canSubmit: boolean; + + /** Reset widget to initial state */ + onReset: () => void; + /** Execute the onramp (build URL and redirect) */ + onContinue: () => void; +} + +const defaultContext: OnrampContextType = { + tokens: [], + tokenSections: undefined, + selectedToken: null, + setSelectedToken: () => {}, + currencies: [], + currencySections: undefined, + selectedCurrency: ONRAMP_CURRENCIES[0]!, + setSelectedCurrency: () => {}, + amount: '', + setAmount: () => {}, + amountInputMode: 'currency', + setAmountInputMode: () => {}, + convertedAmount: '', + presetAmounts: DEFAULT_ONRAMP_PRESETS, + providers: [], + selectedProvider: null, + setSelectedProvider: () => {}, + selectedQuote: undefined, + isReversedAmountSupported: false, + error: null, + isLoadingQuote: false, + canSubmit: false, + onReset: () => {}, + onContinue: () => {}, +}; + +export const OnrampContext = createContext(defaultContext); + +export const useOnrampContext = (): OnrampContextType => { + return useContext(OnrampContext); +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/onramp-widget-provider.tsx b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/onramp-widget-provider.tsx new file mode 100644 index 000000000..69d099e28 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/onramp-widget-provider.tsx @@ -0,0 +1,165 @@ +/** + * 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 { useCallback, useEffect, useMemo } from 'react'; +import type { FC, PropsWithChildren } from 'react'; + +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; +import type { TokenSectionConfig } from '../../../../../components/shared/token-select-modal'; +import type { CurrencySectionConfig } from '../../../types'; +import { ONRAMP_CURRENCIES } from '../../../mock-data/currencies'; +import { DEFAULT_ONRAMP_PRESETS } from '../../../constants'; +import { useBuildOnrampUrl } from '../../../hooks/use-build-onramp-url'; +import { useAddress } from '../../../../wallets'; +import { OnrampContext } from './onramp-context'; +import { useOnrampTokenState } from './use-onramp-token-state'; +import { useOnrampQuote } from './use-onramp-quote'; +import { useOnrampValidation } from './use-onramp-validation'; + +export interface OnrampProviderProps extends PropsWithChildren { + /** Full list of tokens available for purchase */ + tokens: AppkitUIToken[]; + /** Optional section configs for grouping tokens in the selector */ + tokenSections?: TokenSectionConfig[]; + /** Optional section configs for grouping currencies in the selector */ + currencySections?: CurrencySectionConfig[]; + /** Id of the token pre-selected for purchase */ + defaultTokenId?: string; + /** Id of the fiat currency pre-selected */ + defaultCurrencyId?: string; +} + +export const OnrampWidgetProvider: FC = ({ + children, + tokens, + tokenSections, + currencySections, + defaultTokenId, + defaultCurrencyId, +}) => { + // 1. Local state + const { + selectedToken, + setSelectedToken, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + setAmountInputMode, + amountDecimals, + } = useOnrampTokenState({ tokens, defaultTokenId, defaultCurrencyId }); + + // 2. Queries and external readers + const userAddress = useAddress(); + + const { + amountDebounced, + quoteError, + isQuoteFetching, + providers, + selectedProvider, + setSelectedProvider, + selectedQuote, + convertedAmount, + isReversedAmountSupported, + } = useOnrampQuote({ selectedToken, selectedCurrency, amount, amountInputMode }); + + // 3. Derivations + const { error, canSubmit } = useOnrampValidation({ + amount, + amountDebounced, + amountDecimals, + quoteError, + hasQuote: !!selectedQuote, + hasSelectedProvider: !!selectedProvider, + }); + + const isLoadingQuote = isQuoteFetching || amount !== amountDebounced; + + useEffect(() => { + if (!isReversedAmountSupported && amountInputMode === 'token') { + setAmountInputMode('currency'); + } + }, [isReversedAmountSupported, amountInputMode, setAmountInputMode]); + + // 4. Mutations + const { mutateAsync: buildUrl } = useBuildOnrampUrl(); + + // 5. Callbacks + const onContinue = useCallback(async () => { + if (!canSubmit || !selectedQuote || !userAddress) return; + + try { + const url = await buildUrl({ quote: selectedQuote, userAddress }); + window.open(url, '_blank'); + } catch { + // silently swallow — redirect is best-effort + } + }, [canSubmit, selectedQuote, userAddress, buildUrl]); + + const onReset = useCallback(() => { + setAmount(''); + setAmountInputMode('currency'); + }, [setAmount, setAmountInputMode]); + + const value = useMemo( + () => ({ + tokens, + tokenSections, + selectedToken, + setSelectedToken, + currencies: ONRAMP_CURRENCIES, + currencySections, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + setAmountInputMode, + convertedAmount, + presetAmounts: DEFAULT_ONRAMP_PRESETS, + providers, + selectedProvider, + setSelectedProvider, + selectedQuote, + isReversedAmountSupported, + error, + isLoadingQuote, + canSubmit, + onReset, + onContinue, + }), + [ + tokens, + tokenSections, + selectedToken, + setSelectedToken, + currencySections, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + setAmountInputMode, + convertedAmount, + providers, + selectedProvider, + setSelectedProvider, + selectedQuote, + isReversedAmountSupported, + error, + isLoadingQuote, + canSubmit, + onReset, + onContinue, + ], + ); + + return {children}; +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-quote.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-quote.ts new file mode 100644 index 000000000..40d832b84 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-quote.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useEffect, useMemo, useState } from 'react'; +import { keepPreviousData } from '@tanstack/react-query'; + +import { useOnrampQuotes } from '../../../hooks/use-onramp-quotes'; +import { useOnrampProviders } from '../../../hooks/use-onramp-providers'; +import { useDebounceValue } from '../../../../../hooks/use-debounce-value'; +import type { AmountInputMode, OnrampCurrency, OnrampProvider as OnrampWidgetProviderType } from '../../../types'; +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; + +const QUOTE_DEBOUNCE_MS = 500; + +interface UseOnrampQuoteOptions { + selectedToken: AppkitUIToken | null; + selectedCurrency: OnrampCurrency; + amount: string; + amountInputMode: AmountInputMode; +} + +export const useOnrampQuote = ({ selectedToken, selectedCurrency, amount, amountInputMode }: UseOnrampQuoteOptions) => { + const [selectedProvider, setSelectedProvider] = useState(null); + const [amountDebounced] = useDebounceValue(amount, QUOTE_DEBOUNCE_MS); + + const registeredProviders = useOnrampProviders(); + const isReversedAmountSupported = useMemo( + () => + registeredProviders.length > 0 && + registeredProviders.every((p) => p.getMetadata().isReversedAmountSupported ?? true), + [registeredProviders], + ); + + const { + data: quotes, + isFetching: isQuoteFetching, + error: quoteError, + } = useOnrampQuotes({ + fiatCurrency: selectedCurrency.code, + cryptoCurrency: selectedToken?.symbol ?? 'TON', + amount: amountDebounced || '0', + isFiatAmount: amountInputMode === 'currency', + query: { + enabled: !!amountDebounced && !isNaN(parseFloat(amountDebounced)) && parseFloat(amountDebounced) > 0, + retry: false, + placeholderData: keepPreviousData, + refetchOnWindowFocus: false, + }, + }); + + const providers = useMemo( + () => + quotes?.map((q) => ({ + id: q.serviceInfo?.id ?? q.providerId, + name: q.serviceInfo?.name ?? q.providerId, + description: q.serviceInfo?.paymentMethods?.join(', ') ?? '', + logo: q.serviceInfo?.lightLogo ?? '', + })) ?? [], + [quotes], + ); + + const selectedQuote = useMemo(() => { + if (!quotes || quotes.length === 0) return undefined; + if (selectedProvider) { + const match = quotes.find((q) => (q.serviceInfo?.id ?? q.providerId) === selectedProvider.id); + if (match) return match; + } + return quotes[0]; + }, [quotes, selectedProvider]); + + const convertedAmount = useMemo(() => { + if (!selectedQuote) return ''; + return amountInputMode === 'currency' ? selectedQuote.cryptoAmount : selectedQuote.fiatAmount; + }, [selectedQuote, amountInputMode]); + + useEffect(() => { + if (selectedProvider && providers.find((p) => p.id === selectedProvider.id)) return; + setSelectedProvider(providers[0] ?? null); + }, [providers, selectedProvider]); + + return { + amountDebounced, + quotes: quotes ?? null, + quoteError, + isQuoteFetching, + providers, + selectedProvider, + setSelectedProvider, + selectedQuote, + convertedAmount, + isReversedAmountSupported, + }; +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-token-state.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-token-state.ts new file mode 100644 index 000000000..f21deb28a --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-token-state.ts @@ -0,0 +1,56 @@ +/** + * 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 { useCallback, useState } from 'react'; +import { validateNumericString } from '@ton/appkit'; + +import { ONRAMP_CURRENCIES } from '../../../mock-data/currencies'; +import type { AmountInputMode, OnrampCurrency } from '../../../types'; +import type { AppkitUIToken } from '../../../../../types/appkit-ui-token'; + +interface UseOnrampTokenStateOptions { + tokens: AppkitUIToken[]; + defaultTokenId?: string; + defaultCurrencyId?: string; +} + +const FIAT_DECIMALS = 2; + +const pickToken = (tokens: AppkitUIToken[], defaultId?: string): AppkitUIToken | null => + tokens.find((t) => t.id === defaultId) ?? tokens[0] ?? null; + +const pickCurrency = (defaultId?: string): OnrampCurrency => + ONRAMP_CURRENCIES.find((c) => c.id === defaultId) ?? ONRAMP_CURRENCIES[0]!; + +export const useOnrampTokenState = ({ tokens, defaultTokenId, defaultCurrencyId }: UseOnrampTokenStateOptions) => { + const [selectedToken, setSelectedToken] = useState(() => pickToken(tokens, defaultTokenId)); + const [selectedCurrency, setSelectedCurrency] = useState(() => pickCurrency(defaultCurrencyId)); + const [amount, setAmountRaw] = useState(''); + const [amountInputMode, setAmountInputMode] = useState('currency'); + + const amountDecimals = amountInputMode === 'token' ? (selectedToken?.decimals ?? 0) : FIAT_DECIMALS; + + const setAmount = useCallback( + (value: string) => { + if (value === '' || validateNumericString(value, amountDecimals)) setAmountRaw(value); + }, + [amountDecimals], + ); + + return { + selectedToken, + setSelectedToken, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + setAmountInputMode, + amountDecimals, + }; +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-validation.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-validation.ts new file mode 100644 index 000000000..023d46810 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-provider/use-onramp-validation.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo } from 'react'; + +import { hasTooManyDecimals } from '../../../../../utils/validate-amount'; +import { mapOnrampError } from '../utils/map-onramp-error'; + +interface UseOnrampValidationOptions { + amount: string; + amountDebounced: string; + amountDecimals?: number; + quoteError: Error | null; + hasQuote: boolean; + hasSelectedProvider: boolean; +} + +interface UseOnrampValidationResult { + error: string | null; + canSubmit: boolean; +} + +export const useOnrampValidation = ({ + amount, + amountDebounced, + amountDecimals, + quoteError, + hasQuote, + hasSelectedProvider, +}: UseOnrampValidationOptions): UseOnrampValidationResult => { + const tooManyDecimals = hasTooManyDecimals(amount, amountDecimals); + + const mappedError = useMemo(() => { + if (tooManyDecimals) return 'onramp.tooManyDecimals'; + if (quoteError && amountDebounced) return mapOnrampError(quoteError); + return null; + }, [tooManyDecimals, quoteError, amountDebounced]); + + const canSubmit = + (parseFloat(amount) || 0) > 0 && !tooManyDecimals && mappedError === null && hasQuote && hasSelectedProvider; + + return { error: mappedError, canSubmit }; +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/index.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/index.ts new file mode 100644 index 000000000..dd9854310 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/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 { OnrampWidgetUI } from './onramp-widget-ui'; +export type { OnrampWidgetRenderProps } from './onramp-widget-ui'; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/onramp-widget-ui.module.css b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/onramp-widget-ui.module.css new file mode 100644 index 000000000..1d5dcc782 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/onramp-widget-ui.module.css @@ -0,0 +1,28 @@ +.widget { + box-sizing: border-box; + width: 100%; + max-width: 370px; + display: flex; + flex-direction: column; +} + +.selectors { + margin-bottom: 24px; +} + +.inputSection { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 28px 0; + margin-bottom: 24px; +} + +.presets { + margin-bottom: 16px; +} + +.info { + margin-top: 16px; +} diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/onramp-widget-ui.tsx b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/onramp-widget-ui.tsx new file mode 100644 index 000000000..a0fb15ac6 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget-ui/onramp-widget-ui.tsx @@ -0,0 +1,161 @@ +/** + * 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 { useCallback, useState } from 'react'; +import type { FC } from 'react'; + +import { Button } from '../../../../../components/ui/button'; +import type { OnrampContextType } from '../onramp-widget-provider'; +import { OnrampTokenSelectors } from '../../../components/onramp-token-selectors'; +import { CenteredAmountInput } from '../../../../../components/ui/centered-amount-input'; +import { AmountPresets } from '../../../../../components/shared/amount-presets'; +import { TokenSelectModal } from '../../../../../components/shared/token-select-modal'; +import { OnrampCurrencySelectModal } from '../../../components/onramp-currency-select-modal'; +import { OnrampProviderSelect } from '../../../components/onramp-provider-select'; +import { OnrampInfoBlock } from '../onramp-info-block'; +import styles from './onramp-widget-ui.module.css'; +import { OnrampAmountReversed } from '../../../components/onramp-amount-reversed'; +import type { OnrampProvider } from '../../../types'; +import { useI18n } from '../../../../settings/hooks/use-i18n'; + +export type OnrampWidgetRenderProps = OnrampContextType; + +export const OnrampWidgetUI: FC = ({ + tokens, + tokenSections, + selectedToken, + setSelectedToken, + currencies, + currencySections, + selectedCurrency, + setSelectedCurrency, + amount, + setAmount, + amountInputMode, + setAmountInputMode, + convertedAmount, + presetAmounts, + providers, + selectedQuote, + isReversedAmountSupported, + canSubmit, + error, + isLoadingQuote, + onContinue, + setSelectedProvider, +}) => { + const { t } = useI18n(); + const [isTokenSelectOpen, setIsTokenSelectOpen] = useState(false); + + const [isCurrencySelectOpen, setIsCurrencySelectOpen] = useState(false); + const [isProviderSelectOpen, setIsProviderSelectOpen] = useState(false); + + const handleContinue = useCallback(() => { + setIsProviderSelectOpen(true); + }, []); + + const handleProviderSelected = useCallback( + (provider: OnrampProvider) => { + setSelectedProvider(provider); + // We give it a small timeout or wait for state update to ensure + // the buildOnrampUrl uses the correct providerId if it's derived from state. + // In our provider, buildOnrampUrl uses selectedProvider.id from state. + setTimeout(() => { + onContinue(); + setIsProviderSelectOpen(false); + }, 0); + }, + [onContinue, setSelectedProvider], + ); + + return ( +
+ setIsTokenSelectOpen(true)} + onToClick={() => setIsCurrencySelectOpen(true)} + /> + +
+ + setAmountInputMode(amountInputMode === 'token' ? 'currency' : 'token') + : undefined + } + ticker={amountInputMode === 'token' ? undefined : selectedToken?.symbol} + symbol={amountInputMode === 'token' ? selectedCurrency.symbol : undefined} + decimals={amountInputMode === 'token' ? 2 : (selectedToken?.decimals ?? 0)} + /> +
+ + { + setAmountInputMode('currency'); + setAmount(value); + }} + /> + + + + + + setIsTokenSelectOpen(false)} + tokens={tokens} + tokenSections={tokenSections} + onSelect={setSelectedToken} + title={t('onramp.selectToken')} + searchPlaceholder={t('onramp.searchToken')} + /> + + setIsCurrencySelectOpen(false)} + currencies={currencies} + currencySections={currencySections} + onSelect={setSelectedCurrency} + /> + + setIsProviderSelectOpen(false)} + providers={providers} + onSelect={handleProviderSelected} + /> +
+ ); +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/index.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/index.ts new file mode 100644 index 000000000..8c8b72d43 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/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 { OnrampWidget } from './onramp-widget'; +export type { OnrampWidgetProps } from './onramp-widget'; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/onramp-widget.stories.tsx b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/onramp-widget.stories.tsx new file mode 100644 index 000000000..fb858c352 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/onramp-widget.stories.tsx @@ -0,0 +1,69 @@ +/** + * 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 { Meta, StoryObj } from '@storybook/react-vite'; + +import { STORY_TOKENS } from '../../../../../storybook/fixtures/tokens'; +import { OnrampWidget } from './onramp-widget'; + +const meta: Meta = { + title: 'Public/Features/Onramp/OnrampWidget', + component: OnrampWidget, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + tokens: STORY_TOKENS, + defaultTokenId: 'ton', + defaultCurrencyId: 'usd', + tokenSections: [{ title: 'Popular', ids: ['ton', 'usdt'] }], + currencySections: [{ title: 'Popular', ids: ['usd', 'eur'] }], + }, +}; + +export const CustomUI: Story = { + args: { + tokens: STORY_TOKENS, + defaultTokenId: 'ton', + defaultCurrencyId: 'usd', + }, + render: (args) => ( + + {({ selectedToken, selectedCurrency, amount, setAmount, canSubmit }) => ( +
+
+ Buy {selectedToken?.symbol} with {selectedCurrency.code} +
+ setAmount(e.target.value)} + placeholder="0" + inputMode="decimal" + style={{ fontSize: 32, fontWeight: 'bold', border: 'none', outline: 'none' }} + /> + +
+ )} +
+ ), +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/onramp-widget.tsx b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/onramp-widget.tsx new file mode 100644 index 000000000..24d3c1cab --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/onramp-widget/onramp-widget.tsx @@ -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. + * + */ + +import type { FC, ReactNode } from 'react'; + +import type { OnrampWidgetRenderProps } from '../onramp-widget-ui'; +import { OnrampWidgetUI } from '../onramp-widget-ui'; +import { OnrampWidgetProvider, useOnrampContext } from '../onramp-widget-provider'; +import type { OnrampProviderProps } from '../onramp-widget-provider'; + +export interface OnrampWidgetProps extends Omit { + /** Custom render function — when provided, replaces the default widget UI */ + children?: (props: OnrampWidgetRenderProps) => ReactNode; +} + +const OnrampWidgetContent: FC<{ children?: (props: OnrampWidgetRenderProps) => ReactNode }> = ({ children }) => { + const ctx = useOnrampContext(); + + if (children) { + return <>{children(ctx)}; + } + + return ; +}; + +export const OnrampWidget: FC = ({ + children, + tokens, + tokenSections, + currencySections, + defaultTokenId, + defaultCurrencyId, +}) => { + return ( + + {children} + + ); +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/utils/map-onramp-error.ts b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/utils/map-onramp-error.ts new file mode 100644 index 000000000..3ed786f08 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/fiat-onramp/utils/map-onramp-error.ts @@ -0,0 +1,33 @@ +/** + * 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 { OnrampError } from '@ton/appkit/onramp'; + +import { mapDefiError } from '../../../../../utils/map-defi-error'; + +/** + * Map a thrown onramp error to an i18n key. Tries onramp-specific codes first, + * falls back to the shared {@link mapDefiError}, and finally to a generic + * `onramp.genericError`. + */ +export const mapOnrampError = (error: unknown): string => { + if (error instanceof OnrampError) { + switch (error.code) { + case OnrampError.PAIR_NOT_SUPPORTED: + return 'onramp.pairNotSupported'; + case OnrampError.QUOTE_FAILED: + return 'onramp.noQuotesFound'; + case OnrampError.URL_BUILD_FAILED: + return 'onramp.urlBuildFailed'; + case OnrampError.PROVIDER_ERROR: + return 'onramp.providerError'; + } + } + + return mapDefiError(error) ?? 'onramp.genericError'; +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/index.ts b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/index.ts new file mode 100644 index 000000000..ea36f5990 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/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 { TonPayWidget } from './ton-pay-widget'; +export type { TonPayWidgetProps } from './ton-pay-widget'; diff --git a/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.module.css b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.module.css new file mode 100644 index 000000000..f5d2bbd23 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.module.css @@ -0,0 +1,35 @@ +.widget { + box-sizing: border-box; + width: 100%; + max-width: 370px; + display: flex; + flex-direction: column; +} + +.selectors { + display: grid; + gap: 4px; + padding: 2px; + width: 100%; + grid-template-columns: 1fr 1fr; + margin-bottom: 64px; +} + +.selector { + width: 100%; +} + +.input { + margin-bottom: 64px; +} + +.presets { + margin-bottom: 16px; +} + +.error { + composes: footnoteRegular from '../../../../styles/typography.module.css'; + margin-top: 12px; + color: var(--ta-color-error); + text-align: center; +} diff --git a/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.stories.tsx b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.stories.tsx new file mode 100644 index 000000000..ad82292d8 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.stories.tsx @@ -0,0 +1,28 @@ +/** + * 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 { Meta, StoryObj } from '@storybook/react-vite'; + +import { STORY_TOKENS } from '../../../../storybook/fixtures/tokens'; +import { TonPayWidget } from './ton-pay-widget'; + +const meta: Meta = { + title: 'Public/Features/Onramp/TonPayWidget', + component: TonPayWidget, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + tokens: STORY_TOKENS, + defaultTokenId: 'ton', + }, +}; diff --git a/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.tsx b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.tsx new file mode 100644 index 000000000..39845c390 --- /dev/null +++ b/packages/appkit-react/src/features/onramp/widgets/ton-pay-widget/ton-pay-widget.tsx @@ -0,0 +1,208 @@ +/** + * 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 { useCallback, useMemo, useState } from 'react'; +import type { FC } from 'react'; + +import { Button } from '../../../../components/ui/button'; +import { CenteredAmountInput } from '../../../../components/ui/centered-amount-input'; +import { TokenSelector } from '../../../../components/shared/token-selector'; +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import { TokenSelectModal } from '../../../../components/shared/token-select-modal'; +import type { TokenSectionConfig } from '../../../../components/shared/token-select-modal'; +import { useConnectedWallets } from '../../../wallets/hooks/use-connected-wallets'; +import { useI18n } from '../../../settings/hooks/use-i18n'; +import { OnrampCurrencySelectModal } from '../../components/onramp-currency-select-modal'; +import { ONRAMP_CURRENCIES } from '../../mock-data/currencies'; +import type { OnrampAmountPreset, OnrampCurrency, CurrencySectionConfig } from '../../types'; +import styles from './ton-pay-widget.module.css'; +import { AmountPresets } from '../../../../components/shared/amount-presets'; + +const TON_PAY_API_URL = 'https://testnet.pay.ton.org/api/merchant/v1/create-moonpay-transfer'; +const SUPPORTED_ASSETS = ['TON', 'USDT'] as const; + +const DEFAULT_CURRENCIES: OnrampCurrency[] = ONRAMP_CURRENCIES.filter((c) => c.id === 'usd'); + +const DEFAULT_PRESET_AMOUNTS = ['50', '100', '250', '500']; + +export interface TonPayWidgetProps { + /** Tokens available to buy — only TON and USDT are supported by TonPay */ + tokens: AppkitUIToken[]; + /** Optional section configs for grouping tokens in the selector */ + tokenSections?: TokenSectionConfig[]; + /** Id of the token pre-selected for purchase */ + defaultTokenId?: string; + /** Fiat currencies to show in the selector. Defaults to USD only. */ + currencies?: OnrampCurrency[]; + /** Optional section configs for grouping currencies in the selector */ + currencySections?: CurrencySectionConfig[]; + /** Id of the fiat currency pre-selected */ + defaultCurrencyId?: string; + /** Pre-filled amount */ + defaultAmount?: string; + /** Preset amount buttons (crypto amounts). Defaults to 50/100/250/500 with the token ticker. */ + presetAmounts?: OnrampAmountPreset[]; +} + +export const TonPayWidget: FC = ({ + tokens, + tokenSections, + defaultTokenId, + currencies = DEFAULT_CURRENCIES, + currencySections, + defaultCurrencyId, + defaultAmount = '', + presetAmounts, +}) => { + const { t } = useI18n(); + + const supportedTokens = useMemo( + () => + tokens.filter((token) => + SUPPORTED_ASSETS.includes(token.symbol.toUpperCase() as (typeof SUPPORTED_ASSETS)[number]), + ), + [tokens], + ); + + const [selectedToken, setSelectedToken] = useState( + () => supportedTokens.find((t) => t.id === defaultTokenId) ?? supportedTokens[0] ?? null, + ); + const [selectedCurrency, setSelectedCurrency] = useState( + () => currencies.find((c) => c.id === defaultCurrencyId) ?? currencies[0] ?? null, + ); + const [amount, setAmount] = useState(defaultAmount); + const [isTokenSelectOpen, setIsTokenSelectOpen] = useState(false); + const [isCurrencySelectOpen, setIsCurrencySelectOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const wallets = useConnectedWallets(); + const activeWallet = wallets?.[0]; + + const numericAmount = parseFloat(amount); + const isAmountValid = !isNaN(numericAmount) && numericAmount > 0; + const canContinue = isAmountValid && !!selectedToken && !!activeWallet && !isSubmitting; + const isCurrencyReadOnly = currencies.length <= 1; + const isTokenReadOnly = supportedTokens.length <= 1; + + const effectivePresets = useMemo(() => { + if (presetAmounts) return presetAmounts; + + return DEFAULT_PRESET_AMOUNTS.map((amount) => ({ + amount, + label: amount, + })); + }, [presetAmounts]); + + const handleContinue = useCallback(async () => { + if (!canContinue || !selectedToken || !activeWallet) return; + + setIsSubmitting(true); + setError(null); + + try { + const response = await fetch(TON_PAY_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + asset: selectedToken.symbol.toUpperCase(), + amount: numericAmount, + recipientAddr: activeWallet.getAddress(), + directTopUp: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + if (!data.link) { + throw new Error('Missing redirect URL'); + } + + window.open(data.link, '_blank'); + } catch { + setError(t('onramp.tonPayError')); + } finally { + setIsSubmitting(false); + } + }, [canContinue, selectedToken, activeWallet, numericAmount, t]); + + const errorMessage = error ?? (!activeWallet ? t('onramp.connectWallet') : null); + + return ( +
+
+ setIsTokenSelectOpen(true)} + /> + + setIsCurrencySelectOpen(true)} + /> +
+ + + + + + + + {errorMessage &&
{errorMessage}
} + + {!isTokenReadOnly && ( + setIsTokenSelectOpen(false)} + tokens={supportedTokens} + tokenSections={tokenSections} + onSelect={setSelectedToken} + title={t('onramp.selectToken')} + searchPlaceholder={t('onramp.searchToken')} + /> + )} + + {!isCurrencyReadOnly && ( + setIsCurrencySelectOpen(false)} + currencies={currencies} + currencySections={currencySections} + onSelect={setSelectedCurrency} + /> + )} +
+ ); +}; diff --git a/packages/appkit-react/src/locales/en.ts b/packages/appkit-react/src/locales/en.ts index f073ace62..a8d8ab12d 100644 --- a/packages/appkit-react/src/locales/en.ts +++ b/packages/appkit-react/src/locales/en.ts @@ -159,10 +159,15 @@ export default { buyToken: 'Buy {{ symbol }}', forCurrency: 'for {{ symbol }}', noQuotesFound: 'No quotes found', + pairNotSupported: 'This currency pair is not supported', connectWallet: 'Connect a wallet to continue', tonPayError: 'Failed to start TonPay checkout', youGet: 'You get', - exchangeRate: 'Exchange rate', + yourBalance: 'Your balance', + tooManyDecimals: 'Too many decimal places', + providerError: 'Onramp provider unavailable', + urlBuildFailed: 'Could not build the onramp link', + genericError: 'Unable to get a quote', }, // Staking diff --git a/packages/appkit/docs/actions.md b/packages/appkit/docs/actions.md index 3fb255878..74a53c57b 100644 --- a/packages/appkit/docs/actions.md +++ b/packages/appkit/docs/actions.md @@ -429,6 +429,59 @@ const result = await transferNft(appKit, { console.log('NFT Transfer Result:', result); ``` +## Onramp + +### `getOnrampManager` + +Get the `OnrampManager` instance. + +### `getOnrampProvider` + +Get a specific onramp provider by its ID. + +### `getOnrampProviders` + +Get all registered onramp providers. + +### `watchOnrampProviders` + +Watch for new onramp providers registration. + +### `getOnrampQuotes` + +Get onramp quotes from all registered providers (results are flattened). + +```ts +const quotes = await getOnrampQuotes(appKit, { + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + amount: '100', + isFiatAmount: true, +}); +console.log('Onramp Quotes:', quotes); +``` + +### `buildOnrampUrl` + +Build an onramp URL for redirecting the user to the provider. + +```ts +const quotes = await getOnrampQuotes(appKit, { + fiatCurrency: 'USD', + cryptoCurrency: 'TON', + amount: '100', +}); + +const [quote] = quotes; +if (!quote) throw new Error('No onramp quotes available'); + +const url = await buildOnrampUrl(appKit, { + quote, + userAddress: 'UQ...wallet-address...', +}); +console.log('Onramp URL:', url); +``` + ## Crypto Onramp ### `getCryptoOnrampProvider` diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 69a7c3e04..571ff6346 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -61,6 +61,26 @@ "default": "./dist/cjs/staking/tonstakers/index.js" } }, + "./onramp": { + "import": { + "types": "./dist/esm/onramp/index.d.ts", + "default": "./dist/esm/onramp/index.js" + }, + "require": { + "types": "./dist/cjs/onramp/index.d.ts", + "default": "./dist/cjs/onramp/index.js" + } + }, + "./onramp/appkit-onramp": { + "import": { + "types": "./dist/esm/onramp/appkit-onramp/index.d.ts", + "default": "./dist/esm/onramp/appkit-onramp/index.js" + }, + "require": { + "types": "./dist/cjs/onramp/appkit-onramp/index.d.ts", + "default": "./dist/cjs/onramp/appkit-onramp/index.js" + } + }, "./crypto-onramp/decent": { "import": { "types": "./dist/esm/crypto-onramp/decent/index.d.ts", @@ -96,6 +116,12 @@ "staking/tonstakers": [ "./dist/esm/staking/tonstakers/index.d.ts" ], + "onramp": [ + "./dist/esm/onramp/index.d.ts" + ], + "onramp/appkit-onramp": [ + "./dist/esm/onramp/appkit-onramp/index.d.ts" + ], "crypto-onramp/decent": [ "./dist/esm/crypto-onramp/decent/index.d.ts" ], @@ -123,8 +149,7 @@ "@tanstack/query-core": ">=5.0.0", "@ton/core": "^0.62.0 || ^0.63.0", "@ton/crypto": "^3.3.0", - "@tonconnect/ui": ">=2.5.0-alpha.1", - "@tonconnect/sdk": ">=3.5.0-alpha.1" + "@tonconnect/ui": ">=2.5.0-alpha.1" }, "peerDependenciesMeta": { "@tonconnect/ui": { diff --git a/packages/appkit/src/actions/index.ts b/packages/appkit/src/actions/index.ts index 2aa785bc8..fa5b7cf70 100644 --- a/packages/appkit/src/actions/index.ts +++ b/packages/appkit/src/actions/index.ts @@ -180,6 +180,12 @@ export { type BuildSwapTransactionReturnType, } from './swap/build-swap-transaction'; +// Onramp — fiat-onramp: not ready; functions hidden from public API, types kept for internal hooks +export type { GetOnrampProviderOptions, GetOnrampProviderReturnType } from './onramp/get-onramp-provider'; +export type { GetOnrampProvidersReturnType } from './onramp/get-onramp-providers'; +export type { GetOnrampQuotesOptions, GetOnrampQuotesReturnType } from './onramp/get-onramp-quotes'; +export type { WatchOnrampProvidersParameters, WatchOnrampProvidersReturnType } from './onramp/watch-onramp-providers'; +export type { BuildOnrampUrlOptions, BuildOnrampUrlReturnType } from './onramp/build-onramp-url'; // Staking export { getStakingManager, type GetStakingManagerReturnType } from './staking/get-staking-manager'; export { getStakingProviders, type GetStakingProvidersReturnType } from './staking/get-staking-providers'; diff --git a/packages/appkit/src/actions/onramp/build-onramp-url.ts b/packages/appkit/src/actions/onramp/build-onramp-url.ts new file mode 100644 index 000000000..ec4afbd4d --- /dev/null +++ b/packages/appkit/src/actions/onramp/build-onramp-url.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 { OnrampParams } from '@ton/walletkit'; + +import type { AppKit } from '../../core/app-kit'; + +export type BuildOnrampUrlOptions = OnrampParams & { + providerId?: string; +}; + +export type BuildOnrampUrlReturnType = Promise; + +/** + * Build onramp URL + */ +export const buildOnrampUrl = async ( + appKit: AppKit, + options: BuildOnrampUrlOptions, +): BuildOnrampUrlReturnType => { + return appKit.onrampManager.buildOnrampUrl(options, options.providerId); +}; diff --git a/packages/appkit/src/actions/onramp/get-onramp-manager.ts b/packages/appkit/src/actions/onramp/get-onramp-manager.ts new file mode 100644 index 000000000..2fafad61b --- /dev/null +++ b/packages/appkit/src/actions/onramp/get-onramp-manager.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. + * + */ + +import type { OnrampManager } from '@ton/walletkit'; + +import type { AppKit } from '../../core/app-kit'; + +export type GetOnrampManagerReturnType = OnrampManager; + +/** + * Get onramp manager instance + */ +export const getOnrampManager = (appKit: AppKit): GetOnrampManagerReturnType => { + return appKit.onrampManager; +}; diff --git a/packages/appkit/src/actions/onramp/get-onramp-provider.ts b/packages/appkit/src/actions/onramp/get-onramp-provider.ts new file mode 100644 index 000000000..948386573 --- /dev/null +++ b/packages/appkit/src/actions/onramp/get-onramp-provider.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 { OnrampProviderInterface } from '@ton/walletkit'; + +import type { AppKit } from '../../core/app-kit'; + +export interface GetOnrampProviderOptions { + id?: string; +} + +export type GetOnrampProviderReturnType = OnrampProviderInterface; + +/** + * Get onramp provider + */ +export const getOnrampProvider = ( + appKit: AppKit, + options: GetOnrampProviderOptions = {}, +): GetOnrampProviderReturnType => { + return appKit.onrampManager.getProvider(options.id); +}; diff --git a/packages/appkit/src/actions/onramp/get-onramp-providers.ts b/packages/appkit/src/actions/onramp/get-onramp-providers.ts new file mode 100644 index 000000000..97440fc28 --- /dev/null +++ b/packages/appkit/src/actions/onramp/get-onramp-providers.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. + * + */ + +import type { OnrampProviderInterface } from '@ton/walletkit'; + +import type { AppKit } from '../../core/app-kit'; + +export type GetOnrampProvidersReturnType = OnrampProviderInterface[]; + +/** + * Get all registered onramp providers + */ +export const getOnrampProviders = (appKit: AppKit): GetOnrampProvidersReturnType => { + return appKit.onrampManager.getProviders(); +}; diff --git a/packages/appkit/src/actions/onramp/get-onramp-quotes.ts b/packages/appkit/src/actions/onramp/get-onramp-quotes.ts new file mode 100644 index 000000000..971b9cf8a --- /dev/null +++ b/packages/appkit/src/actions/onramp/get-onramp-quotes.ts @@ -0,0 +1,28 @@ +/** + * 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 { OnrampQuote, OnrampQuoteParams } from '@ton/walletkit'; + +import { resolveNetwork } from '../../utils'; +import type { AppKit } from '../../core/app-kit'; + +export type GetOnrampQuotesOptions = OnrampQuoteParams; + +export type GetOnrampQuotesReturnType = Promise; + +/** + * Get onramp quotes from all registered providers (results are flattened). + */ +export const getOnrampQuotes = async ( + appKit: AppKit, + options: GetOnrampQuotesOptions, +): GetOnrampQuotesReturnType => { + const network = resolveNetwork(appKit, options.network); + + return appKit.onrampManager.getQuotes({ ...options, network }); +}; diff --git a/packages/appkit/src/actions/onramp/watch-onramp-providers.ts b/packages/appkit/src/actions/onramp/watch-onramp-providers.ts new file mode 100644 index 000000000..a14d12682 --- /dev/null +++ b/packages/appkit/src/actions/onramp/watch-onramp-providers.ts @@ -0,0 +1,38 @@ +/** + * 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 '../../core/app-kit'; + +export interface WatchOnrampProvidersParameters { + onChange: () => void; +} + +export type WatchOnrampProvidersReturnType = () => void; + +/** + * Watch for new onramp providers registration + */ +export const watchOnrampProviders = ( + appKit: AppKit, + parameters: WatchOnrampProvidersParameters, +): WatchOnrampProvidersReturnType => { + const { onChange } = parameters; + + const unsubscribeRegistered = appKit.emitter.on('provider:registered', (event) => { + if (event.payload.type === 'onramp') onChange(); + }); + + const unsubscribeDefaultChanged = appKit.emitter.on('provider:default-changed', (event) => { + if (event.payload.type === 'onramp') onChange(); + }); + + return () => { + unsubscribeRegistered(); + unsubscribeDefaultChanged(); + }; +}; 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..dd1785620 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 @@ -7,8 +7,7 @@ */ import { Address } from '@ton/core'; -import type { Wallet as TonConnectWallet } from '@tonconnect/sdk'; -import type { SignDataPayload as TonConnectSignDataPayload } from '@tonconnect/sdk'; +import type { Wallet as TonConnectWallet, SignDataPayload as TonConnectSignDataPayload } from '@tonconnect/ui'; import type { SendTransactionResponse, UserFriendlyAddress, Hex } from '@ton/walletkit'; import { asHex, createWalletId, getNormalizedExtMessageHash } from '@ton/walletkit'; import type { TonConnectUI } from '@tonconnect/ui'; diff --git a/packages/appkit/src/connectors/tonconnect/utils/transaction.ts b/packages/appkit/src/connectors/tonconnect/utils/transaction.ts index 2d6d10069..e7cc0e35c 100644 --- a/packages/appkit/src/connectors/tonconnect/utils/transaction.ts +++ b/packages/appkit/src/connectors/tonconnect/utils/transaction.ts @@ -6,7 +6,7 @@ * */ -import type { SendTransactionRequest } from '@tonconnect/sdk'; +import type { SendTransactionRequest } from '@tonconnect/ui'; import type { TransactionRequest, TransactionRequestMessage } from '../../../types/transaction'; 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 f22f1f27e..1d5d930a9 100644 --- a/packages/appkit/src/core/app-kit/services/app-kit.ts +++ b/packages/appkit/src/core/app-kit/services/app-kit.ts @@ -6,11 +6,12 @@ * */ -import { SwapManager, StreamingManager, CryptoOnrampManager } from '@ton/walletkit'; +import { SwapManager, StreamingManager, OnrampManager, CryptoOnrampManager } from '@ton/walletkit'; import type { ProviderInput, SwapProviderInterface, StakingProviderInterface, + OnrampProviderInterface, CryptoOnrampProviderInterface, StreamingProvider, } from '@ton/walletkit'; @@ -39,6 +40,7 @@ export class AppKit { readonly walletsManager: WalletsManager; readonly swapManager: SwapManager; readonly stakingManager: StakingManager; + readonly onrampManager: OnrampManager; readonly cryptoOnrampManager: CryptoOnrampManager; readonly networkManager: AppKitNetworkManager; @@ -63,6 +65,7 @@ export class AppKit { this.swapManager = new SwapManager(() => this.createFactoryContext()); this.stakingManager = new StakingManager(() => this.createFactoryContext()); + this.onrampManager = new OnrampManager(() => this.createFactoryContext()); this.cryptoOnrampManager = new CryptoOnrampManager(() => this.createFactoryContext()); this.streamingManager = new StreamingManager(() => this.createFactoryContext()); @@ -131,6 +134,9 @@ export class AppKit { case 'staking': this.stakingManager.registerProvider(provider as StakingProviderInterface); break; + case 'onramp': + this.onrampManager.registerProvider(provider as OnrampProviderInterface); + break; case 'crypto-onramp': this.cryptoOnrampManager.registerProvider(provider as CryptoOnrampProviderInterface); break; diff --git a/packages/appkit/src/onramp/appkit-onramp/index.ts b/packages/appkit/src/onramp/appkit-onramp/index.ts new file mode 100644 index 000000000..aad7335dd --- /dev/null +++ b/packages/appkit/src/onramp/appkit-onramp/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/onramp/appkit-onramp'; diff --git a/packages/appkit/src/onramp/index.ts b/packages/appkit/src/onramp/index.ts new file mode 100644 index 000000000..cb6152dbd --- /dev/null +++ b/packages/appkit/src/onramp/index.ts @@ -0,0 +1,46 @@ +/** + * 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. + * + */ + +// fiat-onramp: not ready — exported via sub-path to keep off main @ton/appkit API + +export { OnrampProvider, OnrampManager, OnrampError } from '@ton/walletkit'; + +export type { + OnrampAPI, + OnrampProviderInterface, + OnrampProviderMetadata, + OnrampProviderMetadataOverride, + OnrampQuote, + OnrampQuoteParams, + OnrampParams, + OnrampServiceInfo, + OnrampFee, + OnrampFeeType, +} from '@ton/walletkit'; + +export { + getOnrampProvider, + type GetOnrampProviderOptions, + type GetOnrampProviderReturnType, +} from '../actions/onramp/get-onramp-provider'; +export { getOnrampProviders, type GetOnrampProvidersReturnType } from '../actions/onramp/get-onramp-providers'; +export { + getOnrampQuotes, + type GetOnrampQuotesOptions, + type GetOnrampQuotesReturnType, +} from '../actions/onramp/get-onramp-quotes'; +export { + watchOnrampProviders, + type WatchOnrampProvidersParameters, + type WatchOnrampProvidersReturnType, +} from '../actions/onramp/watch-onramp-providers'; +export { + buildOnrampUrl, + type BuildOnrampUrlOptions, + type BuildOnrampUrlReturnType, +} from '../actions/onramp/build-onramp-url'; diff --git a/packages/appkit/src/queries/index.ts b/packages/appkit/src/queries/index.ts index ed97d4b2e..f44741df6 100644 --- a/packages/appkit/src/queries/index.ts +++ b/packages/appkit/src/queries/index.ts @@ -211,6 +211,23 @@ export { type BuildSwapTransactionVariables, } from './swap/build-swap-transaction'; +// Onramp +export { + getOnrampQuotesQueryOptions, + type GetOnrampQuotesQueryConfig, + type GetOnrampQuotesQueryOptions, + type GetOnrampQuotesData, + type GetOnrampQuotesErrorType, + type GetOnrampQuotesQueryFnData, + type GetOnrampQuotesQueryKey, +} from './onramp/get-onramp-quotes'; +export { + buildOnrampUrlMutationOptions, + type BuildOnrampUrlMutationOptions, + type BuildOnrampUrlData, + type BuildOnrampUrlErrorType, + type BuildOnrampUrlVariables, +} from './onramp/build-onramp-url'; // Staking export { getStakingQuoteQueryOptions, diff --git a/packages/appkit/src/queries/onramp/build-onramp-url.ts b/packages/appkit/src/queries/onramp/build-onramp-url.ts new file mode 100644 index 000000000..e930aea18 --- /dev/null +++ b/packages/appkit/src/queries/onramp/build-onramp-url.ts @@ -0,0 +1,37 @@ +/** + * 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 type { AppKit } from '../../core/app-kit'; +import { buildOnrampUrl } from '../../actions/onramp/build-onramp-url'; +import type { BuildOnrampUrlOptions, BuildOnrampUrlReturnType } from '../../actions/onramp/build-onramp-url'; +import type { MutationParameter } from '../../types/query'; + +export type BuildOnrampUrlErrorType = Error; +export type BuildOnrampUrlData = Awaited; +export type BuildOnrampUrlVariables = BuildOnrampUrlOptions; +export type BuildOnrampUrlMutationOptions = MutationParameter< + BuildOnrampUrlData, + BuildOnrampUrlErrorType, + BuildOnrampUrlVariables, + context +>; + +export type BuildOnrampUrlMutationConfig = MutationOptions< + BuildOnrampUrlData, + BuildOnrampUrlErrorType, + BuildOnrampUrlVariables, + context +>; + +export const buildOnrampUrlMutationOptions = ( + appKit: AppKit, +): BuildOnrampUrlMutationConfig => ({ + mutationFn: (variables: BuildOnrampUrlVariables) => buildOnrampUrl(appKit, variables), +}); diff --git a/packages/appkit/src/queries/onramp/get-onramp-quotes.ts b/packages/appkit/src/queries/onramp/get-onramp-quotes.ts new file mode 100644 index 000000000..3f235bdc2 --- /dev/null +++ b/packages/appkit/src/queries/onramp/get-onramp-quotes.ts @@ -0,0 +1,47 @@ +/** + * 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 '../../core/app-kit'; +import { getOnrampQuotes } from '../../actions/onramp/get-onramp-quotes'; +import type { GetOnrampQuotesOptions, GetOnrampQuotesReturnType } from '../../actions/onramp/get-onramp-quotes'; +import type { QueryOptions, QueryParameter } from '../../types/query'; +import type { Compute, ExactPartial } from '../../types/utils'; +import { filterQueryOptions } from '../../utils'; + +export type GetOnrampQuotesErrorType = Error; +export type GetOnrampQuotesData = GetOnrampQuotesQueryFnData; +export type GetOnrampQuotesQueryConfig = Compute< + ExactPartial +> & + QueryParameter; + +export const getOnrampQuotesQueryOptions = ( + appKit: AppKit, + options: GetOnrampQuotesQueryConfig = {}, +): GetOnrampQuotesQueryOptions => { + return { + ...options.query, + queryFn: async (context) => { + const [, parameters] = context.queryKey as [string, GetOnrampQuotesOptions]; + return getOnrampQuotes(appKit, parameters); + }, + queryKey: getOnrampQuotesQueryKey(options), + }; +}; + +export type GetOnrampQuotesQueryFnData = Compute>; +export const getOnrampQuotesQueryKey = ( + options: Compute> = {}, +): GetOnrampQuotesQueryKey => ['onramp-quotes', filterQueryOptions(options)] as const; +export type GetOnrampQuotesQueryKey = readonly ['onramp-quotes', Compute>]; +export type GetOnrampQuotesQueryOptions = QueryOptions< + GetOnrampQuotesQueryFnData, + GetOnrampQuotesErrorType, + selectData, + GetOnrampQuotesQueryKey +>; diff --git a/packages/appkit/src/types/provider.ts b/packages/appkit/src/types/provider.ts index 033ba3bc9..4557bca19 100644 --- a/packages/appkit/src/types/provider.ts +++ b/packages/appkit/src/types/provider.ts @@ -9,6 +9,7 @@ import type { SwapProviderInterface, StakingProviderInterface, + OnrampProviderInterface, StreamingProvider, CryptoOnrampProviderInterface, } from '@ton/walletkit'; @@ -19,5 +20,6 @@ import type { export type AppKitProvider = | SwapProviderInterface | StakingProviderInterface + | OnrampProviderInterface | StreamingProvider | CryptoOnrampProviderInterface; diff --git a/packages/walletkit/package.json b/packages/walletkit/package.json index 19943041b..b65d837e7 100644 --- a/packages/walletkit/package.json +++ b/packages/walletkit/package.json @@ -61,6 +61,16 @@ "default": "./dist/cjs/defi/staking/tonstakers/index.js" } }, + "./onramp/appkit-onramp": { + "import": { + "types": "./dist/esm/defi/onramp/appkit-onramp/index.d.ts", + "default": "./dist/esm/defi/onramp/appkit-onramp/index.js" + }, + "require": { + "types": "./dist/cjs/defi/onramp/appkit-onramp/index.d.ts", + "default": "./dist/cjs/defi/onramp/appkit-onramp/index.js" + } + }, "./crypto-onramp/decent": { "import": { "types": "./dist/esm/defi/crypto-onramp/decent/index.d.ts", @@ -96,6 +106,9 @@ "staking/tonstakers": [ "./dist/cjs/defi/staking/tonstakers/index.d.ts" ], + "onramp/appkit-onramp": [ + "./dist/cjs/defi/onramp/appkit-onramp/index.d.ts" + ], "crypto-onramp/decent": [ "./dist/cjs/defi/crypto-onramp/decent/index.d.ts" ], diff --git a/packages/walletkit/src/api/interfaces/OnrampAPI.ts b/packages/walletkit/src/api/interfaces/OnrampAPI.ts new file mode 100644 index 000000000..9287458bd --- /dev/null +++ b/packages/walletkit/src/api/interfaces/OnrampAPI.ts @@ -0,0 +1,64 @@ +/** + * 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 { OnrampParams, OnrampProviderMetadata, OnrampQuote, OnrampQuoteParams } from '../models'; +import type { DefiManagerAPI } from './DefiManagerAPI'; +import type { DefiProvider } from './DefiProvider'; + +/** + * Onramp API interface exposed by OnrampManager + */ +export interface OnrampAPI extends DefiManagerAPI { + /** + * Get quotes for onramping fiat to crypto from all registered providers. + * Each provider may emit one or many quotes; results are flattened. + * @param params Quote parameters (fiat, crypto, amount, etc.) + * @returns A promise that resolves to a flat array of OnrampQuotes + */ + getQuotes(params: OnrampQuoteParams): Promise; + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params Onramp parameters (quote, user address, etc.) + * @param providerId Provider identifier (optional, uses default if not specified) + * @returns A promise that resolves to a URL string + */ + buildOnrampUrl(params: OnrampParams, providerId?: string): Promise; +} + +/** + * Interface that all onramp providers must implement + */ +export interface OnrampProviderInterface extends DefiProvider { + readonly type: 'onramp'; + + /** + * Unique identifier for the provider + */ + readonly providerId: string; + + /** + * Get static metadata for the provider (display name, logo, capability flags). + */ + getMetadata(): OnrampProviderMetadata; + + /** + * Get a quote (or quotes) for onramping fiat to crypto. + * Aggregating providers may return multiple quotes from a single call; + * single-source providers may return one quote directly. + * @param params Quote parameters including provider-specific options + */ + getQuote(params: OnrampQuoteParams): Promise; + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params Onramp parameters including provider-specific options + * @returns A promise that resolves to a URL string + */ + buildOnrampUrl(params: OnrampParams): Promise; +} diff --git a/packages/walletkit/src/api/interfaces/index.ts b/packages/walletkit/src/api/interfaces/index.ts index 83bd90edf..2180efc84 100644 --- a/packages/walletkit/src/api/interfaces/index.ts +++ b/packages/walletkit/src/api/interfaces/index.ts @@ -13,6 +13,7 @@ export type { WalletSigner, ISigner } from './WalletSigner'; // Defi interfaces export type { DefiManagerAPI } from './DefiManagerAPI'; export type { SwapAPI, SwapProviderInterface } from './SwapAPI'; +export type { OnrampAPI, OnrampProviderInterface } from './OnrampAPI'; export type { CryptoOnrampAPI, CryptoOnrampProviderInterface } from './CryptoOnrampAPI'; export type { DefiProvider } from './DefiProvider'; export type { StakingAPI, StakingProviderInterface } from './StakingAPI'; diff --git a/packages/walletkit/src/api/models/index.ts b/packages/walletkit/src/api/models/index.ts index f0615a618..7817e9c68 100644 --- a/packages/walletkit/src/api/models/index.ts +++ b/packages/walletkit/src/api/models/index.ts @@ -138,6 +138,16 @@ export type { StakingQuoteParams } from './staking/StakingQuoteParams'; export type { UnstakeModes } from './staking/UnstakeMode'; export { UnstakeMode } from './staking/UnstakeMode'; +// Onramp models +export type { OnrampFee, OnrampFeeType } from './onramp/OnrampFee'; +export type { OnrampParams } from './onramp/OnrampParams'; +export type { OnrampProviderMetadata, OnrampProviderMetadataOverride } from './onramp/OnrampProviderMetadata'; +export type { OnrampServiceInfo } from './onramp/OnrampServiceInfo'; +export type { OnrampQuote } from './onramp/OnrampQuote'; +export type { OnrampQuoteParams } from './onramp/OnrampQuoteParams'; +export type { OnrampLimits } from './onramp/OnrampLimits'; +export type { OnrampLimitParams } from './onramp/OnrampLimitParams'; + // Crypto onramp models export type { CryptoOnrampQuote } from './crypto-onramp/CryptoOnrampQuote'; export type { CryptoOnrampQuoteParams } from './crypto-onramp/CryptoOnrampQuoteParams'; diff --git a/packages/walletkit/src/api/models/onramp/OnrampFee.ts b/packages/walletkit/src/api/models/onramp/OnrampFee.ts new file mode 100644 index 000000000..bdeaece80 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampFee.ts @@ -0,0 +1,18 @@ +/** + * 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 type OnrampFeeType = 'service' | 'network' | 'processing'; + +/** + * A single fee charged for an onramp transaction. + */ +export interface OnrampFee { + type: OnrampFeeType; + amount: string; + currency: string; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampLimitParams.ts b/packages/walletkit/src/api/models/onramp/OnrampLimitParams.ts new file mode 100644 index 000000000..d2df9b2ba --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampLimitParams.ts @@ -0,0 +1,24 @@ +/** + * 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 interface OnrampLimitParams { + /** + * Crypto currency ticker (e.g. 'ton') + */ + cryptoCurrency: string; + + /** + * Fiat currency ticker (e.g. 'usd') + */ + fiatCurrency: string; + + /** + * Provider-specific options (e.g., paymentMethod) + */ + providerOptions?: TProviderOptions; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampLimits.ts b/packages/walletkit/src/api/models/onramp/OnrampLimits.ts new file mode 100644 index 000000000..7a8f58dc6 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampLimits.ts @@ -0,0 +1,37 @@ +/** + * 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. + * + */ + +/** + * Onramp limits specify the boundaries of what a user can purchase + */ +export interface OnrampLimits { + /** + * Minimum fiat amount allowed + */ + minBaseAmount: number; + + /** + * Maximum fiat amount allowed + */ + maxBaseAmount: number; + + /** + * Minimum crypto amount allowed + */ + minQuoteAmount?: number; + + /** + * Maximum crypto amount allowed + */ + maxQuoteAmount?: number; + + /** + * Provider identifier + */ + providerId: string; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampParams.ts b/packages/walletkit/src/api/models/onramp/OnrampParams.ts new file mode 100644 index 000000000..f676f1d11 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampParams.ts @@ -0,0 +1,35 @@ +/** + * 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 { OnrampQuote } from './OnrampQuote'; + +/** + * Parameters for building an onramp URL + */ +export interface OnrampParams { + /** + * The onramp quote to base the transaction on + */ + quote: OnrampQuote; + + /** + * Address of the user receiving the crypto + */ + userAddress: UserFriendlyAddress; + + /** + * URL to redirect the user to after a successful transaction (if supported by provider) + */ + redirectUrl?: string; + + /** + * Provider-specific options + */ + providerOptions?: TProviderOptions; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampProviderMetadata.ts b/packages/walletkit/src/api/models/onramp/OnrampProviderMetadata.ts new file mode 100644 index 000000000..22858afb4 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampProviderMetadata.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. + * + */ + +/** + * Static metadata for a registered onramp provider (SDK-level abstraction). + * + * Distinct from {@link OnrampServiceInfo}, which describes the underlying onramp + * service (e.g. MoonPay) attached to an individual quote. + */ +export interface OnrampProviderMetadata { + /** + * Human-readable provider name (e.g. 'AppKit Onramp') + */ + name: string; + + /** + * URL to the provider's logo image + */ + logo?: string; + + /** + * URL to the provider's website + */ + url?: string; + + /** + * Whether this provider supports reversed (crypto-amount) quotes. + * When false, the UI should hide the direction toggle and only allow fiat-amount input. + */ + isReversedAmountSupported?: boolean; +} + +/** + * Used in provider configuration to override fields of the provider's metadata. + */ +export interface OnrampProviderMetadataOverride { + name?: string; + logo?: string; + url?: string; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampQuote.ts b/packages/walletkit/src/api/models/onramp/OnrampQuote.ts new file mode 100644 index 000000000..e02b80250 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampQuote.ts @@ -0,0 +1,61 @@ +/** + * 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 { OnrampFee } from './OnrampFee'; +import type { OnrampServiceInfo } from './OnrampServiceInfo'; + +/** + * Onramp quote response with pricing information + */ +export interface OnrampQuote { + /** + * Fiat currency ticker (e.g. 'USD') + */ + fiatCurrency: string; + + /** + * Crypto currency ticker (e.g. 'TON') + */ + cryptoCurrency: string; + + /** + * Amount of fiat to spend + */ + fiatAmount: string; + + /** + * Amount of crypto to receive + */ + cryptoAmount: string; + + /** + * Exchange rate (amount of crypto per 1 unit of fiat) + */ + rate: string; + + /** + * Fees charged for the transaction + */ + fees?: OnrampFee[]; + + /** + * Identifier of the registered onramp provider that produced the quote + */ + providerId: string; + + /** + * The underlying onramp service that will fulfill this quote + * (set by aggregating providers like AppkitOnramp). + */ + serviceInfo?: OnrampServiceInfo; + + /** + * Provider-specific metadata for the quote (e.g. raw response needed to execute) + */ + metadata?: TMetadata; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampQuoteParams.ts b/packages/walletkit/src/api/models/onramp/OnrampQuoteParams.ts new file mode 100644 index 000000000..63a3b3bc5 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampQuoteParams.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 { Network } from '../core/Network'; + +/** + * Base parameters for requesting an onramp quote + */ +export interface OnrampQuoteParams { + /** + * Amount to onramp (either fiat or crypto, depending on isFiatAmount) + */ + amount: string; + + /** + * Fiat currency ticker (e.g. 'USD', 'EUR') + */ + fiatCurrency: string; + + /** + * Crypto currency ticker (e.g. 'TON', 'USDT') + */ + cryptoCurrency: string; + + /** + * Network on which the crypto will be received + */ + network?: Network; + + /** + * Provider-specific options + */ + providerOptions?: TProviderOptions; + + /** + * If true, amount is the fiat amount to spend. If false, amount is the crypto amount to receive. + * Default depends on the provider implementation but usually defaults to true. + */ + isFiatAmount?: boolean; +} diff --git a/packages/walletkit/src/api/models/onramp/OnrampServiceInfo.ts b/packages/walletkit/src/api/models/onramp/OnrampServiceInfo.ts new file mode 100644 index 000000000..0ebed8ea7 --- /dev/null +++ b/packages/walletkit/src/api/models/onramp/OnrampServiceInfo.ts @@ -0,0 +1,24 @@ +/** + * 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. + * + */ + +/** + * Static info describing an underlying fiat onramp service + * (e.g. MoonPay, Mercuryo) attached to a quote. + * + * Distinct from the SDK-level onramp provider: a single registered provider + * (e.g. AppkitOnramp) may surface quotes from many services. + */ +export interface OnrampServiceInfo { + id: string; + name: string; + url: string; + darkLogo: string; + lightLogo: string; + paymentMethods: string[]; + supportUrl: string; +} diff --git a/packages/walletkit/src/defi/index.ts b/packages/walletkit/src/defi/index.ts index 41d157fbc..29eea6c35 100644 --- a/packages/walletkit/src/defi/index.ts +++ b/packages/walletkit/src/defi/index.ts @@ -9,4 +9,5 @@ export * from './errors'; export * from './DefiManager'; export * from './swap'; +export * from './onramp'; export * from './crypto-onramp'; diff --git a/packages/walletkit/src/defi/onramp/OnrampManager.ts b/packages/walletkit/src/defi/onramp/OnrampManager.ts new file mode 100644 index 000000000..a9dadc6fb --- /dev/null +++ b/packages/walletkit/src/defi/onramp/OnrampManager.ts @@ -0,0 +1,112 @@ +/** + * 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 { OnrampAPI, OnrampProviderInterface } from '../../api/interfaces'; +import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../api/models'; +import { OnrampError } from './errors'; +import { globalLogger } from '../../core/Logger'; +import { DefiManager } from '../DefiManager'; + +const log = globalLogger.createChild('OnrampManager'); + +/** + * OnrampManager - manages onramp providers and delegates onramp operations + * + * Allows registration of multiple onramp providers and provides a unified API + * for fiat-to-crypto onramp operations. Providers can be switched dynamically. + */ +export class OnrampManager extends DefiManager implements OnrampAPI { + /** + * Get quotes for onramping fiat to crypto from all registered providers. + * Each provider may return one quote or an array; results are flattened. + * @param params - Quote parameters + * @returns Promise resolving to a flat array of onramp quotes + */ + async getQuotes(params: OnrampQuoteParams): Promise { + log.debug('Getting onramp quotes from all providers', { + fiatCurrency: params.fiatCurrency, + cryptoCurrency: params.cryptoCurrency, + amount: params.amount, + isFiatAmount: params.isFiatAmount, + }); + + const providers = this.getProviders(); + + const results = await Promise.allSettled( + providers.map((provider: OnrampProviderInterface) => provider.getQuote(params)), + ); + + const quotes: OnrampQuote[] = []; + + results.forEach((result: PromiseSettledResult, index: number) => { + if (result.status === 'fulfilled') { + if (Array.isArray(result.value)) { + quotes.push(...result.value); + return; + } + + quotes.push(result.value); + } else { + log.debug(`Provider ${providers[index].providerId} failed to return a quote`, { + error: result.reason, + params, + }); + } + }); + + log.debug(`Received ${quotes.length} onramp quotes`, { + successfulProviders: quotes.map((q) => q.providerId), + }); + + if (quotes.length === 0) { + throw new OnrampError( + `No onramp service supports ${params.fiatCurrency} → ${params.cryptoCurrency}`, + OnrampError.PAIR_NOT_SUPPORTED, + { + fiatCurrency: params.fiatCurrency, + cryptoCurrency: params.cryptoCurrency, + }, + ); + } + + return quotes; + } + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params - Onramp parameters including quote + * @param providerId - Optional provider name to use + * @returns Promise resolving to a URL string + */ + async buildOnrampUrl( + params: OnrampParams, + providerId?: string, + ): Promise { + const selectedProviderId = providerId || params.quote?.providerId || this.defaultProviderId; + + log.debug('Building onramp URL', { + providerId: selectedProviderId, + userAddress: params.userAddress, + }); + + try { + const url = await this.getProvider(selectedProviderId).buildOnrampUrl(params); + + log.debug('Built onramp URL', { url: url.substring(0, 50) + '...' }); + + return url; + } catch (error) { + log.error('Failed to build onramp URL', { error, params }); + throw error; + } + } + + protected createError(message: string, code: string, details?: unknown): OnrampError { + return new OnrampError(message, code, details); + } +} diff --git a/packages/walletkit/src/defi/onramp/OnrampProvider.ts b/packages/walletkit/src/defi/onramp/OnrampProvider.ts new file mode 100644 index 000000000..7817fb1dc --- /dev/null +++ b/packages/walletkit/src/defi/onramp/OnrampProvider.ts @@ -0,0 +1,57 @@ +/** + * 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 { Network, OnrampParams, OnrampProviderMetadata, OnrampQuote, OnrampQuoteParams } from '../../api/models'; +import type { OnrampProviderInterface } from '../../api/interfaces'; + +/** + * Abstract base class for onramp providers + * + * Provides a common interface for implementing fiat-to-crypto onramp functionality + * across different gateways. + * + * @example + * ```typescript + * class MyOnrampProvider extends OnrampProvider { + * async getQuote(params: OnrampQuoteParams): Promise { + * // Implementation — return one quote, or many if aggregating multiple sources + * } + * + * async buildOnrampUrl(params: OnrampParams): Promise { + * // Implementation + * } + * } + * ``` + */ +export abstract class OnrampProvider< + TQuoteOptions = undefined, + TOnrampOptions = undefined, +> implements OnrampProviderInterface { + readonly type = 'onramp'; + abstract readonly providerId: string; + abstract getSupportedNetworks(): Network[]; + + /** + * Get static metadata for the provider (display name, logo, capability flags). + */ + abstract getMetadata(): OnrampProviderMetadata; + + /** + * Get a quote (or quotes) for onramping fiat to crypto. + * Single-source providers may return one quote; aggregating providers may return many. + * @param params - Quote parameters including currencies and amount + */ + abstract getQuote(params: OnrampQuoteParams): Promise; + + /** + * Build an onramp URL for redirecting the user to the provider + * @param params - Onramp parameters including quote and user address + * @returns Promise resolving to a URL string + */ + abstract buildOnrampUrl(params: OnrampParams): Promise; +} diff --git a/packages/walletkit/src/defi/onramp/appkit-onramp/AppkitOnrampProvider.ts b/packages/walletkit/src/defi/onramp/appkit-onramp/AppkitOnrampProvider.ts new file mode 100644 index 000000000..d0fc700d1 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/appkit-onramp/AppkitOnrampProvider.ts @@ -0,0 +1,203 @@ +/** + * 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 { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../../api/models'; +import { Network } from '../../../api/models'; +import { OnrampProvider } from '../OnrampProvider'; +import { OnrampError } from '../errors'; +import { createProvider } from '../../../types/factory'; +import { isAppkitOnrampBuildUrlResponse, isAppkitOnrampGetQuoteResponse } from './utils'; + +const APPKIT_ONRAMP_PROVIDER_ID = 'appkit-onramp'; +const DEFAULT_BASE_URL = 'http://localhost:8090'; +const DEFAULT_NETWORK = 'TON'; + +export interface AppkitOnrampProviderConfig { + /** + * Project API key issued by the AppKit Onramp backend (e.g. `ak_test_...`, `ak_live_...`). + * Sent as a Bearer token. Required. + */ + apiKey: string; + + /** + * Override the backend base URL. Defaults to `http://localhost:8090`. + */ + baseUrl?: string; +} + +/** + * Onramp provider that delegates to the AppKit Onramp backend. + * + * A single `getQuote` call returns quotes from every underlying onramp service + * the backend has configured (MoonPay, etc.). Each returned `OnrampQuote` carries + * `serviceInfo` describing the underlying service; `buildOnrampUrl` reads + * `serviceInfo.id` to forward the user's choice back to the backend. + * + * @example + * ```typescript + * import { createAppkitOnrampProvider } from '@ton/walletkit/onramp/appkit-onramp'; + * + * kit.onrampManager.registerProvider( + * createAppkitOnrampProvider({ apiKey: 'ak_test_...' }), + * ); + * ``` + */ +export class AppkitOnrampProvider extends OnrampProvider { + readonly providerId = APPKIT_ONRAMP_PROVIDER_ID; + + private readonly apiKey: string; + private readonly baseUrl: string; + + constructor(config: AppkitOnrampProviderConfig) { + super(); + if (!config.apiKey) { + throw new OnrampError('AppkitOnramp: apiKey is required', OnrampError.PROVIDER_ERROR); + } + this.apiKey = config.apiKey; + this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ''); + } + + getSupportedNetworks(): Network[] { + return [Network.mainnet()]; + } + + getMetadata() { + return { + name: 'AppKit Onramp', + url: 'https://ton.org/dev', + isReversedAmountSupported: false, + }; + } + + async getQuote(params: OnrampQuoteParams): Promise { + const url = new URL(`${this.baseUrl}/onramp/get-quote`); + url.searchParams.set('amount', params.amount); + url.searchParams.set('fiatCurrency', params.fiatCurrency); + url.searchParams.set('cryptoCurrency', params.cryptoCurrency); + url.searchParams.set('network', DEFAULT_NETWORK); + + let response: Response; + try { + response = await fetch(url.toString(), { + method: 'GET', + headers: { Authorization: `Bearer ${this.apiKey}` }, + }); + } catch (error) { + throw new OnrampError('AppkitOnramp: network error while fetching quotes', OnrampError.QUOTE_FAILED, error); + } + + if (!response.ok) { + throw new OnrampError(`AppkitOnramp get-quote failed (HTTP ${response.status})`, OnrampError.QUOTE_FAILED, { + status: response.status, + }); + } + + let data: unknown; + try { + data = await response.json(); + } catch (error) { + throw new OnrampError('AppkitOnramp: invalid JSON response', OnrampError.QUOTE_FAILED, error); + } + + if (!isAppkitOnrampGetQuoteResponse(data)) { + throw new OnrampError('AppkitOnramp: unexpected get-quote response shape', OnrampError.QUOTE_FAILED, data); + } + + return data.quotes.map((q) => ({ + fiatCurrency: q.fiatCurrency, + cryptoCurrency: q.cryptoCurrency, + fiatAmount: q.fiatAmount, + cryptoAmount: q.cryptoAmount, + rate: q.rate, + fees: q.fees, + providerId: this.providerId, + serviceInfo: { + id: q.providerMetadata.providerId, + name: q.providerMetadata.name, + url: q.providerMetadata.url, + darkLogo: q.providerMetadata.darkLogo, + lightLogo: q.providerMetadata.lightLogo, + paymentMethods: q.providerMetadata.paymentMethods, + supportUrl: q.providerMetadata.supportUrl, + }, + metadata: q.metadata, + })); + } + + async buildOnrampUrl(params: OnrampParams): Promise { + const serviceId = params.quote.serviceInfo?.id; + if (!serviceId) { + throw new OnrampError( + 'AppkitOnramp: quote is missing serviceInfo.id — quote must come from this provider', + OnrampError.URL_BUILD_FAILED, + ); + } + + const body: Record = { + providerId: serviceId, + amount: params.quote.fiatAmount, + fiatCurrency: params.quote.fiatCurrency, + cryptoCurrency: params.quote.cryptoCurrency, + network: DEFAULT_NETWORK, + userAddress: params.userAddress, + }; + if (params.redirectUrl) { + body.redirectUrl = params.redirectUrl; + } + + let response: Response; + try { + response = await fetch(`${this.baseUrl}/onramp/build-url`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify(body), + }); + } catch (error) { + throw new OnrampError( + 'AppkitOnramp: network error while building URL', + OnrampError.URL_BUILD_FAILED, + error, + ); + } + + if (!response.ok) { + throw new OnrampError( + `AppkitOnramp build-url failed (HTTP ${response.status})`, + OnrampError.URL_BUILD_FAILED, + { status: response.status }, + ); + } + + let data: unknown; + try { + data = await response.json(); + } catch (error) { + throw new OnrampError('AppkitOnramp: invalid JSON response', OnrampError.URL_BUILD_FAILED, error); + } + + if (!isAppkitOnrampBuildUrlResponse(data)) { + throw new OnrampError( + 'AppkitOnramp: unexpected build-url response shape', + OnrampError.URL_BUILD_FAILED, + data, + ); + } + + return data.url; + } +} + +/** + * Returns a `ProviderFactory` for `AppkitOnrampProvider`. + * Pass to `providers: [createAppkitOnrampProvider(config)]`. + */ +export const createAppkitOnrampProvider = (config: AppkitOnrampProviderConfig) => + createProvider(() => new AppkitOnrampProvider(config)); diff --git a/packages/walletkit/src/defi/onramp/appkit-onramp/index.ts b/packages/walletkit/src/defi/onramp/appkit-onramp/index.ts new file mode 100644 index 000000000..ea86f5101 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/appkit-onramp/index.ts @@ -0,0 +1,11 @@ +/** + * 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 { AppkitOnrampProvider, createAppkitOnrampProvider } from './AppkitOnrampProvider'; +export type { AppkitOnrampProviderConfig } from './AppkitOnrampProvider'; +export type { AppkitOnrampQuote, AppkitOnrampGetQuoteResponse, AppkitOnrampBuildUrlResponse } from './types'; diff --git a/packages/walletkit/src/defi/onramp/appkit-onramp/types.ts b/packages/walletkit/src/defi/onramp/appkit-onramp/types.ts new file mode 100644 index 000000000..d3f0575b3 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/appkit-onramp/types.ts @@ -0,0 +1,48 @@ +/** + * 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 { OnrampFee } from '../../../api/models'; + +/** + * Service metadata as it appears on the wire — mirrors the backend's + * `ProviderMetadata` Go struct exactly. The `providerId` here is the + * underlying onramp service id (e.g. `'moonpay'`), not the SDK provider. + */ +export interface AppkitOnrampServiceMetadata { + providerId: string; + name: string; + url: string; + darkLogo: string; + lightLogo: string; + paymentMethods: string[]; + supportUrl: string; +} + +/** + * A quote element returned by the AppKit Onramp `/onramp/get-quote` endpoint. + * Mirrors the backend's `OnrampQuote` Go struct. + */ +export interface AppkitOnrampQuote { + fiatCurrency: string; + cryptoCurrency: string; + fiatAmount: string; + cryptoAmount: string; + rate: string; + fees: OnrampFee[]; + providerMetadata: AppkitOnrampServiceMetadata; + metadata?: unknown; +} + +export interface AppkitOnrampGetQuoteResponse { + quotes: AppkitOnrampQuote[]; + providerIds: string[]; +} + +export interface AppkitOnrampBuildUrlResponse { + url: string; +} diff --git a/packages/walletkit/src/defi/onramp/appkit-onramp/utils.ts b/packages/walletkit/src/defi/onramp/appkit-onramp/utils.ts new file mode 100644 index 000000000..d99e2e770 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/appkit-onramp/utils.ts @@ -0,0 +1,64 @@ +/** + * 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 { OnrampFee } from '../../../api/models'; +import type { + AppkitOnrampBuildUrlResponse, + AppkitOnrampGetQuoteResponse, + AppkitOnrampQuote, + AppkitOnrampServiceMetadata, +} from './types'; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value); + +const isStringArray = (value: unknown): value is string[] => + Array.isArray(value) && value.every((item) => typeof item === 'string'); + +const isOnrampFee = (value: unknown): value is OnrampFee => { + if (!isObject(value)) return false; + if (value.type !== 'service' && value.type !== 'network' && value.type !== 'processing') return false; + return typeof value.amount === 'string' && typeof value.currency === 'string'; +}; + +const isAppkitOnrampServiceMetadata = (value: unknown): value is AppkitOnrampServiceMetadata => { + if (!isObject(value)) return false; + return ( + typeof value.providerId === 'string' && + typeof value.name === 'string' && + typeof value.url === 'string' && + typeof value.darkLogo === 'string' && + typeof value.lightLogo === 'string' && + isStringArray(value.paymentMethods) && + typeof value.supportUrl === 'string' + ); +}; + +const isAppkitOnrampQuote = (value: unknown): value is AppkitOnrampQuote => { + if (!isObject(value)) return false; + return ( + typeof value.fiatCurrency === 'string' && + typeof value.cryptoCurrency === 'string' && + typeof value.fiatAmount === 'string' && + typeof value.cryptoAmount === 'string' && + typeof value.rate === 'string' && + Array.isArray(value.fees) && + value.fees.every(isOnrampFee) && + isAppkitOnrampServiceMetadata(value.providerMetadata) + ); +}; + +export const isAppkitOnrampGetQuoteResponse = (value: unknown): value is AppkitOnrampGetQuoteResponse => { + if (!isObject(value)) return false; + return Array.isArray(value.quotes) && value.quotes.every(isAppkitOnrampQuote) && isStringArray(value.providerIds); +}; + +export const isAppkitOnrampBuildUrlResponse = (value: unknown): value is AppkitOnrampBuildUrlResponse => { + if (!isObject(value)) return false; + return typeof value.url === 'string'; +}; diff --git a/packages/walletkit/src/defi/onramp/errors.ts b/packages/walletkit/src/defi/onramp/errors.ts new file mode 100644 index 000000000..34c3484f9 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/errors.ts @@ -0,0 +1,22 @@ +/** + * 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 { DefiError } from '../errors'; + +export class OnrampError extends DefiError { + static readonly PROVIDER_ERROR = 'PROVIDER_ERROR'; + static readonly InvalidParams = 'INVALID_ONRAMP_PARAMS'; + static readonly QUOTE_FAILED = 'QUOTE_FAILED'; + static readonly URL_BUILD_FAILED = 'URL_BUILD_FAILED'; + static readonly PAIR_NOT_SUPPORTED = 'PAIR_NOT_SUPPORTED'; + + constructor(message: string, code: string, details?: unknown) { + super(message, code, details); + this.name = 'OnrampError'; + } +} diff --git a/packages/walletkit/src/defi/onramp/index.ts b/packages/walletkit/src/defi/onramp/index.ts new file mode 100644 index 000000000..fb2788a54 --- /dev/null +++ b/packages/walletkit/src/defi/onramp/index.ts @@ -0,0 +1,12 @@ +/** + * 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 './errors'; +export * from './OnrampManager'; +export * from './OnrampProvider'; +export * from './appkit-onramp'; diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index 29639b7cf..afea0892a 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -22,6 +22,7 @@ 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 { OnrampManager, OnrampProvider, OnrampError } from './defi/onramp'; export { CryptoOnrampManager, CryptoOnrampProvider, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e9b49444..e5edfd847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -469,9 +469,6 @@ importers: '@ton/walletkit': specifier: workspace:* version: link:../walletkit - '@tonconnect/sdk': - specifier: '>=3.5.0-alpha.1' - version: 3.5.0-alpha.1 lru-cache: specifier: 'catalog:' version: 11.5.0