From c5a84dcaeb035969954604991e7b4f5078bcb831 Mon Sep 17 00:00:00 2001 From: bluezdot <72647326+bluezdot@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:04:20 +0700 Subject: [PATCH] [Issue-3901] feat: push multi-transaction --- .../src/koni/api/contract-handler/evm/web3.ts | 22 +++ .../src/koni/background/handlers/Extension.ts | 160 +++++++++++++++++- .../src/services/balance-service/index.ts | 12 ++ .../transfer/xcm/availBridge.ts | 5 + 4 files changed, 193 insertions(+), 6 deletions(-) diff --git a/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts b/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts index 7d8be82fe15..fd1d49fa3b1 100644 --- a/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts +++ b/packages/extension-base/src/koni/api/contract-handler/evm/web3.ts @@ -5,6 +5,7 @@ import { _Address } from '@subwallet/extension-base/background/KoniTypes'; import { _ERC20_ABI } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { _EvmApi } from '@subwallet/extension-base/services/chain-service/types'; import { calculateGasFeeParams } from '@subwallet/extension-base/services/fee-service/utils'; +import { TokenSpendingApprovalParams } from '@subwallet/extension-base/types'; import { TransactionConfig } from 'web3-core'; import { Contract } from 'web3-eth-contract'; @@ -49,3 +50,24 @@ export async function getERC20SpendingApprovalTx (spender: _Address, owner: _Add maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() } as TransactionConfig; } + +export async function getERC20SpendingApprovalTxV2 (evmApi: _EvmApi, { amount = '115792089237316195423570985008687907853269984665640564039457584007913129639935', contractAddress, owner, spenderAddress }: TokenSpendingApprovalParams): Promise { + const tokenContract = getERC20Contract(contractAddress, evmApi); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const approveCall = tokenContract.methods.approve(spenderAddress, amount); // TODO: need test + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment + const approveEncodedCall = approveCall.encodeABI() as string; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + const gasLimit = await approveCall.estimateGas({ from: owner }) as number; + const priority = await calculateGasFeeParams(evmApi, evmApi.chainSlug); + + return { + from: owner, + to: contractAddress, + data: approveEncodedCall, + gas: gasLimit, + gasPrice: priority.gasPrice, + maxFeePerGas: priority.maxFeePerGas?.toString(), + maxPriorityFeePerGas: priority.maxPriorityFeePerGas?.toString() + } as TransactionConfig; +} diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 7a33f1698ce..d17099969ba 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -15,8 +15,8 @@ import { additionalValidateTransferForRecipient, additionalValidateXcmTransfer, import { FrameSystemAccountInfo } from '@subwallet/extension-base/core/substrate/types'; import { _isSnowBridgeXcm } from '@subwallet/extension-base/core/substrate/xcm-parser'; import { ALLOWED_PATH } from '@subwallet/extension-base/defaults'; -import { getERC20SpendingApprovalTx } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; -import { _ERC721_ABI, isAvailBridgeGatewayContract, isSnowBridgeGatewayContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; +import { getERC20SpendingApprovalTx, getERC20SpendingApprovalTxV2 } from '@subwallet/extension-base/koni/api/contract-handler/evm/web3'; +import { _ERC721_ABI, getAvailBridgeGatewayContract, getSnowBridgeGatewayContract, isAvailBridgeGatewayContract, isSnowBridgeGatewayContract } from '@subwallet/extension-base/koni/api/contract-handler/utils'; import { resolveAzeroAddressToDomain, resolveAzeroDomainToAddress } from '@subwallet/extension-base/koni/api/dotsama/domain'; import { parseSubstrateTransaction } from '@subwallet/extension-base/koni/api/dotsama/parseTransaction'; import { UNSUPPORTED_TRANSFER_EVM_CHAIN_NAME } from '@subwallet/extension-base/koni/api/nft/config'; @@ -32,17 +32,17 @@ import { getERC20TransactionObject, getERC721Transaction, getEVMTransactionObjec import { createTransferExtrinsic, getTransferMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/token'; import { createTonTransaction } from '@subwallet/extension-base/services/balance-service/transfer/ton-transfer'; import { createAvailBridgeExtrinsicFromAvail, createAvailBridgeTxFromEth, createPolygonBridgeExtrinsic, createSnowBridgeExtrinsic, createXcmExtrinsic, CreateXcmExtrinsicProps, FunctionCreateXcmExtrinsic, getXcmMockTxFee } from '@subwallet/extension-base/services/balance-service/transfer/xcm'; -import { getClaimTxOnAvail, getClaimTxOnEthereum, isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; +import { getClaimTxOnAvail, getClaimTxOnEthereum, isAvailBridgeFromEvmToAvail, isAvailChainBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/availBridge'; import { _isPolygonChainBridge, getClaimPolygonBridge, isClaimedPolygonBridge } from '@subwallet/extension-base/services/balance-service/transfer/xcm/polygonBridge'; import { _API_OPTIONS_CHAIN_GROUP, _DEFAULT_MANTA_ZK_CHAIN, _MANTA_ZK_CHAIN_GROUP, _ZK_ASSET_PREFIX, SUFFICIENT_CHAIN } from '@subwallet/extension-base/services/chain-service/constants'; import { _ChainApiStatus, _ChainConnectionStatus, _ChainState, _NetworkUpsertParams, _SubstrateAdapterQueryArgs, _SubstrateApi, _ValidateCustomAssetRequest, _ValidateCustomAssetResponse, EnableChainParams, EnableMultiChainParams } from '@subwallet/extension-base/services/chain-service/types'; import { _getAssetDecimals, _getAssetSymbol, _getChainNativeTokenBasicInfo, _getContractAddressOfToken, _getEvmChainId, _getTokenOnChainAssetId, _getXcmAssetMultilocation, _isAssetSmartContractNft, _isBridgedToken, _isChainEvmCompatible, _isChainSubstrateCompatible, _isChainTonCompatible, _isCustomAsset, _isLocalToken, _isMantaZkAsset, _isNativeToken, _isPureEvmChain, _isTokenEvmSmartContract, _isTokenTransferredByEvm, _isTokenTransferredByTon } from '@subwallet/extension-base/services/chain-service/utils'; -import { _NotificationInfo, NotificationSetup } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; +import { NotificationSetup } from '@subwallet/extension-base/services/inapp-notification-service/interfaces'; import { AppBannerData, AppConfirmationData, AppPopupData } from '@subwallet/extension-base/services/mkt-campaign-service/types'; import { EXTENSION_REQUEST_URL } from '@subwallet/extension-base/services/request-service/constants'; import { AuthUrls } from '@subwallet/extension-base/services/request-service/types'; import { DEFAULT_AUTO_LOCK_TIME } from '@subwallet/extension-base/services/setting-service/constants'; -import { SWTransaction, SWTransactionResponse, SWTransactionResult, TransactionEmitter, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; +import { SWTransaction, SWTransactionInput, SWTransactionResponse, SWTransactionResult, TransactionEmitter, ValidateTransactionResponseInput } from '@subwallet/extension-base/services/transaction-service/types'; import { isProposalExpired, isSupportWalletConnectChain, isSupportWalletConnectNamespace } from '@subwallet/extension-base/services/wallet-connect-service/helpers'; import { ResultApproveWalletConnectSession, WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; import { SWStorage } from '@subwallet/extension-base/storage'; @@ -1558,6 +1558,154 @@ export default class KoniExtension { }); } + private async makeCrossChainTransferV2 (inputData: RequestCrossChainTransfer): Promise { + const { destinationNetworkKey, from, originNetworkKey, to, tokenSlug, transferAll, transferBounceable, value } = inputData; + const originTokenInfo = this.#koniState.getAssetBySlug(tokenSlug); + const isNeedApproveSpending = this.#koniState.balanceService.checkNeedApproveSpending(originNetworkKey, destinationNetworkKey); + const transactionsList: SWTransactionInput[] = []; + + if (isNeedApproveSpending) { + const contractAddress = _getContractAddressOfToken(originTokenInfo); + const isBridgeFromEvmToAvail = isAvailBridgeFromEvmToAvail(originNetworkKey, destinationNetworkKey); + const spenderAddress = isBridgeFromEvmToAvail ? getAvailBridgeGatewayContract(originNetworkKey) : getSnowBridgeGatewayContract(originNetworkKey); + const evmApi = this.#koniState.getEvmApi(originNetworkKey); + + const tokenSpendingApprovalParam: TokenSpendingApprovalParams = { + spenderAddress, + contractAddress, + owner: from, + chain: originNetworkKey, + amount: value + }; + + const transactionConfig = await getERC20SpendingApprovalTxV2(evmApi, tokenSpendingApprovalParam); // todo: convert params to interface TokenSpendingApprovalParams + + transactionsList.push({ + errors: [], + warnings: [], + address: from, + chain: originNetworkKey, + chainType: ChainType.EVM, + transferNativeAmount: '0', + transaction: transactionConfig, + data: tokenSpendingApprovalParam, + resolveOnDone: true, + extrinsicType: ExtrinsicType.TOKEN_SPENDING_APPROVAL, + isTransferAll: false + }); + } + + const destinationTokenInfo = this.#koniState.getXcmEqualAssetByChain(destinationNetworkKey, tokenSlug); + const [errors, fromKeyPair] = validateXcmTransferRequest(destinationTokenInfo, from, value); + let extrinsic: SubmittableExtrinsic<'promise'> | TransactionConfig | null = null; + + if (errors.length > 0) { + return this.#koniState.transactionService.generateBeforeHandleResponseErrors(errors); + } + + const chainInfoMap = this.#koniState.getChainInfoMap(); + const isAvailBridgeFromEvm = _isPureEvmChain(chainInfoMap[originNetworkKey]) && isAvailChainBridge(destinationNetworkKey); + const isAvailBridgeFromAvail = isAvailChainBridge(originNetworkKey) && _isPureEvmChain(chainInfoMap[destinationNetworkKey]); + const isSnowBridgeEvmTransfer = _isPureEvmChain(chainInfoMap[originNetworkKey]) && _isSnowBridgeXcm(chainInfoMap[originNetworkKey], chainInfoMap[destinationNetworkKey]) && !isAvailBridgeFromEvm; + const isPolygonBridgeTransfer = _isPolygonChainBridge(originNetworkKey, destinationNetworkKey); + + let additionalValidator: undefined | ((inputTransaction: SWTransactionResponse) => Promise); + let eventsHandler: undefined | ((eventEmitter: TransactionEmitter) => void); + + if (fromKeyPair && destinationTokenInfo) { + const evmApi = this.#koniState.getEvmApi(originNetworkKey); + const substrateApi = this.#koniState.getSubstrateApi(originNetworkKey); + const params: CreateXcmExtrinsicProps = { + destinationTokenInfo, + originTokenInfo, + sendingValue: value, + sender: from, + recipient: to, + chainInfoMap, + substrateApi, + evmApi + }; + + let funcCreateExtrinsic: FunctionCreateXcmExtrinsic; + + if (isPolygonBridgeTransfer) { + funcCreateExtrinsic = createPolygonBridgeExtrinsic; + } else if (isSnowBridgeEvmTransfer) { + funcCreateExtrinsic = createSnowBridgeExtrinsic; + } else if (isAvailBridgeFromEvm) { + funcCreateExtrinsic = createAvailBridgeTxFromEth; + } else if (isAvailBridgeFromAvail) { + funcCreateExtrinsic = createAvailBridgeExtrinsicFromAvail; + } else { + funcCreateExtrinsic = createXcmExtrinsic; + } + + extrinsic = await funcCreateExtrinsic(params); + + additionalValidator = async (inputTransaction: SWTransactionResponse): Promise => { + const { value: senderTransferable } = await this.getAddressTransferableBalance({ address: from, networkKey: originNetworkKey, token: originTokenInfo.slug }); + const isSnowBridge = _isSnowBridgeXcm(chainInfoMap[originNetworkKey], chainInfoMap[destinationNetworkKey]); + let recipientNativeBalance = '0'; + + if (isSnowBridge) { + const { value } = await this.getAddressTransferableBalance({ address: to, networkKey: destinationNetworkKey, extrinsicType: ExtrinsicType.TRANSFER_BALANCE }); + + recipientNativeBalance = value; + } + + const [warning, error] = additionalValidateXcmTransfer(originTokenInfo, destinationTokenInfo, value, senderTransferable, recipientNativeBalance, chainInfoMap[destinationNetworkKey], isSnowBridge); + + error && inputTransaction.errors.push(error); + warning && inputTransaction.warnings.push(warning); + }; + + eventsHandler = (eventEmitter: TransactionEmitter) => { + eventEmitter.on('send', () => { + try { + const dest = keyring.getPair(to); + + if (dest) { + this.updateAssetSetting({ + autoEnableNativeToken: false, + tokenSlug: destinationTokenInfo.slug, + assetSetting: { visible: true } + }).catch(console.error); + } + } catch (e) { + } + }); + }; + } + + const ignoreWarnings: BasicTxWarningCode[] = []; + + if (transferAll) { + ignoreWarnings.push(BasicTxWarningCode.NOT_ENOUGH_EXISTENTIAL_DEPOSIT); + } + + if (transferBounceable) { + ignoreWarnings.push(BasicTxWarningCode.IS_BOUNCEABLE_ADDRESS); + } + + transactionsList.push({ + url: EXTENSION_REQUEST_URL, + address: from, + chain: originNetworkKey, + transaction: extrinsic, + data: inputData, + extrinsicType: ExtrinsicType.TRANSFER_XCM, + chainType: !isSnowBridgeEvmTransfer && !isAvailBridgeFromEvm && !isPolygonBridgeTransfer ? ChainType.SUBSTRATE : ChainType.EVM, + transferNativeAmount: _isNativeToken(originTokenInfo) ? value : '0', + ignoreWarnings, + isTransferAll: transferAll, + errors, + additionalValidator: additionalValidator, + eventsHandler: eventsHandler + }); + + return await this.#koniState.transactionService.handleTransactionV2(transactionsList); + } + private async evmNftSubmitTransaction (inputData: NftTransactionRequest): Promise { const { networkKey, params, recipientAddress, senderAddress } = inputData; const contractAddress = params.contractAddress as string; @@ -4280,7 +4428,7 @@ export default class KoniExtension { case 'pri(accounts.transfer)': return await this.makeTransfer(request as RequestTransfer); case 'pri(accounts.crossChainTransfer)': - return await this.makeCrossChainTransfer(request as RequestCrossChainTransfer); + return await this.makeCrossChainTransferV2(request as RequestCrossChainTransfer); case 'pri(accounts.getOptimalTransferProcess)': return await this.getOptimalTransferProcess(request as RequestOptimalTransferProcess); case 'pri(accounts.approveSpending)': diff --git a/packages/extension-base/src/services/balance-service/index.ts b/packages/extension-base/src/services/balance-service/index.ts index 9db8df64e1b..bb31526053f 100644 --- a/packages/extension-base/src/services/balance-service/index.ts +++ b/packages/extension-base/src/services/balance-service/index.ts @@ -577,4 +577,16 @@ export class BalanceService implements StoppableServiceInterface { return getDefaultTransferProcess(); } + + public checkNeedApproveSpending (fromChain: string, toChain: string) { + const originChainInfo = this.state.chainService.getChainInfoByKey(fromChain); + + if (!toChain) { // normal transfers + return false; + } + + const destChainInfo = this.state.chainService.getChainInfoByKey(toChain); + + return !_isXcmWithinSameConsensus(originChainInfo, destChainInfo) && _isPureEvmChain(originChainInfo); + } } diff --git a/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts b/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts index 1253eac4964..fd673decaec 100644 --- a/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts +++ b/packages/extension-base/src/services/balance-service/transfer/xcm/availBridge.ts @@ -257,3 +257,8 @@ function getAvailBridgeAbi (chainSlug: string) { export function isAvailChainBridge (chainSlug: string) { return ['avail_mainnet', 'availTuringTest'].includes(chainSlug); } + +export function isAvailBridgeFromEvmToAvail (fromChain: string, toChain: string) { + return (fromChain === COMMON_CHAIN_SLUGS.ETHEREUM && toChain === 'avail_mainnet') || + (fromChain === COMMON_CHAIN_SLUGS.ETHEREUM_SEPOLIA && toChain === 'availTuringTest'); +}