diff --git a/packages/extension-base/src/background/KoniTypes.ts b/packages/extension-base/src/background/KoniTypes.ts index 89c710bba73..cb7e4fd2c7f 100644 --- a/packages/extension-base/src/background/KoniTypes.ts +++ b/packages/extension-base/src/background/KoniTypes.ts @@ -1,7 +1,7 @@ // Copyright 2019-2022 @polkadot/extension-koni authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { Psbt, PsbtTxInput, PsbtTxOutput } from 'bitcoinjs-lib'; +import type { Psbt } from 'bitcoinjs-lib'; import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _FundStatus, _MultiChainAsset } from '@subwallet/chain-list/types'; import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; @@ -13,7 +13,7 @@ import { _BitcoinApi, _ChainState, _EvmApi, _NetworkUpsertParams, _SubstrateApi, import { CrowdloanContributionsResponse } from '@subwallet/extension-base/services/subscan-service/types'; import { BitcoinTransactionData, SWTransactionResponse, SWTransactionResult } from '@subwallet/extension-base/services/transaction-service/types'; import { WalletConnectNotSupportRequest, WalletConnectSessionRequest } from '@subwallet/extension-base/services/wallet-connect-service/types'; -import { BalanceJson, BitcoinFeeDetail, BuyServiceInfo, BuyTokenInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseEarlyValidateYield, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, SubmitYieldStepData, TokenApproveData, UnlockDotTransactionNft, UnstakingStatus, UtxoResponseItem, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo, YieldValidationStatus } from '@subwallet/extension-base/types'; +import { BalanceJson, BuyServiceInfo, BuyTokenInfo, EarningRewardHistoryItem, EarningRewardJson, EarningStatus, HandleYieldStepParams, LeavePoolAdditionalData, NominationPoolInfo, OptimalYieldPath, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitSignPsbtTransfer, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseEarlyValidateYield, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, ResponseSubscribeTransferConfirmation, SubmitYieldStepData, TokenApproveData, UnlockDotTransactionNft, UnstakingStatus, ValidateYieldProcessParams, YieldPoolInfo, YieldPositionInfo, YieldValidationStatus } from '@subwallet/extension-base/types'; import { InjectedAccount, InjectedAccountWithMeta, MetadataDefBase } from '@subwallet/extension-inject/types'; import { KeypairType, KeyringPair$Json, KeyringPair$Meta } from '@subwallet/keyring/types'; import { KeyringOptions } from '@subwallet/ui-keyring/options/types'; @@ -1344,10 +1344,18 @@ export interface BitcoinSendTransactionParams { recipients: BitcoinRecipientTransactionParams[] } +export interface PsbtTransactionArg { + address?: string; + amount?: string; +} + export interface BitcoinSignPsbtPayload extends Omit{ - txInput: PsbtTxInput[]; - txOutput: PsbtTxOutput[]; - psbt: Psbt + txInput: PsbtTransactionArg[]; + txOutput: PsbtTransactionArg[]; + to: string; + value: string; + psbt: Psbt; + tokenSlug: string; } enum SignatureHash { @@ -1363,7 +1371,7 @@ export interface BitcoinSignPsbtRawRequest { allowedSighash?: SignatureHash[]; signAtIndex?: number | number[]; broadcast?: boolean; - network: 'mainnet' | 'testnet'; + network: string; account: string; } @@ -1391,10 +1399,7 @@ export interface EvmSendTransactionRequest extends TransactionConfig, EvmSignReq isToContract: boolean; } -export interface BitcoinSendTransactionRequest extends BitcoinSignRequest, BitcoinTransactionConfig { - outputs?: BitcoinOutputUtox[], - inputs?: UtxoResponseItem[], -} +export interface BitcoinSendTransactionRequest extends BitcoinSignRequest, BitcoinTransactionConfig {} export type EvmWatchTransactionRequest = EvmSendTransactionRequest; export type BitcoinWatchTransactionRequest = BitcoinSendTransactionRequest; @@ -1416,11 +1421,10 @@ export interface BitcoinOutputUtox { export interface BitcoinTransactionConfig{ id?: string, from?: string | number; - to?: string; + to?: BitcoinRecipientTransactionParams[]; value?: number | string | BN; networkKey?: string; tokenSlug?: string; - fee?: BitcoinFeeDetail; } export interface SignMessageBitcoinResult { @@ -2563,6 +2567,7 @@ export interface KoniRequestSignatures { 'pri(transfer.getExistentialDeposit)': [RequestTransferExistentialDeposit, string]; 'pri(transfer.getMaxTransferable)': [RequestMaxTransferable, AmountData]; 'pri(transfer.subscribe)': [RequestSubscribeTransfer, ResponseSubscribeTransfer, ResponseSubscribeTransfer]; + 'pri(transfer.confirmation.subscribe)': [RequestSubscribeTransfer, ResponseSubscribeTransferConfirmation, ResponseSubscribeTransferConfirmation]; 'pri(subscription.cancel)': [string, boolean]; 'pri(freeBalance.get)': [RequestFreeBalance, AmountData]; 'pri(freeBalance.subscribe)': [RequestFreeBalance, AmountDataWithId, AmountDataWithId]; @@ -2570,7 +2575,8 @@ export interface KoniRequestSignatures { // Transfer 'pri(accounts.checkTransfer)': [RequestCheckTransfer, ValidateTransactionResponse]; 'pri(accounts.transfer)': [RequestSubmitTransfer, SWTransactionResponse]; - 'pri(accounts.transfer.after.confirmation)': [RequestSubmitTransferWithId, SWTransactionResponse]; + 'pri(accounts.bitcoin.dapp.transfer.confirmation)': [RequestSubmitTransferWithId, SWTransactionResponse]; + 'pri(accounts.psbt.transfer.confirmation)': [RequestSubmitSignPsbtTransfer, SWTransactionResponse]; 'pri(accounts.getBitcoinTransactionData)': [RequestSubmitTransfer, BitcoinTransactionData]; 'pri(accounts.checkCrossChainTransfer)': [RequestCheckCrossChainTransfer, ValidateTransactionResponse]; diff --git a/packages/extension-base/src/koni/background/handlers/Extension.ts b/packages/extension-base/src/koni/background/handlers/Extension.ts index 49cd1f61054..7c2471f08cf 100644 --- a/packages/extension-base/src/koni/background/handlers/Extension.ts +++ b/packages/extension-base/src/koni/background/handlers/Extension.ts @@ -7,7 +7,7 @@ import { _AssetRef, _AssetType, _ChainAsset, _ChainInfo, _MultiChainAsset } from import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { createSubscription } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountExternalError, AccountExternalErrorCode, AccountProxiesWithCurrentProxy, AccountsWithCurrentAddress, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BasicTxErrorType, BasicTxWarningCode, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CreateDeriveAccountInfo, CronReloadRequest, CrowdloanJson, CurrentAccountInfo, DeriveAccountInfo, ExternalRequestPromiseStatus, ExtrinsicType, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, OptionInputAddress, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateSuriV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAccountMeta, RequestAccountProxyCreateSuri, RequestAccountProxyEdit, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerAccountProxy, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBatchRestoreV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConnectWalletConnect, RequestCrossChainTransfer, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDeriveCreateMultiple, RequestDeriveCreateV2, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetDeriveAccounts, RequestGetTransaction, RequestJsonRestoreV2, RequestKeyringExportAccountProxyMnemonic, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveRecentAccount, RequestSeedCreateV2, RequestSeedValidateV2, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTransferCheckReferenceCount, RequestTransferCheckSupporting, RequestTransferExistentialDeposit, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateSuriV2, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseAccountMeta, ResponseChangeMasterPassword, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseFindRawMetadata, ResponseGetDeriveAccounts, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponsePrivateKeyValidateV2, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSeedCreateV2, ResponseSeedValidateV2, ResponseSubscribeHistory, ResponseUnlockKeyring, StakingJson, StakingRewardJson, StakingTxErrorType, StakingType, SupportTransferResponse, ThemeNames, TransactionHistoryItem, TransactionResponse, TransferTxErrorType, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountExternalError, AccountExternalErrorCode, AccountProxiesWithCurrentProxy, AccountsWithCurrentAddress, AddressBookInfo, AmountData, AmountDataWithId, AssetSetting, AssetSettingUpdateReq, BasicTxErrorType, BasicTxWarningCode, BondingOptionParams, BrowserConfirmationType, CampaignBanner, CampaignData, CampaignDataType, ChainType, CreateDeriveAccountInfo, CronReloadRequest, CrowdloanJson, CurrentAccountInfo, DeriveAccountInfo, ExternalRequestPromiseStatus, ExtrinsicType, FeeData, KeyringState, MantaPayEnableMessage, MantaPayEnableParams, MantaPayEnableResponse, MantaPaySyncState, NftCollection, NftJson, NftTransactionRequest, NftTransactionResponse, OptionInputAddress, PriceJson, RequestAccountCreateExternalV2, RequestAccountCreateHardwareMultiple, RequestAccountCreateHardwareV2, RequestAccountCreateSuriV2, RequestAccountCreateWithSecretKey, RequestAccountExportPrivateKey, RequestAccountMeta, RequestAccountProxyCreateSuri, RequestAccountProxyEdit, RequestAddInjectedAccounts, RequestApproveConnectWalletSession, RequestApproveWalletConnectNotSupport, RequestAuthorization, RequestAuthorizationBlock, RequestAuthorizationPerAccount, RequestAuthorizationPerAccountProxy, RequestAuthorizationPerSite, RequestAuthorizeApproveV2, RequestBatchRestoreV2, RequestBondingSubmit, RequestCameraSettings, RequestCampaignBannerComplete, RequestChangeEnableChainPatrol, RequestChangeLanguage, RequestChangeMasterPassword, RequestChangeShowBalance, RequestChangeShowZeroBalance, RequestChangeTimeAutoLock, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestConnectWalletConnect, RequestCrossChainTransfer, RequestCrowdloanContributions, RequestDeleteContactAccount, RequestDeriveCreateMultiple, RequestDeriveCreateV2, RequestDeriveCreateV3, RequestDeriveValidateV2, RequestDisconnectWalletConnectSession, RequestEditContactAccount, RequestFindRawMetadata, RequestForgetSite, RequestFreeBalance, RequestGetDeriveAccounts, RequestGetTransaction, RequestJsonRestoreV2, RequestKeyringExportAccountProxyMnemonic, RequestKeyringExportMnemonic, RequestMaxTransferable, RequestMigratePassword, RequestParseEvmContractInput, RequestParseTransactionSubstrate, RequestPassPhishingPage, RequestQrParseRLP, RequestQrSignEvm, RequestQrSignSubstrate, RequestRejectConnectWalletSession, RequestRejectExternalRequest, RequestRejectWalletConnectNotSupport, RequestRemoveInjectedAccounts, RequestResetWallet, RequestResolveExternalRequest, RequestSaveRecentAccount, RequestSeedCreateV2, RequestSeedValidateV2, RequestSettingsType, RequestSigningApprovePasswordV2, RequestStakePoolingBonding, RequestStakePoolingUnbonding, RequestSubscribeHistory, RequestSubstrateNftSubmitTransaction, RequestTransferCheckReferenceCount, RequestTransferCheckSupporting, RequestTransferExistentialDeposit, RequestTuringCancelStakeCompound, RequestTuringStakeCompound, RequestUnbondingSubmit, RequestUnlockKeyring, RequestUnlockType, ResolveAddressToDomainRequest, ResolveDomainRequest, ResponseAccountCreateSuriV2, ResponseAccountCreateWithSecretKey, ResponseAccountExportPrivateKey, ResponseAccountMeta, ResponseChangeMasterPassword, ResponseCheckPublicAndSecretKey, ResponseDeriveValidateV2, ResponseFindRawMetadata, ResponseGetDeriveAccounts, ResponseKeyringExportMnemonic, ResponseMigratePassword, ResponseParseEvmContractInput, ResponseParseTransactionSubstrate, ResponsePrivateKeyValidateV2, ResponseQrParseRLP, ResponseQrSignEvm, ResponseQrSignSubstrate, ResponseRejectExternalRequest, ResponseResetWallet, ResponseResolveExternalRequest, ResponseSeedCreateV2, ResponseSeedValidateV2, ResponseSubscribeHistory, ResponseUnlockKeyring, StakingJson, StakingRewardJson, StakingTxErrorType, StakingType, SupportTransferResponse, ThemeNames, TransactionHistoryItem, TransactionResponse, TransferTxErrorType, ValidateNetworkRequest, ValidateNetworkResponse, ValidatorInfo } from '@subwallet/extension-base/background/KoniTypes'; import { AccountJson, AccountProxy, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountProxy, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeCancel, RequestAuthorizeReject, RequestBatchRestore, RequestCurrentAccountAddress, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApproveSignature, RequestSigningCancel, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseType, SigningRequest, WindowOpenParams } from '@subwallet/extension-base/background/types'; import { TransactionWarning } from '@subwallet/extension-base/background/warnings/TransactionWarning'; import { ALL_ACCOUNT_KEY, ALL_GENESIS_HASH, BTC_DUST_AMOUNT, SUPPORT_KEYPAIR_TYPES, XCM_FEE_RATIO, XCM_MIN_AMOUNT_RATIO } from '@subwallet/extension-base/constants'; @@ -39,7 +39,7 @@ import { WALLET_CONNECT_EIP155_NAMESPACE } from '@subwallet/extension-base/servi 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 { AccountsStore } from '@subwallet/extension-base/stores'; -import { BalanceJson, BitcoinFeeInfo, BitcoinFeeRate, BuyServiceInfo, BuyTokenInfo, DetermineUtxosForSpendArgs, EarningRewardJson, EvmEIP1995FeeOption, EvmFeeInfo, FeeChainType, FeeDetail, FeeInfo, GetFeeFunction, NominationPoolInfo, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, SubstrateFeeInfo, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; +import { BalanceJson, BitcoinFeeDetail, BitcoinFeeInfo, BitcoinFeeRate, BuyServiceInfo, BuyTokenInfo, DetermineUtxosForSpendArgs, EarningRewardJson, EvmEIP1995FeeOption, EvmFeeInfo, FeeChainType, FeeCustom, FeeDetail, FeeInfo, FeeOption, GetFeeFunction, NominationPoolInfo, OptimalYieldPathParams, RequestEarlyValidateYield, RequestGetYieldPoolTargets, RequestStakeCancelWithdrawal, RequestStakeClaimReward, RequestSubmitSignPsbtTransfer, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, RequestUnlockDotCheckCanMint, RequestUnlockDotSubscribeMintedData, RequestYieldLeave, RequestYieldStepSubmit, RequestYieldWithdrawal, ResponseGetYieldPoolTargets, ResponseSubscribeTransfer, ResponseSubscribeTransferConfirmation, SubstrateFeeInfo, ValidateYieldProcessParams, YieldPoolType } from '@subwallet/extension-base/types'; import { combineBitcoinFee, combineEthFee, convertSubjectInfoToAddresses, createTransactionFromRLP, determineUtxosForSpend, determineUtxosForSpendAll, filterUneconomicalUtxos, generateAccountProxyId, getSizeInfo, isAddressValidWithAuthType, isSameAddress, keyringGetAccounts, reformatAddress, signatureToHex, Transaction as QrTransaction, uniqueStringArray } from '@subwallet/extension-base/utils'; import { parseContractInput, parseEvmRlp } from '@subwallet/extension-base/utils/eth/parseTransaction'; import { balanceFormatter, BN_ZERO, formatNumber } from '@subwallet/extension-base/utils/number'; @@ -2038,7 +2038,7 @@ export default class KoniExtension { }); } - private async makeTransferAfterConfirmation (inputData: RequestSubmitTransferWithId): Promise { + private async makeBitcoinDappTransferConfirmation (inputData: RequestSubmitTransferWithId): Promise { const { chain, feeCustom, feeOption, from, id, to, tokenSlug, transferAll, value } = inputData; const [errors, , , tokenInfo] = this.validateTransfer(tokenSlug, from, to, value, transferAll); @@ -2048,7 +2048,7 @@ export default class KoniExtension { const nativeTokenInfo = this.#koniState.getNativeTokenInfo(chain); const nativeTokenSlug: string = nativeTokenInfo.slug; const isTransferNativeToken = nativeTokenSlug === tokenSlug; - let chainType = ChainType.SUBSTRATE; + let chainType = ChainType.BITCOIN; const tokenBaseAmount: AmountData = { value: '0', symbol: tokenInfo.symbol, decimals: tokenInfo.decimals || 0 }; const transferAmount: AmountData = { ...tokenBaseAmount }; @@ -2118,6 +2118,81 @@ export default class KoniExtension { }); } + private async makePsbtTransferAfterConfirmation (inputData_: RequestSubmitSignPsbtTransfer): Promise { + const { chain, from, id, psbt, tokenSlug, txInput, txOutput, value } = inputData_; + let inputAmount = new BigN(0); + + const totalUtxoInput = txInput.reduce((total, { address, amount }) => { + if (!address || !amount) { + return total; + } + + if (isSameAddress(address, from)) { + inputAmount = new BigN(amount); + } + + return total.plus(new BigN(amount || 0)); + }, new BigN(0)); + + const totalUtxoOutput = txOutput.reduce((total, { address, amount }) => { + if (!address || !amount) { + return total; + } + + return total.plus(new BigN(amount)); + }, new BigN(0)); + + const estimateFeeValue = totalUtxoInput.minus(totalUtxoOutput).toString(); + const [errors, , , tokenInfo] = this.validateTransfer(tokenSlug, from, txOutput[0]?.address || '', value, false); + + const warnings: TransactionWarning[] = []; + + const chainInfo = this.#koniState.getChainInfo(chain); + const { decimals, symbol } = _getChainNativeTokenBasicInfo(chainInfo); + const estimateFee: FeeData = { + symbol, + decimals, + value: estimateFeeValue, + tooHigh: false + }; + + const nativeTokenInfo = this.#koniState.getNativeTokenInfo(chain); + const nativeTokenSlug: string = nativeTokenInfo.slug; + const isTransferNativeToken = nativeTokenSlug === tokenSlug; + const chainType = ChainType.BITCOIN; + + const tokenBaseAmount: AmountData = { value: inputData_.value, symbol: tokenInfo.symbol, decimals: tokenInfo.decimals || 0 }; + const transferAmount: AmountData = { ...tokenBaseAmount }; + + // Get native token amount + const freeBalance = await this.getAddressFreeBalance({ address: from, networkKey: chain, token: tokenSlug }); + + console.log(freeBalance, inputAmount.toString(), '123123'); + + if (new BigN(freeBalance.value).lt(inputAmount)) { + throw new Error(t('Insufficient balance')); + } + + const transferNativeAmount = isTransferNativeToken ? transferAmount.value : '0'; + + return this.#koniState.transactionService.handleTransactionAfterConfirmation({ + id, + errors, + warnings, + address: from, + chain: chain, + estimateFee, + chainType, + transferNativeAmount, + transaction: psbt, + data: inputData_, + extrinsicType: isTransferNativeToken ? ExtrinsicType.TRANSFER_BALANCE : ExtrinsicType.TRANSFER_TOKEN, + ignoreWarnings: false, + isTransferAll: false, + edAsWarning: isTransferNativeToken + }); + } + private async getBitcoinTransactionData (inputData: RequestSubmitTransfer): Promise { const { chain, feeCustom, feeOption, from, to, transferAll, value } = inputData; @@ -2805,6 +2880,140 @@ export default class KoniExtension { return convertData(freeBalance, fee); } + private async subscribeTransferableWhenConfirmation ({ address, chain, feeCustom, feeOption: _feeOptions, to, token, value }: RequestSubscribeTransfer, id: string, port: chrome.runtime.Port): Promise { + const cb = createSubscription<'pri(transfer.confirmation.subscribe)'>(id, port); + const freeBalanceSubject = new Subject(); + const feeSubject = new Subject(); + const feeType: FeeChainType = 'bitcoin'; + let error: string | undefined; + + const convertData = async (freeBalance: AmountData, fee: BitcoinFeeInfo, feeOption?: FeeOption, feeCustom?: FeeCustom): Promise => { + let estimatedFee = '0'; + let feeOptions: BitcoinFeeDetail | null = null; + const amount = parseInt(value || '0'); + const neededUtxos = []; + let sum = new BigN(0); + let sizeInfo = null; + + try { + const _fee = fee; + const _feeCustom = feeCustom as BitcoinFeeRate; + const combineFee = combineBitcoinFee(_fee, _feeOptions, _feeCustom); + const bitcoinApi = this.#koniState.chainService.getBitcoinApi(chain); + let utxos = await getTransferableBitcoinUtxos(bitcoinApi, address); + + const recipients = [address, to || address]; + + utxos = utxos.sort((a, b) => b.value - a.value); + const filteredUtxos = filterUneconomicalUtxos({ + utxos, + feeRate: combineFee.feeRate, + recipients, + sender: address + }); + + for (const utxo of filteredUtxos) { + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender: address, + recipients + }); + + const currentValue = new BigN(amount).plus(Math.ceil(sizeInfo.txVBytes * combineFee.feeRate)); + + if (sum.gte(currentValue)) { + break; + } + + sum = sum.plus(utxo.value); + neededUtxos.push(utxo); + } + + // re calculate + sizeInfo = getSizeInfo({ + inputLength: neededUtxos.length, + sender: address, + recipients + }); + + if (!sizeInfo) { + sizeInfo = getSizeInfo({ + inputLength: utxos.length || 1, + sender: address, + recipients + }); + } + + estimatedFee = Math.ceil(sizeInfo.txVBytes * combineFee.feeRate).toString(); + + const amountLeft = sum.minus(amount).minus(new BigN(estimatedFee)); + + if (amountLeft.lte(0)) { + error = 'Insufficient balance'; + } + + feeOptions = { + ...fee, + vSize: sizeInfo.txVBytes, + estimatedFee + }; + } catch (e) { + feeOptions = { + ...fee, + estimatedFee, + vSize: 0 + }; + + error = (e as Error).message || e as string; + console.warn('Unable to estimate fee', e); + } + + return { + feeOptions: feeOptions as FeeDetail, + feeType, + error, + id + }; + }; + + const subscription = combineLatest({ + freeBalance: freeBalanceSubject, + fee: feeSubject + }) + .subscribe({ + next: ({ fee, freeBalance }) => { + convertData(freeBalance, fee, _feeOptions, feeCustom) + .then(cb) + .catch(console.error); + } + }); + + const [unsubBalance, freeBalance] = await this.#koniState.balanceService.subscribeTokenFreeBalance(address, chain, token, (data) => { + freeBalanceSubject.next(data); // Must be called after subscription + }); + + const fee = await this.#koniState.feeService.subscribeChainFee(id, chain, feeType, (data) => { + feeSubject.next(data as BitcoinFeeInfo); // Must be called after subscription + }); + + const unsub = () => { + subscription.unsubscribe(); + unsubBalance(); + this.#koniState.feeService.unsubscribeChainFee(id, chain, feeType); + }; + + this.createUnsubscriptionHandle( + id, + unsub + ); + + port.onDisconnect.addListener((): void => { + this.cancelSubscription(id); + }); + + return convertData(freeBalance, fee as BitcoinFeeInfo, _feeOptions, feeCustom); + } + private async subscribeAddressFreeBalance ({ address, networkKey, token }: RequestFreeBalance, id: string, port: chrome.runtime.Port): Promise { const cb = createSubscription<'pri(freeBalance.subscribe)'>(id, port); @@ -5488,6 +5697,8 @@ export default class KoniExtension { return this.transferGetMaxTransferable(request as RequestMaxTransferable); case 'pri(transfer.subscribe)': return this.subscribeMaxTransferable(request as RequestSubscribeTransfer, id, port); + case 'pri(transfer.confirmation.subscribe)': + return this.subscribeTransferableWhenConfirmation(request as RequestSubscribeTransfer, id, port); case 'pri(freeBalance.get)': return this.getAddressFreeBalance(request as RequestFreeBalance); case 'pri(freeBalance.subscribe)': @@ -5513,8 +5724,10 @@ export default class KoniExtension { /// Transfer case 'pri(accounts.transfer)': return await this.makeTransfer(request as RequestSubmitTransfer); - case 'pri(accounts.transfer.after.confirmation)': - return await this.makeTransferAfterConfirmation(request as RequestSubmitTransfer); + case 'pri(accounts.bitcoin.dapp.transfer.confirmation)': + return await this.makeBitcoinDappTransferConfirmation(request as RequestSubmitTransfer); + case 'pri(accounts.psbt.transfer.confirmation)': + return await this.makePsbtTransferAfterConfirmation(request as RequestSubmitSignPsbtTransfer); case 'pri(accounts.crossChainTransfer)': return await this.makeCrossChainTransfer(request as RequestCrossChainTransfer); case 'pri(accounts.getBitcoinTransactionData)': diff --git a/packages/extension-base/src/koni/background/handlers/State.ts b/packages/extension-base/src/koni/background/handlers/State.ts index 97d67c5c4bb..f0345cbe00f 100644 --- a/packages/extension-base/src/koni/background/handlers/State.ts +++ b/packages/extension-base/src/koni/background/handlers/State.ts @@ -6,12 +6,11 @@ import { BitcoinProviderError } from '@subwallet/extension-base/background/error import { EvmProviderError } from '@subwallet/extension-base/background/errors/EvmProviderError'; import { withErrorLog } from '@subwallet/extension-base/background/handlers/helpers'; import { isSubscriptionRunning, unsubscribe } from '@subwallet/extension-base/background/handlers/subscriptions'; -import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, BitcoinOutputUtox, BitcoinProviderErrorType, BitcoinSendTransactionParams, BitcoinSendTransactionRequest, BitcoinSignatureRequest, BitcoinSignPsbtPayload, BitcoinSignPsbtRawRequest, BitcoinSignPsbtRequest, BitcoinTransactionConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SignMessageBitcoinResult, SignPsbtBitcoinResult, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; +import { AccountRefMap, AddTokenRequestExternal, AmountData, APIItemState, ApiMap, AuthRequestV2, BasicTxErrorType, BitcoinProviderErrorType, BitcoinSendTransactionParams, BitcoinSendTransactionRequest, BitcoinSignatureRequest, BitcoinSignPsbtPayload, BitcoinSignPsbtRawRequest, BitcoinSignPsbtRequest, BitcoinTransactionConfig, ChainStakingMetadata, ChainType, ConfirmationsQueue, CrowdloanItem, CrowdloanJson, CurrentAccountInfo, CurrentAccountProxyInfo, EvmProviderErrorType, EvmSendTransactionParams, EvmSendTransactionRequest, EvmSignatureRequest, ExternalRequestPromise, ExternalRequestPromiseStatus, ExtrinsicType, MantaAuthorizationContext, MantaPayConfig, MantaPaySyncState, NftCollection, NftItem, NftJson, NominatorMetadata, PsbtTransactionArg, RequestAccountExportPrivateKey, RequestCheckPublicAndSecretKey, RequestConfirmationComplete, RequestConfirmationCompleteBitcoin, RequestCrowdloanContributions, RequestSettingsType, ResponseAccountExportPrivateKey, ResponseCheckPublicAndSecretKey, ServiceInfo, SignMessageBitcoinResult, SignPsbtBitcoinResult, SingleModeJson, StakingItem, StakingJson, StakingRewardItem, StakingRewardJson, StakingType, UiSettings } from '@subwallet/extension-base/background/KoniTypes'; import { AccountJson, RequestAuthorizeTab, RequestRpcSend, RequestRpcSubscribe, RequestRpcUnsubscribe, RequestSign, ResponseRpcListProviders, ResponseSigning } from '@subwallet/extension-base/background/types'; import { ALL_ACCOUNT_KEY, ALL_GENESIS_HASH, MANTA_PAY_BALANCE_INTERVAL } from '@subwallet/extension-base/constants'; import { NftService } from '@subwallet/extension-base/koni/api/nft'; import { BalanceService } from '@subwallet/extension-base/services/balance-service'; -import { getTransferableBitcoinUtxos } from '@subwallet/extension-base/services/balance-service/helpers/balance/bitcoin'; import { ServiceStatus } from '@subwallet/extension-base/services/base/types'; import BuyService from '@subwallet/extension-base/services/buy-service'; import CampaignService from '@subwallet/extension-base/services/campaign-service'; @@ -40,8 +39,8 @@ import { TransactionEventResponse } from '@subwallet/extension-base/services/tra import WalletConnectService from '@subwallet/extension-base/services/wallet-connect-service'; import { SWStorage } from '@subwallet/extension-base/storage'; import AccountRefStore from '@subwallet/extension-base/stores/AccountRef'; -import { BalanceItem, BalanceMap, DetermineUtxosForSpendArgs, EvmFeeInfo, UtxoResponseItem } from '@subwallet/extension-base/types'; -import { determineUtxosForSpend, filterUneconomicalUtxos, getSizeInfo, isAccountAll, keyringGetAccounts, stripUrl, targetIsWeb } from '@subwallet/extension-base/utils'; +import { BalanceItem, BalanceMap, EvmFeeInfo } from '@subwallet/extension-base/types'; +import { isAccountAll, isSameAddress, keyringGetAccounts, stripUrl, targetIsWeb } from '@subwallet/extension-base/utils'; import { isContractAddress, parseContractInput } from '@subwallet/extension-base/utils/eth/parseTransaction'; import { createPromiseHandler } from '@subwallet/extension-base/utils/promise'; import { MetadataDef, ProviderMeta } from '@subwallet/extension-inject/types'; @@ -51,6 +50,7 @@ import { KeypairType } from '@subwallet/keyring/types'; import { keyring } from '@subwallet/ui-keyring'; import BigN from 'bignumber.js'; import * as bitcoin from 'bitcoinjs-lib'; +import { Psbt } from 'bitcoinjs-lib'; import BN from 'bn.js'; import SimpleKeyring from 'eth-simple-keyring'; import { t } from 'i18next'; @@ -1236,8 +1236,8 @@ export default class KoniState { }); } - public async bitcoinSignPspt (id: string, url: string, method: string, params: BitcoinSignPsbtRawRequest, allowedAccounts: string[]): Promise { - const { account: address, allowedSighash, broadcast, network, psbt, signAtIndex } = params; + public async bitcoinSignPspt (id: string, url: string, networkKey: string, method: string, params: BitcoinSignPsbtRawRequest, allowedAccounts: string[]): Promise { + const { account: address, allowedSighash, broadcast, psbt, signAtIndex } = params; if (!psbt || !address) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found payload to sign')); @@ -1247,10 +1247,6 @@ export default class KoniState { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Psbt to be signed must be hex-encoded')); } - if (!(network === 'mainnet' || network === 'testnet')) { - throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Network to try this request is must be mainnet or testnet')); - } - if (!isBitcoinAddress(address)) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Not found address')); } @@ -1266,11 +1262,11 @@ export default class KoniState { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Unable to find account')); } - if (network === 'mainnet') { + if (networkKey === 'bitcoin') { if (!['bitcoin-86', 'bitcoin-84'].includes(pair.type)) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the mainnet network')); } - } else if (network === 'testnet') { + } else if (networkKey === 'bitcoinTestnet') { if (!['bittest-86', 'bittest-84'].includes(pair.type)) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the testnet network')); } @@ -1278,21 +1274,76 @@ export default class KoniState { const account: AccountJson = { address: pair.address, ...pair.meta }; - const psbtGenerate = bitcoin.Psbt.fromHex(psbt, { - network: network === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin + const network_ = networkKey === 'bitcoinTestnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; + + const psbtGenerate = Psbt.fromHex(psbt, { + network: network_ + }); + + const isExistedInput = (inputs: PsbtTransactionArg[], address: string) => inputs.findIndex(({ address: address_ }) => isSameAddress(address, address_ || '')); + + const tokenInfo = this.getNativeTokenInfo(networkKey); + let to = ''; + let value = new BigN(0); + const psbtInputData = psbtGenerate.data.inputs.reduce((inputs, { nonWitnessUtxo, witnessUtxo }, inputIndex) => { + let inputData: PsbtTransactionArg | null = null; + + if (witnessUtxo) { + inputData = { + address: bitcoin.address.fromOutputScript(witnessUtxo?.script, network_), + amount: witnessUtxo.value.toString() + }; + } else if (nonWitnessUtxo) { + const txin = psbtGenerate.txInputs[inputIndex]; + const txout = bitcoin.Transaction.fromBuffer(nonWitnessUtxo).outs[txin.index]; + + inputData = { + address: bitcoin.address.fromOutputScript(txout.script, network_), + amount: txout.value.toString() + }; + } + + inputData && inputs.push(inputData); + + return inputs; + }, [] as PsbtTransactionArg[]); + + const psbtOutputData = psbtGenerate.txOutputs.map((output) => { + let address = ''; + + try { + address = output.address || bitcoin.address.fromOutputScript(output.script, network_); + } catch (e) { + if (output.script.includes(bitcoin.opcodes.OP_RETURN)) { + address = 'OP_RETURN'; + } else { + address = 'Unknown'; + } + } + + if (isExistedInput(psbtInputData, address) === -1) { + to = address; + value = value.plus(new BigN(output.value)); + } + + return { + address, + amount: output.value.toString() + } as PsbtTransactionArg; }); - const psbtTxInputs = psbtGenerate.txInputs; - const psbtTxOutputs = psbtGenerate.txOutputs; const payload: BitcoinSignPsbtPayload = { psbt: psbtGenerate, broadcast: !!broadcast, - network, + value: value.toString(), + to, + network: networkKey, signAtIndex: isArray(signAtIndex) && signAtIndex.length === 0 ? undefined : signAtIndex, account: account.address, allowedSighash, - txInput: psbtTxInputs, - txOutput: psbtTxOutputs + tokenSlug: tokenInfo.slug, + txInput: psbtInputData, + txOutput: psbtOutputData }; const hashPayload = ''; const canSign = !account.isExternal; @@ -1321,10 +1372,7 @@ export default class KoniState { } public async bitcoinSendTransaction (id: string, url: string, networkKey: string, allowedAccounts: string[], transactionParams: BitcoinSendTransactionParams): Promise { - const bitcoinApi = this.getBitcoinApi(networkKey); - const apiStrategy = bitcoinApi.api; - - const autoFormatNumber = (val?: string | number): string | undefined => { + const autoFormatNumber = (val: string | number): string => { if (typeof val === 'string' && val.startsWith('0x')) { return new BigN(val.replace('0x', ''), 16).toString(); } else if (typeof val === 'number') { @@ -1343,14 +1391,24 @@ export default class KoniState { } const tokenInfo = this.getNativeTokenInfo(networkKey); + let totalValue = new BigN('0'); + const to = transactionParams.recipients.map((value) => { + const amount = autoFormatNumber(value.amount); + + totalValue = totalValue.plus(amount); + return { + ...value, + amount + }; + }); const transaction: BitcoinTransactionConfig = { id, from: transactionParams.account, - to: transactionParams.recipients[0].address, - value: autoFormatNumber(transactionParams.recipients[0].amount), + to, + value: totalValue.toString(), tokenSlug: tokenInfo.slug, - networkKey: transactionParams.network === 'testnet' ? 'bitcoinTestnet' : 'bitcoin' + networkKey }; // Address is validated in before step @@ -1366,11 +1424,11 @@ export default class KoniState { throw new EvmProviderError(EvmProviderErrorType.INVALID_PARAMS, t('Unable to find account')); } - if (networkKey === 'mainnet') { + if (networkKey === 'bitcoin') { if (!['bitcoin-86', 'bitcoin-84'].includes(pair.type)) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the mainnet network')); } - } else if (networkKey === 'testnet') { + } else if (networkKey === 'bitcoinTestnet') { if (!['bittest-86', 'bittest-84'].includes(pair.type)) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Your address is not on the testnet network')); } @@ -1378,109 +1436,10 @@ export default class KoniState { const account: AccountJson = { address: pair.address, ...pair.meta }; - // Calculate transaction data - const [feeOptions_, utxos] = await Promise.all([ - apiStrategy.getRecommendedFeeRate(), - getTransferableBitcoinUtxos(bitcoinApi, fromAddress) - ]); - - const optionDefault = feeOptions_.options.default; - let feeOptions = null; - const determineUtxosArgs: DetermineUtxosForSpendArgs = { - amount: parseInt(transaction.value as string || '0'), - feeRate: feeOptions_.options[optionDefault].feeRate, - recipient: transaction.to as string, - sender: account.address, - utxos - }; - - if (!transaction.to) { - throw new Error(); - } - - const fallbackCalculate = (recipients: string[]) => { - const utxos = filterUneconomicalUtxos({ - utxos: determineUtxosArgs.utxos, - feeRate: determineUtxosArgs.feeRate, - recipients, - sender: determineUtxosArgs.sender - }); - - const { txVBytes: vSize } = getSizeInfo({ - inputLength: utxos.length || 1, - sender: fromAddress, - recipients - }); - - return { - vSize, - maxTransferable: utxos.reduce((previous, input) => previous.plus(input.value), new BigN(0)), - estimatedFee: Math.ceil(determineUtxosArgs.feeRate * vSize).toString() - }; - }; - - const getBalance = async (senderAddress: string) => { - const filteredUtxos = await getTransferableBitcoinUtxos(bitcoinApi, senderAddress); - - let balanceValue = new BigN(0); - - filteredUtxos.forEach((utxo) => { - balanceValue = balanceValue.plus(utxo.value); - }); - - return balanceValue; - }; - - let maxTransferable = new BigN('0'); - let estimatedFee = '0'; - let inputs: UtxoResponseItem[] = []; - let outputs: BitcoinOutputUtox[] = []; - - try { - const { fee: _estimatedFee, inputs: inputsRs, outputs: outputRs } = determineUtxosForSpend(determineUtxosArgs); - - const { txVBytes: vSize } = getSizeInfo({ - inputLength: inputs.length, - sender: fromAddress, - recipients: [transaction.to] - }); - - inputs = [...inputsRs]; - outputs = [...outputRs]; - estimatedFee = new BigN(_estimatedFee).toFixed(0); - feeOptions = { - ...feeOptions_, - estimatedFee, - vSize - }; - } catch (_e) { - if (!feeOptions) { - const fb = fallbackCalculate([transaction.to, transaction.to]); - - estimatedFee = fb.estimatedFee; - - feeOptions = { - ...feeOptions_, - estimatedFee, - vSize: fb.vSize - }; - } - } - - maxTransferable = await getBalance(fromAddress); - - // Validate balance - if (maxTransferable.lt(new BigN(estimatedFee).plus(new BigN(autoFormatNumber(transactionParams.recipients[0].amount) || '0')))) { - throw new EvmProviderError(EvmProviderErrorType.INVALID_PARAMS, t('Insufficient balance')); - } - const requestPayload: BitcoinSendTransactionRequest = { ...transaction, hashPayload: JSON.stringify(transaction), - fee: feeOptions, - inputs, canSign: true, - outputs, account: account }; diff --git a/packages/extension-base/src/koni/background/handlers/Tabs.ts b/packages/extension-base/src/koni/background/handlers/Tabs.ts index e6cbdb6c403..a11c3e750ee 100644 --- a/packages/extension-base/src/koni/background/handlers/Tabs.ts +++ b/packages/extension-base/src/koni/background/handlers/Tabs.ts @@ -963,8 +963,8 @@ export default class KoniTabs { }); } - public async canUseAccount (address: string, url: string) { - const allowedAccounts = await this.getEvmCurrentAccount(url); + public async canUseAccount (address: string, url: string, type?: string) { + const allowedAccounts = await (type === 'bitcoin' ? this.getBitcoinCurrentAccount(url) : this.getEvmCurrentAccount(url)); return !!allowedAccounts.find((acc) => (acc.toLowerCase() === address.toLowerCase())); } @@ -982,7 +982,7 @@ export default class KoniTabs { public async evmSendTransaction (id: string, url: string, { params }: RequestArguments) { const transactionParams = (params as EvmSendTransactionParams[])[0]; - const canUseAccount = transactionParams.from && this.canUseAccount(transactionParams.from, url); + const canUseAccount = !!transactionParams.from && await this.canUseAccount(transactionParams.from, url); const evmState = await this.getEvmState(url); const networkKey = evmState.networkKey; @@ -1240,8 +1240,21 @@ export default class KoniTabs { private async bitcoinSignPspt (id: string, url: string, { method, params }: RequestArguments) { const allowedAccounts = (await this.getBitcoinCurrentAccount(url)); + const psbtParams = params as BitcoinSignPsbtRawRequest; + + if (!(psbtParams.network === 'mainnet' || psbtParams.network === 'testnet')) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Network to try this request is must be mainnet or testnet')); + } + + const bitcoinState = await this.getBitcoinState(url, psbtParams.network); + + const networkKey = bitcoinState.networkKey; - const signResult = await this.#koniState.bitcoinSignPspt(id, url, method, params as BitcoinSignPsbtRawRequest, allowedAccounts); + if (!networkKey) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('Network unavailable. Please switch network or manually add network to wallet')); + } + + const signResult = await this.#koniState.bitcoinSignPspt(id, url, networkKey, method, psbtParams, allowedAccounts); if (signResult) { return signResult; @@ -1252,7 +1265,7 @@ export default class KoniTabs { private async bitcoinSendTransfer (id: string, url: string, { params }: RequestArguments) { const transactionParams = params as BitcoinSendTransactionParams; - const canUseAccount = transactionParams.account && this.canUseAccount(transactionParams.account, url); + const canUseAccount = !!transactionParams.account && await this.canUseAccount(transactionParams.account, url, 'bitcoin'); const bitcoinState = await this.getBitcoinState(url, transactionParams.network); const networkKey = bitcoinState.networkKey; @@ -1266,7 +1279,7 @@ export default class KoniTabs { const senderAccountType = getKeypairTypeByAddress(transactionParams.account); - if ((transactionParams.network === 'mainnet' && senderAccountType !== 'bitcoin-84') || (transactionParams.network === 'testnet' && senderAccountType !== 'bittest-84')) { + if ((networkKey === 'bitcoin' && senderAccountType !== 'bitcoin-84') || (networkKey === 'bitcoinTestnet' && senderAccountType !== 'bittest-84')) { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t('The account or the network is incorrect')); } @@ -1282,14 +1295,18 @@ export default class KoniTabs { throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("We don't support multiple recipients yet. Please provide only one for now.")); } + if (transactionParams.recipients.filter(({ address, amount }) => !address || !amount).length > 0) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS); + } + if (transactionParams.account === transactionParams.recipients[0].address) { - throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("The recipient address cannot be the same as the sender's")); + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("The recipient address cannot be the same as the sender's address")); } const recipientAccountType = getKeypairTypeByAddress(transactionParams.recipients[0].address); if (senderAccountType !== recipientAccountType) { - throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("The recipient address type must be the same as the sender's")); + throw new BitcoinProviderError(BitcoinProviderErrorType.INVALID_PARAMS, t("The type of the recipient's address must match the type of the sender's address")); } const allowedAccounts = await this.getBitcoinCurrentAccount(url); diff --git a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts index 7d1ec9eb2e7..8964f698d67 100644 --- a/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts +++ b/packages/extension-base/src/services/request-service/handler/BitcoinRequestHandler.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { BitcoinProviderError } from '@subwallet/extension-base/background/errors/BitcoinProviderError'; -import { BitcoinProviderErrorType, ConfirmationDefinitionsBitcoin, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationTypeBitcoin, ExtrinsicDataTypeMap, RequestConfirmationCompleteBitcoin, SignMessageBitcoinResult, SignPsbtBitcoinResult } from '@subwallet/extension-base/background/KoniTypes'; +import { TransactionError } from '@subwallet/extension-base/background/errors/TransactionError'; +import { BasicTxErrorType, BitcoinProviderErrorType, ConfirmationDefinitionsBitcoin, ConfirmationsQueueBitcoin, ConfirmationsQueueItemOptions, ConfirmationTypeBitcoin, ExtrinsicDataTypeMap, RequestConfirmationCompleteBitcoin, SignMessageBitcoinResult, SignPsbtBitcoinResult } from '@subwallet/extension-base/background/KoniTypes'; import { ConfirmationRequestBase, Resolver } from '@subwallet/extension-base/background/types'; import { getBitcoinTransactionObject } from '@subwallet/extension-base/services/balance-service/helpers'; import { ChainService } from '@subwallet/extension-base/services/chain-service'; @@ -283,7 +284,14 @@ export default class BitcoinRequestHandler { private async signPsbt (request: ConfirmationDefinitionsBitcoin['bitcoinSignPsbtRequest'][0]): Promise { // Extract necessary information from the BitcoinSendTransactionRequest const { account, payload } = request.payload; - const { allowedSighash, broadcast, network, psbt, signAtIndex } = payload; + const { allowedSighash, broadcast, psbt, signAtIndex } = payload; + const transaction = this.#transactionService.getTransaction(request.id); + let eventData: TransactionEventResponse = { + id: request.id, + errors: [], + warnings: [], + extrinsicHash: request.id + }; // todo: validate type of the account @@ -299,10 +307,22 @@ export default class BitcoinRequestHandler { } const signAtIndexGenerate = signAtIndex ? (isArray(signAtIndex) ? signAtIndex : [signAtIndex]) : [...(Array(psbt.inputCount) as number[])].map((_, i) => i); + let psptSignedTransaction: Psbt | null = null; - console.log(signAtIndexGenerate); // Sign the Psbt using the pair's bitcoin object - const psptSignedTransaction = pair.bitcoin.signTransaction(psbt, signAtIndexGenerate, allowedSighash); + try { + psptSignedTransaction = pair.bitcoin.signTransaction(psbt, signAtIndexGenerate, allowedSighash); + } catch (e) { + if (transaction) { + transaction.emitterTransaction?.emit('error', { ...eventData, errors: [new TransactionError(BasicTxErrorType.INVALID_PARAMS, (e as Error).message)], id: transaction.id, extrinsicHash: transaction.id }); + } + + throw new Error((e as Error).message); + } + + if (!psptSignedTransaction) { + throw new Error('Unable to sign'); + } if (!broadcast) { for (const index of signAtIndexGenerate) { @@ -314,18 +334,53 @@ export default class BitcoinRequestHandler { }; } - psptSignedTransaction.finalizeAllInputs(); + if (!transaction) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INTERNAL_ERROR); + } - const chain = network === 'mainnet' ? 'bitcoin' : 'bitcoinTestnet'; + const { chain, emitterTransaction, id } = transaction; - const txid = await this.#chainService.getBitcoinApi(chain).api.simpleSendRawTransaction(psptSignedTransaction.extractTransaction().toHex()); + eventData = { + id, + errors: [], + warnings: [], + extrinsicHash: id + }; - console.log('TXID', txid); + if (!emitterTransaction) { + throw new BitcoinProviderError(BitcoinProviderErrorType.INTERNAL_ERROR); + } - return { - psbt: psptSignedTransaction.toHex(), - txid - }; + const chainInfo = this.#chainService.getChainInfoByKey(chain); + + try { + psptSignedTransaction.finalizeAllInputs(); + } catch (e) { + emitterTransaction.emit('error', { ...eventData, errors: [new TransactionError(BasicTxErrorType.INVALID_PARAMS, (e as Error).message)] }); + throw new Error((e as Error).message); + } + + const hexTransaction = psptSignedTransaction.extractTransaction().toHex(); + + this.#transactionService.emitterEventTransaction(emitterTransaction, eventData, chainInfo.slug, hexTransaction); + const { promise, reject, resolve } = createPromiseHandler(); + + emitterTransaction.on('extrinsicHash', (data) => { + if (!data.extrinsicHash || !psptSignedTransaction) { + reject(BitcoinProviderErrorType.INTERNAL_ERROR); + } else { + resolve({ + psbt: psptSignedTransaction?.toHex(), + txid: data.extrinsicHash + }); + } + }); + + emitterTransaction.on('error', (error) => { + reject(error); + }); + + return promise; } private async decorateResultBitcoin (t: T, request: ConfirmationDefinitionsBitcoin[T][0], result: ConfirmationDefinitionsBitcoin[T][1]) { diff --git a/packages/extension-base/src/services/transaction-service/index.ts b/packages/extension-base/src/services/transaction-service/index.ts index e64a8764f14..55911131b9a 100644 --- a/packages/extension-base/src/services/transaction-service/index.ts +++ b/packages/extension-base/src/services/transaction-service/index.ts @@ -104,6 +104,7 @@ export default class TransactionService { address, chain, edAsWarning, + estimateFee: estimateFee_, extrinsicType, feeCustom, feeOption, @@ -130,7 +131,7 @@ export default class TransactionService { const chainInfo = this.state.chainService.getChainInfoByKey(chain); // Estimate fee - const estimateFee: FeeData = { + const estimateFee: FeeData = estimateFee_ || { symbol: '', decimals: 0, value: '', @@ -147,7 +148,7 @@ export default class TransactionService { const id = getId(); - if (transaction) { + if (transaction && !estimateFee_) { try { if (isSubstrateTransaction(transaction)) { estimateFee.value = (await transaction.paymentInfo(address)).partialFee.toString(); @@ -175,7 +176,7 @@ export default class TransactionService { if (!web3) { validationResponse.errors.push(new TransactionError(BasicTxErrorType.CHAIN_DISCONNECTED, undefined)); } else { - const gasLimit = await web3.api.eth.estimateGas(transaction); + const gasLimit = await web3.api.eth.estimateGas(transaction as TransactionConfig); const feeInfo = await this.state.feeService.subscribeChainFee(id, chain, 'evm') as EvmFeeInfo; const feeCombine = combineEthFee(feeInfo, feeOption, feeCustom as EvmEIP1995FeeOption); @@ -370,6 +371,7 @@ export default class TransactionService { public async handleTransactionAfterConfirmation (transaction: SWTransactionInput): Promise { const validatedTransaction = await this.generalValidate(transaction); + const stopByErrors = validatedTransaction.errors.length > 0; const stopByWarnings = validatedTransaction.warnings.length > 0 && !validatedTransaction.ignoreWarnings; @@ -388,7 +390,7 @@ export default class TransactionService { const emitter = new EventEmitter(); // Fill transaction default info - const transactionUpdated = this.fillTransactionDefaultInfo(transaction); + const transactionUpdated = this.fillTransactionDefaultInfo(validatedTransaction); // Add Transaction transactionsSubject[transactionUpdated.id] = { ...transactionUpdated, emitterTransaction: emitter }; @@ -397,17 +399,34 @@ export default class TransactionService { emitter.on('success', (data: TransactionEventResponse) => { validatedTransaction.id = data.id; validatedTransaction.extrinsicHash = data.extrinsicHash; + this.handlePostProcessing(data.id); + this.onSuccess(data); }); emitter.on('signed', (data: TransactionEventResponse) => { validatedTransaction.id = data.id; validatedTransaction.extrinsicHash = data.extrinsicHash; + this.onSigned(data); }); emitter.on('error', (data: TransactionEventResponse) => { if (data.errors.length > 0) { validatedTransaction.errors.push(...data.errors); } + + this.onFailed({ ...data, errors: [...data.errors, new TransactionError(BasicTxErrorType.INTERNAL_ERROR)] }); + }); + + emitter.on('send', (data: TransactionEventResponse) => { + this.onSend(data); + }); + + emitter.on('extrinsicHash', (data: TransactionEventResponse) => { + this.onHasTransactionHash(data); + }); + + emitter.on('timeout', (data: TransactionEventResponse) => { + this.onTimeOut({ ...data, errors: [...data.errors, new TransactionError(BasicTxErrorType.TIMEOUT)] }); }); // @ts-ignore diff --git a/packages/extension-base/src/types/balance/transfer.ts b/packages/extension-base/src/types/balance/transfer.ts index b3b96fad746..ae38a49811e 100644 --- a/packages/extension-base/src/types/balance/transfer.ts +++ b/packages/extension-base/src/types/balance/transfer.ts @@ -1,7 +1,8 @@ // Copyright 2019-2022 @subwallet/extension-base authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BaseRequestSign } from '@subwallet/extension-base/background/KoniTypes'; +import { BaseRequestSign, PsbtTransactionArg } from '@subwallet/extension-base/background/KoniTypes'; +import { Psbt } from 'bitcoinjs-lib'; import { FeeChainType, FeeDetail, TransactionFee } from '../fee'; @@ -23,6 +24,10 @@ export interface ResponseSubscribeTransfer { feeType: FeeChainType; } +export interface ResponseSubscribeTransferConfirmation extends Omit { + error?: string; +} + export interface RequestSubmitTransfer extends BaseRequestSign, TransactionFee { chain: string; from: string; @@ -35,3 +40,15 @@ export interface RequestSubmitTransfer extends BaseRequestSign, TransactionFee { export interface RequestSubmitTransferWithId extends RequestSubmitTransfer{ id?: string; } + +export interface RequestSubmitSignPsbtTransfer extends BaseRequestSign { + id: string; + chain: string; + from: string; + to: string; + value: string; + txInput: PsbtTransactionArg[]; + txOutput: PsbtTransactionArg[]; + tokenSlug: string; + psbt: Psbt; +} diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/BaseDetailModal.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/BaseDetailModal.tsx index 0f847bdd651..47726c097b1 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/BaseDetailModal.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/BaseDetailModal.tsx @@ -64,6 +64,10 @@ const BaseDetailModal = styled(Component)(({ theme: { token } }: Props) = '.__label': { textTransform: 'capitalize' + }, + + '.ant-web3-block-right-item': { + marginRight: 0 } }; }); diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/Evm/Transaction.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/Evm/Transaction.tsx index f5c12bc6ffd..b261bf305ee 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/Evm/Transaction.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Detail/Evm/Transaction.tsx @@ -111,14 +111,19 @@ const Component: React.FC = (props: Props) => { ) : null } - + + + { (!request.isToContract || amount !== 0) && ( diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx index b5d24e758e5..8c21aa39dd0 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/parts/Sign/Bitcoin.tsx @@ -1,12 +1,12 @@ // Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BitcoinSignatureRequest, ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; +import { BitcoinSignatureRequest, BitcoinSignPsbtRequest, ConfirmationDefinitionsBitcoin, ConfirmationResult, EvmSendTransactionRequest, ExtrinsicType } from '@subwallet/extension-base/background/KoniTypes'; import { RequestSubmitTransferWithId } from '@subwallet/extension-base/types'; import { wait } from '@subwallet/extension-base/utils'; import { CONFIRMATION_QR_MODAL } from '@subwallet/extension-koni-ui/constants'; import { useGetChainInfoByChainId, useLedger, useNotification, useUnlockChecker } from '@subwallet/extension-koni-ui/hooks'; -import { completeConfirmationBitcoin, makeTransferAfterConfirmation } from '@subwallet/extension-koni-ui/messaging'; +import { completeConfirmationBitcoin, makeBitcoinDappTransferConfirmation, makePSBTTransferAfterConfirmation } from '@subwallet/extension-koni-ui/messaging'; import { AccountSignMode, BitcoinSignatureSupportType, PhosphorIcon, SigData, ThemeProps } from '@subwallet/extension-koni-ui/types'; import { getSignMode, isBitcoinMessage, removeTransactionPersist } from '@subwallet/extension-koni-ui/utils'; import { Button, Icon, ModalContext } from '@subwallet/react-ui'; @@ -55,7 +55,7 @@ const handleSignature = async (type: BitcoinSignatureSupportType, id: string, si const Component: React.FC = (props: Props) => { const { canSign, className, editedPayload, extrinsicType, id, payload, type } = props; const { payload: { hashPayload } } = payload; - const account = (payload.payload as BitcoinSignatureRequest).account; + const { account } = (payload.payload as BitcoinSignatureRequest); const chainId = (payload.payload as EvmSendTransactionRequest)?.chainId || 1; const { t } = useTranslation(); @@ -65,7 +65,6 @@ const Component: React.FC = (props: Props) => { const chain = useGetChainInfoByChainId(chainId); const checkUnlock = useUnlockChecker(); - const signMode = useMemo(() => getSignMode(account), [account]); const isLedger = useMemo(() => signMode === AccountSignMode.LEDGER, [signMode]); const isMessage = isBitcoinMessage(payload); @@ -110,15 +109,40 @@ const Component: React.FC = (props: Props) => { const onApprovePassword = useCallback(() => { setLoading(true); - (type === 'bitcoinSendTransactionRequestAfterConfirmation' && editedPayload ? makeTransferAfterConfirmation(editedPayload) : wait(1000)) - .then(() => { - console.log('complete', type, id); - handleConfirm(type, id, '').finally(() => { - setLoading(false); + + const promise = async () => { + if (type === 'bitcoinSendTransactionRequestAfterConfirmation' && editedPayload) { + await makeBitcoinDappTransferConfirmation(editedPayload); + } else if (type === 'bitcoinSignPsbtRequest') { + const { payload: { account, broadcast, network, psbt, to, tokenSlug, txInput, txOutput, value } } = payload.payload as BitcoinSignPsbtRequest; + + if (broadcast) { + await makePSBTTransferAfterConfirmation({ id, chain: network, txOutput, txInput, tokenSlug, psbt, from: account, to, value }); + } else { + await wait(1000); + } + } else { + await wait(1000); + } + }; + + promise().then(() => { + handleConfirm(type, id, '').finally(() => { + setLoading(false); + }); + }) + .catch((error) => { + console.error(error); + notify({ + message: t((error as Error).message), + type: 'error', + duration: 8 }); }) - .catch(console.error); - }, [editedPayload, id, type]); + .finally(() => { + setLoading(false); + }); + }, [editedPayload, id, notify, payload.payload, t, type]); const onApproveSignature = useCallback((signature: SigData) => { setLoading(true); diff --git a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx index f5b6cdac360..6f3674645d3 100644 --- a/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx +++ b/packages/extension-koni-ui/src/Popup/Confirmations/variants/BitcoinSendTransactionRequestConfirmation.tsx @@ -3,12 +3,12 @@ import { _ChainAsset } from '@subwallet/chain-list/types'; import { BitcoinSendTransactionRequest, ConfirmationsQueueItem } from '@subwallet/extension-base/background/KoniTypes'; -import { BitcoinFeeDetail, RequestSubmitTransferWithId, ResponseSubscribeTransfer, TransactionFee } from '@subwallet/extension-base/types'; -import { BN_ZERO, getDomainFromUrl } from '@subwallet/extension-base/utils'; +import { BitcoinFeeDetail, RequestSubmitTransferWithId, ResponseSubscribeTransferConfirmation, TransactionFee } from '@subwallet/extension-base/types'; +import { getDomainFromUrl } from '@subwallet/extension-base/utils'; import { BitcoinFeeSelector, MetaInfo } from '@subwallet/extension-koni-ui/components'; import { RenderFieldNodeParams } from '@subwallet/extension-koni-ui/components/Field/TransactionFee/BitcoinFeeSelector'; -import { useGetAccountByAddress } from '@subwallet/extension-koni-ui/hooks'; -import { cancelSubscription, subscribeMaxTransfer } from '@subwallet/extension-koni-ui/messaging'; +import { useGetAccountByAddress, useNotification } from '@subwallet/extension-koni-ui/hooks'; +import { cancelSubscription, subscribeTransferWhenConfirmation } from '@subwallet/extension-koni-ui/messaging'; import { BitcoinSignArea } from '@subwallet/extension-koni-ui/Popup/Confirmations/parts'; import { RootState } from '@subwallet/extension-koni-ui/stores'; import { BitcoinSignatureSupportType, ThemeProps } from '@subwallet/extension-koni-ui/types'; @@ -35,33 +35,30 @@ const convertToBigN = (num: BitcoinSendTransactionRequest['value']): string | nu }; function Component ({ className, request, type }: Props) { - const { id, payload: { account, fee, networkKey, to, tokenSlug, value } } = request; + const { id, payload: { account, networkKey, to, tokenSlug, value } } = request; const { t } = useTranslation(); + const transferAmountValue = useMemo(() => value?.toString() as string, [value]); + const fromValue = useMemo(() => account.address, [account.address]); + const toValue = useMemo(() => to ? to[0].address : '', [to]); + const chainValue = useMemo(() => networkKey as string, [networkKey]); + const assetValue = useMemo(() => tokenSlug as string, [tokenSlug]); const [transactionInfo, setTransactionInfo] = useState({ id, chain: networkKey as string, from: account.address, - to: to as string, + to: toValue, tokenSlug: tokenSlug as string, transferAll: false, value: value?.toString() || '0' }); const [isFetchingInfo, setIsFetchingInfo] = useState(false); - const [isTransferAll, setIsTransferAll] = useState(false); - const [transferInfo, setTransferInfo] = useState(); - const [transactionFeeInfo, setTransactionFeeInfo] = useState({ - feeOption: fee?.options.default - }); - + const [transferInfo, setTransferInfo] = useState(); + const [transactionFeeInfo, setTransactionFeeInfo] = useState(undefined); + const [isErrorTransaction, setIsErrorTransaction] = useState(false); + const notify = useNotification(); const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); - const transferAmountValue = value?.toString() as string; - const fromValue = account.address; - const toValue = to as string; - const chainValue = networkKey as string; - const assetValue = tokenSlug as string; - const assetInfo: _ChainAsset | undefined = useMemo(() => { return assetRegistry[assetValue]; }, [assetRegistry, assetValue]); @@ -116,15 +113,6 @@ function Component ({ className, request, type }: Props) { setTransactionInfo((prevState) => ({ ...prevState, ...transactionFeeInfo })); }, [transactionFeeInfo]); - useEffect(() => { - const bnTransferAmount = new BigN(transferAmountValue || '0'); - const bnMaxTransfer = new BigN(transferInfo?.maxTransferable || '0'); - - if (bnTransferAmount.gt(BN_ZERO) && bnTransferAmount.eq(bnMaxTransfer)) { - setIsTransferAll(true); - } - }, [transferInfo, transferAmountValue]); - useEffect(() => { let cancel = false; let id = ''; @@ -132,8 +120,15 @@ function Component ({ className, request, type }: Props) { setIsFetchingInfo(true); - const callback = (transferInfo: ResponseSubscribeTransfer) => { - if (!cancel) { + const callback = (transferInfo: ResponseSubscribeTransferConfirmation) => { + if (transferInfo.error) { + notify({ + message: t(transferInfo.error), + type: 'error', + duration: 8 + }); + setIsErrorTransaction(true); + } else if (!cancel) { setTransferInfo(transferInfo); id = transferInfo.id; } else { @@ -143,22 +138,26 @@ function Component ({ className, request, type }: Props) { if (fromValue && assetValue) { timeout = setTimeout(() => { - subscribeMaxTransfer({ + subscribeTransferWhenConfirmation({ address: fromValue, chain: chainValue, token: assetValue, - isXcmTransfer: false, destChain: chainValue, feeOption: transactionFeeInfo?.feeOption, feeCustom: transactionFeeInfo?.feeCustom, value: transferAmountValue || '0', - transferAll: isTransferAll, + transferAll: false, to: toValue }, callback) .then(callback) .catch((e) => { console.error(e); - + notify({ + message: t(e), + type: 'error', + duration: 8 + }); + setIsErrorTransaction(true); setTransferInfo(undefined); }) .finally(() => { @@ -172,7 +171,7 @@ function Component ({ className, request, type }: Props) { clearTimeout(timeout); id && cancelSubscription(id).catch(console.error); }; - }, [assetRegistry, assetValue, chainValue, fromValue, toValue, transactionFeeInfo, transferAmountValue, isTransferAll]); + }, [assetRegistry, assetValue, chainValue, fromValue, toValue, transactionFeeInfo, transferAmountValue, notify, t]); return ( <> @@ -206,14 +205,14 @@ function Component ({ className, request, type }: Props) { value={amount} /> - + />} {/* {!!transaction.estimateFee?.tooHigh && ( */} @@ -226,7 +225,7 @@ function Component ({ className, request, type }: Props) { {/* )} */} state.accountState.accounts); + const assetRegistry = useSelector((root: RootState) => root.assetRegistry.assetRegistry); const onClickDetail = useOpenDetailModal(); + const assetInfo: _ChainAsset | undefined = useMemo(() => { + return assetRegistry[tokenSlug]; + }, [assetRegistry, tokenSlug]); + const renderAccount = useCallback((accountsPsbt: PsbtTransactionArg[]) => { + return ( +
+ { + accountsPsbt.map(({ address, amount }) => { + const account = findAccountByAddress(accounts, address); + + return ( + : <>} + />); + } + ) + } + +
+ ); + }, [accounts, assetInfo.decimals, assetInfo.symbol]); return ( <> @@ -66,11 +101,12 @@ function Component ({ className, request, type }: Props) { > - {JSON.stringify(request.payload.payload.txInput)} + {renderAccount(txInput)} - {JSON.stringify(request.payload.payload.txOutput)} + {renderAccount(txOutput)} + diff --git a/packages/extension-koni-ui/src/hooks/account/useGetDefaultAccountProxyName.ts b/packages/extension-koni-ui/src/hooks/account/useGetDefaultAccountProxyName.ts index 28f5f73bfeb..9bf112c4769 100644 --- a/packages/extension-koni-ui/src/hooks/account/useGetDefaultAccountProxyName.ts +++ b/packages/extension-koni-ui/src/hooks/account/useGetDefaultAccountProxyName.ts @@ -10,9 +10,15 @@ const useGetDefaultAccountProxyName = () => { const accountProxies = useSelector((state: RootState) => state.accountState.accountProxies); return useMemo(() => { - const filtered = accountProxies.filter((ap) => !isAccountAll(ap.proxyId)); + let accountIndex = 0; + const filtered = accountProxies + .filter((ap) => { + accountIndex = Math.max(Number.parseInt(ap.name?.split(' ')[1] || '0'), accountIndex); - return `Account ${filtered.length + 1}`; + return !isAccountAll(ap.proxyId); + }); + + return `Account ${Math.max(filtered.length, accountIndex) + 1}`; }, [accountProxies]); }; diff --git a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts index d261551d101..f326f4c42f3 100644 --- a/packages/extension-koni-ui/src/messaging/transaction/transfer.ts +++ b/packages/extension-koni-ui/src/messaging/transaction/transfer.ts @@ -3,7 +3,7 @@ import { AmountData, RequestCrossChainTransfer, RequestMaxTransferable, RequestTransferCheckReferenceCount, RequestTransferCheckSupporting, RequestTransferExistentialDeposit, SupportTransferResponse } from '@subwallet/extension-base/background/KoniTypes'; import { BitcoinTransactionData, SWTransactionResponse } from '@subwallet/extension-base/services/transaction-service/types'; -import { RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, ResponseSubscribeTransfer } from '@subwallet/extension-base/types'; +import { RequestSubmitSignPsbtTransfer, RequestSubmitTransfer, RequestSubmitTransferWithId, RequestSubscribeTransfer, ResponseSubscribeTransfer, ResponseSubscribeTransferConfirmation } from '@subwallet/extension-base/types'; import { sendMessage } from '../base'; @@ -11,8 +11,12 @@ export async function makeTransfer (request: RequestSubmitTransfer): Promise { - return sendMessage('pri(accounts.transfer.after.confirmation)', request); +export async function makeBitcoinDappTransferConfirmation (request: RequestSubmitTransferWithId): Promise { + return sendMessage('pri(accounts.bitcoin.dapp.transfer.confirmation)', request); +} + +export async function makePSBTTransferAfterConfirmation (request: RequestSubmitSignPsbtTransfer): Promise { + return sendMessage('pri(accounts.psbt.transfer.confirmation)', request); } export async function makeCrossChainTransfer (request: RequestCrossChainTransfer): Promise { @@ -42,3 +46,7 @@ export async function getMaxTransfer (request: RequestMaxTransferable): Promise< export async function subscribeMaxTransfer (request: RequestSubscribeTransfer, callback: (data: ResponseSubscribeTransfer) => void): Promise { return sendMessage('pri(transfer.subscribe)', request, callback); } + +export async function subscribeTransferWhenConfirmation (request: RequestSubscribeTransfer, callback: (data: ResponseSubscribeTransferConfirmation) => void): Promise { + return sendMessage('pri(transfer.confirmation.subscribe)', request, callback); +} diff --git a/packages/extension-koni/public/main.css b/packages/extension-koni/public/main.css index 6e85e1467c7..640d1fedc0e 100644 --- a/packages/extension-koni/public/main.css +++ b/packages/extension-koni/public/main.css @@ -34,6 +34,13 @@ body { position: relative; } +@media (max-height: 600px) { + html { + align-items: flex-start; + overflow: hidden; + } +} + [tabindex='-1']:focus { outline: none; } diff --git a/packages/extension-koni/public/notification.html b/packages/extension-koni/public/notification.html index b4c52e458ed..f445017a51a 100644 --- a/packages/extension-koni/public/notification.html +++ b/packages/extension-koni/public/notification.html @@ -10,8 +10,6 @@ margin: 0 auto; height: 600px; width: 390px; - display: flex; - align-items: center; min-height: 600px; }