Skip to content
Open
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
19 changes: 19 additions & 0 deletions AndroidDemo/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties

fun String.escapeForBuildConfig(): String = replace("\\", "\\\\").replace("\"", "\\\"")

plugins {
alias(libs.plugins.androidApplication)
Expand Down Expand Up @@ -41,6 +44,19 @@ android {
val allureToken =
findProperty("allureToken") as String?
?: System.getenv("ALLURE_API_TOKEN")
val localProps =
Properties().also { props ->
val f = rootProject.file("local.properties")
if (f.exists()) f.inputStream().use { stream -> props.load(stream) }
}
val tonCenterApiKey =
localProps.getProperty("tonCenterApiKey")
?: System.getenv("TONCENTER_API_KEY")
?: ""
val tonApiKey =
localProps.getProperty("tonApiKey")
?: System.getenv("TONAPI_API_KEY")
?: ""

testMnemonic?.let {
testInstrumentationRunnerArguments["testMnemonic"] = it
Expand All @@ -49,6 +65,8 @@ android {
allureToken?.let {
testInstrumentationRunnerArguments["allureToken"] = it
}
buildConfigField("String", "TONCENTER_API_KEY", "\"${tonCenterApiKey.escapeForBuildConfig()}\"")
buildConfigField("String", "TONAPI_API_KEY", "\"${tonApiKey.escapeForBuildConfig()}\"")
}

buildTypes {
Expand All @@ -63,6 +81,7 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
buildConfig = true
compose = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import io.ton.walletkit.api.generated.TONMasterchainInfo
import io.ton.walletkit.api.generated.TONNetwork
import io.ton.walletkit.api.generated.TONRawStackItem
import io.ton.walletkit.client.TONAPIClient
import io.ton.walletkit.demo.BuildConfig
import io.ton.walletkit.model.TONBase64
import io.ton.walletkit.model.TONHex
import io.ton.walletkit.model.TONUserFriendlyAddress
Expand Down Expand Up @@ -332,8 +333,8 @@ class TonAPIClient(
}

companion object {
fun mainnet(apiKey: String = "") = TonAPIClient(TONNetwork.MAINNET, apiKey)
fun testnet(apiKey: String = "") = TonAPIClient(TONNetwork.TESTNET, apiKey)
fun tetra(apiKey: String = "") = TonAPIClient(TONNetwork.TETRA, apiKey)
fun mainnet(apiKey: String = BuildConfig.TONAPI_API_KEY) = TonAPIClient(TONNetwork.MAINNET, apiKey)
fun testnet(apiKey: String = BuildConfig.TONAPI_API_KEY) = TonAPIClient(TONNetwork.TESTNET, apiKey)
fun tetra(apiKey: String = BuildConfig.TONAPI_API_KEY) = TonAPIClient(TONNetwork.TETRA, apiKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import io.ton.walletkit.api.generated.TONSignatureDomain
import io.ton.walletkit.api.isTetra
import io.ton.walletkit.config.SignDataType
import io.ton.walletkit.config.TONWalletKitConfiguration
import io.ton.walletkit.demo.BuildConfig
import io.ton.walletkit.demo.data.storage.DemoAppStorage
import io.ton.walletkit.demo.data.storage.SecureDemoAppStorage
import io.ton.walletkit.event.TONWalletKitEvent
Expand All @@ -62,49 +63,34 @@ class WalletKitDemoApp :
Application(),
SingletonImageLoader.Factory {

// Application-level coroutine scope
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

/**
* Create Coil ImageLoader with optimized settings for LazyGrid
*/
override fun newImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context)
.crossfade(true)
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, 0.25) // Use 25% of app memory for image cache
.maxSizePercent(context, 0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache").toOkioPath())
.maxSizeBytes(512L * 1024 * 1024) // 512 MB
.maxSizeBytes(512L * 1024 * 1024)
.build()
}
// Add components with retry capability
.components {
add(OkHttpNetworkFetcherFactory())
}
.logger(DebugLogger()) // Enable logging for debugging
.logger(DebugLogger())
.build()

/**
* Demo app storage for wallet metadata and user preferences.
* Note: Wallet data (mnemonics) are managed by SDK's internal persistent storage.
*/
val storage: DemoAppStorage by lazy {
SecureDemoAppStorage(this)
}

/**
* Shared flow for SDK events to be consumed by ViewModel.
*/
private val _sdkEvents = MutableSharedFlow<TONWalletKitEvent>(extraBufferCapacity = 10)
val sdkEvents = _sdkEvents.asSharedFlow()

/**
* State flow for SDK initialization status.
*/
private val _sdkInitialized = MutableSharedFlow<Boolean>(replay = 1)
val sdkInitialized = _sdkInitialized.asSharedFlow()

Expand All @@ -115,14 +101,8 @@ class WalletKitDemoApp :
applicationScope.launch {
try {
val kit = TONWalletKitHelper.mainnet(this@WalletKitDemoApp)

// CRITICAL: Load and add wallets BEFORE setting up event listeners
// This ensures that when events are replayed (which happens when the first
// event handler is added), the wallets are already available in the SDK
// for event approval/rejection operations.
loadAndAddStoredWallets(kit)

// Add event handler (this triggers setEventsListeners() and event replay)
kit.addEventsHandler(object : TONBridgeEventsHandler {
override fun handle(event: TONWalletKitEvent) {
_sdkEvents.tryEmit(event)
Expand All @@ -136,18 +116,11 @@ class WalletKitDemoApp :
}
}

/**
* Load wallets from encrypted storage and add them to the SDK.
* This must be done BEFORE adding event handlers to ensure wallets are available
* when replayed events are processed.
*/
private suspend fun loadAndAddStoredWallets(kit: ITONWalletKit) {
try {
// Get all stored wallet records
val storage = getSharedPreferences("wallet_storage", MODE_PRIVATE)
val walletDataJson = storage.getString("wallets", "[]") ?: "[]"

// Skip if no wallets stored (empty list is fine)
if (walletDataJson == "[]") {
Log.d(TAG, "No stored wallets to load")
return
Expand All @@ -157,13 +130,10 @@ class WalletKitDemoApp :

Log.d(TAG, "Loading ${walletDataList.size} stored wallets into SDK")

// Add each wallet to the SDK
for (walletRecord in walletDataList) {
try {
// Convert mnemonic string to list of words
val mnemonicWords = walletRecord.mnemonic.split(" ").filter { it.isNotBlank() }

// Convert network string to TONNetwork enum
val network = when (walletRecord.network) {
ChainIds.MAINNET -> TONNetwork.MAINNET
ChainIds.TESTNET -> TONNetwork.TESTNET
Expand Down Expand Up @@ -210,50 +180,23 @@ class WalletKitDemoApp :
}
}

/**
* Helper to get cached ITONWalletKit instance.
*/
object TONWalletKitHelper {
private var mainnetInstance: ITONWalletKit? = null
private val mutex = kotlinx.coroutines.sync.Mutex()

/**
* Flag to disable network send for testing.
* When true, transactions will be simulated but not actually sent to the network.
* Set this BEFORE initializing the SDK.
*/
@Volatile
var disableNetworkSend: Boolean = false

/**
* Flag to use custom session manager for testing.
* When true, uses TestSessionManager instead of SDK's built-in session storage.
* Set this BEFORE initializing the SDK.
*/
@Volatile
var useCustomSessionManager: Boolean = true // Enable by default for testing
var useCustomSessionManager: Boolean = true

/**
* Flag to use custom API client for testing.
* When true, uses TestAPIClient instead of SDK's built-in API client.
* Set this BEFORE initializing the SDK.
*/
@Volatile
var useCustomApiClient: Boolean = true // Enable by default for testing
var useCustomApiClient: Boolean = false

/**
* The custom session manager instance (if enabled).
* Exposed so tests can inspect session state.
*/
var sessionManager: TestSessionManager? = null
private set

/**
* Check if we're running under instrumentation tests and if disableNetworkSend is requested.
* Uses reflection to avoid compile-time dependency on test libraries.
*/
private fun checkInstrumentationDisableNetworkSend(): Boolean = try {
// Use reflection to access InstrumentationRegistry without compile-time dependency
val registryClass = Class.forName("androidx.test.platform.app.InstrumentationRegistry")
val getArgumentsMethod = registryClass.getMethod("getArguments")
val arguments = getArgumentsMethod.invoke(null) as? android.os.Bundle
Expand All @@ -264,22 +207,17 @@ object TONWalletKitHelper {
}
result
} catch (e: Exception) {
// Not running under instrumentation or class not found, ignore
false
}

suspend fun mainnet(application: Application): ITONWalletKit {
// Fast path: already initialized
mainnetInstance?.let { return it }

// Slow path: need to initialize (with mutex to prevent double-init)
return mutex.withLock {
// Double-check after acquiring lock
mainnetInstance?.let {
return@withLock it
}

// Check both the flag and instrumentation arguments
val shouldDisableNetwork = disableNetworkSend || checkInstrumentationDisableNetworkSend()

val devOptions = if (shouldDisableNetwork) {
Expand All @@ -288,19 +226,13 @@ object TONWalletKitHelper {
null
}

// Create custom session manager if enabled
val customSessionManager = if (useCustomSessionManager) {
TestSessionManager().also { sessionManager = it }
} else {
null
}

// Create network configurations for both mainnet and testnet
// This demonstrates the iOS-like pattern where each network config has either:
// - apiClientConfiguration: Use SDK's built-in API client with your API key
// - apiClient: Use your own custom API client implementation
val networkConfigurations = if (useCustomApiClient) {
// Demonstrate using different API providers per network
setOf(
TONWalletKitConfiguration.NetworkConfiguration(
network = TONNetwork.MAINNET,
Expand All @@ -316,7 +248,6 @@ object TONWalletKitHelper {
),
)
} else {
// Use SDK's built-in API client with default configuration
setOf(
TONWalletKitConfiguration.NetworkConfiguration(
network = TONNetwork.MAINNET,
Expand Down Expand Up @@ -366,14 +297,30 @@ object TONWalletKitHelper {
)

val kit = ITONWalletKit.initialize(application, config)
val tonCenterApiKey = BuildConfig.TONCENTER_API_KEY.takeIf { it.isNotBlank() }
if (tonCenterApiKey != null) {
listOf(TONNetwork.MAINNET, TONNetwork.TESTNET, TONNetwork.TETRA).forEach { network ->
try {
val streamingProvider = kit.createStreamingProvider(
io.ton.walletkit.api.generated.TONTonCenterStreamingProviderConfig(
network = network,
apiKey = tonCenterApiKey,
),
)
kit.streaming().register(streamingProvider)
} catch (e: Exception) {
Log.e("WalletKitDemoApp", "Streaming init ERROR network=${network.chainId} - ${e.message}", e)
}
}
} else {
Log.w("WalletKitDemoApp", "TONCENTER_API_KEY is not set; TonCenter streaming provider disabled")
}

mainnetInstance = kit
kit
}
}

/**
* Clear the cached instance (for testing or logout scenarios).
*/
suspend fun clearMainnet() {
mutex.withLock {
mainnetInstance?.destroy()
Expand All @@ -387,4 +334,5 @@ object TONWalletKitHelper {
private const val DEFAULT_MANIFEST_ABOUT_URL = "https://wallet.ton.org"
private const val DEFAULT_MANIFEST_UNIVERSAL_LINK = "https://wallet.ton.org/tc"
private const val DEFAULT_BRIDGE_URL = "https://bridge.tonapi.io/bridge"
private const val TAG = "TONWalletKitHelper"
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,52 +49,31 @@ data class JettonSummary(
get() = imageUrl ?: imageData

companion object {
/**
* Create JettonSummary from SDK's TONJetton.
*
* @param jetton Jetton from SDK
* @return UI-friendly jetton summary
*/
fun from(jetton: TONJetton): JettonSummary {
val info = jetton.info

val name = info.name ?: "Unknown Jetton"
val symbol = info.symbol ?: "UNKNOWN"
val address = jetton.walletAddress.value
val balance = jetton.balance

// Format balance with decimals
val formattedBalance = try {
val decimals = jetton.decimalsNumber ?: 9
val balanceBigInt = BigDecimal(balance)
val divisor = BigDecimal.TEN.pow(decimals)
val formattedValue = balanceBigInt.divide(divisor, decimals, RoundingMode.DOWN)

// Remove trailing zeros
val strippedValue = formattedValue.stripTrailingZeros()
val plainString = strippedValue.toPlainString()

// Format with symbol
"$plainString $symbol"
fun formatBalance(rawBalance: String, decimals: Int?, symbol: String): String =
try {
val d = decimals ?: 9
val divisor = BigDecimal.TEN.pow(d)
val formatted = BigDecimal(rawBalance)
.divide(divisor, d, RoundingMode.DOWN)
.stripTrailingZeros()
.toPlainString()
"$formatted $symbol"
} catch (e: Exception) {
"$balance $symbol (raw)"
"$rawBalance $symbol (raw)"
}

val imageUrl = info.image?.mediumUrl ?: info.image?.url
val imageData = info.image?.data

// Placeholder for estimated value - would need price data
val estimatedValue = "≈ \$0.00"

fun from(jetton: TONJetton): JettonSummary {
val info = jetton.info
val symbol = info.symbol ?: "UNKNOWN"
return JettonSummary(
name = name,
name = info.name ?: "Unknown Jetton",
symbol = symbol,
address = address,
balance = balance,
formattedBalance = formattedBalance,
imageUrl = imageUrl,
imageData = imageData,
estimatedValue = estimatedValue,
address = jetton.walletAddress.value,
balance = jetton.balance,
formattedBalance = formatBalance(jetton.balance, jetton.decimalsNumber, symbol),
imageUrl = info.image?.mediumUrl ?: info.image?.url,
imageData = info.image?.data,
estimatedValue = "≈ \$0.00",
jetton = jetton,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ data class WalletUiState(
val isLoadingJettons: Boolean = false,
val jettonsError: String? = null,
val canLoadMoreJettons: Boolean = false,
val isStreamingConnected: Boolean? = null,
)
Loading
Loading