A Swift Package that provides everything you need to build a TON wallet on Apple platforms: wallet creation and import, balances, transfers, NFTs and jettons, TON Connect (dApp connections, transaction / sign-message / sign-data requests), live streaming updates, and DeFi (swap, staking, gasless).
The kit ships the official @tonconnect/walletkit JavaScript core embedded via JavaScriptCore
and exposes a fully typed, async/await Swift API on top of it — you never touch JS.
- Platforms: iOS 14+, macOS 11+
- Language: Swift 5 mode (Swift tools 6.2+)
- Installation
- Configuration
- Creating the kit
- Storage
- Wallets
- Reading balance & assets
- Sending TON
- TON Connect
- Streaming live updates
- DeFi: swap, staking, gasless
- Working with amounts & addresses
- Error handling
- Development
File ▸ Add Package Dependencies..., enter the repository URL:
https://github.com/ton-connect/kit-ios.git
then add the TONWalletKit product to your target.
// Package.swift dependencies: [ .package(url: "https://github.com/ton-connect/kit-ios.git", branch: "main") ], targets: [ .target( name: "YourApp", dependencies: ["TONWalletKit"] ) ]
import TONWalletKitEverything starts with a TONWalletKitConfiguration. It describes the networks you support, your
wallet's TON Connect manifest, where keys are stored, the TON Connect bridge, and which TON Connect
features your wallet implements.
import TONWalletKit let apiClientConfig = TONWalletKitConfiguration.APIClientConfiguration( key: "YOUR_TONCENTER_API_KEY" ) let configuration = TONWalletKitConfiguration( // One entry per network you want to support. networkConfigurations: [ TONWalletKitConfiguration.NetworkConfiguration( network: .mainnet, apiClient: .toncenter(apiClientConfig) ), TONWalletKitConfiguration.NetworkConfiguration( network: .testnet, apiClient: .toncenter(apiClientConfig) ), ], // Your TON Connect manifest — shown to dApps that connect to your wallet. walletManifest: TONWalletKitConfiguration.Manifest( name: "My TON Wallet", appName: "my_ton_wallet", imageUrl: "https://example.com/icon.png", aboutUrl: "https://example.com/about", universalLink: "https://example.com/ton-connect", bridgeUrl: "https://connect.ton.org/bridge" ), // Where private keys / sessions are persisted (see "Storage"). storage: .keychain, // TON Connect bridge. Pass `nil` if you don't need TON Connect. bridge: TONWalletKitConfiguration.Bridge( bridgeUrl: "https://connect.ton.org/bridge" ), // The TON Connect capabilities your wallet supports. features: [ TONSendTransactionFeature(maxMessages: 255), TONSignDataFeature(types: [.text, .binary, .cell]), TONEmbeddedRequestFeature(), ] )
Each NetworkConfiguration picks how that network talks to the chain:
// TON Center (key required): .init(network: .mainnet, apiClient: .toncenter(.init(key: "TONCENTER_KEY"))) // TON API: .init(network: .mainnet, apiClient: .tonApi(.init(key: "TONAPI_KEY"))) // A fully custom client conforming to `TONAPIClient`: .init(network: .mainnet, apiClient: myCustomClient)
APIClientConfiguration also accepts an optional url, timeout, disableNetworkSend, and
dnsResolver.
let kit = TONWalletKit(configuration: configuration) // Optional: warm up the embedded JS core ahead of time. try await kit.initialize()
initialize() is optional — every async method initializes the kit lazily on first use. Calling
it up front simply moves that one-time cost to a moment you control. kit.isInitialized tells you
the current state.
Hold on to a single, long-lived TONWalletKit instance for the lifetime of your app.
Keys and TON Connect sessions are persisted through the storage you pass in the configuration:
.memory // in-memory, lost on relaunch — good for tests/demos .keychain // persisted in the iOS/macOS Keychain (default, recommended) .custom(myStorage) // your own type conforming to `TONWalletKitStorage`
A custom backend implements the async TONWalletKitStorage protocol:
final class MyStorage: TONWalletKitStorage { func set(key: String, value: String) async throws { /* ... */ } func get(key: String) async throws -> String? { /* ... */ } func remove(key: String) async throws { /* ... */ } func clear() async throws { /* ... */ } }
A wallet is created in two steps: build a wallet adapter (key material + contract version), then
add it to the kit to get a usable TONWalletProtocol.
createWallet generates a fresh mnemonic and a v5r1 adapter in one call:
let result = try await kit.createWallet( parameters: TONV5R1WalletParameters(network: .mainnet, domain: nil) ) // IMPORTANT: back this mnemonic up securely and show it to the user once. let mnemonic: TONMnemonic = result.mnemonic let wallet = try await kit.add(walletAdapter: result.walletAdapter) print("New wallet:", wallet.address.value)
let mnemonic = TONMnemonic(string: "word1 word2 ... word24") // 12 or 24 words let signer = try await kit.signer(mnemonic: mnemonic) let adapter = try await kit.walletV5R1Adapter( signer: signer, parameters: TONV5R1WalletParameters(network: .mainnet, domain: nil) ) let wallet = try await kit.add(walletAdapter: adapter)
Use kit.walletV4R2Adapter(signer:parameters:) with TONV4R2WalletParameters for v4r2 contracts.
// From a raw 32-byte private key: let signer = try await kit.signer(privateKey: privateKeyData) // Or supply your own signer (e.g. a hardware/secure-enclave backed key): final class MySigner: TONWalletSignerProtocol { func sign(data: Data) async throws -> TONHex { /* ... */ } func publicKey() -> TONHex { /* ... */ } }
Either signer can then be passed to walletV5R1Adapter / walletV4R2Adapter.
let mnemonic = try await kit.generateMnemonic()
let wallets = try await kit.wallets() // [any TONWalletProtocol] let wallet = try await kit.wallet(id: walletID) try await kit.remove(walletId: walletID)
let address = wallet.address.value // user-friendly string let balance = try await wallet.balance() // TONBalance (nano-units) // Format nano-units into a human string: let formatter = TONBalanceFormatter() print("Balance:", formatter.string(from: balance) ?? "0", "TON") // Jettons (tokens) and NFTs: let jettons = try await wallet.jettons(limit: 50) let nfts = try await wallet.nfts(limit: 50) let usdtBalance = try await wallet.jettonBalance( jettonAddress: try TONUserFriendlyAddress(value: "EQ...jettonMaster") )
Build a transfer, turn it into a transaction, then send it:
let request = TONTransferRequest( transferAmount: TONBalanceFormatter().amount(from: "1.5")!, // 1.5 TON recipientAddress: try TONUserFriendlyAddress(value: "EQ...recipient"), comment: "Thanks!" ) let transaction = try await wallet.transferTONTransaction(request: request) // Optional: emulate before sending. let preview = try await wallet.preview(transactionRequest: transaction) let response = try await wallet.send(transactionRequest: transaction) print("Sent. Normalized hash:", response.normalizedHash.value)
You can also send a prepared transaction through the kit:
try await kit.send(transaction: transaction, from: wallet)
Jetton and NFT transfers follow the same shape via
wallet.transferJettonTransaction(request:) and wallet.transferNFTTransaction(request:).
Your wallet acts as the TON Connect wallet side: a dApp asks to connect, then to sign or send.
A tc://, universal, or deep link from a dApp is passed straight to connect:
try await kit.connect(url: "tc://connect?v=2&id=...")
The kit then delivers the resulting requests to your event handler.
import TONWalletKit final class WalletEventsHandler: TONBridgeEventsHandler { func handle(event: TONWalletKitEvent) throws { switch event { case .connectRequest(let request): // Show approval UI, then approve with the wallet the user chose: Task { try await request.approve(walletId: chosenWalletID) // or: try await request.reject(reason: "User cancelled") } case .transactionRequest(let request): Task { _ = try await request.approve() } // or request.reject(reason:) case .signMessageRequest(let request): Task { _ = try await request.approve() } case .signDataRequest(let request): Task { _ = try await request.approve() } case .disconnect(let event): print("dApp disconnected:", event) } } } let handler = WalletEventsHandler() try kit.add(eventsHandler: handler) // may be called before initialize(); it's queued
Each request exposes the dApp's intent on its event property (so you can render details for the
user) plus approve(...) / reject(reason:). Approving a connection can return an embedded
follow-up request (e.g. a transaction to sign immediately) — approve returns the next
TONWalletKitEvent? for that case.
Call kit.remove(eventsHandler:) when the handler is no longer needed.
Streaming pushes balance / jetton / transaction changes in real time over Combine publishers. Register a streaming provider once, connect, then subscribe.
import Combine let streaming = try await kit.streaming() // Register a provider for the network you want to watch (once): let provider = try await kit.streamingProvider( config: TONTonCenterStreamingProviderConfig( network: .mainnet, apiKey: "YOUR_STREAMING_API_KEY" ) ) try streaming.register(provider: provider) try streaming.connect() // Subscribe to live balance updates: let cancellable = streaming .balance(network: .mainnet, address: wallet.address.value) .sink( receiveCompletion: { completion in if case .failure(let error) = completion { print("stream error:", error) } }, receiveValue: { update in print("New balance:", update.balance) } )
streaming also offers .jettons(network:address:), .transactions(network:address:),
.updates(network:address:types:), and .connectionChange(network:). Updates are emitted on
change — seed your UI with the current wallet.balance() first, then let the stream keep it
fresh. TONTonApiStreamingProviderConfig is available as an alternative provider.
Each DeFi area is a manager you obtain from the kit, after registering the relevant provider:
// Swap let swap = try await kit.swap() let omniston = try await kit.omnistonSwapProvider(config: nil) try swap.register(provider: omniston) // let dedust = try await kit.dedustSwapProvider(config: nil) // quote / build a swap transaction via `swap.quote(...)` and `swap.swapTransaction(params:)` // Staking let staking = try await kit.staking() let stakers = try await kit.stakingProvider(config: TONTonStakersProviderConfig(/* ... */)) try staking.register(provider: stakers) // Gasless (pay fees in jettons) let gasless = try await kit.gasless() let gaslessProvider = try await kit.tonApiGaslessProvider(config: TONTonApiGaslessProviderConfig(/* ... */)) try gasless.register(provider: gaslessProvider) // Jettons metadata/utilities let jettons = try await kit.jettons()
-
TONTokenAmount/TONBalancestore value in nano-units (1 TON = 109 nanoTON):let oneTon = TONTokenAmount(nanoUnits: "1000000000")! let display = TONBalanceFormatter().string(from: oneTon) // "1" let parsed = TONBalanceFormatter().amount(from: "1.5") // TONTokenAmount?
-
TONUserFriendlyAddressvalidates on init and exposes.value:let address = try TONUserFriendlyAddress(value: "EQ...")
-
TONNetworkprovides.mainnetand.testnet, orTONNetwork(chainId:)for custom chains.
Every failure is a strongly-typed Swift error — the SDK never throws bare strings. The user-facing
ones conform to LocalizedError, so error.localizedDescription is presentable:
do { let wallet = try await kit.wallet(id: walletID) _ = try await wallet.balance() } catch let error as TONWalletKitError { // .notInitialized, .bridgeUnavailable, .streamingNetworkUnavailable, .bridgeRequestTimeout print("WalletKit error:", error.localizedDescription) } catch { print("Unexpected:", error.localizedDescription) }
Notable error types:
| Type | Raised when |
|---|---|
TONWalletKitError |
SDK lifecycle / bridge / streaming failures |
TONBase64ValidationError, TONHexValidationError |
invalid base64 / hex inputs |
JSValueConversionError |
a value can't be bridged to/from the JS core |
JSError |
an error surfaced from the embedded JS core |
The Swift API wraps a prebuilt JavaScript bundle (Resources/JS/walletkit-ios-bridge.mjs). You only
need this if you are developing the package itself and want to rebuild that bundle:
make js # build from the pinned walletkit source make js WALLETKIT_PATH=<local_path> # build from a local checkout
Run the test suite with the Xcode toolchain (which ships Swift Testing):
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcrun swift test