From 9be77e2a9c12ed0d6270f68c795e5f135faa0c1f Mon Sep 17 00:00:00 2001 From: Enes Date: Tue, 30 Apr 2024 15:18:56 +0300 Subject: [PATCH] refactor: replace convert word to swap (#2182) Co-authored-by: Sven --- packages/core/index.ts | 7 +- .../core/src/controllers/AccountController.ts | 6 +- .../controllers/BlockchainApiController.ts | 30 +- .../core/src/controllers/RouterController.ts | 10 +- ...ConvertController.ts => SwapController.ts} | 80 ++--- .../{ConvertApiUtil.ts => SwapApiUtil.ts} | 27 +- packages/core/src/utils/TypeUtil.ts | 18 +- .../controllers/ConvertController.test.ts | 78 ----- .../tests/controllers/SwapController.test.ts | 78 +++++ ...ConvertController.ts => SwapController.ts} | 0 packages/scaffold/index.ts | 14 +- .../src/modal/w3m-onramp-widget/index.ts | 8 +- .../scaffold/src/modal/w3m-router/index.ts | 12 +- .../index.ts | 12 +- .../src/partials/w3m-convert-input/index.ts | 222 -------------- .../src/partials/w3m-convert-input/styles.ts | 131 -------- .../scaffold/src/partials/w3m-header/index.ts | 6 +- .../src/partials/w3m-onramp-input/index.ts | 113 +++++++ .../src/partials/w3m-onramp-input/styles.ts | 35 +++ .../index.ts | 10 +- .../styles.ts | 0 .../src/partials/w3m-swap-input/index.ts | 280 ++++++++++++------ .../src/partials/w3m-swap-input/styles.ts | 132 +++++++-- .../index.ts | 52 ++-- .../styles.ts | 6 +- .../index.ts | 45 ++- .../styles.ts | 0 .../index.ts | 109 ++++--- .../styles.ts | 2 +- packages/ui/index.ts | 4 +- ...sk-bottom.ts => swap-input-mask-bottom.ts} | 2 +- ...put-mask-top.ts => swap-input-mask-top.ts} | 2 +- .../composites/wui-token-list-item/index.ts | 15 +- packages/wallet/src/W3mFrame.ts | 2 - 34 files changed, 763 insertions(+), 785 deletions(-) rename packages/core/src/controllers/{ConvertController.ts => SwapController.ts} (90%) rename packages/core/src/utils/{ConvertApiUtil.ts => SwapApiUtil.ts} (79%) delete mode 100644 packages/core/tests/controllers/ConvertController.test.ts create mode 100644 packages/core/tests/controllers/SwapController.test.ts rename packages/core/tests/mocks/{ConvertController.ts => SwapController.ts} (100%) delete mode 100644 packages/scaffold/src/partials/w3m-convert-input/index.ts delete mode 100644 packages/scaffold/src/partials/w3m-convert-input/styles.ts create mode 100644 packages/scaffold/src/partials/w3m-onramp-input/index.ts create mode 100644 packages/scaffold/src/partials/w3m-onramp-input/styles.ts rename packages/scaffold/src/partials/{w3m-convert-details => w3m-swap-details}/index.ts (95%) rename packages/scaffold/src/partials/{w3m-convert-details => w3m-swap-details}/styles.ts (100%) rename packages/scaffold/src/views/{w3m-convert-preview-view => w3m-swap-preview-view}/index.ts (80%) rename packages/scaffold/src/views/{w3m-convert-preview-view => w3m-swap-preview-view}/styles.ts (95%) rename packages/scaffold/src/views/{w3m-convert-select-token-view => w3m-swap-select-token-view}/index.ts (83%) rename packages/scaffold/src/views/{w3m-convert-select-token-view => w3m-swap-select-token-view}/styles.ts (100%) rename packages/scaffold/src/views/{w3m-convert-view => w3m-swap-view}/index.ts (72%) rename packages/scaffold/src/views/{w3m-convert-view => w3m-swap-view}/styles.ts (98%) rename packages/ui/src/assets/svg/{convert-input-mask-bottom.ts => swap-input-mask-bottom.ts} (97%) rename packages/ui/src/assets/svg/{convert-input-mask-top.ts => swap-input-mask-top.ts} (97%) diff --git a/packages/core/index.ts b/packages/core/index.ts index a87c64cd55..e7e4af8760 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -55,11 +55,8 @@ export type { EventsControllerState } from './src/controllers/EventsController.j export { TransactionsController } from './src/controllers/TransactionsController.js' export type { TransactionsControllerState } from './src/controllers/TransactionsController.js' -export { ConvertController } from './src/controllers/ConvertController.js' -export type { - ConvertControllerState, - ConvertInputTarget -} from './src/controllers/ConvertController.js' +export { SwapController } from './src/controllers/SwapController.js' +export type { SwapControllerState, SwapInputTarget } from './src/controllers/SwapController.js' export { SendController } from './src/controllers/SendController.js' export type { SendControllerState } from './src/controllers/SendController.js' diff --git a/packages/core/src/controllers/AccountController.ts b/packages/core/src/controllers/AccountController.ts index bd7df3541b..aa6ff0d414 100644 --- a/packages/core/src/controllers/AccountController.ts +++ b/packages/core/src/controllers/AccountController.ts @@ -5,8 +5,8 @@ import type { CaipAddress, ConnectedWalletInfo } from '../utils/TypeUtil.js' import type { Balance } from '@web3modal/common' import { BlockchainApiController } from './BlockchainApiController.js' import { SnackController } from './SnackController.js' -import { ConvertController } from './ConvertController.js' -import { ConvertApiUtil } from '../utils/ConvertApiUtil.js' +import { SwapController } from './SwapController.js' +import { SwapApiUtil } from '../utils/SwapApiUtil.js' import type { W3mFrameTypes } from '@web3modal/wallet' // -- Types --------------------------------------------- // @@ -105,7 +105,7 @@ export const AccountController = { const response = await BlockchainApiController.getBalance(state.address) this.setTokenBalance(response.balances) - ConvertController.setBalances(ConvertApiUtil.mapBalancesToConvertTokens(response.balances)) + SwapController.setBalances(SwapApiUtil.mapBalancesToSwapTokens(response.balances)) } } catch (error) { SnackController.showError('Failed to fetch token balance') diff --git a/packages/core/src/controllers/BlockchainApiController.ts b/packages/core/src/controllers/BlockchainApiController.ts index bb520be0a3..37799eef46 100644 --- a/packages/core/src/controllers/BlockchainApiController.ts +++ b/packages/core/src/controllers/BlockchainApiController.ts @@ -4,14 +4,14 @@ import { FetchUtil } from '../utils/FetchUtil.js' import type { BlockchainApiTransactionsRequest, BlockchainApiTransactionsResponse, - BlockchainApiConvertTokensRequest, - BlockchainApiConvertTokensResponse, - BlockchainApiGenerateConvertCalldataRequest, - BlockchainApiGenerateConvertCalldataResponse, + BlockchainApiSwapTokensRequest, + BlockchainApiSwapTokensResponse, + BlockchainApiGenerateSwapCalldataRequest, + BlockchainApiGenerateSwapCalldataResponse, BlockchainApiGenerateApproveCalldataRequest, BlockchainApiGenerateApproveCalldataResponse, - BlockchainApiConvertAllowanceRequest, - BlockchainApiConvertAllowanceResponse, + BlockchainApiSwapAllowanceRequest, + BlockchainApiSwapAllowanceResponse, BlockchainApiGasPriceRequest, BlockchainApiGasPriceResponse, BlockchainApiTokenPriceRequest, @@ -136,8 +136,8 @@ export const BlockchainApiController = { }) }, - fetchConvertTokens({ projectId, chainId }: BlockchainApiConvertTokensRequest) { - return api.get({ + fetchSwapTokens({ projectId, chainId }: BlockchainApiSwapTokensRequest) { + return api.get({ path: `/v1/convert/tokens?projectId=${projectId}&chainId=${chainId}` }) }, @@ -156,14 +156,10 @@ export const BlockchainApiController = { }) }, - fetchConvertAllowance({ - projectId, - tokenAddress, - userAddress - }: BlockchainApiConvertAllowanceRequest) { + fetchSwapAllowance({ projectId, tokenAddress, userAddress }: BlockchainApiSwapAllowanceRequest) { const { sdkType, sdkVersion } = OptionsController.state - return api.get({ + return api.get({ path: `/v1/convert/allowance?projectId=${projectId}&tokenAddress=${tokenAddress}&userAddress=${userAddress}`, headers: { 'Content-Type': 'application/json', @@ -186,14 +182,14 @@ export const BlockchainApiController = { }) }, - generateConvertCalldata({ + generateSwapCalldata({ amount, from, projectId, to, userAddress - }: BlockchainApiGenerateConvertCalldataRequest) { - return api.post({ + }: BlockchainApiGenerateSwapCalldataRequest) { + return api.post({ path: '/v1/convert/build-transaction', headers: { 'Content-Type': 'application/json' diff --git a/packages/core/src/controllers/RouterController.ts b/packages/core/src/controllers/RouterController.ts index 3bc78779e5..34dcd339dd 100644 --- a/packages/core/src/controllers/RouterController.ts +++ b/packages/core/src/controllers/RouterController.ts @@ -1,7 +1,7 @@ import { subscribeKey as subKey } from 'valtio/vanilla/utils' import { proxy } from 'valtio/vanilla' import type { CaipNetwork, Connector, WcWallet } from '../utils/TypeUtil.js' -import type { ConvertInputTarget } from './ConvertController.js' +import type { SwapInputTarget } from './SwapController.js' // -- Types --------------------------------------------- // type TransactionAction = { @@ -47,9 +47,9 @@ export interface RouterControllerState { | 'WhatIsANetwork' | 'WhatIsAWallet' | 'WhatIsABuy' - | 'Convert' - | 'ConvertSelectToken' - | 'ConvertPreview' + | 'Swap' + | 'SwapSelectToken' + | 'SwapPreview' history: RouterControllerState['view'][] data?: { connector?: Connector @@ -57,7 +57,7 @@ export interface RouterControllerState { network?: CaipNetwork email?: string newEmail?: string - target?: ConvertInputTarget + target?: SwapInputTarget } transactionStack: TransactionAction[] } diff --git a/packages/core/src/controllers/ConvertController.ts b/packages/core/src/controllers/SwapController.ts similarity index 90% rename from packages/core/src/controllers/ConvertController.ts rename to packages/core/src/controllers/SwapController.ts index 8a09ea4a3e..984f826542 100644 --- a/packages/core/src/controllers/ConvertController.ts +++ b/packages/core/src/controllers/SwapController.ts @@ -3,11 +3,11 @@ import { proxy, subscribe as sub } from 'valtio/vanilla' import { AccountController } from './AccountController.js' import { ConstantsUtil } from '../utils/ConstantsUtil.js' import { ConnectionController } from './ConnectionController.js' -import { ConvertApiUtil } from '../utils/ConvertApiUtil.js' +import { SwapApiUtil } from '../utils/SwapApiUtil.js' import { SnackController } from './SnackController.js' import { RouterController } from './RouterController.js' import { NumberUtil } from '@web3modal/common' -import type { ConvertTokenWithBalance } from '../utils/TypeUtil.js' +import type { SwapTokenWithBalance } from '../utils/TypeUtil.js' import { NetworkController } from './NetworkController.js' import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' import { BlockchainApiController } from './BlockchainApiController.js' @@ -17,7 +17,7 @@ import { OptionsController } from './OptionsController.js' export const INITIAL_GAS_LIMIT = 150000 // -- Types --------------------------------------------- // -export type ConvertInputTarget = 'sourceToken' | 'toToken' +export type SwapInputTarget = 'sourceToken' | 'toToken' type TransactionParams = { data: string @@ -38,23 +38,23 @@ class TransactionError extends Error { } } -export interface ConvertControllerState { +export interface SwapControllerState { // Loading states initialized: boolean loadingPrices: boolean loading?: boolean - // Approval & Convert transaction states + // Approval & Swap transaction states approvalTransaction: TransactionParams | undefined - convertTransaction: TransactionParams | undefined + swapTransaction: TransactionParams | undefined transactionLoading?: boolean transactionError?: string // Input values - sourceToken?: ConvertTokenWithBalance + sourceToken?: SwapTokenWithBalance sourceTokenAmount: string sourceTokenPriceInUSD: number - toToken?: ConvertTokenWithBalance + toToken?: SwapTokenWithBalance toTokenAmount: string toTokenPriceInUSD: number networkPrice: string @@ -65,11 +65,11 @@ export interface ConvertControllerState { slippage: number // Tokens - tokens?: ConvertTokenWithBalance[] - suggestedTokens?: ConvertTokenWithBalance[] - popularTokens?: ConvertTokenWithBalance[] - foundTokens?: ConvertTokenWithBalance[] - myTokensWithBalance?: ConvertTokenWithBalance[] + tokens?: SwapTokenWithBalance[] + suggestedTokens?: SwapTokenWithBalance[] + popularTokens?: SwapTokenWithBalance[] + foundTokens?: SwapTokenWithBalance[] + myTokensWithBalance?: SwapTokenWithBalance[] tokensPriceMap: Record // Calculations @@ -91,18 +91,18 @@ export interface TokenInfo { tags?: string[] } -type StateKey = keyof ConvertControllerState +type StateKey = keyof SwapControllerState // -- State --------------------------------------------- // -const state = proxy({ +const state = proxy({ // Loading states initialized: false, loading: false, loadingPrices: false, - // Approval & Convert transaction states + // Approval & Swap transaction states approvalTransaction: undefined, - convertTransaction: undefined, + swapTransaction: undefined, transactionError: undefined, transactionLoading: false, @@ -136,14 +136,14 @@ const state = proxy({ }) // -- Controller ---------------------------------------- // -export const ConvertController = { +export const SwapController = { state, - subscribe(callback: (newState: ConvertControllerState) => void) { + subscribe(callback: (newState: SwapControllerState) => void) { return sub(state, () => callback(state)) }, - subscribeKey(key: K, callback: (value: ConvertControllerState[K]) => void) { + subscribeKey(key: K, callback: (value: SwapControllerState[K]) => void) { return subKey(state, key, callback) }, @@ -172,7 +172,7 @@ export const ConvertController = { state.loading = loading }, - setSourceToken(sourceToken: ConvertTokenWithBalance | undefined) { + setSourceToken(sourceToken: SwapTokenWithBalance | undefined) { if (!sourceToken) { return } @@ -191,7 +191,7 @@ export const ConvertController = { } }, - setToToken(toToken: ConvertTokenWithBalance | undefined) { + setToToken(toToken: SwapTokenWithBalance | undefined) { const { sourceTokenAddress, sourceTokenAmount } = this.getParams() if (!toToken) { @@ -219,7 +219,7 @@ export const ConvertController = { } }, - async setTokenValues(address: string, target: ConvertInputTarget) { + async setTokenValues(address: string, target: SwapInputTarget) { let price = state.tokensPriceMap[address] || 0 if (!price) { @@ -241,7 +241,7 @@ export const ConvertController = { this.setToToken(newToToken) this.setSourceTokenAmount(state.toTokenAmount || '0') - ConvertController.convertTokens() + SwapController.swapTokens() }, resetTokens() { @@ -288,7 +288,7 @@ export const ConvertController = { }, async getTokenList() { - const tokens = await ConvertApiUtil.getTokenList() + const tokens = await SwapApiUtil.getTokenList() state.tokens = tokens state.popularTokens = tokens @@ -354,7 +354,7 @@ export const ConvertController = { }, async getMyTokensWithBalance() { - const balances = await ConvertApiUtil.getMyTokensWithBalance() + const balances = await SwapApiUtil.getMyTokensWithBalance() if (!balances) { return @@ -364,7 +364,7 @@ export const ConvertController = { this.setBalances(balances) }, - setBalances(balances: ConvertTokenWithBalance[]) { + setBalances(balances: SwapTokenWithBalance[]) { const { networkAddress } = this.getParams() const networkToken = balances.find(token => token.address === networkAddress) @@ -379,7 +379,7 @@ export const ConvertController = { }, async getInitialGasPrice() { - const res = await ConvertApiUtil.fetchGasPrice() + const res = await SwapApiUtil.fetchGasPrice() if (!res) { return @@ -393,7 +393,7 @@ export const ConvertController = { state.gasPriceInUSD = gasPrice }, - async refreshConvertValues() { + async refreshSwapValues() { const { fromAddress, toTokenDecimals, toTokenAddress } = this.getParams() if (fromAddress && toTokenAddress && toTokenDecimals && !state.loading) { @@ -441,7 +441,7 @@ export const ConvertController = { return maxSlippageAmount.toNumber() }, - async convertTokens() { + async swapTokens() { const { sourceTokenAddress, toTokenAddress } = this.getParams() if (!sourceTokenAddress || !toTokenAddress) { @@ -478,7 +478,7 @@ export const ConvertController = { return undefined } - const hasAllowance = await ConvertApiUtil.fetchConvertAllowance({ + const hasAllowance = await SwapApiUtil.fetchSwapAllowance({ userAddress: fromCaipAddress, tokenAddress: sourceTokenAddress, sourceTokenAmount, @@ -489,10 +489,10 @@ export const ConvertController = { if (hasAllowance) { state.approvalTransaction = undefined - transaction = await this.createConvert() - state.convertTransaction = transaction + transaction = await this.createSwap() + state.swapTransaction = transaction } else { - state.convertTransaction = undefined + state.swapTransaction = undefined transaction = await this.createTokenAllowance() state.approvalTransaction = transaction } @@ -505,14 +505,14 @@ export const ConvertController = { const decimals = sourceTokenDecimals || 18 const multiplier = 10 ** decimals - const toTokenConvertedAmount = + const toTokenSwapedAmount = state.sourceTokenPriceInUSD && state.toTokenPriceInUSD && state.sourceTokenAmount ? NumberUtil.bigNumber(state.sourceTokenAmount) .multipliedBy(state.sourceTokenPriceInUSD) .dividedBy(state.toTokenPriceInUSD) : NumberUtil.bigNumber(0) - return toTokenConvertedAmount.multipliedBy(multiplier).toString() + return toTokenSwapedAmount.multipliedBy(multiplier).toString() }, async createTokenAllowance() { @@ -581,7 +581,7 @@ export const ConvertController = { } }, - async createConvert() { + async createSwap() { const { networkAddress, fromCaipAddress, @@ -607,7 +607,7 @@ export const ConvertController = { sourceTokenDecimals ).toString() - const response = await BlockchainApiController.generateConvertCalldata({ + const response = await BlockchainApiController.generateSwapCalldata({ projectId: OptionsController.state.projectId, userAddress: fromCaipAddress, from: sourceTokenAddress, @@ -638,7 +638,7 @@ export const ConvertController = { } }, - async sendTransactionForConvert(data: TransactionParams | undefined) { + async sendTransactionForSwap(data: TransactionParams | undefined) { if (!data) { return undefined } @@ -651,7 +651,7 @@ export const ConvertController = { view: 'Account', goBack: false, onSuccess() { - ConvertController.resetValues() + SwapController.resetValues() } }) diff --git a/packages/core/src/utils/ConvertApiUtil.ts b/packages/core/src/utils/SwapApiUtil.ts similarity index 79% rename from packages/core/src/utils/ConvertApiUtil.ts rename to packages/core/src/utils/SwapApiUtil.ts index fe7d323a62..db513e2916 100644 --- a/packages/core/src/utils/ConvertApiUtil.ts +++ b/packages/core/src/utils/SwapApiUtil.ts @@ -1,14 +1,11 @@ import { NetworkController } from '../controllers/NetworkController.js' import { AccountController } from '../controllers/AccountController.js' import { ConnectionController } from '../controllers/ConnectionController.js' -import { ConstantsUtil } from '../utils/ConstantsUtil.js' +import { ConstantsUtil } from './ConstantsUtil.js' import { BlockchainApiController } from '../controllers/BlockchainApiController.js' -import type { ConvertTokenWithBalance } from './TypeUtil.js' +import type { SwapTokenWithBalance } from './TypeUtil.js' import { OptionsController } from '../controllers/OptionsController.js' -import type { - BlockchainApiConvertAllowanceRequest, - BlockchainApiBalanceResponse -} from '../utils/TypeUtil.js' +import type { BlockchainApiSwapAllowanceRequest, BlockchainApiBalanceResponse } from './TypeUtil.js' // -- Types --------------------------------------------- // export type TokenInfo = { @@ -24,9 +21,9 @@ export type TokenInfo = { } // -- Controller ---------------------------------------- // -export const ConvertApiUtil = { +export const SwapApiUtil = { async getTokenList() { - const response = await BlockchainApiController.fetchConvertTokens({ + const response = await BlockchainApiController.fetchSwapTokens({ chainId: NetworkController.state.caipNetwork?.id, projectId: OptionsController.state.projectId }) @@ -41,7 +38,7 @@ export const ConvertApiUtil = { }, price: 0, value: 0 - } as ConvertTokenWithBalance + } as SwapTokenWithBalance }) return tokens @@ -61,18 +58,18 @@ export const ConvertApiUtil = { }) }, - async fetchConvertAllowance({ + async fetchSwapAllowance({ tokenAddress, userAddress, sourceTokenAmount, sourceTokenDecimals - }: Pick & { + }: Pick & { sourceTokenAmount: string sourceTokenDecimals: number }) { const projectId = OptionsController.state.projectId - const response = await BlockchainApiController.fetchConvertAllowance({ + const response = await BlockchainApiController.fetchSwapAllowance({ projectId, tokenAddress, userAddress @@ -99,10 +96,10 @@ export const ConvertApiUtil = { const response = await BlockchainApiController.getBalance(address, caipNetwork.id) const balances = response.balances - return this.mapBalancesToConvertTokens(balances) + return this.mapBalancesToSwapTokens(balances) }, - mapBalancesToConvertTokens(balances: BlockchainApiBalanceResponse['balances']) { + mapBalancesToSwapTokens(balances: BlockchainApiBalanceResponse['balances']) { return balances.map(token => { return { symbol: token.symbol, @@ -116,7 +113,7 @@ export const ConvertApiUtil = { quantity: token.quantity, price: token.price, value: token.value - } as ConvertTokenWithBalance + } as SwapTokenWithBalance }) } } diff --git a/packages/core/src/utils/TypeUtil.ts b/packages/core/src/utils/TypeUtil.ts index 697937ab5a..5c1d86f44e 100644 --- a/packages/core/src/utils/TypeUtil.ts +++ b/packages/core/src/utils/TypeUtil.ts @@ -160,7 +160,7 @@ export interface BlockchainApiTransactionsResponse { next: string | null } -export type ConvertToken = { +export type SwapToken = { name: string symbol: string address: `${string}:${string}:${string}` @@ -169,7 +169,7 @@ export type ConvertToken = { eip2612?: boolean } -export type ConvertTokenWithBalance = ConvertToken & { +export type SwapTokenWithBalance = SwapToken & { quantity: { decimals: string numeric: string @@ -178,13 +178,13 @@ export type ConvertTokenWithBalance = ConvertToken & { value: number } -export interface BlockchainApiConvertTokensRequest { +export interface BlockchainApiSwapTokensRequest { projectId: string chainId?: string } -export interface BlockchainApiConvertTokensResponse { - tokens: ConvertToken[] +export interface BlockchainApiSwapTokensResponse { + tokens: SwapToken[] } export interface BlockchainApiTokenPriceRequest { @@ -202,13 +202,13 @@ export interface BlockchainApiTokenPriceResponse { }[] } -export interface BlockchainApiConvertAllowanceRequest { +export interface BlockchainApiSwapAllowanceRequest { projectId: string tokenAddress: string userAddress: string } -export interface BlockchainApiConvertAllowanceResponse { +export interface BlockchainApiSwapAllowanceResponse { allowance: string } @@ -223,7 +223,7 @@ export interface BlockchainApiGasPriceResponse { instant: string } -export interface BlockchainApiGenerateConvertCalldataRequest { +export interface BlockchainApiGenerateSwapCalldataRequest { projectId: string userAddress: string from: string @@ -235,7 +235,7 @@ export interface BlockchainApiGenerateConvertCalldataRequest { } } -export interface BlockchainApiGenerateConvertCalldataResponse { +export interface BlockchainApiGenerateSwapCalldataResponse { tx: { from: `${string}:${string}:${string}` to: `${string}:${string}:${string}` diff --git a/packages/core/tests/controllers/ConvertController.test.ts b/packages/core/tests/controllers/ConvertController.test.ts deleted file mode 100644 index 9ce77ee500..0000000000 --- a/packages/core/tests/controllers/ConvertController.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { beforeAll, describe, expect, it } from 'vitest' -import { AccountController, ConvertController, type ConvertTokenWithBalance } from '../../index.js' -import { prices, tokenInfo } from '../mocks/ConvertController.js' -import { INITIAL_GAS_LIMIT } from '../../src/controllers/ConvertController.js' - -// - Mocks --------------------------------------------------------------------- -const caipAddress = 'eip155:1:0x123' -const gasLimit = BigInt(INITIAL_GAS_LIMIT) -const gasFee = BigInt(455966887160) - -const sourceTokenAddress = 'eip155:137:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' - -const sourceToken = tokenInfo[0] as ConvertTokenWithBalance -const toToken = tokenInfo[1] as ConvertTokenWithBalance - -// - Helpers -function setSourceTokenAmount(value: string) { - ConvertController.setSourceTokenAmount(value) - const toTokenAmount = ConvertController.getToAmount() - const toTokenValues = ConvertController.getToTokenValues(toTokenAmount, toToken?.decimals) - ConvertController.state.toTokenAmount = toTokenValues.toTokenAmount - ConvertController.state.toTokenPriceInUSD = toTokenValues.toTokenPriceInUSD -} - -// - Setup --------------------------------------------------------------------- -beforeAll(() => { - AccountController.setCaipAddress(caipAddress) - ConvertController.state.tokensPriceMap = prices - ConvertController.state.networkPrice = prices[sourceTokenAddress].toString() - ConvertController.state.networkBalanceInUSD = '2' - ConvertController.state.gasPriceInUSD = ConvertController.calculateGasPriceInUSD(gasLimit, gasFee) - ConvertController.setSourceToken(sourceToken) - ConvertController.state.sourceTokenPriceInUSD = sourceToken.price - ConvertController.setToToken(toToken) - setSourceTokenAmount('1') -}) - -// -- Tests -------------------------------------------------------------------- -describe('ConvertController', () => { - it('should set toToken as expected', () => { - expect(ConvertController.state.toToken?.address).toEqual(toToken.address) - }) - - it('should set sourceToken as expected', () => { - expect(ConvertController.state.sourceToken?.address).toEqual(sourceToken.address) - }) - - it('should calculate gas price in Ether and USD as expected', () => { - const gasPriceInEther = ConvertController.calculateGasPriceInEther(gasLimit, gasFee) - const gasPriceInUSD = ConvertController.calculateGasPriceInUSD(gasLimit, gasFee) - - expect(gasPriceInEther).toEqual(0.068395033074) - expect(gasPriceInUSD).toEqual(0.06395499714651795) - }) - - it('should return insufficient balance as expected', () => { - ConvertController.state.networkBalanceInUSD = '0' - expect(ConvertController.isInsufficientNetworkTokenForGas()).toEqual(true) - }) - - it('should calculate convert values as expected', () => { - expect(ConvertController.state.toTokenAmount).toEqual('6.77656269188470721788') - expect(ConvertController.state.toTokenPriceInUSD).toEqual(0.10315220553291868) - }) - - it('should calculate the price impact as expected', () => { - const priceImpact = ConvertController.calculatePriceImpact( - ConvertController.state.toTokenAmount, - ConvertController.calculateGasPriceInUSD(gasLimit, gasFee) - ) - expect(priceImpact).equal(9.14927128867287) - }) - - it('should calculate the maximum slippage as expected', () => { - const maxSlippage = ConvertController.calculateMaxSlippage() - expect(maxSlippage).toEqual(0.01) - }) -}) diff --git a/packages/core/tests/controllers/SwapController.test.ts b/packages/core/tests/controllers/SwapController.test.ts new file mode 100644 index 0000000000..e07853f097 --- /dev/null +++ b/packages/core/tests/controllers/SwapController.test.ts @@ -0,0 +1,78 @@ +import { beforeAll, describe, expect, it } from 'vitest' +import { AccountController, SwapController, type SwapTokenWithBalance } from '../../index.js' +import { prices, tokenInfo } from '../mocks/SwapController.js' +import { INITIAL_GAS_LIMIT } from '../../src/controllers/SwapController.js' + +// - Mocks --------------------------------------------------------------------- +const caipAddress = 'eip155:1:0x123' +const gasLimit = BigInt(INITIAL_GAS_LIMIT) +const gasFee = BigInt(455966887160) + +const sourceTokenAddress = 'eip155:137:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + +const sourceToken = tokenInfo[0] as SwapTokenWithBalance +const toToken = tokenInfo[1] as SwapTokenWithBalance + +// - Helpers +function setSourceTokenAmount(value: string) { + SwapController.setSourceTokenAmount(value) + const toTokenAmount = SwapController.getToAmount() + const toTokenValues = SwapController.getToTokenValues(toTokenAmount, toToken?.decimals) + SwapController.state.toTokenAmount = toTokenValues.toTokenAmount + SwapController.state.toTokenPriceInUSD = toTokenValues.toTokenPriceInUSD +} + +// - Setup --------------------------------------------------------------------- +beforeAll(() => { + AccountController.setCaipAddress(caipAddress) + SwapController.state.tokensPriceMap = prices + SwapController.state.networkPrice = prices[sourceTokenAddress].toString() + SwapController.state.networkBalanceInUSD = '2' + SwapController.state.gasPriceInUSD = SwapController.calculateGasPriceInUSD(gasLimit, gasFee) + SwapController.setSourceToken(sourceToken) + SwapController.state.sourceTokenPriceInUSD = sourceToken.price + SwapController.setToToken(toToken) + setSourceTokenAmount('1') +}) + +// -- Tests -------------------------------------------------------------------- +describe('SwapController', () => { + it('should set toToken as expected', () => { + expect(SwapController.state.toToken?.address).toEqual(toToken.address) + }) + + it('should set sourceToken as expected', () => { + expect(SwapController.state.sourceToken?.address).toEqual(sourceToken.address) + }) + + it('should calculate gas price in Ether and USD as expected', () => { + const gasPriceInEther = SwapController.calculateGasPriceInEther(gasLimit, gasFee) + const gasPriceInUSD = SwapController.calculateGasPriceInUSD(gasLimit, gasFee) + + expect(gasPriceInEther).toEqual(0.068395033074) + expect(gasPriceInUSD).toEqual(0.06395499714651795) + }) + + it('should return insufficient balance as expected', () => { + SwapController.state.networkBalanceInUSD = '0' + expect(SwapController.isInsufficientNetworkTokenForGas()).toEqual(true) + }) + + it('should calculate swap values as expected', () => { + expect(SwapController.state.toTokenAmount).toEqual('6.77656269188470721788') + expect(SwapController.state.toTokenPriceInUSD).toEqual(0.10315220553291868) + }) + + it('should calculate the price impact as expected', () => { + const priceImpact = SwapController.calculatePriceImpact( + SwapController.state.toTokenAmount, + SwapController.calculateGasPriceInUSD(gasLimit, gasFee) + ) + expect(priceImpact).equal(9.14927128867287) + }) + + it('should calculate the maximum slippage as expected', () => { + const maxSlippage = SwapController.calculateMaxSlippage() + expect(maxSlippage).toEqual(0.01) + }) +}) diff --git a/packages/core/tests/mocks/ConvertController.ts b/packages/core/tests/mocks/SwapController.ts similarity index 100% rename from packages/core/tests/mocks/ConvertController.ts rename to packages/core/tests/mocks/SwapController.ts diff --git a/packages/scaffold/index.ts b/packages/scaffold/index.ts index 57c1ab1b36..9ba073d69e 100644 --- a/packages/scaffold/index.ts +++ b/packages/scaffold/index.ts @@ -21,10 +21,10 @@ export * from './src/views/w3m-onramp-activity-view/index.js' export * from './src/views/w3m-onramp-fiat-select-view/index.js' export * from './src/views/w3m-onramp-providers-view/index.js' export * from './src/views/w3m-onramp-tokens-select-view/index.js' -export * from './src/views/w3m-convert-view/index.js' -export * from './src/views/w3m-convert-preview-view/index.js' -export * from './src/views/w3m-convert-select-token-view/index.js' -export * from './src/views/w3m-convert-view/index.js' +export * from './src/views/w3m-swap-view/index.js' +export * from './src/views/w3m-swap-preview-view/index.js' +export * from './src/views/w3m-swap-select-token-view/index.js' +export * from './src/views/w3m-swap-view/index.js' export * from './src/views/w3m-transactions-view/index.js' export * from './src/views/w3m-what-is-a-network-view/index.js' export * from './src/views/w3m-what-is-a-wallet-view/index.js' @@ -53,11 +53,11 @@ export * from './src/partials/w3m-connecting-wc-mobile/index.js' export * from './src/partials/w3m-connecting-wc-qrcode/index.js' export * from './src/partials/w3m-connecting-wc-unsupported/index.js' export * from './src/partials/w3m-connecting-wc-web/index.js' -export * from './src/partials/w3m-convert-details/index.js' -export * from './src/partials/w3m-convert-input/index.js' +export * from './src/partials/w3m-swap-details/index.js' +export * from './src/partials/w3m-swap-input/index.js' export * from './src/partials/w3m-header/index.js' export * from './src/partials/w3m-help-widget/index.js' -export * from './src/partials/w3m-swap-input/index.js' +export * from './src/partials/w3m-onramp-input/index.js' export * from './src/partials/w3m-legal-footer/index.js' export * from './src/partials/w3m-mobile-download-links/index.js' export * from './src/partials/w3m-onramp-providers-footer/index.js' diff --git a/packages/scaffold/src/modal/w3m-onramp-widget/index.ts b/packages/scaffold/src/modal/w3m-onramp-widget/index.ts index fd735869f3..2a9c6f5bed 100644 --- a/packages/scaffold/src/modal/w3m-onramp-widget/index.ts +++ b/packages/scaffold/src/modal/w3m-onramp-widget/index.ts @@ -64,16 +64,16 @@ export class W3mOnrampWidget extends LitElement { return html` - - + + > ${BUY_PRESET_AMOUNTS.map( amount => diff --git a/packages/scaffold/src/modal/w3m-router/index.ts b/packages/scaffold/src/modal/w3m-router/index.ts index 44928a9a43..3f73184755 100644 --- a/packages/scaffold/src/modal/w3m-router/index.ts +++ b/packages/scaffold/src/modal/w3m-router/index.ts @@ -117,12 +117,12 @@ export class W3mRouter extends LitElement { return html`` case 'WalletCompatibleNetworks': return html`` - case 'Convert': - return html`` - case 'ConvertSelectToken': - return html`` - case 'ConvertPreview': - return html`` + case 'Swap': + return html`` + case 'SwapSelectToken': + return html`` + case 'SwapPreview': + return html`` case 'WalletSend': return html`` case 'WalletSendSelectToken': diff --git a/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts b/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts index c3112eed28..fb2f8632cc 100644 --- a/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts +++ b/packages/scaffold/src/partials/w3m-account-wallet-features-widget/index.ts @@ -62,7 +62,7 @@ export class W3mAccountWalletFeaturesWidget extends LitElement { this.network = val.caipNetwork }) ) - this.watchConvertValues() + this.watchSwapValues() } public override disconnectedCallback() { @@ -105,8 +105,8 @@ export class W3mAccountWalletFeaturesWidget extends LitElement { icon="card" > AccountController.fetchTokenBalance(), 10000) } @@ -191,8 +191,8 @@ export class W3mAccountWalletFeaturesWidget extends LitElement { RouterController.push('OnRampProviders') } - private onConvertClick() { - RouterController.push('Convert') + private onSwapClick() { + RouterController.push('Swap') } private onReceiveClick() { diff --git a/packages/scaffold/src/partials/w3m-convert-input/index.ts b/packages/scaffold/src/partials/w3m-convert-input/index.ts deleted file mode 100644 index 2536797f5f..0000000000 --- a/packages/scaffold/src/partials/w3m-convert-input/index.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { html, LitElement } from 'lit' -import { property } from 'lit/decorators.js' -import { - EventsController, - RouterController, - type ConvertToken, - type ConvertInputTarget -} from '@web3modal/core' -import { NumberUtil } from '@web3modal/common' -import { - UiHelperUtil, - customElement, - convertInputMaskBottomSvg, - convertInputMaskTopSvg -} from '@web3modal/ui' -import styles from './styles.js' - -const MINIMUM_USD_VALUE_TO_CONVERT = 0.00005 - -@customElement('w3m-convert-input') -export class W3mConvertInput extends LitElement { - public static override styles = [styles] - - // -- State & Properties -------------------------------- // - @property() public focused = false - - @property() public balance: string | undefined - - @property() public value?: string - - @property() public price = 0 - - @property() public marketValue?: string = '$1.0345,00' - - @property() public disabled?: boolean - - @property() public target: ConvertInputTarget = 'sourceToken' - - @property() public token?: ConvertToken - - @property() public onSetAmount: ((target: ConvertInputTarget, value: string) => void) | null = - null - - @property() public onSetMaxValue: - | ((target: ConvertInputTarget, balance: string | undefined) => void) - | null = null - - // -- Render -------------------------------------------- // - public override render() { - const marketValue = this.marketValue || '0' - const isMarketValueGreaterThanZero = NumberUtil.bigNumber(marketValue).isGreaterThan(0) - - return html` - - ${this.target === 'sourceToken' ? convertInputMaskTopSvg : convertInputMaskBottomSvg} - - this.onFocusChange(true)} - @focusout=${() => this.onFocusChange(false)} - ?disabled=${this.disabled} - .value=${this.value} - @input=${this.dispatchInputChangeEvent} - @keydown=${this.handleKeydown} - placeholder="0" - /> - - ${isMarketValueGreaterThanZero ? `$${this.marketValue}` : null} - - - ${this.templateTokenSelectButton()} - - ` - } - - // -- Private ------------------------------------------- // - private handleKeydown(event: KeyboardEvent) { - const allowedKeys = [ - 'Backspace', - 'Meta', - 'Ctrl', - 'a', - 'c', - 'v', - 'ArrowLeft', - 'ArrowRight', - 'Tab' - ] - const isComma = event.key === ',' - const isDot = event.key === '.' - const isNumericKey = event.key >= '0' && event.key <= '9' - const currentValue = this.value - - if (!isNumericKey && !allowedKeys.includes(event.key) && !isDot && !isComma) { - event.preventDefault() - } - - if (isComma || isDot) { - if (currentValue?.includes('.') || currentValue?.includes(',')) { - event.preventDefault() - } - } - } - - private dispatchInputChangeEvent(event: InputEvent) { - if (!this.onSetAmount) { - return - } - - const value = (event.target as HTMLInputElement).value - if (value === ',' || value === '.') { - this.onSetAmount(this.target, '0.') - } else if (value.endsWith(',')) { - this.onSetAmount(this.target, value.replace(',', '.')) - } else { - this.onSetAmount(this.target, value) - } - } - - private setMaxValueToInput() { - this.onSetMaxValue?.(this.target, this.balance) - } - - private templateTokenSelectButton() { - if (!this.token) { - return html` - Select token - ` - } - - const tokenElement = this.token.logoUri - ? html`` - : html` - - ` - - return html` - - - ${this.tokenBalanceTemplate()} - - ` - } - - private tokenBalanceTemplate() { - const balanceValueInUSD = NumberUtil.multiply(this.balance, this.price) - const haveBalance = balanceValueInUSD - ? balanceValueInUSD?.isGreaterThan(MINIMUM_USD_VALUE_TO_CONVERT) - : false - - return html` - ${haveBalance - ? html` - ${UiHelperUtil.formatNumberToLocalString(this.balance, 3)} - ` - : null} - ${this.target === 'sourceToken' ? this.tokenActionButtonTemplate(haveBalance) : null} - ` - } - - private tokenActionButtonTemplate(haveBalance: boolean) { - if (haveBalance) { - return html` ` - } - - return html` ` - } - - private onFocusChange(state: boolean) { - this.focused = state - } - - private onSelectToken() { - EventsController.sendEvent({ type: 'track', event: 'CLICK_SELECT_TOKEN_TO_SWAP' }) - RouterController.push('ConvertSelectToken', { - target: this.target - }) - } - - private onBuyToken() { - RouterController.push('OnRampProviders') - } -} - -declare global { - interface HTMLElementTagNameMap { - 'w3m-convert-input': W3mConvertInput - } -} diff --git a/packages/scaffold/src/partials/w3m-convert-input/styles.ts b/packages/scaffold/src/partials/w3m-convert-input/styles.ts deleted file mode 100644 index 43a4782e4a..0000000000 --- a/packages/scaffold/src/partials/w3m-convert-input/styles.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { css } from 'lit' - -export default css` - :host > wui-flex { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - border-radius: var(--wui-border-radius-s); - padding: var(--wui-spacing-xl); - padding-right: var(--wui-spacing-s); - width: 100%; - height: 100px; - box-sizing: border-box; - position: relative; - } - - :host > wui-flex > svg.input_mask { - position: absolute; - inset: 0; - z-index: 5; - } - - :host wui-flex .input_mask__border, - :host wui-flex .input_mask__background { - transition: fill var(--wui-duration-md) var(--wui-ease-out-power-1); - will-change: fill; - } - - :host wui-flex .input_mask__border { - fill: var(--wui-gray-glass-005); - } - - :host wui-flex .input_mask__background { - fill: var(--wui-gray-glass-002); - } - - :host wui-flex.focus .input_mask__border { - fill: var(--wui-gray-glass-020); - } - - :host > wui-flex .swap-input, - :host > wui-flex .swap-token-button { - z-index: 10; - } - - :host > wui-flex .swap-input { - -webkit-mask-image: linear-gradient( - 270deg, - transparent 0px, - transparent 8px, - black 24px, - black 25px, - black 32px, - black 100% - ); - mask-image: linear-gradient( - 270deg, - transparent 0px, - transparent 8px, - black 24px, - black 25px, - black 32px, - black 100% - ); - } - - :host > wui-flex .swap-input input { - background: none; - border: none; - height: 42px; - width: 100%; - font-size: 32px; - font-style: normal; - font-weight: 400; - line-height: 130%; - letter-spacing: -1.28px; - outline: none; - caret-color: var(--wui-color-accent-100); - color: var(--wui-color-fg-100); - } - - :host > wui-flex .swap-input input:focus-visible { - outline: none; - } - - :host > wui-flex .swap-input input::-webkit-outer-spin-button, - :host > wui-flex .swap-input input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - .token-select-button { - display: flex; - align-items: center; - justify-content: center; - gap: var(--wui-spacing-xxs); - padding: var(--wui-spacing-xs); - padding-right: var(--wui-spacing-1xs); - height: 40px; - border: none; - border-radius: 80px; - background: var(--wui-gray-glass-002); - box-shadow: inset 0 0 0 1px var(--wui-gray-glass-002); - cursor: pointer; - transition: background 0.2s linear; - } - - .token-select-button:hover { - background: var(--wui-gray-glass-005); - } - - .token-select-button wui-image { - width: 24px; - height: 24px; - border-radius: var(--wui-border-radius-s); - box-shadow: inset 0 0 0 1px var(--wui-gray-glass-010); - } - - .max-value-button { - background-color: transparent; - border: none; - cursor: pointer; - color: var(--wui-gray-glass-020); - padding-left: 0px; - } - - .market-value { - min-height: 18px; - } -` diff --git a/packages/scaffold/src/partials/w3m-header/index.ts b/packages/scaffold/src/partials/w3m-header/index.ts index d3ddef2108..51a1662d3e 100644 --- a/packages/scaffold/src/partials/w3m-header/index.ts +++ b/packages/scaffold/src/partials/w3m-header/index.ts @@ -53,9 +53,9 @@ function headings() { OnRampFiatSelect: 'Select Currency', WalletReceive: 'Receive', WalletCompatibleNetworks: 'Compatible Networks', - Convert: 'Convert', - ConvertSelectToken: 'Select token', - ConvertPreview: 'Preview convert', + Swap: 'Swap', + SwapSelectToken: 'Select token', + SwapPreview: 'Preview swap', WalletSend: 'Send', WalletSendPreview: 'Review send', WalletSendSelectToken: 'Select Token' diff --git a/packages/scaffold/src/partials/w3m-onramp-input/index.ts b/packages/scaffold/src/partials/w3m-onramp-input/index.ts new file mode 100644 index 0000000000..56ab348e15 --- /dev/null +++ b/packages/scaffold/src/partials/w3m-onramp-input/index.ts @@ -0,0 +1,113 @@ +import { html, LitElement } from 'lit' +import { property, state } from 'lit/decorators.js' +import { ifDefined } from 'lit/directives/if-defined.js' +import { customElement } from '@web3modal/ui' +import styles from './styles.js' +import { + AssetController, + ModalController, + OnRampController, + type PaymentCurrency, + type PurchaseCurrency +} from '@web3modal/core' + +type Currency = { + name: string + symbol: string +} + +@customElement('w3m-onramp-input') +export class W3mInputCurrency extends LitElement { + public static override styles = styles + + // -- Members ------------------------------------------- // + private unsubscribe: (() => void)[] = [] + + // -- State & Properties -------------------------------- // + @property({ type: String }) public type: 'Token' | 'Fiat' = 'Token' + @property({ type: Number }) public value = 0 + @state() public currencies: Currency[] | null = [] + @state() public selectedCurrency = this.currencies?.[0] + + // -- Private ------------------------------------------- // + @state() private currencyImages = AssetController.state.currencyImages + @state() private tokenImages = AssetController.state.tokenImages + + public constructor() { + super() + this.unsubscribe.push( + OnRampController.subscribeKey('purchaseCurrency', val => { + if (!val || this.type === 'Fiat') { + return + } + this.selectedCurrency = this.formatPurchaseCurrency(val) + }), + OnRampController.subscribeKey('paymentCurrency', val => { + if (!val || this.type === 'Token') { + return + } + this.selectedCurrency = this.formatPaymentCurrency(val) + }), + OnRampController.subscribe(val => { + if (this.type === 'Fiat') { + this.currencies = val.purchaseCurrencies.map(this.formatPurchaseCurrency) + } else { + this.currencies = val.paymentCurrencies.map(this.formatPaymentCurrency) + } + }), + AssetController.subscribe(val => { + this.currencyImages = { ...val.currencyImages } + this.tokenImages = { ...val.tokenImages } + }) + ) + } + + // -- Lifecycle ----------------------------------------- // + public override firstUpdated() { + OnRampController.getAvailableCurrencies() + } + + public override disconnectedCallback() { + this.unsubscribe.forEach(unsubscribe => unsubscribe()) + } + + // -- Render -------------------------------------------- // + public override render() { + const symbol = this.selectedCurrency?.symbol || '' + const image = this.currencyImages[symbol] || this.tokenImages[symbol] + + return html` + ${this.selectedCurrency + ? html` ModalController.open({ view: `OnRamp${this.type}Select` })} + > + + ${this.selectedCurrency.symbol} + ` + : html``} + ` + } + + private formatPaymentCurrency(currency: PaymentCurrency) { + return { + name: currency.id, + symbol: currency.id + } + } + private formatPurchaseCurrency(currency: PurchaseCurrency) { + return { + name: currency.name, + symbol: currency.symbol + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'w3m-onramp-input': W3mInputCurrency + } +} diff --git a/packages/scaffold/src/partials/w3m-onramp-input/styles.ts b/packages/scaffold/src/partials/w3m-onramp-input/styles.ts new file mode 100644 index 0000000000..2c82ea0f5b --- /dev/null +++ b/packages/scaffold/src/partials/w3m-onramp-input/styles.ts @@ -0,0 +1,35 @@ +import { css } from 'lit' + +export default css` + :host { + width: 100%; + } + + wui-loading-spinner { + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); + } + + .currency-container { + position: absolute; + top: 50%; + transform: translateY(-50%); + right: var(--wui-spacing-1xs); + height: 40px; + padding: var(--wui-spacing-xs) var(--wui-spacing-1xs) var(--wui-spacing-xs) + var(--wui-spacing-xs); + min-width: 95px; + border-radius: var(--FULL, 1000px); + border: 1px solid var(--wui-gray-glass-002); + background: var(--wui-gray-glass-002); + cursor: pointer; + } + + .currency-container > wui-image { + height: 24px; + width: 24px; + border-radius: 50%; + } +` diff --git a/packages/scaffold/src/partials/w3m-convert-details/index.ts b/packages/scaffold/src/partials/w3m-swap-details/index.ts similarity index 95% rename from packages/scaffold/src/partials/w3m-convert-details/index.ts rename to packages/scaffold/src/partials/w3m-swap-details/index.ts index b78bf3edd9..16f144aa91 100644 --- a/packages/scaffold/src/partials/w3m-convert-details/index.ts +++ b/packages/scaffold/src/partials/w3m-swap-details/index.ts @@ -3,8 +3,8 @@ import { property } from 'lit/decorators.js' import styles from './styles.js' import { UiHelperUtil, customElement } from '@web3modal/ui' -@customElement('w3m-convert-details') -export class WuiConvertDetails extends LitElement { +@customElement('w3m-swap-details') +export class WuiSwapDetails extends LitElement { public static override styles = [styles] // -- State & Properties -------------------------------- // @@ -16,7 +16,7 @@ export class WuiConvertDetails extends LitElement { @property() public toTokenSymbol?: string - @property() public toTokenConvertedAmount?: number + @property() public toTokenSwapedAmount?: number @property() public gasPriceInUSD?: number @@ -36,7 +36,7 @@ export class WuiConvertDetails extends LitElement { 1 ${this.sourceTokenSymbol} = - ${UiHelperUtil.formatNumberToLocalString(this.toTokenConvertedAmount, 3)} + ${UiHelperUtil.formatNumberToLocalString(this.toTokenSwapedAmount, 3)} ${this.toTokenSymbol} @@ -122,6 +122,6 @@ export class WuiConvertDetails extends LitElement { declare global { interface HTMLElementTagNameMap { - 'wui-w3m-details': WuiConvertDetails + 'wui-w3m-details': WuiSwapDetails } } diff --git a/packages/scaffold/src/partials/w3m-convert-details/styles.ts b/packages/scaffold/src/partials/w3m-swap-details/styles.ts similarity index 100% rename from packages/scaffold/src/partials/w3m-convert-details/styles.ts rename to packages/scaffold/src/partials/w3m-swap-details/styles.ts diff --git a/packages/scaffold/src/partials/w3m-swap-input/index.ts b/packages/scaffold/src/partials/w3m-swap-input/index.ts index 5509895190..ea2b1752bb 100644 --- a/packages/scaffold/src/partials/w3m-swap-input/index.ts +++ b/packages/scaffold/src/partials/w3m-swap-input/index.ts @@ -1,113 +1,221 @@ import { html, LitElement } from 'lit' -import { property, state } from 'lit/decorators.js' -import { ifDefined } from 'lit/directives/if-defined.js' -import { customElement } from '@web3modal/ui' -import styles from './styles.js' +import { property } from 'lit/decorators.js' import { - AssetController, - ModalController, - OnRampController, - type PaymentCurrency, - type PurchaseCurrency + EventsController, + RouterController, + type SwapToken, + type SwapInputTarget } from '@web3modal/core' +import { NumberUtil } from '@web3modal/common' +import { + UiHelperUtil, + customElement, + swapInputMaskBottomSvg, + swapInputMaskTopSvg +} from '@web3modal/ui' +import styles from './styles.js' -type Currency = { - name: string - symbol: string -} +const MINIMUM_USD_VALUE_TO_CONVERT = 0.00005 @customElement('w3m-swap-input') -export class W3mInputCurrency extends LitElement { - public static override styles = styles - - // -- Members ------------------------------------------- // - private unsubscribe: (() => void)[] = [] +export class W3mSwapInput extends LitElement { + public static override styles = [styles] // -- State & Properties -------------------------------- // - @property({ type: String }) public type: 'Token' | 'Fiat' = 'Token' - @property({ type: Number }) public value = 0 - @state() public currencies: Currency[] | null = [] - @state() public selectedCurrency = this.currencies?.[0] + @property() public focused = false - // -- Private ------------------------------------------- // - @state() private currencyImages = AssetController.state.currencyImages - @state() private tokenImages = AssetController.state.tokenImages - - public constructor() { - super() - this.unsubscribe.push( - OnRampController.subscribeKey('purchaseCurrency', val => { - if (!val || this.type === 'Fiat') { - return - } - this.selectedCurrency = this.formatPurchaseCurrency(val) - }), - OnRampController.subscribeKey('paymentCurrency', val => { - if (!val || this.type === 'Token') { - return - } - this.selectedCurrency = this.formatPaymentCurrency(val) - }), - OnRampController.subscribe(val => { - if (this.type === 'Fiat') { - this.currencies = val.purchaseCurrencies.map(this.formatPurchaseCurrency) - } else { - this.currencies = val.paymentCurrencies.map(this.formatPaymentCurrency) - } - }), - AssetController.subscribe(val => { - this.currencyImages = { ...val.currencyImages } - this.tokenImages = { ...val.tokenImages } - }) - ) + @property() public balance: string | undefined + + @property() public value?: string + + @property() public price = 0 + + @property() public marketValue?: string = '$1.0345,00' + + @property() public disabled?: boolean + + @property() public target: SwapInputTarget = 'sourceToken' + + @property() public token?: SwapToken + + @property() public onSetAmount: ((target: SwapInputTarget, value: string) => void) | null = null + + @property() public onSetMaxValue: + | ((target: SwapInputTarget, balance: string | undefined) => void) + | null = null + + // -- Render -------------------------------------------- // + public override render() { + const marketValue = this.marketValue || '0' + const isMarketValueGreaterThanZero = NumberUtil.bigNumber(marketValue).isGreaterThan(0) + + return html` + + ${this.target === 'sourceToken' ? swapInputMaskTopSvg : swapInputMaskBottomSvg} + + this.onFocusChange(true)} + @focusout=${() => this.onFocusChange(false)} + ?disabled=${this.disabled} + .value=${this.value} + @input=${this.dispatchInputChangeEvent} + @keydown=${this.handleKeydown} + placeholder="0" + /> + + ${isMarketValueGreaterThanZero ? `$${this.marketValue}` : null} + + + ${this.templateTokenSelectButton()} + + ` } - // -- Lifecycle ----------------------------------------- // - public override firstUpdated() { - OnRampController.getAvailableCurrencies() + // -- Private ------------------------------------------- // + private handleKeydown(event: KeyboardEvent) { + const allowedKeys = [ + 'Backspace', + 'Meta', + 'Ctrl', + 'a', + 'c', + 'v', + 'ArrowLeft', + 'ArrowRight', + 'Tab' + ] + const isComma = event.key === ',' + const isDot = event.key === '.' + const isNumericKey = event.key >= '0' && event.key <= '9' + const currentValue = this.value + + if (!isNumericKey && !allowedKeys.includes(event.key) && !isDot && !isComma) { + event.preventDefault() + } + + if (isComma || isDot) { + if (currentValue?.includes('.') || currentValue?.includes(',')) { + event.preventDefault() + } + } } - public override disconnectedCallback() { - this.unsubscribe.forEach(unsubscribe => unsubscribe()) + private dispatchInputChangeEvent(event: InputEvent) { + if (!this.onSetAmount) { + return + } + + const value = (event.target as HTMLInputElement).value + if (value === ',' || value === '.') { + this.onSetAmount(this.target, '0.') + } else if (value.endsWith(',')) { + this.onSetAmount(this.target, value.replace(',', '.')) + } else { + this.onSetAmount(this.target, value) + } } - // -- Render -------------------------------------------- // - public override render() { - const symbol = this.selectedCurrency?.symbol || '' - const image = this.currencyImages[symbol] || this.tokenImages[symbol] - - return html` - ${this.selectedCurrency - ? html` ModalController.open({ view: `OnRamp${this.type}Select` })} - > - - ${this.selectedCurrency.symbol} - ` - : html``} - ` + private setMaxValueToInput() { + this.onSetMaxValue?.(this.target, this.balance) } - private formatPaymentCurrency(currency: PaymentCurrency) { - return { - name: currency.id, - symbol: currency.id + private templateTokenSelectButton() { + if (!this.token) { + return html` + Select token + ` } + + const tokenElement = this.token.logoUri + ? html`` + : html` + + ` + + return html` + + + ${this.tokenBalanceTemplate()} + + ` + } + + private tokenBalanceTemplate() { + const balanceValueInUSD = NumberUtil.multiply(this.balance, this.price) + const haveBalance = balanceValueInUSD + ? balanceValueInUSD?.isGreaterThan(MINIMUM_USD_VALUE_TO_CONVERT) + : false + + return html` + ${haveBalance + ? html` + ${UiHelperUtil.formatNumberToLocalString(this.balance, 3)} + ` + : null} + ${this.target === 'sourceToken' ? this.tokenActionButtonTemplate(haveBalance) : null} + ` } - private formatPurchaseCurrency(currency: PurchaseCurrency) { - return { - name: currency.name, - symbol: currency.symbol + + private tokenActionButtonTemplate(haveBalance: boolean) { + if (haveBalance) { + return html` ` } + + return html` ` + } + + private onFocusChange(state: boolean) { + this.focused = state + } + + private onSelectToken() { + EventsController.sendEvent({ type: 'track', event: 'CLICK_SELECT_TOKEN_TO_SWAP' }) + RouterController.push('SwapSelectToken', { + target: this.target + }) + } + + private onBuyToken() { + RouterController.push('OnRampProviders') } } declare global { interface HTMLElementTagNameMap { - 'w3m-swap-input': W3mInputCurrency + 'w3m-swap-input': W3mSwapInput } } diff --git a/packages/scaffold/src/partials/w3m-swap-input/styles.ts b/packages/scaffold/src/partials/w3m-swap-input/styles.ts index 2c82ea0f5b..43a4782e4a 100644 --- a/packages/scaffold/src/partials/w3m-swap-input/styles.ts +++ b/packages/scaffold/src/partials/w3m-swap-input/styles.ts @@ -1,35 +1,131 @@ import { css } from 'lit' export default css` - :host { + :host > wui-flex { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-radius: var(--wui-border-radius-s); + padding: var(--wui-spacing-xl); + padding-right: var(--wui-spacing-s); width: 100%; + height: 100px; + box-sizing: border-box; + position: relative; } - wui-loading-spinner { + :host > wui-flex > svg.input_mask { position: absolute; - top: 50%; - right: 20px; - transform: translateY(-50%); + inset: 0; + z-index: 5; } - .currency-container { - position: absolute; - top: 50%; - transform: translateY(-50%); - right: var(--wui-spacing-1xs); + :host wui-flex .input_mask__border, + :host wui-flex .input_mask__background { + transition: fill var(--wui-duration-md) var(--wui-ease-out-power-1); + will-change: fill; + } + + :host wui-flex .input_mask__border { + fill: var(--wui-gray-glass-005); + } + + :host wui-flex .input_mask__background { + fill: var(--wui-gray-glass-002); + } + + :host wui-flex.focus .input_mask__border { + fill: var(--wui-gray-glass-020); + } + + :host > wui-flex .swap-input, + :host > wui-flex .swap-token-button { + z-index: 10; + } + + :host > wui-flex .swap-input { + -webkit-mask-image: linear-gradient( + 270deg, + transparent 0px, + transparent 8px, + black 24px, + black 25px, + black 32px, + black 100% + ); + mask-image: linear-gradient( + 270deg, + transparent 0px, + transparent 8px, + black 24px, + black 25px, + black 32px, + black 100% + ); + } + + :host > wui-flex .swap-input input { + background: none; + border: none; + height: 42px; + width: 100%; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 130%; + letter-spacing: -1.28px; + outline: none; + caret-color: var(--wui-color-accent-100); + color: var(--wui-color-fg-100); + } + + :host > wui-flex .swap-input input:focus-visible { + outline: none; + } + + :host > wui-flex .swap-input input::-webkit-outer-spin-button, + :host > wui-flex .swap-input input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .token-select-button { + display: flex; + align-items: center; + justify-content: center; + gap: var(--wui-spacing-xxs); + padding: var(--wui-spacing-xs); + padding-right: var(--wui-spacing-1xs); height: 40px; - padding: var(--wui-spacing-xs) var(--wui-spacing-1xs) var(--wui-spacing-xs) - var(--wui-spacing-xs); - min-width: 95px; - border-radius: var(--FULL, 1000px); - border: 1px solid var(--wui-gray-glass-002); + border: none; + border-radius: 80px; background: var(--wui-gray-glass-002); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-002); cursor: pointer; + transition: background 0.2s linear; } - .currency-container > wui-image { - height: 24px; + .token-select-button:hover { + background: var(--wui-gray-glass-005); + } + + .token-select-button wui-image { width: 24px; - border-radius: 50%; + height: 24px; + border-radius: var(--wui-border-radius-s); + box-shadow: inset 0 0 0 1px var(--wui-gray-glass-010); + } + + .max-value-button { + background-color: transparent; + border: none; + cursor: pointer; + color: var(--wui-gray-glass-020); + padding-left: 0px; + } + + .market-value { + min-height: 18px; } ` diff --git a/packages/scaffold/src/views/w3m-convert-preview-view/index.ts b/packages/scaffold/src/views/w3m-swap-preview-view/index.ts similarity index 80% rename from packages/scaffold/src/views/w3m-convert-preview-view/index.ts rename to packages/scaffold/src/views/w3m-swap-preview-view/index.ts index f332c00861..4959e850c2 100644 --- a/packages/scaffold/src/views/w3m-convert-preview-view/index.ts +++ b/packages/scaffold/src/views/w3m-swap-preview-view/index.ts @@ -5,13 +5,13 @@ import { AccountController, NetworkController, RouterController, - ConvertController, + SwapController, ConstantsUtil } from '@web3modal/core' import { state } from 'lit/decorators.js' -@customElement('w3m-convert-preview-view') -export class W3mConvertPreviewView extends LitElement { +@customElement('w3m-swap-preview-view') +export class W3mSwapPreviewView extends LitElement { public static override styles = styles private unsubscribe: ((() => void) | undefined)[] = [] @@ -19,33 +19,33 @@ export class W3mConvertPreviewView extends LitElement { // -- State & Properties -------------------------------- // @state() private detailsOpen = true - @state() private approvalTransaction = ConvertController.state.approvalTransaction + @state() private approvalTransaction = SwapController.state.approvalTransaction - @state() private convertTransaction = ConvertController.state.convertTransaction + @state() private swapTransaction = SwapController.state.swapTransaction - @state() private sourceToken = ConvertController.state.sourceToken + @state() private sourceToken = SwapController.state.sourceToken - @state() private sourceTokenAmount = ConvertController.state.sourceTokenAmount ?? '' + @state() private sourceTokenAmount = SwapController.state.sourceTokenAmount ?? '' - @state() private sourceTokenPriceInUSD = ConvertController.state.sourceTokenPriceInUSD + @state() private sourceTokenPriceInUSD = SwapController.state.sourceTokenPriceInUSD - @state() private toToken = ConvertController.state.toToken + @state() private toToken = SwapController.state.toToken - @state() private toTokenAmount = ConvertController.state.toTokenAmount ?? '' + @state() private toTokenAmount = SwapController.state.toTokenAmount ?? '' - @state() private toTokenPriceInUSD = ConvertController.state.toTokenPriceInUSD + @state() private toTokenPriceInUSD = SwapController.state.toTokenPriceInUSD @state() private caipNetwork = NetworkController.state.caipNetwork - @state() private transactionLoading = ConvertController.state.transactionLoading + @state() private transactionLoading = SwapController.state.transactionLoading @state() private balanceSymbol = AccountController.state.balanceSymbol - @state() private gasPriceInUSD = ConvertController.state.gasPriceInUSD + @state() private gasPriceInUSD = SwapController.state.gasPriceInUSD - @state() private priceImpact = ConvertController.state.priceImpact + @state() private priceImpact = SwapController.state.priceImpact - @state() private maxSlippage = ConvertController.state.maxSlippage + @state() private maxSlippage = SwapController.state.maxSlippage // -- Lifecycle ----------------------------------------- // public constructor() { @@ -64,9 +64,9 @@ export class W3mConvertPreviewView extends LitElement { this.caipNetwork = newCaipNetwork } }), - ConvertController.subscribe(newState => { + SwapController.subscribe(newState => { this.approvalTransaction = newState.approvalTransaction - this.convertTransaction = newState.convertTransaction + this.swapTransaction = newState.swapTransaction this.sourceToken = newState.sourceToken this.gasPriceInUSD = newState.gasPriceInUSD this.toToken = newState.toToken @@ -166,7 +166,7 @@ export class W3mConvertPreviewView extends LitElement { Cancel