Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add approval popup #2529

Merged
merged 11 commits into from
May 23, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script lang="ts">
import { Table } from '@bloomwalletio/ui'
import { localize } from '@core/i18n'
import { IParsedTokenApproval } from '@core/layer-2'
import { EvmNetworkId, ExplorerEndpoint, getExplorerUrl } from '@core/network'
import { getTokenFromSelectedAccountTokens } from '@core/token/stores'
import { truncateString } from '@core/utils'
import { TransactionAssetSection } from '@ui'
import { EvmTransactionAlert } from '.'
import { IToken } from '@core/token'
import { openUrlInBrowser } from '@core/app/utils'

export let parsedTokenApproval: IParsedTokenApproval
export let networkId: EvmNetworkId

const { tokenId, spender, rawAmount } = parsedTokenApproval
const tokenTransfer = {
token: getTokenFromSelectedAccountTokens(tokenId, networkId) as IToken,
rawAmount: rawAmount,
}

function onExplorerClick(address: string): void {
const url = getExplorerUrl(networkId, ExplorerEndpoint.Address, address)
openUrlInBrowser(url)
}
</script>

<EvmTransactionAlert
variant="info"
message={localize('popups.tokenApproval.hint', {
address: truncateString(spender, 6, 6),
assetName: tokenTransfer.token?.metadata?.name ?? truncateString(tokenId, 6, 6),
})}
{networkId}
contractAddress={tokenId}
>
{#if tokenTransfer.token}
<TransactionAssetSection {tokenTransfer} />
{:else}
<Table
collapsible
collapsibleTitle={localize('general.details')}
items={[
{
key: localize('general.tokenId'),
value: truncateString(tokenId, 16, 16),
onClick: () => onExplorerClick(tokenId),
},
{ key: localize('general.spender'), value: spender, onClick: () => onExplorerClick(spender) },
{ key: localize('general.amount'), value: String(rawAmount) },
]}
/>
{/if}
</EvmTransactionAlert>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import { openUrlInBrowser } from '@core/app/utils'
import { EvmNetworkId, ExplorerEndpoint, getExplorerUrl } from '@core/network'
import { Alert, Link, Text } from '@bloomwalletio/ui'
import { localize } from '@core/i18n'

export let variant: string
export let message: string
export let networkId: EvmNetworkId
export let contractAddress: string

function onExplorerClick(address: string): void {
const url = getExplorerUrl(networkId, ExplorerEndpoint.Address, address)
openUrlInBrowser(url)
}
</script>

<Alert {variant}>
<Text slot="text">
{message}
<Link
on:click={() => onExplorerClick(contractAddress)}
text={localize('popups.smartContractCall.viewSmartContract')}
/>
</Text>
<slot slot="body" />
</Alert>
2 changes: 2 additions & 0 deletions packages/desktop/components/evm-transactions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as EvmTokenApprovalAlert } from './EvmTokenApprovalAlert.svelte'
export { default as EvmTransactionAlert } from './EvmTransactionAlert.svelte'
1 change: 1 addition & 0 deletions packages/desktop/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './drawers'
export * from './evm-transactions'
export * from './filter'
export * from './menus'
export * from './modals'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { DappVerification, RpcMethod } from '@auxiliary/wallet-connect/enums'
import { IConnectedDapp } from '@auxiliary/wallet-connect/interface'
import { CallbackParameters } from '@auxiliary/wallet-connect/types'
import { Alert, Link, Table, Text } from '@bloomwalletio/ui'
import { Table } from '@bloomwalletio/ui'
import { IAccountState } from '@core/account'
import { getSelectedAccount, selectedAccount } from '@core/account/stores'
import { openUrlInBrowser } from '@core/app'
Expand All @@ -11,29 +11,28 @@
import {
GasSpeed,
IGasPricesBySpeed,
IParsedInput,
getHexEncodedTransaction,
parseSmartContractDataFromTransactionData,
} from '@core/layer-2'
import { EvmTransactionData } from '@core/layer-2/types'
import { LedgerAppName } from '@core/ledger'
import { ExplorerEndpoint, IEvmNetwork, getExplorerUrl } from '@core/network'
import { Nft } from '@core/nfts'
import { getNftByIdForAccount } from '@core/nfts/stores'
import { checkActiveProfileAuth } from '@core/profile/actions'
import { getActiveProfileId } from '@core/profile/stores'
import { BASE_TOKEN_ID } from '@core/token/constants'
import { getTokenFromSelectedAccountTokens } from '@core/token/stores'
import { Converter, MILLISECONDS_PER_SECOND, truncateString } from '@core/utils'
import { TokenTransferData } from '@core/wallet'
import { sendAndPersistTransactionFromEvm, signEvmTransaction } from '@core/wallet/actions'
import { PopupId, closePopup, modifyPopupState, openPopup } from '@desktop/auxiliary/popup'
import { LegacyTransaction } from '@ethereumjs/tx'
import { DappInfo, TransactionAssetSection } from '@ui'
import { EvmTransactionAlert, EvmTokenApprovalAlert } from '@components'
import { EvmTransactionDetails } from '@views/dashboard/send-flow/views/components'
import { onDestroy, onMount } from 'svelte'
import PopupTemplate from '../PopupTemplate.svelte'
import { ParsedSmartContractType } from '@core/layer-2/enums/parsed-smart-contract-type.enum'
import { ParsedSmartContractData } from '@core/layer-2/types/parsed-smart-contract-data.type'

