From 81c8ceb3ebd26dc78c9c64b480ae22dca6d2bff8 Mon Sep 17 00:00:00 2001 From: Marin Petrunic Date: Wed, 21 Feb 2024 10:35:03 +0100 Subject: [PATCH 01/22] chore: update ci to support dev branch Signed-off-by: Marin Petrunic --- .github/workflows/cf-deploy-react.yml | 7 ++----- .github/workflows/cf-deploy-widget.yml | 7 ++----- .github/workflows/ci.yml | 3 ++- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cf-deploy-react.yml b/.github/workflows/cf-deploy-react.yml index 249feae2..1c6584ca 100644 --- a/.github/workflows/cf-deploy-react.yml +++ b/.github/workflows/cf-deploy-react.yml @@ -3,13 +3,11 @@ on: push: branches: - main - paths: - - 'packages/react/**' + - dev pull_request: branches: - main - paths: - - 'packages/react/**' + - dev jobs: deploy: @@ -37,5 +35,4 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: sygma-react-widget directory: ./examples/react-widget-app/dist - branch: main gitHubToken: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/cf-deploy-widget.yml b/.github/workflows/cf-deploy-widget.yml index 149dc662..0ce9728c 100644 --- a/.github/workflows/cf-deploy-widget.yml +++ b/.github/workflows/cf-deploy-widget.yml @@ -3,13 +3,11 @@ on: push: branches: - main - paths: - - 'packages/widget/**' + - dev pull_request: branches: - main - paths: - - 'packages/widget/**' + - dev jobs: deploy: @@ -37,5 +35,4 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} projectName: sygma-widget directory: ./packages/widget/dist - branch: main gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a852ea2..66b77941 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,8 @@ name: "ci / test" on: push: branches: - - main # runs on push to master, add more branches if you use them + - main + - dev pull_request: branches: - '**' # runs on update to pull request on any branch From c0fe09d8d2d4aa8eaf4b02db9daf53532d0f9eb7 Mon Sep 17 00:00:00 2001 From: Anton Lykhoyda Date: Mon, 4 Mar 2024 14:14:20 +0100 Subject: [PATCH 02/22] feat: wallet account selector for substrate (#106) Closes #74 --- packages/widget/src/assets/icons/identicon.ts | 157 ++++++++++++++++++ packages/widget/src/assets/index.ts | 8 +- .../src/components/address-input/styles.ts | 2 + .../amount-selector/amount-selector.ts | 2 +- .../src/components/amount-selector/styles.ts | 1 - .../common/buttons/connect-wallet.styles.ts | 26 ++- .../common/buttons/connect-wallet.ts | 98 ++++++----- .../components/common/dropdown/dropdown.ts | 38 ++++- .../src/components/common/dropdown/styles.ts | 13 +- .../widget/src/components/common/index.ts | 1 - .../overlay-component/overlay-component.ts | 2 +- packages/widget/src/components/index.ts | 1 + .../network-selector/network-selector.ts | 2 +- .../src/components/network-selector/styles.ts | 2 - .../substrate-account-selector/index.ts | 1 + .../substrate-account-selector/styles.ts | 81 +++++++++ .../substrate-account-selector.ts | 99 +++++++++++ .../fungible/fungible-token-transfer.ts | 2 +- .../components/transfer/fungible/styles.ts | 3 +- .../transfer-button/transfer-button.ts | 2 +- .../fungible/transfer-status/styles.ts | 2 +- .../transfer-status/transfer-status.ts | 4 +- packages/widget/src/context/wallet.ts | 8 +- .../transfers/fungible-token-transfer.ts | 11 +- .../src/controllers/wallet-manager/manager.ts | 17 ++ packages/widget/src/styles.ts | 4 +- packages/widget/src/widget.ts | 2 +- .../components/buttons/connect-wallet.test.ts | 47 +----- .../network-selector/network-selector.test.ts | 11 +- .../substrate-account-selector.test.ts | 57 +++++++ 30 files changed, 574 insertions(+), 130 deletions(-) create mode 100644 packages/widget/src/assets/icons/identicon.ts create mode 100644 packages/widget/src/components/substrate-account-selector/index.ts create mode 100644 packages/widget/src/components/substrate-account-selector/styles.ts create mode 100644 packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts create mode 100644 packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts diff --git a/packages/widget/src/assets/icons/identicon.ts b/packages/widget/src/assets/icons/identicon.ts new file mode 100644 index 00000000..bb9edc53 --- /dev/null +++ b/packages/widget/src/assets/icons/identicon.ts @@ -0,0 +1,157 @@ +import { html } from 'lit'; + +const identicon = html` + + + + + + + + + + + + + + + + + + + + + + +`; + +export default identicon; diff --git a/packages/widget/src/assets/index.ts b/packages/widget/src/assets/index.ts index 0c0c6ee5..2fa9df2c 100644 --- a/packages/widget/src/assets/index.ts +++ b/packages/widget/src/assets/index.ts @@ -13,6 +13,9 @@ import phalaNetworkIcon from './icons/phalaNetworkIcon'; import polygonNetworkIcon from './icons/polygonNetworkIcon'; import switchNetworkIcon from './icons/switchNetwork'; import sygmaLogo from './icons/sygmaLogo'; +import identicon from './icons/identicon'; +import plusIcon from './icons/plusIcon'; +import greenCircleIcon from './icons/greenCircleIcon'; export const networkIconsMap = { ethereum: ethereumIcon, @@ -37,5 +40,8 @@ export { noNetworkIcon, chevronIcon, greenMark, - loaderIcon + loaderIcon, + identicon, + plusIcon, + greenCircleIcon }; diff --git a/packages/widget/src/components/address-input/styles.ts b/packages/widget/src/components/address-input/styles.ts index 05d6a6b5..25fdfeb7 100644 --- a/packages/widget/src/components/address-input/styles.ts +++ b/packages/widget/src/components/address-input/styles.ts @@ -6,6 +6,7 @@ export const styles = css` flex-direction: column; justify-content: center; gap: 0.5rem; + margin-top: 1rem; min-height: 7.75rem; // TOO: remove this hardcoded value } @@ -47,5 +48,6 @@ export const styles = css` display: flex; flex-direction: row; justify-content: space-between; + margin-bottom: 0.25rem; } `; diff --git a/packages/widget/src/components/amount-selector/amount-selector.ts b/packages/widget/src/components/amount-selector/amount-selector.ts index 097043a7..37fb2c93 100644 --- a/packages/widget/src/components/amount-selector/amount-selector.ts +++ b/packages/widget/src/components/amount-selector/amount-selector.ts @@ -8,9 +8,9 @@ import { when } from 'lit/directives/when.js'; import { networkIconsMap } from '../../assets'; import { TokenBalanceController } from '../../controllers/wallet-manager/token-balance'; import { tokenBalanceToNumber } from '../../utils/token'; -import { BaseComponent } from '../common'; import type { DropdownOption } from '../common/dropdown/dropdown'; import { DEFAULT_ETH_DECIMALS } from '../../constants'; +import { BaseComponent } from '../common/base-component'; import { styles } from './styles'; @customElement('sygma-resource-selector') diff --git a/packages/widget/src/components/amount-selector/styles.ts b/packages/widget/src/components/amount-selector/styles.ts index 87f70c63..142193b8 100644 --- a/packages/widget/src/components/amount-selector/styles.ts +++ b/packages/widget/src/components/amount-selector/styles.ts @@ -4,7 +4,6 @@ export const styles = css` .amountSelectorContainer { display: flex; padding: 0.75rem; - margin: 0.5rem 0; align-items: center; border-radius: 1.5rem; flex-direction: column; diff --git a/packages/widget/src/components/common/buttons/connect-wallet.styles.ts b/packages/widget/src/components/common/buttons/connect-wallet.styles.ts index 42cbd0f2..03bb886d 100644 --- a/packages/widget/src/components/common/buttons/connect-wallet.styles.ts +++ b/packages/widget/src/components/common/buttons/connect-wallet.styles.ts @@ -14,8 +14,7 @@ export const connectWalletStyles = css` align-items: center; } - .walletAddress, - .connectWalletButton { + .walletAddress { font-size: 0.875rem; font-weight: 500; color: var(--zinc-700); @@ -26,14 +25,25 @@ export const connectWalletStyles = css` } .connectWalletButton { - padding: 0.375rem 0.5rem; + display: flex; + align-items: center; + padding: 0.38rem 0.75rem; border-radius: 2.5rem; - background-color: var(--zinc-100); - border: 1px solid var(--gray-100); + background: var(--zinc-800); + color: var(--zinc-200); + font-size: 0.875rem; + font-weight: 500; + outline: none; + border: none; cursor: pointer; - } + transition: filter 0.3s ease; + + &:hover { + filter: brightness(120%); + } - .connectWalletButton:hover { - background-image: linear-gradient(rgb(0 0 0/3%) 0 0); + svg { + margin-right: 0.5rem; + } } `; diff --git a/packages/widget/src/components/common/buttons/connect-wallet.ts b/packages/widget/src/components/common/buttons/connect-wallet.ts index ca1eb0a3..51fe68c0 100644 --- a/packages/widget/src/components/common/buttons/connect-wallet.ts +++ b/packages/widget/src/components/common/buttons/connect-wallet.ts @@ -1,18 +1,20 @@ import type { Domain } from '@buildwithsygma/sygma-sdk-core'; import { consume } from '@lit/context'; -import type { HTMLTemplateResult, PropertyValues } from 'lit'; +import type { HTMLTemplateResult, PropertyValues, TemplateResult } from 'lit'; import { html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { when } from 'lit/directives/when.js'; -import greenCircleIcon from '../../../assets/icons/greenCircleIcon'; -import plusIcon from '../../../assets/icons/plusIcon'; +import { choose } from 'lit/directives/choose.js'; +import { greenCircleIcon, plusIcon } from '../../../assets'; import type { WalletContext } from '../../../context'; import { walletContext } from '../../../context'; import { WalletController } from '../../../controllers'; import { shortAddress } from '../../../utils'; -import { BaseComponent } from '../base-component/base-component'; +import { BaseComponent } from '../base-component'; +import { WalletContextKeys } from '../../../context/wallet'; import { connectWalletStyles } from './connect-wallet.styles'; @customElement('sygma-connect-wallet-btn') @@ -28,9 +30,7 @@ export class ConnectWalletButton extends BaseComponent { }) sourceNetwork?: Domain; - @property({ - type: String - }) + @property({ type: String }) dappUrl?: string; @consume({ context: walletContext, subscribe: true }) @@ -62,49 +62,63 @@ export class ConnectWalletButton extends BaseComponent { return !!this.wallets.evmWallet || !!this.wallets.substrateWallet; } - render(): HTMLTemplateResult { + private renderConnectWalletButton(): HTMLTemplateResult | undefined { + if (this.wallets.substrateWallet) return; + + return when( + this.isWalletConnected(), + () => + html` `, + () => + html` ` + ); + } + + private renderWalletAddress(): TemplateResult | undefined { const evmWallet = this.wallets.evmWallet; - const substrateWallet = this.wallets.substrateWallet; - //TODO: this is wrong we need to enable user to select account - const substrateAccount = substrateWallet?.accounts[0]; - return html`
- ${when( - !!evmWallet?.address, - () => - html`${greenCircleIcon} ${shortAddress(evmWallet?.address ?? '')}` - )} - ${when( - !!substrateAccount, + const activeWalletKey = ( + Object.keys(this.wallets) as (keyof typeof this.wallets)[] + ).find((key) => !!this.wallets[key]); + + return choose(activeWalletKey, [ + [ + WalletContextKeys.EVM_WALLET, () => html`${greenCircleIcon} ${substrateAccount?.name} - ${shortAddress(substrateAccount?.address ?? '')}` - )} - ${when( - this.isWalletConnected(), - () => - html``, - () => - html`` - )} + ${greenCircleIcon} ${shortAddress(evmWallet?.address ?? '')} + ` + ], + [ + WalletContextKeys.SUBSTRATE_WALLET, + () => html` + + ` + ] + ]); + } + + render(): HTMLTemplateResult { + return html`
+ ${this.renderWalletAddress()} ${this.renderConnectWalletButton()}
`; } } + declare global { interface HTMLElementTagNameMap { 'sygma-connect-wallet-btn': ConnectWalletButton; diff --git a/packages/widget/src/components/common/dropdown/dropdown.ts b/packages/widget/src/components/common/dropdown/dropdown.ts index 6f0b7e5b..dd6a4a3e 100644 --- a/packages/widget/src/components/common/dropdown/dropdown.ts +++ b/packages/widget/src/components/common/dropdown/dropdown.ts @@ -6,13 +6,14 @@ import { when } from 'lit/directives/when.js'; import { chevronIcon, networkIconsMap } from '../../../assets'; import { capitalize } from '../../../utils'; -import { BaseComponent } from '../base-component/base-component'; +import { BaseComponent } from '../base-component'; import { styles } from './styles'; export interface DropdownOption> { id?: string; name: string; + customOptionHtml?: HTMLTemplateResult; icon?: HTMLTemplateResult | string; value: T; } @@ -36,6 +37,9 @@ export class Dropdown extends BaseComponent { @property({ type: Array }) options: DropdownOption[] = []; + @property({ type: Object }) + actionOption: HTMLTemplateResult | null = null; + @state() selectedOption: DropdownOption | null = null; @@ -57,6 +61,7 @@ export class Dropdown extends BaseComponent { //if options changed, check if we have selected option that doesn't exist if (changedProperties.has('options') && this.selectedOption) { if ( + Array.isArray(this.options) && !this.options.map((o) => o.value).includes(this.selectedOption.value) ) { this.selectedOption = null; @@ -90,6 +95,11 @@ export class Dropdown extends BaseComponent { } _renderTriggerContent(): HTMLTemplateResult | undefined { + // set first option as selected if no option is selected and there is no placeholder + if (!this.placeholder && !this.selectedOption) { + this.selectedOption = this.options[0]; + } + return when( this.selectedOption, () => @@ -105,7 +115,22 @@ export class Dropdown extends BaseComponent { ); } - _renderOptions(): Generator { + private renderOptionContent({ + customOptionHtml, + name, + icon + }: DropdownOption): HTMLTemplateResult | undefined { + return when( + customOptionHtml, + () => customOptionHtml, + () => html` + ${icon || ''} + ${capitalize(name)} + ` + ); + } + + _renderOptions(): Generator | HTMLTemplateResult { return map( this.options, (option) => html` @@ -114,8 +139,7 @@ export class Dropdown extends BaseComponent { @click="${(e: Event) => this._selectOption(option, e)}" role="option" > - ${option.icon || ''} - ${capitalize(option.name)} + ${this.renderOptionContent(option)}
` ); @@ -131,6 +155,8 @@ export class Dropdown extends BaseComponent { > diff --git a/packages/widget/src/components/common/dropdown/styles.ts b/packages/widget/src/components/common/dropdown/styles.ts index 610578d5..f871534e 100644 --- a/packages/widget/src/components/common/dropdown/styles.ts +++ b/packages/widget/src/components/common/dropdown/styles.ts @@ -3,14 +3,15 @@ import { css } from 'lit'; export const styles = css` .dropdownWrapper { min-width: 7.5rem; + padding: 0.75rem 1rem; position: relative; - width: 100%; height: 100%; } .dropdown { outline: none; height: 100%; + width: 100%; } .dropdownTrigger { @@ -45,6 +46,8 @@ export const styles = css` position: absolute; background-color: var(--white); width: 100%; + left: 0; + min-width: fit-content; border-radius: 0.75rem; border: 0.0625rem solid var(--gray-100); box-shadow: @@ -63,6 +66,11 @@ export const styles = css` padding: 0.75rem 1rem; cursor: pointer; transition: background-color 0.3s ease; + border-bottom: 1px solid var(--zinc-200); + + &:last-child { + border-bottom: none; + } svg { max-width: 1.43656rem; @@ -87,9 +95,6 @@ export const styles = css` .optionName { margin-left: 0.5rem; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; } .dropdownLabel { diff --git a/packages/widget/src/components/common/index.ts b/packages/widget/src/components/common/index.ts index 3c56acd7..11560cfa 100644 --- a/packages/widget/src/components/common/index.ts +++ b/packages/widget/src/components/common/index.ts @@ -2,4 +2,3 @@ export { Button } from './buttons/button'; export { ConnectWalletButton } from './buttons/connect-wallet'; export { Dropdown } from './dropdown/dropdown'; export { OverlayComponent } from './overlay-component'; -export { BaseComponent } from './base-component'; diff --git a/packages/widget/src/components/common/overlay-component/overlay-component.ts b/packages/widget/src/components/common/overlay-component/overlay-component.ts index 4ecc9259..66cc5f23 100644 --- a/packages/widget/src/components/common/overlay-component/overlay-component.ts +++ b/packages/widget/src/components/common/overlay-component/overlay-component.ts @@ -1,7 +1,7 @@ import { html } from 'lit'; import type { HTMLTemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { BaseComponent } from '../base-component/base-component'; +import { BaseComponent } from '../base-component'; import { styles } from './styles'; @customElement('sygma-overlay-component') diff --git a/packages/widget/src/components/index.ts b/packages/widget/src/components/index.ts index 46976eaa..f838614d 100644 --- a/packages/widget/src/components/index.ts +++ b/packages/widget/src/components/index.ts @@ -2,5 +2,6 @@ export { AmountSelector } from './amount-selector'; export { NetworkSelector } from './network-selector'; export { OverlayComponent } from './common/overlay-component'; export { ConnectWalletButton } from './common/buttons/connect-wallet'; +export { SubstrateAccountSelector } from './substrate-account-selector'; export { AddressInput } from './address-input'; export { FungibleTokenTransfer } from './transfer/fungible/fungible-token-transfer'; diff --git a/packages/widget/src/components/network-selector/network-selector.ts b/packages/widget/src/components/network-selector/network-selector.ts index 48da6fbf..9e08b866 100644 --- a/packages/widget/src/components/network-selector/network-selector.ts +++ b/packages/widget/src/components/network-selector/network-selector.ts @@ -5,7 +5,7 @@ import { customElement, property } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { networkIconsMap } from '../../assets'; -import { BaseComponent } from '../common/base-component/base-component'; +import { BaseComponent } from '../common/base-component'; import type { DropdownOption } from '../common/dropdown/dropdown'; import { styles } from './styles'; diff --git a/packages/widget/src/components/network-selector/styles.ts b/packages/widget/src/components/network-selector/styles.ts index 9de3c99b..3a1174a9 100644 --- a/packages/widget/src/components/network-selector/styles.ts +++ b/packages/widget/src/components/network-selector/styles.ts @@ -7,10 +7,8 @@ export const styles = css` display: flex; max-width: 19.625rem; max-height: 4.625rem; - padding: 0.75rem 1rem; flex-direction: column; justify-content: center; align-items: stretch; - gap: 0.25rem; } `; diff --git a/packages/widget/src/components/substrate-account-selector/index.ts b/packages/widget/src/components/substrate-account-selector/index.ts new file mode 100644 index 00000000..04150cd7 --- /dev/null +++ b/packages/widget/src/components/substrate-account-selector/index.ts @@ -0,0 +1 @@ +export { SubstrateAccountSelector } from './substrate-account-selector'; diff --git a/packages/widget/src/components/substrate-account-selector/styles.ts b/packages/widget/src/components/substrate-account-selector/styles.ts new file mode 100644 index 00000000..88b10eaa --- /dev/null +++ b/packages/widget/src/components/substrate-account-selector/styles.ts @@ -0,0 +1,81 @@ +import { css } from 'lit'; + +export const substrateAccountSelectorStyles = css` + // Custom option for dropdown. We are using part as we pass the custom option + // to the dropdown component as a property + dropdown-component::part(dropdown) { + border-radius: 2.5rem; + } + + dropdown-component::part(dropdownWrapper) { + padding: 0; + } + + dropdown-component::part(customOptionContent) { + display: flex; + flex-direction: column; + } + + dropdown-component::part(customOptionContentName) { + margin-bottom: 0.25rem; + font-size: 0.875rem; + font-weight: 500; + } + + dropdown-component::part(customOptionContentMain) { + display: flex; + align-items: center; + } + + dropdown-component::part(identicon) { + max-width: 3rem; + width: fit-content; + } + + dropdown-component::part(customOptionContentAccountData) { + display: flex; + flex-direction: column; + margin-left: 0.5rem; + } + + dropdown-component::part(customOptionContentType) { + margin-bottom: 0.16rem; + font-size: 0.75rem; + font-weight: 500; + } + + dropdown-component::part(customOptionContentAddress) { + color: var(--neutral-500); + font-size: 0.75rem; + font-weight: 400; + } + + dropdown-component::part(dropdownContent) { + margin-top: 0.75rem; + } + + dropdown-component::part(dropdownTrigger) { + min-width: 11.5rem; + border-radius: 2.5rem; + padding: 0.375rem 0.5rem 0.375rem 0.75rem; + background: var(--zinc-200); + } + + dropdown-component::part(substrateDisconnectButton) { + width: 100%; + cursor: pointer; + padding: 0.75rem 1rem; + color: var(--zinc-800); + font-size: 0.875rem; + font-weight: 500; + text-align: left; + background: rgba(0, 0, 0, 0); + outline: none; + border: none; + box-sizing: border-box; + } + + dropdown-component::part(substrateDisconnectButton):hover { + background-color: var(--neutral-100); + } +`; diff --git a/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts b/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts new file mode 100644 index 00000000..206a6337 --- /dev/null +++ b/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts @@ -0,0 +1,99 @@ +import { consume } from '@lit/context'; +import type { Account } from '@polkadot-onboard/core'; +import type { HTMLTemplateResult } from 'lit'; +import { html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { when } from 'lit/directives/when.js'; + +import { greenCircleIcon, identicon } from '../../assets'; +import { type WalletContext, walletContext } from '../../context'; +import { WalletController } from '../../controllers'; +import { shortAddress } from '../../utils'; +import { BaseComponent } from '../common/base-component'; +import type { DropdownOption } from '../common/dropdown/dropdown'; + +import { substrateAccountSelectorStyles } from './styles'; + +@customElement('sygma-substrate-account-selector') +export class SubstrateAccountSelector extends BaseComponent { + static styles = substrateAccountSelectorStyles; + + @consume({ context: walletContext, subscribe: true }) + @state() + private wallets!: WalletContext; + + private walletController = new WalletController(this); + + private onDisconnectClicked = (): void => { + this.walletController.disconnectWallet(); + }; + + private handleSubstrateAccountSelected = ( + option: DropdownOption + ): void => this.walletController.onSubstrateAccountSelected(option.value); + + private renderDisconnectSubstrateButton(): HTMLTemplateResult | undefined { + return html` `; + } + + private normalizeOptionsData(): DropdownOption[] { + const substrateWallet = this.wallets.substrateWallet; + if (!substrateWallet) return []; + + return substrateWallet.accounts.map((account: Account) => ({ + id: account.address, + name: shortAddress(account?.address ?? ''), + value: account, + icon: greenCircleIcon, + customOptionHtml: this.renderCustomOptionContent(account) + })); + } + + private renderCustomOptionContent({ + name, + address + }: Account): HTMLTemplateResult { + return html` +
+
+ ${name} +
+
+ ${identicon} +
+ ${address} +
+
+
+ `; + } + + render(): HTMLTemplateResult { + const substrateWallet = this.wallets.substrateWallet; + const substrateAccount = substrateWallet?.accounts[0]; + const options = this.normalizeOptionsData(); + + return when( + !!substrateAccount, + () => + html` + ` + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'sygma-substrate-account-selector': SubstrateAccountSelector; + } +} 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 9ebd50fe..335ebb92 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -13,7 +13,7 @@ import '../../address-input'; import '../../amount-selector'; import './transfer-button'; import './transfer-status'; -import { BaseComponent } from '../../common/base-component/base-component'; +import { BaseComponent } from '../../common/base-component'; import '../../network-selector'; import { Directions } from '../../network-selector/network-selector'; import { WalletController } from '../../../controllers'; diff --git a/packages/widget/src/components/transfer/fungible/styles.ts b/packages/widget/src/components/transfer/fungible/styles.ts index 6d3d6870..8fa36ee6 100644 --- a/packages/widget/src/components/transfer/fungible/styles.ts +++ b/packages/widget/src/components/transfer/fungible/styles.ts @@ -5,11 +5,12 @@ export const styles = css` display: flex; justify-content: center; } + form { width: 100%; display: flex; flex-direction: column; align-items: stretch; - gap: 0.25rem; + gap: 0.5rem; } `; diff --git a/packages/widget/src/components/transfer/fungible/transfer-button/transfer-button.ts b/packages/widget/src/components/transfer/fungible/transfer-button/transfer-button.ts index b1203ae1..f1ba25b5 100644 --- a/packages/widget/src/components/transfer/fungible/transfer-button/transfer-button.ts +++ b/packages/widget/src/components/transfer/fungible/transfer-button/transfer-button.ts @@ -4,7 +4,7 @@ import { customElement, property } from 'lit/decorators.js'; import { choose } from 'lit/directives/choose.js'; import { FungibleTransferState } from '../../../../controllers/transfers/fungible-token-transfer'; import type { Button } from '../../../common'; -import { BaseComponent } from '../../../common'; +import { BaseComponent } from '../../../common/base-component'; const enabledStates = [ FungibleTransferState.WRONG_CHAIN, diff --git a/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts b/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts index 608098a0..9de9abf7 100644 --- a/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts +++ b/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts @@ -16,7 +16,7 @@ export const styles = css` flex-direction: column; align-items: center; border-radius: 1.5rem; - border: 1px solid var(--zinc-200); + border: 0.0625rem solid var(--zinc-200); } .destinationMessage { diff --git a/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts b/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts index 1ef11399..523f35db 100644 --- a/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts +++ b/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts @@ -1,9 +1,9 @@ import { BigNumber, utils } from 'ethers'; import { html, type HTMLTemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { BaseComponent } from '../../../common'; -import { greenMark, networkIconsMap } from '../../../../assets'; import { DEFAULT_ETH_DECIMALS } from '../../../../constants'; +import { greenMark, networkIconsMap } from '../../../../assets'; +import { BaseComponent } from '../../../common/base-component'; import { styles } from './styles'; @customElement('sygma-transfer-status') diff --git a/packages/widget/src/context/wallet.ts b/packages/widget/src/context/wallet.ts index b54de745..66548fc5 100644 --- a/packages/widget/src/context/wallet.ts +++ b/packages/widget/src/context/wallet.ts @@ -5,7 +5,7 @@ import type { EIP1193Provider } from '@web3-onboard/core'; import type { HTMLTemplateResult } from 'lit'; import { html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { BaseComponent } from '../components/common/base-component/base-component'; +import { BaseComponent } from '../components/common/base-component'; export interface EvmWallet { address: string; @@ -15,6 +15,7 @@ export interface EvmWallet { export interface SubstrateWallet { signer: Signer; + signerAddress: string; accounts: Account[]; unsubscribeSubstrateAccounts?: UnsubscribeFn; disconnect?: () => Promise; @@ -25,6 +26,11 @@ export interface WalletContext { substrateWallet?: SubstrateWallet; } +export enum WalletContextKeys { + EVM_WALLET = 'evmWallet', + SUBSTRATE_WALLET = 'substrateWallet' +} + declare global { interface HTMLElementEventMap { walletUpdate: WalletUpdateEvent; diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index ee990bfb..afcfab9f 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -9,8 +9,10 @@ import { ContextConsumer } from '@lit/context'; import type { UnsignedTransaction } from 'ethers'; import { BigNumber } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; -import { walletContext } from '../../context/wallet'; + import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; +import { walletContext } from '../../context'; + import { buildEvmFungibleTransactions, executeNextEvmTransaction } from './evm'; export enum FungibleTransferState { @@ -84,10 +86,9 @@ export class FungibleTokenTransferController implements ReactiveController { this.host.requestUpdate(); this.env = env; await this.config.init(1, env); - this.supportedSourceNetworks = this.config - .getDomains() - //remove once we have proper substrate transfer support - .filter((n) => n.type === Network.EVM); + this.supportedSourceNetworks = this.config.getDomains(); + //remove once we have proper substrate transfer support + // .filter((n) => n.type === Network.EVM); this.supportedDestinationNetworks = this.config.getDomains(); this.host.requestUpdate(); } diff --git a/packages/widget/src/controllers/wallet-manager/manager.ts b/packages/widget/src/controllers/wallet-manager/manager.ts index f51f727a..03051af7 100644 --- a/packages/widget/src/controllers/wallet-manager/manager.ts +++ b/packages/widget/src/controllers/wallet-manager/manager.ts @@ -163,6 +163,7 @@ export class WalletController implements ReactiveController { new WalletUpdateEvent({ substrateWallet: { signer: wallet.signer, + signerAddress: accounts[0].address, accounts, unsubscribeSubstrateAccounts: unsub, disconnect: wallet.disconnect @@ -199,6 +200,8 @@ export class WalletController implements ReactiveController { new WalletUpdateEvent({ substrateWallet: { signer: this.walletContext.value.substrateWallet.signer, + signerAddress: + this.walletContext.value.substrateWallet.signerAddress, disconnect: this.walletContext.value.substrateWallet.disconnect, unsubscribeSubstrateAccounts: this.walletContext.value.substrateWallet @@ -213,4 +216,18 @@ export class WalletController implements ReactiveController { this.disconnectSubstrateWallet(); } }; + + onSubstrateAccountSelected = (account: Account): void => { + if (this.walletContext.value?.substrateWallet) { + this.host.dispatchEvent( + new WalletUpdateEvent({ + substrateWallet: { + signer: this.walletContext.value.substrateWallet.signer, + signerAddress: account.address, + accounts: this.walletContext.value.substrateWallet.accounts + } + }) + ); + } + }; } diff --git a/packages/widget/src/styles.ts b/packages/widget/src/styles.ts index 65ac5316..ec08d3ab 100644 --- a/packages/widget/src/styles.ts +++ b/packages/widget/src/styles.ts @@ -7,10 +7,12 @@ export const styles = css` --zinc-400: #a1a1aa; --zinc-500: #71717a; --zinc-700: #3f3f46; + --zinc-800: #27272a; --zinc-900: #18181b; --white: #fff; --gray-100: #f3f4f6; --neutral-100: #f5f5f5; + --neutral-500: #334155; --neutral-600: #525252; --primary-300: #a5b4fc; --primary-500: #6366f1; @@ -49,7 +51,7 @@ export const styles = css` } .networkSelectionWrapper { - margin: 1rem 0 0.5rem 0; + margin: 1rem 0; } .widgetHeader { diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 79fd8451..74dfc2dd 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -14,7 +14,7 @@ import { sygmaLogo } from './assets'; import './components'; import './components/address-input'; import './components/amount-selector'; -import { BaseComponent } from './components/common/base-component/base-component'; +import { BaseComponent } from './components/common/base-component'; import './components/transfer/fungible/fungible-token-transfer'; import './components/network-selector'; import './context/wallet'; diff --git a/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts b/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts index ea4aeb38..978aaa16 100644 --- a/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts +++ b/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts @@ -92,51 +92,6 @@ describe('connect-wallet button', function () { assert.equal(disconnectButton.textContent?.trim(), 'Disconnect'); }); - it('displays connected substrate wallet', async () => { - const walletContext = await fixture(html` - - `); - walletContext.dispatchEvent( - new WalletUpdateEvent({ - substrateWallet: { - accounts: [ - { - address: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe' - } - ], - signer: {} - } - }) - ); - const connectComponent = await fixture( - html` `, - { parentNode: walletContext } - ); - - const walletAddressEl = - connectComponent.shadowRoot!.querySelector( - '.walletAddress' - ); - - assert.isDefined( - walletAddressEl, - 'Connected wallet address is not displayed' - ); - - assert.equal( - walletAddressEl?.title, - '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe' - ); - assert.equal(walletAddressEl?.textContent?.trim(), '155Eek...Xzjqe'); - - const disconnectButton = connectComponent.shadowRoot!.querySelector( - '.connectWalletButton' - ) as HTMLButtonElement; - - assert.isDefined(disconnectButton, 'Button missing'); - assert.equal(disconnectButton.textContent?.trim(), 'Disconnect'); - }); - it('updates connected evm wallet', async () => { const walletContext = await fixture(html` @@ -237,6 +192,7 @@ describe('connect-wallet button', function () { address: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe' } ], + signerAddress: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe', signer: {} } }) @@ -314,6 +270,7 @@ describe('connect-wallet button', function () { address: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe' } ], + signerAddress: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe', signer: {}, disconnect: disconnectFn } diff --git a/packages/widget/tests/unit/components/network-selector/network-selector.test.ts b/packages/widget/tests/unit/components/network-selector/network-selector.test.ts index fbda7bf2..764db5e1 100644 --- a/packages/widget/tests/unit/components/network-selector/network-selector.test.ts +++ b/packages/widget/tests/unit/components/network-selector/network-selector.test.ts @@ -1,10 +1,11 @@ -import { fixture, fixtureCleanup, nextFrame } from '@open-wc/testing-helpers'; import { Network } from '@buildwithsygma/sygma-sdk-core'; import type { Domain } from '@buildwithsygma/sygma-sdk-core'; -import { afterEach, assert, describe, expect, it, vi } from 'vitest'; +import { fixture, fixtureCleanup, nextFrame } from '@open-wc/testing-helpers'; import { html } from 'lit'; +import { afterEach, assert, describe, expect, it, vi } from 'vitest'; + import { NetworkSelector } from '../../../../src/components'; -import type { Dropdown } from '../../../../src/components/common/dropdown/dropdown'; +import type { Dropdown } from '../../../../src/components/common'; describe('network-selector component', function () { afterEach(() => { @@ -28,7 +29,9 @@ describe('network-selector component', function () { const dropdown = el.shadowRoot?.querySelector( 'dropdown-component' ) as Dropdown; - expect(dropdown?.options.length).toBe(testNetworks.length); + + const dropdownOptions = dropdown.options; + expect(dropdownOptions?.length).toBe(testNetworks.length); }); it('calls "onNetworkSelected" with the correct Domain object when an option is selected', async () => { diff --git a/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts b/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts new file mode 100644 index 00000000..0ff855ec --- /dev/null +++ b/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts @@ -0,0 +1,57 @@ +import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; +import { afterEach, assert, describe, it } from 'vitest'; + +import { html } from 'lit'; +import type { ConnectWalletButton } from '../../../../src/components'; +import { SubstrateAccountSelector } from '../../../../src/components'; +import type { WalletContextProvider } from '../../../../src/context'; +import { WalletUpdateEvent } from '../../../../src/context'; +import type { Dropdown } from '../../../../src/components/common'; + +describe('Substrate account selector component', function () { + afterEach(() => { + fixtureCleanup(); + }); + + it('is defined', () => { + const el = document.createElement('sygma-substrate-account-selector'); + assert.instanceOf(el, SubstrateAccountSelector); + }); + + it('displays connected substrate wallet', async () => { + const walletContext = await fixture(html` + + `); + walletContext.dispatchEvent( + new WalletUpdateEvent({ + substrateWallet: { + accounts: [ + { + address: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe' + } + ], + signerAddress: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe', + signer: {} + } + }) + ); + const connectComponent = await fixture( + html` + + `, + { parentNode: walletContext } + ); + + const dropdown = connectComponent.shadowRoot!.querySelector( + 'dropdown-component' + ) as Dropdown; + + assert.equal(dropdown.selectedOption!.name, '155Eek...Xzjqe'); + + const disconnectButton = dropdown.shadowRoot!.querySelector( + '.substrateDisconnectButton' + ) as HTMLButtonElement; + + assert.equal(disconnectButton.textContent!.trim(), 'Disconnect'); + }); +}); From f3244c4f362d197455f6eff029e66732a732e959 Mon Sep 17 00:00:00 2001 From: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Date: Fri, 8 Mar 2024 20:20:56 +0500 Subject: [PATCH 03/22] feat: Retry loading bridge routes (#136) ## Description Added retry sdk initialization ## Related Issue Or Context https://github.com/sygmaprotocol/sygma-widget/issues/96 Closes: #96 ## How Has This Been Tested? Testing details. - Tested locally ## Types of changes Changes: - [x] Added state in widget component to keep track of sdk initialization - [x] Added event to be fired when sdk is initialized - [x] Retry sdk init method --- .../transfers/fungible-token-transfer.ts | 23 ++++++++++++++++++- packages/widget/src/interfaces/index.ts | 8 +++++++ packages/widget/src/widget.ts | 8 ++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index afcfab9f..a0a28532 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -13,6 +13,7 @@ import type { ReactiveController, ReactiveElement } from 'lit'; import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; import { walletContext } from '../../context'; +import { SdkInitializedEvent } from '../../interfaces'; import { buildEvmFungibleTransactions, executeNextEvmTransaction } from './evm'; export enum FungibleTransferState { @@ -82,10 +83,30 @@ export class FungibleTokenTransferController implements ReactiveController { this.reset(); } + /** + * Infinite Try/catch wrapper around + * {@link Config} from `@buildwithsygma/sygma-sdk-core` + * and emits a {@link SdkInitializedEvent} + * @param {number} time to wait before retrying request in ms + * @returns {void} + */ + async retryInitSdk(retryMs = 100): Promise { + try { + await this.config.init(1, this.env); + this.host.dispatchEvent( + new SdkInitializedEvent({ hasInitialized: true }) + ); + } catch (error) { + setTimeout(() => { + this.retryInitSdk(retryMs * 2).catch(console.error); + }, retryMs); + } + } + async init(env: Environment): Promise { this.host.requestUpdate(); this.env = env; - await this.config.init(1, env); + await this.retryInitSdk(); this.supportedSourceNetworks = this.config.getDomains(); //remove once we have proper substrate transfer support // .filter((n) => n.type === Network.EVM); diff --git a/packages/widget/src/interfaces/index.ts b/packages/widget/src/interfaces/index.ts index b4cc43fe..f02bf4c1 100644 --- a/packages/widget/src/interfaces/index.ts +++ b/packages/widget/src/interfaces/index.ts @@ -34,3 +34,11 @@ export interface ISygmaProtocolWidget { customLogo?: SVGElement; theme?: Theme; } + +export class SdkInitializedEvent extends CustomEvent<{ + hasInitialized: boolean; +}> { + constructor(update: { hasInitialized: boolean }) { + super('sdk-initialized', { detail: update, composed: true, bubbles: true }); + } +} diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 74dfc2dd..930b6f59 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -21,6 +21,7 @@ import './context/wallet'; import type { Eip1193Provider, ISygmaProtocolWidget, + SdkInitializedEvent, Theme } from './interfaces'; import { styles } from './styles'; @@ -59,6 +60,9 @@ class SygmaProtocolWidget @state() private isLoading = false; + @state() + private sdkInitialized = false; + @state() private sourceNetwork?: Domain; @@ -85,6 +89,8 @@ class SygmaProtocolWidget
+ (this.sdkInitialized = event.detail.hasInitialized)} .onSourceNetworkSelected=${(domain: Domain) => (this.sourceNetwork = domain)} .whitelistedSourceResources=${this.whitelistedSourceNetworks} @@ -94,7 +100,7 @@ class SygmaProtocolWidget
${sygmaLogo} Powered by Sygma
${when( - this.isLoading, + this.isLoading || !this.sdkInitialized, () => html`` )} From 370882b15e471b22ac829eb548851287812cb829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Petruni=C4=87?= Date: Wed, 13 Mar 2024 13:30:40 +0100 Subject: [PATCH 04/22] fix: typesafety when handling changed properties (#141) ## Description ## Related Issue Or Context Closes: # ## How Has This Been Tested? Testing details. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in sygma-docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot Signed-off-by: Marin Petrunic --- packages/widget/src/components/common/buttons/connect-wallet.ts | 2 +- packages/widget/src/components/common/dropdown/dropdown.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/widget/src/components/common/buttons/connect-wallet.ts b/packages/widget/src/components/common/buttons/connect-wallet.ts index 51fe68c0..c9d0cc1d 100644 --- a/packages/widget/src/components/common/buttons/connect-wallet.ts +++ b/packages/widget/src/components/common/buttons/connect-wallet.ts @@ -39,7 +39,7 @@ export class ConnectWalletButton extends BaseComponent { private walletController = new WalletController(this); - updated(changedProperties: PropertyValues): void { + updated(changedProperties: PropertyValues): void { super.updated(changedProperties); if (changedProperties.has('sourceNetwork')) { this.walletController.sourceNetworkUpdated(this.sourceNetwork); diff --git a/packages/widget/src/components/common/dropdown/dropdown.ts b/packages/widget/src/components/common/dropdown/dropdown.ts index dd6a4a3e..6646cf6b 100644 --- a/packages/widget/src/components/common/dropdown/dropdown.ts +++ b/packages/widget/src/components/common/dropdown/dropdown.ts @@ -56,7 +56,7 @@ export class Dropdown extends BaseComponent { removeEventListener('click', this._handleOutsideClick); } - updated(changedProperties: PropertyValues): void { + updated(changedProperties: PropertyValues): void { super.updated(changedProperties); //if options changed, check if we have selected option that doesn't exist if (changedProperties.has('options') && this.selectedOption) { From 13235bb6784e2d0e6249d30aa58c1beef4068358 Mon Sep 17 00:00:00 2001 From: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:03:52 +0500 Subject: [PATCH 05/22] feat: Display Fee (#145) This PR adds UI in widget to display fee that will be charged on `FungibleTransfer` ## Description - [X] Added Transfer detail component - [X] Modified `buildEvmFungibleTransactions` to store fee in component state - [X] Fixed button visibility issue ## Related Issue Or Context https://github.com/sygmaprotocol/sygma-widget/issues/55 Closes: #55 ## How Has This Been Tested? Testing details. ## Types of changes - [X] Component Added with styles - [X] Fixed button visibility issue - [X] Changes added to `buildTransactions` and `buildEvmTransactions` methods ## Checklist: - [ ] Add `sygma-fungible-transfer-detail` tests --- .../fungible/fungible-token-transfer.ts | 8 ++ .../transfer-button/transfer-button.ts | 1 + .../fungible/transfer-detail/index.ts | 1 + .../fungible/transfer-detail/styles.ts | 19 ++++ .../transfer-detail/transfer-detail.ts | 85 ++++++++++++++++++ .../src/controllers/transfers/evm/build.ts | 7 +- .../transfers/fungible-token-transfer.ts | 23 ++++- .../transfer-detail/transfer-detail.test.ts | 88 +++++++++++++++++++ 8 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 packages/widget/src/components/transfer/fungible/transfer-detail/index.ts create mode 100644 packages/widget/src/components/transfer/fungible/transfer-detail/styles.ts create mode 100644 packages/widget/src/components/transfer/fungible/transfer-detail/transfer-detail.ts create mode 100644 packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts 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 335ebb92..a0878d13 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -13,6 +13,7 @@ import '../../address-input'; import '../../amount-selector'; import './transfer-button'; import './transfer-status'; +import './transfer-detail'; import { BaseComponent } from '../../common/base-component'; import '../../network-selector'; import { Directions } from '../../network-selector/network-selector'; @@ -147,6 +148,13 @@ export class FungibleTokenTransfer extends BaseComponent { > +
+ +
; + + getFeeParams(type: FeeHandlerType): { decimals?: number; symbol: string } { + let decimals = undefined; + let symbol = ''; + + switch (type) { + case FeeHandlerType.BASIC: + if (this.sourceDomainConfig) { + decimals = Number(this.sourceDomainConfig.nativeTokenDecimals); + symbol = this.sourceDomainConfig.nativeTokenSymbol.toUpperCase(); + } + return { decimals, symbol }; + case FeeHandlerType.PERCENTAGE: + if (this.selectedResource) { + symbol = this.selectedResource.symbol ?? ''; + decimals = this.selectedResource.decimals ?? undefined; + } + return { decimals, symbol }; + default: + return { decimals, symbol }; + } + } + + getFee(): string { + if (!this.fee) return ''; + const { symbol, decimals } = this.getFeeParams(this.fee.type); + const { fee } = this.fee; + let _fee = ''; + + if (decimals) { + _fee = tokenBalanceToNumber(fee, decimals).toFixed(4); + } + + return `${_fee} ${symbol}`; + } + + render(): HTMLTemplateResult { + return html` +
+ ${when( + this.fee !== undefined, + () => + html`
+
Bridge Fee
+
${this.getFee()}
+
` + )} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'sygma-fungible-transfer-detail': FungibleTransferDetail; + } +} diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index 8df88e76..e8d1fda8 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -23,6 +23,7 @@ export async function buildEvmFungibleTransactions( !address || providerChaiId !== this.sourceNetwork.chainId ) { + this.resetFee(); return; } @@ -35,12 +36,12 @@ export async function buildEvmFungibleTransactions( this.selectedResource.resourceId, String(this.resourceAmount) ); - const fee = await evmTransfer.getFee(transfer); + this.fee = await evmTransfer.getFee(transfer); this.pendingEvmApprovalTransactions = await evmTransfer.buildApprovals( transfer, - fee + this.fee ); this.pendingEvmTransferTransaction = - await evmTransfer.buildTransferTransaction(transfer, fee); + await evmTransfer.buildTransferTransaction(transfer, this.fee); this.host.requestUpdate(); } diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index a0a28532..d17dbd30 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -1,4 +1,11 @@ -import type { Domain, Resource, Route } from '@buildwithsygma/sygma-sdk-core'; +import type { + Domain, + EthereumConfig, + EvmFee, + Resource, + Route, + SubstrateConfig +} from '@buildwithsygma/sygma-sdk-core'; import { Config, Environment, @@ -47,6 +54,7 @@ export class FungibleTokenTransferController implements ReactiveController { public supportedSourceNetworks: Domain[] = []; public supportedDestinationNetworks: Domain[] = []; public supportedResources: Resource[] = []; + public fee?: EvmFee; //Evm transfer protected buildEvmTransactions = buildEvmFungibleTransactions; @@ -62,6 +70,13 @@ export class FungibleTokenTransferController implements ReactiveController { host: ReactiveElement; walletContext: ContextConsumer; + get sourceDomainConfig(): EthereumConfig | SubstrateConfig | undefined { + if (this.sourceNetwork) { + return this.config.getDomainConfig(this.sourceNetwork.id); + } + return undefined; + } + constructor(host: ReactiveElement) { (this.host = host).addController(this); this.config = new Config(); @@ -114,6 +129,10 @@ export class FungibleTokenTransferController implements ReactiveController { this.host.requestUpdate(); } + resetFee(): void { + this.fee = undefined; + } + reset(): void { this.sourceNetwork = undefined; this.destinationNetwork = undefined; @@ -123,6 +142,7 @@ export class FungibleTokenTransferController implements ReactiveController { this.waitingTxExecution = false; this.waitingUserConfirmation = false; this.transferTransactionId = undefined; + this.resetFee(); void this.init(this.env); } @@ -286,6 +306,7 @@ export class FungibleTokenTransferController implements ReactiveController { !this.selectedResource || !this.destinatonAddress ) { + this.resetFee(); return; } switch (this.sourceNetwork.type) { 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 new file mode 100644 index 00000000..9e8b63e0 --- /dev/null +++ b/packages/widget/tests/unit/components/transfer-detail/transfer-detail.test.ts @@ -0,0 +1,88 @@ +import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; +import { html } from 'lit'; +import { afterEach, assert, describe, it } from 'vitest'; +import type { + BaseConfig, + EvmFee, + EvmResource +} from '@buildwithsygma/sygma-sdk-core'; +import { + FeeHandlerType, + Network, + ResourceType +} from '@buildwithsygma/sygma-sdk-core'; +import { constants } from 'ethers'; +import { parseUnits } from 'ethers/lib/utils'; +import { FungibleTransferDetail } from '../../../../src/components/transfer/fungible/transfer-detail'; + +describe('sygma-fungible-transfer-detail', function () { + const mockedFee: EvmFee = { + fee: constants.Zero, + type: FeeHandlerType.BASIC, + handlerAddress: '' + }; + + const mockedResource: EvmResource = { + resourceId: '', + type: ResourceType.FUNGIBLE, + address: '' + }; + + const mockedSourceDomainConfig: BaseConfig = { + id: 1, + chainId: 1, + name: 'ethereum', + type: Network.EVM, + bridge: '', + nativeTokenSymbol: 'ETH', + nativeTokenFullName: 'Ether', + nativeTokenDecimals: BigInt(18), + startBlock: BigInt(0), + blockConfirmations: 0, + resources: [] + }; + + afterEach(() => { + fixtureCleanup(); + }); + + it('is defined', () => { + const el = document.createElement('sygma-fungible-transfer-detail'); + assert.instanceOf(el, FungibleTransferDetail); + }); + + it('renders transfer-detail', async () => { + const el = await fixture(html` + + `); + + const transferDetail = el.shadowRoot!.querySelector( + '.transferDetail' + ) as HTMLElement; + + assert.isNotNull(transferDetail); + }); + + it('shows fee correctly', async () => { + const value = '1.0000 ETH'; + mockedFee.fee = parseUnits('1'); + + const el = await fixture(html` + + `); + + const transferDetail = el.shadowRoot!.querySelector( + '.transferDetailContainerValue' + ) as HTMLElement; + + assert.include(transferDetail.innerHTML, value); + }); +}); From 46cb4ea686514bdc8f387bbbb0884e0f37e1202c Mon Sep 17 00:00:00 2001 From: Anton Lykhoyda Date: Thu, 4 Apr 2024 15:05:17 +0200 Subject: [PATCH 06/22] chore: Merge main into dev (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Petrunic Co-authored-by: Filip Štoković <59089574+sztok7@users.noreply.github.com> Co-authored-by: Nicolás Riquelme Guzmán Co-authored-by: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Co-authored-by: mj52951 <116341045+mj52951@users.noreply.github.com> Co-authored-by: mj52951 Co-authored-by: Marin Petrunic --- .github/CODEOWNERS | 1 + .github/workflows/cf-deploy-react.yml | 3 - .github/workflows/cf-deploy-widget.yml | 3 - examples/react-widget-app/src/App.css | 117 ++++++++ examples/react-widget-app/src/App.tsx | 55 +++- .../react-widget-app/src/public/closeIcon.png | Bin 0 -> 8350 bytes .../react-widget-app/src/public/docsIcon.png | Bin 0 -> 8221 bytes .../src/public/githubIcon.png | Bin 0 -> 16945 bytes .../src/public/sidebarIcon.png | Bin 0 -> 6335 bytes .../react-widget-app/src/public/sygmaIcon.svg | 5 + packages/widget/.env.development | 1 + packages/widget/.env.production | 1 + packages/widget/package.json | 5 +- .../components/address-input/address-input.ts | 20 +- .../src/components/amount-selector/index.ts | 1 - .../common/buttons/connect-wallet.ts | 21 +- .../components/common/dropdown/dropdown.ts | 1 + packages/widget/src/components/index.ts | 2 +- .../network-selector/network-selector.ts | 4 +- .../resource-amount-selector/index.ts | 1 + .../resource-amount-selector.ts} | 142 +++++---- .../styles.ts | 1 + .../fungible/fungible-token-transfer.ts | 41 +-- .../transfer-detail/transfer-detail.ts | 2 +- .../transfer-status/transfer-status.ts | 12 +- packages/widget/src/context/config.ts | 65 ++++ packages/widget/src/context/index.ts | 3 + packages/widget/src/context/wallet.ts | 6 +- .../src/controllers/transfers/evm/build.ts | 4 +- .../transfers/fungible-token-transfer.ts | 172 ++++++----- .../src/controllers/wallet-manager/manager.ts | 37 ++- .../wallet-manager/token-balance.ts | 18 +- packages/widget/src/interfaces/index.ts | 4 + packages/widget/src/utils/token.ts | 21 +- packages/widget/src/vite-env.d.ts | 10 + packages/widget/src/widget.ts | 84 ++++-- .../address-input/address-input.test.ts | 95 ++++-- .../amount-selector/amount-selector.test.ts | 183 ----------- .../resource-amount-selector.test.ts | 284 ++++++++++++++++++ .../transfer-detail/transfer-detail.test.ts | 4 +- .../fungible/fungible-token-transfer.test.ts | 102 +++++++ .../widget/tests/unit/context/config.test.ts | 47 +++ .../transfers/fungible-token-transfer.test.ts | 25 ++ packages/widget/tests/unit/setup.ts | 12 + packages/widget/vite.config.ts | 1 + 45 files changed, 1165 insertions(+), 451 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 examples/react-widget-app/src/App.css create mode 100644 examples/react-widget-app/src/public/closeIcon.png create mode 100644 examples/react-widget-app/src/public/docsIcon.png create mode 100644 examples/react-widget-app/src/public/githubIcon.png create mode 100644 examples/react-widget-app/src/public/sidebarIcon.png create mode 100644 examples/react-widget-app/src/public/sygmaIcon.svg create mode 100644 packages/widget/.env.development create mode 100644 packages/widget/.env.production delete mode 100644 packages/widget/src/components/amount-selector/index.ts create mode 100644 packages/widget/src/components/resource-amount-selector/index.ts rename packages/widget/src/components/{amount-selector/amount-selector.ts => resource-amount-selector/resource-amount-selector.ts} (56%) rename packages/widget/src/components/{amount-selector => resource-amount-selector}/styles.ts (98%) create mode 100644 packages/widget/src/context/config.ts create mode 100644 packages/widget/src/vite-env.d.ts delete mode 100644 packages/widget/tests/unit/components/amount-selector/amount-selector.test.ts create mode 100644 packages/widget/tests/unit/components/resource-amount-selector/resource-amount-selector.test.ts create mode 100644 packages/widget/tests/unit/components/transfer/fungible/fungible-token-transfer.test.ts create mode 100644 packages/widget/tests/unit/context/config.test.ts create mode 100644 packages/widget/tests/unit/controllers/transfers/fungible-token-transfer.test.ts create mode 100644 packages/widget/tests/unit/setup.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..bc8e27d8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Lykhoyda @wainola @saadjhk @mpetrunic \ No newline at end of file diff --git a/.github/workflows/cf-deploy-react.yml b/.github/workflows/cf-deploy-react.yml index 1c6584ca..c7d379fb 100644 --- a/.github/workflows/cf-deploy-react.yml +++ b/.github/workflows/cf-deploy-react.yml @@ -5,9 +5,6 @@ on: - main - dev pull_request: - branches: - - main - - dev jobs: deploy: diff --git a/.github/workflows/cf-deploy-widget.yml b/.github/workflows/cf-deploy-widget.yml index 0ce9728c..00567106 100644 --- a/.github/workflows/cf-deploy-widget.yml +++ b/.github/workflows/cf-deploy-widget.yml @@ -5,9 +5,6 @@ on: - main - dev pull_request: - branches: - - main - - dev jobs: deploy: diff --git a/examples/react-widget-app/src/App.css b/examples/react-widget-app/src/App.css new file mode 100644 index 00000000..b08e6a4f --- /dev/null +++ b/examples/react-widget-app/src/App.css @@ -0,0 +1,117 @@ + +body, html { + background-color: #F5F0E9; + margin: 0; + padding: 0; + font-family: 'Inter', sans-serif; +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + src: url('https://fonts.googleapis.com/css2?family=Inter:wght@200..900'); +} + +.page{ + display: flex; +} + +.main{ + flex: 1; + overflow: auto; + height: 100vh; +} + +.centered { + display: flex; + justify-content: center; + align-items: center; +} + +.sidebar{ + position: relative; + height: 100vh; + width: 20%; + background-color: rgba(254, 86, 20, 0.2); + display: none; + flex-direction: column; +} + +.sidebar.open{ + display: flex; +} + +.sidebar-title{ + font-size: 2rem; + text-align: center; + color: black; + margin-top: 10%; +} + +.sidebar-button { + margin-top: 1.5rem; + width: 12rem; + font-size: 1.2rem; + border: 1; + border-color: #F5F0E9; + border-radius: 4rem; + background-color: #FE5614; + color: #F5F0E9; + cursor: pointer; +} + +.icon-column{ + display: flex; + height: 100%; + flex-direction: column; +} + +.icon-wrapper { + padding-top: 1.8rem; + flex-direction: row; + display: flex; + align-items: center; + margin: 0 auto; +} + +.icon-wrapper img { + width: 2.3rem; + height: 2.3rem; +} + +.icon-wrapper a { + color: black; + text-decoration: none; + font-size: 1.3rem; + margin-left: 5%; +} + +.icon-wrapper a:hover { + color: white; + font-weight: bold; +} + +#open-sidebar-button { + position: fixed; + top: 2rem; + left: 2rem; + cursor: pointer; +} + +#open-sidebar-button img { + height: 3.2rem; + width: 3.2rem; +} + +#close-icon{ + position: absolute; + top: 0.5rem; + right: 0.5rem; + cursor: pointer; +} + +#close-icon img{ + width: 0.8rem; + height: 0.8rem; +} \ No newline at end of file diff --git a/examples/react-widget-app/src/App.tsx b/examples/react-widget-app/src/App.tsx index fc2b640c..7a4f3675 100644 --- a/examples/react-widget-app/src/App.tsx +++ b/examples/react-widget-app/src/App.tsx @@ -1,10 +1,59 @@ import { SygmaProtocolReactWidget } from '@buildwithsygma/sygmaprotocol-react-widget' +import './App.css'; +import sygmaIcon from './public/sygmaIcon.svg' +import gitHubIcon from './public/githubIcon.png' +import docsIcon from './public/docsIcon.png' +import closeIcon from './public/closeIcon.png' +import sidebarIcon from './public/sidebarIcon.png' +import { useState } from 'react'; function App() { + const [sidebarOpen, setSidebarOpen] = useState(false); + + const toggleSidebar = () => { + setSidebarOpen(!sidebarOpen); + }; + + const obtainTokensClick = () => { + window.open("https://docs.buildwithsygma.com/environments/testnet/obtain-testnet-tokens", "_blank"); + }; + return ( - - ) -} +
+ +
+ +
+ {!sidebarOpen && ( +
+ Toggle Sidebar +
+ )} +
+ ) + } export default App diff --git a/examples/react-widget-app/src/public/closeIcon.png b/examples/react-widget-app/src/public/closeIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..a9413831d91aed81a7fe9944150e0985e935beb8 GIT binary patch literal 8350 zcmYLP3pmu-`#*zXyRfzkqNdHb6d}4OlhIZ*5nWwMOoVNTi89TSnQGfEZZ+gmn6|d; zE@D$+-KOnk5bfV?lH0TmYK0nw$(Z?{%jDPd^bFtk^ZA_jyyrdVyzh6M-RkK!bK1OV z0DzgyO&hiYpaFl>06J6Q$A_bTe}W%cG2bwE>cD^TI{VMS-@jmQ@{0jL-wFMv?6=t3 z3?CXD-spRHM^xxx&c5gnz~OLg4n&5>1n*;q*hEE#T^^**17IOwZdkYTXwE?A-ohg8 zzPa!EB0{g&HM(38th95`@NEB9v&@(AbXrly>^VhSF1>h=ns~$9e97+Ms{i}jeZ`8G zC$e^if4AbsykzeOw{ILjZvXdg4}FE@d}rm>?~gBjs=M{axl?VQ`88%cjOUb&boK9T z9)CY>=EeUyN6}h&KOyg9P54mE()^DnXnK2_6VvG9-{)mZhnWec;$Y^2>byXv?7`V) z9q^c)AwB+f%@D_|!d5(dtG~Lt{^qE&Q2MIvNSAh6{C_)7SFU_7RNbO=Uy!DnbGO7^ z=jjXGWiiaIZ0W&PRrNX`GRZZQ-)Gh=t?U|K&&-Rt$9VOM*>z#>&_pZk)tSD_9f<

J6-bF!@=UZT5#bnk6B&Oj%URP^E3y=udL0 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 T?fbSHngDa7=Z2f#uul9x0v3b9 literal 0 HcmV?d00001 diff --git a/examples/react-widget-app/src/public/docsIcon.png b/examples/react-widget-app/src/public/docsIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca6d2404dae717bf8241b98390efcdcd690d00b GIT binary patch literal 8221 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 BL@xjU literal 0 HcmV?d00001 diff --git a/examples/react-widget-app/src/public/sygmaIcon.svg b/examples/react-widget-app/src/public/sygmaIcon.svg new file mode 100644 index 00000000..12c1488d --- /dev/null +++ b/examples/react-widget-app/src/public/sygmaIcon.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/packages/widget/.env.development b/packages/widget/.env.development new file mode 100644 index 00000000..d633c346 --- /dev/null +++ b/packages/widget/.env.development @@ -0,0 +1 @@ +VITE_BRIDGE_ENV=testnet \ No newline at end of file diff --git a/packages/widget/.env.production b/packages/widget/.env.production new file mode 100644 index 00000000..3ae7e2f5 --- /dev/null +++ b/packages/widget/.env.production @@ -0,0 +1 @@ +VITE_BRIDGE_ENV=mainnet \ No newline at end of file diff --git a/packages/widget/package.json b/packages/widget/package.json index 96873f58..b7fabfe2 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -13,8 +13,9 @@ ], "scripts": { "start": "yarn run dev", - "dev": "vite", - "build": "yarn run lint:types && yarn run clean && vite build", + "dev": "VITE_BRIDGE_ENV=testnet vite", + "build": "yarn run lint:types && yarn run clean && vite build --mode development", + "build:production": "yarn run lint:types && yarn run clean && vite build", "build:preview": "yarn run clean && vite build --config vite.preview.config.ts", "serve": "vite preview --config vite.preview.config.ts", "clean": "rm -rf ./build", diff --git a/packages/widget/src/components/address-input/address-input.ts b/packages/widget/src/components/address-input/address-input.ts index df285c12..ca265292 100644 --- a/packages/widget/src/components/address-input/address-input.ts +++ b/packages/widget/src/components/address-input/address-input.ts @@ -4,6 +4,7 @@ import type { HTMLTemplateResult } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; +import type { PropertyValues } from '@lit/reactive-element'; import { validateAddress } from '../../utils'; import { BaseComponent } from '../common/base-component/base-component'; @@ -24,7 +25,7 @@ export class AddressInput extends BaseComponent { @property({ type: String }) - network: Network = Network.EVM; + networkType: Network = Network.EVM; @state() errorMessage: string | null = null; @@ -46,15 +47,18 @@ export class AddressInput extends BaseComponent { return; } - this.errorMessage = validateAddress(trimedValue, this.network); + this.errorMessage = validateAddress(trimedValue, this.networkType); - if (!this.errorMessage) { - void this.onAddressChange(trimedValue); - } else { - void this.onAddressChange(''); - } + this.onAddressChange(trimedValue); }; + protected updated(changedProperties: PropertyValues): void { + //revalidating address on network change + if (changedProperties.has('networkType')) { + this.handleAddressChange(this.address); + } + } + render(): HTMLTemplateResult { return html`

@@ -74,7 +78,7 @@ export class AddressInput extends BaseComponent { e.preventDefault(); } }} - @change=${(evt: Event) => + @input=${(evt: Event) => this.handleAddressChange((evt.target as HTMLInputElement).value)} >
diff --git a/packages/widget/src/components/amount-selector/index.ts b/packages/widget/src/components/amount-selector/index.ts deleted file mode 100644 index 2c52c3a2..00000000 --- a/packages/widget/src/components/amount-selector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AmountSelector } from './amount-selector'; diff --git a/packages/widget/src/components/common/buttons/connect-wallet.ts b/packages/widget/src/components/common/buttons/connect-wallet.ts index c9d0cc1d..4f3dc3ae 100644 --- a/packages/widget/src/components/common/buttons/connect-wallet.ts +++ b/packages/widget/src/components/common/buttons/connect-wallet.ts @@ -7,9 +7,10 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { when } from 'lit/directives/when.js'; import { choose } from 'lit/directives/choose.js'; -import { greenCircleIcon, plusIcon } from '../../../assets'; -import type { WalletContext } from '../../../context'; -import { walletContext } from '../../../context'; +import greenCircleIcon from '../../../assets/icons/greenCircleIcon'; +import plusIcon from '../../../assets/icons/plusIcon'; +import type { ConfigContext, WalletContext } from '../../../context'; +import { configContext, walletContext } from '../../../context'; import { WalletController } from '../../../controllers'; import { shortAddress } from '../../../utils'; import { BaseComponent } from '../base-component'; @@ -30,13 +31,14 @@ export class ConnectWalletButton extends BaseComponent { }) sourceNetwork?: Domain; - @property({ type: String }) - dappUrl?: string; - @consume({ context: walletContext, subscribe: true }) @state() private wallets!: WalletContext; + @consume({ context: configContext, subscribe: true }) + @state() + private configContext!: ConfigContext; + private walletController = new WalletController(this); updated(changedProperties: PropertyValues): void { @@ -48,9 +50,10 @@ export class ConnectWalletButton extends BaseComponent { private onConnectClicked = (): void => { if (this.sourceNetwork) { - this.walletController.connectWallet(this.sourceNetwork, { - dappUrl: this.dappUrl - }); + this.walletController.connectWallet( + this.sourceNetwork, + this.configContext + ); } }; diff --git a/packages/widget/src/components/common/dropdown/dropdown.ts b/packages/widget/src/components/common/dropdown/dropdown.ts index 6646cf6b..9ee56fa3 100644 --- a/packages/widget/src/components/common/dropdown/dropdown.ts +++ b/packages/widget/src/components/common/dropdown/dropdown.ts @@ -65,6 +65,7 @@ export class Dropdown extends BaseComponent { !this.options.map((o) => o.value).includes(this.selectedOption.value) ) { this.selectedOption = null; + this.onOptionSelected(undefined); } } } diff --git a/packages/widget/src/components/index.ts b/packages/widget/src/components/index.ts index f838614d..ab2c7e46 100644 --- a/packages/widget/src/components/index.ts +++ b/packages/widget/src/components/index.ts @@ -1,4 +1,4 @@ -export { AmountSelector } from './amount-selector'; +export { ResourceAmountSelector } from './resource-amount-selector'; export { NetworkSelector } from './network-selector'; export { OverlayComponent } from './common/overlay-component'; export { ConnectWalletButton } from './common/buttons/connect-wallet'; diff --git a/packages/widget/src/components/network-selector/network-selector.ts b/packages/widget/src/components/network-selector/network-selector.ts index 9e08b866..8f00e381 100644 --- a/packages/widget/src/components/network-selector/network-selector.ts +++ b/packages/widget/src/components/network-selector/network-selector.ts @@ -34,8 +34,8 @@ export class NetworkSelector extends BaseComponent { @property({ type: Array }) networks: Domain[] = []; - _onOptionSelected = ({ value }: DropdownOption): void => { - this.onNetworkSelected(value); + _onOptionSelected = (option?: DropdownOption): void => { + this.onNetworkSelected(option ? option.value : undefined); }; _renderNetworkIcon(name: string): HTMLTemplateResult { diff --git a/packages/widget/src/components/resource-amount-selector/index.ts b/packages/widget/src/components/resource-amount-selector/index.ts new file mode 100644 index 00000000..b6adf846 --- /dev/null +++ b/packages/widget/src/components/resource-amount-selector/index.ts @@ -0,0 +1 @@ +export { ResourceAmountSelector } from './resource-amount-selector'; diff --git a/packages/widget/src/components/amount-selector/amount-selector.ts b/packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts similarity index 56% rename from packages/widget/src/components/amount-selector/amount-selector.ts rename to packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts index 37fb2c93..bfc680a2 100644 --- a/packages/widget/src/components/amount-selector/amount-selector.ts +++ b/packages/widget/src/components/resource-amount-selector/resource-amount-selector.ts @@ -1,26 +1,27 @@ import type { Resource } from '@buildwithsygma/sygma-sdk-core'; -import { utils, type BigNumber } from 'ethers'; -import type { HTMLTemplateResult } from 'lit'; +import type { PropertyValues } from '@lit/reactive-element'; +import { BigNumber, utils } from 'ethers'; +import type { HTMLTemplateResult, PropertyDeclaration } from 'lit'; import { html } from 'lit'; 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 { TokenBalanceController } from '../../controllers/wallet-manager/token-balance'; +import { DEFAULT_ETH_DECIMALS } from '../../constants'; +import { + BALANCE_UPDATE_KEY, + TokenBalanceController +} from '../../controllers/wallet-manager/token-balance'; import { tokenBalanceToNumber } from '../../utils/token'; +import { BaseComponent } from '../common/base-component/base-component'; import type { DropdownOption } from '../common/dropdown/dropdown'; -import { DEFAULT_ETH_DECIMALS } from '../../constants'; -import { BaseComponent } from '../common/base-component'; import { styles } from './styles'; -@customElement('sygma-resource-selector') -export class AmountSelector extends BaseComponent { +@customElement('sygma-resource-amount-selector') +export class ResourceAmountSelector extends BaseComponent { static styles = styles; - @property({ - type: Array, - hasChanged: (n, o) => n !== o - }) + @property({ type: Array }) resources: Resource[] = []; @property({ type: Boolean }) @@ -31,14 +32,14 @@ export class AmountSelector extends BaseComponent { @property({ attribute: false }) /** - * amount is in lowest denomination (it's up to parent component to get resource decimals) + * amount is in the lowest denomination (it's up to parent component to get resource decimals) */ onResourceSelected: (resource: Resource, amount: BigNumber) => void = () => {}; - @state() validationMessage: string | null = null; @state() selectedResource: Resource | null = null; - @state() amount: number = 0; + @state() validationMessage: string | null = null; + @state() amount: string = ''; tokenBalanceController = new TokenBalanceController(this); @@ -47,13 +48,6 @@ export class AmountSelector extends BaseComponent { this.tokenBalanceController.balance, this.tokenBalanceController.decimals ); - }; - - _onInputAmountChangeHandler = (event: Event): void => { - const { value } = event.target as HTMLInputElement; - if (!this._validateAmount(value)) return; - - this.amount = Number.parseFloat(value); if (this.selectedResource) { this.onResourceSelected( this.selectedResource, @@ -65,33 +59,74 @@ export class AmountSelector extends BaseComponent { } }; - _onResourceSelectedHandler = ({ value }: DropdownOption): void => { - this.selectedResource = value; - this.amount = 0; - this.tokenBalanceController.startBalanceUpdates(value); + _onInputAmountChangeHandler = (event: Event): void => { + let { value } = event.target as HTMLInputElement; + + if (value === '') { + value = '0'; + } + try { + const amount = utils.parseUnits( + value, + this.tokenBalanceController.decimals + ); + this.amount = value; + if (!this._validateAmount(value)) return; + if (this.selectedResource) { + this.onResourceSelected(this.selectedResource, BigNumber.from(amount)); + } + } catch (error) { + this.validationMessage = 'Invalid amount value'; + } + }; + + requestUpdate( + name?: PropertyKey, + oldValue?: unknown, + options?: PropertyDeclaration + ): void { + super.requestUpdate(name, oldValue, options); + if (name === BALANCE_UPDATE_KEY) { + this._validateAmount(String(this.amount)); + } + } + + _onResourceSelectedHandler = (option?: DropdownOption): void => { + if (option) { + this.selectedResource = option.value; + this.amount = ''; + this.tokenBalanceController.startBalanceUpdates(this.selectedResource); + } else { + this.selectedResource = null; + this.tokenBalanceController.resetBalance(); + } }; _validateAmount(amount: string): boolean { - const parsedAmount = Number.parseFloat(amount); - if (isNaN(parsedAmount)) { - this.validationMessage = 'Invalid amount value'; - return false; + if (amount === '') { + amount = '0'; } - if (parsedAmount < 0) { - this.validationMessage = 'Amount must be greater than 0'; - return false; - } else if ( - parsedAmount > - tokenBalanceToNumber( - this.tokenBalanceController.balance, + try { + const parsedAmount = utils.parseUnits( + amount, this.tokenBalanceController.decimals - ) - ) { - this.validationMessage = 'Amount exceeds account balance'; - return false; - } else { + ); + + if (parsedAmount.lte(BigNumber.from(0))) { + this.validationMessage = 'Amount must be greater than 0'; + return false; + } + + if (parsedAmount.gt(this.tokenBalanceController.balance)) { + this.validationMessage = 'Amount exceeds account balance'; + return false; + } + this.validationMessage = null; return true; + } catch (error) { + this.validationMessage = 'Invalid amount value'; + return false; } } @@ -99,7 +134,7 @@ export class AmountSelector extends BaseComponent { return html`
${`${tokenBalanceToNumber(this.tokenBalanceController.balance, this.tokenBalanceController.decimals).toFixed(4)}`}${`${tokenBalanceToNumber(this.tokenBalanceController.balance, this.tokenBalanceController.decimals, 4)}`}
@@ -109,7 +144,8 @@ export class AmountSelector extends BaseComponent { _renderErrorMessages(): HTMLTemplateResult { return when( this.validationMessage, - () => html`
${this.validationMessage}
` + () => + html`
${this.validationMessage}
` ); } @@ -124,6 +160,15 @@ export class AmountSelector extends BaseComponent { ); } + updated(changedProperties: PropertyValues): void { + if (changedProperties.has('selectedResource')) { + if (changedProperties.get('selectedResource') !== null) { + this.tokenBalanceController.resetBalance(); + this.amount = ''; + } + } + } + render(): HTMLTemplateResult { const amountSelectorContainerClasses = classMap({ amountSelectorContainer: true, @@ -142,16 +187,11 @@ export class AmountSelector extends BaseComponent { type="number" class="amountSelectorInput" placeholder="0.000" - @change=${this._onInputAmountChangeHandler} - value=${this.amount.toString()} + @input=${this._onInputAmountChangeHandler} + .value=${this.amount} />
- o.id === this.preselectedToken || - o.name === this.preselectedToken - )} ?disabled=${this.disabled} .onOptionSelected=${this._onResourceSelectedHandler} .options=${this._normalizeOptions()} @@ -167,6 +207,6 @@ export class AmountSelector extends BaseComponent { declare global { interface HTMLElementTagNameMap { - 'sygma-resource-selector': AmountSelector; + 'sygma-resource-amount-selector': ResourceAmountSelector; } } diff --git a/packages/widget/src/components/amount-selector/styles.ts b/packages/widget/src/components/resource-amount-selector/styles.ts similarity index 98% rename from packages/widget/src/components/amount-selector/styles.ts rename to packages/widget/src/components/resource-amount-selector/styles.ts index 142193b8..419a5f6f 100644 --- a/packages/widget/src/components/amount-selector/styles.ts +++ b/packages/widget/src/components/resource-amount-selector/styles.ts @@ -68,6 +68,7 @@ export const styles = css` .balanceContent { display: flex; width: 100%; + align-items: center; justify-content: flex-end; gap: 0.375rem; } 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 a0878d13..1247258d 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -9,15 +9,15 @@ import { FungibleTokenTransferController, FungibleTransferState } from '../../../controllers/transfers/fungible-token-transfer'; +import '../../common/buttons/button'; import '../../address-input'; -import '../../amount-selector'; +import '../../resource-amount-selector'; import './transfer-button'; import './transfer-status'; -import './transfer-detail'; -import { BaseComponent } from '../../common/base-component'; import '../../network-selector'; import { Directions } from '../../network-selector/network-selector'; import { WalletController } from '../../../controllers'; +import { BaseComponent } from '../../common/base-component'; import { styles } from './styles'; @customElement('sygma-fungible-transfer') @@ -27,7 +27,7 @@ export class FungibleTokenTransfer extends BaseComponent { @property({ type: Array }) whitelistedSourceResources?: Array; @property({ type: String }) - environment: Environment = Environment.MAINNET; + environment?: Environment = Environment.MAINNET; @property({ type: Object }) onSourceNetworkSelected?: (domain: Domain) => void; @@ -37,7 +37,7 @@ export class FungibleTokenTransfer extends BaseComponent { connectedCallback(): void { super.connectedCallback(); - void this.transferController.init(this.environment); + void this.transferController.init(this.environment!); } private onClick = (): void => { @@ -73,21 +73,12 @@ export class FungibleTokenTransfer extends BaseComponent { } if (state === FungibleTransferState.COMPLETED) { - return; + this.transferController.reset(); } }; - render(): HTMLTemplateResult { - const state = this.transferController.getTransferState(); - return choose( - state, - [[FungibleTransferState.COMPLETED, () => this.renderTransferStatus()]], - () => this.renderTransfer() - )!; - } - renderTransferStatus(): HTMLTemplateResult { - return html`
+ return html`
- - +
@@ -163,6 +155,15 @@ export class FungibleTokenTransfer extends BaseComponent {
`; } + + render(): HTMLTemplateResult { + const state = this.transferController.getTransferState(); + return choose( + state, + [[FungibleTransferState.COMPLETED, () => this.renderTransferStatus()]], + () => this.renderTransfer() + )!; + } } declare global { diff --git a/packages/widget/src/components/transfer/fungible/transfer-detail/transfer-detail.ts b/packages/widget/src/components/transfer/fungible/transfer-detail/transfer-detail.ts index d5ace90e..f70753ba 100644 --- a/packages/widget/src/components/transfer/fungible/transfer-detail/transfer-detail.ts +++ b/packages/widget/src/components/transfer/fungible/transfer-detail/transfer-detail.ts @@ -56,7 +56,7 @@ export class FungibleTransferDetail extends BaseComponent { let _fee = ''; if (decimals) { - _fee = tokenBalanceToNumber(fee, decimals).toFixed(4); + _fee = tokenBalanceToNumber(fee, decimals, 4); } return `${_fee} ${symbol}`; diff --git a/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts b/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts index 523f35db..5540746a 100644 --- a/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts +++ b/packages/widget/src/components/transfer/fungible/transfer-status/transfer-status.ts @@ -1,4 +1,5 @@ -import { BigNumber, utils } from 'ethers'; +import type { BigNumber } from 'ethers'; +import { ethers, utils } from 'ethers'; import { html, type HTMLTemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { DEFAULT_ETH_DECIMALS } from '../../../../constants'; @@ -14,7 +15,7 @@ export class TransferStatus extends BaseComponent { @property({ type: String }) destinationNetworkName: string = ''; - @property({ type: Object }) amount: BigNumber = BigNumber.from(0); + @property({ type: Object }) amount: BigNumber = ethers.constants.Zero; @property({ type: Number }) tokenDecimals: number = DEFAULT_ETH_DECIMALS; @@ -37,15 +38,14 @@ export class TransferStatus extends BaseComponent {

Started a transfer

- From ${this.renderNetworkIcon(this.sourceNetworkName ?? '')} + From ${this.renderNetworkIcon(this.sourceNetworkName)} ${this.sourceNetworkName ?? 'Unknown'} to - ${this.renderNetworkIcon(this.destinationNetworkName ?? '')} + ${this.renderNetworkIcon(this.destinationNetworkName)} ${this.destinationNetworkName ?? 'Unknown'}
- ${this.formatAmount(this.amount ?? BigNumber.from(0))} - ${this.resourceSymbol} + ${this.formatAmount(this.amount)} ${this.resourceSymbol}
diff --git a/packages/widget/src/context/config.ts b/packages/widget/src/context/config.ts new file mode 100644 index 00000000..5355325a --- /dev/null +++ b/packages/widget/src/context/config.ts @@ -0,0 +1,65 @@ +import { customElement, property } from 'lit/decorators.js'; +import { createContext, ContextProvider } from '@lit/context'; +import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; +import type { HTMLTemplateResult, PropertyValues } from 'lit'; +import { html } from 'lit'; +import type { AppMetadata } from '@web3-onboard/common'; +import { BaseComponent } from '../components/common/base-component'; +import type { Theme } from '../interfaces'; + +export interface ConfigContext { + theme?: Theme; + walletConnectOptions?: WalletConnectOptions; + appMetaData?: AppMetadata; +} + +export const configContext = createContext( + Symbol('sygma-config-context') +); + +@customElement('sygma-config-context-provider') +export class ConfigContextProvider extends BaseComponent { + private configContextProvider = new ContextProvider(this, { + context: configContext, + initialValue: {} + }); + + @property({ attribute: false, type: Object }) + walletConnectOptions?: WalletConnectOptions; + + @property({ attribute: false, type: Object }) + appMetadata?: AppMetadata; + + @property({ attribute: false, type: Object }) + theme?: Theme; + + connectedCallback(): void { + super.connectedCallback(); + + this.configContextProvider.setValue({ + theme: this.theme, + walletConnectOptions: this.walletConnectOptions, + appMetaData: this.appMetadata + }); + } + + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if (changedProperties.has('theme')) { + this.configContextProvider.setValue({ + ...this.configContextProvider.value, + theme: this.theme + }); + } + } + + protected render(): HTMLTemplateResult { + return html``; + } +} + +declare global { + interface HTMLElementEventMap { + 'sygma-config-context-provider': ConfigContextProvider; + } +} diff --git a/packages/widget/src/context/index.ts b/packages/widget/src/context/index.ts index f63065ba..1c57c437 100644 --- a/packages/widget/src/context/index.ts +++ b/packages/widget/src/context/index.ts @@ -4,3 +4,6 @@ export { WalletContextProvider } from './wallet'; export type { EvmWallet, WalletContext } from './wallet'; + +export { configContext, ConfigContextProvider } from './config'; +export type { ConfigContext } from './config'; diff --git a/packages/widget/src/context/wallet.ts b/packages/widget/src/context/wallet.ts index 66548fc5..b840f130 100644 --- a/packages/widget/src/context/wallet.ts +++ b/packages/widget/src/context/wallet.ts @@ -55,12 +55,12 @@ export class WalletContextProvider extends BaseComponent { private walletContext: WalletContext = {}; @property({ attribute: false, type: Object }) - evmWalllet?: EvmWallet; + evmWallet?: EvmWallet; connectedCallback(): void { super.connectedCallback(); - if (this.evmWalllet) { - this.walletContext.evmWallet = this.evmWalllet; + if (this.evmWallet) { + this.walletContext.evmWallet = this.evmWallet; } this.addEventListener('walletUpdate', (event: WalletUpdateEvent) => { this.walletContext = { diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index e8d1fda8..30b096c2 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -18,7 +18,7 @@ export async function buildEvmFungibleTransactions( !this.destinationNetwork || !this.resourceAmount || !this.selectedResource || - !this.destinatonAddress || + !this.destinationAddress || !provider || !address || providerChaiId !== this.sourceNetwork.chainId @@ -32,7 +32,7 @@ export async function buildEvmFungibleTransactions( const transfer = await evmTransfer.createFungibleTransfer( address, this.destinationNetwork.chainId, - this.destinatonAddress, + this.destinationAddress, this.selectedResource.resourceId, String(this.resourceAmount) ); diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index d17dbd30..b46a9818 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -13,14 +13,12 @@ import { getRoutes } from '@buildwithsygma/sygma-sdk-core'; import { ContextConsumer } from '@lit/context'; -import type { UnsignedTransaction } from 'ethers'; -import { BigNumber } from 'ethers'; +import type { UnsignedTransaction, BigNumber } from 'ethers'; +import { ethers } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; - -import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; +import type { WalletContext } from '../../context'; import { walletContext } from '../../context'; - -import { SdkInitializedEvent } from '../../interfaces'; +import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; import { buildEvmFungibleTransactions, executeNextEvmTransaction } from './evm'; export enum FungibleTransferState { @@ -48,8 +46,8 @@ export class FungibleTokenTransferController implements ReactiveController { public sourceNetwork?: Domain; public destinationNetwork?: Domain; public selectedResource?: Resource; - public resourceAmount: BigNumber = BigNumber.from(0); - public destinatonAddress: string = ''; + public resourceAmount: BigNumber = ethers.constants.Zero; + public destinationAddress: string = ''; public supportedSourceNetworks: Domain[] = []; public supportedDestinationNetworks: Domain[] = []; @@ -70,11 +68,11 @@ export class FungibleTokenTransferController implements ReactiveController { host: ReactiveElement; walletContext: ContextConsumer; - get sourceDomainConfig(): EthereumConfig | SubstrateConfig | undefined { - if (this.sourceNetwork) { - return this.config.getDomainConfig(this.sourceNetwork.id); - } - return undefined; + isWalletDisconnected(context: WalletContext): boolean { + // Skip the method call during init + if (Object.values(context).length === 0) return false; + + return !(!!context.evmWallet || !!context.substrateWallet); } constructor(host: ReactiveElement) { @@ -83,48 +81,40 @@ export class FungibleTokenTransferController implements ReactiveController { this.walletContext = new ContextConsumer(host, { context: walletContext, subscribe: true, - callback: () => { + callback: (context: Partial) => { try { this.buildTransactions(); } catch (e) { console.error(e); } this.host.requestUpdate(); + + if (this.isWalletDisconnected(context)) { + this.reset(); + this.supportedResources = []; + } } }); } + get sourceDomainConfig(): EthereumConfig | SubstrateConfig | undefined { + if (this.config.environment && this.sourceNetwork) { + return this.config.getDomainConfig(this.sourceNetwork.id); + } + return undefined; + } hostDisconnected(): void { this.reset(); } - /** - * Infinite Try/catch wrapper around - * {@link Config} from `@buildwithsygma/sygma-sdk-core` - * and emits a {@link SdkInitializedEvent} - * @param {number} time to wait before retrying request in ms - * @returns {void} - */ - async retryInitSdk(retryMs = 100): Promise { - try { - await this.config.init(1, this.env); - this.host.dispatchEvent( - new SdkInitializedEvent({ hasInitialized: true }) - ); - } catch (error) { - setTimeout(() => { - this.retryInitSdk(retryMs * 2).catch(console.error); - }, retryMs); - } - } - async init(env: Environment): Promise { this.host.requestUpdate(); this.env = env; - await this.retryInitSdk(); - this.supportedSourceNetworks = this.config.getDomains(); - //remove once we have proper substrate transfer support - // .filter((n) => n.type === Network.EVM); + await this.config.init(1, this.env); + this.supportedSourceNetworks = this.config + .getDomains() + //remove once we have proper substrate transfer support + .filter((n) => n.type === Network.EVM); this.supportedDestinationNetworks = this.config.getDomains(); this.host.requestUpdate(); } @@ -138,7 +128,7 @@ export class FungibleTokenTransferController implements ReactiveController { this.destinationNetwork = undefined; this.pendingEvmApprovalTransactions = []; this.pendingEvmTransferTransaction = undefined; - this.destinatonAddress = ''; + this.destinationAddress = ''; this.waitingTxExecution = false; this.waitingUserConfirmation = false; this.transferTransactionId = undefined; @@ -156,9 +146,26 @@ export class FungibleTokenTransferController implements ReactiveController { void this.filterDestinationNetworksAndResources(network); }; + setSenderDefaultDestinationAddress = (): void => { + if (!this.sourceNetwork || !this.destinationNetwork) { + this.destinationAddress = ''; + return; + } + + const isSameNetwork = + this.sourceNetwork.chainId === this.destinationNetwork.chainId; + const isSameType = this.sourceNetwork.type === this.destinationNetwork.type; + + this.destinationAddress = + isSameNetwork || isSameType + ? this.walletContext.value?.evmWallet?.address || '' + : ''; + }; + onDestinationNetworkSelected = (network: Domain | undefined): void => { this.destinationNetwork = network; - if (this.sourceNetwork && !this.selectedResource) { + this.setSenderDefaultDestinationAddress(); + if (this.sourceNetwork) { //filter resources void this.filterDestinationNetworksAndResources(this.sourceNetwork); return; @@ -175,8 +182,8 @@ export class FungibleTokenTransferController implements ReactiveController { }; onDestinationAddressChange = (address: string): void => { - this.destinatonAddress = address; - if (this.destinatonAddress.length === 0) { + this.destinationAddress = address; + if (this.destinationAddress.length === 0) { this.pendingEvmApprovalTransactions = []; this.pendingEvmTransferTransaction = undefined; } @@ -185,6 +192,21 @@ export class FungibleTokenTransferController implements ReactiveController { }; getTransferState(): FungibleTransferState { + if (this.transferTransactionId) { + return FungibleTransferState.COMPLETED; + } + if (this.waitingUserConfirmation) { + return FungibleTransferState.WAITING_USER_CONFIRMATION; + } + if (this.waitingTxExecution) { + return FungibleTransferState.WAITING_TX_EXECUTION; + } + if (this.pendingEvmApprovalTransactions.length > 0) { + return FungibleTransferState.PENDING_APPROVALS; + } + if (this.pendingEvmTransferTransaction) { + return FungibleTransferState.PENDING_TRANSFER; + } if (!this.sourceNetwork) { return FungibleTransferState.MISSING_SOURCE_NETWORK; } @@ -197,7 +219,7 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.resourceAmount.eq(0)) { return FungibleTransferState.MISSING_RESOURCE_AMOUNT; } - if (this.destinatonAddress === '') { + if (this.destinationAddress === '') { return FungibleTransferState.MISSING_DESTINATION_ADDRESS; } if ( @@ -213,26 +235,12 @@ export class FungibleTokenTransferController implements ReactiveController { ) { return FungibleTransferState.WRONG_CHAIN; } - if (this.waitingUserConfirmation) { - return FungibleTransferState.WAITING_USER_CONFIRMATION; - } - if (this.waitingTxExecution) { - return FungibleTransferState.WAITING_TX_EXECUTION; - } - if (this.transferTransactionId) { - return FungibleTransferState.COMPLETED; - } - if (this.pendingEvmApprovalTransactions.length > 0) { - return FungibleTransferState.PENDING_APPROVALS; - } - if (this.pendingEvmTransferTransaction) { - return FungibleTransferState.PENDING_TRANSFER; - } return FungibleTransferState.UNKNOWN; } executeTransaction(): void { if (!this.sourceNetwork) { + this.resetFee(); return; } switch (this.sourceNetwork.type) { @@ -267,17 +275,40 @@ export class FungibleTokenTransferController implements ReactiveController { await getRoutes(this.env, sourceNetwork.chainId, 'fungible') ); } + this.supportedResources = []; + const routes = this.routesCache.get(sourceNetwork.chainId)!; + + // unselect destination if equal to source network or isn't in list of available destination networks + if (this.destinationNetwork?.id === sourceNetwork.id || !routes.length) { + this.destinationNetwork = undefined; + this.selectedResource = undefined; + this.supportedDestinationNetworks = []; + } + + // either first time or we had source === destination if (!this.destinationNetwork) { - this.supportedDestinationNetworks = this.routesCache - .get(sourceNetwork.chainId)! - .filter( - (route) => - route.toDomain.chainId !== sourceNetwork.chainId && - !this.supportedDestinationNetworks.includes(route.toDomain) - ) + this.supportedDestinationNetworks = routes + .filter((route) => route.toDomain.chainId !== sourceNetwork.chainId) .map((route) => route.toDomain); + } // source change but not destination, check if route is supported + else if (this.supportedDestinationNetworks.length && routes.length) { + const isSourceOnSuportedDestinations = + this.supportedDestinationNetworks.some( + (destinationDomain) => + destinationDomain.chainId === this.sourceNetwork?.chainId + ); + if (isSourceOnSuportedDestinations) { + this.supportedDestinationNetworks = routes + .filter( + (route) => + route.toDomain.chainId !== sourceNetwork.chainId && + !this.supportedDestinationNetworks.includes(route.toDomain) + ) + .map((route) => route.toDomain); + } } + this.supportedResources = this.routesCache .get(sourceNetwork.chainId)! .filter( @@ -287,13 +318,7 @@ export class FungibleTokenTransferController implements ReactiveController { !this.supportedResources.includes(route.resource)) ) .map((route) => route.resource); - //unselect destination if equal to source network or isn't in list of available destination networks - if ( - this.destinationNetwork?.id === sourceNetwork.id || - !this.supportedDestinationNetworks.includes(this.destinationNetwork!) - ) { - this.destinationNetwork = undefined; - } + void this.buildTransactions(); this.host.requestUpdate(); }; @@ -304,9 +329,8 @@ export class FungibleTokenTransferController implements ReactiveController { !this.destinationNetwork || !this.resourceAmount || !this.selectedResource || - !this.destinatonAddress + !this.destinationAddress ) { - this.resetFee(); return; } switch (this.sourceNetwork.type) { diff --git a/packages/widget/src/controllers/wallet-manager/manager.ts b/packages/widget/src/controllers/wallet-manager/manager.ts index 03051af7..4a580134 100644 --- a/packages/widget/src/controllers/wallet-manager/manager.ts +++ b/packages/widget/src/controllers/wallet-manager/manager.ts @@ -8,6 +8,8 @@ import injectedModule from '@web3-onboard/injected-wallets'; import walletConnectModule from '@web3-onboard/walletconnect'; import type { ReactiveController, ReactiveElement } from 'lit'; +import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; +import type { AppMetadata } from '@web3-onboard/common'; import { utils } from 'ethers'; import { WalletUpdateEvent, walletContext } from '../../context'; @@ -33,7 +35,13 @@ export class WalletController implements ReactiveController { } } - connectWallet = (network: Domain, options?: { dappUrl?: string }): void => { + connectWallet = ( + network: Domain, + options?: { + walletConnectOptions?: WalletConnectOptions; + appMetaData?: AppMetadata; + } + ): void => { switch (network.type) { case Network.EVM: { @@ -90,18 +98,22 @@ export class WalletController implements ReactiveController { connectEvmWallet = async ( network: Domain, - options?: { dappUrl?: string } + options?: { + walletConnectOptions?: WalletConnectOptions; + appMetaData?: AppMetadata; + } ): Promise => { const injected = injectedModule(); + const walletSetup = [injected]; + + if (options?.walletConnectOptions?.projectId) { + walletSetup.push(walletConnectModule(options.walletConnectOptions)); + } + const onboard = Onboard({ - wallets: [ - injected, - walletConnectModule({ - projectId: '2f5a3439ef861e2a3959d85afcd32d06', - dappUrl: options?.dappUrl - }) - ], + appMetadata: options?.appMetaData, + wallets: walletSetup, chains: [ { id: network.chainId @@ -142,11 +154,14 @@ export class WalletController implements ReactiveController { connectSubstrateWallet = async ( _network: Domain, // TODO: remove underscore prefix once arg usage is added - options?: { dappUrl?: string; dappName?: string } + options?: { + walletConnectOptions?: WalletConnectOptions; + appMetaData?: AppMetadata; + } ): Promise => { const injectedWalletProvider = new InjectedWalletProvider( { disallowed: [] }, - options?.dappName ?? 'Sygma Widget' + options?.appMetaData?.name ?? 'Sygma Widget' ); const wallets = await injectedWalletProvider.getWallets(); diff --git a/packages/widget/src/controllers/wallet-manager/token-balance.ts b/packages/widget/src/controllers/wallet-manager/token-balance.ts index dbbb0a74..8876c4cd 100644 --- a/packages/widget/src/controllers/wallet-manager/token-balance.ts +++ b/packages/widget/src/controllers/wallet-manager/token-balance.ts @@ -3,13 +3,16 @@ import type { EvmResource, Resource } from '@buildwithsygma/sygma-sdk-core'; import { ResourceType } from '@buildwithsygma/sygma-sdk-core'; import { Web3Provider } from '@ethersproject/providers'; import { ContextConsumer } from '@lit/context'; -import { BigNumber } from 'ethers'; +import { ethers } from 'ethers'; +import type { BigNumber } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; import { walletContext } from '../../context'; import { isEvmResource } from '../../utils'; const BALANCE_REFRESH_MS = 5_000; +export const BALANCE_UPDATE_KEY = 'accountBalance'; + export class TokenBalanceController implements ReactiveController { host: ReactiveElement; @@ -18,7 +21,7 @@ export class TokenBalanceController implements ReactiveController { loadingBalance: boolean = true; //"wei" - balance: BigNumber = BigNumber.from(0); + balance: BigNumber = ethers.constants.Zero; decimals: number = 18; timeout?: ReturnType; @@ -41,7 +44,7 @@ export class TokenBalanceController implements ReactiveController { if (this.timeout) { clearInterval(this.timeout); } - this.balance = BigNumber.from(0); + this.balance = ethers.constants.Zero; this.host.requestUpdate(); if (isEvmResource(resource)) { if (resource.type === ResourceType.FUNGIBLE) { @@ -67,6 +70,13 @@ export class TokenBalanceController implements ReactiveController { throw new Error('Unsupported resource'); } + resetBalance(): void { + if (this.timeout) { + clearInterval(this.timeout); + } + this.balance = ethers.constants.Zero; + } + subscribeERC20BalanceUpdate = (resource: EvmResource): void => { const provider = this.walletContext.value?.evmWallet?.provider; const address = this.walletContext.value?.evmWallet?.address; @@ -80,7 +90,7 @@ export class TokenBalanceController implements ReactiveController { this.decimals = await ierc20.decimals(); this.balance = await ierc20.balanceOf(address); this.loadingBalance = false; - this.host.requestUpdate(); + this.host.requestUpdate(BALANCE_UPDATE_KEY); }.bind(this)(); }; diff --git a/packages/widget/src/interfaces/index.ts b/packages/widget/src/interfaces/index.ts index f02bf4c1..ab60867f 100644 --- a/packages/widget/src/interfaces/index.ts +++ b/packages/widget/src/interfaces/index.ts @@ -1,9 +1,11 @@ import type { + Environment, EvmResource, SubstrateResource } from '@buildwithsygma/sygma-sdk-core'; import type { ApiPromise } from '@polkadot/api'; import type { Signer } from '@polkadot/api/types'; +import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; export type ThemeVariables = | 'mainColor' @@ -22,6 +24,7 @@ export interface Eip1193Provider { } export interface ISygmaProtocolWidget { + environment?: Environment; whitelistedSourceNetworks?: string[]; whitelistedDestinationNetworks?: string[]; evmProvider?: Eip1193Provider; @@ -33,6 +36,7 @@ export interface ISygmaProtocolWidget { darkTheme?: boolean; customLogo?: SVGElement; theme?: Theme; + walletConnectOptions?: WalletConnectOptions; } export class SdkInitializedEvent extends CustomEvent<{ diff --git a/packages/widget/src/utils/token.ts b/packages/widget/src/utils/token.ts index 2bca6ca4..10b4eb94 100644 --- a/packages/widget/src/utils/token.ts +++ b/packages/widget/src/utils/token.ts @@ -1,9 +1,20 @@ -import type { BigNumber } from 'ethers'; -import { utils } from 'ethers'; +import { BigNumber, utils } from 'ethers'; export function tokenBalanceToNumber( amount: BigNumber, - decimals: number -): number { - return Number.parseFloat(utils.formatUnits(amount, decimals)); + decimals: number, + formattedDecimals?: number +): string { + let value = utils.formatUnits(amount, decimals); + + if (formattedDecimals) { + let valueBigNumber = utils.parseUnits(value, decimals); + const factor = BigNumber.from(10).pow(formattedDecimals); + valueBigNumber = valueBigNumber + .mul(factor) + .div(BigNumber.from(10).pow(decimals)); + value = utils.formatUnits(valueBigNumber, formattedDecimals); + } + + return value; } diff --git a/packages/widget/src/vite-env.d.ts b/packages/widget/src/vite-env.d.ts new file mode 100644 index 00000000..d2c5e134 --- /dev/null +++ b/packages/widget/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_BRIDGE_ENV: string; + // more env variables... +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 930b6f59..72655bad 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -10,18 +10,20 @@ import type { HTMLTemplateResult } from 'lit'; import { html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; + +import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; +import type { AppMetadata } from '@web3-onboard/common'; import { sygmaLogo } from './assets'; import './components'; import './components/address-input'; -import './components/amount-selector'; -import { BaseComponent } from './components/common/base-component'; +import './components/resource-amount-selector'; +import { BaseComponent } from './components/common/base-component/base-component'; import './components/transfer/fungible/fungible-token-transfer'; import './components/network-selector'; import './context/wallet'; import type { Eip1193Provider, ISygmaProtocolWidget, - SdkInitializedEvent, Theme } from './interfaces'; import { styles } from './styles'; @@ -33,6 +35,8 @@ class SygmaProtocolWidget { static styles = styles; + @property({ type: String }) environment?: Environment; + @property({ type: Array }) whitelistedSourceNetworks?: string[]; @property({ type: Array }) whitelistedDestinationNetworks?: string[]; @@ -57,11 +61,12 @@ class SygmaProtocolWidget @property({ type: Object }) theme?: Theme; - @state() - private isLoading = false; + @property({ type: Object }) walletConnectOptions?: WalletConnectOptions; + + @property({ type: Object }) appMetadata?: AppMetadata; @state() - private sdkInitialized = false; + private isLoading = false; @state() private sourceNetwork?: Domain; @@ -77,34 +82,51 @@ class SygmaProtocolWidget return html``; } + connectedCallback(): void { + super.connectedCallback(); + const env = import.meta.env.VITE_BRIDGE_ENV ?? Environment.MAINNET; + if (Object.values(Environment).includes(env as Environment)) { + this.environment = env as Environment; + } else { + throw new Error( + `Invalid environment value, please choose following: ${Object.values(Environment).join(', ')}` + ); + } + } + render(): HTMLTemplateResult { return html` - -
-
-
[Brand] Transfer
- ${this.renderConnect()} -
-
- - (this.sdkInitialized = event.detail.hasInitialized)} - .onSourceNetworkSelected=${(domain: Domain) => - (this.sourceNetwork = domain)} - .whitelistedSourceResources=${this.whitelistedSourceNetworks} - environment=${Environment.TESTNET} - > - + + +
+
+
[Brand] Transfer
+ ${this.renderConnect()} +
+
+ + (this.sourceNetwork = domain)} + .whitelistedSourceResources=${this.whitelistedSourceNetworks} + environment=${Environment.TESTNET} + > + +
+
${sygmaLogo} Powered by Sygma
+ ${when( + this.isLoading, + () => html`` + )}
-
${sygmaLogo} Powered by Sygma
- ${when( - this.isLoading || !this.sdkInitialized, - () => html`` - )} -
- + + `; } } diff --git a/packages/widget/tests/unit/components/address-input/address-input.test.ts b/packages/widget/tests/unit/components/address-input/address-input.test.ts index 80f1e7f2..61eef08d 100644 --- a/packages/widget/tests/unit/components/address-input/address-input.test.ts +++ b/packages/widget/tests/unit/components/address-input/address-input.test.ts @@ -1,5 +1,10 @@ import { afterEach, assert, describe, it, vi } from 'vitest'; -import { fixture, fixtureCleanup, oneEvent } from '@open-wc/testing-helpers'; +import { + elementUpdated, + fixture, + fixtureCleanup, + oneEvent +} from '@open-wc/testing-helpers'; import { html } from 'lit'; import { Network } from '@buildwithsygma/sygma-sdk-core'; import { AddressInput } from '../../../../src/components'; @@ -64,7 +69,7 @@ describe('address-input component', function () { el = await fixture(html` @@ -97,15 +102,15 @@ describe('address-input component', function () { assert.equal(input.value.trim(), '0x123'); - const listener = oneEvent(input, 'change', false); + const listener = oneEvent(input, 'input', false); input.value = '0xebFC7A970CAAbC18C8e8b7367147C18FC7585492'; - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 2); - assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); + assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['0x123']); assert.deepEqual(mockAddressChangeHandler.mock.lastCall, [ '0xebFC7A970CAAbC18C8e8b7367147C18FC7585492' ]); @@ -124,14 +129,14 @@ describe('address-input component', function () { '.inputAddress' ) as HTMLInputElement; - const listener = oneEvent(input, 'change', false); + const listener = oneEvent(input, 'input', false); input.value = '0xebFC7A970CAAbC18C8e8b7367147C18FC7'; - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 2); + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); assert.deepEqual(mockAddressChangeHandler.mock.calls[1], ['']); @@ -143,12 +148,15 @@ describe('address-input component', function () { input.value = ''; - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 3); - assert.deepEqual(mockAddressChangeHandler.mock.calls[2], ['']); + assert.equal(mockAddressChangeHandler.mock.calls.length, 4); + assert.deepEqual(mockAddressChangeHandler.mock.calls[2], [ + '0xebFC7A970CAAbC18C8e8b7367147C18FC7' + ]); + assert.deepEqual(mockAddressChangeHandler.mock.lastCall, ['']); const errorMessageAfterClean = el.shadowRoot!.querySelector( '.errorMessage' @@ -162,7 +170,7 @@ describe('address-input component', function () { const el = await fixture(html` `); @@ -171,15 +179,15 @@ describe('address-input component', function () { '.inputAddress' ) as HTMLInputElement; - const listener = oneEvent(input, 'change', false); + const listener = oneEvent(input, 'input', false); input.value = '42sydUvocBuEorweEPqxY5vZae1VaTtWoJFiKMrPbRamy2BL'; - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 2); + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); assert.deepEqual(mockAddressChangeHandler.mock.lastCall, [ '42sydUvocBuEorweEPqxY5vZae1VaTtWoJFiKMrPbRamy2BL' @@ -199,15 +207,15 @@ describe('address-input component', function () { '.inputAddress' ) as HTMLInputElement; - const listener = oneEvent(input, 'change', false); + const listener = oneEvent(input, 'input', false); input.value = '0xebFC7A970CAAbC18C8e8b7367147C18FC7585492'; - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 2); + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); assert.deepEqual(mockAddressChangeHandler.mock.lastCall, [ '0xebFC7A970CAAbC18C8e8b7367147C18FC7585492' @@ -219,7 +227,7 @@ describe('address-input component', function () { const el = await fixture(html` `); @@ -228,15 +236,15 @@ describe('address-input component', function () { '.inputAddress' ) as HTMLInputElement; - const listener = oneEvent(input, 'change', false); + const listener = oneEvent(input, 'input', false); input.value = '42sydUvocBuEorweEPqxY5vZae1VaTtWoJFiKMrPbRamy'; // invalid substrate address - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 2); + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); assert.deepEqual(mockAddressChangeHandler.mock.calls[1], ['']); @@ -260,15 +268,15 @@ describe('address-input component', function () { '.inputAddress' ) as HTMLInputElement; - const listener = oneEvent(input, 'change', false); + const listener = oneEvent(input, 'input', false); input.value = '0xebFC7A970CAAbC18C8e8b7367147C18FC7585'; // invalid eth address - input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('input')); await listener; - assert.equal(mockAddressChangeHandler.mock.calls.length, 2); + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); assert.deepEqual(mockAddressChangeHandler.mock.calls[1], ['']); @@ -278,4 +286,39 @@ describe('address-input component', function () { assert.equal(errorMessage.textContent, 'invalid Ethereum address'); }); + + it('displays error message when there is an address but we change network', async () => { + const mockAddressChangeHandler = vi.fn(); + + const el = await fixture(html` + + `); + + const input = el.shadowRoot!.querySelector( + '.inputAddress' + ) as HTMLInputElement; + + const listener = oneEvent(input, 'input', false); + input.value = '0xebFC7A970CAAbC18C8e8b7367147C18FC7585492'; + + input.dispatchEvent(new Event('input')); + + await listener; + + assert.equal(mockAddressChangeHandler.mock.calls.length, 3); + assert.deepEqual(mockAddressChangeHandler.mock.calls[0], ['']); + assert.deepEqual(mockAddressChangeHandler.mock.calls[1], ['']); + + el.networkType = Network.SUBSTRATE; + + await elementUpdated(el); + + const errorMessage = el.shadowRoot!.querySelector( + '.errorMessage' + ) as HTMLInputElement; + + assert.equal(errorMessage.textContent, 'invalid Substrate address'); + }); }); diff --git a/packages/widget/tests/unit/components/amount-selector/amount-selector.test.ts b/packages/widget/tests/unit/components/amount-selector/amount-selector.test.ts deleted file mode 100644 index 04d73842..00000000 --- a/packages/widget/tests/unit/components/amount-selector/amount-selector.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -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 { utils } from 'ethers'; -import { html } from 'lit'; -import { afterEach, assert, describe, expect, it, vi } from 'vitest'; -import { AmountSelector } from '../../../../src/components'; -import type { DropdownOption } from '../../../../src/components/common/dropdown/dropdown'; - -describe('Amount selector component - sygma-resource-selector', () => { - afterEach(() => { - fixtureCleanup(); - }); - - it('is defined', () => { - const el = document.createElement('sygma-resource-selector'); - assert.instanceOf(el, AmountSelector); - }); - - it('displays account balance correctly', async () => { - const el = await fixture( - html` ` - ); - el.tokenBalanceController.balance = utils.parseEther('100'); - el.requestUpdate(); - await el.updateComplete; - - const balanceDisplay = el.shadowRoot!.querySelector('.balanceContent span'); - assert.strictEqual(balanceDisplay!.textContent, '100.0000'); - }); - - it('useMax button works', async () => { - const el = await fixture( - html` ` - ); - el.tokenBalanceController.balance = utils.parseEther('100'); - el.requestUpdate(); - await el.updateComplete; - - const useMaxButton = - el.shadowRoot!.querySelector('.maxButton'); - useMaxButton?.click(); - await el.updateComplete; - assert.equal(el.amount, 100); - }); - - it('resets input and acc balance on resource change', async () => { - const mockOptionSelectHandler = vi.fn(); - const amount = '50'; - const resources: Resource[] = [ - { - resourceId: 'resourceId1', - address: 'address1', - symbol: 'PHA', - type: ResourceType.FUNGIBLE - } - ]; - - const dropdownOption: DropdownOption = { - name: 'Resource1', - value: { ...resources[0] } - }; - - const el = await fixture( - html`` - ); - - // Set amount - const input = el.shadowRoot!.querySelector( - '.amountSelectorInput' - ) as HTMLInputElement; - input.value = amount; - input.dispatchEvent(new Event('change', { bubbles: true, composed: true })); - await el.updateComplete; - - //set acc balance - el.tokenBalanceController.balance = utils.parseEther('100'); - el.requestUpdate(); - await el.updateComplete; - - el._onResourceSelectedHandler(dropdownOption); - await el.updateComplete; - - expect(el.amount).toEqual(0); - expect(el.tokenBalanceController.balance.toNumber()).toEqual(0); - expect(el.tokenBalanceController.decimals).toEqual(18); - }); - - it('calls onResourceSelected callback correctly', async () => { - const mockOptionSelectHandler = vi.fn(); - const amount = '50'; - const resources: Resource[] = [ - { - resourceId: 'resourceId1', - address: 'address1', - symbol: 'PHA', - type: ResourceType.FUNGIBLE - } - ]; - - const dropdownOption: DropdownOption = { - name: 'Resource1', - value: { ...resources[0] } - }; - - const el = await fixture( - html`` - ); - el._onResourceSelectedHandler(dropdownOption); - await el.updateComplete; - - el.tokenBalanceController.balance = utils.parseEther('100'); - el.requestUpdate(); - - // Set resource - await el.updateComplete; - // Set amount - const input = el.shadowRoot!.querySelector( - '.amountSelectorInput' - ) as HTMLInputElement; - input.value = amount; - input.dispatchEvent(new Event('change', { bubbles: true, composed: true })); - await el.updateComplete; - - expect(mockOptionSelectHandler).toHaveBeenCalledOnce(); - expect(mockOptionSelectHandler).toHaveBeenCalledWith( - el.selectedResource, - utils.parseEther(amount) - ); - }); - - describe('Validation', () => { - it('validates input amount when balance is low', async () => { - const el = await fixture( - html` ` - ); - - // input amount greater than balance - const input = el.shadowRoot!.querySelector( - '.amountSelectorInput' - ) as HTMLInputElement; - input.value = '150'; - input.dispatchEvent(new Event('change')); - await nextFrame(); - - const validationMessage = el.shadowRoot!.querySelector( - '.validationMessage' - ) as HTMLDivElement; - assert.strictEqual( - validationMessage.textContent, - 'Amount exceeds account balance' - ); - }); - - it('validates input when amount is less than zero', async () => { - const el = await fixture( - html` ` - ); - - // input amount less than zero - const input = el.shadowRoot!.querySelector( - '.amountSelectorInput' - ) as HTMLInputElement; - input.value = '-2'; - input.dispatchEvent(new Event('change')); - await el.updateComplete; - - const validationMessage = el.shadowRoot!.querySelector( - '.validationMessage' - ) as HTMLDivElement; - assert.strictEqual( - validationMessage.textContent, - 'Amount must be greater than 0' - ); - }); - }); -}); 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 new file mode 100644 index 00000000..e100fe69 --- /dev/null +++ b/packages/widget/tests/unit/components/resource-amount-selector/resource-amount-selector.test.ts @@ -0,0 +1,284 @@ +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 { utils } from 'ethers'; +import { html } from 'lit'; +import { afterEach, assert, describe, expect, it, vi } from 'vitest'; +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'; + +describe('Resource amount selector component - sygma-resource-amount-selector', () => { + afterEach(() => { + fixtureCleanup(); + }); + + const resources: Resource[] = [ + { + resourceId: 'resourceId1', + address: 'address1', + symbol: 'PHA', + type: ResourceType.FUNGIBLE + } + ]; + + it('is defined', () => { + const el = document.createElement('sygma-resource-amount-selector'); + assert.instanceOf(el, ResourceAmountSelector); + }); + + it('displays account balance correctly', async () => { + const el = await fixture( + html` ` + ); + el.tokenBalanceController.balance = utils.parseEther('5.000199'); + el.requestUpdate(); + await el.updateComplete; + + const balanceDisplay = el.shadowRoot!.querySelector('.balanceContent span'); + assert.strictEqual(balanceDisplay!.textContent, '5.0001'); + }); + + it('useMax button works', async () => { + const balance = '100'; + const mockOptionSelectHandler = vi.fn(); + const dropdownOption: DropdownOption = { + name: 'Resource1', + value: { ...resources[0] } + }; + + const el = await fixture( + html` ` + ); + + // Set Resource + el._onResourceSelectedHandler(dropdownOption); + await el.updateComplete; + + // Set Account balance + el.tokenBalanceController.balance = utils.parseEther(balance.toString()); + el.requestUpdate(); + await el.updateComplete; + + const useMaxButton = + el.shadowRoot!.querySelector('.maxButton'); + useMaxButton?.click(); + await el.updateComplete; + + assert.equal(el.amount, '100.0'); + expect(mockOptionSelectHandler).toHaveBeenCalledOnce(); + expect(mockOptionSelectHandler).toHaveBeenCalledWith( + el.selectedResource, + utils.parseEther(el.amount.toString()) + ); + }); + + it('resets input and acc balance on resource change', async () => { + const mockOptionSelectHandler = vi.fn(); + const amount = '50'; + + const dropdownOption: DropdownOption = { + name: 'Resource1', + value: { ...resources[0] } + }; + + const el = await fixture( + html`` + ); + + // Set amount + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = amount; + input.dispatchEvent(new Event('change', { bubbles: true, composed: true })); + await el.updateComplete; + + //set acc balance + el.tokenBalanceController.balance = utils.parseEther('100'); + el.requestUpdate(); + await el.updateComplete; + + el._onResourceSelectedHandler(dropdownOption); + await el.updateComplete; + + expect(el.amount).toEqual(''); + expect(el.tokenBalanceController.balance.toNumber()).toEqual(0); + expect(el.tokenBalanceController.decimals).toEqual(18); + }); + + it('calls onResourceSelected callback correctly', async () => { + const mockOptionSelectHandler = vi.fn(); + const amount = '50'; + const resources: Resource[] = [ + { + resourceId: 'resourceId1', + address: 'address1', + symbol: 'PHA', + type: ResourceType.FUNGIBLE + } + ]; + + const dropdownOption: DropdownOption = { + name: 'Resource1', + value: { ...resources[0] } + }; + + const el = await fixture( + html`` + ); + el._onResourceSelectedHandler(dropdownOption); + await el.updateComplete; + + el.tokenBalanceController.balance = utils.parseEther('100'); + el.requestUpdate(); + + // Set resource + await el.updateComplete; + // Set amount + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = amount; + input.dispatchEvent(new Event('input', { bubbles: true, composed: true })); + await el.updateComplete; + + expect(mockOptionSelectHandler).toHaveBeenCalledTimes(1); + expect(mockOptionSelectHandler).toHaveBeenCalledWith( + el.selectedResource, + utils.parseEther(amount) + ); + }); + + describe('Validation', () => { + it('validates input amount when balance is low', async () => { + const el = await fixture( + html` ` + ); + + // input amount greater than balance + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = '150'; + input.dispatchEvent(new Event('input')); + await nextFrame(); + + const validationMessage = el.shadowRoot!.querySelector( + '.validationMessage' + ) as HTMLDivElement; + assert.strictEqual( + validationMessage.textContent, + 'Amount exceeds account balance' + ); + }); + it('revalidates on account balance change', async () => { + const el = await fixture( + html` ` + ); + + // input amount greater than balance + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = '150'; + input.dispatchEvent(new Event('input')); + await nextFrame(); + + const validationMessage = el.shadowRoot!.querySelector( + '.validationMessage' + ) as HTMLDivElement; + assert.strictEqual( + validationMessage.textContent, + 'Amount exceeds account balance' + ); + + el.tokenBalanceController.balance = utils.parseUnits('400', 'ether'); + el.requestUpdate(BALANCE_UPDATE_KEY); + await el.updateComplete; + assert.isNull(el.shadowRoot!.querySelector('.validationMessage')); + }); + + it('revalidates on account balance change', async () => { + const el = await fixture( + html` ` + ); + + // input amount greater than balance + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = '150'; + input.dispatchEvent(new Event('input')); + await nextFrame(); + + const validationMessage = el.shadowRoot!.querySelector( + '.validationMessage' + ) as HTMLDivElement; + assert.strictEqual( + validationMessage.textContent, + 'Amount exceeds account balance' + ); + + el.tokenBalanceController.balance = utils.parseUnits('400', 'ether'); + el.requestUpdate(BALANCE_UPDATE_KEY); + await el.updateComplete; + assert.isNull(el.shadowRoot!.querySelector('.validationMessage')); + }); + + it('validates input when amount is less than zero', async () => { + const el = await fixture( + html` ` + ); + + // input amount less than zero + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = '-2'; + input.dispatchEvent(new Event('input')); + await el.updateComplete; + + const validationMessage = el.shadowRoot!.querySelector( + '.validationMessage' + ) as HTMLDivElement; + assert.strictEqual( + validationMessage.textContent, + 'Amount must be greater than 0' + ); + }); + + it('throw error when amount is NOT parseable', async () => { + const el = await fixture( + html` ` + ); + + // input amount with non-numeric value + const input = el.shadowRoot!.querySelector( + '.amountSelectorInput' + ) as HTMLInputElement; + input.value = 'nonParseableValue'; + input.dispatchEvent(new Event('input')); + await el.updateComplete; + + const validationMessage = el.shadowRoot!.querySelector( + '.validationMessage' + ) as HTMLDivElement; + + assert.strictEqual(el.amount, '0'); + assert.strictEqual( + validationMessage.textContent, + 'Amount must be greater than 0' + ); + }); + }); +}); 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 9e8b63e0..be01d1f7 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 @@ -68,8 +68,8 @@ describe('sygma-fungible-transfer-detail', function () { }); it('shows fee correctly', async () => { - const value = '1.0000 ETH'; - mockedFee.fee = parseUnits('1'); + const value = '1.02 ETH'; + mockedFee.fee = parseUnits('1.02', 18); const el = await fixture(html` { + fixtureCleanup(); + }); + + it('is defined', async () => { + const el = await fixture( + html` ` + ); + + assert.instanceOf(el, FungibleTokenTransfer); + }); + + it('Fill the destination address -> when networks types are the same', async () => { + const sourceNetwork: Domain = { + id: 2, + chainId: 11155111, + name: 'sepolia', + type: Network.EVM + }; + const destinationNetwork: Domain = { + id: 5, + chainId: 338, + name: 'cronos', + type: Network.EVM + }; + const connectedAddress = '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5'; + const walletContext = await fixture(html` + + `); + + walletContext.dispatchEvent( + new WalletUpdateEvent({ + evmWallet: { + address: connectedAddress, + providerChainId: 1, + provider: getMockedEvmWallet().provider + } + }) + ); + const fungibleTransfer = await fixture( + html` `, + { parentNode: walletContext } + ); + + // Set Source and Destination Networks + fungibleTransfer.transferController.onSourceNetworkSelected(sourceNetwork); + fungibleTransfer.transferController.onDestinationNetworkSelected( + destinationNetwork + ); + fungibleTransfer.requestUpdate(); + await fungibleTransfer.updateComplete; + + const sygmaAddressInput = fungibleTransfer.shadowRoot!.querySelector( + 'sygma-address-input' + ) as AddressInput; + + assert(sygmaAddressInput.address === connectedAddress); + }); + + it('Should NOT fill the destination address -> when networks type is substrate', async () => { + const walletContext = await fixture(html` + + `); + + walletContext.dispatchEvent( + new WalletUpdateEvent({ + substrateWallet: { + accounts: [ + { + address: '155EekKo19tWKAPonRFywNVsVduDegYChrDVsLE8HKhXzjqe' + } + ], + signer: {}, + signerAddress: '155EekK' + } + }) + ); + + const fungibleTransfer = await fixture( + html` `, + { parentNode: walletContext } + ); + + const sygmaAddressInput = fungibleTransfer.shadowRoot!.querySelector( + 'sygma-address-input' + ) as AddressInput; + + assert(sygmaAddressInput.address === ''); + }); +}); diff --git a/packages/widget/tests/unit/context/config.test.ts b/packages/widget/tests/unit/context/config.test.ts new file mode 100644 index 00000000..f5df0189 --- /dev/null +++ b/packages/widget/tests/unit/context/config.test.ts @@ -0,0 +1,47 @@ +import { ContextConsumer } from '@lit/context'; +import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; +import { LitElement, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { afterEach, assert, describe, it } from 'vitest'; +import { configContext, ConfigContextProvider } from '../../../src/context'; + +@customElement('my-element') +export class MyElement extends LitElement {} + +declare global { + interface HTMLElementTagNameMap { + 'my-element': MyElement; + } +} + +describe('config context provider', function () { + afterEach(() => { + fixtureCleanup(); + }); + + it('is defined', () => { + const el = document.createElement('sygma-config-context-provider'); + assert.instanceOf(el, ConfigContextProvider); + }); + + it('handles and provides config updates', async () => { + const contextProvider = await fixture(html` + + `); + const child = await fixture(html` `, { + parentNode: contextProvider + }); + const context = new ContextConsumer(child, { + context: configContext, + subscribe: true + }); + + assert.deepEqual(context.value, { + theme: undefined, + walletConnectOptions: undefined, + appMetaData: { name: 'My Dapp' } + }); + }); +}); diff --git a/packages/widget/tests/unit/controllers/transfers/fungible-token-transfer.test.ts b/packages/widget/tests/unit/controllers/transfers/fungible-token-transfer.test.ts new file mode 100644 index 00000000..8df1a8b7 --- /dev/null +++ b/packages/widget/tests/unit/controllers/transfers/fungible-token-transfer.test.ts @@ -0,0 +1,25 @@ +import { fixtureCleanup } from '@open-wc/testing-helpers'; +import { afterEach, assert, describe, it, vi } from 'vitest'; +import { LitElement } from 'lit'; +import { + FungibleTokenTransferController, + FungibleTransferState +} from '../../../../src/controllers/transfers/fungible-token-transfer'; + +describe('Amount selector component - sygma-resource-selector', () => { + afterEach(() => { + fixtureCleanup(); + }); + + it('should return completed state even though wallet is disconnected', function () { + const controller = new FungibleTokenTransferController( + vi.mocked(LitElement.prototype) + ); + controller.transferTransactionId = '0x'; + + assert.equal( + controller.getTransferState(), + FungibleTransferState.COMPLETED + ); + }); +}); diff --git a/packages/widget/tests/unit/setup.ts b/packages/widget/tests/unit/setup.ts new file mode 100644 index 00000000..602ba42e --- /dev/null +++ b/packages/widget/tests/unit/setup.ts @@ -0,0 +1,12 @@ +//remove annoying lit warnings +const warn = console.warn; +console.warn = (...msg) => { + if ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + !msg[0]?.includes('Lit is in dev mode') && + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + !msg[0]?.includes('change-in-update') + ) { + warn(...msg); + } +}; diff --git a/packages/widget/vite.config.ts b/packages/widget/vite.config.ts index 5fceec6c..88bbbdcd 100644 --- a/packages/widget/vite.config.ts +++ b/packages/widget/vite.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ }, test: { environment: 'happy-dom', + setupFiles: ['./tests/unit/setup.ts'], include: ['**/*.test.ts'], exclude: ['**/node_modules/**', '**/dist/**', '**/build/**'] } From d285f95842b2638de892dc3cdc3fffdcbcfb90ee Mon Sep 17 00:00:00 2001 From: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:06:58 +0500 Subject: [PATCH 07/22] feat: enable user to specify wallets (#161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add functionality to enable users to specify `onboard.js` wallet modules ## Description Add Widget property `wallets` that accepts list of `WalletInit[]` objects from 3rd party app ## Related Issue Or Context [Enable adding other web3 wallets](https://github.com/sygmaprotocol/sygma-widget/issues/87) Closes: #87 ## Types of changes - [x] `WalletController` now supports ability to specify onboard wallets --------- Co-authored-by: Filip Štoković <59089574+sztok7@users.noreply.github.com> --- packages/widget/src/context/config.ts | 9 ++++- .../src/controllers/wallet-manager/manager.ts | 37 ++++++++++++++----- packages/widget/src/widget.ts | 5 ++- .../widget/tests/unit/context/config.test.ts | 3 +- .../widget/tests/unit/context/wallet.test.ts | 4 +- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/widget/src/context/config.ts b/packages/widget/src/context/config.ts index 5355325a..34b676ee 100644 --- a/packages/widget/src/context/config.ts +++ b/packages/widget/src/context/config.ts @@ -3,7 +3,7 @@ import { createContext, ContextProvider } from '@lit/context'; import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; import type { HTMLTemplateResult, PropertyValues } from 'lit'; import { html } from 'lit'; -import type { AppMetadata } from '@web3-onboard/common'; +import type { AppMetadata, WalletInit } from '@web3-onboard/common'; import { BaseComponent } from '../components/common/base-component'; import type { Theme } from '../interfaces'; @@ -11,6 +11,7 @@ export interface ConfigContext { theme?: Theme; walletConnectOptions?: WalletConnectOptions; appMetaData?: AppMetadata; + walletModules?: WalletInit[]; } export const configContext = createContext( @@ -30,6 +31,9 @@ export class ConfigContextProvider extends BaseComponent { @property({ attribute: false, type: Object }) appMetadata?: AppMetadata; + @property({ type: Array }) + walletModules?: WalletInit[]; + @property({ attribute: false, type: Object }) theme?: Theme; @@ -39,7 +43,8 @@ export class ConfigContextProvider extends BaseComponent { this.configContextProvider.setValue({ theme: this.theme, walletConnectOptions: this.walletConnectOptions, - appMetaData: this.appMetadata + appMetaData: this.appMetadata, + walletModules: this.walletModules }); } diff --git a/packages/widget/src/controllers/wallet-manager/manager.ts b/packages/widget/src/controllers/wallet-manager/manager.ts index 4a580134..101b2acc 100644 --- a/packages/widget/src/controllers/wallet-manager/manager.ts +++ b/packages/widget/src/controllers/wallet-manager/manager.ts @@ -5,11 +5,10 @@ import type { Account } from '@polkadot-onboard/core'; import { InjectedWalletProvider } from '@polkadot-onboard/injected-wallets'; import Onboard from '@web3-onboard/core'; import injectedModule from '@web3-onboard/injected-wallets'; -import walletConnectModule from '@web3-onboard/walletconnect'; import type { ReactiveController, ReactiveElement } from 'lit'; +import type { WalletInit, AppMetadata } from '@web3-onboard/common'; import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; -import type { AppMetadata } from '@web3-onboard/common'; import { utils } from 'ethers'; import { WalletUpdateEvent, walletContext } from '../../context'; @@ -18,8 +17,31 @@ export class WalletController implements ReactiveController { walletContext: ContextConsumer; + /** + * Provides list of wallets specified by 3rd party + * along with default injected connector + * @param {{ dappUrl?: string }} options + * @returns {WalletInit[]} + */ + getWallets(options?: { + walletConnectOptions?: WalletConnectOptions; + walletModules?: WalletInit[]; + appMetaData?: AppMetadata; + }): WalletInit[] { + // always have injected ones + const injected = injectedModule(); + const wallets = [injected]; + + if (options?.walletModules?.length) { + wallets.push(...options.walletModules); + } + + return wallets; + } + constructor(host: ReactiveElement) { (this.host = host).addController(this); + this.walletContext = new ContextConsumer(host, { context: walletContext, subscribe: true @@ -40,6 +62,7 @@ export class WalletController implements ReactiveController { options?: { walletConnectOptions?: WalletConnectOptions; appMetaData?: AppMetadata; + walletModules?: WalletInit[]; } ): void => { switch (network.type) { @@ -103,17 +126,11 @@ export class WalletController implements ReactiveController { appMetaData?: AppMetadata; } ): Promise => { - const injected = injectedModule(); - - const walletSetup = [injected]; - - if (options?.walletConnectOptions?.projectId) { - walletSetup.push(walletConnectModule(options.walletConnectOptions)); - } + const walletsToConnect = this.getWallets(options); const onboard = Onboard({ appMetadata: options?.appMetaData, - wallets: walletSetup, + wallets: walletsToConnect, chains: [ { id: network.chainId diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 72655bad..be2fa4b9 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -12,7 +12,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; -import type { AppMetadata } from '@web3-onboard/common'; +import type { WalletInit, AppMetadata } from '@web3-onboard/common'; import { sygmaLogo } from './assets'; import './components'; import './components/address-input'; @@ -35,6 +35,8 @@ class SygmaProtocolWidget { static styles = styles; + @property({ type: Array }) walletModules?: WalletInit[]; + @property({ type: String }) environment?: Environment; @property({ type: Array }) whitelistedSourceNetworks?: string[]; @@ -100,6 +102,7 @@ class SygmaProtocolWidget .appMetadata=${this.appMetadata} .theme=${this.theme} .walletConnectOptions=${this.walletConnectOptions} + .walletModules=${this.walletModules} >
Date: Thu, 11 Apr 2024 09:09:23 -0400 Subject: [PATCH 08/22] feat: substrate fungible transfer (#134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description ## Related Issue Or Context Closes: #93 ## How Has This Been Tested? Testing details. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in sygma-docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot --------- Signed-off-by: Marin Petrunic Co-authored-by: Marin Petrunić Co-authored-by: Saad Ahmed Siddiqui Co-authored-by: Marin Petrunic --- packages/widget/package.json | 2 +- .../common/buttons/connect-wallet.ts | 4 +- .../resource-amount-selector.ts | 17 ++- .../substrate-account-selector.ts | 10 +- .../fungible/fungible-token-transfer.ts | 3 +- packages/widget/src/constants.ts | 18 +++ packages/widget/src/context/wallet.ts | 73 ++++++++++- .../src/controllers/transfers/evm/build.ts | 6 +- .../src/controllers/transfers/evm/execute.ts | 4 +- .../transfers/fungible-token-transfer.ts | 116 ++++++++++++++++-- .../controllers/transfers/substrate/build.ts | 39 ++++++ .../transfers/substrate/execute.ts | 52 ++++++++ .../controllers/transfers/substrate/index.ts | 2 + .../wallet-manager/token-balance.ts | 83 ++++++++++++- packages/widget/src/interfaces/index.ts | 2 +- packages/widget/src/utils/substrate.ts | 8 ++ packages/widget/src/widget.ts | 17 ++- .../tests/__mocks__/@polkadot/api/index.ts | 5 + .../address-input/address-input.test.ts | 1 + .../components/buttons/connect-wallet.test.ts | 2 + .../substrate-account-selector.test.ts | 4 +- .../fungible/fungible-token-transfer.test.ts | 4 +- .../widget/tests/unit/context/wallet.test.ts | 14 ++- yarn.lock | 10 +- 24 files changed, 449 insertions(+), 47 deletions(-) create mode 100644 packages/widget/src/controllers/transfers/substrate/build.ts create mode 100644 packages/widget/src/controllers/transfers/substrate/execute.ts create mode 100644 packages/widget/src/controllers/transfers/substrate/index.ts create mode 100644 packages/widget/src/utils/substrate.ts create mode 100644 packages/widget/tests/__mocks__/@polkadot/api/index.ts diff --git a/packages/widget/package.json b/packages/widget/package.json index b7fabfe2..6f5f8d06 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -29,7 +29,7 @@ "author": "Sygmaprotocol Product Team", "dependencies": { "@buildwithsygma/sygma-contracts": "^2.5.1", - "@buildwithsygma/sygma-sdk-core": "^2.7.2", + "@buildwithsygma/sygma-sdk-core": "^2.10.0", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.2", diff --git a/packages/widget/src/components/common/buttons/connect-wallet.ts b/packages/widget/src/components/common/buttons/connect-wallet.ts index 4f3dc3ae..4a11845b 100644 --- a/packages/widget/src/components/common/buttons/connect-wallet.ts +++ b/packages/widget/src/components/common/buttons/connect-wallet.ts @@ -62,11 +62,11 @@ export class ConnectWalletButton extends BaseComponent { }; private isWalletConnected(): boolean { - return !!this.wallets.evmWallet || !!this.wallets.substrateWallet; + return !!this.wallets.evmWallet || !!this.wallets.substrateWallet?.signer; } private renderConnectWalletButton(): HTMLTemplateResult | undefined { - if (this.wallets.substrateWallet) return; + if (this.wallets.substrateWallet?.signer) return; return when( this.isWalletConnected(), 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 bfc680a2..10a5c905 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 @@ -1,4 +1,9 @@ -import type { Resource } from '@buildwithsygma/sygma-sdk-core'; +import { + Network, + type EthereumConfig, + type Resource, + type SubstrateConfig +} from '@buildwithsygma/sygma-sdk-core'; import type { PropertyValues } from '@lit/reactive-element'; import { BigNumber, utils } from 'ethers'; import type { HTMLTemplateResult, PropertyDeclaration } from 'lit'; @@ -37,6 +42,9 @@ export class ResourceAmountSelector extends BaseComponent { onResourceSelected: (resource: Resource, amount: BigNumber) => void = () => {}; + @property({ type: Object }) + sourceDomainConfig?: EthereumConfig | SubstrateConfig; + @state() selectedResource: Resource | null = null; @state() validationMessage: string | null = null; @state() amount: string = ''; @@ -95,7 +103,12 @@ export class ResourceAmountSelector extends BaseComponent { if (option) { this.selectedResource = option.value; this.amount = ''; - this.tokenBalanceController.startBalanceUpdates(this.selectedResource); + this.tokenBalanceController.startBalanceUpdates( + this.selectedResource, + this.sourceDomainConfig?.type === Network.SUBSTRATE + ? this.sourceDomainConfig + : undefined + ); } else { this.selectedResource = null; this.tokenBalanceController.resetBalance(); diff --git a/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts b/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts index 206a6337..a4bcb1e4 100644 --- a/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts +++ b/packages/widget/src/components/substrate-account-selector/substrate-account-selector.ts @@ -29,8 +29,12 @@ export class SubstrateAccountSelector extends BaseComponent { }; private handleSubstrateAccountSelected = ( - option: DropdownOption - ): void => this.walletController.onSubstrateAccountSelected(option.value); + option?: DropdownOption + ): void => { + if (option) { + this.walletController.onSubstrateAccountSelected(option.value); + } + }; private renderDisconnectSubstrateButton(): HTMLTemplateResult | undefined { return html`
diff --git a/packages/widget/src/constants.ts b/packages/widget/src/constants.ts index 156d35bd..fd64309f 100644 --- a/packages/widget/src/constants.ts +++ b/packages/widget/src/constants.ts @@ -1,5 +1,23 @@ +import { Environment } from '@buildwithsygma/sygma-sdk-core'; +import type { ParachainID } from '@buildwithsygma/sygma-sdk-core/substrate'; + export const DEFAULT_ETH_DECIMALS = 18; export const MAINNET_EXPLORER_URL = 'https://scan.buildwithsygma.com/transfer/'; export const TESTNET_EXPLORER_URL = 'https://scan.test.buildwithsygma.com/transfer/'; + +type WsUrl = `ws://${string}` | `wss://${string}`; +export const SUBSTRATE_RPCS: { + [env in Environment]: Record; +} = { + [Environment.DEVNET]: {}, + [Environment.LOCAL]: {}, + [Environment.TESTNET]: { + 2004: 'wss://rhala-node.phala.network/ws' + }, + [Environment.MAINNET]: { + 2004: 'wss://rpc.helikon.io/khala', + 2035: 'wss://phala.api.onfinality.io/public-ws' + } +}; diff --git a/packages/widget/src/context/wallet.ts b/packages/widget/src/context/wallet.ts index b840f130..8058ec78 100644 --- a/packages/widget/src/context/wallet.ts +++ b/packages/widget/src/context/wallet.ts @@ -3,9 +3,14 @@ import type { Account, UnsubscribeFn } from '@polkadot-onboard/core'; import type { Signer } from '@polkadot/api/types'; import type { EIP1193Provider } from '@web3-onboard/core'; import type { HTMLTemplateResult } from 'lit'; +import { Environment } from '@buildwithsygma/sygma-sdk-core'; import { html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import type { ParachainID } from '@buildwithsygma/sygma-sdk-core/substrate'; import { BaseComponent } from '../components/common/base-component'; +import { SUBSTRATE_RPCS } from '../constants'; +import { fetchParachainId } from '../utils/substrate'; export interface EvmWallet { address: string; @@ -31,6 +36,12 @@ export enum WalletContextKeys { SUBSTRATE_WALLET = 'substrateWallet' } +export type ParachainProviders = Map; + +export interface SubstrateProviderContext { + substrateProviders?: ParachainProviders; +} + declare global { interface HTMLElementEventMap { walletUpdate: WalletUpdateEvent; @@ -41,6 +52,10 @@ export const walletContext = createContext( Symbol('sygma-wallet-context') ); +export const substrateProviderContext = createContext( + Symbol('substrate-provider-context') +); + export class WalletUpdateEvent extends CustomEvent { constructor(update: Partial) { super('walletUpdate', { detail: update, composed: true, bubbles: true }); @@ -54,14 +69,29 @@ export class WalletContextProvider extends BaseComponent { @provide({ context: walletContext }) private walletContext: WalletContext = {}; + @provide({ context: substrateProviderContext }) + substrateProviderContext: SubstrateProviderContext = {}; + @property({ attribute: false, type: Object }) evmWallet?: EvmWallet; - connectedCallback(): void { + @property({ attribute: false }) + substrateProviders?: Array = []; + + @property({ type: String }) environment?: Environment; + + async connectedCallback(): Promise { super.connectedCallback(); if (this.evmWallet) { this.walletContext.evmWallet = this.evmWallet; } + + const substrateProviders = await this.getSubstrateProviders(); + + this.substrateProviderContext = { + substrateProviders: substrateProviders + }; + this.addEventListener('walletUpdate', (event: WalletUpdateEvent) => { this.walletContext = { ...this.walletContext, @@ -123,6 +153,47 @@ export class WalletContextProvider extends BaseComponent { } }; + private async getSubstrateProviders(): Promise { + const substrateProviders: ParachainProviders = new Map(); + const specifiedProviders = this.substrateProviders ?? []; + const environment = this.environment ?? Environment.TESTNET; + + // create a id -> api map of all specified providers + for (const provider of specifiedProviders) { + try { + const parachainId = await fetchParachainId(provider); + console.log(`provided provider for ${parachainId}`); + substrateProviders.set(parachainId, provider); + } catch (error) { + console.error('unable to fetch parachain id'); + } + } + + // all chains hardcoded on ui + // and create their providers + // if not already specified by the user + const parachainIds = Object.keys(SUBSTRATE_RPCS[environment]); + for (const parachainId of parachainIds) { + console.log(`creating default provider for ${parachainId}`); + const _parachainId = parseInt(parachainId); + + if (!substrateProviders.has(_parachainId)) { + const rpcUrls = SUBSTRATE_RPCS[environment][_parachainId]; + const provider = new WsProvider(rpcUrls); + const api = new ApiPromise({ provider }); + + try { + await api.isReady; + substrateProviders.set(_parachainId, api); + } catch (error) { + console.error('api error'); + } + } + } + + return substrateProviders; + } + protected render(): HTMLTemplateResult { return html``; } diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index 30b096c2..7b640955 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -41,7 +41,9 @@ export async function buildEvmFungibleTransactions( transfer, this.fee ); - this.pendingEvmTransferTransaction = - await evmTransfer.buildTransferTransaction(transfer, this.fee); + this.pendingTransferTransaction = await evmTransfer.buildTransferTransaction( + transfer, + this.fee + ); this.host.requestUpdate(); } diff --git a/packages/widget/src/controllers/transfers/evm/execute.ts b/packages/widget/src/controllers/transfers/evm/execute.ts index 1b8230e4..a759f206 100644 --- a/packages/widget/src/controllers/transfers/evm/execute.ts +++ b/packages/widget/src/controllers/transfers/evm/execute.ts @@ -43,13 +43,13 @@ export async function executeNextEvmTransaction( this.host.requestUpdate(); try { const tx = await signer.sendTransaction( - this.pendingEvmTransferTransaction! as TransactionRequest + this.pendingTransferTransaction! as TransactionRequest ); this.waitingUserConfirmation = false; this.waitingTxExecution = true; this.host.requestUpdate(); const receipt = await tx.wait(); - this.pendingEvmTransferTransaction = undefined; + this.pendingTransferTransaction = undefined; this.transferTransactionId = receipt.transactionHash; } catch (e) { console.log(e); diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index b46a9818..b0c37902 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -16,10 +16,28 @@ import { ContextConsumer } from '@lit/context'; import type { UnsignedTransaction, BigNumber } from 'ethers'; import { ethers } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; -import type { WalletContext } from '../../context'; +import type { SubmittableExtrinsic } from '@polkadot/api/types'; +import type { ApiPromise, SubmittableResult } from '@polkadot/api'; +import type { + ParachainID, + SubstrateFee +} from '@buildwithsygma/sygma-sdk-core/substrate'; import { walletContext } from '../../context'; import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; + +import { SdkInitializedEvent } from '../../interfaces'; +import { substrateProviderContext } from '../../context/wallet'; +import type { WalletContext } from '../../context'; import { buildEvmFungibleTransactions, executeNextEvmTransaction } from './evm'; +import { + buildSubstrateFungibleTransactions, + executeNextSubstrateTransaction +} from './substrate'; + +export type SubstrateTransaction = SubmittableExtrinsic< + 'promise', + SubmittableResult +>; export enum FungibleTransferState { MISSING_SOURCE_NETWORK, @@ -52,13 +70,19 @@ export class FungibleTokenTransferController implements ReactiveController { public supportedSourceNetworks: Domain[] = []; public supportedDestinationNetworks: Domain[] = []; public supportedResources: Resource[] = []; - public fee?: EvmFee; + public fee: EvmFee | SubstrateFee | null = null; //Evm transfer protected buildEvmTransactions = buildEvmFungibleTransactions; protected executeNextEvmTransaction = executeNextEvmTransaction; protected pendingEvmApprovalTransactions: UnsignedTransaction[] = []; - protected pendingEvmTransferTransaction?: UnsignedTransaction; + protected pendingTransferTransaction?: + | UnsignedTransaction + | SubstrateTransaction; + + // Substrate transfer + protected buildSubstrateTransactions = buildSubstrateFungibleTransactions; + protected executeSubstrateTransaction = executeNextSubstrateTransaction; protected config: Config; protected env: Environment = Environment.MAINNET; @@ -67,6 +91,10 @@ export class FungibleTokenTransferController implements ReactiveController { host: ReactiveElement; walletContext: ContextConsumer; + substrateProviderContext: ContextConsumer< + typeof substrateProviderContext, + ReactiveElement + >; isWalletDisconnected(context: WalletContext): boolean { // Skip the method call during init @@ -75,6 +103,29 @@ export class FungibleTokenTransferController implements ReactiveController { return !(!!context.evmWallet || !!context.substrateWallet); } + get sourceSubstrateProvider(): ApiPromise | undefined { + if (this.sourceNetwork && this.sourceNetwork.type === Network.SUBSTRATE) { + const domainConfig = this.config.getDomainConfig( + this.sourceNetwork.id + ) as SubstrateConfig; + return this.getSubstrateProvider(domainConfig.parachainId as ParachainID); + } + + return undefined; + } + + /** + * Provides substrate provider + * based on parachain id + * @param {ParachainId} parachainId + * @returns {ApiPromise | undefined} + */ + getSubstrateProvider(parachainId: ParachainID): ApiPromise | undefined { + return this.substrateProviderContext.value?.substrateProviders?.get( + parachainId + ); + } + constructor(host: ReactiveElement) { (this.host = host).addController(this); this.config = new Config(); @@ -95,6 +146,11 @@ export class FungibleTokenTransferController implements ReactiveController { } } }); + + this.substrateProviderContext = new ContextConsumer(host, { + context: substrateProviderContext, + subscribe: true + }); } get sourceDomainConfig(): EthereumConfig | SubstrateConfig | undefined { if (this.config.environment && this.sourceNetwork) { @@ -107,27 +163,45 @@ export class FungibleTokenTransferController implements ReactiveController { this.reset(); } + /** + * Infinite Try/catch wrapper around + * {@link Config} from `@buildwithsygma/sygma-sdk-core` + * and emits a {@link SdkInitializedEvent} + * @param {number} time to wait before retrying request in ms + * @returns {void} + */ + async retryInitSdk(retryMs = 100): Promise { + try { + await this.config.init(1, this.env); + this.host.dispatchEvent( + new SdkInitializedEvent({ hasInitialized: true }) + ); + } catch (error) { + setTimeout(() => { + this.retryInitSdk(retryMs * 2).catch(console.error); + }, retryMs); + } + } + async init(env: Environment): Promise { this.host.requestUpdate(); this.env = env; + await this.retryInitSdk(); await this.config.init(1, this.env); - this.supportedSourceNetworks = this.config - .getDomains() - //remove once we have proper substrate transfer support - .filter((n) => n.type === Network.EVM); + this.supportedSourceNetworks = this.config.getDomains(); this.supportedDestinationNetworks = this.config.getDomains(); this.host.requestUpdate(); } resetFee(): void { - this.fee = undefined; + this.fee = null; } reset(): void { this.sourceNetwork = undefined; this.destinationNetwork = undefined; this.pendingEvmApprovalTransactions = []; - this.pendingEvmTransferTransaction = undefined; + this.pendingTransferTransaction = undefined; this.destinationAddress = ''; this.waitingTxExecution = false; this.waitingUserConfirmation = false; @@ -185,7 +259,7 @@ export class FungibleTokenTransferController implements ReactiveController { this.destinationAddress = address; if (this.destinationAddress.length === 0) { this.pendingEvmApprovalTransactions = []; - this.pendingEvmTransferTransaction = undefined; + this.pendingTransferTransaction = undefined; } void this.buildTransactions(); this.host.requestUpdate(); @@ -204,7 +278,7 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.pendingEvmApprovalTransactions.length > 0) { return FungibleTransferState.PENDING_APPROVALS; } - if (this.pendingEvmTransferTransaction) { + if (this.pendingTransferTransaction) { return FungibleTransferState.PENDING_TRANSFER; } if (!this.sourceNetwork) { @@ -235,6 +309,22 @@ export class FungibleTokenTransferController implements ReactiveController { ) { return FungibleTransferState.WRONG_CHAIN; } + if (this.waitingUserConfirmation) { + return FungibleTransferState.WAITING_USER_CONFIRMATION; + } + if (this.waitingTxExecution) { + return FungibleTransferState.WAITING_TX_EXECUTION; + } + if (this.transferTransactionId) { + return FungibleTransferState.COMPLETED; + } + if (this.pendingEvmApprovalTransactions.length > 0) { + return FungibleTransferState.PENDING_APPROVALS; + } + if (this.pendingTransferTransaction) { + return FungibleTransferState.PENDING_TRANSFER; + } + return FungibleTransferState.UNKNOWN; } @@ -251,7 +341,7 @@ export class FungibleTokenTransferController implements ReactiveController { break; case Network.SUBSTRATE: { - //TODO: add substrate logic + void this.executeSubstrateTransaction(); } break; default: @@ -341,7 +431,7 @@ export class FungibleTokenTransferController implements ReactiveController { break; case Network.SUBSTRATE: { - //TODO: add substrate logic + void this.buildSubstrateTransactions(); } break; default: diff --git a/packages/widget/src/controllers/transfers/substrate/build.ts b/packages/widget/src/controllers/transfers/substrate/build.ts new file mode 100644 index 00000000..edbff776 --- /dev/null +++ b/packages/widget/src/controllers/transfers/substrate/build.ts @@ -0,0 +1,39 @@ +import { SubstrateAssetTransfer } from '@buildwithsygma/sygma-sdk-core/substrate'; +import { type FungibleTokenTransferController } 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 + ) { + return; + } + + const substrateTransfer = new SubstrateAssetTransfer(); + await substrateTransfer.init(substrateProvider, this.env); + + const transfer = await substrateTransfer.createFungibleTransfer( + address, + this.destinationNetwork.chainId, + this.destinationAddress, + this.selectedResource.resourceId, + String(this.resourceAmount) + ); + + this.fee = await substrateTransfer.getFee(transfer); + this.pendingTransferTransaction = substrateTransfer.buildTransferTransaction( + transfer, + this.fee + ); + this.host.requestUpdate(); +} diff --git a/packages/widget/src/controllers/transfers/substrate/execute.ts b/packages/widget/src/controllers/transfers/substrate/execute.ts new file mode 100644 index 00000000..a5d631a6 --- /dev/null +++ b/packages/widget/src/controllers/transfers/substrate/execute.ts @@ -0,0 +1,52 @@ +import type { + FungibleTokenTransferController, + SubstrateTransaction +} from '../fungible-token-transfer'; + +export async function executeNextSubstrateTransaction( + this: FungibleTokenTransferController +): Promise { + this.errorMessage = null; + const destinationAddress = this.destinationAddress; + const sender = this.walletContext.value?.substrateWallet?.signerAddress; + const signer = this.walletContext.value?.substrateWallet?.signer; + if ( + this.pendingTransferTransaction === undefined || + destinationAddress == undefined || + sender == undefined || + this.sourceNetwork === undefined + ) + return; + + const provider = this.sourceSubstrateProvider; + this.waitingTxExecution = true; + await (this.pendingTransferTransaction as SubstrateTransaction).signAndSend( + sender, + { signer: signer }, + ({ blockNumber, txIndex, status, dispatchError }) => { + if (status.isInBlock) { + this.waitingTxExecution = false; + this.pendingTransferTransaction = undefined; + this.transferTransactionId = `${blockNumber?.toString()}-${txIndex?.toString()}`; + this.host.requestUpdate(); + } + + if (status.isBroadcast) { + this.waitingUserConfirmation = false; + this.host.requestUpdate(); + } + + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = provider?.registry.findMetaError( + dispatchError.asModule + ); + const [docs] = decoded?.docs || ['Transfer failed']; + this.errorMessage = docs; + this.waitingTxExecution = false; + this.host.requestUpdate(); + } + } + } + ); +} diff --git a/packages/widget/src/controllers/transfers/substrate/index.ts b/packages/widget/src/controllers/transfers/substrate/index.ts new file mode 100644 index 00000000..af297692 --- /dev/null +++ b/packages/widget/src/controllers/transfers/substrate/index.ts @@ -0,0 +1,2 @@ +export { buildSubstrateFungibleTransactions } from './build'; +export { executeNextSubstrateTransaction } from './execute'; diff --git a/packages/widget/src/controllers/wallet-manager/token-balance.ts b/packages/widget/src/controllers/wallet-manager/token-balance.ts index 8876c4cd..9905a38e 100644 --- a/packages/widget/src/controllers/wallet-manager/token-balance.ts +++ b/packages/widget/src/controllers/wallet-manager/token-balance.ts @@ -1,13 +1,22 @@ import { ERC20__factory } from '@buildwithsygma/sygma-contracts'; -import type { EvmResource, Resource } from '@buildwithsygma/sygma-sdk-core'; +import type { + EthereumConfig, + EvmResource, + Resource, + SubstrateConfig, + SubstrateResource +} from '@buildwithsygma/sygma-sdk-core'; import { ResourceType } from '@buildwithsygma/sygma-sdk-core'; import { Web3Provider } from '@ethersproject/providers'; import { ContextConsumer } from '@lit/context'; -import { ethers } from 'ethers'; -import type { BigNumber } from 'ethers'; +import { ethers, BigNumber } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; +import type { ParachainID } from '@buildwithsygma/sygma-sdk-core/substrate'; +import { getAssetBalance } from '@buildwithsygma/sygma-sdk-core/substrate'; import { walletContext } from '../../context'; import { isEvmResource } from '../../utils'; +import { substrateProviderContext } from '../../context/wallet'; +import type { SubstrateWallet } from '../../context/wallet'; const BALANCE_REFRESH_MS = 5_000; @@ -17,6 +26,10 @@ export class TokenBalanceController implements ReactiveController { host: ReactiveElement; walletContext: ContextConsumer; + substrateProviderContext: ContextConsumer< + typeof substrateProviderContext, + ReactiveElement + >; loadingBalance: boolean = true; @@ -32,6 +45,10 @@ export class TokenBalanceController implements ReactiveController { context: walletContext, subscribe: true }); + this.substrateProviderContext = new ContextConsumer(host, { + context: substrateProviderContext, + subscribe: true + }); } hostConnected(): void {} @@ -40,12 +57,16 @@ export class TokenBalanceController implements ReactiveController { clearInterval(this.timeout); } - startBalanceUpdates(resource: Resource): void { + startBalanceUpdates( + resource: Resource, + sourceDomainConfig?: EthereumConfig | SubstrateConfig + ): void { if (this.timeout) { clearInterval(this.timeout); } this.balance = ethers.constants.Zero; this.host.requestUpdate(); + if (isEvmResource(resource)) { if (resource.type === ResourceType.FUNGIBLE) { //trigger so we don't wait BALANCE_REFRESH_MS before displaying balance @@ -60,12 +81,29 @@ export class TokenBalanceController implements ReactiveController { //resource.native is not set :shrug: if (resource.symbol === 'eth') { void this.subscribeEvmNativeBalanceUpdate(); + this.timeout = setInterval( this.subscribeEvmNativeBalanceUpdate, BALANCE_REFRESH_MS ); return; } + } else { + const config = sourceDomainConfig as SubstrateConfig; + if (config.parachainId) { + const params = { + resource, + parachainId: config.parachainId as ParachainID + }; + void this.suscribeSubstrateBalanceUpdate(params); + this.timeout = setInterval( + this.suscribeSubstrateBalanceUpdate, + BALANCE_REFRESH_MS, + params + ); + return; + } + throw new Error('parachainId unavailable'); } throw new Error('Unsupported resource'); } @@ -110,4 +148,41 @@ export class TokenBalanceController implements ReactiveController { this.host.requestUpdate(); }.bind(this)(); }; + + suscribeSubstrateBalanceUpdate = (params: { + resource: SubstrateResource; + parachainId: ParachainID; + }): void => { + const { resource, parachainId } = params; + + const substrateProvider = + this.substrateProviderContext.value?.substrateProviders?.get(parachainId); + const { signerAddress } = this.walletContext.value + ?.substrateWallet as SubstrateWallet; + + if (!substrateProvider) { + console.error('substrate provider unavailable'); + return; + } + + void async function (this: TokenBalanceController) { + try { + this.loadingBalance = true; + this.host.requestUpdate(); + const tokenBalance = await getAssetBalance( + substrateProvider, + resource.assetID as number, + signerAddress + ); + this.loadingBalance = false; + this.balance = BigNumber.from(tokenBalance.balance.toString()); + this.decimals = resource.decimals!; + this.host.requestUpdate(BALANCE_UPDATE_KEY); + } catch (e) { + console.error("Failed to fetch account's token balance", e); + this.loadingBalance = false; + this.host.requestUpdate(); + } + }.bind(this)(); + }; } diff --git a/packages/widget/src/interfaces/index.ts b/packages/widget/src/interfaces/index.ts index ab60867f..eb9032f9 100644 --- a/packages/widget/src/interfaces/index.ts +++ b/packages/widget/src/interfaces/index.ts @@ -28,7 +28,7 @@ export interface ISygmaProtocolWidget { whitelistedSourceNetworks?: string[]; whitelistedDestinationNetworks?: string[]; evmProvider?: Eip1193Provider; - substrateProvider?: ApiPromise | string; + substrateProviders?: Array; substrateSigner?: Signer; show?: boolean; whitelistedSourceResources?: Array; diff --git a/packages/widget/src/utils/substrate.ts b/packages/widget/src/utils/substrate.ts new file mode 100644 index 00000000..f3ca2298 --- /dev/null +++ b/packages/widget/src/utils/substrate.ts @@ -0,0 +1,8 @@ +import type { ApiPromise } from '@polkadot/api'; +import type { u32 } from '@polkadot/types-codec'; + +export async function fetchParachainId(api: ApiPromise): Promise { + // TODO: use polkadot type augmentation to remove "as 32" + const parachainId = await api.query.parachainInfo.parachainId(); + return (parachainId as u32).toNumber(); +} diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index be2fa4b9..307bba83 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -24,7 +24,8 @@ import './context/wallet'; import type { Eip1193Provider, ISygmaProtocolWidget, - Theme + Theme, + SdkInitializedEvent } from './interfaces'; import { styles } from './styles'; @@ -45,7 +46,7 @@ class SygmaProtocolWidget @property({ type: Object }) evmProvider?: Eip1193Provider; - @property() substrateProvider?: ApiPromise | string; + @property({ type: Array }) substrateProviders?: Array; @property({ type: Object }) substrateSigner?: Signer; @@ -70,6 +71,9 @@ class SygmaProtocolWidget @state() private isLoading = false; + @state() + private sdkInitialized = false; + @state() private sourceNetwork?: Domain; @@ -104,7 +108,10 @@ class SygmaProtocolWidget .walletConnectOptions=${this.walletConnectOptions} .walletModules=${this.walletModules} > - +
@@ -114,6 +121,8 @@ class SygmaProtocolWidget
+ (this.sdkInitialized = event.detail.hasInitialized)} .environment=${this.environment as Environment} .onSourceNetworkSelected=${(domain: Domain) => (this.sourceNetwork = domain)} @@ -124,7 +133,7 @@ class SygmaProtocolWidget
${sygmaLogo} Powered by Sygma
${when( - this.isLoading, + this.isLoading || !this.sdkInitialized, () => html`` )}
diff --git a/packages/widget/tests/__mocks__/@polkadot/api/index.ts b/packages/widget/tests/__mocks__/@polkadot/api/index.ts new file mode 100644 index 00000000..617f6bff --- /dev/null +++ b/packages/widget/tests/__mocks__/@polkadot/api/index.ts @@ -0,0 +1,5 @@ +export default class ApiPromise { + get isReady(): Promise { + return Promise.resolve(true); + } +} diff --git a/packages/widget/tests/unit/components/address-input/address-input.test.ts b/packages/widget/tests/unit/components/address-input/address-input.test.ts index 61eef08d..fc1caabd 100644 --- a/packages/widget/tests/unit/components/address-input/address-input.test.ts +++ b/packages/widget/tests/unit/components/address-input/address-input.test.ts @@ -50,6 +50,7 @@ describe('address-input component', function () { let el = await fixture(html` diff --git a/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts b/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts index 978aaa16..33e9bace 100644 --- a/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts +++ b/packages/widget/tests/unit/components/buttons/connect-wallet.test.ts @@ -14,6 +14,8 @@ import { } from '../../../../src/context'; import { getMockedEvmWallet } from '../../../utils'; +vi.mock('@polkadot/api'); + describe('connect-wallet button', function () { afterEach(() => { fixtureCleanup(); diff --git a/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts b/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts index 0ff855ec..75dce2e1 100644 --- a/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts +++ b/packages/widget/tests/unit/components/substrate-account-selector/substrate-account-selector.test.ts @@ -1,5 +1,5 @@ import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; -import { afterEach, assert, describe, it } from 'vitest'; +import { afterEach, assert, describe, it, vi } from 'vitest'; import { html } from 'lit'; import type { ConnectWalletButton } from '../../../../src/components'; @@ -8,6 +8,8 @@ import type { WalletContextProvider } from '../../../../src/context'; import { WalletUpdateEvent } from '../../../../src/context'; import type { Dropdown } from '../../../../src/components/common'; +vi.mock('@polkadot/api'); + describe('Substrate account selector component', function () { afterEach(() => { fixtureCleanup(); diff --git a/packages/widget/tests/unit/components/transfer/fungible/fungible-token-transfer.test.ts b/packages/widget/tests/unit/components/transfer/fungible/fungible-token-transfer.test.ts index eb71e99e..eaa0099a 100644 --- a/packages/widget/tests/unit/components/transfer/fungible/fungible-token-transfer.test.ts +++ b/packages/widget/tests/unit/components/transfer/fungible/fungible-token-transfer.test.ts @@ -1,5 +1,5 @@ import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; -import { afterEach, assert, describe, it } from 'vitest'; +import { afterEach, assert, describe, it, vi } from 'vitest'; import { html } from 'lit'; import type { Domain } from '@buildwithsygma/sygma-sdk-core'; import { Network } from '@buildwithsygma/sygma-sdk-core'; @@ -9,6 +9,8 @@ import type { WalletContextProvider } from '../../../../../src/context'; import { WalletUpdateEvent } from '../../../../../src/context'; import { getMockedEvmWallet } from '../../../../utils'; +vi.mock('@polkadot/api'); + describe('Fungible token Transfer', function () { afterEach(() => { fixtureCleanup(); diff --git a/packages/widget/tests/unit/context/wallet.test.ts b/packages/widget/tests/unit/context/wallet.test.ts index e6173828..36ef9654 100644 --- a/packages/widget/tests/unit/context/wallet.test.ts +++ b/packages/widget/tests/unit/context/wallet.test.ts @@ -2,7 +2,7 @@ import { ContextConsumer } from '@lit/context'; import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { afterEach, assert, describe, it } from 'vitest'; +import { afterEach, assert, describe, it, vi } from 'vitest'; import { WalletContextProvider, WalletUpdateEvent, @@ -11,6 +11,8 @@ import { import type { EvmWallet } from '../../../src/context'; import { getMockedEvmWallet } from '../../utils'; +vi.mock('@polkadot/api'); + @customElement('my-element') export class MyElement extends LitElement {} @@ -52,8 +54,12 @@ describe('wallet context provider', function () { new WalletUpdateEvent({ evmWallet: fakeEvmWallet }) ); - assert.deepEqual(context.value, { - evmWallet: fakeEvmWallet - }); + assert.deepEqual(context.value, { evmWallet: fakeEvmWallet }); + + contextProvider.dispatchEvent( + new WalletUpdateEvent({ evmWallet: undefined }) + ); + + assert.deepEqual(context.value, { evmWallet: undefined }); }); }); diff --git a/yarn.lock b/yarn.lock index c57221f5..e718f972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -313,9 +313,9 @@ __metadata: languageName: node linkType: hard -"@buildwithsygma/sygma-sdk-core@npm:^2.7.2": - version: 2.7.2 - resolution: "@buildwithsygma/sygma-sdk-core@npm:2.7.2" +"@buildwithsygma/sygma-sdk-core@npm:^2.10.0": + version: 2.10.0 + resolution: "@buildwithsygma/sygma-sdk-core@npm:2.10.0" dependencies: "@buildwithsygma/sygma-contracts": "npm:2.5.1" "@ethersproject/abi": "npm:^5.7.0" @@ -331,7 +331,7 @@ __metadata: "@polkadot/util": "npm:12.2.1" "@polkadot/util-crypto": "npm:12.2.1" ethers: "npm:5.6.2" - checksum: fbfdffd22eee5fad947e453b31d7d2263127f653c6bf8d7ccaaaff19ef5aa0f892e0c509a35b1a0f09910bb5d7a0e7584dd19d6aa30c3d30d55187b02d681e12 + checksum: fec17e1a40e57e196db2237efbd1fc4308a17d207131d840c0cd4a6ea756e55506f0a8c9b1388d190dbdf2019444c8786888e9fb9c7285b78bcc703ba67704e2 languageName: node linkType: hard @@ -357,7 +357,7 @@ __metadata: resolution: "@buildwithsygma/sygmaprotocol-widget@workspace:packages/widget" dependencies: "@buildwithsygma/sygma-contracts": "npm:^2.5.1" - "@buildwithsygma/sygma-sdk-core": "npm:^2.7.2" + "@buildwithsygma/sygma-sdk-core": "npm:^2.10.0" "@ethersproject/abstract-signer": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.2" From 2b3276af14740c8a5910dfd2579ac38439adc9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Petruni=C4=87?= Date: Thu, 11 Apr 2024 16:02:16 +0200 Subject: [PATCH 09/22] chore: optimize eslint (#165) ## Description import/no-cycle rule was parsing dependencies causing slow eslint execution. With this change ci/lint will run a lot faster Before: ![image](https://github.com/sygmaprotocol/sygma-widget/assets/8836210/4c9dd540-9b99-422d-a34e-9130ab1cce7c) After: ![image](https://github.com/sygmaprotocol/sygma-widget/assets/8836210/b1c7c031-ab21-4433-8d41-78556eff82f1) ## Related Issue Or Context Closes: # ## How Has This Been Tested? Testing details. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in sygma-docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot Signed-off-by: Marin Petrunic --- .eslintrc.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 85071cb0..b265bd04 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -6,5 +6,8 @@ "project": ["'./packages/*/tsconfig.json'", "'./examples/*/tsconfig.json'"], "ecmaVersion": 2020, "sourceType": "module" + }, + "rules": { + "import/no-cycle": ["error", {"ignoreExternal": true }] } } \ No newline at end of file From 044797e7197dcd16d6dde9615b23b75147215fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s?= Date: Tue, 16 Apr 2024 09:37:55 -0400 Subject: [PATCH 10/22] chore: bring changes from main to dev (#173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - brings last changes from main into dev. I had to cherry pick those commits, because since the last merge, doing a PR from main to dev resulted in a lot of conflicts and duplicated changes. - Main has two commits that weren't included in the last merge. One of those commits is needed for #168 ## Related Issue Or Context Closes: # ## How Has This Been Tested? Testing details. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in sygma-docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot --------- Signed-off-by: Marin Petrunic Co-authored-by: Marin Petrunić Co-authored-by: Anton Lykhoyda --- .../components/common/dropdown/dropdown.ts | 34 ++++++++++++++- .../widget/src/components/common/index.ts | 1 + .../network-selector/network-selector.ts | 6 ++- .../fungible/fungible-token-transfer.ts | 3 +- .../fungible/transfer-status/styles.ts | 7 ++-- .../src/controllers/transfers/evm/build.ts | 42 ++++++++++++++++++- .../transfers/fungible-token-transfer.ts | 6 ++- 7 files changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/widget/src/components/common/dropdown/dropdown.ts b/packages/widget/src/components/common/dropdown/dropdown.ts index 9ee56fa3..9a232926 100644 --- a/packages/widget/src/components/common/dropdown/dropdown.ts +++ b/packages/widget/src/components/common/dropdown/dropdown.ts @@ -40,14 +40,30 @@ export class Dropdown extends BaseComponent { @property({ type: Object }) actionOption: HTMLTemplateResult | null = null; + @property({ type: String }) + preSelectedOption = ''; + @state() selectedOption: DropdownOption | null = null; @property({ attribute: false }) onOptionSelected: (option?: DropdownOption) => void = () => {}; + _setPreselectedOption = (): void => { + if (this.preSelectedOption) { + const newOption = + this.options.find((o) => o.name === this.preSelectedOption) || null; + + if (newOption) { + this.selectedOption = newOption; + this.onOptionSelected(newOption); + } + } + }; + connectedCallback(): void { super.connectedCallback(); + this._setPreselectedOption(); addEventListener('click', this._handleOutsideClick); } @@ -58,14 +74,28 @@ export class Dropdown extends BaseComponent { updated(changedProperties: PropertyValues): void { super.updated(changedProperties); + + // Set pre-selected option after transfer is completed + if ( + changedProperties.has('options') && + this.preSelectedOption && + this.selectedOption?.name !== this.preSelectedOption + ) { + this._setPreselectedOption(); + } + //if options changed, check if we have selected option that doesn't exist if (changedProperties.has('options') && this.selectedOption) { if ( Array.isArray(this.options) && !this.options.map((o) => o.value).includes(this.selectedOption.value) ) { - this.selectedOption = null; - this.onOptionSelected(undefined); + if (this.preSelectedOption) { + this._setPreselectedOption(); + } else { + this.selectedOption = null; + this.onOptionSelected(undefined); + } } } } diff --git a/packages/widget/src/components/common/index.ts b/packages/widget/src/components/common/index.ts index 11560cfa..3c56acd7 100644 --- a/packages/widget/src/components/common/index.ts +++ b/packages/widget/src/components/common/index.ts @@ -2,3 +2,4 @@ export { Button } from './buttons/button'; export { ConnectWalletButton } from './buttons/connect-wallet'; export { Dropdown } from './dropdown/dropdown'; export { OverlayComponent } from './overlay-component'; +export { BaseComponent } from './base-component'; diff --git a/packages/widget/src/components/network-selector/network-selector.ts b/packages/widget/src/components/network-selector/network-selector.ts index 8f00e381..09472dd5 100644 --- a/packages/widget/src/components/network-selector/network-selector.ts +++ b/packages/widget/src/components/network-selector/network-selector.ts @@ -5,7 +5,7 @@ import { customElement, property } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { networkIconsMap } from '../../assets'; -import { BaseComponent } from '../common/base-component'; +import { BaseComponent } from '../common'; import type { DropdownOption } from '../common/dropdown/dropdown'; import { styles } from './styles'; @@ -28,6 +28,9 @@ export class NetworkSelector extends BaseComponent { @property({ type: String }) direction?: Direction; + @property({ type: String }) + selectedNetwork?: string; + @property({ attribute: false }) onNetworkSelected: (option?: Domain) => void = () => {}; @@ -55,6 +58,7 @@ export class NetworkSelector extends BaseComponent { render(): HTMLTemplateResult { return html`
{}}>
{ diff --git a/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts b/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts index 9de9abf7..2e846fa4 100644 --- a/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts +++ b/packages/widget/src/components/transfer/fungible/transfer-status/styles.ts @@ -16,7 +16,8 @@ export const styles = css` flex-direction: column; align-items: center; border-radius: 1.5rem; - border: 0.0625rem solid var(--zinc-200); + border: 1px solid var(--zinc-200); + margin-bottom: 1rem; } .destinationMessage { @@ -36,7 +37,7 @@ export const styles = css` display: flex; flex-direction: row; align-items: center; - padding: 0rem 4.78125rem 0.9375rem 4.78125rem; + padding: 0 4.78125rem 0.9375rem 4.78125rem; color: var(--zinc-600); text-align: center; @@ -53,9 +54,7 @@ export const styles = css` align-items: center; justify-content: center; text-align: center; - color: var(--zinc-500); - text-align: center; font-size: 0.75rem; font-style: normal; font-weight: 400; diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index 7b640955..a85c375d 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -1,5 +1,10 @@ -import { EVMAssetTransfer } from '@buildwithsygma/sygma-sdk-core'; +import { + EVMAssetTransfer, + FeeHandlerType, + type PercentageFee +} from '@buildwithsygma/sygma-sdk-core'; import { Web3Provider } from '@ethersproject/providers'; +import { constants, utils } from 'ethers'; import { type FungibleTokenTransferController } from '../fungible-token-transfer'; /** @@ -29,12 +34,45 @@ export async function buildEvmFungibleTransactions( const evmTransfer = new EVMAssetTransfer(); await evmTransfer.init(new Web3Provider(provider, providerChaiId), this.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() + ); + const originalFee = await evmTransfer.getFee(originalTransfer); + //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) { + const { lowerBound, upperBound, percentage } = originalFee as PercentageFee; + const userInputAmount = this.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; + //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); + } + //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); + } + } + const transfer = await evmTransfer.createFungibleTransfer( address, this.destinationNetwork.chainId, this.destinationAddress, this.selectedResource.resourceId, - String(this.resourceAmount) + this.resourceAmount.toString() ); this.fee = await evmTransfer.getFee(transfer); this.pendingEvmApprovalTransactions = await evmTransfer.buildApprovals( diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index b0c37902..8875c5b4 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -197,8 +197,10 @@ export class FungibleTokenTransferController implements ReactiveController { this.fee = null; } - reset(): void { - this.sourceNetwork = undefined; + reset({ omitSourceNetworkReset } = { omitSourceNetworkReset: false }): void { + if (!omitSourceNetworkReset) { + this.sourceNetwork = undefined; + } this.destinationNetwork = undefined; this.pendingEvmApprovalTransactions = []; this.pendingTransferTransaction = undefined; From 1fc3b221e43e999c74d1098adc397c1600daa6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s?= Date: Tue, 16 Apr 2024 10:05:23 -0400 Subject: [PATCH 11/22] feat: automatically add unknown source evm network into wallet (#162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Whenever user switches to a unsuported network, upon swtich error we allow the user to add the network ## Related Issue Or Context Closes: #132 ## How Has This Been Tested? Testing details. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in sygma-docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot --------- Signed-off-by: Marin Petrunic Co-authored-by: Marin Petrunić Co-authored-by: Filip Štoković <59089574+sztok7@users.noreply.github.com> Co-authored-by: Anton Lykhoyda --- .../fungible/fungible-token-transfer.ts | 20 ++--- packages/widget/src/constants.ts | 1 + .../src/controllers/wallet-manager/manager.ts | 75 ++++++++++++++++--- 3 files changed, 77 insertions(+), 19 deletions(-) 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 a782191b..b7af1da3 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -5,6 +5,7 @@ import { html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import '../../../context/wallet'; import { choose } from 'lit/directives/choose.js'; +import type { Eip1193Provider } from 'packages/widget/src/interfaces'; import { FungibleTokenTransferController, FungibleTransferState @@ -58,15 +59,10 @@ export class FungibleTokenTransfer extends BaseComponent { break; case FungibleTransferState.WRONG_CHAIN: { - this.walletController.switchChain( - this.transferController.sourceNetwork!.chainId - ); - } - break; - case FungibleTransferState.COMPLETED: - { - this.walletController.switchChain( - this.transferController.sourceNetwork!.chainId + void this.walletController.switchEvmChain( + this.transferController.sourceNetwork!.chainId, + this.transferController.walletContext.value?.evmWallet + ?.provider as Eip1193Provider ); } break; @@ -107,7 +103,11 @@ export class FungibleTokenTransfer extends BaseComponent { if (network) { this.onSourceNetworkSelected?.(network); this.transferController.onSourceNetworkSelected(network); - void this.walletController.switchChain(network?.chainId); + void this.walletController.switchEvmChain( + network?.chainId, + this.transferController.walletContext.value?.evmWallet + ?.provider as Eip1193Provider + ); } }} .networks=${this.transferController.supportedSourceNetworks} diff --git a/packages/widget/src/constants.ts b/packages/widget/src/constants.ts index fd64309f..76a2706b 100644 --- a/packages/widget/src/constants.ts +++ b/packages/widget/src/constants.ts @@ -6,6 +6,7 @@ export const DEFAULT_ETH_DECIMALS = 18; export const MAINNET_EXPLORER_URL = 'https://scan.buildwithsygma.com/transfer/'; export const TESTNET_EXPLORER_URL = 'https://scan.test.buildwithsygma.com/transfer/'; +export const CHAIN_ID_URL = 'https://chainid.network/chains_mini.json'; type WsUrl = `ws://${string}` | `wss://${string}`; export const SUBSTRATE_RPCS: { diff --git a/packages/widget/src/controllers/wallet-manager/manager.ts b/packages/widget/src/controllers/wallet-manager/manager.ts index 101b2acc..276aa0aa 100644 --- a/packages/widget/src/controllers/wallet-manager/manager.ts +++ b/packages/widget/src/controllers/wallet-manager/manager.ts @@ -11,6 +11,20 @@ import type { WalletInit, AppMetadata } from '@web3-onboard/common'; import type { WalletConnectOptions } from '@web3-onboard/walletconnect/dist/types'; import { utils } from 'ethers'; import { WalletUpdateEvent, walletContext } from '../../context'; +import type { Eip1193Provider } from '../../interfaces'; +import { CHAIN_ID_URL } from '../../constants'; + +/** + * This is a stripped version of the response that is returned from chainId service + */ +type ChainData = { + chainId: number; + name: string; + rpc: string[]; + nativeCurrency: { name: string; symbol: string; decimals: number }; +}; + +type ChainDataResponse = Array; export class WalletController implements ReactiveController { host: ReactiveElement; @@ -110,12 +124,27 @@ export class WalletController implements ReactiveController { } }; - switchChain(chainId: number): void { + async switchEvmChain( + chainId: number, + provider: Eip1193Provider + ): Promise { if (this.walletContext.value?.evmWallet) { - void this.walletContext.value.evmWallet.provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: utils.hexValue(chainId) }] - }); + try { + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: utils.hexValue(chainId) }] + }); + } catch (switchError) { + const chainData = (await ( + await fetch(CHAIN_ID_URL) + ).json()) as ChainDataResponse; + + const selectedChain = chainData.find( + (chain) => chain.chainId === chainId + ) as ChainData; + + void this.addEvmChain(selectedChain, provider); + } } } @@ -160,10 +189,7 @@ export class WalletController implements ReactiveController { }) ); if (network.chainId !== providerChainId) { - void provider.request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: utils.hexValue(network.chainId) }] - }); + await this.switchEvmChain(network.chainId, provider); } this.host.requestUpdate(); } @@ -249,6 +275,37 @@ export class WalletController implements ReactiveController { } }; + private async addEvmChain( + chainData: ChainData, + provider: Eip1193Provider + ): Promise { + const { + chainId, + name, + nativeCurrency: { name: tokenName, symbol, decimals }, + rpc + } = chainData; + try { + await provider.request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: `0x${chainId.toString(16)}`, + chainName: name, + rpcUrls: [rpc[0]], + nativeCurrency: { + name: tokenName, + symbol: symbol, + decimals: decimals + } + } + ] + }); + } catch (addEvmError) { + console.error('Failed to add evm network into wallet', addEvmError); + } + } + onSubstrateAccountSelected = (account: Account): void => { if (this.walletContext.value?.substrateWallet) { this.host.dispatchEvent( From 21ceb181f3cd621f3327c3bd83f861f8a52fc90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s?= Date: Thu, 18 Apr 2024 11:52:40 -0400 Subject: [PATCH 12/22] feat: display amount to be received on destination (#168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - display amount to receive in destination ## Related Issue Or Context Closes: #156 ## How Has This Been Tested? Testing details. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in sygma-docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot --------- Signed-off-by: Marin Petrunic Co-authored-by: Marin Petrunić Co-authored-by: Filip Štoković <59089574+sztok7@users.noreply.github.com> Co-authored-by: Anton Lykhoyda Co-authored-by: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Co-authored-by: mj52951 <116341045+mj52951@users.noreply.github.com> Co-authored-by: mj52951 --- .../fungible/fungible-token-transfer.ts | 29 ++++++++++++++++++- .../components/transfer/fungible/styles.ts | 10 +++++++ .../transfers/fungible-token-transfer.ts | 16 +++++----- .../controllers/transfers/substrate/build.ts | 2 ++ .../src/controllers/wallet-manager/manager.ts | 1 - packages/widget/src/widget.ts | 2 +- 6 files changed, 49 insertions(+), 11 deletions(-) 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 b7af1da3..9f1c097e 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -5,6 +5,7 @@ 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 { FungibleTokenTransferController, @@ -16,9 +17,10 @@ import '../../resource-amount-selector'; import './transfer-button'; import './transfer-status'; import '../../network-selector'; +import { BaseComponent } from '../../common'; import { Directions } from '../../network-selector/network-selector'; import { WalletController } from '../../../controllers'; -import { BaseComponent } from '../../common/base-component'; +import { tokenBalanceToNumber } from '../../../utils/token'; import { styles } from './styles'; @customElement('sygma-fungible-transfer') @@ -73,6 +75,28 @@ 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! + )} + ${symbol} + +
+ `; + } + return null; + } + renderTransferStatus(): HTMLTemplateResult { return html`
+ ${when(this.transferController.destinationAddress, () => + this.renderAmountOnDestination() + )} ; - isWalletDisconnected(context: WalletContext): boolean { - // Skip the method call during init - if (Object.values(context).length === 0) return false; - - return !(!!context.evmWallet || !!context.substrateWallet); - } - get sourceSubstrateProvider(): ApiPromise | undefined { if (this.sourceNetwork && this.sourceNetwork.type === Network.SUBSTRATE) { const domainConfig = this.config.getDomainConfig( @@ -126,6 +119,13 @@ export class FungibleTokenTransferController implements ReactiveController { ); } + isWalletDisconnected(context: WalletContext): boolean { + // Skip the method call during init + if (Object.values(context).length === 0) return false; + + return !(!!context.evmWallet || !!context.substrateWallet); + } + constructor(host: ReactiveElement) { (this.host = host).addController(this); this.config = new Config(); diff --git a/packages/widget/src/controllers/transfers/substrate/build.ts b/packages/widget/src/controllers/transfers/substrate/build.ts index edbff776..ab9427f1 100644 --- a/packages/widget/src/controllers/transfers/substrate/build.ts +++ b/packages/widget/src/controllers/transfers/substrate/build.ts @@ -31,6 +31,8 @@ export async function buildSubstrateFungibleTransactions( ); this.fee = await substrateTransfer.getFee(transfer); + + this.resourceAmount = this.resourceAmount.sub(this.fee.fee.toString()); this.pendingTransferTransaction = substrateTransfer.buildTransferTransaction( transfer, this.fee diff --git a/packages/widget/src/controllers/wallet-manager/manager.ts b/packages/widget/src/controllers/wallet-manager/manager.ts index 276aa0aa..514f3cc8 100644 --- a/packages/widget/src/controllers/wallet-manager/manager.ts +++ b/packages/widget/src/controllers/wallet-manager/manager.ts @@ -147,7 +147,6 @@ export class WalletController implements ReactiveController { } } } - connectEvmWallet = async ( network: Domain, options?: { diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 307bba83..1fc57f3f 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -106,9 +106,9 @@ class SygmaProtocolWidget .appMetadata=${this.appMetadata} .theme=${this.theme} .walletConnectOptions=${this.walletConnectOptions} - .walletModules=${this.walletModules} > From 7114f62d1aa525a664096d198bc3c9aaf50edac6 Mon Sep 17 00:00:00 2001 From: Saad Ahmed <48211799+saadjhk@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:54:07 +0500 Subject: [PATCH 13/22] feat: display transfer gas cost estimation (#174) ## Description Display gas estimation of transactions that will be executed ## Related Issue Or Context Closes: #157 ## How Has This Been Tested? Testing details. - [x] Mocked gas fee test ## Types of changes - [x] Transactions gas fee estimations for evm and substrate networks ## Checklist: - [x] Add tests. --- .../fungible/fungible-token-transfer.ts | 2 + .../fungible/transfer-detail/styles.ts | 6 ++ .../transfer-detail/transfer-detail.ts | 64 +++++++++++++++++-- .../src/controllers/transfers/evm/build.ts | 2 + .../src/controllers/transfers/evm/execute.ts | 1 + .../controllers/transfers/evm/gas-estimate.ts | 31 +++++++++ .../transfers/fungible-token-transfer.ts | 61 +++++++++++++++++- .../controllers/transfers/substrate/build.ts | 3 + .../transfer-detail/transfer-detail.test.ts | 26 +++++++- 9 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 packages/widget/src/controllers/transfers/evm/gas-estimate.ts 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 9f1c097e..cd2f8982 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -15,6 +15,7 @@ import '../../common/buttons/button'; import '../../address-input'; import '../../resource-amount-selector'; import './transfer-button'; +import './transfer-detail'; import './transfer-status'; import '../../network-selector'; import { BaseComponent } from '../../common'; @@ -171,6 +172,7 @@ export class FungibleTokenTransfer extends BaseComponent { this.renderAmountOnDestination() )} ; - getFeeParams(type: FeeHandlerType): { decimals?: number; symbol: string } { + @property({ type: Object }) + estimatedGasFee?: BigNumber; + + getSygmaFeeParams(type: FeeHandlerType): { + decimals?: number; + symbol: string; + } { let decimals = undefined; let symbol = ''; @@ -49,28 +57,70 @@ export class FungibleTransferDetail extends BaseComponent { } } - getFee(): string { + getGasFeeParams(): { + decimals?: number; + symbol: string; + } { + let decimals = undefined; + let symbol = ''; + if (this.sourceDomainConfig) { + decimals = Number(this.sourceDomainConfig.nativeTokenDecimals); + symbol = this.sourceDomainConfig.nativeTokenSymbol.toUpperCase(); + } + return { decimals, symbol }; + } + + getSygmaFee(): string { if (!this.fee) return ''; - const { symbol, decimals } = this.getFeeParams(this.fee.type); + const { symbol, decimals } = this.getSygmaFeeParams(this.fee.type); const { fee } = this.fee; let _fee = ''; if (decimals) { - _fee = tokenBalanceToNumber(fee, decimals, 4); + // * BigNumber.from(fee.toString()) from + // * substrate gas + // * hex doesn't start with 0x :shrug: + _fee = tokenBalanceToNumber(BigNumber.from(fee.toString()), decimals, 4); } return `${_fee} ${symbol}`; } + getEstimatedGasFee(): string { + if (!this.estimatedGasFee) return ''; + const { symbol, decimals } = this.getGasFeeParams(); + + if (decimals && this.estimatedGasFee) { + const gasFee = tokenBalanceToNumber(this.estimatedGasFee, decimals, 4); + return `${gasFee} ${symbol}`; + } + + return 'calculating...'; + } + render(): HTMLTemplateResult { return html`
${when( - this.fee !== undefined, + this.fee !== null, () => html`
Bridge Fee
-
${this.getFee()}
+
+ ${this.getSygmaFee()} +
+
` + )} + ${when( + this.estimatedGasFee !== undefined, + () => + html`
+
+ Gas Fee +
+
+ ${this.getEstimatedGasFee()} +
` )}
diff --git a/packages/widget/src/controllers/transfers/evm/build.ts b/packages/widget/src/controllers/transfers/evm/build.ts index a85c375d..f4150ce0 100644 --- a/packages/widget/src/controllers/transfers/evm/build.ts +++ b/packages/widget/src/controllers/transfers/evm/build.ts @@ -28,6 +28,7 @@ export async function buildEvmFungibleTransactions( !address || providerChaiId !== this.sourceNetwork.chainId ) { + this.estimatedGas = undefined; this.resetFee(); return; } @@ -83,5 +84,6 @@ export async function buildEvmFungibleTransactions( transfer, this.fee ); + await this.estimateGas(); this.host.requestUpdate(); } diff --git a/packages/widget/src/controllers/transfers/evm/execute.ts b/packages/widget/src/controllers/transfers/evm/execute.ts index a759f206..64131ec5 100644 --- a/packages/widget/src/controllers/transfers/evm/execute.ts +++ b/packages/widget/src/controllers/transfers/evm/execute.ts @@ -28,6 +28,7 @@ export async function executeNextEvmTransaction( this.host.requestUpdate(); await tx.wait(); this.pendingEvmApprovalTransactions.shift(); + await this.estimateGas(); } catch (e) { console.log(e); this.errorMessage = 'Approval transaction reverted or rejected'; diff --git a/packages/widget/src/controllers/transfers/evm/gas-estimate.ts b/packages/widget/src/controllers/transfers/evm/gas-estimate.ts new file mode 100644 index 00000000..53625deb --- /dev/null +++ b/packages/widget/src/controllers/transfers/evm/gas-estimate.ts @@ -0,0 +1,31 @@ +import { Web3Provider } from '@ethersproject/providers'; +import type { EIP1193Provider } from '@web3-onboard/core'; +import { ethers, type BigNumber, type PopulatedTransaction } from 'ethers'; + +/** + * This method calculate the amount of gas + * list of transactions will cost + * @param {number} chainId blockchain ID + * @param {Eip1193Provider} eip1193Provider EIP compatible provider + * @param {string} sender address of signer connected with provider + * @param {PopulatedTransaction[]} transactions list of EVM transactions + * @returns {Promise} gas cost in 18 decimals // or chain native decimals + */ +export async function estimateEvmTransactionsGasCost( + chainId: number, + eip1193Provider: EIP1193Provider, + sender: string, + transactions: PopulatedTransaction[] +): Promise { + const provider = new Web3Provider(eip1193Provider, chainId); + const signer = provider.getSigner(sender); + + let cost = ethers.constants.Zero; + for (const transaction of transactions) { + const _cost = await signer.estimateGas(transaction); + cost = cost.add(_cost); + } + + const gasPrice = await provider.getGasPrice(); + return gasPrice.mul(cost); +} diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index d9ac38c2..0b30c19c 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -13,8 +13,8 @@ import { getRoutes } from '@buildwithsygma/sygma-sdk-core'; import { ContextConsumer } from '@lit/context'; -import type { UnsignedTransaction, BigNumber } from 'ethers'; -import { ethers } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; +import type { UnsignedTransaction, PopulatedTransaction } from 'ethers'; import type { ReactiveController, ReactiveElement } from 'lit'; import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { ApiPromise, SubmittableResult } from '@polkadot/api'; @@ -33,6 +33,7 @@ import { buildSubstrateFungibleTransactions, executeNextSubstrateTransaction } from './substrate'; +import { estimateEvmTransactionsGasCost } from './evm/gas-estimate'; export type SubstrateTransaction = SubmittableExtrinsic< 'promise', @@ -71,6 +72,7 @@ export class FungibleTokenTransferController implements ReactiveController { public supportedDestinationNetworks: Domain[] = []; public supportedResources: Resource[] = []; public fee: EvmFee | SubstrateFee | null = null; + public estimatedGas: BigNumber | undefined; //Evm transfer protected buildEvmTransactions = buildEvmFungibleTransactions; @@ -208,6 +210,7 @@ export class FungibleTokenTransferController implements ReactiveController { this.waitingTxExecution = false; this.waitingUserConfirmation = false; this.transferTransactionId = undefined; + this.estimatedGas = undefined; this.resetFee(); void this.init(this.env); } @@ -440,4 +443,58 @@ export class FungibleTokenTransferController implements ReactiveController { 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); + 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 ab9427f1..38a36031 100644 --- a/packages/widget/src/controllers/transfers/substrate/build.ts +++ b/packages/widget/src/controllers/transfers/substrate/build.ts @@ -16,6 +16,8 @@ export async function buildSubstrateFungibleTransactions( !substrateProvider || !address ) { + this.estimatedGas = undefined; + this.resetFee(); return; } @@ -37,5 +39,6 @@ export async function buildSubstrateFungibleTransactions( transfer, this.fee ); + await this.estimateGas(); this.host.requestUpdate(); } 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 be01d1f7..08fce4d2 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 @@ -11,8 +11,9 @@ import { Network, ResourceType } from '@buildwithsygma/sygma-sdk-core'; +import type { BigNumber } from 'ethers'; import { constants } from 'ethers'; -import { parseUnits } from 'ethers/lib/utils'; +import { parseEther, parseUnits } from 'ethers/lib/utils'; import { FungibleTransferDetail } from '../../../../src/components/transfer/fungible/transfer-detail'; describe('sygma-fungible-transfer-detail', function () { @@ -42,6 +43,8 @@ describe('sygma-fungible-transfer-detail', function () { resources: [] }; + const mockedEstimatedGas: BigNumber = parseEther('0.0004'); + afterEach(() => { fixtureCleanup(); }); @@ -67,7 +70,7 @@ describe('sygma-fungible-transfer-detail', function () { assert.isNotNull(transferDetail); }); - it('shows fee correctly', async () => { + it('shows sygma fee correctly', async () => { const value = '1.02 ETH'; mockedFee.fee = parseUnits('1.02', 18); @@ -85,4 +88,23 @@ describe('sygma-fungible-transfer-detail', function () { assert.include(transferDetail.innerHTML, value); }); + + it('shows gas fee correctly', async () => { + const MOCKED_GAS = '0.0004 ETH'; + + const el = await fixture(html` + + `); + + const valueElements = el.shadowRoot!.querySelector( + '#gasFeeValue' + ) as HTMLElement; + + assert.equal(valueElements.textContent?.trim(), MOCKED_GAS); + }); }); From f7d4a350eeca9a13cca70184b02ceac8e7967836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 10:05:28 -0400 Subject: [PATCH 14/22] chore: pr review --- .../src/components/address-input/address-input.ts | 2 +- .../controllers/transfers/fungible-token-transfer.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/widget/src/components/address-input/address-input.ts b/packages/widget/src/components/address-input/address-input.ts index 85a21c75..08487297 100644 --- a/packages/widget/src/components/address-input/address-input.ts +++ b/packages/widget/src/components/address-input/address-input.ts @@ -78,7 +78,7 @@ export class AddressInput extends BaseComponent { } }} @input=${(evt: Event) => - this.handleAddressChange((evt.target as HTMLInputElement).value)} + this.handleAddressChange((evt.target as HTMLTextAreaElement).value)} >
`; diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index c623c009..07545d20 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -68,7 +68,7 @@ export class FungibleTokenTransferController implements ReactiveController { public destinationNetwork?: Domain; public selectedResource?: Resource; public resourceAmount: BigNumber = ethers.constants.Zero; - public destinationAddress: string = ''; + public destinationAddress?: string | null = ''; public supportedSourceNetworks: Domain[] = []; public supportedDestinationNetworks: Domain[] = []; @@ -265,18 +265,23 @@ export class FungibleTokenTransferController implements ReactiveController { onDestinationAddressChange = (address: string): void => { this.destinationAddress = address; - if (this.destinationAddress.length === 0) { + + if (this.destinationAddress && this.destinationAddress.length === 0) { this.pendingEvmApprovalTransactions = []; this.pendingTransferTransaction = undefined; + this.destinationAddress = null; } void this.buildTransactions(); this.host.requestUpdate(); }; getTransferState(): FungibleTransferState { + // Enabled state if (this.transferTransactionId) { return FungibleTransferState.COMPLETED; } + + // Loading states if (this.waitingUserConfirmation) { return FungibleTransferState.WAITING_USER_CONFIRMATION; } @@ -289,6 +294,8 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.pendingTransferTransaction) { return FungibleTransferState.PENDING_TRANSFER; } + + // Error States if (!this.sourceNetwork) { return FungibleTransferState.MISSING_SOURCE_NETWORK; } From 145dfb91088c03acef57c7ec9c84840317c6502d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 10:12:11 -0400 Subject: [PATCH 15/22] chore: restoring comment and adjusting property --- .../widget/src/components/address-input/address-input.ts | 5 ++--- .../src/controllers/transfers/fungible-token-transfer.ts | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/widget/src/components/address-input/address-input.ts b/packages/widget/src/components/address-input/address-input.ts index 08487297..9c716512 100644 --- a/packages/widget/src/components/address-input/address-input.ts +++ b/packages/widget/src/components/address-input/address-input.ts @@ -22,9 +22,7 @@ export class AddressInput extends BaseComponent { @property({ attribute: false }) onAddressChange: (address: string) => void = () => {}; - @property({ - type: String - }) + @property({ attribute: false }) networkType: Network = Network.EVM; @state() @@ -48,6 +46,7 @@ export class AddressInput extends BaseComponent { } this.errorMessage = validateAddress(trimedValue, this.networkType); + this.onAddressChange(trimedValue); }; diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index 07545d20..2bffbb1d 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -324,6 +324,8 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.destinationAddress === '') { return FungibleTransferState.MISSING_DESTINATION_ADDRESS; } + + // Enabled States if ( !this.walletContext.value?.evmWallet && !this.walletContext.value?.substrateWallet From 8f750e7e8221e318f2fbdf8348807ef8cee600f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 10:13:52 -0400 Subject: [PATCH 16/22] chore: removing duplicated --- .../src/controllers/transfers/fungible-token-transfer.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index 2bffbb1d..c50ed445 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -306,10 +306,6 @@ export class FungibleTokenTransferController implements ReactiveController { return FungibleTransferState.MISSING_RESOURCE; } - if (this.destinationAddress === '') { - return FungibleTransferState.MISSING_DESTINATION_ADDRESS; - } - if ( this.destinationAddress === null || this.destinationAddress === undefined || From 34a203de26543564c9262b54601847c84bc6a910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 10:15:11 -0400 Subject: [PATCH 17/22] chore: changing place for if for consistency --- .../src/controllers/transfers/fungible-token-transfer.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index c50ed445..48c3fd83 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -306,6 +306,10 @@ export class FungibleTokenTransferController implements ReactiveController { return FungibleTransferState.MISSING_RESOURCE; } + if (this.destinationAddress === '') { + return FungibleTransferState.MISSING_DESTINATION_ADDRESS; + } + if ( this.destinationAddress === null || this.destinationAddress === undefined || @@ -317,9 +321,6 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.resourceAmount.eq(0)) { return FungibleTransferState.MISSING_RESOURCE_AMOUNT; } - if (this.destinationAddress === '') { - return FungibleTransferState.MISSING_DESTINATION_ADDRESS; - } // Enabled States if ( From 3114cad64051ccc69e83c0912940684399f7f1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 10:17:48 -0400 Subject: [PATCH 18/22] chore: remove another duplicate --- .../src/controllers/transfers/fungible-token-transfer.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index 48c3fd83..aba17ada 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -288,9 +288,6 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.waitingTxExecution) { return FungibleTransferState.WAITING_TX_EXECUTION; } - if (this.pendingEvmApprovalTransactions.length > 0) { - return FungibleTransferState.PENDING_APPROVALS; - } if (this.pendingTransferTransaction) { return FungibleTransferState.PENDING_TRANSFER; } From 3d9a527eb59773999a3f84a3336ba49d8a7075a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 11:09:32 -0400 Subject: [PATCH 19/22] chore: removing unnecesary retry since we do init in the line below --- .../widget/src/controllers/transfers/fungible-token-transfer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index aba17ada..56c50e69 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -190,7 +190,6 @@ export class FungibleTokenTransferController implements ReactiveController { async init(env: Environment): Promise { this.host.requestUpdate(); this.env = env; - await this.retryInitSdk(); await this.config.init(1, this.env); this.supportedSourceNetworks = this.config.getDomains(); this.supportedDestinationNetworks = this.config.getDomains(); From 075f2c568fc50a62e98119848edb9ac3cd2111cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Wed, 24 Apr 2024 17:00:46 -0400 Subject: [PATCH 20/22] chore: restoring function call --- .../widget/src/controllers/transfers/fungible-token-transfer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index 56c50e69..aba17ada 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -190,6 +190,7 @@ export class FungibleTokenTransferController implements ReactiveController { async init(env: Environment): Promise { this.host.requestUpdate(); this.env = env; + await this.retryInitSdk(); await this.config.init(1, this.env); this.supportedSourceNetworks = this.config.getDomains(); this.supportedDestinationNetworks = this.config.getDomains(); From cc60afc9a21461ecb1b9226fac6cb7acd4c24a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Mon, 29 Apr 2024 19:38:34 -0400 Subject: [PATCH 21/22] chore: restoring if clause --- .../src/controllers/transfers/fungible-token-transfer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index aba17ada..c6708bab 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -288,6 +288,10 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.waitingTxExecution) { return FungibleTransferState.WAITING_TX_EXECUTION; } + if (this.pendingEvmApprovalTransactions.length > 0) { + return FungibleTransferState.PENDING_APPROVALS; + } + if (this.pendingTransferTransaction) { return FungibleTransferState.PENDING_TRANSFER; } From 18227c8f4dcd01e4fd7415b6407898d95c65f639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Riquelme=20Guzm=C3=A1n?= Date: Mon, 29 Apr 2024 20:17:21 -0400 Subject: [PATCH 22/22] chore: removing duplicate if clause --- .../src/controllers/transfers/fungible-token-transfer.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index c6708bab..a70fd1d9 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -291,7 +291,6 @@ export class FungibleTokenTransferController implements ReactiveController { if (this.pendingEvmApprovalTransactions.length > 0) { return FungibleTransferState.PENDING_APPROVALS; } - if (this.pendingTransferTransaction) { return FungibleTransferState.PENDING_TRANSFER; } @@ -338,13 +337,6 @@ export class FungibleTokenTransferController implements ReactiveController { return FungibleTransferState.WRONG_CHAIN; } - if (this.pendingEvmApprovalTransactions.length > 0) { - return FungibleTransferState.PENDING_APPROVALS; - } - if (this.pendingTransferTransaction) { - return FungibleTransferState.PENDING_TRANSFER; - } - return FungibleTransferState.UNKNOWN; }