diff --git a/wallets/core/namespaces/ton/package.json b/wallets/core/namespaces/ton/package.json new file mode 100644 index 0000000000..f50e75a3cf --- /dev/null +++ b/wallets/core/namespaces/ton/package.json @@ -0,0 +1,8 @@ +{ + "name": "@rango-dev/wallets-core/namespaces/ton", + "type": "module", + "main": "../../dist/namespaces/ton/mod.js", + "module": "../../dist/namespaces/ton/mod.js", + "types": "../../dist/namespaces/ton/mod.d.ts", + "sideEffects": false +} diff --git a/wallets/core/package.json b/wallets/core/package.json index 66418a6de2..7e44707ac3 100644 --- a/wallets/core/package.json +++ b/wallets/core/package.json @@ -58,6 +58,10 @@ "./namespaces/xrpl": { "types": "./dist/namespaces/xrpl/mod.d.ts", "default": "./dist/namespaces/xrpl/mod.js" + }, + "./namespaces/ton": { + "types": "./dist/namespaces/ton/mod.d.ts", + "default": "./dist/namespaces/ton/mod.js" } }, "files": [ @@ -66,7 +70,7 @@ "legacy" ], "scripts": { - "build": "node ../../scripts/build/command.mjs --path wallets/core --inputs src/mod.ts,src/utils/mod.ts,src/legacy/mod.ts,src/hub/store/mod.ts,src/namespaces/evm/mod.ts,src/namespaces/solana/mod.ts,src/namespaces/cosmos/mod.ts,src/namespaces/utxo/mod.ts,src/namespaces/sui/mod.ts,src/namespaces/tron/mod.ts,src/namespaces/starknet/mod.ts,src/namespaces/xrpl/mod.ts,src/namespaces/common/mod.ts", + "build": "node ../../scripts/build/command.mjs --path wallets/core --inputs src/mod.ts,src/utils/mod.ts,src/legacy/mod.ts,src/hub/store/mod.ts,src/namespaces/evm/mod.ts,src/namespaces/solana/mod.ts,src/namespaces/cosmos/mod.ts,src/namespaces/utxo/mod.ts,src/namespaces/sui/mod.ts,src/namespaces/tron/mod.ts,src/namespaces/starknet/mod.ts,src/namespaces/xrpl/mod.ts,src/namespaces/common/mod.ts,src/namespaces/ton/mod.ts", "ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json", "clean": "rimraf dist", "format": "prettier --write '{.,src}/**/*.{ts,tsx}'", diff --git a/wallets/core/src/hub/hub.ts b/wallets/core/src/hub/hub.ts index 385dcefe65..f0895644e6 100644 --- a/wallets/core/src/hub/hub.ts +++ b/wallets/core/src/hub/hub.ts @@ -14,6 +14,15 @@ type RunAllResult = { namespaces: unknown[]; }; +type RunAllArgs = { + [providerId: string]: { + provider?: unknown; + namespaces?: { + [namespaceId: string]: unknown; + }; + }; +}; + interface HubOptions { store?: Store; } @@ -25,8 +34,8 @@ export class Hub { this.#options = options ?? {}; } - init() { - this.runAll('init'); + init(args?: RunAllArgs) { + this.runAll('init', args); } /* @@ -34,7 +43,7 @@ export class Hub { * * TODO: Some of methods may accepts args, with this implementation we only limit to those one without any argument. */ - runAll(action: string): RunAllResult[] { + runAll(action: string, args?: RunAllArgs): RunAllResult[] { const output: RunAllResult[] = []; // run action on all providers eagerConnect, disconnect @@ -52,7 +61,10 @@ export class Hub { if (typeof providerMethod === 'function') { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-next-line - providerOutput.provider = providerMethod.call(provider); + providerOutput.provider = providerMethod.call( + provider, + args?.[provider.id]?.provider + ); } // Namespace instances can have their own `action` as well. we will call them as well. @@ -62,7 +74,9 @@ export class Hub { // @ts-ignore-next-line const namespaceMethod = namespace[action]; if (typeof namespaceMethod === 'function') { - const result = namespaceMethod(); + const result = namespaceMethod( + args?.[provider.id]?.namespaces?.[namespace.namespaceId] + ); providerOutput.namespaces.push(result); } } diff --git a/wallets/core/src/hub/provider/provider.ts b/wallets/core/src/hub/provider/provider.ts index f8a47382b5..f1b2b71dbb 100644 --- a/wallets/core/src/hub/provider/provider.ts +++ b/wallets/core/src/hub/provider/provider.ts @@ -68,14 +68,14 @@ export class Provider { * provider.init() * ``` */ - public init(): void { + public init(args?: unknown): void { if (this.#initiated) { return; } const definedInitByUser = this.#extendInternalActions.init; if (definedInitByUser) { - definedInitByUser(this.#context()); + definedInitByUser(this.#context(), args); } this.#initiated = true; diff --git a/wallets/core/src/hub/provider/types.ts b/wallets/core/src/hub/provider/types.ts index e77f3ed52a..5e375f9234 100644 --- a/wallets/core/src/hub/provider/types.ts +++ b/wallets/core/src/hub/provider/types.ts @@ -6,6 +6,7 @@ import type { EvmActions } from '../../namespaces/evm/mod.js'; import type { SolanaActions } from '../../namespaces/solana/mod.js'; import type { StarknetActions } from '../../namespaces/starknet/types.js'; import type { SuiActions } from '../../namespaces/sui/mod.js'; +import type { TonActions } from '../../namespaces/ton/types.js'; import type { TronActions } from '../../namespaces/tron/types.js'; import type { UtxoActions } from '../../namespaces/utxo/mod.js'; import type { XRPLActions } from '../../namespaces/xrpl/mod.js'; @@ -38,6 +39,7 @@ export interface CommonNamespaces { tron: TronActions; starknet: StarknetActions; xrpl: XRPLActions; + ton: TonActions; } export type CommonNamespaceKeys = Prettify; diff --git a/wallets/core/src/namespaces/ton/builders.ts b/wallets/core/src/namespaces/ton/builders.ts new file mode 100644 index 0000000000..c6bb25ecba --- /dev/null +++ b/wallets/core/src/namespaces/ton/builders.ts @@ -0,0 +1,15 @@ +import type { TonActions } from './types.js'; + +import { ActionBuilder } from '../../builders/action.js'; +import { intoConnectionFinished } from '../common/after.js'; +import { connectAndUpdateStateForSingleNetwork } from '../common/and.js'; +import { intoConnecting } from '../common/before.js'; + +export const connect = () => + new ActionBuilder('connect') + .and(connectAndUpdateStateForSingleNetwork) + .before(intoConnecting) + .after(intoConnectionFinished); + +export const canEagerConnect = () => + new ActionBuilder('canEagerConnect'); diff --git a/wallets/core/src/namespaces/ton/constants.ts b/wallets/core/src/namespaces/ton/constants.ts new file mode 100644 index 0000000000..992caba81d --- /dev/null +++ b/wallets/core/src/namespaces/ton/constants.ts @@ -0,0 +1,2 @@ +export const CAIP_NAMESPACE = 'tvm'; +export const CAIP_TON_CHAIN_ID = '-239'; diff --git a/wallets/core/src/namespaces/ton/mod.ts b/wallets/core/src/namespaces/ton/mod.ts new file mode 100644 index 0000000000..e70919c661 --- /dev/null +++ b/wallets/core/src/namespaces/ton/mod.ts @@ -0,0 +1,5 @@ +export * as utils from './utils.js'; +export * as builders from './builders.js'; + +export type { ProviderAPI, TonActions } from './types.js'; +export { CAIP_NAMESPACE, CAIP_TON_CHAIN_ID } from './constants.js'; diff --git a/wallets/core/src/namespaces/ton/types.ts b/wallets/core/src/namespaces/ton/types.ts new file mode 100644 index 0000000000..ad54809a32 --- /dev/null +++ b/wallets/core/src/namespaces/ton/types.ts @@ -0,0 +1,15 @@ +import type { Accounts } from '../../types/accounts.js'; +import type { + AutoImplementedActionsByRecommended, + CommonActions, +} from '../common/types.js'; + +export interface TonActions + extends AutoImplementedActionsByRecommended, + CommonActions { + connect: () => Promise; + canEagerConnect: () => Promise; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ProviderAPI = Record; diff --git a/wallets/core/src/namespaces/ton/utils.ts b/wallets/core/src/namespaces/ton/utils.ts new file mode 100644 index 0000000000..33802bba83 --- /dev/null +++ b/wallets/core/src/namespaces/ton/utils.ts @@ -0,0 +1,18 @@ +import type { CaipAccount } from '../../types/accounts.js'; + +import { AccountId } from 'caip'; + +import { CAIP_NAMESPACE, CAIP_TON_CHAIN_ID } from './constants.js'; + +export function formatAccountsToCAIP(accounts: string[]) { + return accounts.map( + (account) => + AccountId.format({ + address: account.toString(), + chainId: { + namespace: CAIP_NAMESPACE, + reference: CAIP_TON_CHAIN_ID, + }, + }) as CaipAccount + ); +} diff --git a/wallets/provider-all/src/index.ts b/wallets/provider-all/src/index.ts index 56a5819569..bcfc2dc3af 100644 --- a/wallets/provider-all/src/index.ts +++ b/wallets/provider-all/src/index.ts @@ -30,7 +30,7 @@ import { versions as solflare } from '@rango-dev/provider-solflare'; import { versions as taho } from '@rango-dev/provider-taho'; import { versions as tokenPocket } from '@rango-dev/provider-tokenpocket'; import { versions as tomo } from '@rango-dev/provider-tomo'; -import * as tonconnect from '@rango-dev/provider-tonconnect'; +import { versions as tonconnect } from '@rango-dev/provider-tonconnect'; import * as trezor from '@rango-dev/provider-trezor'; import { versions as tronLink } from '@rango-dev/provider-tron-link'; import { versions as trustwallet } from '@rango-dev/provider-trustwallet'; @@ -84,23 +84,12 @@ export const allProviders = ( } } - if ( - !isWalletExcluded(providers, { - type: WalletTypes.TON_CONNECT, - name: 'tonconnect', - }) - ) { - if (!!options?.tonConnect?.manifestUrl) { - tonconnect.init(options.tonConnect); - } - } - return [ lazyProvider(legacyProviderImportsToVersionsInterface(safe)), lazyProvider(legacyProviderImportsToVersionsInterface(defaultInjected)), metamask, lazyProvider(legacyProviderImportsToVersionsInterface(walletconnect2)), - lazyProvider(legacyProviderImportsToVersionsInterface(tonconnect)), + tonconnect, keplr, phantom, ready, diff --git a/wallets/provider-tonconnect/package.json b/wallets/provider-tonconnect/package.json index a4dda814d9..98935fcfdf 100644 --- a/wallets/provider-tonconnect/package.json +++ b/wallets/provider-tonconnect/package.json @@ -3,21 +3,18 @@ "version": "0.20.1-next.1", "license": "MIT", "type": "module", - "source": "./src/index.ts", - "main": "./dist/index.js", + "source": "./src/mod.ts", + "main": "./dist/mod.js", "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } + ".": "./dist/mod.js" }, - "typings": "dist/index.d.ts", + "typings": "dist/mod.d.ts", "files": [ "dist", "src" ], "scripts": { - "build": "node ../../scripts/build/command.mjs --path wallets/provider-tonconnect", + "build": "node ../../scripts/build/command.mjs --path wallets/provider-tonconnect --inputs src/mod.ts", "ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json", "clean": "rimraf dist", "format": "prettier --write '{.,src}/**/*.{ts,tsx}'", @@ -33,4 +30,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/wallets/provider-tonconnect/readme.md b/wallets/provider-tonconnect/readme.md index 5da1c55d43..bd65e6ab28 100644 --- a/wallets/provider-tonconnect/readme.md +++ b/wallets/provider-tonconnect/readme.md @@ -1,3 +1,29 @@ # @rango-dev/provider-tonconnect +TonConnect Wallet integration for hub. +[Homepage](https://ton.org/) | [Docs](https://docs.ton.org/ecosystem/ton-connect/overview) -TonConnect \ No newline at end of file +More about implementation status can be found [here](../readme.md). + +## Implementation notes/limitations + +#### ⚠️ Initialization +You should provide TonConnect configs in configs.walletOptions[WalletTypes.TON_CONNECT] (which equals configs.walletOptions.tonconnect) + +### Feature + +#### ❌ Switch Account + +Ton wallets don't emit account change events: https://github.com/ton-blockchain/ton-connect/blob/main/wallet-guidelines.md + +#### ⚠️ Disconnect + +Some Ton wallets like **Tonkeeper** don't emit disconnect events + +#### ⚠️ Init + +We set **'installed'** to `true` on initialization even if we can't initialize the TonConnect instance. +Instead, we throw an error when the user tries to connect and we haven't initialized the TonConnect instance. + +--- + +More wallet information can be found in [readme.md](../readme.md). \ No newline at end of file diff --git a/wallets/provider-tonconnect/src/actions/ton.ts b/wallets/provider-tonconnect/src/actions/ton.ts new file mode 100644 index 0000000000..9a3993e049 --- /dev/null +++ b/wallets/provider-tonconnect/src/actions/ton.ts @@ -0,0 +1,56 @@ +import type { Context, FunctionWithContext } from '@rango-dev/wallets-core'; +import type { TonConnectUI } from '@tonconnect/ui'; + +import { actions as commonActions } from '@rango-dev/wallets-core/namespaces/common'; +import { + type TonActions, + type ProviderAPI as TonProviderAPI, + utils, +} from '@rango-dev/wallets-core/namespaces/ton'; + +import { tonConnect, waitForConnection } from '../utils.js'; + +export function connect( + getInstance: () => TonConnectUI +): FunctionWithContext { + return async () => { + const tonInstance = getInstance(); + const connectionRestored = await tonInstance.connectionRestored; + const { toUserFriendlyAddress } = tonConnect.getModule(); + let userFriendlyAddress: string; + + if (connectionRestored && tonInstance.account?.address) { + userFriendlyAddress = toUserFriendlyAddress(tonInstance.account.address); + } else { + await tonInstance.openModal(); + const result = await waitForConnection(tonInstance); + userFriendlyAddress = toUserFriendlyAddress(result); + } + + return utils.formatAccountsToCAIP([userFriendlyAddress]); + }; +} + +export function disconnect( + getInstance: () => TonProviderAPI +): FunctionWithContext { + return async (context) => { + const tonInstance = getInstance(); + if (tonInstance.connected) { + await tonInstance.disconnect(); + } + commonActions.disconnect(context); + }; +} + +export function canEagerConnect( + getInstance: () => TonProviderAPI +): FunctionWithContext { + return async () => { + const tonConnectUI = getInstance() as TonConnectUI; + const connectionRestored = await tonConnectUI.connectionRestored; + return connectionRestored; + }; +} + +export const tonActions = { connect, disconnect, canEagerConnect }; diff --git a/wallets/provider-tonconnect/src/constants.ts b/wallets/provider-tonconnect/src/constants.ts new file mode 100644 index 0000000000..1d536f75d7 --- /dev/null +++ b/wallets/provider-tonconnect/src/constants.ts @@ -0,0 +1,38 @@ +import type { ProviderMetadata } from '@rango-dev/wallets-core'; + +import { WalletTypes } from '@rango-dev/wallets-shared'; +import { type BlockchainMeta, tonBlockchain } from 'rango-types'; + +import getSigners from './signer.js'; +import { getInstanceOrThrow } from './utils.js'; + +export const WALLET_ID = WalletTypes.TON_CONNECT; + +export const metadata: ProviderMetadata = { + name: 'TON Connect', + icon: 'https://raw.githubusercontent.com/rango-exchange/assets/7fb19ed5d5019b4d6a41ce91b39cde64f86af4c6/wallets/tonconnect/icon.svg', + extensions: {}, + properties: [ + { + name: 'namespaces', + value: { + selection: 'single', + data: [ + { + label: 'Ton', + value: 'Ton', + id: 'TON', + getSupportedChains: (allBlockchains: BlockchainMeta[]) => + tonBlockchain(allBlockchains), + }, + ], + }, + }, + { + name: 'signers', + value: { + getSigners: async () => getSigners(getInstanceOrThrow()), + }, + }, + ], +}; diff --git a/wallets/provider-tonconnect/src/helpers.ts b/wallets/provider-tonconnect/src/helpers.ts deleted file mode 100644 index f5e5913447..0000000000 --- a/wallets/provider-tonconnect/src/helpers.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { TonConnectUI } from '@tonconnect/ui'; - -import { dynamicImportWithRefinedError } from '@rango-dev/wallets-shared'; - -export async function getTonConnectUIModule() { - const tonConnectUI = await dynamicImportWithRefinedError( - async () => await import('@tonconnect/ui') - ); - return tonConnectUI; -} - -export async function getTonCoreModule() { - const tonCore = await dynamicImportWithRefinedError( - async () => await import('@ton/core') - ); - return tonCore; -} - -export async function waitForConnection( - tonConnectUI: TonConnectUI -): Promise { - return new Promise((resolve, reject) => { - const unsubscribeStatusChange = tonConnectUI.onStatusChange( - (state) => { - const walletConnected = !!state?.account.address; - - if (walletConnected) { - unsubscribeStatusChange(); - resolve(state.account.address); - } - }, - (error) => { - unsubscribeStatusChange(); - reject(error); - } - ); - - const unsubscribeModalStateChange = tonConnectUI.onModalStateChange( - (modalState) => { - if (modalState.closeReason === 'action-cancelled') { - unsubscribeStatusChange(); - unsubscribeModalStateChange(); - reject(new Error('The action was canceled by the user')); - } - } - ); - }); -} - -export async function parseAddress(rawAddress: string): Promise { - const tonCore = await getTonCoreModule(); - return tonCore.Address.parse(rawAddress).toString({ bounceable: false }); -} diff --git a/wallets/provider-tonconnect/src/hooks/ton.ts b/wallets/provider-tonconnect/src/hooks/ton.ts new file mode 100644 index 0000000000..f45b13aef8 --- /dev/null +++ b/wallets/provider-tonconnect/src/hooks/ton.ts @@ -0,0 +1,47 @@ +import type { + AnyFunction, + Subscriber, + SubscriberCleanUp, +} from '@rango-dev/wallets-core'; +import type { TonActions } from '@rango-dev/wallets-core/namespaces/ton'; +import type { ITonConnect, TonConnectUI } from '@tonconnect/ui'; + +function getDisconnectSubscriber( + instance: () => TonConnectUI +): [Subscriber, SubscriberCleanUp] { + let eventCallback: AnyFunction; + let unsubscribe: ReturnType; + + // subscriber can be passed to `or`, it will get the error and should rethrow error to pass the error to next `or` or throw error. + return [ + (context, err) => { + const tonInstance = instance(); + + if (!tonInstance) { + throw new Error( + 'Trying to subscribe to your Ton wallet, but seems its instance is not available.' + ); + } + + eventCallback = (event) => { + if (!event.payload) { + context.action('disconnect'); + } + }; + + unsubscribe = tonInstance.onStatusChange(eventCallback); + + if (err instanceof Error) { + throw err; + } + }, + (_, err) => { + if (unsubscribe) { + unsubscribe(); + } + + return err; + }, + ]; +} +export const tonHooks = { getDisconnectSubscriber }; diff --git a/wallets/provider-tonconnect/src/index.ts b/wallets/provider-tonconnect/src/index.ts deleted file mode 100644 index 11726377a8..0000000000 --- a/wallets/provider-tonconnect/src/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Environments } from './types.js'; -import type { - CanEagerConnect, - CanSwitchNetwork, - Connect, - Disconnect, - GetInstance, - WalletInfo, -} from '@rango-dev/wallets-shared'; -import type { TonConnectUI } from '@tonconnect/ui'; -import type { BlockchainMeta, SignerFactory } from 'rango-types'; - -import { Networks, WalletTypes } from '@rango-dev/wallets-shared'; -import { tonBlockchain } from 'rango-types'; - -import { - getTonConnectUIModule, - parseAddress, - waitForConnection, -} from './helpers.js'; -import signer from './signer.js'; - -let envs: Environments = { - manifestUrl: '', -}; - -const WALLET = WalletTypes.TON_CONNECT; - -export const config = { - type: WALLET, - isAsyncInstance: true, - checkInstallation: false, -}; - -export type { Environments }; - -export const init = (environments: Environments) => { - envs = environments; -}; - -let instance: TonConnectUI | null = null; - -export const getInstance: GetInstance = async () => { - if (!instance) { - const { TonConnectUI } = await getTonConnectUIModule(); - instance = new TonConnectUI(envs); - } - return instance; -}; - -export const connect: Connect = async ({ instance }) => { - const tonConnectUI: TonConnectUI = instance; - const connectionRestored = await tonConnectUI.connectionRestored; - - if (connectionRestored && tonConnectUI.account?.address) { - const parsedAddress = await parseAddress(tonConnectUI.account.address); - return { accounts: [parsedAddress], chainId: Networks.TON }; - } - - await tonConnectUI.openModal(); - const result = await waitForConnection(tonConnectUI); - const parsedAddress = await parseAddress(result); - - return { - accounts: [parsedAddress], - chainId: Networks.TON, - }; -}; - -export const canEagerConnect: CanEagerConnect = async ({ instance }) => { - const tonConnectUI = instance as TonConnectUI; - const connectionRestored = await tonConnectUI.connectionRestored; - return connectionRestored; -}; - -export const canSwitchNetworkTo: CanSwitchNetwork = () => false; - -export const getSigners: (provider: TonConnectUI) => Promise = - signer; - -export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = ( - allBlockChains -) => { - const ton = tonBlockchain(allBlockChains); - return { - name: 'TON Connect', - img: 'https://raw.githubusercontent.com/rango-exchange/assets/7fb19ed5d5019b4d6a41ce91b39cde64f86af4c6/wallets/tonconnect/icon.svg', - installLink: '', - color: '#fff', - supportedChains: ton, - }; -}; - -export const disconnect: Disconnect = async ({ instance }) => { - const tonConnectUI = instance as TonConnectUI; - await tonConnectUI.disconnect(); -}; diff --git a/wallets/provider-tonconnect/src/mod.ts b/wallets/provider-tonconnect/src/mod.ts new file mode 100644 index 0000000000..00d33b9b02 --- /dev/null +++ b/wallets/provider-tonconnect/src/mod.ts @@ -0,0 +1,10 @@ +import { defineVersions } from '@rango-dev/wallets-core/utils'; + +import { buildProvider } from './provider.js'; + +export type { Environments } from './types.js'; + +const versions = () => + defineVersions().version('1.0.0', buildProvider()).build(); + +export { versions }; diff --git a/wallets/provider-tonconnect/src/namespaces/ton.ts b/wallets/provider-tonconnect/src/namespaces/ton.ts new file mode 100644 index 0000000000..ebc7f93bcd --- /dev/null +++ b/wallets/provider-tonconnect/src/namespaces/ton.ts @@ -0,0 +1,39 @@ +import type { TonActions } from '@rango-dev/wallets-core/namespaces/ton'; + +import { ActionBuilder, NamespaceBuilder } from '@rango-dev/wallets-core'; +import { standardizeAndThrowError } from '@rango-dev/wallets-core/namespaces/common'; +import { builders } from '@rango-dev/wallets-core/namespaces/ton'; + +import { tonActions } from '../actions/ton.js'; +import { WALLET_ID } from '../constants.js'; +import { tonHooks } from '../hooks/ton.js'; +import { tonConnect } from '../utils.js'; + +const [disconnectSubscriber, disconnectCleanUp] = + tonHooks.getDisconnectSubscriber(tonConnect.getInstance); + +const connect = builders + .connect() + .action(tonActions.connect(tonConnect.getInstance)) + .and(disconnectSubscriber) + .or(disconnectCleanUp) + .or(standardizeAndThrowError) + .build(); + +const canEagerConnect = builders + .canEagerConnect() + .action(tonActions.canEagerConnect(tonConnect.getInstance)) + .build(); + +const disconnect = new ActionBuilder('disconnect') + .action(tonActions.disconnect(tonConnect.getInstance)) + .after(disconnectCleanUp) + .build(); + +const ton = new NamespaceBuilder('Ton', WALLET_ID) + .action(connect) + .action(disconnect) + .action(canEagerConnect) + .build(); + +export { ton }; diff --git a/wallets/provider-tonconnect/src/provider.ts b/wallets/provider-tonconnect/src/provider.ts new file mode 100644 index 0000000000..b3023e783c --- /dev/null +++ b/wallets/provider-tonconnect/src/provider.ts @@ -0,0 +1,35 @@ +import type { Environments } from './types.js'; + +import { ProviderBuilder } from '@rango-dev/wallets-core'; + +import { metadata, WALLET_ID } from './constants.js'; +import { ton } from './namespaces/ton.js'; +import { tonConnect } from './utils.js'; + +const buildProvider = () => + new ProviderBuilder(WALLET_ID) + .init(async function (context, environments: Environments) { + const [, setState] = context.state(); + + async function initializeTon() { + try { + await tonConnect.initialize(environments); + console.debug('[ton] instance initialized.', context); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + /* empty */ + } finally { + /* + * we want to still show the TonConnect wallets + * but we'll throw an error that we couldn't initialize it when the users want to connect to it + */ + setState('installed', true); + } + } + void initializeTon(); + }) + .config('metadata', metadata) + .add('ton', ton) + .build(); + +export { buildProvider }; diff --git a/wallets/provider-tonconnect/src/signer.ts b/wallets/provider-tonconnect/src/signer.ts index b5a7c78f8d..60153b9cb0 100644 --- a/wallets/provider-tonconnect/src/signer.ts +++ b/wallets/provider-tonconnect/src/signer.ts @@ -1,16 +1,21 @@ -import type { TonConnectUI } from '@tonconnect/ui'; +import type { Provider } from './types.js'; import type { SignerFactory } from 'rango-types'; -import { dynamicImportWithRefinedError } from '@rango-dev/wallets-shared'; +import { + dynamicImportWithRefinedError, + getNetworkInstance, + Networks, +} from '@rango-dev/wallets-shared'; import { DefaultSignerFactory, TransactionType as TxType } from 'rango-types'; export default async function getSigners( - provider: TonConnectUI + provider: Provider ): Promise { + const tonProvider = getNetworkInstance(provider, Networks.TON); const signers = new DefaultSignerFactory(); const { CustomTonSigner } = await dynamicImportWithRefinedError( - async () => await import('./ton-signer.js') + async () => await import('./signers/ton.js') ); - signers.registerSigner(TxType.TON, new CustomTonSigner(provider)); + signers.registerSigner(TxType.TON, new CustomTonSigner(tonProvider)); return signers; } diff --git a/wallets/provider-tonconnect/src/ton-signer.ts b/wallets/provider-tonconnect/src/signers/ton.ts similarity index 100% rename from wallets/provider-tonconnect/src/ton-signer.ts rename to wallets/provider-tonconnect/src/signers/ton.ts diff --git a/wallets/provider-tonconnect/src/tonConnect.ts b/wallets/provider-tonconnect/src/tonConnect.ts new file mode 100644 index 0000000000..001aa40a67 --- /dev/null +++ b/wallets/provider-tonconnect/src/tonConnect.ts @@ -0,0 +1,43 @@ +import type { Environments } from './types.js'; +import type { TonConnectUI } from '@tonconnect/ui'; + +import { dynamicImportWithRefinedError } from '@rango-dev/wallets-shared'; + +export class TonConnect { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + #tonModule?: typeof import('@tonconnect/ui'); + #env?: Environments; + #tonConnectInstance?: TonConnectUI; + + async initialize(env?: Environments) { + if (!env) { + throw new Error('Environments are not set'); + } + this.#env = env; + if (this.#env) { + throw new Error('Environments are not set'); + } + + this.#tonModule = await dynamicImportWithRefinedError( + async () => await import('@tonconnect/ui') + ); + const { TonConnectUI } = this.#tonModule; + this.#tonConnectInstance = new TonConnectUI(this.#env); + } + + getInstance() { + if (!this.#tonConnectInstance) { + throw new Error( + "TonConnect instance isn't initialized. Please ensure you have provided the TonConnect config." + ); + } + return this.#tonConnectInstance; + } + + getModule() { + if (!this.#tonModule) { + throw new Error("Couldn't initialize the TonConnect module"); + } + return this.#tonModule; + } +} diff --git a/wallets/provider-tonconnect/src/types.ts b/wallets/provider-tonconnect/src/types.ts index 8403e975c1..70f308f532 100644 --- a/wallets/provider-tonconnect/src/types.ts +++ b/wallets/provider-tonconnect/src/types.ts @@ -1,3 +1,16 @@ -export interface Environments extends Record { - manifestUrl: string; -} +import type { LegacyNetworks } from '@rango-dev/wallets-core/legacy'; +import type { + TonConnectUI, + TonConnectUiOptionsWithManifest, +} from '@tonconnect/ui'; + +export type Environments = TonConnectUiOptionsWithManifest; + +type ProviderObject = { + [LegacyNetworks.TON]: TonConnectUI; +}; + +export type Provider = Map< + keyof ProviderObject, + ProviderObject[keyof ProviderObject] +>; diff --git a/wallets/provider-tonconnect/src/utils.ts b/wallets/provider-tonconnect/src/utils.ts new file mode 100644 index 0000000000..63793cfeb4 --- /dev/null +++ b/wallets/provider-tonconnect/src/utils.ts @@ -0,0 +1,50 @@ +import type { Provider } from './types.js'; +import type { TonConnectUI } from '@tonconnect/ui'; + +import { Networks } from '@rango-dev/wallets-shared'; + +import { TonConnect } from './tonConnect.js'; + +export const tonConnect = new TonConnect(); + +export function getInstanceOrThrow(): Provider { + const instance = tonConnect.getInstance(); + + const instances = new Map([[Networks.TON, instance]]); + return instances as Provider; +} + +export async function waitForConnection( + tonConnectUI: TonConnectUI +): Promise { + return new Promise((resolve, reject) => { + const unsubscribeStatusChange = tonConnectUI.onStatusChange( + (state) => { + const walletConnected = !!state?.account.address; + + if (walletConnected) { + unsubscribe(); + resolve(state.account.address); + } + }, + (error) => { + unsubscribe(); + reject(error); + } + ); + + const unsubscribeModalStateChange = tonConnectUI.onModalStateChange( + (modalState) => { + if (modalState.closeReason === 'action-cancelled') { + unsubscribe(); + reject(new Error('The action was canceled by the user')); + } + } + ); + + const unsubscribe = () => { + unsubscribeStatusChange(); + unsubscribeModalStateChange(); + }; + }); +} diff --git a/wallets/react/src/hub/helpers.ts b/wallets/react/src/hub/helpers.ts index 4278c9b03a..b94c5efbeb 100644 --- a/wallets/react/src/hub/helpers.ts +++ b/wallets/react/src/hub/helpers.ts @@ -8,6 +8,7 @@ import type { Result } from 'ts-results'; import { legacyFormatAddressWithNetwork as formatAddressWithNetwork } from '@rango-dev/wallets-core/legacy'; import { CAIP_NAMESPACE as CAIP_COSMOS_NAMESPACE } from '@rango-dev/wallets-core/namespaces/cosmos'; +import { CAIP_NAMESPACE as CAIP_TON_NAMESPACE } from '@rango-dev/wallets-core/namespaces/ton'; import { CAIP_TRON_CHAIN_ID } from '@rango-dev/wallets-core/namespaces/tron'; import { CAIP_BITCOIN_CHAIN_ID } from '@rango-dev/wallets-core/namespaces/utxo'; import { CAIP } from '@rango-dev/wallets-core/utils'; @@ -33,6 +34,9 @@ export function mapCaipNamespaceToLegacyNetworkName( return 'BTC'; } + if (chainId.namespace.toLowerCase() === CAIP_TON_NAMESPACE) { + return 'TON'; + } if (chainId.namespace.toLowerCase() === CAIP_COSMOS_NAMESPACE) { const network = getBlockChainNameFromId(chainId.reference, allBlockChains); if (!network) { diff --git a/wallets/react/src/hub/useHubAdapter.ts b/wallets/react/src/hub/useHubAdapter.ts index 18a5cec20a..d4e9773923 100644 --- a/wallets/react/src/hub/useHubAdapter.ts +++ b/wallets/react/src/hub/useHubAdapter.ts @@ -86,7 +86,7 @@ export function useHubAdapter(params: UseAdapterParams): ProviderContext { // Initialize instances useEffect(() => { const runOnInit = () => { - getHub().init(); + getHub().init(params.configs?.walletOptions); rerender((currentRender) => currentRender + 1); }; diff --git a/wallets/react/src/legacy/types.ts b/wallets/react/src/legacy/types.ts index 0f0b9c5f1c..1f79d2d976 100644 --- a/wallets/react/src/legacy/types.ts +++ b/wallets/react/src/legacy/types.ts @@ -80,6 +80,14 @@ export type ProviderProps = PropsWithChildren<{ providers: VersionedProviders[]; configs?: { wallets?: (WalletType | LegacyProviderInterface | Provider)[]; + walletOptions?: { + [key: WalletType]: { + provider?: unknown; + namespaces?: { + [namespaceId: string]: unknown; + }; + }; + }; }; }>; diff --git a/wallets/readme.md b/wallets/readme.md index c96356e691..bdbbbd117c 100644 --- a/wallets/readme.md +++ b/wallets/readme.md @@ -161,6 +161,7 @@ For better user experience, wallet provider tries to connect to a wallet only wh | [UniSat](provider-unisat/readme.md) | ❌ | ⚠️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | [Xverse](provider-xverse/readme.md) | ❌ | ⚠️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | [Tomo](provider-tomo/readme.md) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| [TonConnect](provider-tonconnect/readme.md) | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ## By Feature @@ -192,6 +193,7 @@ For better user experience, wallet provider tries to connect to a wallet only wh | Unisat | ✅ | 🚧 | ❌ | Injected | ❌ | | Xverse | ⚠️ | 🚧 | ✅ | Injected | ❌ | | Tomo | ✅ | ✅ | ✅ | Injected | ❌ | +| TonConnect | ❌ | ❌ | ✅ | TonConnect | ❌ | # Supported Wallets (Legacy) diff --git a/widget/embedded/src/containers/Wallets/Wallets.tsx b/widget/embedded/src/containers/Wallets/Wallets.tsx index 552526abed..ae82a2d170 100644 --- a/widget/embedded/src/containers/Wallets/Wallets.tsx +++ b/widget/embedded/src/containers/Wallets/Wallets.tsx @@ -9,6 +9,7 @@ import type { LegacyEventHandler } from '@rango-dev/wallets-core/legacy'; import type { PropsWithChildren } from 'react'; import { Provider } from '@rango-dev/wallets-react'; +import { WalletTypes } from '@rango-dev/wallets-shared'; import React, { createContext, useEffect, useMemo, useRef } from 'react'; import { useWalletProviders } from '../../hooks/useWalletProviders'; @@ -104,6 +105,11 @@ function Main(props: PropsWithChildren) { autoConnect={!!isActiveTab} configs={{ wallets: config.wallets, + walletOptions: { + [WalletTypes.TON_CONNECT]: { + provider: { manifestUrl: config.tonConnect?.manifestUrl }, + }, + }, }}> {props.children}