export let preparedTransaction: EvmTransactionData
export let evmNetwork: IEvmNetwork
Expand All @@ -42,21 +41,10 @@
export let method: RpcMethod.EthSendTransaction | RpcMethod.EthSignTransaction | RpcMethod.EthSendRawTransaction
export let callback: (params: CallbackParameters) => void

$: localeKey =
method === RpcMethod.EthSignTransaction
? 'signTransaction'
: isSmartContractCall
? 'smartContractCall'
: 'sendTransaction'

let nft: Nft | undefined
let tokenTransfer: TokenTransferData | undefined
let baseCoinTransfer: TokenTransferData | undefined
let isSmartContractCall = false
let methodName: string | undefined = undefined
let inputs: Record<string, unknown> | undefined = undefined
let busy = false

let parsedData: ParsedSmartContractData | undefined

let selectedGasSpeed = GasSpeed.Required
let gasPrices: IGasPricesBySpeed = {
[GasSpeed.Required]: Converter.bigIntLikeToBigInt(preparedTransaction?.gasPrice as number),
Expand All @@ -69,68 +57,53 @@
}
$: preparedTransaction.gasPrice = Converter.bigIntToHex(gasPrices?.[selectedGasSpeed] ?? gasPrices.required)

setTransactionInformation()
function setTransactionInformation(): void {
setParsedContractData()
function setParsedContractData(): void {
if (!preparedTransaction.data) {
baseCoinTransfer = {
token: getTokenFromSelectedAccountTokens(BASE_TOKEN_ID, evmNetwork.id),
rawAmount: Converter.bigIntLikeToBigInt(preparedTransaction.value),
} as TokenTransferData
return
}

const parsedData = parseSmartContractDataFromTransactionData(
parsedData = parseSmartContractDataFromTransactionData(
{
to: preparedTransaction.to?.toString(),
data: String(preparedTransaction.data),
value: preparedTransaction.value,
},
evmNetwork
)
}

methodName = parsedData?.parsedMethod?.name
inputs = parsedData?.parsedMethod?.inputs.reduce((acc, input: IParsedInput) => {
acc[input.name] = input.value
return acc
}, {})

switch (parsedData?.type) {
case ParsedSmartContractType.CoinTransfer: {
const transfer = {
token: getTokenFromSelectedAccountTokens(BASE_TOKEN_ID, evmNetwork.id),
rawAmount: parsedData.rawAmount,
} as TokenTransferData
if (!transfer.token) {
return
}
$: localeKey = getLocalKey(parsedData?.type)
function getLocalKey(type?: ParsedSmartContractType): string {
if (method === RpcMethod.EthSignTransaction) {
return 'signTransaction'
}
switch (type) {
case ParsedSmartContractType.CoinTransfer:
case ParsedSmartContractType.TokenTransfer:
case ParsedSmartContractType.NftTransfer:
return 'sendTransaction'
case ParsedSmartContractType.TokenApproval:
return 'tokenApproval'
case ParsedSmartContractType.SmartContract:
default:
return 'smartContractCall'
}
}

baseCoinTransfer = transfer
break
}
case ParsedSmartContractType.TokenTransfer: {
const transfer = {
token: getTokenFromSelectedAccountTokens(parsedData.tokenId, evmNetwork.id),
rawAmount: parsedData.rawAmount,
} as TokenTransferData
if (!transfer.token) {
return
}
$: title = getTitle(parsedData)
function getTitle(data?: ParsedSmartContractData): string {
const locale = `popups.${localeKey}.title`

tokenTransfer = transfer
break
}
case ParsedSmartContractType.NftTransfer: {
nft = getNftByIdForAccount($selectedAccount?.index as number, parsedData.nftId)
break
}
case ParsedSmartContractType.SmartContract: {
isSmartContractCall = true
break
}
default: {
break
}
if (data?.type === ParsedSmartContractType.TokenApproval) {
const token = getTokenFromSelectedAccountTokens(data.tokenId, evmNetwork.id)
return localize(locale, {
dappName: dapp.metadata?.name,
assetName: token?.metadata?.name ?? truncateString(data.tokenId, 6, 6),
})
}

return localize(locale, { contractAddress: truncateString(String(preparedTransaction.to), 6, 6) })
}

async function getSignedTransaction(account: IAccountState): Promise<string> {
Expand Down Expand Up @@ -176,10 +149,15 @@

modifyPopupState({ preventClose: false }, true)
busy = false

const successMessage = localize(`popups.${localeKey}.success`, {
recipient: truncateString(String(preparedTransaction.to), 6, 6),
assetName: getAssetName(),
})
openPopup({
id: PopupId.SuccessfulDappInteraction,
props: {
successMessage: getSuccessMessage(),
successMessage,
dapp,
},
})
Expand All @@ -190,11 +168,17 @@
}
}

function getSuccessMessage(): string {
const recipient = truncateString(String(preparedTransaction.to), 6, 6)
const assetName =
tokenTransfer?.token?.metadata?.name ?? baseCoinTransfer?.token?.metadata?.name ?? nft?.name ?? ''
return localize(`popups.${localeKey}.success`, { recipient, assetName })
function getAssetName(): string {
switch (parsedData?.type) {
case ParsedSmartContractType.CoinTransfer:
return getTokenFromSelectedAccountTokens(BASE_TOKEN_ID, evmNetwork.id)?.metadata?.name ?? ''
case ParsedSmartContractType.TokenTransfer:
return getTokenFromSelectedAccountTokens(parsedData.tokenId, evmNetwork.id)?.metadata?.name ?? ''
case ParsedSmartContractType.NftTransfer:
return getNftByIdForAccount($selectedAccount?.index, parsedData.nftId)?.metadata?.name ?? ''
default:
return ''
}
}

function onCancelClick(): void {
Expand All @@ -218,9 +202,7 @@
</script>

<PopupTemplate
title={localize(`popups.${localeKey}.title`, {
contractAddress: truncateString(String(preparedTransaction.to), 6, 6),
})}
{title}
backButton={{
text: localize('actions.cancel'),
onClick: onCancelClick,
Expand All @@ -241,35 +223,61 @@
/>

<div class="space-y-5">
{#if isSmartContractCall}
{#if !preparedTransaction.data}
{@const baseCoinTransfer = {
token: getTokenFromSelectedAccountTokens(BASE_TOKEN_ID, evmNetwork.id),
rawAmount: Converter.bigIntLikeToBigInt(preparedTransaction.value),
}}
<TransactionAssetSection {baseCoinTransfer} />
{:else if parsedData?.type === ParsedSmartContractType.CoinTransfer}
{@const baseCoinTransfer = {
token: getTokenFromSelectedAccountTokens(BASE_TOKEN_ID, evmNetwork.id),
rawAmount: parsedData.rawAmount,
}}
<TransactionAssetSection {baseCoinTransfer} />
{:else if parsedData?.type === ParsedSmartContractType.TokenTransfer}
{@const tokenTransfer = {
token: getTokenFromSelectedAccountTokens(parsedData.tokenId, evmNetwork.id),
rawAmount: parsedData.rawAmount,
}}
<TransactionAssetSection {tokenTransfer} />
{:else if parsedData?.type === ParsedSmartContractType.NftTransfer}
{@const nft = getNftByIdForAccount($selectedAccount?.index, parsedData.nftId)}
<TransactionAssetSection {nft} />
{:else if parsedData?.type === ParsedSmartContractType.TokenApproval}
<div class="flex flex-col gap-3">
<EvmTokenApprovalAlert parsedTokenApproval={parsedData} networkId={evmNetwork.id} />
</div>
{:else if parsedData?.type === ParsedSmartContractType.SmartContract}
<div class="flex flex-col gap-3">
<Alert variant="warning">
<Text slot="text">
{localize('popups.smartContractCall.unableToVerify')}
<Link
on:click={() => onExplorerClick(String(preparedTransaction.to))}
text={localize('popups.smartContractCall.viewSmartContract')}
/>
</Text>
<EvmTransactionAlert
variant="warning"
message={localize('popups.smartContractCall.unableToVerify')}
networkId={evmNetwork.id}
contractAddress={String(preparedTransaction.to)}
>
<Table
collapsible
collapsibleTitle={localize('general.details')}
slot="body"
items={[
{
key: localize('general.address'),
value: truncateString(String(preparedTransaction.to), 16, 16),
onClick: () => onExplorerClick(String(preparedTransaction.to)),
},
{ key: localize('general.methodName'), value: methodName },
{ key: localize('general.parameters'), value: inputs },
{ key: localize('general.methodName'), value: parsedData.parsedMethod?.name },
{
key: localize('general.parameters'),
value: parsedData?.parsedMethod?.inputs.reduce((acc, input) => {
acc[input.name] = input.value
return acc
}, {}),
},
{ key: localize('general.data'), value: String(preparedTransaction.data), copyable: true },
]}
/>
</Alert>
</EvmTransactionAlert>
</div>
{:else}
<TransactionAssetSection {baseCoinTransfer} {tokenTransfer} {nft} />
{/if}
<EvmTransactionDetails
bind:selectedGasSpeed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export enum ParsedSmartContractType {
TokenTransfer,
NftTransfer,
SmartContract,
TokenApproval,
}
Loading
Loading