UNPKG

@reown/appkit-scaffold-ui

Version:

The full stack toolkit to build onchain app UX.

436 lines • 17.4 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { LitElement, html } from 'lit'; import { property, state } from 'lit/decorators.js'; import { NumberUtil } from '@reown/appkit-common'; import { ChainController, CoreHelperUtil, EventsController, ModalController, RouterController, SwapController, getPreferredAccountType } from '@reown/appkit-controllers'; import { customElement } from '@reown/appkit-ui'; import '@reown/appkit-ui/wui-button'; import '@reown/appkit-ui/wui-flex'; import '@reown/appkit-ui/wui-icon'; import '@reown/appkit-ui/wui-icon-box'; import '@reown/appkit-ui/wui-text'; import { W3mFrameRpcConstants } from '@reown/appkit-wallet/utils'; import '../../partials/w3m-swap-details/index.js'; import '../../partials/w3m-swap-input-skeleton/index.js'; import '../../partials/w3m-swap-input/index.js'; import styles from './styles.js'; let W3mSwapView = class W3mSwapView extends LitElement { subscribe({ resetSwapState, initializeSwapState }) { return () => { ChainController.subscribeKey('activeCaipNetwork', newCaipNetwork => this.onCaipNetworkChange({ newCaipNetwork, resetSwapState, initializeSwapState })); ChainController.subscribeChainProp('accountState', val => { this.onCaipAddressChange({ newCaipAddress: val?.caipAddress, resetSwapState, initializeSwapState }); }); }; } constructor() { super(); this.unsubscribe = []; this.initialParams = RouterController.state.data?.swap; this.detailsOpen = false; this.caipAddress = ChainController.getAccountData()?.caipAddress; this.caipNetworkId = ChainController.state.activeCaipNetwork?.caipNetworkId; this.initialized = SwapController.state.initialized; this.loadingQuote = SwapController.state.loadingQuote; this.loadingPrices = SwapController.state.loadingPrices; this.loadingTransaction = SwapController.state.loadingTransaction; this.sourceToken = SwapController.state.sourceToken; this.sourceTokenAmount = SwapController.state.sourceTokenAmount; this.sourceTokenPriceInUSD = SwapController.state.sourceTokenPriceInUSD; this.toToken = SwapController.state.toToken; this.toTokenAmount = SwapController.state.toTokenAmount; this.toTokenPriceInUSD = SwapController.state.toTokenPriceInUSD; this.inputError = SwapController.state.inputError; this.fetchError = SwapController.state.fetchError; this.lastTokenPriceUpdate = 0; this.minTokenPriceUpdateInterval = 10_000; this.visibilityChangeHandler = () => { if (document?.hidden) { clearInterval(this.interval); this.interval = undefined; } else { this.startTokenPriceInterval(); } }; this.startTokenPriceInterval = () => { if (this.interval && Date.now() - this.lastTokenPriceUpdate < this.minTokenPriceUpdateInterval) { return; } if (this.lastTokenPriceUpdate && Date.now() - this.lastTokenPriceUpdate > this.minTokenPriceUpdateInterval) { this.fetchTokensAndValues(); } clearInterval(this.interval); this.interval = setInterval(() => { this.fetchTokensAndValues(); }, this.minTokenPriceUpdateInterval); }; this.watchTokensAndValues = () => { if (!this.sourceToken || !this.toToken) { return; } this.subscribeToVisibilityChange(); this.startTokenPriceInterval(); }; this.onDebouncedGetSwapCalldata = CoreHelperUtil.debounce(async () => { await SwapController.swapTokens(); }, 200); this.subscribe({ resetSwapState: true, initializeSwapState: false })(); this.unsubscribe.push(...[ this.subscribe({ resetSwapState: false, initializeSwapState: true }), ModalController.subscribeKey('open', isOpen => { if (!isOpen) { SwapController.resetState(); } }), RouterController.subscribeKey('view', newRoute => { if (!newRoute.includes('Swap')) { SwapController.resetValues(); } }), SwapController.subscribe(newState => { this.initialized = newState.initialized; this.loadingQuote = newState.loadingQuote; this.loadingPrices = newState.loadingPrices; this.loadingTransaction = newState.loadingTransaction; this.sourceToken = newState.sourceToken; this.sourceTokenAmount = newState.sourceTokenAmount; this.sourceTokenPriceInUSD = newState.sourceTokenPriceInUSD; this.toToken = newState.toToken; this.toTokenAmount = newState.toTokenAmount; this.toTokenPriceInUSD = newState.toTokenPriceInUSD; this.inputError = newState.inputError; this.fetchError = newState.fetchError; if (newState.sourceToken && newState.toToken) { this.watchTokensAndValues(); } }) ]); } async firstUpdated() { SwapController.initializeState(); this.watchTokensAndValues(); await this.handleSwapParameters(); } disconnectedCallback() { this.unsubscribe.forEach(unsubscribe => unsubscribe?.()); clearInterval(this.interval); document?.removeEventListener('visibilitychange', this.visibilityChangeHandler); } render() { return html ` <wui-flex flexDirection="column" .padding=${['0', '4', '4', '4']} gap="3"> ${this.initialized ? this.templateSwap() : this.templateLoading()} </wui-flex> `; } subscribeToVisibilityChange() { document?.removeEventListener('visibilitychange', this.visibilityChangeHandler); document?.addEventListener('visibilitychange', this.visibilityChangeHandler); } fetchTokensAndValues() { SwapController.getNetworkTokenPrice(); SwapController.getMyTokensWithBalance(); SwapController.swapTokens(); this.lastTokenPriceUpdate = Date.now(); } templateSwap() { return html ` <wui-flex flexDirection="column" gap="3"> <wui-flex flexDirection="column" alignItems="center" gap="2" class="swap-inputs-container"> ${this.templateTokenInput('sourceToken', this.sourceToken)} ${this.templateTokenInput('toToken', this.toToken)} ${this.templateReplaceTokensButton()} </wui-flex> ${this.templateDetails()} ${this.templateActionButton()} </wui-flex> `; } actionButtonLabel() { const haveNoAmount = !this.sourceTokenAmount || this.sourceTokenAmount === '0'; if (this.fetchError) { return 'Swap'; } if (!this.sourceToken || !this.toToken) { return 'Select token'; } if (haveNoAmount) { return 'Enter amount'; } if (this.inputError) { return this.inputError; } return 'Review swap'; } templateReplaceTokensButton() { return html ` <wui-flex class="replace-tokens-button-container"> <wui-icon-box @click=${this.onSwitchTokens.bind(this)} icon="recycleHorizontal" size="md" variant="default" ></wui-icon-box> </wui-flex> `; } templateLoading() { return html ` <wui-flex flexDirection="column" gap="4"> <wui-flex flexDirection="column" alignItems="center" gap="2" class="swap-inputs-container"> <w3m-swap-input-skeleton target="sourceToken"></w3m-swap-input-skeleton> <w3m-swap-input-skeleton target="toToken"></w3m-swap-input-skeleton> ${this.templateReplaceTokensButton()} </wui-flex> ${this.templateActionButton()} </wui-flex> `; } templateTokenInput(target, token) { const myToken = SwapController.state.myTokensWithBalance?.find(ct => ct?.address === token?.address); const amount = target === 'toToken' ? this.toTokenAmount : this.sourceTokenAmount; const price = target === 'toToken' ? this.toTokenPriceInUSD : this.sourceTokenPriceInUSD; const marketValue = NumberUtil.parseLocalStringToNumber(amount) * price; return html `<w3m-swap-input .value=${target === 'toToken' ? this.toTokenAmount : this.sourceTokenAmount} .disabled=${target === 'toToken'} .onSetAmount=${this.handleChangeAmount.bind(this)} target=${target} .token=${token} .balance=${myToken?.quantity?.numeric} .price=${myToken?.price} .marketValue=${marketValue} .onSetMaxValue=${this.onSetMaxValue.bind(this)} ?autoFocus=${target === 'sourceToken'} ></w3m-swap-input>`; } onSetMaxValue(target, balance) { const maxValue = NumberUtil.bigNumber(balance || '0'); if (!maxValue.gt(0)) { this.handleChangeAmount(target, '0'); return; } const token = target === 'sourceToken' ? this.sourceToken : this.toToken; const decimals = token?.decimals ?? 18; const { networkAddress } = SwapController.getParams(); const gasFee = SwapController.state.gasFee; if (target === 'sourceToken' && token?.address === networkAddress && gasFee && gasFee !== '0') { const SWAP_GAS_LIMIT = 150000n; const gasCostWei = SWAP_GAS_LIMIT * BigInt(gasFee); const gasCost = NumberUtil.bigNumber(gasCostWei.toString()).div(NumberUtil.bigNumber(10).pow(decimals)); const maxAfterGas = maxValue.minus(gasCost); this.handleChangeAmount(target, maxAfterGas.gt(0) ? maxAfterGas.toFixed(decimals, 0) : '0'); } else { this.handleChangeAmount(target, maxValue.toFixed(decimals, 0)); } } templateDetails() { if (!this.sourceToken || !this.toToken || this.inputError) { return null; } return html `<w3m-swap-details .detailsOpen=${this.detailsOpen}></w3m-swap-details>`; } handleChangeAmount(target, value) { SwapController.clearError(); if (target === 'sourceToken') { SwapController.setSourceTokenAmount(value); } else { SwapController.setToTokenAmount(value); } this.onDebouncedGetSwapCalldata(); } templateActionButton() { const haveNoTokenSelected = !this.toToken || !this.sourceToken; const haveNoAmount = !this.sourceTokenAmount || this.sourceTokenAmount === '0'; const loading = this.loadingQuote || this.loadingPrices || this.loadingTransaction; const disabled = loading || haveNoTokenSelected || haveNoAmount || this.inputError; return html ` <wui-flex gap="2"> <wui-button data-testid="swap-action-button" class="action-button" fullWidth size="lg" borderRadius="xs" variant="accent-primary" ?loading=${Boolean(loading)} ?disabled=${Boolean(disabled)} @click=${this.onSwapPreview.bind(this)} > ${this.actionButtonLabel()} </wui-button> </wui-flex>`; } async onSwitchTokens() { await SwapController.switchTokens(); } async onSwapPreview() { if (this.fetchError) { await SwapController.swapTokens(); } EventsController.sendEvent({ type: 'track', event: 'INITIATE_SWAP', properties: { network: this.caipNetworkId || '', swapFromToken: this.sourceToken?.symbol || '', swapToToken: this.toToken?.symbol || '', swapFromAmount: this.sourceTokenAmount || '', swapToAmount: this.toTokenAmount || '', isSmartAccount: getPreferredAccountType(ChainController.state.activeChain) === W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT } }); RouterController.push('SwapPreview'); } async handleSwapParameters() { if (!this.initialParams) { return; } if (!SwapController.state.initialized) { const waitForInitialization = new Promise(resolve => { const unsubscribe = SwapController.subscribeKey('initialized', initialized => { if (initialized) { unsubscribe?.(); resolve(); } }); }); await waitForInitialization; } await this.setSwapParameters(this.initialParams); } async setSwapParameters({ amount, fromToken, toToken }) { if (!SwapController.state.tokens || !SwapController.state.myTokensWithBalance) { const waitForTokens = new Promise(resolve => { const unsubscribe = SwapController.subscribeKey('myTokensWithBalance', tokens => { if (tokens && tokens.length > 0) { unsubscribe?.(); resolve(); } }); setTimeout(() => { unsubscribe?.(); resolve(); }, 5000); }); await waitForTokens; } const allTokens = [ ...(SwapController.state.tokens || []), ...(SwapController.state.myTokensWithBalance || []) ]; if (fromToken) { const token = allTokens.find(t => t.symbol.toLowerCase() === fromToken.toLowerCase()); if (token) { SwapController.setSourceToken(token); } } if (toToken) { const token = allTokens.find(t => t.symbol.toLowerCase() === toToken.toLowerCase()); if (token) { SwapController.setToToken(token); } } if (amount && !isNaN(Number(amount))) { SwapController.setSourceTokenAmount(amount); } } onCaipAddressChange({ newCaipAddress, resetSwapState, initializeSwapState }) { if (this.caipAddress !== newCaipAddress) { this.caipAddress = newCaipAddress; if (resetSwapState) { SwapController.resetState(); } if (initializeSwapState) { SwapController.initializeState(); } } } onCaipNetworkChange({ newCaipNetwork, resetSwapState, initializeSwapState }) { if (this.caipNetworkId !== newCaipNetwork?.caipNetworkId) { this.caipNetworkId = newCaipNetwork?.caipNetworkId; if (resetSwapState) { SwapController.resetState(); } if (initializeSwapState) { SwapController.initializeState(); } } } }; W3mSwapView.styles = styles; __decorate([ property({ type: Object }) ], W3mSwapView.prototype, "initialParams", void 0); __decorate([ state() ], W3mSwapView.prototype, "interval", void 0); __decorate([ state() ], W3mSwapView.prototype, "detailsOpen", void 0); __decorate([ state() ], W3mSwapView.prototype, "caipAddress", void 0); __decorate([ state() ], W3mSwapView.prototype, "caipNetworkId", void 0); __decorate([ state() ], W3mSwapView.prototype, "initialized", void 0); __decorate([ state() ], W3mSwapView.prototype, "loadingQuote", void 0); __decorate([ state() ], W3mSwapView.prototype, "loadingPrices", void 0); __decorate([ state() ], W3mSwapView.prototype, "loadingTransaction", void 0); __decorate([ state() ], W3mSwapView.prototype, "sourceToken", void 0); __decorate([ state() ], W3mSwapView.prototype, "sourceTokenAmount", void 0); __decorate([ state() ], W3mSwapView.prototype, "sourceTokenPriceInUSD", void 0); __decorate([ state() ], W3mSwapView.prototype, "toToken", void 0); __decorate([ state() ], W3mSwapView.prototype, "toTokenAmount", void 0); __decorate([ state() ], W3mSwapView.prototype, "toTokenPriceInUSD", void 0); __decorate([ state() ], W3mSwapView.prototype, "inputError", void 0); __decorate([ state() ], W3mSwapView.prototype, "fetchError", void 0); __decorate([ state() ], W3mSwapView.prototype, "lastTokenPriceUpdate", void 0); W3mSwapView = __decorate([ customElement('w3m-swap-view') ], W3mSwapView); export { W3mSwapView }; //# sourceMappingURL=index.js.map