Skip to content

Commit

Permalink
[Issue-3901] feat: push multi-transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
bluezdot committed Dec 13, 2024
1 parent eee26a4 commit c5a84dc
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 6 deletions.
22 changes: 22 additions & 0 deletions packages/extension-base/src/koni/api/contract-handler/evm/web3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<TransactionConfig> {
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;
}
160 changes: 154 additions & 6 deletions packages/extension-base/src/koni/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -1558,6 +1558,154 @@ export default class KoniExtension {
});
}

private async makeCrossChainTransferV2 (inputData: RequestCrossChainTransfer): Promise<SWTransactionResponse> {
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<void>);
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<void> => {
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);

Check failure on line 1706 in packages/extension-base/src/koni/background/handlers/Extension.ts

View workflow job for this annotation

GitHub Actions / Build Development Preview

Unsafe return of an `any` typed value

Check failure on line 1706 in packages/extension-base/src/koni/background/handlers/Extension.ts

View workflow job for this annotation

GitHub Actions / Build Development Preview

Unsafe call of an `any` typed value
}

private async evmNftSubmitTransaction (inputData: NftTransactionRequest): Promise<SWTransactionResponse> {
const { networkKey, params, recipientAddress, senderAddress } = inputData;
const contractAddress = params.contractAddress as string;
Expand Down Expand Up @@ -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)':
Expand Down
12 changes: 12 additions & 0 deletions packages/extension-base/src/services/balance-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

0 comments on commit c5a84dc

Please sign in to comment.