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 cd2f8982..1e64abe1 100644 --- a/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts +++ b/packages/widget/src/components/transfer/fungible/fungible-token-transfer.ts @@ -7,6 +7,7 @@ import '../../../context/wallet'; import { choose } from 'lit/directives/choose.js'; import { when } from 'lit/directives/when.js'; import type { Eip1193Provider } from 'packages/widget/src/interfaces'; +import type { PropertyValues } from '@lit/reactive-element'; import { FungibleTokenTransferController, FungibleTransferState @@ -28,11 +29,18 @@ import { styles } from './styles'; export class FungibleTokenTransfer extends BaseComponent { static styles = styles; - @property({ type: Array }) whitelistedSourceResources?: Array; - @property({ type: String }) environment?: Environment = Environment.MAINNET; + @property({ type: Object }) + whitelistedSourceNetworks?: string[]; + + @property({ type: Object }) + whitelistedDestinationNetworks?: string[]; + + @property({ type: Object }) + whitelistedSourceResources?: string[]; + @property({ type: Object }) onSourceNetworkSelected?: (domain: Domain) => void; @@ -41,7 +49,26 @@ export class FungibleTokenTransfer extends BaseComponent { connectedCallback(): void { super.connectedCallback(); - void this.transferController.init(this.environment!); + void this.transferController.init(this.environment!, { + whitelistedSourceNetworks: this.whitelistedSourceNetworks, + whitelistedDestinationNetworks: this.whitelistedDestinationNetworks, + whitelistedSourceResources: this.whitelistedSourceResources + }); + } + + updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if ( + changedProperties.has('whitelistedSourceNetworks') || + changedProperties.has('whitelistedDestinationNetworks') || + changedProperties.has('whitelistedSourceResources') + ) { + void this.transferController.init(this.environment!, { + whitelistedSourceNetworks: this.whitelistedSourceNetworks, + whitelistedDestinationNetworks: this.whitelistedDestinationNetworks, + whitelistedSourceResources: this.whitelistedSourceResources + }); + } } private onClick = (): void => { diff --git a/packages/widget/src/context/config.ts b/packages/widget/src/context/config.ts index 34b676ee..569cbfcb 100644 --- a/packages/widget/src/context/config.ts +++ b/packages/widget/src/context/config.ts @@ -1,11 +1,11 @@ import { customElement, property } from 'lit/decorators.js'; -import { createContext, ContextProvider } from '@lit/context'; +import { ContextProvider, createContext } 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, WalletInit } from '@web3-onboard/common'; -import { BaseComponent } from '../components/common/base-component'; import type { Theme } from '../interfaces'; +import { BaseComponent } from '../components/common/base-component'; export interface ConfigContext { theme?: Theme; diff --git a/packages/widget/src/context/wallet.ts b/packages/widget/src/context/wallet.ts index 8058ec78..d309df6a 100644 --- a/packages/widget/src/context/wallet.ts +++ b/packages/widget/src/context/wallet.ts @@ -78,7 +78,8 @@ export class WalletContextProvider extends BaseComponent { @property({ attribute: false }) substrateProviders?: Array = []; - @property({ type: String }) environment?: Environment; + @property({ type: String }) + environment?: Environment; async connectedCallback(): Promise { super.connectedCallback(); @@ -174,7 +175,6 @@ export class WalletContextProvider extends BaseComponent { // 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)) { diff --git a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts index 6057e1d1..3efaa080 100644 --- a/packages/widget/src/controllers/transfers/fungible-token-transfer.ts +++ b/packages/widget/src/controllers/transfers/fungible-token-transfer.ts @@ -9,8 +9,8 @@ import type { import { Config, Environment, - Network, - getRoutes + getRoutes, + Network } from '@buildwithsygma/sygma-sdk-core'; import { ContextConsumer } from '@lit/context'; import { BigNumber, ethers } from 'ethers'; @@ -22,12 +22,12 @@ import type { ParachainID, SubstrateFee } from '@buildwithsygma/sygma-sdk-core/substrate'; +import type { WalletContext } from '../../context'; import { walletContext } from '../../context'; import { MAINNET_EXPLORER_URL, TESTNET_EXPLORER_URL } from '../../constants'; import { validateAddress } from '../../utils'; import { SdkInitializedEvent } from '../../interfaces'; import { substrateProviderContext } from '../../context/wallet'; -import type { WalletContext } from '../../context'; import { buildEvmFungibleTransactions, executeNextEvmTransaction } from './evm'; import { buildSubstrateFungibleTransactions, @@ -92,6 +92,10 @@ export class FungibleTokenTransferController implements ReactiveController { //source network chain id -> Route[] protected routesCache: Map = new Map(); + protected whitelistedSourceNetworks?: string[] = []; + protected whitelistedDestinationNetworks?: string[] = []; + protected whitelistedSourceResources?: string[] = []; + host: ReactiveElement; walletContext: ContextConsumer; substrateProviderContext: ContextConsumer< @@ -127,7 +131,7 @@ export class FungibleTokenTransferController implements ReactiveController { /** * Provides substrate provider * based on parachain id - * @param {ParachainId} parachainId + * @param {parachainId} parachainId * @returns {ApiPromise | undefined} */ getSubstrateProvider(parachainId: ParachainID): ApiPromise | undefined { @@ -146,6 +150,7 @@ export class FungibleTokenTransferController implements ReactiveController { constructor(host: ReactiveElement) { (this.host = host).addController(this); this.config = new Config(); + this.walletContext = new ContextConsumer(host, { context: walletContext, subscribe: true, @@ -200,15 +205,59 @@ export class FungibleTokenTransferController implements ReactiveController { } } - async init(env: Environment): Promise { + /** + * Filter source and destination networks specified by User + * @param whitelistedNetworks + * @param network + */ + filterWhitelistedNetworks = ( + whitelistedNetworks: string[] | undefined, + network: Domain + ): boolean => { + // skip filtering if whitelisted networks are empty + if (!whitelistedNetworks?.length) return true; + + return whitelistedNetworks.some( + (networkName) => networkName.toLowerCase() === network.name.toLowerCase() + ); + }; + + async init( + env: Environment, + options?: { + whitelistedSourceNetworks?: string[]; + whitelistedDestinationNetworks?: string[]; + whitelistedSourceResources?: string[]; + } + ): Promise { this.host.requestUpdate(); this.env = env; + + this.whitelistedSourceNetworks = options?.whitelistedSourceNetworks; + this.whitelistedDestinationNetworks = + options?.whitelistedDestinationNetworks; + this.whitelistedSourceResources = options?.whitelistedSourceResources; + await this.retryInitSdk(); - this.supportedSourceNetworks = this.config.getDomains(); - this.supportedDestinationNetworks = this.config.getDomains(); + this.supportedSourceNetworks = this.config + .getDomains() + .filter((network) => + this.filterWhitelistedNetworks( + options?.whitelistedSourceNetworks, + network + ) + ); + this.supportedDestinationNetworks = this.config + .getDomains() + .filter((network) => + this.filterWhitelistedNetworks( + options?.whitelistedDestinationNetworks, + network + ) + ); this.host.requestUpdate(); } - + resetFee(): void { this.fee = null; } @@ -415,6 +464,12 @@ export class FungibleTokenTransferController implements ReactiveController { if (!this.destinationNetwork) { this.supportedDestinationNetworks = routes .filter((route) => route.toDomain.chainId !== sourceNetwork.chainId) + .filter((route) => + this.filterWhitelistedNetworks( + this.whitelistedDestinationNetworks, + route.toDomain + ) + ) .map((route) => route.toDomain); } // source change but not destination, check if route is supported else if (this.supportedDestinationNetworks.length && routes.length) { @@ -442,8 +497,13 @@ export class FungibleTokenTransferController implements ReactiveController { (route.toDomain.chainId === this.destinationNetwork?.chainId && !this.supportedResources.includes(route.resource)) ) - .map((route) => route.resource); + .filter((route) => { + // skip filter if resources are not specified + if (!this.whitelistedSourceResources?.length) return true; + return this.whitelistedSourceResources.includes(route.resource.symbol!); + }) + .map((route) => route.resource); void this.buildTransactions(); this.host.requestUpdate(); }; diff --git a/packages/widget/src/interfaces/index.ts b/packages/widget/src/interfaces/index.ts index eb9032f9..e5df52ed 100644 --- a/packages/widget/src/interfaces/index.ts +++ b/packages/widget/src/interfaces/index.ts @@ -1,8 +1,4 @@ -import type { - Environment, - EvmResource, - SubstrateResource -} from '@buildwithsygma/sygma-sdk-core'; +import type { Environment } 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'; @@ -27,11 +23,11 @@ export interface ISygmaProtocolWidget { environment?: Environment; whitelistedSourceNetworks?: string[]; whitelistedDestinationNetworks?: string[]; + whitelistedSourceResources?: string[]; evmProvider?: Eip1193Provider; substrateProviders?: Array; substrateSigner?: Signer; show?: boolean; - whitelistedSourceResources?: Array; expandable?: boolean; darkTheme?: boolean; customLogo?: SVGElement; diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 566fa35c..54d33073 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -1,8 +1,4 @@ -import type { - Domain, - EvmResource, - SubstrateResource -} from '@buildwithsygma/sygma-sdk-core'; +import type { Domain } from '@buildwithsygma/sygma-sdk-core'; import { Environment } from '@buildwithsygma/sygma-sdk-core'; import type { ApiPromise } from '@polkadot/api'; import type { Signer } from '@polkadot/api/types'; @@ -28,6 +24,7 @@ import type { Theme } from './interfaces'; import { styles } from './styles'; +import { BaseComponent } from './components/common'; @customElement('sygmaprotocol-widget') class SygmaProtocolWidget @@ -44,6 +41,8 @@ class SygmaProtocolWidget @property({ type: Array }) whitelistedDestinationNetworks?: string[]; + @property({ type: Array }) whitelistedSourceResources?: string[]; + @property({ type: Object }) evmProvider?: Eip1193Provider; @property({ type: Array }) substrateProviders?: Array; @@ -52,10 +51,6 @@ class SygmaProtocolWidget @property({ type: Boolean }) show?: boolean; - @property({ type: Array }) whitelistedSourceResources?: Array< - EvmResource | SubstrateResource - >; - @property({ type: Boolean }) expandable?: boolean; @property({ type: Boolean }) darkTheme?: boolean; @@ -126,8 +121,11 @@ class SygmaProtocolWidget .environment=${this.environment as Environment} .onSourceNetworkSelected=${(domain: Domain) => (this.sourceNetwork = domain)} - .whitelistedSourceResources=${this.whitelistedSourceNetworks} environment=${Environment.TESTNET} + .whitelistedSourceNetworks=${this.whitelistedSourceNetworks} + .whitelistedDestinationNetworks=${this + .whitelistedDestinationNetworks} + .whitelistedSourceResources=${this.whitelistedSourceResources} > 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 0805e9a6..105747e3 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,15 +1,109 @@ -import { fixture, fixtureCleanup } from '@open-wc/testing-helpers'; -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'; -import type { AddressInput } from '../../../../../src/components'; +import { Environment, Network } from '@buildwithsygma/sygma-sdk-core'; +import { fixture, fixtureCleanup, waitUntil } from '@open-wc/testing-helpers'; +import { html } from 'lit'; +import { afterEach, assert, describe, expect, it, vi } from 'vitest'; +import type { + AddressInput, + ResourceAmountSelector +} from '../../../../../src/components'; import { FungibleTokenTransfer } from '../../../../../src/components'; import type { WalletContextProvider } from '../../../../../src/context'; import { WalletUpdateEvent } from '../../../../../src/context'; import { getMockedEvmWallet } from '../../../../utils'; vi.mock('@polkadot/api'); +vi.mock( + '@buildwithsygma/sygma-sdk-core', + + async (importOriginal) => { + const mod = + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + await importOriginal(); + const modConfig = vi.fn<[], typeof mod.Config>(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + modConfig.prototype.init = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + modConfig.prototype.getDomains = vi.fn().mockReturnValue([ + { id: 2, chainId: 11155111, name: 'sepolia', type: 'evm' }, + { id: 3, chainId: 5231, name: 'rococo-phala', type: 'substrate' }, + { id: 5, chainId: 338, name: 'cronos', type: 'evm' }, + { id: 6, chainId: 17000, name: 'holesky', type: 'evm' }, + { id: 8, chainId: 421614, name: 'arbitrum_sepolia', type: 'evm' }, + { id: 9, chainId: 10200, name: 'gnosis_chiado', type: 'evm' }, + { id: 10, chainId: 84532, name: 'base_sepolia', type: 'evm' } + ]); + const modGetRoutes = vi.fn().mockReturnValue([ + { + fromDomain: { id: 2, chainId: 11155111, name: 'sepolia', type: 'evm' }, + toDomain: { id: 10, chainId: 84532, name: 'base_sepolia', type: 'evm' }, + resource: { + resourceId: '123', + type: 'fungible', + address: '0x123', + symbol: 'sygUSDC' + } + }, + { + fromDomain: { id: 2, chainId: 11155111, name: 'sepolia', type: 'evm' }, + toDomain: { id: 10, chainId: 84532, name: 'base_sepolia', type: 'evm' }, + resource: { + resourceId: '124', + type: 'fungible', + address: '0x123', + symbol: 'ERC20LRTest' + } + } + ]); + return { + ...mod, + Config: modConfig, + getRoutes: modGetRoutes + }; + } +); + +const sepoliaNetwork: Domain = { + id: 2, + chainId: 11155111, + name: 'sepolia', + type: Network.EVM +}; + +const baseSepolia: Domain = { + id: 10, + chainId: 84532, + name: 'base_sepolia', + type: Network.EVM +}; + +const cronosNetwork: Domain = { + id: 5, + chainId: 338, + name: 'cronos', + type: Network.EVM +}; + +const whitelistedSourceNetworks = ['sepolia']; +const whitelistedDestinationNetworks = ['base_sepolia']; +const whitelistedResources = ['ERC20LRTest']; +const connectedAddress = '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5'; + +function containsWhitelistedData( + data: T[], + whitelist: string[], + dataExtractor: (item: T) => string, + errorMessageContext: string +): void { + assert.isNotEmpty(data, 'Data must not be empty.'); + data.forEach((item) => { + const key = dataExtractor(item); + assert.isTrue( + whitelist.includes(key), + `${key} expected to be whitelisted in ${errorMessageContext}` + ); + }); +} describe('Fungible token Transfer', function () { afterEach(() => { @@ -18,25 +112,15 @@ describe('Fungible token Transfer', function () { it('is defined', async () => { const el = await fixture( - html` ` + 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` @@ -52,14 +136,16 @@ describe('Fungible token Transfer', function () { }) ); const fungibleTransfer = await fixture( - html` `, + html` `, { parentNode: walletContext } ); // Set Source and Destination Networks - fungibleTransfer.transferController.onSourceNetworkSelected(sourceNetwork); + fungibleTransfer.transferController.onSourceNetworkSelected(sepoliaNetwork); fungibleTransfer.transferController.onDestinationNetworkSelected( - destinationNetwork + baseSepolia ); fungibleTransfer.requestUpdate(); await fungibleTransfer.updateComplete; @@ -91,7 +177,9 @@ describe('Fungible token Transfer', function () { ); const fungibleTransfer = await fixture( - html` `, + html` `, { parentNode: walletContext } ); @@ -101,4 +189,159 @@ describe('Fungible token Transfer', function () { assert.equal(sygmaAddressInput.address, ''); }); + + it('should filter whitelisted networks and resources', async () => { + const walletContext = await fixture(html` + + `); + + const fungibleTransfer = await fixture( + html` `, + { parentNode: walletContext } + ); + + walletContext.dispatchEvent( + new WalletUpdateEvent({ + evmWallet: { + address: connectedAddress, + providerChainId: 11155111, + provider: getMockedEvmWallet().provider + } + }) + ); + + await fungibleTransfer.updateComplete; + + containsWhitelistedData( + fungibleTransfer.transferController.supportedSourceNetworks, + whitelistedSourceNetworks, + (network) => network.name, + 'transfer controller for source networks' + ); + + containsWhitelistedData( + fungibleTransfer.transferController.supportedDestinationNetworks, + whitelistedDestinationNetworks, + (network) => network.name, + 'transfer controller for destination networks' + ); + + // Set Source and Destination Networks + fungibleTransfer.transferController.onSourceNetworkSelected(sepoliaNetwork); + fungibleTransfer.transferController.onDestinationNetworkSelected( + baseSepolia + ); + fungibleTransfer.requestUpdate(); + await fungibleTransfer.updateComplete; + + const [sygmaSourceNetwork, sygmaDestinationNetwork] = + fungibleTransfer.shadowRoot!.querySelectorAll('sygma-network-selector'); + + const resourceSelector = fungibleTransfer.shadowRoot!.querySelector( + 'sygma-resource-amount-selector' + ) as ResourceAmountSelector; + + containsWhitelistedData( + fungibleTransfer.transferController.supportedResources, + whitelistedResources, + (resource) => resource.symbol!, + 'transfer controller for resources' + ); + + assert.isTrue( + whitelistedSourceNetworks.includes( + sygmaSourceNetwork.networks?.[0]?.name + ), + `Expected source network to be one of ${whitelistedSourceNetworks.join(', ')}, but got ${sygmaSourceNetwork.networks?.[0]?.name}` + ); + assert.isTrue( + whitelistedDestinationNetworks.includes( + sygmaDestinationNetwork.networks?.[0]?.name + ), + `Expected destination network to be one of ${whitelistedDestinationNetworks.join(', ')}, but got ${sygmaDestinationNetwork.networks?.[0]?.name}` + ); + assert.isTrue( + whitelistedResources.includes( + resourceSelector.resources[0]?.symbol || '' + ), + `Expected Resource to be one of ${whitelistedResources.join(', ')}, but got ${resourceSelector.resources.join(', ')}` + ); + }); + + it('should re-init the transfer controller -> when networks or resources are updated', async () => { + const walletContext = await fixture(html` + + `); + + const fungibleTransfer = await fixture( + html` `, + { parentNode: walletContext } + ); + const spyInit = vi.spyOn(fungibleTransfer.transferController, 'init'); + + fungibleTransfer.whitelistedSourceNetworks = ['cronos']; + fungibleTransfer.whitelistedDestinationNetworks = ['sepolia']; + fungibleTransfer.whitelistedSourceResources = ['ERC20LRTest']; + + fungibleTransfer.requestUpdate(); + await fungibleTransfer.updateComplete; + + expect(spyInit).toHaveBeenCalledTimes(1); + expect(spyInit).toHaveBeenCalledWith(Environment.TESTNET, { + whitelistedSourceNetworks: ['cronos'], + whitelistedDestinationNetworks: ['sepolia'], + whitelistedSourceResources: ['ERC20LRTest'] + }); + + await waitUntil( + () => { + if ( + fungibleTransfer.transferController.supportedSourceNetworks.length > 0 + ) { + return true; + } + return undefined; + }, + '', + { interval: 1, timeout: 100 } + ); + + containsWhitelistedData( + fungibleTransfer.transferController.supportedSourceNetworks, + ['cronos'], + (network) => network.name, + 'transfer controller for source networks' + ); + + containsWhitelistedData( + fungibleTransfer.transferController.supportedDestinationNetworks, + ['sepolia'], + (network) => network.name, + 'transfer controller for source networks' + ); + + // Set Source Network + fungibleTransfer.transferController.onSourceNetworkSelected(cronosNetwork); + fungibleTransfer.requestUpdate(); + await fungibleTransfer.updateComplete; + + containsWhitelistedData( + fungibleTransfer.transferController.supportedResources, + ['ERC20LRTest'], + (resource) => resource.symbol!, + 'transfer controller for resources' + ); + + spyInit.mockRestore(); + }); });