diff --git a/.changeset/gold-tips-travel.md b/.changeset/gold-tips-travel.md new file mode 100644 index 00000000..b49df81c --- /dev/null +++ b/.changeset/gold-tips-travel.md @@ -0,0 +1,6 @@ +--- +'@moonbeam-network/xcm-config': minor +'@moonbeam-network/xcm-sdk': minor +--- + +Introduce config service to support mutability of static xcm config diff --git a/.gitignore b/.gitignore index 55350715..7137c53b 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,6 @@ dist # mac .DS_Store + +# IntelliJ Idea +.idea diff --git a/packages/config/src/ConfigBuilder/ConfigBuilder.test.ts b/packages/config/src/ConfigBuilder/ConfigBuilder.test.ts index 32f91c6c..94619cd1 100644 --- a/packages/config/src/ConfigBuilder/ConfigBuilder.test.ts +++ b/packages/config/src/ConfigBuilder/ConfigBuilder.test.ts @@ -1,5 +1,6 @@ /* eslint-disable sort-keys */ import { Ecosystem } from '@moonbeam-network/xcm-types'; +import { ConfigService } from '../ConfigService'; import { dev } from '../assets'; import { equilibriumAlphanet, moonbaseAlpha } from '../chains'; import { equilibriumAlphanetConfig } from '../configs/equilibriumAlphanet'; @@ -33,4 +34,32 @@ describe('configBuilder', () => { }, }); }); + + it('should return correct dev config using mutable service', () => { + const configService = new ConfigService(); + const config = ConfigBuilder(configService) + .assets(Ecosystem.AlphanetRelay) + .asset(dev) + .source(moonbaseAlpha) + .destination(equilibriumAlphanet) + .build(); + + expect(config).toStrictEqual({ + asset: dev, + source: { + chain: moonbaseAlpha, + config: moonbaseAlphaConfig.getAssetDestinationConfig( + dev, + equilibriumAlphanet, + ), + }, + destination: { + chain: equilibriumAlphanet, + config: equilibriumAlphanetConfig.getAssetDestinationConfig( + dev, + moonbaseAlpha, + ), + }, + }); + }); }); diff --git a/packages/config/src/ConfigBuilder/ConfigBuilder.ts b/packages/config/src/ConfigBuilder/ConfigBuilder.ts index 6761f154..029dab5a 100644 --- a/packages/config/src/ConfigBuilder/ConfigBuilder.ts +++ b/packages/config/src/ConfigBuilder/ConfigBuilder.ts @@ -1,31 +1,28 @@ /* eslint-disable sort-keys */ import { AnyChain, Asset, Ecosystem } from '@moonbeam-network/xcm-types'; +import { ConfigService, IConfigService } from '../ConfigService'; import { TransferConfig } from './ConfigBuilder.interfaces'; -import { - getAsset, - getAssetDestinationConfig, - getChain, - getDestinationChains, - getEcosystemAssets, - getSourceChains, -} from './ConfigBuilder.utils'; -export function ConfigBuilder() { +export function ConfigBuilder(service?: IConfigService) { + const config = service ?? new ConfigService(); return { assets: (ecosystem?: Ecosystem) => { - const assets = getEcosystemAssets(ecosystem); + const assets = config.getEcosystemAssets(ecosystem); return { assets, asset: (keyOrAsset: string | Asset) => { - const asset = getAsset(keyOrAsset); - const sourceChains = getSourceChains(asset, ecosystem); + const asset = config.getAsset(keyOrAsset); + const sourceChains = config.getSourceChains(asset, ecosystem); return { sourceChains, source: (keyOrChain: string | AnyChain) => { - const source = getChain(keyOrChain); - const destinationChains = getDestinationChains(asset, source); + const source = config.getChain(keyOrChain); + const destinationChains = config.getDestinationChains( + asset, + source, + ); return { destinationChains, @@ -33,13 +30,13 @@ export function ConfigBuilder() { // eslint-disable-next-line @typescript-eslint/no-shadow keyOrChain: string | AnyChain, ) => { - const destination = getChain(keyOrChain); - const sourceConfig = getAssetDestinationConfig( + const destination = config.getChain(keyOrChain); + const sourceConfig = config.getAssetDestinationConfig( asset, source, destination, ); - const destinationConfig = getAssetDestinationConfig( + const destinationConfig = config.getAssetDestinationConfig( asset, destination, source, diff --git a/packages/config/src/ConfigBuilder/ConfigBuilder.utils.test.ts b/packages/config/src/ConfigBuilder/ConfigBuilder.utils.test.ts deleted file mode 100644 index 87cdd6f6..00000000 --- a/packages/config/src/ConfigBuilder/ConfigBuilder.utils.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Asset, Ecosystem, EvmParachain } from '@moonbeam-network/xcm-types'; -import { assetsList, dev, tt1, unit } from '../assets'; -import { equilibriumAlphanet, moonbaseAlpha } from '../chains'; -import { - getAsset, - getChain, - getEcosystemAssets, - getSourceChains, -} from './ConfigBuilder.utils'; - -describe('config utils', () => { - describe('getEcosystemAssets', () => { - it('should return all assets', () => { - const assets = getEcosystemAssets(); - - expect(assets).toStrictEqual(assetsList); - }); - - it('should return assets for a given ecosystem', () => { - const assets = getEcosystemAssets(Ecosystem.AlphanetRelay); - - expect(assets.length).toBeLessThan(assetsList.length); - expect(assets).toStrictEqual(expect.arrayContaining([dev, unit, tt1])); - }); - }); - - describe('getAsset', () => { - it('should get asset by key', () => { - expect(getAsset(dev.key)).toStrictEqual(dev); - }); - - it('should get asset by asset', () => { - expect(getAsset(dev)).toStrictEqual(dev); - }); - - it('should throw an error if asset not found', () => { - expect(() => getAsset('test')).toThrow(new Error('Asset test not found')); - }); - - it('should throw an error if asset is not in the config', () => { - expect(() => - getAsset( - new Asset({ - key: 'test', - originSymbol: 'test', - }), - ), - ).toThrow(new Error('Asset test not found')); - }); - }); - - describe('getChain', () => { - it('should get chain by key', () => { - expect(getChain(moonbaseAlpha.key)).toStrictEqual(moonbaseAlpha); - }); - - it('should get chain by chain', () => { - expect(getChain(moonbaseAlpha)).toStrictEqual(moonbaseAlpha); - }); - - it('should throw an error if chain not found', () => { - expect(() => getChain('test')).toThrow(new Error('Chain test not found')); - }); - - it('should throw an error if chain is not in the config', () => { - expect(() => - getChain( - new EvmParachain({ - ecosystem: Ecosystem.AlphanetRelay, - genesisHash: '', - id: 1287, - isTestChain: true, - key: 'test', - name: 'test', - parachainId: 1000, - rpc: '', - ss58Format: 1287, - ws: '', - }), - ), - ).toThrow(new Error('Chain test not found')); - }); - }); - - describe('getSourceChains', () => { - it('should get source chains for asset', () => { - const chains = getSourceChains(dev, Ecosystem.AlphanetRelay); - - expect(chains).toStrictEqual( - expect.arrayContaining([moonbaseAlpha, equilibriumAlphanet]), - ); - }); - }); -}); diff --git a/packages/config/src/ConfigBuilder/ConfigBuilder.utils.ts b/packages/config/src/ConfigBuilder/ConfigBuilder.utils.ts deleted file mode 100644 index 22f54d2d..00000000 --- a/packages/config/src/ConfigBuilder/ConfigBuilder.utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { AnyChain, Asset, Ecosystem } from '@moonbeam-network/xcm-types'; -import { assetsList, assetsMap } from '../assets'; -import { chainsMap } from '../chains'; -import { chainsConfigMap } from '../configs'; -import { AssetConfig } from '../types/AssetConfig'; - -export function getEcosystemAssets(ecosystem?: Ecosystem): Asset[] { - if (!ecosystem) { - return assetsList; - } - - return Array.from( - new Set( - Array.from(chainsConfigMap.values()) - .filter((chainConfig) => chainConfig.chain.ecosystem === ecosystem) - .map((chainConfig) => chainConfig.getAssetsConfigs()) - .flat(2) - .map((assetConfig) => assetConfig.asset), - ), - ).sort((a, b) => a.key.localeCompare(b.key)); -} - -export function getAsset(keyOrAsset: string | Asset): Asset { - const key = typeof keyOrAsset === 'string' ? keyOrAsset : keyOrAsset.key; - const asset = assetsMap.get(key); - - if (!asset) { - throw new Error(`Asset ${key} not found`); - } - - return asset; -} - -export function getChain(keyOrAsset: string | AnyChain): AnyChain { - const key = typeof keyOrAsset === 'string' ? keyOrAsset : keyOrAsset.key; - const chain = chainsMap.get(key); - - if (!chain) { - throw new Error(`Chain ${key} not found`); - } - - return chain; -} - -export function getSourceChains( - asset: Asset, - ecosystem: Ecosystem | undefined, -): AnyChain[] { - return Array.from(chainsConfigMap.values()) - .filter((chainConfig) => chainConfig.getAssetConfigs(asset).length) - .filter( - (chainConfig) => !ecosystem || chainConfig.chain.ecosystem === ecosystem, - ) - .map((chainConfig) => chainConfig.chain); -} - -export function getDestinationChains( - asset: Asset, - source: AnyChain, -): AnyChain[] { - const chainConfig = chainsConfigMap.get(source.key); - - if (!chainConfig) { - throw new Error(`Config for chain ${source.key} not found`); - } - - return chainConfig.getAssetDestinations(asset); -} - -export function getAssetDestinationConfig( - asset: Asset, - source: AnyChain, - destination: AnyChain, -): AssetConfig { - const chainConfig = chainsConfigMap.get(source.key); - - if (!chainConfig) { - throw new Error(`Config for chain ${source.key} not found`); - } - - return chainConfig.getAssetDestinationConfig(asset, destination); -} diff --git a/packages/config/src/ConfigService/ConfigService.interfaces.ts b/packages/config/src/ConfigService/ConfigService.interfaces.ts new file mode 100644 index 00000000..2e0448ff --- /dev/null +++ b/packages/config/src/ConfigService/ConfigService.interfaces.ts @@ -0,0 +1,20 @@ +import { AnyChain, Asset, Ecosystem } from '@moonbeam-network/xcm-types'; +import { AssetConfig } from '../types/AssetConfig'; + +export interface IConfigService { + getEcosystemAssets(ecosystem?: Ecosystem): Asset[]; + + getAsset(keyOrAsset: string | Asset): Asset; + + getChain(keyOrAsset: string | AnyChain): AnyChain; + + getSourceChains(asset: Asset, ecosystem: Ecosystem | undefined): AnyChain[]; + + getDestinationChains(asset: Asset, source: AnyChain): AnyChain[]; + + getAssetDestinationConfig( + asset: Asset, + source: AnyChain, + destination: AnyChain, + ): AssetConfig; +} diff --git a/packages/config/src/ConfigService/ConfigService.test.ts b/packages/config/src/ConfigService/ConfigService.test.ts new file mode 100644 index 00000000..fc3e88e1 --- /dev/null +++ b/packages/config/src/ConfigService/ConfigService.test.ts @@ -0,0 +1,204 @@ +import { + BalanceBuilder, + ExtrinsicBuilder, +} from '@moonbeam-network/xcm-builder'; +import { + Asset, + Ecosystem, + EvmParachain, + Parachain, +} from '@moonbeam-network/xcm-types'; +import { assetsList, dev, glmr, tt1, unit } from '../assets'; +import { + equilibriumAlphanet, + hydraDX, + moonbaseAlpha, + moonbeam, +} from '../chains'; +import { ConfigService } from './ConfigService'; + +import { AssetConfig } from '../types/AssetConfig'; +import { ChainConfig } from '../types/ChainConfig'; + +const TEST_CHAIN = new Parachain({ + ecosystem: Ecosystem.Polkadot, + genesisHash: '', + isTestChain: true, + key: 'test', + name: 'test', + parachainId: 9999, + ss58Format: 1999, + ws: '', +}); + +describe('config service', () => { + const configService = new ConfigService(); + + describe('getEcosystemAssets', () => { + it('should return all assets', () => { + const assets = configService.getEcosystemAssets(); + + expect(assets).toStrictEqual(assetsList); + }); + + it('should return assets for a given ecosystem', () => { + const assets = configService.getEcosystemAssets(Ecosystem.AlphanetRelay); + + expect(assets.length).toBeLessThan(assetsList.length); + expect(assets).toStrictEqual(expect.arrayContaining([dev, unit, tt1])); + }); + }); + + describe('getAsset', () => { + it('should get asset by key', () => { + expect(configService.getAsset(dev.key)).toStrictEqual(dev); + }); + + it('should get asset by asset', () => { + expect(configService.getAsset(dev)).toStrictEqual(dev); + }); + + it('should throw an error if asset not found', () => { + expect(() => configService.getAsset('test')).toThrow( + new Error('Asset test not found'), + ); + }); + + it('should throw an error if asset is not in the config', () => { + expect(() => + configService.getAsset( + new Asset({ + key: 'test', + originSymbol: 'test', + }), + ), + ).toThrow(new Error('Asset test not found')); + }); + }); + + describe('getChain', () => { + it('should get chain by key', () => { + expect(configService.getChain(moonbaseAlpha.key)).toStrictEqual( + moonbaseAlpha, + ); + }); + + it('should get chain by chain', () => { + expect(configService.getChain(moonbaseAlpha)).toStrictEqual( + moonbaseAlpha, + ); + }); + + it('should throw an error if chain not found', () => { + expect(() => configService.getChain('test')).toThrow( + new Error('Chain test not found'), + ); + }); + + it('should throw an error if chain is not in the config', () => { + expect(() => + configService.getChain( + new EvmParachain({ + ecosystem: Ecosystem.AlphanetRelay, + genesisHash: '', + id: 1287, + isTestChain: true, + key: 'test', + name: 'test', + parachainId: 1000, + rpc: '', + ss58Format: 1287, + ws: '', + }), + ), + ).toThrow(new Error('Chain test not found')); + }); + }); + + describe('getSourceChains', () => { + it('should get source chains for asset', () => { + const chains = configService.getSourceChains( + dev, + Ecosystem.AlphanetRelay, + ); + + expect(chains).toStrictEqual( + expect.arrayContaining([moonbaseAlpha, equilibriumAlphanet]), + ); + }); + }); + + describe('updateAssets', () => { + it('should register new asset', () => { + const asset = new Asset({ + key: 'test', + originSymbol: 'TEST', + }); + configService.updateAsset(asset); + const registeredAsset = configService.getAsset(asset.key); + expect(asset).toStrictEqual(registeredAsset); + }); + }); + + describe('updateChains', () => { + it('should register new chain', () => { + configService.updateChain(TEST_CHAIN); + const registeredChain = configService.getChain(TEST_CHAIN.key); + expect(TEST_CHAIN).toStrictEqual(registeredChain); + }); + }); + + describe('updateChainConfig', () => { + it('should update existing chain config', () => { + const assetConfig = new AssetConfig({ + asset: glmr, + balance: BalanceBuilder().substrate().tokens().accounts(), + destination: moonbeam, + destinationFee: { + amount: 0.02, + asset: glmr, + balance: BalanceBuilder().substrate().tokens().accounts(), + }, + extrinsic: ExtrinsicBuilder().xTokens().transfer(), + }); + + const chainConfig = new ChainConfig({ + assets: [assetConfig], + chain: hydraDX, + }); + + configService.updateChainConfig(chainConfig); + const updated = configService.getChainConfig(hydraDX); + expect(updated.getAssetsConfigs()).toStrictEqual( + chainConfig.getAssetsConfigs(), + ); + }); + + it('should create new chain config', () => { + configService.updateChain(TEST_CHAIN); + + const assetConfig = new AssetConfig({ + asset: glmr, + balance: BalanceBuilder().substrate().tokens().accounts(), + destination: moonbeam, + destinationFee: { + amount: 0.02, + asset: glmr, + balance: BalanceBuilder().substrate().tokens().accounts(), + }, + extrinsic: ExtrinsicBuilder().xTokens().transfer(), + }); + + const chainConfig = new ChainConfig({ + assets: [assetConfig], + chain: TEST_CHAIN, + }); + + configService.updateChainConfig(chainConfig); + const updated = configService.getChainConfig('test'); + expect(updated.getAssetsConfigs()).toStrictEqual( + chainConfig.getAssetsConfigs(), + ); + }); + }); +}); diff --git a/packages/config/src/ConfigService/ConfigService.ts b/packages/config/src/ConfigService/ConfigService.ts new file mode 100644 index 00000000..2c80072f --- /dev/null +++ b/packages/config/src/ConfigService/ConfigService.ts @@ -0,0 +1,122 @@ +import { AnyChain, Asset, Ecosystem } from '@moonbeam-network/xcm-types'; +import { assetsMap } from '../assets'; +import { chainsMap } from '../chains'; +import { chainsConfigMap } from '../configs'; +import { AssetConfig } from '../types/AssetConfig'; +import { ChainConfig } from '../types/ChainConfig'; +import { IConfigService } from './ConfigService.interfaces'; + +export interface ConfigServiceOptions { + assets?: Map; + chains?: Map; + chainsConfig?: Map; +} + +export class ConfigService implements IConfigService { + protected assets: Map; + + protected chains: Map; + + protected chainsConfig: Map; + + constructor(options?: ConfigServiceOptions) { + this.assets = options?.assets ?? assetsMap; + this.chains = options?.chains ?? chainsMap; + this.chainsConfig = options?.chainsConfig ?? chainsConfigMap; + } + + getEcosystemAssets(ecosystem?: Ecosystem): Asset[] { + if (!ecosystem) { + return Array.from(this.assets.values()); + } + + return Array.from( + new Set( + Array.from(this.chainsConfig.values()) + .filter((chainConfig) => chainConfig.chain.ecosystem === ecosystem) + .map((chainConfig) => chainConfig.getAssetsConfigs()) + .flat(2) + .map((assetConfig) => assetConfig.asset), + ), + ).sort((a, b) => a.key.localeCompare(b.key)); + } + + getAsset(keyOrAsset: string | Asset): Asset { + const key = typeof keyOrAsset === 'string' ? keyOrAsset : keyOrAsset.key; + const asset = this.assets.get(key); + + if (!asset) { + throw new Error(`Asset ${key} not found`); + } + + return asset; + } + + getChain(keyOrAsset: string | AnyChain): AnyChain { + const key = typeof keyOrAsset === 'string' ? keyOrAsset : keyOrAsset.key; + const chain = this.chains.get(key); + + if (!chain) { + throw new Error(`Chain ${key} not found`); + } + + return chain; + } + + getChainConfig(keyOrAsset: string | AnyChain): ChainConfig { + const key = typeof keyOrAsset === 'string' ? keyOrAsset : keyOrAsset.key; + const chainConfig = this.chainsConfig.get(key); + + if (!chainConfig) { + throw new Error(`Chain config for ${key} not found`); + } + + return chainConfig; + } + + getSourceChains(asset: Asset, ecosystem: Ecosystem | undefined): AnyChain[] { + return Array.from(this.chainsConfig.values()) + .filter((chainConfig) => chainConfig.getAssetConfigs(asset).length) + .filter( + (chainConfig) => + !ecosystem || chainConfig.chain.ecosystem === ecosystem, + ) + .map((chainConfig) => chainConfig.chain); + } + + getDestinationChains(asset: Asset, source: AnyChain): AnyChain[] { + const chainConfig = this.chainsConfig.get(source.key); + + if (!chainConfig) { + throw new Error(`Config for chain ${source.key} not found`); + } + + return chainConfig.getAssetDestinations(asset); + } + + getAssetDestinationConfig( + asset: Asset, + source: AnyChain, + destination: AnyChain, + ): AssetConfig { + const chainConfig = this.chainsConfig.get(source.key); + + if (!chainConfig) { + throw new Error(`Config for chain ${source.key} not found`); + } + + return chainConfig.getAssetDestinationConfig(asset, destination); + } + + updateAsset(asset: Asset): void { + this.assets.set(asset.key, asset); + } + + updateChain(chain: AnyChain): void { + this.chains.set(chain.key, chain); + } + + updateChainConfig(chainConfig: ChainConfig): void { + this.chainsConfig.set(chainConfig.chain.key, chainConfig); + } +} diff --git a/packages/config/src/ConfigService/index.ts b/packages/config/src/ConfigService/index.ts new file mode 100644 index 00000000..5c818e98 --- /dev/null +++ b/packages/config/src/ConfigService/index.ts @@ -0,0 +1,2 @@ +export * from './ConfigService'; +export * from './ConfigService.interfaces'; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 7b7e0f31..18cfc47c 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,4 +1,5 @@ export * from './ConfigBuilder'; +export * from './ConfigService'; export * from './assets'; export * from './chains'; export * from './types/AssetConfig'; diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index eb1c0b38..794d45ba 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -1,15 +1,18 @@ /* eslint-disable sort-keys */ -import { ConfigBuilder } from '@moonbeam-network/xcm-config'; +import { ConfigBuilder, IConfigService } from '@moonbeam-network/xcm-config'; import { AnyChain, Asset, Ecosystem } from '@moonbeam-network/xcm-types'; import { getTransferData as gtd } from './getTransferData/getTransferData'; import { Signers, TransferData } from './sdk.interfaces'; -export interface SdkOptions extends Partial {} +export interface SdkOptions extends Partial { + configService?: IConfigService; +} export function Sdk(options?: SdkOptions) { + const configService = options?.configService; return { assets(ecosystem?: Ecosystem) { - const { assets, asset } = ConfigBuilder().assets(ecosystem); + const { assets, asset } = ConfigBuilder(configService).assets(ecosystem); return { assets, @@ -60,7 +63,7 @@ export function Sdk(options?: SdkOptions) { ethersSigner, polkadotSigner, sourceAddress, - transferConfig: ConfigBuilder() + transferConfig: ConfigBuilder(configService) .assets() .asset(keyOrAsset) .source(sourceKeyOrChain)