diff --git a/packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts b/packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts index d3773480..05a57a1f 100644 --- a/packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts +++ b/packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts @@ -12,7 +12,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { when } from 'lit/directives/when.js'; import { networkIconsMap } from '../../assets'; -import { DEFAULT_ETH_DECIMALS } from '../../constants'; +import { DEFAULT_ETH_DECIMALS, INPUT_DEBOUNCE_TIME } from '../../constants'; import { BALANCE_UPDATE_KEY, TokenBalanceController @@ -20,6 +20,7 @@ import { import { tokenBalanceToNumber } from '../../utils/token'; import type { DropdownOption } from '../common/dropdown/dropdown'; import { BaseComponent } from '../common/base-component'; +import { debounce } from '../../utils'; import { styles } from './styles'; @customElement('sygma-resource-amount-selector') @@ -67,9 +68,7 @@ export class ResourceAmountSelector extends BaseComponent { } }; - _onInputAmountChangeHandler = (event: Event): void => { - let { value } = event.target as HTMLInputElement; - + _onInputAmountChangeHandler = (value: string): void => { if (value === '') { value = '0'; } @@ -88,6 +87,11 @@ export class ResourceAmountSelector extends BaseComponent { } }; + debouncedHandler = debounce( + this._onInputAmountChangeHandler, + INPUT_DEBOUNCE_TIME + ); + requestUpdate( name?: PropertyKey, oldValue?: unknown, @@ -200,7 +204,10 @@ export class ResourceAmountSelector extends BaseComponent { type="number" class="amountSelectorInput" placeholder="0.000" - @input=${this._onInputAmountChangeHandler} + @input=${(event: Event) => { + this.debouncedHandler((event.target as HTMLInputElement).value); + }} + .disabled=${this.disabled} .value=${this.amount} />
diff --git a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts index 6a9ecbe6..15687d67 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -5,7 +5,6 @@ import { html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import '../../../context/wallet'; import { choose } from 'lit/directives/choose.js'; -import { when } from 'lit/directives/when.js'; import type { Eip1193Provider } from 'packages/widget/src/interfaces'; import type { PropertyValues } from '@lit/reactive-element'; import { @@ -22,7 +21,6 @@ import '../../network-selector'; import { BaseComponent } from '../../common'; import { Directions } from '../../network-selector/network-selector'; import { WalletController } from '../../../controllers'; -import { tokenBalanceToNumber } from '../../../utils/token'; import { styles } from './styles'; @customElement('sygma-fungible-transfer') @@ -103,29 +101,6 @@ export class FungibleTokenTransfer extends BaseComponent { } }; - renderAmountOnDestination(): HTMLTemplateResult | null { - if ( - this.transferController.selectedResource && - this.transferController.pendingTransferTransaction !== undefined - ) { - const { decimals, symbol } = this.transferController.selectedResource; - return html` -
- Amount to receive: - - ${tokenBalanceToNumber( - this.transferController.resourceAmount, - decimals!, - 4 - )} - ${symbol} - -
- `; - } - return null; - } - renderTransferStatus(): HTMLTemplateResult { return html`
@@ -196,10 +172,8 @@ export class FungibleTokenTransfer extends BaseComponent {
- ${when(this.transferController.destinationAddress, () => - this.renderAmountOnDestination() - )} ; @@ -101,6 +104,20 @@ export class FungibleTransferDetail extends BaseComponent { render(): HTMLTemplateResult { return html`
+ ${when( + this.fee !== null, + () => + html`
+
Amount to receive:
+
+ ${tokenBalanceToNumber( + this.amountToReceive, + this.selectedResource?.decimals as number + )} + ${this.selectedResource?.symbol} +
+
` + )} ${when( this.fee !== null, () => diff --git a/packages/widget/src/constants.ts b/packages/widget/src/constants.ts index d5b85000..0bf316b9 100644 --- a/packages/widget/src/constants.ts +++ b/packages/widget/src/constants.ts @@ -22,3 +22,5 @@ export const SUBSTRATE_RPCS: { 2035: 'wss://phala.api.onfinality.io/public-ws' } }; + +export const INPUT_DEBOUNCE_TIME = 600; diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index f4150ce0..c4839957 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -1,89 +1,111 @@ import { EVMAssetTransfer, - FeeHandlerType, - type PercentageFee + FeeHandlerType +} from '@buildwithsygma/sygma-sdk-core'; +import type { + Domain, + Environment, + EvmFee, + PercentageFee } from '@buildwithsygma/sygma-sdk-core'; import { Web3Provider } from '@ethersproject/providers'; +import type { UnsignedTransaction, BigNumber } from 'ethers'; import { constants, utils } from 'ethers'; -import { type FungibleTokenTransferController } from '../fungible-token-transfer'; +import type { EvmWallet } from 'packages/widget/src/context'; + +type BuildEvmFungibleTransactionsArtifacts = { + pendingEvmApprovalTransactions: UnsignedTransaction[]; + pendingTransferTransaction: UnsignedTransaction; + fee: EvmFee; + resourceAmount: BigNumber; +}; /** * @dev If we did proper validation this shouldn't throw. * Not sure how to handle if it throws :shrug: */ -export async function buildEvmFungibleTransactions( - this: FungibleTokenTransferController -): Promise { - //we already check that but this prevents those typescript errors - const provider = this.walletContext.value?.evmWallet?.provider; - const providerChaiId = this.walletContext.value?.evmWallet?.providerChainId; - const address = this.walletContext.value?.evmWallet?.address; - if ( - !this.sourceNetwork || - !this.destinationNetwork || - !this.resourceAmount || - !this.selectedResource || - !this.destinationAddress || - !provider || - !address || - providerChaiId !== this.sourceNetwork.chainId - ) { - this.estimatedGas = undefined; - this.resetFee(); - return; - } - +export async function buildEvmFungibleTransactions({ + evmWallet: { address, provider, providerChainId }, + chainId, + destinationAddress, + resourceId, + resourceAmount, + env, + pendingEvmApprovalTransactions, + pendingTransferTransaction, + fee +}: { + evmWallet: EvmWallet; + chainId: number; + destinationAddress: string; + resourceId: string; + resourceAmount: BigNumber; + env: Environment; + pendingEvmApprovalTransactions: UnsignedTransaction[]; + pendingTransferTransaction: UnsignedTransaction; + sourceNetwork: Domain | null; + fee: EvmFee; +}): Promise { const evmTransfer = new EVMAssetTransfer(); - await evmTransfer.init(new Web3Provider(provider, providerChaiId), this.env); + await evmTransfer.init(new Web3Provider(provider, providerChainId), env); // Hack to make fungible transfer behave like it does on substrate side // where fee is deducted from user inputted amount rather than added on top const originalTransfer = await evmTransfer.createFungibleTransfer( address, - this.destinationNetwork.chainId, - this.destinationAddress, - this.selectedResource.resourceId, - this.resourceAmount.toString() + chainId, + destinationAddress, + resourceId, + resourceAmount.toString() ); const originalFee = await evmTransfer.getFee(originalTransfer); + // NOTE: for percentage fee, if both are equal, it means we can calculate the amount with fee avoiding second subtraction + const calculateAmountWithFee = originalFee.type === FeeHandlerType.PERCENTAGE; + //in case of percentage fee handler, we are calculating what amount + fee will result int user inputed amount //in case of fixed(basic) fee handler, fee is taken from native token - if (originalFee.type === FeeHandlerType.PERCENTAGE) { + if (calculateAmountWithFee) { const { lowerBound, upperBound, percentage } = originalFee as PercentageFee; - const userInputAmount = this.resourceAmount; + const userInputAmount = resourceAmount; //calculate amount without fee (percentage) const feelessAmount = userInputAmount .mul(constants.WeiPerEther) .div(utils.parseEther(String(1 + percentage))); const calculatedFee = userInputAmount.sub(feelessAmount); - this.resourceAmount = feelessAmount; + resourceAmount = feelessAmount; //if calculated percentage fee is less than lower fee bound, substract lower bound from user input. If lower bound is 0, bound is ignored if (calculatedFee.lt(lowerBound) && lowerBound.gt(0)) { - this.resourceAmount = userInputAmount.sub(lowerBound); + resourceAmount = userInputAmount.sub(lowerBound); } //if calculated percentage fee is more than upper fee bound, substract upper bound from user input. If upper bound is 0, bound is ignored if (calculatedFee.gt(upperBound) && upperBound.gt(0)) { - this.resourceAmount = userInputAmount.sub(upperBound); + resourceAmount = userInputAmount.sub(upperBound); } } const transfer = await evmTransfer.createFungibleTransfer( address, - this.destinationNetwork.chainId, - this.destinationAddress, - this.selectedResource.resourceId, - this.resourceAmount.toString() + chainId, + destinationAddress, + resourceId, + resourceAmount.toString() ); - this.fee = await evmTransfer.getFee(transfer); - this.pendingEvmApprovalTransactions = await evmTransfer.buildApprovals( + fee = await evmTransfer.getFee(transfer); + + pendingEvmApprovalTransactions = await evmTransfer.buildApprovals( transfer, - this.fee + fee ); - this.pendingTransferTransaction = await evmTransfer.buildTransferTransaction( + + pendingTransferTransaction = await evmTransfer.buildTransferTransaction( transfer, - this.fee + fee ); - await this.estimateGas(); - this.host.requestUpdate(); + return { + pendingEvmApprovalTransactions, + pendingTransferTransaction, + fee, + resourceAmount + }; } diff --git a/packages/widget/src/controllers/transfers/evm/execute.ts b/packages/widget/src/controllers/transfers/evm/execute.ts index efc494d2..e5f2142e 100644 --- a/packages/widget/src/controllers/transfers/evm/execute.ts +++ b/packages/widget/src/controllers/transfers/evm/execute.ts @@ -2,6 +2,9 @@ import { Web3Provider, type TransactionRequest } from '@ethersproject/providers'; +import type { PopulatedTransaction } from 'ethers'; +import type { Eip1193Provider } from '../../../interfaces'; +import { estimateEvmTransactionsGasCost } from '../../../utils/gas'; import { FungibleTransferState, type FungibleTokenTransferController @@ -19,6 +22,7 @@ export async function executeNextEvmTransaction( if (this.getTransferState() === FungibleTransferState.PENDING_APPROVALS) { this.waitingUserConfirmation = true; this.host.requestUpdate(); + const transactions: PopulatedTransaction[] = []; try { const tx = await signer.sendTransaction( this.pendingEvmApprovalTransactions[0] as TransactionRequest @@ -28,6 +32,18 @@ export async function executeNextEvmTransaction( this.host.requestUpdate(); await tx.wait(); this.pendingEvmApprovalTransactions.shift(); + + transactions.push( + ...(this.pendingEvmApprovalTransactions as PopulatedTransaction[]), + this.pendingTransferTransaction as PopulatedTransaction + ); + + this.estimatedGas = await estimateEvmTransactionsGasCost( + this.sourceNetwork?.chainId as number, + this.walletContext.value?.evmWallet?.provider as Eip1193Provider, + this.walletContext.value?.evmWallet?.address as string, + transactions + ); } catch (e) { console.log(e); this.errorMessage = 'Approval transaction reverted or rejected'; @@ -35,7 +51,12 @@ export async function executeNextEvmTransaction( this.waitingUserConfirmation = false; this.waitingTxExecution = false; this.host.requestUpdate(); - await this.estimateGas(); + await estimateEvmTransactionsGasCost( + this.sourceNetwork?.chainId as number, + this.walletContext.value?.evmWallet?.provider as Eip1193Provider, + this.walletContext.value?.evmWallet?.address as string, + transactions + ); } return; } diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index bf1d44b1..64657104 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -13,8 +13,12 @@ import { Network } from '@buildwithsygma/sygma-sdk-core'; import { ContextConsumer } from '@lit/context'; -import { BigNumber, ethers } from 'ethers'; -import type { UnsignedTransaction, PopulatedTransaction } from 'ethers'; +import { ethers } from 'ethers'; +import type { + UnsignedTransaction, + BigNumber, + PopulatedTransaction +} from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { ApiPromise, SubmittableResult } from '@polkadot/api'; @@ -22,18 +26,22 @@ import type { ParachainID, SubstrateFee } from '@buildwithsygma/sygma-sdk-core/substrate'; -import type { WalletContext } from '../../context'; +import type { EvmWallet, WalletContext } from '../../context'; import { walletContext } from '../../context'; import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; import { validateAddress } from '../../utils'; +import type { Eip1193Provider } from '../../interfaces'; import { SdkInitializedEvent } from '../../interfaces'; import { substrateProviderContext } from '../../context/wallet'; +import { + estimateEvmTransactionsGasCost, + estimateSubstrateGas +} from '../../utils/gas'; import { buildEvmFungibleTransactions, executeNextEvmTransaction } from './evm'; import { buildSubstrateFungibleTransactions, executeNextSubstrateTransaction } from './substrate'; -import { estimateEvmTransactionsGasCost } from './evm/gas-estimate'; export type SubstrateTransaction = SubmittableExtrinsic< 'promise', @@ -76,7 +84,6 @@ export class FungibleTokenTransferController implements ReactiveController { public estimatedGas: BigNumber | undefined; //Evm transfer - protected buildEvmTransactions = buildEvmFungibleTransactions; protected executeNextEvmTransaction = executeNextEvmTransaction; protected pendingEvmApprovalTransactions: UnsignedTransaction[] = []; public pendingTransferTransaction?: @@ -96,6 +103,9 @@ export class FungibleTokenTransferController implements ReactiveController { protected whitelistedDestinationNetworks?: string[] = []; protected whitelistedSourceResources?: string[] = []; + public resourceAmountToDisplay = ethers.constants.Zero; + public isBuildingTransactions = false; + host: ReactiveElement; walletContext: ContextConsumer; substrateProviderContext: ContextConsumer< @@ -142,7 +152,7 @@ export class FungibleTokenTransferController implements ReactiveController { subscribe: true, callback: (context: Partial) => { try { - this.buildTransactions(); + void this.buildTransactions(); } catch (e) { console.error(e); } @@ -246,6 +256,7 @@ export class FungibleTokenTransferController implements ReactiveController { resetFee(): void { this.fee = null; + this.estimatedGas = undefined; } reset({ omitSourceNetworkReset } = { omitSourceNetworkReset: false }): void { @@ -260,7 +271,6 @@ export class FungibleTokenTransferController implements ReactiveController { this.waitingTxExecution = false; this.waitingUserConfirmation = false; this.transferTransactionId = undefined; - this.estimatedGas = undefined; this.resetFee(); void this.init(this.env); } @@ -304,10 +314,12 @@ export class FungibleTokenTransferController implements ReactiveController { }; onResourceSelected = (resource: Resource, amount: BigNumber): void => { - this.selectedResource = resource; - this.resourceAmount = amount; - void this.buildTransactions(); - this.host.requestUpdate(); + if (!this.isBuildingTransactions) { + this.selectedResource = resource; + this.resourceAmount = amount; + void this.buildTransactions(); + this.host.requestUpdate(); + } }; onDestinationAddressChange = (address: string): void => { @@ -394,7 +406,6 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.pendingTransferTransaction) { return FungibleTransferState.PENDING_TRANSFER; } - return FungibleTransferState.UNKNOWN; } @@ -494,83 +505,147 @@ export class FungibleTokenTransferController implements ReactiveController { this.host.requestUpdate(); }; - private buildTransactions(): void { + private canBuildTransactions(): boolean { if ( !this.sourceNetwork || !this.destinationNetwork || !this.resourceAmount || !this.selectedResource || - !this.destinationAddress + !this.destinationAddress || + this.isBuildingTransactions ) { - return; + return false; } + switch (this.sourceNetwork.type) { + case Network.EVM: { + if (!this.walletContext.value?.evmWallet) return false; + + const { address, provider, providerChainId } = + this.walletContext.value.evmWallet; + + return !!(address && provider && providerChainId); + } + case Network.SUBSTRATE: { + return !!( + this.sourceSubstrateProvider && + this.walletContext.value?.substrateWallet?.signerAddress + ); + } + } + } + + private async buildTransactions(): Promise { + const isAbleToBuildTransactions = this.canBuildTransactions(); + + if (!isAbleToBuildTransactions) { + this.estimatedGas = undefined; + this.resetFee(); + return; + } + + switch (this.sourceNetwork!.type) { case Network.EVM: { - void this.buildEvmTransactions(); + this.isBuildingTransactions = true; + + try { + const { + pendingEvmApprovalTransactions, + pendingTransferTransaction, + fee, + resourceAmount + } = await buildEvmFungibleTransactions({ + evmWallet: this.walletContext.value?.evmWallet as EvmWallet, + chainId: this.destinationNetwork!.chainId, + destinationAddress: this.destinationAddress!, + resourceId: this.selectedResource!.resourceId, + resourceAmount: this.resourceAmount, + env: this.env, + pendingEvmApprovalTransactions: + this.pendingEvmApprovalTransactions, + pendingTransferTransaction: this + .pendingTransferTransaction as UnsignedTransaction, + sourceNetwork: this.sourceNetwork!, + fee: this.fee as EvmFee + }); + + this.fee = fee; + this.pendingEvmApprovalTransactions = + pendingEvmApprovalTransactions; + this.pendingTransferTransaction = pendingTransferTransaction; + this.resourceAmountToDisplay = resourceAmount; + const state = this.getTransferState(); + const transactions = []; + + if (state === FungibleTransferState.PENDING_APPROVALS) { + transactions.push(this.pendingEvmApprovalTransactions[0]); + } else { + transactions.push(this.pendingTransferTransaction); + } + + this.estimatedGas = await estimateEvmTransactionsGasCost( + this.sourceNetwork?.chainId as number, + this.walletContext.value?.evmWallet?.provider as Eip1193Provider, + this.walletContext.value?.evmWallet?.address as string, + transactions as PopulatedTransaction[] + ); + } catch (error) { + console.error('Error Building transactions: ', error); + this.fee = null; + this.estimatedGas = undefined; + this.pendingEvmApprovalTransactions = []; + this.pendingTransferTransaction = undefined; + } finally { + this.isBuildingTransactions = false; + this.host.requestUpdate(); + } } break; case Network.SUBSTRATE: { - void this.buildSubstrateTransactions(); + this.isBuildingTransactions = true; + const substrateProvider = this.sourceSubstrateProvider!; + const address = + this.walletContext.value?.substrateWallet?.signerAddress; + + try { + const { pendingTransferTransaction, fee, resourceAmount } = + await this.buildSubstrateTransactions({ + address: address!, + substrateProvider, + env: this.env, + chainId: this.destinationNetwork!.chainId, + destinationAddress: this.destinationAddress!, + resourceId: this.selectedResource!.resourceId, + resourceAmount: this.resourceAmount, + pendingTransferTransaction: this + .pendingTransferTransaction as SubstrateTransaction, + fee: this.fee as SubstrateFee + }); + + this.fee = fee; + this.resourceAmountToDisplay = resourceAmount; + this.pendingTransferTransaction = pendingTransferTransaction; + + this.estimatedGas = await estimateSubstrateGas( + address as string, + this.pendingTransferTransaction + ); + } catch (error) { + console.error('Error Building transactions: ', error); + this.fee = null; + this.estimatedGas = undefined; + this.pendingEvmApprovalTransactions = []; + this.pendingTransferTransaction = undefined; + } finally { + this.isBuildingTransactions = false; + this.host.requestUpdate(); + } } break; default: throw new Error('Unsupported network type'); } } - - public async estimateGas(): Promise { - if (!this.sourceNetwork) return; - switch (this.sourceNetwork.type) { - case Network.EVM: - await this.estimateEvmGas(); - break; - case Network.SUBSTRATE: - await this.estimateSubstrateGas(); - break; - } - } - - private async estimateSubstrateGas(): Promise { - if (!this.walletContext.value?.substrateWallet?.signerAddress) return; - const sender = this.walletContext.value?.substrateWallet?.signerAddress; - - const paymentInfo = await ( - this.pendingTransferTransaction as SubstrateTransaction - ).paymentInfo(sender); - - const { partialFee } = paymentInfo; - this.estimatedGas = BigNumber.from(partialFee.toString()); - } - - private async estimateEvmGas(): Promise { - if ( - !this.sourceNetwork?.chainId || - !this.walletContext.value?.evmWallet?.provider || - !this.walletContext.value.evmWallet.address - ) - return; - - const state = this.getTransferState(); - const transactions = []; - - switch (state) { - case FungibleTransferState.PENDING_APPROVALS: - transactions.push(this.pendingEvmApprovalTransactions[0]); - break; - case FungibleTransferState.PENDING_TRANSFER: - transactions.push(this.pendingTransferTransaction); - break; - } - - const estimatedGas = await estimateEvmTransactionsGasCost( - this.sourceNetwork?.chainId, - this.walletContext.value?.evmWallet?.provider, - this.walletContext.value.evmWallet.address, - transactions as PopulatedTransaction[] - ); - - this.estimatedGas = estimatedGas; - } } diff --git a/packages/widget/src/controllers/transfers/substrate/build.ts b/packages/widget/src/controllers/transfers/substrate/build.ts index 38a36031..c7260825 100644 --- a/packages/widget/src/controllers/transfers/substrate/build.ts +++ b/packages/widget/src/controllers/transfers/substrate/build.ts @@ -1,44 +1,62 @@ +import type { Environment } from '@buildwithsygma/sygma-sdk-core'; +import type { SubstrateFee } from '@buildwithsygma/sygma-sdk-core/substrate'; import { SubstrateAssetTransfer } from '@buildwithsygma/sygma-sdk-core/substrate'; -import { type FungibleTokenTransferController } from '../fungible-token-transfer'; +import type { ApiPromise } from '@polkadot/api'; +import type { BigNumber } from 'ethers'; +import type { SubstrateTransaction } from '../fungible-token-transfer'; -export async function buildSubstrateFungibleTransactions( - this: FungibleTokenTransferController -): Promise { - const substrateProvider = this.sourceSubstrateProvider; - const address = this.walletContext.value?.substrateWallet?.signerAddress; - - if ( - !this.sourceNetwork || - !this.destinationNetwork || - !this.resourceAmount || - !this.selectedResource || - !this.destinationAddress || - !substrateProvider || - !address - ) { - this.estimatedGas = undefined; - this.resetFee(); - return; - } +type BuildSubstrateFungibleTransactionsArtifacts = { + pendingTransferTransaction: SubstrateTransaction; + resourceAmount: BigNumber; + fee: SubstrateFee; +}; +export async function buildSubstrateFungibleTransactions({ + address, + substrateProvider, + env, + chainId, + destinationAddress, + resourceId, + resourceAmount, + pendingTransferTransaction, + fee +}: { + address: string; + substrateProvider: ApiPromise; + env: Environment; + chainId: number; + destinationAddress: string; + resourceId: string; + resourceAmount: BigNumber; + pendingTransferTransaction: SubstrateTransaction; + fee: SubstrateFee; +}): Promise { const substrateTransfer = new SubstrateAssetTransfer(); - await substrateTransfer.init(substrateProvider, this.env); + await substrateTransfer.init(substrateProvider, env); const transfer = await substrateTransfer.createFungibleTransfer( address, - this.destinationNetwork.chainId, - this.destinationAddress, - this.selectedResource.resourceId, - String(this.resourceAmount) + chainId, + destinationAddress, + resourceId, + String(resourceAmount) ); - this.fee = await substrateTransfer.getFee(transfer); + fee = await substrateTransfer.getFee(transfer); - this.resourceAmount = this.resourceAmount.sub(this.fee.fee.toString()); - this.pendingTransferTransaction = substrateTransfer.buildTransferTransaction( + if (resourceAmount.toString() === transfer.details.amount.toString()) { + resourceAmount = resourceAmount.sub(fee.fee.toString()); + } + + pendingTransferTransaction = substrateTransfer.buildTransferTransaction( transfer, - this.fee + fee ); - await this.estimateGas(); - this.host.requestUpdate(); + + return { + pendingTransferTransaction, + resourceAmount, + fee + }; } diff --git a/packages/widget/src/controllers/transfers/evm/gas-estimate.ts b/packages/widget/src/utils/gas.ts similarity index 62% rename from packages/widget/src/controllers/transfers/evm/gas-estimate.ts rename to packages/widget/src/utils/gas.ts index 53625deb..30f7bdde 100644 --- a/packages/widget/src/controllers/transfers/evm/gas-estimate.ts +++ b/packages/widget/src/utils/gas.ts @@ -1,6 +1,8 @@ +import { BigNumber, ethers } from 'ethers'; +import type { PopulatedTransaction } from 'ethers'; import { Web3Provider } from '@ethersproject/providers'; -import type { EIP1193Provider } from '@web3-onboard/core'; -import { ethers, type BigNumber, type PopulatedTransaction } from 'ethers'; +import type { SubstrateTransaction } from '../controllers/transfers/fungible-token-transfer'; +import type { Eip1193Provider } from '../interfaces'; /** * This method calculate the amount of gas @@ -13,7 +15,7 @@ import { ethers, type BigNumber, type PopulatedTransaction } from 'ethers'; */ export async function estimateEvmTransactionsGasCost( chainId: number, - eip1193Provider: EIP1193Provider, + eip1193Provider: Eip1193Provider, sender: string, transactions: PopulatedTransaction[] ): Promise { @@ -29,3 +31,14 @@ export async function estimateEvmTransactionsGasCost( const gasPrice = await provider.getGasPrice(); return gasPrice.mul(cost); } + +export async function estimateSubstrateGas( + signerAddress: string, + pendingTransferTransaction: SubstrateTransaction +): Promise { + const { partialFee } = + await pendingTransferTransaction.paymentInfo(signerAddress); + const estimatedGas = BigNumber.from(partialFee.toString()); + + return estimatedGas; +} diff --git a/packages/widget/src/utils/index.ts b/packages/widget/src/utils/index.ts index 9b5768e3..edf8f697 100644 --- a/packages/widget/src/utils/index.ts +++ b/packages/widget/src/utils/index.ts @@ -84,3 +84,16 @@ export const validateAddress = ( return 'unsupported network'; } }; + +export const debounce = ( + cb: (args: T) => void, + delay: number +): ((value: T) => void) => { + let timeout: NodeJS.Timeout; + return (args: T): void => { + if (timeout !== undefined) { + clearTimeout(timeout); + } + timeout = setTimeout(() => cb(args), delay); + }; +}; diff --git a/packages/widget/tests/unit/components/resource-amount-selector/resource-amount-selector.test.ts b/packages/widget/tests/unit/components/resource-amount-selector/resource-amount-selector.test.ts index e100fe69..195d8126 100644 --- a/packages/widget/tests/unit/components/resource-amount-selector/resource-amount-selector.test.ts +++ b/packages/widget/tests/unit/components/resource-amount-selector/resource-amount-selector.test.ts @@ -1,9 +1,15 @@ import type { Resource } from '@buildwithsygma/sygma-sdk-core'; import { ResourceType } from '@buildwithsygma/sygma-sdk-core'; -import { fixture, fixtureCleanup, nextFrame } from '@open-wc/testing-helpers'; +import { + aTimeout, + fixture, + fixtureCleanup, + nextFrame +} from '@open-wc/testing-helpers'; import { utils } from 'ethers'; import { html } from 'lit'; import { afterEach, assert, describe, expect, it, vi } from 'vitest'; +import { INPUT_DEBOUNCE_TIME } from '../../../../src/constants'; import { ResourceAmountSelector } from '../../../../src/components/resource-amount-selector/resource-amount-selector'; import type { DropdownOption } from '../../../../src/components/common/dropdown/dropdown'; import { BALANCE_UPDATE_KEY } from '../../../../src/controllers/wallet-manager/token-balance'; @@ -152,6 +158,8 @@ describe('Resource amount selector component - sygma-resource-amount-selector', input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); await el.updateComplete; + await aTimeout(INPUT_DEBOUNCE_TIME); + expect(mockOptionSelectHandler).toHaveBeenCalledTimes(1); expect(mockOptionSelectHandler).toHaveBeenCalledWith( el.selectedResource, @@ -161,7 +169,7 @@ describe('Resource amount selector component - sygma-resource-amount-selector', describe('Validation', () => { it('validates input amount when balance is low', async () => { - const el = await fixture( + const el = await fixture( html` ` ); @@ -173,9 +181,12 @@ describe('Resource amount selector component - sygma-resource-amount-selector', input.dispatchEvent(new Event('input')); await nextFrame(); + await aTimeout(INPUT_DEBOUNCE_TIME); + const validationMessage = el.shadowRoot!.querySelector( '.validationMessage' ) as HTMLDivElement; + assert.strictEqual( validationMessage.textContent, 'Amount exceeds account balance' @@ -194,6 +205,8 @@ describe('Resource amount selector component - sygma-resource-amount-selector', input.dispatchEvent(new Event('input')); await nextFrame(); + await aTimeout(INPUT_DEBOUNCE_TIME); + const validationMessage = el.shadowRoot!.querySelector( '.validationMessage' ) as HTMLDivElement; @@ -221,6 +234,8 @@ describe('Resource amount selector component - sygma-resource-amount-selector', input.dispatchEvent(new Event('input')); await nextFrame(); + await aTimeout(INPUT_DEBOUNCE_TIME); + const validationMessage = el.shadowRoot!.querySelector( '.validationMessage' ) as HTMLDivElement; @@ -246,6 +261,7 @@ describe('Resource amount selector component - sygma-resource-amount-selector', ) as HTMLInputElement; input.value = '-2'; input.dispatchEvent(new Event('input')); + await aTimeout(INPUT_DEBOUNCE_TIME); await el.updateComplete; const validationMessage = el.shadowRoot!.querySelector( @@ -268,6 +284,7 @@ describe('Resource amount selector component - sygma-resource-amount-selector', ) as HTMLInputElement; input.value = 'nonParseableValue'; input.dispatchEvent(new Event('input')); + await aTimeout(INPUT_DEBOUNCE_TIME); await el.updateComplete; const validationMessage = el.shadowRoot!.querySelector( diff --git a/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts b/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts index 08fce4d2..b0459a2f 100644 --- a/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts +++ b/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts @@ -60,6 +60,7 @@ describe('sygma-fungible-transfer-detail', function () { .fee=${mockedFee} .selectedResource=${mockedResource} .sourceDomainConfig=${mockedSourceDomainConfig} + .amountToReceive=${constants.Zero} > `); @@ -79,11 +80,12 @@ describe('sygma-fungible-transfer-detail', function () { .fee=${mockedFee} .selectedResource=${mockedResource} .sourceDomainConfig=${mockedSourceDomainConfig} + .amountToReceive=${parseUnits('12', 18)} > `); const transferDetail = el.shadowRoot!.querySelector( - '.transferDetailContainerValue' + '.transferDetail' ) as HTMLElement; assert.include(transferDetail.innerHTML, value); @@ -98,6 +100,7 @@ describe('sygma-fungible-transfer-detail', function () { .selectedResource=${mockedResource} .sourceDomainConfig=${mockedSourceDomainConfig} .estimatedGasFee=${mockedEstimatedGas} + .amountToReceive=${constants.Zero} > `);