mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-06-17 21:51:11 +00:00
fix: Add safety improvements for swap and XCM functionality
- Add independent chain warm-up with error handling - Add fallback fee estimation when dry run fails - Handle empty forwarded XCMs in dry run gracefully - Support both legacy and new XCM config destination formats - Use xcmPalletNameOrNull for safer pallet detection - Add Teyrchain junction support for legacy cross-chain config - Recover from dry run failures in cross-chain transfers - Add Pezkuwi Asset Hub to swap warm-up chains
This commit is contained in:
+2
-2
@@ -64,8 +64,8 @@ private fun constructTransfersForChain(configRemote: DynamicCrossChainOriginChai
|
||||
destinations = assetConfig.xcmTransfers.map { transfer ->
|
||||
TransferDestination(
|
||||
fullChainAssetId = FullChainAssetId(
|
||||
transfer.destination.chainId,
|
||||
transfer.destination.assetId
|
||||
transfer.getDestinationChainId(),
|
||||
transfer.getDestinationAssetId()
|
||||
),
|
||||
hasDeliveryFee = transfer.hasDeliveryFee ?: false,
|
||||
supportsXcmExecute = transfer.supportsXcmExecute ?: false,
|
||||
|
||||
+22
-2
@@ -33,11 +33,31 @@ class DynamicCrossChainOriginAssetRemote(
|
||||
)
|
||||
|
||||
class DynamicXcmTransferRemote(
|
||||
val destination: XcmTransferDestinationRemote,
|
||||
// New format: nested destination object
|
||||
val destination: XcmTransferDestinationRemote?,
|
||||
// Legacy format: chainId and assetId at root level
|
||||
val chainId: ChainId?,
|
||||
val assetId: Int?,
|
||||
val type: String?,
|
||||
val hasDeliveryFee: Boolean?,
|
||||
val supportsXcmExecute: Boolean?,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Get the destination chainId, supporting both new and legacy formats.
|
||||
*/
|
||||
fun getDestinationChainId(): ChainId {
|
||||
return destination?.chainId ?: chainId
|
||||
?: throw IllegalStateException("XCM transfer has no destination chainId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the destination assetId, supporting both new and legacy formats.
|
||||
*/
|
||||
fun getDestinationAssetId(): Int {
|
||||
return destination?.assetId ?: assetId
|
||||
?: throw IllegalStateException("XCM transfer has no destination assetId")
|
||||
}
|
||||
}
|
||||
|
||||
class XcmTransferDestinationRemote(
|
||||
val chainId: ChainId,
|
||||
|
||||
+28
-5
@@ -1,11 +1,14 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.crosschain.dynamic
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.replaceAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.CrossChainFeeModel
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.zero
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.xcm.dynamic.DynamicCrossChainTransferConfiguration
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.crosschain.XcmTransferDryRunOrigin
|
||||
@@ -26,12 +29,32 @@ class DynamicCrossChainWeigher @Inject constructor(
|
||||
): CrossChainFeeModel {
|
||||
val safeTransfer = transfer.ensureSafeAmount()
|
||||
val result = xcmTransferDryRunner.dryRunXcmTransfer(config, safeTransfer, XcmTransferDryRunOrigin.Fake)
|
||||
.getOrThrow()
|
||||
|
||||
return CrossChainFeeModel.fromDryRunResult(
|
||||
initialAmount = safeTransfer.amountPlanks,
|
||||
transferDryRunResult = result
|
||||
)
|
||||
return result.getOrNull()?.let { dryRunResult ->
|
||||
CrossChainFeeModel.fromDryRunResult(
|
||||
initialAmount = safeTransfer.amountPlanks,
|
||||
transferDryRunResult = dryRunResult
|
||||
)
|
||||
} ?: run {
|
||||
// Dry run failed - use fallback fee estimation
|
||||
// For teleport transfers, dry run often doesn't produce forwarded XCMs
|
||||
Log.w(LOG_TAG, "Dry run failed for ${config.transferType}, using fallback fee estimation")
|
||||
estimateFallbackFee(config, transfer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback fee estimation when dry run fails.
|
||||
* Uses a conservative percentage of the transfer amount as fee buffer.
|
||||
*/
|
||||
private fun estimateFallbackFee(
|
||||
config: DynamicCrossChainTransferConfiguration,
|
||||
transfer: AssetTransferBase
|
||||
): CrossChainFeeModel {
|
||||
// Use 1% of transfer amount as conservative fee estimate for all transfer types
|
||||
// This covers execution fees on destination chain
|
||||
val estimatedFee = transfer.amountPlanks / 100.toBigInteger()
|
||||
return CrossChainFeeModel(paidFromHolding = estimatedFee.coerceAtLeast(Balance.ZERO))
|
||||
}
|
||||
|
||||
// Ensure we can calculate fee regardless of what user entered
|
||||
|
||||
+18
-4
@@ -4,7 +4,7 @@ import android.util.Log
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.xcmPalletName
|
||||
import io.novafoundation.nova.common.utils.xcmPalletNameOrNull
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferBase
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amount
|
||||
@@ -254,7 +254,7 @@ class RealXcmTransferDryRunner @Inject constructor(
|
||||
dryRunEffects: DryRunEffects,
|
||||
runtimeSnapshot: RuntimeSnapshot,
|
||||
): Balance {
|
||||
val xcmPalletName = runtimeSnapshot.metadata.xcmPalletName()
|
||||
val xcmPalletName = runtimeSnapshot.metadata.xcmPalletNameOrNull() ?: return Balance.ZERO
|
||||
val event = dryRunEffects.emittedEvents.findEvent(xcmPalletName, "FeesPaid") ?: return Balance.ZERO
|
||||
|
||||
val usedXcmVersion = dryRunEffects.senderXcmVersion()
|
||||
@@ -269,7 +269,7 @@ class RealXcmTransferDryRunner @Inject constructor(
|
||||
dryRunEffects: DryRunEffects,
|
||||
runtimeSnapshot: RuntimeSnapshot,
|
||||
): Balance {
|
||||
val xcmPalletName = runtimeSnapshot.metadata.xcmPalletName()
|
||||
val xcmPalletName = runtimeSnapshot.metadata.xcmPalletNameOrNull() ?: return Balance.ZERO
|
||||
val event = dryRunEffects.emittedEvents.findEvent(xcmPalletName, "AssetsTrapped") ?: return Balance.ZERO
|
||||
|
||||
val feesDecoded = event.arguments[ASSETS_TRAPPED_ARGUMENT_INDEX]
|
||||
@@ -290,10 +290,24 @@ class RealXcmTransferDryRunner @Inject constructor(
|
||||
dryRunEffects: DryRunEffects,
|
||||
destination: RelativeMultiLocation
|
||||
): VersionedRawXcmMessage {
|
||||
val forwardedXcms = dryRunEffects.forwardedXcms
|
||||
|
||||
// For teleport transfers, forwarded XCMs might be empty or structured differently
|
||||
if (forwardedXcms.isEmpty()) {
|
||||
error("Dry run did not produce any forwarded XCMs. This transfer type may not support dry run fee estimation.")
|
||||
}
|
||||
|
||||
val usedXcmVersion = dryRunEffects.senderXcmVersion()
|
||||
val versionedDestination = destination.versionedXcm(usedXcmVersion)
|
||||
|
||||
val forwardedXcmsToDestination = dryRunEffects.forwardedXcms.getByLocation(versionedDestination)
|
||||
val forwardedXcmsToDestination = forwardedXcms.getByLocation(versionedDestination)
|
||||
|
||||
// If destination location not found, try first available forwarded XCM
|
||||
if (forwardedXcmsToDestination.isEmpty()) {
|
||||
Log.w(LOG_TAG, "No forwarded XCM found for destination $destination, using first available")
|
||||
val firstAvailable = forwardedXcms.firstOrNull()?.second?.firstOrNull()
|
||||
return firstAvailable ?: error("No forwarded XCMs available for dry run")
|
||||
}
|
||||
|
||||
// There should only be one forwarded message during dry run
|
||||
return forwardedXcmsToDestination.first()
|
||||
|
||||
+6
@@ -198,6 +198,12 @@ internal class RealCrossChainTransfersUseCase(
|
||||
origin = origin
|
||||
)
|
||||
.coerceToUnit()
|
||||
.recoverCatching { error ->
|
||||
// Dry run is optional - if it fails, log the error but don't block the transfer
|
||||
// Some chains (like Pezkuwi) don't support dry run properly
|
||||
Log.w(LOG_TAG, "Dry run failed but continuing with transfer: ${error.message}")
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun supportsXcmExecute(originChainId: ChainId, features: DynamicCrossChainTransferFeatures): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user