Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 17 additions & 14 deletions TONWalletKit-Android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?: "<none>"}")
println("Balance: ${balance ?: "<unknown>"}")
```#### 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)
}
}

Expand All @@ -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:
Expand All @@ -125,7 +128,7 @@ val wallets = kit.getWallets()

#### Remove wallet:
```kotlin
kit.removeWallet(wallet.address)
kit.removeWallet(wallet.identifier())
```

#### Clean up when done:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TONTransferRequest>): 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)))
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -238,7 +238,7 @@ interface ITONWalletKit {
/**
* Get the streaming manager.
*/
fun streaming(): ITONStreamingManager
suspend fun streaming(): ITONStreamingManager

suspend fun createStreamingProvider(
config: TONTonCenterStreamingProviderConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,53 +35,61 @@ 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,
stack: List<TONRawStackItem>? = null,
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<TONUserFriendlyAddress>,
): Map<TONUserFriendlyAddress, TONAccountState>

/** 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?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<TONWalletKitConfiguration.Feature>? = null
Comment on lines +80 to 81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Gradle/Kotlin files =="
fd -H -t f 'build.gradle.kts|build.gradle|gradle.properties|settings.gradle.kts|settings.gradle'

echo
echo "== Kotlin JVM default-method/compiler settings =="
rg -n --iglob '*gradle*' --iglob 'gradle.properties' \
  'jvmDefault|-Xjvm-default|freeCompilerArgs|kotlinOptions|compilerOptions|JvmDefault'

echo
echo "== API/binary compatibility tooling (if any) =="
rg -n --iglob '*gradle*' 'binary-compatibility-validator|apiValidation|explicitApi'

Repository: ton-org/kit-android

Length of output: 1517


🏁 Script executed:

# Read the full API module build.gradle.kts
cat -n TONWalletKit-Android/api/build.gradle.kts | head -80

Repository: ton-org/kit-android

Length of output: 2793


🏁 Script executed:

# Examine the ITONWalletAdapter.kt interface file
cat -n TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/ITONWalletAdapter.kt

Repository: ton-org/kit-android

Length of output: 4012


🏁 Script executed:

# Search for `@JvmDefault` annotations in the codebase
rg -n '`@JvmDefault`' --type kotlin

Repository: ton-org/kit-android

Length of output: 45


Add JVM default-method compatibility configuration for the new interface member.

Line 81 adds a new interface method with a default body, but the API module's build.gradle.kts lacks the -Xjvm-default compiler flag. Existing precompiled custom ITONWalletAdapter implementations will face binary compatibility issues at runtime. Either:

  • Add -Xjvm-default=all to compilerOptions.freeCompilerArgs in TONWalletKit-Android/api/build.gradle.kts, or
  • Annotate the supportedFeatures() method with @JvmDefault
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/ITONWalletAdapter.kt`
around lines 80 - 81, The new interface method supportedFeatures() in
ITONWalletAdapter has a default implementation body, which requires JVM
default-method compatibility configuration. Add the `-Xjvm-default=all` compiler
flag to the compilerOptions.freeCompilerArgs in
TONWalletKit-Android/api/build.gradle.kts, or alternatively annotate the
supportedFeatures() method with `@JvmDefault` to ensure existing custom
implementations of this interface remain binary compatible at runtime.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines +119 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for TONMnemonic usage in HashSet, HashMap, or as map keys

# Search for HashSet/HashMap usage with TONMnemonic
rg -n --type=kotlin -C3 'HashSet.*TONMnemonic|HashMap.*TONMnemonic|mutableSetOf.*TONMnemonic|mutableMapOf.*TONMnemonic'

# Search for TONMnemonic.updateWord calls to confirm mutability usage
rg -n --type=kotlin -C2 '\.updateWord\('

Repository: ton-org/kit-android

Length of output: 1786


🏁 Script executed:

cat -n TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONMnemonic.kt | head -140

Repository: ton-org/kit-android

Length of output: 5675


🏁 Script executed:

rg -n "class TONHex" --type=kotlin -A 20

Repository: ton-org/kit-android

Length of output: 4579


Overriding equals/hashCode on a mutable class is an anti-pattern.

TONMnemonic is intentionally mutable (via updateWord) for UI input handling, but overrides equals and hashCode based on mutable internal state. If an instance is added to a HashSet or used as a HashMap key and then mutated via updateWord, the collection contracts will be violated, leading to silent bugs where the object can no longer be found or removed.

While no current usage in hash-based collections was found, the codebase exposes this as a public API, so external callers could encounter this issue.

Consider one of the following approaches:

  1. Make TONMnemonic immutable by removing updateWord and making words immutable. (Conflicts with documented UI input handling use case.)
  2. Remove the equals/hashCode overrides and rely on reference equality for a mutable object.
  3. Document the constraint with a KDoc warning that instances must not be used in hash-based collections and should not be mutated after creation.

Note: Other model classes like TONHex are immutable data classes, suggesting that value-based equality should align with immutability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONMnemonic.kt`
around lines 119 - 125, The TONMnemonic class overrides equals and hashCode
based on mutable state (the words field), which violates the contract for use in
hash-based collections like HashSet or HashMap since the object can be mutated
via the updateWord method. Either remove the equals and hashCode method
overrides entirely to rely on reference equality for this mutable class, or add
a KDoc warning on the TONMnemonic class that clearly documents that instances
must never be used as keys in HashMap/HashSet and should not be mutated after
creation. Choose the approach that best fits your design intent for this class.


override fun toString(): String = toPhrase()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class TONWalletConnectionRequest(
}
}

/** Reject the connection request. */
suspend fun reject(reason: String? = null, errorCode: Int? = null) {
handler.rejectConnect(event, reason, errorCode)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading
Loading