diff --git a/README.md b/README.md index 51b089ec..27275bd4 100644 --- a/README.md +++ b/README.md @@ -34,19 +34,24 @@ Keys are optional; features that require them are disabled or fall back to unaut Use [Scripts/rebuild-sdk.sh](Scripts/rebuild-sdk.sh) when you've made changes to the TypeScript bridge in the [walletkit](https://github.com/ton-blockchain/walletkit) repo and need to propagate them into the Android SDK. -The script: -1. Builds `@ton/walletkit` (TypeScript → CJS + ESM) and the `walletkit-android-bridge` Vite bundle +The script runs five steps: +1. Builds the `walletkit-android-bridge` Vite bundle (in the `kit` repo) 2. Copies the bundle into `dist-android/` -3. Builds the SDK AAR and installs the demo app +3. Regenerates the OpenAPI Kotlin models — **only with `--regen-models`** (skipped otherwise) +4. Builds the SDK AAR and copies it into the demo's `libs` +5. Installs the demo app on a connected device/emulator — **only with `--install-demo`** (skipped otherwise) -**Prerequisites:** `pnpm`, `npx`, Android SDK with a connected device or emulator. +**Prerequisites:** `pnpm`, `npx`, and the Android SDK. A connected device or emulator is needed only when passing `--install-demo`. **Setup:** Place the `kit` and `kit-android` repos as siblings in the same directory, or set `KIT_DIR` to the walletkit repo path. ```sh -# Standard usage +# Standard usage (builds the bundle + AAR; does NOT install the demo) ./Scripts/rebuild-sdk.sh +# Also build and install the demo app on a connected device/emulator +./Scripts/rebuild-sdk.sh --install-demo + # Also regenerate OpenAPI Kotlin models (requires openapi-generator) ./Scripts/rebuild-sdk.sh --regen-models diff --git a/TONWalletKit-Android/README.md b/TONWalletKit-Android/README.md index 28bc76fe..94f9fe2b 100644 --- a/TONWalletKit-Android/README.md +++ b/TONWalletKit-Android/README.md @@ -79,29 +79,32 @@ val adapter = kit.createV5R1Adapter( ) // Step 3: Add wallet -val wallet = kit.addWallet(adapter.adapterId) +val wallet = kit.addWallet(adapter) ``` #### Read wallet address and balance: ```kotlin -val address = wallet.address +val address = wallet.address(testnet = true) val balance = wallet.balance() -println("Address: ${address ?: ""}") -println("Balance: ${balance ?: ""}") -```#### Add wallet with external signer (e.g., hardware wallet): +println("Address: $address") +println("Balance: $balance") +``` + +#### Add wallet with external signer (e.g., hardware wallet): ```kotlin import io.ton.walletkit.model.WalletSigner -import io.ton.walletkit.model.KeyPair +import io.ton.walletkit.model.TONHex // Create custom signer implementation val customSigner = object : WalletSigner { - override val publicKey: ByteArray = // ... your public key from hardware wallet - - override suspend fun sign(data: ByteArray): ByteArray { - // Forward to external signing service (e.g., hardware wallet) - // Show confirmation dialog and get signature - return signature + // Return the public key from your hardware wallet / external signer. + override fun publicKey(): TONHex = TONHex.fromData(hardwareWalletPublicKey) + + override suspend fun sign(data: ByteArray): TONHex { + // Forward `data` to your external signer, show a confirmation + // dialog if needed, and return the resulting signature. + return TONHex.fromData(externalSignature) } } @@ -115,7 +118,7 @@ val adapter = kit.createV4R2Adapter( ) // Step 3: Add wallet -val wallet = kit.addWallet(adapter.adapterId) +val wallet = kit.addWallet(adapter) ``` #### Get all wallets: @@ -125,7 +128,7 @@ val wallets = kit.getWallets() #### Remove wallet: ```kotlin -kit.removeWallet(wallet.address) +kit.removeWallet(wallet.identifier()) ``` #### Clean up when done: diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWallet.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWallet.kt index 4bc9df41..48dde359 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWallet.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWallet.kt @@ -26,39 +26,58 @@ import io.ton.walletkit.model.ITONWalletAdapter import io.ton.walletkit.model.TONBalance import io.ton.walletkit.model.TONUserFriendlyAddress +/** + * A TON wallet, obtained from [ITONWalletKit.addWallet]. Extends [ITONWalletAdapter]; suspend + * members may throw [WalletKitBridgeException]. + */ interface ITONWallet : ITONWalletAdapter { + /** Get the wallet balance, in nano-TON. */ suspend fun balance(): TONBalance + /** Create a TON transfer transaction, ready for [send] or [preview]. */ suspend fun transferTONTransaction(request: TONTransferRequest): TONTransactionRequest + /** Create a multi-recipient TON transfer transaction. */ suspend fun transferTONTransaction(requests: List): TONTransactionRequest + /** Emulate a transaction to preview its outcome and estimated fees. */ suspend fun preview( transactionRequest: TONTransactionRequest, options: TONTransactionPreviewOptions? = null, ): TONTransactionEmulatedPreview + /** Sign and broadcast a transaction. */ suspend fun send(transactionRequest: TONTransactionRequest): TONSendTransactionResponse + /** Create an NFT transfer transaction. */ suspend fun transferNFTTransaction(request: TONNFTTransferRequest): TONTransactionRequest + /** Create an NFT transfer transaction from raw parameters. */ suspend fun transferNFTTransaction(request: TONNFTRawTransferRequest): TONTransactionRequest + /** Get NFTs owned by this wallet. */ suspend fun nfts(request: TONNFTsRequest): TONNFTsResponse + /** Get a single NFT by address, or null if none. */ suspend fun nft(address: TONUserFriendlyAddress): TONNFT? + /** Get this wallet's balance of a specific jetton. */ suspend fun jettonBalance(jettonAddress: TONUserFriendlyAddress): TONBalance + /** Resolve this wallet's jetton-wallet address for a jetton. */ suspend fun jettonWalletAddress(jettonAddress: TONUserFriendlyAddress): TONUserFriendlyAddress + /** Create a jetton transfer transaction. */ suspend fun transferJettonTransaction(request: TONJettonsTransferRequest): TONTransactionRequest + /** Get jettons owned by this wallet. */ suspend fun jettons(request: TONJettonsRequest? = null): TONJettonsResponse } +/** Fetch the first [limit] NFTs owned by this wallet. */ suspend fun ITONWallet.nfts(limit: Int): TONNFTsResponse = nfts(TONNFTsRequest(pagination = TONPagination(limit = limit))) +/** Fetch the first [limit] jettons owned by this wallet. */ suspend fun ITONWallet.jettons(limit: Int): TONJettonsResponse = jettons(TONJettonsRequest(pagination = TONPagination(limit = limit))) diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWalletKit.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWalletKit.kt index bc5bda9b..612430de 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWalletKit.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/ITONWalletKit.kt @@ -219,7 +219,7 @@ interface ITONWalletKit { /** * Access the staking manager for registering providers and performing staking operations. */ - fun staking(): ITONStakingManager + suspend fun staking(): ITONStakingManager /** * Create a TonStakers staking provider. @@ -238,7 +238,7 @@ interface ITONWalletKit { /** * Get the streaming manager. */ - fun streaming(): ITONStreamingManager + suspend fun streaming(): ITONStreamingManager suspend fun createStreamingProvider( config: TONTonCenterStreamingProviderConfig, diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/client/TONAPIClient.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/client/TONAPIClient.kt index 5e1c5a66..ac36526c 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/client/TONAPIClient.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/client/TONAPIClient.kt @@ -35,20 +35,19 @@ import io.ton.walletkit.model.TONTokenAmount import io.ton.walletkit.model.TONUserFriendlyAddress /** - * Interface for custom API client implementations. - * - * Implement this interface to provide custom TON blockchain API access. Covers the three - * responsibilities a wallet kit needs from a user-supplied client: send signed BOCs, run - * contract get methods, and read masterchain info. Network identity is established at - * registration time via - * [io.ton.walletkit.config.TONWalletKitConfiguration.NetworkConfiguration], not on the - * client itself. + * Interface for custom TON API client implementations: send signed BoCs, run contract get-methods, + * read chain state. Network identity is set at registration via + * [io.ton.walletkit.config.TONWalletKitConfiguration.NetworkConfiguration], not here. Suspend + * methods may throw WalletKitBridgeException when the SDK-backed client is used. */ interface TONAPIClient { + /** The network this client talks to. */ fun network(): TONNetwork + /** Broadcast a signed external message (base64 BoC); returns its hash. */ suspend fun sendBoc(boc: TONBase64): String + /** Run a contract get-method and return its result stack. */ suspend fun runGetMethod( address: TONUserFriendlyAddress, method: String, @@ -56,32 +55,41 @@ interface TONAPIClient { seqno: UInt? = null, ): TONGetMethodResult + /** Get an account's balance, in nano-TON. */ suspend fun getBalance( address: TONUserFriendlyAddress, seqno: UInt? = null, ): TONTokenAmount + /** Get the latest masterchain info. */ suspend fun getMasterchainInfo(): TONMasterchainInfo + /** List NFT items by their contract addresses. */ suspend fun nftItemsByAddress(request: TONNFTsRequest): TONNFTsResponse + /** List NFT items owned by an account. */ suspend fun nftItemsByOwner(request: TONUserNFTsRequest): TONNFTsResponse + /** Emulate an external message and return its predicted result. */ suspend fun fetchEmulation( messageBoc: TONBase64, ignoreSignature: Boolean = false, ): TONEmulationResult + /** Get an account's on-chain state. */ suspend fun accountState( address: TONUserFriendlyAddress, seqno: UInt? = null, ): TONAccountState + /** Batch [accountState] for several accounts. */ suspend fun accountStates( addresses: List, ): Map + /** Resolve a TON DNS domain to its wallet address, or null. */ suspend fun resolveDnsWallet(domain: String): String? + /** Reverse-resolve a wallet address to its TON DNS domain, or null. */ suspend fun backResolveDnsWallet(address: TONUserFriendlyAddress): String? } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/ITONWalletAdapter.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/ITONWalletAdapter.kt index 1a1c46af..c78a1719 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/ITONWalletAdapter.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/ITONWalletAdapter.kt @@ -28,39 +28,55 @@ import io.ton.walletkit.api.generated.TONTransactionRequest import io.ton.walletkit.client.TONAPIClient import io.ton.walletkit.config.TONWalletKitConfiguration +/** + * The account-level signing contract for a wallet. Implemented by ITONWallet, or directly to back a + * custom/hardware wallet for `ITONWalletKit.addWallet`. Suspend members may throw + * WalletKitBridgeException. On the `signed*` members, `fakeSignature = true` produces a placeholder + * signature for emulation. + */ interface ITONWalletAdapter { + /** Stable wallet/adapter identifier (also the wallet id). */ fun identifier(): String + /** The wallet's Ed25519 public key. */ suspend fun publicKey(): TONHex + /** The network this adapter operates on. */ fun network(): TONNetwork + /** The API client bound to this adapter's network. */ fun client(): TONAPIClient + /** The wallet's user-friendly address ([testnet] selects the testnet encoding). */ fun address(testnet: Boolean = false): TONUserFriendlyAddress - /** State init (base64 BOC) for contract deployment. */ + /** State init (base64 BOC) for first-use contract deployment. */ suspend fun stateInit(): TONBase64 + /** Sign a transaction into a broadcastable external-message BoC. */ suspend fun signedSendTransaction( input: TONTransactionRequest, fakeSignature: Boolean? = null, ): TONBase64 + /** Sign a transaction as an internal sign-message BoC (gasless relay flows). */ suspend fun signedSignMessage( input: TONTransactionRequest, fakeSignature: Boolean? = null, ): TONBase64 + /** Sign prepared data (TON Connect signData); returns hex. */ suspend fun signedSignData( input: TONPreparedSignData, fakeSignature: Boolean? = null, ): TONHex + /** Sign a TON Proof challenge; returns hex. */ suspend fun signedTonProof( input: TONProofMessage, fakeSignature: Boolean? = null, ): TONHex + /** The wallet features this adapter supports, or null for SDK defaults. */ fun supportedFeatures(): List? = null } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONMnemonic.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONMnemonic.kt index 578c2d7d..2d86d864 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONMnemonic.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONMnemonic.kt @@ -116,6 +116,14 @@ class TONMnemonic private constructor( */ fun toPhrase(): String = words.filter { it.isNotEmpty() }.joinToString(" ") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TONMnemonic) return false + return words == other.words + } + + override fun hashCode(): Int = words.hashCode() + override fun toString(): String = toPhrase() } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/RequestHandler.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/RequestHandler.kt index d6b04a45..80942dce 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/RequestHandler.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/RequestHandler.kt @@ -52,21 +52,21 @@ interface RequestHandler { suspend fun approveTransaction( event: TONSendTransactionRequestEvent, response: TONSendTransactionApprovalResponse? = null, - ) + ): TONSendTransactionApprovalResponse suspend fun rejectTransaction(event: TONSendTransactionRequestEvent, reason: String?, errorCode: Int?) suspend fun approveSignData( event: TONSignDataRequestEvent, response: TONSignDataApprovalResponse? = null, - ) + ): TONSignDataApprovalResponse suspend fun rejectSignData(event: TONSignDataRequestEvent, reason: String?, errorCode: Int?) suspend fun approveSignMessage( event: TONSignMessageRequestEvent, response: TONSignMessageApprovalResponse? = null, - ) + ): TONSignMessageApprovalResponse suspend fun rejectSignMessage(event: TONSignMessageRequestEvent, reason: String?, errorCode: Int?) } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletConnectionRequest.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletConnectionRequest.kt index c29022e0..b9336043 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletConnectionRequest.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletConnectionRequest.kt @@ -64,6 +64,7 @@ class TONWalletConnectionRequest( } } + /** Reject the connection request. */ suspend fun reject(reason: String? = null, errorCode: Int? = null) { handler.rejectConnect(event, reason, errorCode) } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignDataRequest.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignDataRequest.kt index dc2d2e41..65ebfa9f 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignDataRequest.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignDataRequest.kt @@ -35,10 +35,11 @@ class TONWalletSignDataRequest( val event: TONSignDataRequestEvent, private val handler: RequestHandler, ) { - suspend fun approve(response: TONSignDataApprovalResponse? = null) { + /** Approve and sign the data; returns the finalized approval response (signature for the dApp). */ + suspend fun approve(response: TONSignDataApprovalResponse? = null): TONSignDataApprovalResponse = handler.approveSignData(event, response) - } + /** Reject the sign-data request. */ suspend fun reject(reason: String? = null, errorCode: Int? = null) { handler.rejectSignData(event, reason, errorCode) } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignMessageRequest.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignMessageRequest.kt index 9d714a5a..7d8c3ac0 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignMessageRequest.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletSignMessageRequest.kt @@ -36,10 +36,11 @@ class TONWalletSignMessageRequest( val event: TONSignMessageRequestEvent, private val handler: RequestHandler, ) { - suspend fun approve(response: TONSignMessageApprovalResponse? = null) { + /** Approve and sign the message; returns the finalized approval response (signed BoC for the dApp). */ + suspend fun approve(response: TONSignMessageApprovalResponse? = null): TONSignMessageApprovalResponse = handler.approveSignMessage(event, response) - } + /** Reject the sign-message request. */ suspend fun reject(reason: String? = null, errorCode: Int? = null) { handler.rejectSignMessage(event, reason, errorCode) } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletTransactionRequest.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletTransactionRequest.kt index da6708ae..c9f5bbda 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletTransactionRequest.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/request/TONWalletTransactionRequest.kt @@ -35,10 +35,11 @@ class TONWalletTransactionRequest( val event: TONSendTransactionRequestEvent, private val handler: RequestHandler, ) { - suspend fun approve(response: TONSendTransactionApprovalResponse? = null) { + /** Approve, sign, and broadcast the transaction; returns the finalized approval response. */ + suspend fun approve(response: TONSendTransactionApprovalResponse? = null): TONSendTransactionApprovalResponse = handler.approveTransaction(event, response) - } + /** Reject the transaction request. */ suspend fun reject(reason: String? = null, errorCode: Int? = null) { handler.rejectTransaction(event, reason, errorCode) } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingManager.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingManager.kt index f86bf360..7c3b6f78 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingManager.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingManager.kt @@ -29,22 +29,32 @@ import io.ton.walletkit.api.generated.TONStreamingWatchType import io.ton.walletkit.api.generated.TONTransactionsUpdate import kotlinx.coroutines.flow.Flow +/** Real-time subscriptions (balance, transactions, jettons) backed by registered [ITONStreamingProvider]s, delivered as cold [Flow]s. */ interface ITONStreamingManager { + /** Whether a streaming provider is registered for [network]. */ suspend fun hasProvider(network: TONNetwork): Boolean + /** Register a streaming provider; its [ITONStreamingProvider.network] selects the network. */ suspend fun registerProvider(provider: ITONStreamingProvider) + /** Open the streaming connection(s). */ suspend fun connect() + /** Close the streaming connection(s). */ suspend fun disconnect() + /** Connection-state changes for [network] (true = connected). */ fun connectionChange(network: TONNetwork): Flow + /** Watch an account's balance on [network]. */ fun balance(network: TONNetwork, address: String): Flow + /** Watch an account's transactions on [network]. */ fun transactions(network: TONNetwork, address: String): Flow + /** Watch an account's jetton balances on [network]. */ fun jettons(network: TONNetwork, address: String): Flow + /** Watch the given [types] for an account on [network] in one stream. */ fun updates(network: TONNetwork, address: String, types: List): Flow } diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingProvider.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingProvider.kt index 2385c1f3..abfd02b9 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingProvider.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/streaming/ITONStreamingProvider.kt @@ -28,20 +28,32 @@ import io.ton.walletkit.api.generated.TONNetwork import io.ton.walletkit.api.generated.TONTransactionsUpdate import kotlinx.coroutines.flow.Flow +/** A streaming data source for one [network]; register with [ITONStreamingManager.registerProvider]. Built-ins come from `ITONWalletKit.createStreamingProvider`. */ interface ITONStreamingProvider { + /** Stable provider identifier. */ val identifier: String + + /** Always [TONProviderType.Streaming]. */ val type: TONProviderType get() = TONProviderType.Streaming + + /** The network this provider streams from. */ val network: TONNetwork + /** Open the streaming connection. */ suspend fun connect() + /** Close the streaming connection. */ suspend fun disconnect() + /** A [Flow] of connection-state changes (`true` = connected). */ fun connectionChange(): Flow + /** Watch an account's balance. */ fun balance(address: String): Flow + /** Watch an account's transactions. */ fun transactions(address: String): Flow + /** Watch an account's jetton balances. */ fun jettons(address: String): Flow } diff --git a/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONHexTest.kt b/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONHexTest.kt new file mode 100644 index 00000000..ef7a0a3a --- /dev/null +++ b/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONHexTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit.model + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class TONHexTest { + + @Test + fun `value is preserved verbatim`() { + assertEquals("0x1234", TONHex("0x1234").value) + assertEquals("abcd", TONHex("abcd").value) + } + + @Test + fun `rawValue strips a 0x or 0X prefix`() { + assertEquals("1234abcd", TONHex("0x1234abcd").rawValue) + assertEquals("1234abcd", TONHex("0X1234abcd").rawValue) + assertEquals("1234abcd", TONHex("1234abcd").rawValue) + } + + @Test + fun `withPrefix adds 0x only when absent`() { + assertEquals("0xabcd", TONHex("abcd").withPrefix) + assertEquals("0xabcd", TONHex("0xabcd").withPrefix) + assertEquals("0Xabcd", TONHex("0Xabcd").withPrefix) + } + + @Test + fun `data decodes valid hex with and without prefix`() { + assertArrayEquals(byteArrayOf(0x12, 0x34), TONHex("0x1234").data) + assertArrayEquals(byteArrayOf(0x12, 0x34), TONHex("1234").data) + } + + @Test + fun `data decodes high bytes to negative byte values`() { + assertArrayEquals(byteArrayOf(0xFF.toByte(), 0xAB.toByte()), TONHex("0xffab").data) + } + + @Test + fun `data of an empty hex is an empty array`() { + assertArrayEquals(ByteArray(0), TONHex("0x").data) + assertArrayEquals(ByteArray(0), TONHex("").data) + } + + @Test + fun `data is null for odd-length hex`() { + assertNull(TONHex("0x123").data) + } + + @Test + fun `data is null for non-hex characters`() { + assertNull(TONHex("0xzz").data) + } + + @Test + fun `fromData encodes bytes with a 0x prefix by default`() { + assertEquals("0x1234ff", TONHex.fromData(byteArrayOf(0x12, 0x34, 0xFF.toByte())).value) + } + + @Test + fun `fromData can omit the prefix`() { + assertEquals("1234ff", TONHex.fromData(byteArrayOf(0x12, 0x34, 0xFF.toByte()), withPrefix = false).value) + } + + @Test + fun `fromString encodes UTF-8 bytes`() { + // "ab" -> 0x61 0x62 + assertEquals("0x6162", TONHex.fromString("ab").value) + } + + @Test + fun `fromData and data round-trip arbitrary bytes`() { + val bytes = byteArrayOf(0x00, 0x12, 0x7F, 0xFF.toByte(), 0xAB.toByte(), 0x80.toByte()) + assertArrayEquals(bytes, TONHex.fromData(bytes).data) + } + + @Test + fun `equals and hashCode are value-based`() { + assertEquals(TONHex("0x1234"), TONHex("0x1234")) + assertEquals(TONHex("0x1234").hashCode(), TONHex("0x1234").hashCode()) + assertNotEquals(TONHex("0x1234"), TONHex("0x5678")) + // Equality is on the raw stored string, so prefixed vs unprefixed are distinct values. + assertNotEquals(TONHex("0x1234"), TONHex("1234")) + } + + @Test + fun `toString returns the stored value`() { + assertEquals("0x1234", TONHex("0x1234").toString()) + } +} diff --git a/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONMnemonicTest.kt b/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONMnemonicTest.kt new file mode 100644 index 00000000..307faf09 --- /dev/null +++ b/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONMnemonicTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit.model + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class TONMnemonicTest { + + private fun words(count: Int): List = (1..count).map { "word$it" } + + @Test + fun `empty mnemonic has max capacity and no filled words`() { + val mnemonic = TONMnemonic.empty() + + assertEquals(TONMnemonicLength.MAX.wordCount, mnemonic.value.size) + assertEquals(0, mnemonic.filledWordCount) + assertFalse(mnemonic.isFilled) + } + + @Test + fun `fromWords pads to max capacity`() { + val mnemonic = TONMnemonic.fromWords(words(12)) + + assertEquals(TONMnemonicLength.MAX.wordCount, mnemonic.value.size) + assertEquals(12, mnemonic.filledWordCount) + } + + @Test + fun `a 12-word mnemonic is filled`() { + assertTrue(TONMnemonic.fromWords(words(12)).isFilled) + } + + @Test + fun `a 24-word mnemonic is filled`() { + val mnemonic = TONMnemonic.fromWords(words(24)) + assertEquals(24, mnemonic.filledWordCount) + assertTrue(mnemonic.isFilled) + } + + @Test + fun `an invalid word count is not filled`() { + assertFalse(TONMnemonic.fromWords(words(5)).isFilled) + } + + @Test + fun `fromPhrase splits on spaces`() { + val mnemonic = TONMnemonic.fromPhrase("alpha bravo charlie") + + assertEquals(3, mnemonic.filledWordCount) + assertEquals("alpha", mnemonic.getWord(0)) + assertEquals("charlie", mnemonic.getWord(2)) + assertFalse(mnemonic.isFilled) + } + + @Test + fun `updateWord changes a word in range and ignores out-of-range`() { + val mnemonic = TONMnemonic.empty() + + mnemonic.updateWord(0, "first") + assertEquals("first", mnemonic.getWord(0)) + + // Out of range is a no-op, not a crash. + mnemonic.updateWord(999, "ignored") + assertEquals(1, mnemonic.filledWordCount) + } + + @Test + fun `getWord returns empty string out of bounds`() { + assertEquals("", TONMnemonic.empty().getWord(999)) + } + + @Test + fun `toPhrase joins only the filled words`() { + assertEquals("alpha bravo charlie", TONMnemonic.fromPhrase("alpha bravo charlie").toPhrase()) + assertEquals("first", TONMnemonic.empty().also { it.updateWord(0, "first") }.toPhrase()) + } + + @Test + fun `equals and hashCode are content-based`() { + val a = TONMnemonic.fromWords(words(12)) + val b = TONMnemonic.fromWords(words(12)) + + assertEquals(a, b) + assertEquals(a.hashCode(), b.hashCode()) + assertNotEquals(a, TONMnemonic.fromWords(words(24))) + } + + @Test + fun `TONMnemonicLength maps word counts`() { + assertEquals(TONMnemonicLength.BITS_128, TONMnemonicLength.fromWordCount(12)) + assertEquals(TONMnemonicLength.BITS_256, TONMnemonicLength.fromWordCount(24)) + assertNull(TONMnemonicLength.fromWordCount(13)) + assertEquals(TONMnemonicLength.BITS_256, TONMnemonicLength.MAX) + } +} diff --git a/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONTokenAmountTest.kt b/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONTokenAmountTest.kt new file mode 100644 index 00000000..6d62b4c6 --- /dev/null +++ b/TONWalletKit-Android/api/src/test/java/io/ton/walletkit/model/TONTokenAmountTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit.model + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Assert.fail +import org.junit.Test +import java.math.BigInteger + +class TONTokenAmountTest { + + @Test + fun `string and long constructors agree with BigInteger`() { + val fromBig = TONTokenAmount(BigInteger.valueOf(100)) + val fromString = TONTokenAmount("100") + val fromLong = TONTokenAmount(100L) + + assertEquals(fromBig, fromString) + assertEquals(fromBig, fromLong) + assertEquals("100", fromBig.value) + } + + @Test + fun `value and toString render the nano units`() { + val amount = TONTokenAmount("1000000000") + assertEquals("1000000000", amount.value) + assertEquals("1000000000", amount.toString()) + } + + @Test + fun `ZERO is zero`() { + assertEquals(TONTokenAmount("0"), TONTokenAmount.ZERO) + assertEquals("0", TONTokenAmount.ZERO.value) + } + + @Test + fun `amounts beyond Long range are preserved`() { + val huge = "123456789012345678901234567890" + assertEquals(huge, TONTokenAmount(huge).value) + } + + @Test + fun `negative amounts are allowed`() { + assertEquals("-5", TONTokenAmount(-5L).value) + } + + @Test + fun `parseOrNull returns the amount for a valid string`() { + assertEquals(TONTokenAmount("42"), TONTokenAmount.parseOrNull("42")) + } + + @Test + fun `parseOrNull returns null for an invalid string`() { + assertNull(TONTokenAmount.parseOrNull("not-a-number")) + assertNull(TONTokenAmount.parseOrNull("")) + } + + @Test + fun `string constructor throws on an invalid number`() { + try { + TONTokenAmount("not-a-number") + fail("expected NumberFormatException") + } catch (e: NumberFormatException) { + // expected + } + } + + @Test + fun `equals and hashCode are value-based`() { + assertEquals(TONTokenAmount("100"), TONTokenAmount("100")) + assertEquals(TONTokenAmount("100").hashCode(), TONTokenAmount("100").hashCode()) + assertNotEquals(TONTokenAmount("100"), TONTokenAmount("101")) + } +} diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt index 6c8d4b75..4dfdcc3c 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt @@ -206,7 +206,7 @@ internal class TONWalletKit private constructor( return TONStreamingProviderImpl(engine = engine, network = config.network, identifier = result.optString("providerId")) } - override fun streaming(): ITONStreamingManager { + override suspend fun streaming(): ITONStreamingManager { checkNotDestroyed() return streamingManager } @@ -492,7 +492,7 @@ internal class TONWalletKit private constructor( override suspend fun gasless(): ITONGaslessManager = gaslessManager - override fun staking(): ITONStakingManager { + override suspend fun staking(): ITONStakingManager { checkNotDestroyed() return _stakingManager } diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WalletKitEngine.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WalletKitEngine.kt index a415632b..1641cb6b 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WalletKitEngine.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WalletKitEngine.kt @@ -320,12 +320,13 @@ internal interface WalletKitEngine : RequestHandler { * * @param event Typed event from the transaction request * @param response Optional pre-computed approval response + * @return The finalized approval response. * @throws WalletKitBridgeException if approval or signing fails */ override suspend fun approveTransaction( event: TONSendTransactionRequestEvent, response: TONSendTransactionApprovalResponse?, - ) + ): TONSendTransactionApprovalResponse /** * Reject a transaction request. @@ -346,12 +347,13 @@ internal interface WalletKitEngine : RequestHandler { * * @param event Typed event from the sign data request * @param response Optional pre-computed approval response + * @return The finalized approval response. * @throws WalletKitBridgeException if approval or signing fails */ override suspend fun approveSignData( event: TONSignDataRequestEvent, response: TONSignDataApprovalResponse?, - ) + ): TONSignDataApprovalResponse /** * Reject a data signing request. @@ -375,7 +377,7 @@ internal interface WalletKitEngine : RequestHandler { override suspend fun approveSignMessage( event: TONSignMessageRequestEvent, response: TONSignMessageApprovalResponse?, - ) + ): TONSignMessageApprovalResponse /** * Reject a sign-message request. diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt index b067b35a..92536f73 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt @@ -447,7 +447,7 @@ internal class WebViewWalletKitEngine private constructor( override suspend fun approveTransaction( event: TONSendTransactionRequestEvent, response: TONSendTransactionApprovalResponse?, - ) = rpcClient.approveTransaction(event, response) + ): TONSendTransactionApprovalResponse = rpcClient.approveTransaction(event, response) override suspend fun rejectTransaction( event: TONSendTransactionRequestEvent, @@ -458,7 +458,7 @@ internal class WebViewWalletKitEngine private constructor( override suspend fun approveSignData( event: TONSignDataRequestEvent, response: TONSignDataApprovalResponse?, - ) = rpcClient.approveSignData(event, response) + ): TONSignDataApprovalResponse = rpcClient.approveSignData(event, response) override suspend fun rejectSignData( event: TONSignDataRequestEvent, @@ -469,7 +469,7 @@ internal class WebViewWalletKitEngine private constructor( override suspend fun approveSignMessage( event: TONSignMessageRequestEvent, response: TONSignMessageApprovalResponse?, - ) = rpcClient.approveSignMessage(event, response) + ): TONSignMessageApprovalResponse = rpcClient.approveSignMessage(event, response) override suspend fun rejectSignMessage( event: TONSignMessageRequestEvent, diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/operations/TonConnectOperations.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/operations/TonConnectOperations.kt index cc9274c5..d516a3aa 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/operations/TonConnectOperations.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/operations/TonConnectOperations.kt @@ -138,10 +138,10 @@ internal suspend fun BridgeRpcClient.rejectConnect( internal suspend fun BridgeRpcClient.approveTransaction( event: TONSendTransactionRequestEvent, response: TONSendTransactionApprovalResponse? = null, -) { +): TONSendTransactionApprovalResponse { event.walletAddress ?: throw WalletKitBridgeException(ERROR_WALLET_ADDRESS_REQUIRED) event.walletId ?: throw WalletKitBridgeException(ERROR_WALLET_ID_REQUIRED) - send(BridgeMethodConstants.METHOD_APPROVE_TRANSACTION_REQUEST, listOf(event, response)) + return callTyped(BridgeMethodConstants.METHOD_APPROVE_TRANSACTION_REQUEST, listOf(event, response)) } internal suspend fun BridgeRpcClient.rejectTransaction( @@ -156,10 +156,10 @@ internal suspend fun BridgeRpcClient.rejectTransaction( internal suspend fun BridgeRpcClient.approveSignData( event: TONSignDataRequestEvent, response: TONSignDataApprovalResponse? = null, -) { +): TONSignDataApprovalResponse { event.walletAddress ?: throw WalletKitBridgeException(ERROR_WALLET_ADDRESS_REQUIRED) event.walletId ?: throw WalletKitBridgeException(ERROR_WALLET_ID_REQUIRED) - send(BridgeMethodConstants.METHOD_APPROVE_SIGN_DATA_REQUEST, listOf(event, response)) + return callTyped(BridgeMethodConstants.METHOD_APPROVE_SIGN_DATA_REQUEST, listOf(event, response)) } internal suspend fun BridgeRpcClient.rejectSignData( @@ -173,10 +173,10 @@ internal suspend fun BridgeRpcClient.rejectSignData( internal suspend fun BridgeRpcClient.approveSignMessage( event: TONSignMessageRequestEvent, response: TONSignMessageApprovalResponse? = null, -) { +): TONSignMessageApprovalResponse { event.walletAddress ?: throw WalletKitBridgeException(ERROR_WALLET_ADDRESS_REQUIRED) event.walletId ?: throw WalletKitBridgeException(ERROR_WALLET_ID_REQUIRED) - send(BridgeMethodConstants.METHOD_APPROVE_SIGN_MESSAGE_REQUEST, listOf(event, response)) + return callTyped(BridgeMethodConstants.METHOD_APPROVE_SIGN_MESSAGE_REQUEST, listOf(event, response)) } internal suspend fun BridgeRpcClient.rejectSignMessage( diff --git a/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/GaslessOperationsTest.kt b/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/GaslessOperationsTest.kt new file mode 100644 index 00000000..eefc5998 --- /dev/null +++ b/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/GaslessOperationsTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit.engine.operations + +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Tests for [io.ton.walletkit.engine.operations] gasless functions — the bridge contract for + * gasless provider creation, registration, and the provider registry. These pin the + * `METHOD_*_GASLESS_*` constants against the JS bridge handler keys (a mismatch fails silently + * at runtime). Mirrors [SwapOperationsTest] at the bridge protocol layer. + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [28]) +class GaslessOperationsTest : OperationsTestBase() { + + companion object { + const val PROVIDER_ID = "tonapi" + } + + // --- createTonApiGaslessProvider --- + + @Test + fun createTonApiGaslessProvider_returnsProviderId() = runBlocking { + givenBridgeReturns(buildJsonObject { put("providerId", PROVIDER_ID) }) + + val id = rpcClient.createTonApiGaslessProvider(null) + + assertEquals(PROVIDER_ID, id) + assertEquals("createTonApiGaslessProvider", capturedMethod) + } + + // --- register / remove / setDefault --- + + @Test + fun registerGaslessProvider_sendsProviderId() = runBlocking { + rpcClient.registerGaslessProvider(PROVIDER_ID) + + assertEquals("registerGaslessProvider", capturedMethod) + val encoded = encodeCapturedParams() as JsonObject + assertEquals(PROVIDER_ID, (encoded["providerId"] as JsonPrimitive).content) + } + + @Test + fun removeGaslessProvider_sendsProviderId() = runBlocking { + rpcClient.removeGaslessProvider(PROVIDER_ID) + + assertEquals("removeGaslessProvider", capturedMethod) + val encoded = encodeCapturedParams() as JsonObject + assertEquals(PROVIDER_ID, (encoded["providerId"] as JsonPrimitive).content) + } + + @Test + fun setDefaultGaslessProvider_sendsProviderId() = runBlocking { + rpcClient.setDefaultGaslessProvider(PROVIDER_ID) + + assertEquals("setDefaultGaslessProvider", capturedMethod) + val encoded = encodeCapturedParams() as JsonObject + assertEquals(PROVIDER_ID, (encoded["providerId"] as JsonPrimitive).content) + } + + // --- getRegisteredGaslessProviders --- + + @Test + fun getRegisteredGaslessProviders_returnsProviderIds() = runBlocking { + givenBridgeReturns( + buildJsonObject { + put( + "providerIds", + buildJsonArray { + add("tonapi") + add("custom") + }, + ) + }, + ) + + val ids = rpcClient.getRegisteredGaslessProviders() + + assertEquals(listOf("tonapi", "custom"), ids) + assertEquals("getRegisteredGaslessProviders", capturedMethod) + } + + @Test + fun getRegisteredGaslessProviders_emptyProviders() = runBlocking { + givenBridgeReturns(buildJsonObject { put("providerIds", JsonArray(emptyList())) }) + + assertTrue(rpcClient.getRegisteredGaslessProviders().isEmpty()) + } + + // --- hasGaslessProvider --- + + @Test + fun hasGaslessProvider_returnsTrueWhenResultTrue() = runBlocking { + givenBridgeReturns(buildJsonObject { put("result", true) }) + + assertTrue(rpcClient.hasGaslessProvider(PROVIDER_ID)) + assertEquals("hasGaslessProvider", capturedMethod) + } + + @Test + fun hasGaslessProvider_returnsFalseWhenResultFalse() = runBlocking { + givenBridgeReturns(buildJsonObject { put("result", false) }) + + assertFalse(rpcClient.hasGaslessProvider(PROVIDER_ID)) + } + + // --- getGaslessProviderSupportedNetworks --- + + @Test + fun getGaslessProviderSupportedNetworks_decodesNetworks() = runBlocking { + givenBridgeReturns( + buildJsonObject { + put( + "networks", + buildJsonArray { + add(buildJsonObject { put("chainId", "-239") }) + add(buildJsonObject { put("chainId", "-3") }) + }, + ) + }, + ) + + val networks = rpcClient.getGaslessProviderSupportedNetworks(PROVIDER_ID) + + assertEquals(2, networks.size) + assertEquals("-239", networks[0].chainId) + assertEquals("-3", networks[1].chainId) + assertEquals("getGaslessProviderSupportedNetworks", capturedMethod) + } +} diff --git a/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/TonConnectOperationsTest.kt b/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/TonConnectOperationsTest.kt index 6045d061..b6ca825a 100644 --- a/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/TonConnectOperationsTest.kt +++ b/TONWalletKit-Android/impl/src/test/java/io/ton/walletkit/engine/operations/TonConnectOperationsTest.kt @@ -262,8 +262,8 @@ class TonConnectOperationsTest : OperationsTestBase() { // --- approveTransaction tests --- @Test - fun approveTransaction_completesSuccessfully() = runBlocking { - givenBridgeReturns(JsonObject(emptyMap())) + fun approveTransaction_returnsApprovalResponse() = runBlocking { + givenBridgeReturns(buildJsonObject { put("signedBoc", "te6cckEBAQEAAgAAAEysuc0=") }) val event = createTransactionRequestEvent( id = "tx-req-123", @@ -271,8 +271,8 @@ class TonConnectOperationsTest : OperationsTestBase() { walletId = TEST_WALLET_ID, ) - // Should not throw - rpcClient.approveTransaction(event) + val response = rpcClient.approveTransaction(event) + assertEquals("te6cckEBAQEAAgAAAEysuc0=", response.signedBoc.value) } // --- rejectTransaction tests ---