From 4830c46be753e50abd3926d6890444cea17f505c Mon Sep 17 00:00:00 2001 From: Felipe Mendes Date: Tue, 10 Sep 2024 19:49:08 -0300 Subject: [PATCH] feat: solana send transaction (#2802) --- packages/base/adapters/evm/ethers/client.ts | 8 ++ packages/base/adapters/evm/ethers5/client.ts | 4 + packages/base/adapters/evm/wagmi/client.ts | 8 ++ .../base/adapters/solana/web3js/client.ts | 78 +++++++++++++++---- .../tests/createSendTransaction.test.ts | 47 +++++++++++ .../solana/web3js/tests/mocks/Connection.ts | 13 ++++ .../web3js/utils/createSendTransaction.ts | 57 ++++++++++++++ .../src/controllers/ConnectionController.ts | 2 +- .../core/src/controllers/NetworkController.ts | 7 ++ .../core/src/controllers/SendController.ts | 44 +++++++++++ .../core/src/controllers/SwapController.ts | 42 +++++++--- packages/core/src/utils/ConstantsUtil.ts | 7 +- packages/core/src/utils/CoreHelperUtil.ts | 27 +++++-- packages/core/src/utils/SwapApiUtil.ts | 31 ++++++-- packages/core/src/utils/TypeUtil.ts | 34 ++++---- .../controllers/NetworkController.test.ts | 24 +++++- .../core/tests/utils/CoreHelperUtil.test.ts | 14 ++++ .../src/partials/w3m-input-token/index.ts | 20 +++-- .../src/views/w3m-swap-view/index.ts | 4 +- .../src/views/w3m-wallet-send-view/index.ts | 16 +++- 20 files changed, 421 insertions(+), 66 deletions(-) create mode 100644 packages/base/adapters/solana/web3js/tests/createSendTransaction.test.ts create mode 100644 packages/base/adapters/solana/web3js/tests/mocks/Connection.ts create mode 100644 packages/base/adapters/solana/web3js/utils/createSendTransaction.ts diff --git a/packages/base/adapters/evm/ethers/client.ts b/packages/base/adapters/evm/ethers/client.ts index 5c106d11c4..c6648a1eed 100644 --- a/packages/base/adapters/evm/ethers/client.ts +++ b/packages/base/adapters/evm/ethers/client.ts @@ -393,6 +393,10 @@ export class EVMEthersClient implements ChainAdapter { const { chainId, provider, address } = EthersStoreUtil.state + if (data.chainNamespace && data.chainNamespace !== 'eip155') { + throw new Error('ethersClient:sendTransaction - invalid chain namespace') + } + if (!provider) { throw new Error('ethersClient:sendTransaction - provider is undefined') } diff --git a/packages/base/adapters/evm/ethers5/client.ts b/packages/base/adapters/evm/ethers5/client.ts index 327a4bea3e..1de7053480 100644 --- a/packages/base/adapters/evm/ethers5/client.ts +++ b/packages/base/adapters/evm/ethers5/client.ts @@ -369,6 +369,10 @@ export class EVMEthers5Client implements ChainAdapter { + if (args.chainNamespace && args.chainNamespace !== 'eip155') { + throw new Error('connectionControllerClient:estimateGas - invalid chain namespace') + } + try { return await wagmiEstimateGas(this.wagmiConfig, { account: args.address, @@ -292,6 +296,10 @@ export class EVMWagmiClient implements ChainAdapter { }, sendTransaction: async (data: SendTransactionArgs) => { + if (data.chainNamespace && data.chainNamespace !== 'eip155') { + throw new Error('connectionControllerClient:sendTransaction - invalid chain namespace') + } + const { chainId } = getAccount(this.wagmiConfig) const txParams = { diff --git a/packages/base/adapters/solana/web3js/client.ts b/packages/base/adapters/solana/web3js/client.ts index 1b5c45ae93..6295307d30 100644 --- a/packages/base/adapters/solana/web3js/client.ts +++ b/packages/base/adapters/solana/web3js/client.ts @@ -47,6 +47,7 @@ import type { AppKit } from '../../../src/client.js' import type { AppKitOptions } from '../../../utils/TypesUtil.js' import type { OptionsControllerState } from '@web3modal/core' import { SafeLocalStorage } from '../../../utils/SafeLocalStorage.js' +import { createSendTransaction } from './utils/createSendTransaction.js' export interface Web3ModalClientOptions extends Omit { @@ -183,7 +184,30 @@ export class SolanaWeb3JsClient implements ChainAdapter await Promise.resolve(BigInt(0)), + estimateGas: async params => { + if (params.chainNamespace !== 'solana') { + throw new Error('Chain namespace is not supported') + } + + const connection = SolStoreUtil.state.connection + + if (!connection) { + throw new Error('Connection is not set') + } + + const provider = this.getProvider() + + const transaction = await createSendTransaction({ + provider, + connection, + to: '11111111111111111111111111111111', + value: 1 + }) + + const fee = await transaction.getEstimatedFee(connection) + + return BigInt(fee || 0) + }, // -- Transaction methods --------------------------------------------------- /** * @@ -196,7 +220,44 @@ export class SolanaWeb3JsClient implements ChainAdapter await Promise.resolve('0x'), - sendTransaction: async () => await Promise.resolve('0x'), + sendTransaction: async params => { + if (params.chainNamespace !== 'solana') { + throw new Error('Chain namespace is not supported') + } + + const connection = SolStoreUtil.state.connection + const address = SolStoreUtil.state.address + + if (!connection || !address) { + throw new Error('Connection is not set') + } + + const provider = this.getProvider() + + const transaction = await createSendTransaction({ + provider, + connection, + to: params.to, + value: params.value + }) + + const result = await provider.sendTransaction(transaction, connection) + + await new Promise(resolve => { + const interval = setInterval(async () => { + const status = await connection.getSignatureStatus(result) + + if (status?.value) { + clearInterval(interval) + resolve() + } + }, 1000) + }) + + await this.syncBalance(address) + + return result + }, parseUnits: () => BigInt(0), @@ -493,18 +554,7 @@ export class SolanaWeb3JsClient implements ChainAdapter { + return { + publicKey: new PublicKey('2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP') + } as unknown as Provider +} + +describe('createSendTransaction', () => { + let provider = mockProvider() + let connection = mockConnection() + + beforeEach(() => { + provider = mockProvider() + connection = mockConnection() + }) + + it('should create a transaction', async () => { + const transaction = await createSendTransaction({ + provider, + connection, + to: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP', + value: 10 + }) + + expect(transaction).toBeDefined() + }) + + it('should create a correct serialized transaction', async () => { + const transaction = await createSendTransaction({ + provider, + connection, + to: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgoP', + value: 10 + }) + + // Serializing to base64 only for comparison of the transaction bytes + const serialized = transaction.serialize({ verifySignatures: false }).toString('base64') + expect(serialized).toBe( + 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIDFj6WhBP/eepC4T4bDgYuJMiSVXNh9IvPWv1ZDUV52gYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAyZpToWInFL+zDFy34fwit57sURE//y+sa4B0X3QA16UDAgAJAwAtMQEAAAAAAgAFAvQBAAABAgAADAIAAAAA5AtUAgAAAA==' + ) + }) +}) diff --git a/packages/base/adapters/solana/web3js/tests/mocks/Connection.ts b/packages/base/adapters/solana/web3js/tests/mocks/Connection.ts new file mode 100644 index 0000000000..41850f9161 --- /dev/null +++ b/packages/base/adapters/solana/web3js/tests/mocks/Connection.ts @@ -0,0 +1,13 @@ +import { Connection } from '@solana/web3.js' +import { vi } from 'vitest' + +export function mockConnection() { + const connection = new Connection('https://mocked.api.connection') + + return Object.assign(connection, { + getLatestBlockhash: vi.fn().mockResolvedValue({ + blockhash: 'EZySCpmzXRuUtM95P2JGv9SitqYph6Nv6HaYBK7a8PKJ', + lastValidBlockHeight: 1 + }) + }) +} diff --git a/packages/base/adapters/solana/web3js/utils/createSendTransaction.ts b/packages/base/adapters/solana/web3js/utils/createSendTransaction.ts new file mode 100644 index 0000000000..cba16459a6 --- /dev/null +++ b/packages/base/adapters/solana/web3js/utils/createSendTransaction.ts @@ -0,0 +1,57 @@ +import { + PublicKey, + SystemProgram, + type Connection, + Transaction, + LAMPORTS_PER_SOL, + ComputeBudgetProgram +} from '@solana/web3.js' +import type { Provider } from '@web3modal/scaffold-utils/solana' + +type SendTransactionArgs = { + provider: Provider + connection: Connection + to: string + value: number +} + +/** + * These constants defines the cost of running the program, allowing to calculate the maximum + * amount of SOL that can be sent in case of cleaning the account and remove the rent exemption error. + */ +const COMPUTE_BUDGET_CONSTANTS = { + UNIT_PRICE_MICRO_LAMPORTS: 20000000, + UNIT_LIMIT: 500 +} + +export async function createSendTransaction({ + provider, + to, + value, + connection +}: SendTransactionArgs): Promise { + if (!provider.publicKey) { + throw Error('No public key found') + } + + const toPubkey = new PublicKey(to) + const lamports = Math.floor(value * LAMPORTS_PER_SOL) + + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + + const instructions = [ + ComputeBudgetProgram.setComputeUnitPrice({ + microLamports: COMPUTE_BUDGET_CONSTANTS.UNIT_PRICE_MICRO_LAMPORTS + }), + ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_BUDGET_CONSTANTS.UNIT_LIMIT }), + SystemProgram.transfer({ + fromPubkey: provider.publicKey, + toPubkey, + lamports + }) + ] + + return new Transaction({ feePayer: provider.publicKey, blockhash, lastValidBlockHeight }).add( + ...instructions + ) +} diff --git a/packages/core/src/controllers/ConnectionController.ts b/packages/core/src/controllers/ConnectionController.ts index a58952057a..a560a5a744 100644 --- a/packages/core/src/controllers/ConnectionController.ts +++ b/packages/core/src/controllers/ConnectionController.ts @@ -30,7 +30,7 @@ export interface ConnectionControllerClient { connectWalletConnect: (onUri: (uri: string) => void) => Promise disconnect: () => Promise signMessage: (message: string) => Promise - sendTransaction: (args: SendTransactionArgs) => Promise<`0x${string}` | null> + sendTransaction: (args: SendTransactionArgs) => Promise estimateGas: (args: EstimateGasTransactionArgs) => Promise parseUnits: (value: string, decimals: number) => bigint formatUnits: (value: bigint, decimals: number) => string diff --git a/packages/core/src/controllers/NetworkController.ts b/packages/core/src/controllers/NetworkController.ts index d86447b555..aa75f6873c 100644 --- a/packages/core/src/controllers/NetworkController.ts +++ b/packages/core/src/controllers/NetworkController.ts @@ -6,6 +6,7 @@ import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' import { NetworkUtil, type Chain } from '@web3modal/common' import { ChainController } from './ChainController.js' import { PublicStateController } from './PublicStateController.js' +import { ConstantsUtil } from '../utils/ConstantsUtil.js' // -- Types --------------------------------------------- // export interface NetworkControllerClient { @@ -312,5 +313,11 @@ export const NetworkController = { setTimeout(() => { ModalController.open({ view: 'UnsupportedChain' }) }, 300) + }, + + getActiveNetworkTokenAddress() { + const address = ConstantsUtil.NATIVE_TOKEN_ADDRESS[this.state.caipNetwork?.chain || 'evm'] + + return `${this.state.caipNetwork?.id || 'eip155:1'}:${address}` } } diff --git a/packages/core/src/controllers/SendController.ts b/packages/core/src/controllers/SendController.ts index 7eb6e0eaf1..657f08dfcc 100644 --- a/packages/core/src/controllers/SendController.ts +++ b/packages/core/src/controllers/SendController.ts @@ -10,6 +10,7 @@ import { CoreHelperUtil } from '../utils/CoreHelperUtil.js' import { EventsController } from './EventsController.js' import { NetworkController } from './NetworkController.js' import { W3mFrameRpcConstants } from '@web3modal/wallet' +import { ChainController } from './ChainController.js' // -- Types --------------------------------------------- // @@ -93,6 +94,21 @@ export const SendController = { }, sendToken() { + switch (ChainController.state.activeCaipNetwork?.chain) { + case 'evm': + this.sendEvmToken() + + return + case 'solana': + this.sendSolanaToken() + + return + default: + throw new Error('Unsupported chain') + } + }, + + sendEvmToken() { if (this.state.token?.address && this.state.sendTokenAmount && this.state.receiverAddress) { EventsController.sendEvent({ type: 'track', @@ -228,6 +244,34 @@ export const SendController = { } }, + sendSolanaToken() { + if (!this.state.sendTokenAmount || !this.state.receiverAddress) { + SnackController.showError('Please enter a valid amount and receiver address') + + return + } + + RouterController.pushTransactionStack({ + view: 'Account', + goBack: false + }) + + ConnectionController.sendTransaction({ + chainNamespace: 'solana', + to: this.state.receiverAddress, + value: this.state.sendTokenAmount + }) + .then(() => { + this.resetSend() + AccountController.fetchTokenBalance() + }) + .catch(error => { + SnackController.showError('Failed to send transaction. Please try again.') + // eslint-disable-next-line no-console + console.error('SendController:sendToken - failed to send solana transaction', error) + }) + }, + resetSend() { state.token = undefined state.sendTokenAmount = undefined diff --git a/packages/core/src/controllers/SwapController.ts b/packages/core/src/controllers/SwapController.ts index 262194cee9..6b66616da8 100644 --- a/packages/core/src/controllers/SwapController.ts +++ b/packages/core/src/controllers/SwapController.ts @@ -172,9 +172,8 @@ export const SwapController = { }, getParams() { - const caipNetwork = NetworkController.state.caipNetwork const address = AccountController.state.address - const networkAddress = `${caipNetwork?.id}:${ConstantsUtil.NATIVE_TOKEN_ADDRESS}` + const networkAddress = NetworkController.getActiveNetworkTokenAddress() const type = StorageUtil.getConnectedConnector() const authConnector = ConnectorController.getAuthConnector() @@ -404,6 +403,10 @@ export const SwapController = { const response = await BlockchainApiController.fetchTokenPrice({ projectId: OptionsController.state.projectId, addresses: [networkAddress] + }).catch(() => { + SnackController.showError('Failed to fetch network token price') + + return { fungibles: [] } }) const token = response.fungibles?.[0] const price = token?.price.toString() || '0' @@ -446,18 +449,37 @@ export const SwapController = { const res = await SwapApiUtil.fetchGasPrice() if (!res) { - return { gasPrice: null, gasPriceInUsd: null } + return { gasPrice: null, gasPriceInUSD: null } } - const value = res.standard - const gasFee = BigInt(value) - const gasLimit = BigInt(INITIAL_GAS_LIMIT) - const gasPrice = SwapCalculationUtil.getGasPriceInUSD(state.networkPrice, gasLimit, gasFee) + switch (NetworkController.state.caipNetwork?.chain) { + case 'solana': + state.gasFee = res.standard + state.gasPriceInUSD = NumberUtil.multiply(res.standard, state.networkPrice) + .dividedBy(1e9) + .toNumber() - state.gasFee = value - state.gasPriceInUSD = gasPrice + return { + gasPrice: BigInt(state.gasFee), + gasPriceInUSD: Number(state.gasPriceInUSD) + } - return { gasPrice: gasFee, gasPriceInUSD: state.gasPriceInUSD } + case 'evm': + default: + // eslint-disable-next-line no-case-declarations + const value = res.standard + // eslint-disable-next-line no-case-declarations + const gasFee = BigInt(value) + // eslint-disable-next-line no-case-declarations + const gasLimit = BigInt(INITIAL_GAS_LIMIT) + // eslint-disable-next-line no-case-declarations + const gasPrice = SwapCalculationUtil.getGasPriceInUSD(state.networkPrice, gasLimit, gasFee) + + state.gasFee = value + state.gasPriceInUSD = gasPrice + + return { gasPrice: gasFee, gasPriceInUSD: gasPrice } + } }, // -- Swap -------------------------------------- // diff --git a/packages/core/src/utils/ConstantsUtil.ts b/packages/core/src/utils/ConstantsUtil.ts index 5c38d9971e..e6f2ebdcd4 100644 --- a/packages/core/src/utils/ConstantsUtil.ts +++ b/packages/core/src/utils/ConstantsUtil.ts @@ -1,3 +1,5 @@ +import type { Chain } from '@web3modal/common' + const SECURE_SITE = 'https://secure.walletconnect.org' export const ONRAMP_PROVIDERS = [ @@ -181,7 +183,10 @@ export const ConstantsUtil = { 'eip155:1313161554' ], - NATIVE_TOKEN_ADDRESS: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + NATIVE_TOKEN_ADDRESS: { + evm: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + solana: 'So11111111111111111111111111111111111111111' + } as const satisfies Record, CONVERT_SLIPPAGE_TOLERANCE: 1 } diff --git a/packages/core/src/utils/CoreHelperUtil.ts b/packages/core/src/utils/CoreHelperUtil.ts index 0cab40eb85..10ab8f66b9 100644 --- a/packages/core/src/utils/CoreHelperUtil.ts +++ b/packages/core/src/utils/CoreHelperUtil.ts @@ -1,4 +1,4 @@ -import type { Balance } from '@web3modal/common' +import type { Balance, Chain } from '@web3modal/common' import { ConstantsUtil as CommonConstants } from '@web3modal/common' import { ConstantsUtil } from './ConstantsUtil.js' import type { CaipAddress, LinkingRecord, CaipNetwork } from './TypeUtil.js' @@ -260,14 +260,25 @@ export const CoreHelperUtil = { return { dollars, pennies } }, - isAddress(address: string): boolean { - if (!/^(?:0x)?[0-9a-f]{40}$/iu.test(address)) { - return false - } else if (/^(?:0x)?[0-9a-f]{40}$/iu.test(address) || /^(?:0x)?[0-9A-F]{40}$/iu.test(address)) { - return true - } + isAddress(address: string, chain: Chain = 'evm'): boolean { + switch (chain) { + case 'evm': + if (!/^(?:0x)?[0-9a-f]{40}$/iu.test(address)) { + return false + } else if ( + /^(?:0x)?[0-9a-f]{40}$/iu.test(address) || + /^(?:0x)?[0-9A-F]{40}$/iu.test(address) + ) { + return true + } - return false + return false + case 'solana': + return /[1-9A-HJ-NP-Za-km-z]{32,44}$/iu.test(address) + + default: + return false + } }, uniqueBy(arr: T[], key: keyof T) { diff --git a/packages/core/src/utils/SwapApiUtil.ts b/packages/core/src/utils/SwapApiUtil.ts index 0b7d6c15f0..ff8c645d19 100644 --- a/packages/core/src/utils/SwapApiUtil.ts +++ b/packages/core/src/utils/SwapApiUtil.ts @@ -1,5 +1,4 @@ import { ConnectionController } from '../controllers/ConnectionController.js' -import { ConstantsUtil } from './ConstantsUtil.js' import { BlockchainApiController } from '../controllers/BlockchainApiController.js' import type { SwapTokenWithBalance } from './TypeUtil.js' import { OptionsController } from '../controllers/OptionsController.js' @@ -55,10 +54,30 @@ export const SwapApiUtil = { return null } - return await BlockchainApiController.fetchGasPrice({ - projectId, - chainId: caipNetwork.id - }) + try { + switch (caipNetwork.chain) { + case 'solana': + // eslint-disable-next-line no-case-declarations + const lamportsPerSignature = ( + await ConnectionController.estimateGas({ chainNamespace: 'solana' }) + ).toString() + + return { + standard: lamportsPerSignature, + fast: lamportsPerSignature, + instant: lamportsPerSignature + } + + case 'evm': + default: + return await BlockchainApiController.fetchGasPrice({ + projectId, + chainId: caipNetwork.id + }) + } + } catch { + return null + } }, async fetchSwapAllowance({ @@ -113,7 +132,7 @@ export const SwapApiUtil = { ...token, address: token?.address ? token.address - : `${token.chainId}:${ConstantsUtil.NATIVE_TOKEN_ADDRESS}`, + : NetworkController.getActiveNetworkTokenAddress(), decimals: parseInt(token.quantity.decimals, 10), logoUri: token.iconUrl, eip2612: false diff --git a/packages/core/src/utils/TypeUtil.ts b/packages/core/src/utils/TypeUtil.ts index 46b6df2257..2e8a928d89 100644 --- a/packages/core/src/utils/TypeUtil.ts +++ b/packages/core/src/utils/TypeUtil.ts @@ -801,20 +801,28 @@ export type AccountType = { type: 'eoa' | 'smartAccount' } -export interface SendTransactionArgs { - to: `0x${string}` - data: `0x${string}` - value: bigint - gas?: bigint - gasPrice: bigint - address: `0x${string}` -} +export type SendTransactionArgs = + | { + chainNamespace?: undefined | 'eip155' + to: `0x${string}` + data: `0x${string}` + value: bigint + gas?: bigint + gasPrice: bigint + address: `0x${string}` + } + | { chainNamespace: 'solana'; to: string; value: number } -export interface EstimateGasTransactionArgs { - address: `0x${string}` - to: `0x${string}` - data: `0x${string}` -} +export type EstimateGasTransactionArgs = + | { + chainNamespace?: undefined | 'eip155' + address: `0x${string}` + to: `0x${string}` + data: `0x${string}` + } + | { + chainNamespace: 'solana' + } export interface WriteContractArgs { receiverAddress: `0x${string}` diff --git a/packages/core/tests/controllers/NetworkController.test.ts b/packages/core/tests/controllers/NetworkController.test.ts index bb5d1827c6..02f23ba189 100644 --- a/packages/core/tests/controllers/NetworkController.test.ts +++ b/packages/core/tests/controllers/NetworkController.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import type { CaipNetwork, CaipNetworkId, NetworkControllerClient } from '../../index.js' import { ChainController, EventsController, NetworkController } from '../../index.js' import { ConstantsUtil } from '@web3modal/common' @@ -102,4 +102,26 @@ describe('NetworkController', () => { }) expect(NetworkController.checkIfSmartAccountEnabled()).toEqual(true) }) + + it('should get correct active network token address', () => { + let mock = vi.spyOn(NetworkController.state, 'caipNetwork', 'get').mockReturnValue(undefined) + expect(NetworkController.getActiveNetworkTokenAddress()).toEqual( + 'eip155:1:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ) + + mock.mockReturnValue(caipNetwork) + expect(NetworkController.getActiveNetworkTokenAddress()).toEqual( + 'eip155:1:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' + ) + + mock.mockReturnValue({ + chain: 'solana', + id: 'solana:mainnet' + }) + expect(NetworkController.getActiveNetworkTokenAddress()).toEqual( + 'solana:mainnet:So11111111111111111111111111111111111111111' + ) + + mock.mockClear() + }) }) diff --git a/packages/core/tests/utils/CoreHelperUtil.test.ts b/packages/core/tests/utils/CoreHelperUtil.test.ts index f86a6726e6..1766850c7d 100644 --- a/packages/core/tests/utils/CoreHelperUtil.test.ts +++ b/packages/core/tests/utils/CoreHelperUtil.test.ts @@ -14,4 +14,18 @@ describe('CoreHelperUtil', () => { expect(CoreHelperUtil.formatBalance('', 'USD')).toBe('0.000 USD') expect(CoreHelperUtil.formatBalance('0', 'ETH')).toBe('0.000 ETH') }) + + it.each([ + { address: '0x0', chain: undefined, expected: false }, + { address: '0x0', chain: 'evm', expected: false }, + { address: '0xb3F068DCc2f92ED42E0417d4f2C2191f743fBfdA', chain: undefined, expected: true }, + { address: '0xb3F068DCc2f92ED42E0417d4f2C2191f743fBfdA', chain: 'evm', expected: true }, + { address: '0xb3F068DCc2f92ED42E0417d4f2C2191f743fBfdA', chain: 'solana', expected: false }, + { address: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgpU', chain: 'solana', expected: true } + ] as const)( + 'should return $expected validating $address when chain is $chain', + ({ address, chain, expected }) => { + expect(CoreHelperUtil.isAddress(address, chain)).toBe(expected) + } + ) }) diff --git a/packages/scaffold-ui/src/partials/w3m-input-token/index.ts b/packages/scaffold-ui/src/partials/w3m-input-token/index.ts index 545e8c2f45..32ee2e13d8 100644 --- a/packages/scaffold-ui/src/partials/w3m-input-token/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-input-token/index.ts @@ -2,7 +2,7 @@ import { UiHelperUtil, customElement } from '@web3modal/ui' import { LitElement, html } from 'lit' import styles from './styles.js' import { property } from 'lit/decorators.js' -import { RouterController, SendController } from '@web3modal/core' +import { ConstantsUtil, RouterController, SendController } from '@web3modal/core' import type { Balance } from '@web3modal/common' import { NumberUtil } from '@web3modal/common' @@ -17,6 +17,8 @@ export class W3mInputToken extends LitElement { @property({ type: Number }) public gasPriceInUSD?: number + @property({ type: Number }) public gasPrice?: number + // -- Render -------------------------------------------- // public override render() { return html` this.token?.address === nativeAddress + ) - const isNetworkToken = this.token.address === undefined + const numericGas = NumberUtil.bigNumber(this.gasPrice).shiftedBy( + -this.token.quantity.decimals + ) const maxValue = isNetworkToken - ? NumberUtil.bigNumber(this.token.quantity.numeric).minus(amountOfTokenGasRequires) + ? NumberUtil.bigNumber(this.token.quantity.numeric).minus(numericGas) : NumberUtil.bigNumber(this.token.quantity.numeric) SendController.setTokenAmount(Number(maxValue.toFixed(20))) diff --git a/packages/scaffold-ui/src/views/w3m-swap-view/index.ts b/packages/scaffold-ui/src/views/w3m-swap-view/index.ts index eeb0452c10..aafe820d89 100644 --- a/packages/scaffold-ui/src/views/w3m-swap-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-swap-view/index.ts @@ -8,7 +8,6 @@ import { CoreHelperUtil, NetworkController, ModalController, - ConstantsUtil, type SwapToken, type SwapInputTarget, EventsController, @@ -207,8 +206,7 @@ export class W3mSwapView extends LitElement { private onSetMaxValue(target: SwapInputTarget, balance: string | undefined) { const token = target === 'sourceToken' ? this.sourceToken : this.toToken - const isNetworkToken = token?.address === ConstantsUtil.NATIVE_TOKEN_ADDRESS - + const isNetworkToken = token?.address === NetworkController.getActiveNetworkTokenAddress() let value = '0' if (!balance) { diff --git a/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts b/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts index 6d3f5159af..b0d7f4b6b5 100644 --- a/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts +++ b/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts @@ -1,7 +1,13 @@ import { customElement } from '@web3modal/ui' import { LitElement, html } from 'lit' import styles from './styles.js' -import { SwapController, CoreHelperUtil, RouterController, SendController } from '@web3modal/core' +import { + SwapController, + CoreHelperUtil, + RouterController, + SendController, + ChainController +} from '@web3modal/core' import { state } from 'lit/decorators.js' @customElement('w3m-wallet-send-view') @@ -24,6 +30,8 @@ export class W3mWalletSendView extends LitElement { @state() private gasPriceInUSD = SendController.state.gasPriceInUSD + @state() private gasPrice = SendController.state.gasPrice + @state() private message: | 'Preview Send' | 'Select Token' @@ -64,6 +72,7 @@ export class W3mWalletSendView extends LitElement { .token=${this.token} .sendTokenAmount=${this.sendTokenAmount} .gasPriceInUSD=${this.gasPriceInUSD} + .gasPrice=${this.gasPrice} >