diff --git a/packages/extension/.env.example b/packages/extension/.env.example new file mode 100644 index 000000000..aa9ba3fa6 --- /dev/null +++ b/packages/extension/.env.example @@ -0,0 +1,2 @@ +# VITE_DEBUG_LOG="swap:*" +VITE_DEBUG_LOG= diff --git a/packages/extension/src/libs/activity-state/index.ts b/packages/extension/src/libs/activity-state/index.ts index ff9c339c8..735c91e29 100644 --- a/packages/extension/src/libs/activity-state/index.ts +++ b/packages/extension/src/libs/activity-state/index.ts @@ -61,6 +61,7 @@ class ActivityState { this.getActivityId(options), ); } + async updateActivity( activity: Activity, options: ActivityOptions, @@ -75,11 +76,13 @@ class ActivityState { }); await this.setActivitiesById(clone, this.getActivityId(options)); } + async setCacheTime(options: ActivityOptions): Promise { await this.#storage.set(this.getActivityCacheId(options), { [STORAGE_KEY]: new Date().getTime(), }); } + async getCacheTime(options: ActivityOptions): Promise { const cacheTime: Record = await this.#storage.get( this.getActivityCacheId(options), @@ -87,12 +90,15 @@ class ActivityState { if (!cacheTime || !cacheTime[STORAGE_KEY]) return 0; return cacheTime[STORAGE_KEY]; } + async getAllActivities(options: ActivityOptions): Promise { return this.getActivitiesById(this.getActivityId(options)); } + async deleteAllActivities(options: ActivityOptions): Promise { this.setActivitiesById([], this.getActivityId(options)); } + private async setActivitiesById( activities: Activity[], id: string, @@ -101,6 +107,7 @@ class ActivityState { [STORAGE_KEY]: activities, }); } + private async getActivitiesById(id: string): Promise { const allStates: Record = await this.#storage.get(id); if (!allStates || !allStates[STORAGE_KEY]) return []; diff --git a/packages/extension/src/libs/utils/unicode-detection.ts b/packages/extension/src/libs/utils/unicode-detection.ts new file mode 100644 index 000000000..58146051c --- /dev/null +++ b/packages/extension/src/libs/utils/unicode-detection.ts @@ -0,0 +1,22 @@ +const fixedHex = (number: number, length: number) => { + let str = number.toString(16).toUpperCase(); + while (str.length < length) str = '0' + str; + return str; +}; + +/* Creates a unicode literal based on the string */ +const unicodeLiteral = (str: string) => { + let i; + let result = ''; + for (i = 0; i < str.length; ++i) { + if (str.charCodeAt(i) > 126 || str.charCodeAt(i) < 32) + result += '\\u' + fixedHex(str.charCodeAt(i), 4); + else result += str[i]; + } + return result; +}; +export const getRTLOLTLOSafeString = (str: string): string => { + const dangerous = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u061C]/.test(str); + if (dangerous) return unicodeLiteral(str); + return str; +}; diff --git a/packages/extension/src/providers/bitcoin/ui/btc-sign-message.vue b/packages/extension/src/providers/bitcoin/ui/btc-sign-message.vue index 15b9d69a0..988d2ce00 100644 --- a/packages/extension/src/providers/bitcoin/ui/btc-sign-message.vue +++ b/packages/extension/src/providers/bitcoin/ui/btc-sign-message.vue @@ -65,6 +65,7 @@ import { ProviderRequestOptions } from '@/types/provider'; import { BitcoinNetwork } from '../types/bitcoin-network'; import { EnkryptAccount } from '@enkryptcom/types'; import { MessageSigner } from './libs/signer'; +import { getRTLOLTLOSafeString } from '@/libs/utils/unicode-detection'; const windowPromise = WindowPromiseHandler(4); const network = ref(DEFAULT_BTC_NETWORK); @@ -92,7 +93,7 @@ onBeforeMount(async () => { account.value = Request.value.params![2] as EnkryptAccount; identicon.value = network.value.identicon(account.value.address); Options.value = options; - message.value = Request.value.params![0]; + message.value = getRTLOLTLOSafeString(Request.value.params![0]); type.value = Request.value.params![1]; }); diff --git a/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue b/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue index 410ee13fe..66138e361 100644 --- a/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue +++ b/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue @@ -312,7 +312,10 @@ const isInputsValid = computed(() => { isSendToken.value ) return false; - if (new BigNumber(sendAmount.value).gt(assetMaxValue.value)) return false; + + const sendAmountBigNumber = new BigNumber(sendAmount.value) + if (sendAmountBigNumber.isNaN()) return false + if (sendAmountBigNumber.gt(assetMaxValue.value)) return false; return true; }); diff --git a/packages/extension/src/providers/common/libs/new-features.ts b/packages/extension/src/providers/common/libs/new-features.ts index 2f0faadd1..189011b41 100644 --- a/packages/extension/src/providers/common/libs/new-features.ts +++ b/packages/extension/src/providers/common/libs/new-features.ts @@ -1,6 +1,6 @@ import { NetworkNames } from '@enkryptcom/types'; -const newNetworks = [NetworkNames.Solana]; +const newNetworks = [NetworkNames.Bitrock, NetworkNames.Fraxtal]; const newSwaps: NetworkNames[] = []; export { newNetworks, newSwaps }; diff --git a/packages/extension/src/providers/common/ui/send-transaction/send-input-amount.vue b/packages/extension/src/providers/common/ui/send-transaction/send-input-amount.vue index 206a5fafd..d444b943a 100644 --- a/packages/extension/src/providers/common/ui/send-transaction/send-input-amount.vue +++ b/packages/extension/src/providers/common/ui/send-transaction/send-input-amount.vue @@ -73,9 +73,16 @@ const amount = computed({ const onlyNumber = ($event: KeyboardEvent) => { const keyCode = $event.keyCode ? $event.keyCode : $event.which; - if ((keyCode < 48 || keyCode > 57) && keyCode !== 46) { - $event.preventDefault(); + // Numeric + if (keyCode >= /* 0 */ 48 && keyCode <= /* 9 */ 57) { + return; } + // Only allow a single period + if (keyCode === /* '.' */ 46 && amount.value.indexOf('.') === -1) { + return; + } + // Alphabetical (/non-numeric) or mulitple periods. Don't propagate change + $event.preventDefault(); }; const changeFocus = () => { isFocus.value = !isFocus.value; diff --git a/packages/extension/src/providers/ethereum/libs/transaction/gas-utils.ts b/packages/extension/src/providers/ethereum/libs/transaction/gas-utils.ts index c2246bf1a..8abbb3228 100644 --- a/packages/extension/src/providers/ethereum/libs/transaction/gas-utils.ts +++ b/packages/extension/src/providers/ethereum/libs/transaction/gas-utils.ts @@ -62,7 +62,7 @@ const getGasBasedOnType = ( } }; const getMinPriorityFee = (): BNType => { - return toBN(toWei('0.1', 'gwei')); + return toBN(toWei('1', 'gwei')); }; const getPriorityFeeAvg = (arr: BNType[]): BNType => { const sum = arr.reduce((a, v) => a.add(v)); diff --git a/packages/extension/src/providers/ethereum/libs/transaction/index.ts b/packages/extension/src/providers/ethereum/libs/transaction/index.ts index a0e740936..8936a04d0 100644 --- a/packages/extension/src/providers/ethereum/libs/transaction/index.ts +++ b/packages/extension/src/providers/ethereum/libs/transaction/index.ts @@ -38,14 +38,13 @@ class Transaction { value: this.tx.value || '0x0', }); } - async getOPfees(): Promise { + async getOPfees( + fTx: LegacyTransaction | FeeMarketEIP1559Transaction, + ): Promise { const OPContract = new this.web3.Contract( OPTIMISM_PRICE_ORACLE_ABI as any, OPTIMISM_PRICE_ORACLE, ); - const fTx = await this.getFinalizedTransaction({ - gasPriceType: GasPriceTypes.ECONOMY, - }); const serializedTx = fTx.serialize(); return OPContract.methods .getL1Fee(bufferToHex(serializedTx)) @@ -87,6 +86,7 @@ class Transaction { maxFeePerGas?: string; gasLimit: string; formattedFeeHistory?: FormattedFeeHistory; + finalizedTransaction: LegacyTransaction | FeeMarketEIP1559Transaction; }> { const latestBlock = await this.web3.getBlock('latest', false); const { isFeeMarketNetwork, feeHistory } = await this.web3 @@ -123,10 +123,20 @@ class Transaction { nonce: this.tx.nonce || (numberToHex(nonce) as `0x${string}`), value: this.tx.value || '0x0', }; + const common = Common.custom({ + chainId: BigInt(this.tx.chainId), + }); + const finalizedTransaction = LegacyTransaction.fromTxData( + legacyTx as FinalizedLegacyEthereumTransaction, + { + common, + }, + ); return { transaction: legacyTx, gasPrice: gasPrice, gasLimit: legacyTx.gasLimit, + finalizedTransaction, }; } else { // Fee market transaction (post EIP1559) @@ -141,7 +151,7 @@ class Transaction { const gasLimit = this.tx.gasLimit || (numberToHex(await this.estimateGas()) as `0x${string}`); - const maxFeePerGas = !options.totalGasPrice + let maxFeePerGas = !options.totalGasPrice ? feeMarket.maxFeePerGas : options.totalGasPrice.div(toBN(gasLimit)); const maxPriorityFeePerGas = feeMarket.maxPriorityFeePerGas; @@ -162,6 +172,35 @@ class Transaction { type: '0x02', accessList: this.tx.accessList || [], }; + const common = Common.custom({ + chainId: BigInt(this.tx.chainId), + defaultHardfork: Hardfork.London, + }); + let finalizedTransaction = FeeMarketEIP1559Transaction.fromTxData( + feeMarketTx as FinalizedFeeMarketEthereumTransaction, + { + common, + }, + ); + if (options.totalGasPrice) { + const opFee = await this.getOPfees(finalizedTransaction); + if (opFee.gtn(0)) { + const gasFeeWithoutOPFee = options.totalGasPrice.sub(opFee); + maxFeePerGas = gasFeeWithoutOPFee.div(toBN(gasLimit)); + feeMarketTx.maxFeePerGas = numberToHex(maxFeePerGas) as `0x${string}`; + feeMarketTx.maxPriorityFeePerGas = numberToHex( + maxPriorityFeePerGas.gt(maxFeePerGas) + ? maxFeePerGas + : maxPriorityFeePerGas, + ) as `0x${string}`; + finalizedTransaction = FeeMarketEIP1559Transaction.fromTxData( + feeMarketTx as FinalizedFeeMarketEthereumTransaction, + { + common, + }, + ); + } + } return { transaction: feeMarketTx, gasLimit: feeMarketTx.gasLimit, @@ -169,6 +208,7 @@ class Transaction { maxFeePerGas: numberToHex(feeMarket.maxFeePerGas), maxPriorityFeePerGas: numberToHex(feeMarket.maxPriorityFeePerGas), formattedFeeHistory, + finalizedTransaction, }; } } @@ -182,30 +222,8 @@ class Transaction { async getFinalizedTransaction( options: TransactionOptions, ): Promise { - const { transaction } = await this.finalizeTransaction(options); - - if (!transaction.maxFeePerGas) { - const common = Common.custom({ - chainId: BigInt(transaction.chainId), - }); - return LegacyTransaction.fromTxData( - transaction as FinalizedLegacyEthereumTransaction, - { - common, - }, - ); - } else { - const common = Common.custom({ - chainId: BigInt(transaction.chainId), - defaultHardfork: Hardfork.London, - }); - return FeeMarketEIP1559Transaction.fromTxData( - transaction as FinalizedFeeMarketEthereumTransaction, - { - common, - }, - ); - } + const { finalizedTransaction } = await this.finalizeTransaction(options); + return finalizedTransaction; } async getMessageToSign(options: TransactionOptions): Promise { @@ -214,35 +232,38 @@ class Transaction { } async getGasCosts(): Promise { - const { gasLimit, gasPrice, baseFeePerGas, formattedFeeHistory } = - await this.finalizeTransaction({ - gasPriceType: GasPriceTypes.ECONOMY, - }); - const opFee = await this.getOPfees(); + const { + gasLimit, + gasPrice, + baseFeePerGas, + formattedFeeHistory, + finalizedTransaction, + } = await this.finalizeTransaction({ + gasPriceType: GasPriceTypes.ECONOMY, + }); if (gasPrice) { return { [GasPriceTypes.ECONOMY]: numberToHex( - getGasBasedOnType(gasPrice, GasPriceTypes.ECONOMY) - .mul(toBN(gasLimit)) - .add(opFee), + getGasBasedOnType(gasPrice, GasPriceTypes.ECONOMY).mul( + toBN(gasLimit), + ), ), [GasPriceTypes.REGULAR]: numberToHex( - getGasBasedOnType(gasPrice, GasPriceTypes.REGULAR) - .mul(toBN(gasLimit)) - .add(opFee), + getGasBasedOnType(gasPrice, GasPriceTypes.REGULAR).mul( + toBN(gasLimit), + ), ), [GasPriceTypes.FAST]: numberToHex( - getGasBasedOnType(gasPrice, GasPriceTypes.FAST) - .mul(toBN(gasLimit)) - .add(opFee), + getGasBasedOnType(gasPrice, GasPriceTypes.FAST).mul(toBN(gasLimit)), ), [GasPriceTypes.FASTEST]: numberToHex( - getGasBasedOnType(gasPrice, GasPriceTypes.FASTEST) - .mul(toBN(gasLimit)) - .add(opFee), + getGasBasedOnType(gasPrice, GasPriceTypes.FASTEST).mul( + toBN(gasLimit), + ), ), }; } else { + const opFee = await this.getOPfees(finalizedTransaction); return { [GasPriceTypes.ECONOMY]: numberToHex( this.getFeeMarketGasInfo( diff --git a/packages/extension/src/providers/ethereum/networks/maticzk.ts b/packages/extension/src/providers/ethereum/networks/maticzk.ts index 16d5437ad..c546e667e 100644 --- a/packages/extension/src/providers/ethereum/networks/maticzk.ts +++ b/packages/extension/src/providers/ethereum/networks/maticzk.ts @@ -14,11 +14,11 @@ const maticOptions: EvmNetworkOptions = { blockExplorerAddr: 'https://zkevm.polygonscan.com/address/[[address]]', chainID: '0x44d', isTestNetwork: false, - currencyName: 'POL', - currencyNameLong: 'Polygon POL', + currencyName: 'ETH', + currencyNameLong: 'Ethereum', node: 'wss://nodes.mewapi.io/ws/maticzk', icon, - coingeckoID: 'polygon-ecosystem-token', + coingeckoID: 'ethereum', coingeckoPlatform: CoingeckoPlatform.MaticZK, NFTHandler: shNFTHandler, assetsInfoHandler, diff --git a/packages/extension/src/providers/ethereum/ui/eth-sign-message.vue b/packages/extension/src/providers/ethereum/ui/eth-sign-message.vue index d2d716d6d..5d26a9ae9 100644 --- a/packages/extension/src/providers/ethereum/ui/eth-sign-message.vue +++ b/packages/extension/src/providers/ethereum/ui/eth-sign-message.vue @@ -59,6 +59,7 @@ import HardwareWalletMsg from '@/providers/common/ui/verify-transaction/hardware import { EvmNetwork } from '../types/evm-network'; import { EnkryptAccount } from '@enkryptcom/types'; import { MessageSigner } from './libs/signer'; +import { getRTLOLTLOSafeString } from '@/libs/utils/unicode-detection'; const windowPromise = WindowPromiseHandler(3); const network = ref(DEFAULT_EVM_NETWORK); @@ -85,7 +86,7 @@ onBeforeMount(async () => { identicon.value = network.value.identicon(account.value.address); Options.value = options; message.value = isUtf8(Request.value.params![0]) - ? hexToUtf8(Request.value.params![0]) + ? getRTLOLTLOSafeString(hexToUtf8(Request.value.params![0])) : Request.value.params![0]; }); diff --git a/packages/extension/src/providers/ethereum/ui/send-transaction/components/send-alert.vue b/packages/extension/src/providers/ethereum/ui/send-transaction/components/send-alert.vue new file mode 100644 index 000000000..046f53424 --- /dev/null +++ b/packages/extension/src/providers/ethereum/ui/send-transaction/components/send-alert.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/extension/src/providers/ethereum/ui/send-transaction/index.vue b/packages/extension/src/providers/ethereum/ui/send-transaction/index.vue index 5d638b745..4fd40a1f8 100644 --- a/packages/extension/src/providers/ethereum/ui/send-transaction/index.vue +++ b/packages/extension/src/providers/ethereum/ui/send-transaction/index.vue @@ -103,18 +103,7 @@ @gas-type-changed="selectFee" /> - +
@@ -143,7 +132,7 @@ import SendContactsList from '@/providers/common/ui/send-transaction/send-contac import AssetsSelectList from '@action/views/assets-select-list/index.vue'; import NftSelectList from '@/providers/common/ui/send-transaction/nft-select-list/index.vue'; import SendTokenSelect from './components/send-token-select.vue'; -import SendAlert from '@/providers/common/ui/send-transaction/send-alert.vue'; +import SendAlert from './components/send-alert.vue'; import SendNftSelect from '@/providers/common/ui/send-transaction/send-nft-select.vue'; import SendInputAmount from '@/providers/common/ui/send-transaction/send-input-amount.vue'; import SendFeeSelect from '@/providers/common/ui/send-transaction/send-fee-select.vue'; @@ -169,6 +158,7 @@ import erc721 from '../../libs/abi/erc721'; import erc1155 from '../../libs/abi/erc1155'; import { SendTransactionDataType, VerifyTransactionParams } from '../types'; import { + formatFiatValue, formatFloatingPointValue, isNumericPositive, } from '@/libs/utils/number-formatter'; @@ -217,11 +207,19 @@ const accountAssets = ref([]); const selectedAsset = ref>(loadingAsset); const amount = ref(''); const isEstimateValid = ref(true); +const hasValidDecimals = computed(() => { + return isValidDecimals(sendAmount.value, selectedAsset.value.decimals!); +}); +const hasPositiveSendAmount = computed(() => { + return isNumericPositive(sendAmount.value); +}); const hasEnoughBalance = computed(() => { - if (!isValidDecimals(sendAmount.value, selectedAsset.value.decimals!)) { + if (!hasValidDecimals.value) { + return false; + } + if (!hasPositiveSendAmount.value) { return false; } - // check if valid sendAmount.value if (!isNumericPositive(sendAmount.value)) { return false; @@ -341,7 +339,11 @@ const Tx = computed(() => { return tx; }); -const nativeBalanceAfterTransaction = computed(() => { +/** + * Native balance after the transaction in the base unit of the + * native currency (eg in WETH, Lamports, Satoshis, ...) + */ +const nativeBalanceAfterTransactionInBaseUnits = computed(() => { if ( isSendToken.value && nativeBalance.value && @@ -390,6 +392,71 @@ const nativeBalanceAfterTransaction = computed(() => { return toBN(0); }); +/** + * Native balance after the transaction in the human unit of the + * native currency (eg in ETH, SOL, BTC, ...) + */ +const nativeBalanceAfterTransactionInHumanUnits = computed(() => { + return fromBase( + nativeBalanceAfterTransactionInBaseUnits.value.abs().toString(), + props.network.decimals, + ); +}); + +const nativeCurrencyUsdPrice = computed(() => { + return accountAssets.value[0]?.price || '0'; +}); + +const balanceAfterInUsd = computed(() => { + return new BigNumber( + nativeBalanceAfterTransactionInHumanUnits.value.toString(), + ) + .times(nativeCurrencyUsdPrice.value ?? '0') + .toFixed(); +}); + +const errorMsg = computed(() => { + if (!hasValidDecimals.value) { + return `Too many decimals.`; + } + + if (!hasPositiveSendAmount.value) { + return `Invalid amount.`; + } + + if ( + !hasEnoughBalance.value && + nativeBalanceAfterTransactionInBaseUnits.value.isNeg() + ) { + return `Not enough funds. You are + ~${formatFloatingPointValue(nativeBalanceAfterTransactionInHumanUnits.value).value} + ${props.network.currencyName} ($ ${ + formatFiatValue(balanceAfterInUsd.value).value + }) short.`; + } + + if (!props.network.isAddress(addressTo.value) && addressTo.value !== '') { + return `Invalid to address.`; + } + + if ( + isSendToken.value && + !isValidDecimals(sendAmount.value, selectedAsset.value.decimals!) + ) { + return `Invalid decimals for ${selectedAsset.value.symbol}.`; + } + + if (!isSendToken.value && !selectedNft.value.id) { + return `Invalid NFT selected.`; + } + + if (new BigNumber(sendAmount.value).gt(assetMaxValue.value)) { + return `Amount exceeds maximum value.`; + } + + return ''; +}); + const setTransactionFees = (tx: Transaction) => { return tx .getGasCosts() @@ -479,7 +546,7 @@ const sendButtonTitle = computed(() => { const isValidSend = computed(() => { if (!isInputsValid.value) return false; - if (nativeBalanceAfterTransaction.value.isNeg()) return false; + if (nativeBalanceAfterTransactionInBaseUnits.value.isNeg()) return false; if (!isEstimateValid.value) return false; return true; }); @@ -495,7 +562,9 @@ const isInputsValid = computed(() => { if (!isSendToken.value && !selectedNft.value.id) { return false; } - if (new BigNumber(sendAmount.value).gt(assetMaxValue.value)) return false; + const sendAmountBigNumber = new BigNumber(sendAmount.value) + if (sendAmountBigNumber.isNaN()) return false + if (sendAmountBigNumber.gt(assetMaxValue.value)) return false; if (gasCostValues.value.REGULAR.nativeValue === '0') return false; if (!isNumericPositive(sendAmount.value)) return false; return true; diff --git a/packages/extension/src/providers/kadena/ui/kda-sign-message.vue b/packages/extension/src/providers/kadena/ui/kda-sign-message.vue index e5142f655..462e10489 100644 --- a/packages/extension/src/providers/kadena/ui/kda-sign-message.vue +++ b/packages/extension/src/providers/kadena/ui/kda-sign-message.vue @@ -64,6 +64,7 @@ import { DEFAULT_KADENA_NETWORK, getNetworkByName, } from '@/libs/utils/networks'; +import { getRTLOLTLOSafeString } from '@/libs/utils/unicode-detection'; const windowPromise = WindowPromiseHandler(0); const network = ref(DEFAULT_KADENA_NETWORK); @@ -81,7 +82,7 @@ const account = ref({ address: '' } as EnkryptAccount); onBeforeMount(async () => { const { Request, options } = await windowPromise; Options.value = options; - message.value = Request.value.params![0].data; + message.value = getRTLOLTLOSafeString(Request.value.params![0].data); account.value = Request.value.params![1] as EnkryptAccount; network.value = (await getNetworkByName( Request.value.params![0].network, diff --git a/packages/extension/src/providers/polkadot/ui/dot-sign-message.vue b/packages/extension/src/providers/polkadot/ui/dot-sign-message.vue index 6751f4b19..3708a8384 100644 --- a/packages/extension/src/providers/polkadot/ui/dot-sign-message.vue +++ b/packages/extension/src/providers/polkadot/ui/dot-sign-message.vue @@ -56,6 +56,7 @@ import { ProviderRequestOptions } from '@/types/provider'; import { EnkryptAccount } from '@enkryptcom/types'; import { MessageSigner } from './libs/signer'; import { hexToBuffer } from '@enkryptcom/utils'; +import { getRTLOLTLOSafeString } from '@/libs/utils/unicode-detection'; const windowPromise = WindowPromiseHandler(0); @@ -74,7 +75,9 @@ onBeforeMount(async () => { Options.value = options; message.value = isUtf8(Request.value.params![0]) - ? u8aToString(u8aUnwrapBytes(Request.value.params![0])) + ? getRTLOLTLOSafeString( + u8aToString(u8aUnwrapBytes(Request.value.params![0])), + ) : Request.value.params![0]; account.value = Request.value.params![1] as EnkryptAccount; diff --git a/packages/extension/src/providers/solana/libs/api.ts b/packages/extension/src/providers/solana/libs/api.ts index 9931e7526..2893cbdae 100644 --- a/packages/extension/src/providers/solana/libs/api.ts +++ b/packages/extension/src/providers/solana/libs/api.ts @@ -23,36 +23,48 @@ class API implements ProviderAPIInterface { return getSolAddress(pubkey); } - async init(): Promise {} + async init(): Promise { } + + /** + * Returns null if the transaction hasn't been received by the node + * or has been dropped + */ async getTransactionStatus(hash: string): Promise { - return this.web3 - .getTransaction(hash, { - maxSupportedTransactionVersion: 0, - commitment: 'confirmed', - }) - .then(tx => { - if (!tx) return null; - const retVal: SOLRawInfo = { - blockNumber: tx.slot, - timestamp: tx.blockTime, - transactionHash: hash, - status: tx.meta?.err ? false : true, - }; - return retVal; - }); + const tx = await this.web3.getTransaction(hash, { + maxSupportedTransactionVersion: 0, + commitment: 'confirmed', + }) + + if (!tx) { + // Transaction hasn't been picked up by the node + // (maybe it's too soon, or maybe node is behind, or maybe it's been dropped) + return null; + } + + const retVal: SOLRawInfo = { + blockNumber: tx.slot, + timestamp: tx.blockTime, + transactionHash: hash, + status: tx.meta?.err ? false : true, + }; + + return retVal; } + async getBalance(pubkey: string): Promise { const balance = await this.web3.getBalance( new PublicKey(this.getAddress(pubkey)), ); return numberToHex(balance); } + async broadcastTx(rawtx: string): Promise { return this.web3 .sendRawTransaction(hexToBuffer(rawtx)) .then(() => true) .catch(() => false); } + getTokenInfo = async (contractAddress: string): Promise => { interface TokenDetails { address: string; diff --git a/packages/extension/src/providers/solana/ui/send-transaction/index.vue b/packages/extension/src/providers/solana/ui/send-transaction/index.vue index 1f59aeda9..0ecf92072 100644 --- a/packages/extension/src/providers/solana/ui/send-transaction/index.vue +++ b/packages/extension/src/providers/solana/ui/send-transaction/index.vue @@ -140,6 +140,7 @@ import { VerifyTransactionParams, SendTransactionDataType } from '../types'; import { formatFloatingPointValue, formatFiatValue, + isNumericPositive, } from '@/libs/utils/number-formatter'; import { routes as RouterNames } from '@/ui/action/router'; import getUiPath from '@/libs/utils/get-ui-path'; @@ -172,6 +173,7 @@ import { import getPriorityFees from '../libs/get-priority-fees'; import bs58 from 'bs58'; import SolanaAPI from '@/providers/solana/libs/api'; +import { ComputedRefSymbol } from '@vue/reactivity'; const props = defineProps({ network: { @@ -211,8 +213,17 @@ const amount = ref(''); const isEstimateValid = ref(true); const storageFee = ref(0); const SolTx = ref(); -const hasEnoughBalance = computed(() => { - if (!isValidDecimals(sendAmount.value, selectedAsset.value.decimals!)) { +const hasValidDecimals = computed((): boolean => { + return isValidDecimals(sendAmount.value, selectedAsset.value.decimals!); +}); +const hasPositiveSendAmount = computed(() => { + return isNumericPositive(sendAmount.value); +}); +const hasEnoughBalance = computed((): boolean => { + if (!hasValidDecimals.value) { + return false; + } + if (!hasPositiveSendAmount.value) { return false; } return toBN(selectedAsset.value.balance ?? '0').gte( @@ -276,7 +287,11 @@ const TxInfo = computed(() => { }; }); -const nativeBalanceAfterTransaction = computed(() => { +/** + * Native balance after the transaction in the base unit of the + * native currency (eg in WETH, Lamports, Satoshis, ...) + */ +const nativeBalanceAfterTransactionInBaseUnits = computed(() => { if ( isSendToken.value && nativeBalance.value && @@ -320,47 +335,71 @@ const nativeBalanceAfterTransaction = computed(() => { return toBN(0); }); -const nativeValue = computed(() => { +/** + * Native balance after the transaction in the human unit of the + * native currency (eg in ETH, SOL, BTC, ...) + */ +const nativeBalanceAfterTransactionInHumanUnits = computed(() => { return fromBase( - nativeBalanceAfterTransaction.value.toString(), + nativeBalanceAfterTransactionInBaseUnits.value.abs().toString(), props.network.decimals, ); }); -const nativePrice = computed(() => { +const nativeCurrencyUsdPrice = computed(() => { return accountAssets.value[0]?.price || '0'; }); -const priceDifference = computed(() => { - return new BigNumber(nativeValue.value) - .times(nativePrice.value ?? '0') +const balanceAfterInUsd = computed(() => { + return new BigNumber( + nativeBalanceAfterTransactionInHumanUnits.value.toString(), + ) + .times(nativeCurrencyUsdPrice.value ?? '0') .toFixed(); }); const errorMsg = computed(() => { - if (hasEnoughBalance.value && nativeBalanceAfterTransaction.value.isNeg()) { + if (!hasValidDecimals.value) { + return `Too many decimals.`; + } + + if (!hasPositiveSendAmount.value) { + return `Invalid amount.`; + } + + if ( + !hasEnoughBalance.value && + nativeBalanceAfterTransactionInBaseUnits.value.isNeg() + ) { return `Not enough funds. You are - ~${formatFloatingPointValue(nativeValue.value).value} + ~${formatFloatingPointValue(nativeBalanceAfterTransactionInHumanUnits.value).value} ${props.network.currencyName} ($ ${ - formatFiatValue(priceDifference.value).value + formatFiatValue(balanceAfterInUsd.value).value }) short.`; } + if ( !props.network.isAddress(getAddress(addressTo.value)) && addressTo.value !== '' - ) + ) { return `Invalid to address.`; + } + if ( isSendToken.value && !isValidDecimals(sendAmount.value, selectedAsset.value.decimals!) ) { return `Invalid decimals for ${selectedAsset.value.symbol}.`; } + if (!isSendToken.value && !selectedNft.value.id) { return `Invalid NFT selected.`; } - if (new BigNumber(sendAmount.value).gt(assetMaxValue.value)) + + if (new BigNumber(sendAmount.value).gt(assetMaxValue.value)) { return `Amount exceeds maximum value.`; + } + return ''; }); @@ -418,7 +457,7 @@ const sendButtonTitle = computed(() => { const isValidSend = computed(() => { if (!isInputsValid.value) return false; - if (nativeBalanceAfterTransaction.value.isNeg()) return false; + if (nativeBalanceAfterTransactionInBaseUnits.value.isNeg()) return false; if (!isEstimateValid.value) return false; if (gasCostValues.value.ECONOMY.nativeValue === '0') return false; return true; @@ -435,7 +474,9 @@ const isInputsValid = computed(() => { if (!isSendToken.value && !selectedNft.value.id) { return false; } - if (new BigNumber(sendAmount.value).gt(assetMaxValue.value)) return false; + const sendAmountBigNumber = new BigNumber(sendAmount.value); + if (sendAmountBigNumber.isNaN()) return false; + if (sendAmountBigNumber.gt(assetMaxValue.value)) return false; return true; }); diff --git a/packages/extension/src/providers/solana/ui/sol-sign-message.vue b/packages/extension/src/providers/solana/ui/sol-sign-message.vue index 210b33b36..7859c18e8 100644 --- a/packages/extension/src/providers/solana/ui/sol-sign-message.vue +++ b/packages/extension/src/providers/solana/ui/sol-sign-message.vue @@ -76,6 +76,7 @@ import { SolSignInResponse } from './types'; import { isUtf8 } from '@polkadot/util'; import { hexToUtf8 } from 'web3-utils'; import { MessageSigner } from './libs/signer'; +import { getRTLOLTLOSafeString } from '@/libs/utils/unicode-detection'; const windowPromise = WindowPromiseHandler(3); const keyring = new PublicKeyRing(); @@ -144,7 +145,7 @@ onBeforeMount(async () => { } else if (reqMethod.value === 'sol_signMessage') { signMessage.value = JSON.parse(Request.value.params![1]); message.value = isUtf8(signMessage.value!.message) - ? hexToUtf8(signMessage.value!.message) + ? getRTLOLTLOSafeString(hexToUtf8(signMessage.value!.message)) : signMessage.value!.message; keyring .getAccount(bufferToHex(bs58.decode(signMessage.value!.address))) diff --git a/packages/extension/src/types/activity.ts b/packages/extension/src/types/activity.ts index 89db8af7d..1c7511af8 100644 --- a/packages/extension/src/types/activity.ts +++ b/packages/extension/src/types/activity.ts @@ -95,6 +95,7 @@ enum ActivityStatus { pending = 'pending', success = 'success', failed = 'failed', + dropped = 'dropped', } enum ActivityType { @@ -121,13 +122,13 @@ interface Activity { status: ActivityStatus; type: ActivityType; rawInfo?: - | EthereumRawInfo - | SubstrateRawInfo - | SubscanExtrinsicInfo - | BTCRawInfo - | SwapRawInfo - | KadenaRawInfo - | SOLRawInfo; + | EthereumRawInfo + | SubstrateRawInfo + | SubscanExtrinsicInfo + | BTCRawInfo + | SwapRawInfo + | KadenaRawInfo + | SOLRawInfo; } export { diff --git a/packages/extension/src/types/env.d.ts b/packages/extension/src/types/env.d.ts new file mode 100644 index 000000000..4e906114c --- /dev/null +++ b/packages/extension/src/types/env.d.ts @@ -0,0 +1,4 @@ +interface ImportMetaEnv { + VITE_DEBUG_LOG?: string +} + diff --git a/packages/extension/src/types/window.d.ts b/packages/extension/src/types/window.d.ts index 77fed48f1..22508aa6d 100644 --- a/packages/extension/src/types/window.d.ts +++ b/packages/extension/src/types/window.d.ts @@ -1,3 +1,12 @@ interface Window { [key: string]: any; + __ENKRYPT_DEBUG_LOG_CONF__: undefined | string } + +declare global { + var __ENKRYPT_DEBUG_LOG_CONF__: undefined | string +} + +// Required to make the `delare global` exports work for some reason +export { } + diff --git a/packages/extension/src/ui/action/main.ts b/packages/extension/src/ui/action/main.ts index 04cfdd9ec..7ec2bcbf2 100644 --- a/packages/extension/src/ui/action/main.ts +++ b/packages/extension/src/ui/action/main.ts @@ -6,6 +6,11 @@ import Vue3Lottie from 'vue3-lottie'; global.WeakMap = WeakMap; +if (import.meta.env.DEV) { + globalThis.__ENKRYPT_DEBUG_LOG_CONF__ = import.meta.env.VITE_DEBUG_LOG +} + + const app = createApp(App); app.use(router).use(Vue3Lottie, { name: 'vue3lottie' }); diff --git a/packages/extension/src/ui/action/views/network-activity/components/network-activity-transaction.vue b/packages/extension/src/ui/action/views/network-activity/components/network-activity-transaction.vue index 1f4795de8..1f81a6f8a 100644 --- a/packages/extension/src/ui/action/views/network-activity/components/network-activity-transaction.vue +++ b/packages/extension/src/ui/action/views/network-activity/components/network-activity-transaction.vue @@ -37,7 +37,10 @@

{{ status }}

- Swap from - {{ (activity.rawInfo as SwapRawInfo).fromToken.symbol }} to - {{ (activity.rawInfo as SwapRawInfo).toToken.symbol }} + {{ swapMessage }}

{{ status }} diff --git a/packages/extension/src/ui/action/views/swap/components/swap-error/index.vue b/packages/extension/src/ui/action/views/swap/components/swap-error/index.vue index 5890dc1d7..3d48193ec 100644 --- a/packages/extension/src/ui/action/views/swap/components/swap-error/index.vue +++ b/packages/extension/src/ui/action/views/swap/components/swap-error/index.vue @@ -130,7 +130,7 @@ const supportedNets = getSupportedNetworks() &__container { width: 100%; - height: 600px; + height: 100%; left: 0px; top: 0px; position: fixed; diff --git a/packages/extension/src/ui/action/views/swap/components/swap-loading/index.vue b/packages/extension/src/ui/action/views/swap/components/swap-loading/index.vue index 3310af9ee..68a0a5968 100644 --- a/packages/extension/src/ui/action/views/swap/components/swap-loading/index.vue +++ b/packages/extension/src/ui/action/views/swap/components/swap-loading/index.vue @@ -84,7 +84,7 @@ withDefaults(defineProps(), { &__container { width: 100%; - height: 600px; + height: 100%; left: 0px; top: 0px; position: fixed; diff --git a/packages/extension/src/ui/action/views/swap/index.vue b/packages/extension/src/ui/action/views/swap/index.vue index a3caa71c0..ea0b71530 100644 --- a/packages/extension/src/ui/action/views/swap/index.vue +++ b/packages/extension/src/ui/action/views/swap/index.vue @@ -840,7 +840,7 @@ const sendAction = async () => { const tradeStatusOptions = trades.map(t => t!.getStatusObject({ - transactionHashes: [], + transactions: [], }), ); diff --git a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts index d5cef90db..192357bb7 100644 --- a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts +++ b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts @@ -75,7 +75,7 @@ const getBaseActivity = (options: ExecuteSwapOptions): Activity => { */ export const executeSwap = async ( options: ExecuteSwapOptions, -): Promise => { +): Promise<{ hash: string, sentAt: number }[]> => { const activityState = new ActivityState(); const api = await options.network.api(); if (options.networkType === NetworkType.Bitcoin) { @@ -116,7 +116,7 @@ export const executeSwap = async ( network: options.network.name, }); }); - return [signedTx.getId() as `0x${string}`]; + return [{ hash: signedTx.getId() as `0x${string}`, sentAt: Date.now() }]; } else if (options.networkType === NetworkType.Substrate) { const substrateTx = await getSubstrateNativeTransation( options.network as SubstrateNetwork, @@ -173,12 +173,12 @@ export const executeSwap = async ( { address: txActivity.from, network: options.network.name }, ); }); - return [hash]; + return [{ hash, sentAt: Date.now(), }]; } else if (options.networkType === NetworkType.Solana) { // Execute the swap on Solana const conn = (api as SolanaAPI).api.web3; - const solTxHashes: string[] = []; + const solTxs: { hash: string, sentAt: number, }[] = []; /** Enkrypt representation of the swap transactions */ const enkSolTxs = options.swap.transactions as EnkryptSolanaTransaction[]; @@ -200,7 +200,7 @@ export const executeSwap = async ( const hasThirdPartySignatures = // Serialized versioned transaction was already signed legacyTx.signatures.length > 1 || - // Need to apply third aprty signatures to the transaction + // Need to apply third party signatures to the transaction thirdPartySignatures.length > 0; if (hasThirdPartySignatures) { @@ -219,7 +219,7 @@ export const executeSwap = async ( // Might need to update the block hash console.warn( `Failed to get fee for legacy transaction while checking` + - ` whether to update block hash: ${String(err)}`, + ` whether to update block hash: ${String(err)}`, ); shouldUpdateBlockHash = true; } @@ -228,7 +228,7 @@ export const executeSwap = async ( if (shouldUpdateBlockHash) { console.warn( `Unsigned legacy transaction might have an` + - ` out-of-date block hash, trying to update it...`, + ` out-of-date block hash, trying to update it...`, ); const backoff = [0, 500, 1_000, 2_000]; let backoffi = 0; @@ -238,7 +238,7 @@ export const executeSwap = async ( // Just continue and hope for the best with old block hash... console.warn( `Failed to get latest blockhash after ${backoffi} attempts,` + - ` continuing with old block hash for legacy transaction...`, + ` continuing with old block hash for legacy transaction...`, ); break update_block_hash; } @@ -246,7 +246,7 @@ export const executeSwap = async ( if (backoffMs > 0) { console.warn( `Waiting ${backoffMs}ms before retrying latest block` + - ` hash for legacy transaction...`, + ` hash for legacy transaction...`, ); await new Promise(res => setTimeout(res, backoffMs)); } @@ -257,7 +257,7 @@ export const executeSwap = async ( } catch (err) { console.warn( `Failed to get latest blockhash on attempt` + - ` ${backoffi + 1}: ${String(err)}`, + ` ${backoffi + 1}: ${String(err)}`, ); } backoffi++; @@ -330,7 +330,7 @@ export const executeSwap = async ( // Might need to update the block hash console.warn( `Failed to get fee for versioned transaction while checking` + - ` whether to update block hash: ${String(err)}`, + ` whether to update block hash: ${String(err)}`, ); shouldUpdateBlockHash = true; } @@ -339,7 +339,7 @@ export const executeSwap = async ( if (shouldUpdateBlockHash) { console.warn( `Unsigned versioned transaction might have an` + - ` out-of-date block hash, trying to update it...`, + ` out-of-date block hash, trying to update it...`, ); const backoff = [0, 500, 1_000, 2_000]; let backoffi = 0; @@ -349,7 +349,7 @@ export const executeSwap = async ( // Just continue and hope for the best with old block hash... console.warn( `Failed to get latest blockhash after ${backoffi} attempts,` + - ` continuing with old block hash for versioned transaction...`, + ` continuing with old block hash for versioned transaction...`, ); break update_block_hash; } @@ -357,7 +357,7 @@ export const executeSwap = async ( if (backoffMs > 0) { console.warn( `Waiting ${backoffMs}ms before retrying latest block` + - ` hash for versioned transaction...`, + ` hash for versioned transaction...`, ); await new Promise(res => setTimeout(res, backoffMs)); } @@ -368,7 +368,7 @@ export const executeSwap = async ( } catch (err) { console.warn( `Failed to get latest blockhash on attempt` + - ` ${backoffi + 1}: ${String(err)}`, + ` ${backoffi + 1}: ${String(err)}`, ); } backoffi++; @@ -434,8 +434,8 @@ export const executeSwap = async ( ); throw new Error( 'Failed to send Solana swap transaction: blockhash not found.' + - ' Too much time may have passed between the creation and sending' + - ' of the transaction', + ' Too much time may have passed between the creation and sending' + + ' of the transaction', ); } @@ -454,7 +454,7 @@ export const executeSwap = async ( } else { console.error( `Failed to send Solana swap transaction,` + - ` unhandled error ${(err as Error).name}`, + ` unhandled error ${(err as Error).name}`, ); } // Solana transactions can have big errors @@ -475,12 +475,12 @@ export const executeSwap = async ( network: options.network.name, }); - solTxHashes.push(txHash); + solTxs.push({ hash: txHash, sentAt: Date.now(), }); } // Finished executing the swap on Solana - return solTxHashes; + return solTxs; } else if (options.networkType === NetworkType.EVM) { const web3 = (api as EvmAPI).web3; const nonce = await web3.getTransactionCount(options.from.address); @@ -504,7 +504,7 @@ export const executeSwap = async ( ); const txs = await Promise.all(txsPromises); /** Hashes of transactions successfully sent & mined, in order of execution */ - const txPromises: `0x${string}`[] = []; + const txPromises: { hash: `0x${string}`, sentAt: number, }[] = []; for (const txInfo of txs) { // Submit each transaction, in-order one-by-one @@ -566,7 +566,7 @@ export const executeSwap = async ( ); }), ); - txPromises.push(hash); + txPromises.push({ hash, sentAt: Date.now(), }); } return txPromises; } else { diff --git a/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-warning.vue b/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-warning.vue index 2f1597081..890f48586 100644 --- a/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-warning.vue +++ b/packages/extension/src/ui/action/views/swap/views/swap-best-offer/components/swap-best-offer-block/components/best-offer-warning.vue @@ -15,6 +15,8 @@