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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class TonConnectViewModel(
private val getWalletByAddress: (String) -> ITONWallet?,
private val onRequestApproved: () -> Unit = {},
private val onRequestRejected: () -> Unit = {},
private val onRequestFailed: (String) -> Unit = {},
private val onSessionsChanged: () -> Unit = {},
private val onEmbeddedRequest: (TONWalletKitEvent) -> Unit = {},
) : ViewModel() {
Expand Down Expand Up @@ -207,6 +208,7 @@ class TonConnectViewModel(
isProcessing = false,
error = error.message ?: "Failed to approve transaction",
)
onRequestFailed(error.message ?: "Failed to approve transaction")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.math.BigInteger
import javax.inject.Inject
import kotlin.collections.ArrayDeque
import kotlin.collections.firstOrNull
Expand Down Expand Up @@ -151,6 +150,7 @@ class WalletKitViewModel @Inject constructor(
getWalletByAddress = { address -> lifecycleManager.tonWallets[address] },
onRequestApproved = { onTonConnectRequestApproved() },
onRequestRejected = { onTonConnectRequestRejected() },
onRequestFailed = { message -> onTonConnectRequestFailed(message) },
onSessionsChanged = { viewModelScope.launch { sessionsViewModel.refresh() } },
onEmbeddedRequest = { followUp -> handleSdkEvent(followUp) },
)
Expand Down Expand Up @@ -479,6 +479,12 @@ class WalletKitViewModel @Inject constructor(
pendingTonConnectAction = null
}

private fun onTonConnectRequestFailed(message: String) {
pendingTonConnectAction = null
dismissSheet()
eventLogger.showTemporaryStatus(uiString(R.string.wallet_status_transaction_failed, message))
}

private fun onTonConnectRequestRejected() {
when (val action = pendingTonConnectAction) {
is TonConnectAction.Connect -> {
Expand Down Expand Up @@ -1391,79 +1397,38 @@ class WalletKitViewModel @Inject constructor(

private fun onTransactionRequest(request: TONWalletTransactionRequest) {
Log.d(LOG_TAG, "=== onTransactionRequest called ===")
// Extract wallet address from active wallet
val walletAddress = state.value.activeWalletAddress ?: ""
val dAppInfo = request.event.dAppInfo
val fallbackDAppName = uiString(R.string.wallet_event_generic_dapp)
val txRequest = request.event.request

Log.d(LOG_TAG, "Transaction request - walletAddress: $walletAddress, dAppName: ${dAppInfo?.name}")

// Check balance before showing transaction UI (like web demo-wallet does)
viewModelScope.launch {
val wallet = lifecycleManager.tonWallets[walletAddress]
if (wallet != null) {
try {
val balance = wallet.balance()
val totalAmount = txRequest.messages.sumOf { msg ->
msg.amount.toBigIntegerOrNull() ?: BigInteger.ZERO
}
Log.d(LOG_TAG, "Balance check: balance=${balance.value}, totalAmount=$totalAmount")

if (balance.value.toBigInteger() < totalAmount) {
Log.d(LOG_TAG, "Insufficient balance - auto-rejecting transaction")
// Use NonCancellable to ensure rejection completes even if Activity goes to background
withContext(NonCancellable) {
// Use BAD_REQUEST_ERROR (1) for insufficient balance, matching web demo-wallet
request.reject("Insufficient balance", BAD_REQUEST_ERROR_CODE)
}
return@launch
}
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to check balance, proceeding with transaction UI", e)
// Continue to show the UI even if balance check fails
}
}

// Map actual transaction messages from request
val messages = txRequest.messages.map { msg ->
// Try to decode comment from payload if it's a simple text comment
val comment = try {
msg.payload?.let { _ ->
// Simple text comments are base64 encoded with opcode 0
// For now, we'll just show null - full decoding can be added later
null
}
} catch (_: Exception) {
null
}

TransactionMessageUi(
to = msg.address,
amount = msg.amount,
comment = comment,
payload = msg.payload?.value,
stateInit = msg.stateInit?.value,
)
}

val uiRequest = TransactionRequestUi(
id = request.hashCode().toString(),
walletAddress = walletAddress,
dAppName = dAppInfo?.name ?: fallbackDAppName,
validUntil = txRequest.validUntil?.toLong(),
messages = messages,
preview = null,
raw = JSONObject(),
transactionRequest = request,
val messages = txRequest.messages.map { msg ->
TransactionMessageUi(
to = msg.address,
amount = msg.amount,
comment = null,
payload = msg.payload?.value,
stateInit = msg.stateInit?.value,
)

Log.d(LOG_TAG, "Setting sheet to Transaction state with ${messages.size} messages")
uiCoordinator.setSheet(SheetState.Transaction(uiRequest))
Log.d(LOG_TAG, "Sheet state updated: ${state.value.sheetState}")
val eventDAppName = dAppInfo?.name ?: fallbackDAppName
eventLogger.log(R.string.wallet_event_transaction_request, eventDAppName)
}

val uiRequest = TransactionRequestUi(
id = request.hashCode().toString(),
walletAddress = walletAddress,
dAppName = dAppInfo?.name ?: fallbackDAppName,
validUntil = txRequest.validUntil?.toLong(),
messages = messages,
preview = null,
raw = JSONObject(),
transactionRequest = request,
)

Log.d(LOG_TAG, "Setting sheet to Transaction state with ${messages.size} messages")
uiCoordinator.setSheet(SheetState.Transaction(uiRequest))
val eventDAppName = dAppInfo?.name ?: fallbackDAppName
eventLogger.log(R.string.wallet_event_transaction_request, eventDAppName)
}

private fun onSignMessageRequest(request: TONWalletSignMessageRequest) {
Expand Down
1 change: 1 addition & 0 deletions AndroidDemo/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@
<string name="wallet_error_reject_transaction">Failed to reject transaction</string>
<string name="wallet_event_sign_data_approved">✅ Sign data approved</string>
<string name="wallet_status_signed_success">✅ Signed successfully</string>
<string name="wallet_status_transaction_failed">⚠️ Transaction failed: %1$s</string>
<string name="wallet_status_walletkit_ready">WalletKit ready</string>
<string name="wallet_event_sign_data_failed">❌ Sign data approval failed: %1$s</string>
<string name="wallet_error_approve_sign_request">Failed to approve sign request</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
package io.ton.walletkit.api.generated

import io.ton.walletkit.model.TONBase64
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

Expand All @@ -44,7 +43,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class TONConnectionApprovalProof(

@Contextual @SerialName(value = "signature")
@SerialName(value = "signature")
var signature: io.ton.walletkit.model.TONBase64,

@SerialName(value = "timestamp")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.
*/
@file:Suppress(
"ArrayInDataClass",
"EnumEntryName",
"RemoveRedundantQualifierName",
"UnusedImport",
)

package io.ton.walletkit.api.generated

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Discriminator for DeFi-style providers (swap quotes, staking, gasless relayers).
*
* Values: swap,staking,gasless
*/
@Serializable
enum class TONDefiProviderType(val value: kotlin.String) {

@SerialName(value = "swap")
swap("swap"),

@SerialName(value = "staking")
staking("staking"),

@SerialName(value = "gasless")
gasless("gasless"),
;

/**
* Override [toString()] to avoid using the enum variable name as the value, and instead use
* the actual value defined in the API spec file.
*
* This solves a problem when the variable name and its value are different, and ensures that
* the client sends the correct enum values to the server always.
*/
override fun toString(): kotlin.String = value

companion object {
/**
* Converts the provided [data] to a [String] on success, null otherwise.
*/
fun encode(data: kotlin.Any?): kotlin.String? = if (data is TONDefiProviderType) "$data" else null

/**
* Returns a valid [TONDefiProviderType] for [data], null otherwise.
*/
fun decode(data: kotlin.Any?): TONDefiProviderType? = data?.let {
val normalizedData = "$it".lowercase()
values().firstOrNull { value ->
it == value || normalizedData == "$value".lowercase()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ data class TONEmulationMessageContent(
@Contextual @SerialName(value = "hash")
var hash: io.ton.walletkit.model.TONHex? = null,

@Contextual @SerialName(value = "body")
@SerialName(value = "body")
var body: io.ton.walletkit.model.TONBase64? = null,

/* Structured decoded representation of the message body, if available */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.
*/
@file:Suppress(
"ArrayInDataClass",
"EnumEntryName",
"RemoveRedundantQualifierName",
"UnusedImport",
)

package io.ton.walletkit.api.generated

import io.ton.walletkit.model.TONUserFriendlyAddress
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Provider-level configuration for a gasless relayer on a given network. Bundles every piece of provider state a consumer needs to drive a gasless transfer end-to-end: - `relayAddress` — where the relayer wants residual TON (e.g. jetton-transfer `responseDestination`) returned to. - `supportedAssets` — what the relayer accepts as fee payment.
*
* @param relayAddress
* @param supportedAssets Assets the relayer accepts as fee payment.
*/
@Serializable
data class TONGaslessConfig(

@SerialName(value = "relayAddress")
var relayAddress: io.ton.walletkit.model.TONUserFriendlyAddress,

/* Assets the relayer accepts as fee payment. */
@SerialName(value = "supportedAssets")
var supportedAssets: kotlin.collections.List<TONGaslessSupportedAsset>,

) {

companion object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.
*/
@file:Suppress(
"ArrayInDataClass",
"EnumEntryName",
"RemoveRedundantQualifierName",
"UnusedImport",
)

package io.ton.walletkit.api.generated

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Static metadata for a gasless provider.
*
* @param name
* @param logo
* @param url
*/
@Serializable
data class TONGaslessProviderMetadata(

@SerialName(value = "name")
var name: kotlin.String,

@SerialName(value = "logo")
var logo: kotlin.String? = null,

@SerialName(value = "url")
var url: kotlin.String? = null,

) {

companion object
}
Loading
Loading