Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UFT-1398 Fix WalletConnect cost estimation failing #333

Merged
merged 3 commits into from
Apr 16, 2024
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- Sending WalletConnect transaction with 0 energy if its payload is too large

## [1.5.1] - 2024-03-18

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ tasks.withType(Test) {

def versionBuildNumberMajor = 1
def versionBuildNumberMinor = 5
def versionBuildNumberPatch = 1
def versionBuildNumberMeta = ""
def versionBuildNumberSequential = 70
def versionBuildNumberPatch = 2
def versionBuildNumberMeta = "-qa.1"
def versionBuildNumberSequential = 71

task printVersionName {
doLast {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.concordium.wallet.data.model

import com.google.gson.annotations.SerializedName
import java.io.Serializable
import java.math.BigInteger

Expand All @@ -20,4 +21,7 @@ data class ChainParameters(
val transactionCommissionRange: TransactionCommissionRange,
val transactionCommissionRate: Double? = null,
val finalizationCommissionRange: FinalizationCommissionRange,
val euroPerEnergy: SimpleFraction,
@SerializedName("microGTUPerEuro")
val microGtuPerEuro: SimpleFraction,
) : Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.concordium.wallet.data.model

import java.math.BigInteger

/**
* A fraction defined as 2 integers:
* ```
* N
* –
* D
* ```
*/
data class SimpleFraction(
val numerator: BigInteger,
val denominator: BigInteger,
)
17 changes: 15 additions & 2 deletions app/src/main/java/com/concordium/wallet/data/model/TransferCost.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
package com.concordium.wallet.data.model

import java.math.BigInteger

data class TransferCost(
val energy: Long,
val cost: String,
val cost: BigInteger,
val success: Boolean?
)
){
constructor(
energy: Long,
euroPerEnergy: SimpleFraction,
microGTUPerEuro: SimpleFraction,
) : this(
energy = energy,
cost = (energy.toBigInteger() * euroPerEnergy.numerator * microGTUPerEuro.numerator)
.divide(euroPerEnergy.denominator * microGTUPerEuro.denominator),
success = null,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.concordium.wallet.data.util

import com.walletconnect.util.hexToBytes
import okio.utf8Size

/**
* Local transaction cost calculations.
*
* @see <a href="https://github.com/Concordium/concordium-rust-sdk/blob/01b00f7a82e62d3642be51282e6a89045727759d/src/types/transactions.rs#L1276">Rust SDK reference methods</a>
*/
object TransactionCostCalculator {
/**
* Base cost of a transaction is the minimum cost that accounts for
* transaction size and signature checking. In addition to base cost
* each transaction has a transaction-type specific cost.
*
* @param transactionSize size of the transaction, for example [getContractTransactionSize].
* @param numSignatures number of signatures, which is 1 unless we implement multisig in the wallet.
*
* @return energy required to execute this transaction.
*/
fun getBaseCostEnergy(
transactionSize: Long,
numSignatures: Int = 1,
): Long =
B * transactionSize + A * numSignatures

/**
* @param receiveName `contractName.methodName`.
* @param message serialized parameters in hex.
*
* @return size in bytes of a transaction invoking a smart contract.
*/
fun getContractTransactionSize(
receiveName: String,
message: String,
): Long =
TRANSACTION_HEADER_SIZE +
TRANSACTION_TAG_SIZE +
8 + 16 +
2 + receiveName.utf8Size() +
2 + message.hexToBytes().size

/**
* The B constant for NRG assignment.
* This scales the effect of the number of signatures on the energy.
*/
private const val A: Long = 100

/**
* The A constant for NRG assignment.
* This scales the effect of transaction size on the energy.
*/
private const val B: Long = 1

/**
* Size of a transaction header.
* This is currently always 60 bytes.
* Future chain updates might revise this,
* but this is a big change so this is expected to change seldomly.
*/
private const val TRANSACTION_HEADER_SIZE: Long = 32 + 8 + 8 + 4 + 8

private const val TRANSACTION_TAG_SIZE: Long = 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ sealed interface Payload {
data class ContractUpdateTransaction(
val address: ContractAddress,
val amount: String,
var maxEnergy: Long,
var maxContractExecutionEnergy: Long,
/**
* Energy for the whole transaction including the administrative fee.
* Legacy field.
*/
val maxEnergy: Long?,
/**
* Energy for the smart contract execution only,
* without the administrative transaction fee.
*/
val maxContractExecutionEnergy: Long?,
val message: String,
val receiveName: String
) : Payload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import com.concordium.wallet.data.util.FileUtil
import com.concordium.wallet.ui.common.BackendErrorHandler
import com.concordium.wallet.util.DateTimeUtil
import com.concordium.wallet.util.Log
import com.concordium.wallet.util.toBigInteger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
Expand Down Expand Up @@ -358,7 +357,7 @@ class DelegationBakerViewModel(application: Application) : AndroidViewModel(appl
openStatus = openStatus,
success = {
bakerDelegationData.energy = it.energy
bakerDelegationData.cost = it.cost.toBigInteger()
bakerDelegationData.cost = it.cost
if (notifyObservers)
_transactionFeeLiveData.value = Pair(bakerDelegationData.cost, requestId)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import com.concordium.wallet.ui.common.BackendErrorHandler
import com.concordium.wallet.util.DateTimeUtil
import com.concordium.wallet.util.Log
import com.concordium.wallet.util.TokenUtil
import com.concordium.wallet.util.toBigInteger
import com.concordium.wallet.util.toHex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -275,7 +274,7 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
handleBackendError(TransactionSimulationException())
} else {
sendTokenData.energy = it.energy
sendTokenData.fee = it.cost.toBigInteger()
sendTokenData.fee = it.cost
sendTokenData.account?.let { account ->
sendTokenData.max =
account.getAtDisposalWithoutStakedOrScheduled(account.totalUnshieldedBalance) -
Expand Down Expand Up @@ -313,7 +312,7 @@ class SendTokenViewModel(application: Application) : AndroidViewModel(applicatio
handleBackendError(TransactionSimulationException())
} else {
sendTokenData.energy = it.energy
sendTokenData.fee = it.cost.toBigInteger()
sendTokenData.fee = it.cost
waiting.postValue(false)
feeReady.postValue(sendTokenData.fee)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class SendFundsViewModel(application: Application) : AndroidViewModel(applicatio
memoSize = if (tempData.memo == null) null else tempData.memo!!.length / 2, //div by 2 because hex takes up twice the length
success = {
tempData.energy = it.energy
_transactionFeeLiveData.value = it.cost.toBigInteger()
_transactionFeeLiveData.value = it.cost
updateSendAllAmount()
},
failure = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import com.concordium.wallet.data.cryptolib.StorageAccountData
import com.concordium.wallet.data.model.AccountData
import com.concordium.wallet.data.model.AccountNonce
import com.concordium.wallet.data.model.SubmissionData
import com.concordium.wallet.data.model.TransferCost
import com.concordium.wallet.data.room.Account
import com.concordium.wallet.data.room.AccountWithIdentity
import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.data.util.TransactionCostCalculator
import com.concordium.wallet.data.walletconnect.Payload
import com.concordium.wallet.data.walletconnect.TransactionError
import com.concordium.wallet.data.walletconnect.TransactionSuccess
Expand All @@ -49,7 +51,6 @@ import javax.crypto.Cipher
data class WalletConnectData(
var account: Account? = null,
var wcUri: String? = null,
var energy: Long? = null,
var maxEnergy: Long? = null,
var cost: BigInteger? = null,
var isTransaction: Boolean = true
Expand Down Expand Up @@ -161,24 +162,37 @@ class WalletConnectViewModel(application: Application) : AndroidViewModel(applic
binder?.getSessionRequestParams()?.parsePayload()?.let { payload ->
when (payload) {
is Payload.ContractUpdateTransaction -> {
proxyRepository.getTransferCost(
type = "update",
amount = payload.amount.toBigInteger(),
sender = walletConnectData.account!!.address,
contractIndex = payload.address.index,
contractSubindex = payload.address.subIndex,
receiveName = payload.receiveName,
parameter = payload.message,
success = {
walletConnectData.maxEnergy =
if (payload.maxEnergy != 0L) payload.maxEnergy else payload.maxContractExecutionEnergy
walletConnectData.cost = it.cost.toBigInteger()
walletConnectData.energy = it.energy
proxyRepository.getChainParameters(
success = { chainParameters ->
val maxEnergy: Long =
if (payload.maxEnergy != null) {
// Use the legacy total value if provided.
payload.maxEnergy
} else if (payload.maxContractExecutionEnergy != null) {
// Calculate the total value locally if only the execution energy provided.
// Max energy = contract execution energy + base transaction energy.
payload.maxContractExecutionEnergy +
TransactionCostCalculator.getBaseCostEnergy(
transactionSize = TransactionCostCalculator.getContractTransactionSize(
receiveName = payload.receiveName,
message = payload.message,
),
)
} else {
error("The account transaction payload must contain either maxEnergy or maxContractExecutionEnergy")
}

val cost = TransferCost(
energy = maxEnergy,
euroPerEnergy = chainParameters.euroPerEnergy,
microGTUPerEuro = chainParameters.microGtuPerEuro,
)

walletConnectData.maxEnergy = cost.energy
walletConnectData.cost = cost.cost
transactionFee.postValue(walletConnectData.cost)
},
failure = {
handleBackendError(it)
}
failure = ::handleBackendError
Radiokot marked this conversation as resolved.
Show resolved Hide resolved
)
}

Expand All @@ -187,13 +201,11 @@ class WalletConnectViewModel(application: Application) : AndroidViewModel(applic
type = ProxyRepository.SIMPLE_TRANSFER,
memoSize = null,
success = {
walletConnectData.energy = it.energy
walletConnectData.cost = it.cost.toBigInteger()
walletConnectData.maxEnergy = it.energy
walletConnectData.cost = it.cost
transactionFee.postValue(walletConnectData.cost)
},
failure = {
handleBackendError(it)
}
failure = ::handleBackendError
)
}
}
Expand Down Expand Up @@ -295,10 +307,6 @@ class WalletConnectViewModel(application: Application) : AndroidViewModel(applic
errorInt.postValue(R.string.app_error_lib)
return
}
if (payload is Payload.ContractUpdateTransaction) {
payload.maxEnergy = walletConnectData.energy ?: 0
payload.maxContractExecutionEnergy = walletConnectData.maxEnergy ?: 0
}
val accountTransactionInput = CreateAccountTransactionInput(
expiry.toInt(),
from,
Expand Down
Loading