diff --git a/.changeset/cool-readers-kiss.md b/.changeset/cool-readers-kiss.md new file mode 100644 index 0000000000..fbb85f515b --- /dev/null +++ b/.changeset/cool-readers-kiss.md @@ -0,0 +1,7 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Added RPC `concurrency` property to `ChainMetadata`. +Added `CrudModule` abstraction and related types. +Removed `Fuel` ProtocolType. diff --git a/typescript/sdk/src/core/read.ts b/typescript/sdk/src/core/read.ts index 31b1291a38..85873e76df 100644 --- a/typescript/sdk/src/core/read.ts +++ b/typescript/sdk/src/core/read.ts @@ -7,7 +7,7 @@ import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; import { EvmHookReader } from '../hook/read.js'; import { EvmIsmReader } from '../ism/read.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { ChainName } from '../types.js'; +import { ChainNameOrId } from '../types.js'; import { CoreConfig } from './types.js'; @@ -21,8 +21,10 @@ export class EvmCoreReader implements CoreReader { evmIsmReader: EvmIsmReader; constructor( protected readonly multiProvider: MultiProvider, - protected readonly chain: ChainName, - protected readonly concurrency: number = DEFAULT_CONTRACT_READ_CONCURRENCY, + protected readonly chain: ChainNameOrId, + protected readonly concurrency: number = multiProvider.tryGetRpcConcurrency( + chain, + ) ?? DEFAULT_CONTRACT_READ_CONCURRENCY, ) { this.provider = this.multiProvider.getProvider(chain); this.evmHookReader = new EvmHookReader(multiProvider, chain, concurrency); diff --git a/typescript/sdk/src/crud/AbstractCrudModule.ts b/typescript/sdk/src/crud/AbstractCrudModule.ts new file mode 100644 index 0000000000..255258449f --- /dev/null +++ b/typescript/sdk/src/crud/AbstractCrudModule.ts @@ -0,0 +1,57 @@ +import { Logger } from 'pino'; + +import { Address, Annotated, ProtocolType } from '@hyperlane-xyz/utils'; + +import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; +import { + ProtocolTypedProvider, + ProtocolTypedTransaction, +} from '../providers/ProviderType.js'; +import { ChainNameOrId } from '../types.js'; + +export type CrudModuleArgs< + TProtocol extends ProtocolType, + TConfig, + TAddressMap extends Record, +> = { + addresses: TAddressMap; + chain: ChainNameOrId; + chainMetadataManager: ChainMetadataManager; + config: TConfig; + provider: ProtocolTypedProvider['provider']; +}; + +export abstract class CrudModule< + TProtocol extends ProtocolType, + TConfig, + TAddressMap extends Record, +> { + protected abstract readonly logger: Logger; + + protected constructor( + protected readonly args: CrudModuleArgs, + ) {} + + public serialize(): TAddressMap { + return this.args.addresses; + } + + public abstract read(address: Address): Promise; + public abstract update( + config: TConfig, + ): Promise[]>>; + + // /* + // Types and static methods can be challenging. Ensure each implementation includes a static create function. + // Currently, include TConfig to maintain the structure for ISM/Hook configurations. + // If found to be unnecessary, we may consider revisiting and potentially removing these config requirements later. + // */ + // public static create< + // TConfig extends CrudConfig, + // TProtocol extends ProtocolType, + // TAddress extends Record, + // TModule extends CrudModule, + // >(_config: TConfig): Promise { + // throw new Error('not implemented'); + // } +} diff --git a/typescript/sdk/src/crud/EvmHookModule.ts b/typescript/sdk/src/crud/EvmHookModule.ts new file mode 100644 index 0000000000..727135090e --- /dev/null +++ b/typescript/sdk/src/crud/EvmHookModule.ts @@ -0,0 +1,52 @@ +import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { HyperlaneAddresses } from '../contracts/types.js'; +import { HookFactories } from '../hook/contracts.js'; +import { EvmHookReader } from '../hook/read.js'; +import { HookConfig } from '../hook/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { EthersV5Transaction } from '../providers/ProviderType.js'; + +import { CrudModule, CrudModuleArgs } from './AbstractCrudModule.js'; + +// WIP example implementation of EvmHookModule +export class EvmHookModule extends CrudModule< + ProtocolType.Ethereum, + HookConfig, + HyperlaneAddresses +> { + protected logger = rootLogger.child({ module: 'EvmHookModule' }); + protected reader: EvmHookReader; + + protected constructor( + protected readonly multiProvider: MultiProvider, + args: Omit< + CrudModuleArgs< + ProtocolType.Ethereum, + HookConfig, + HyperlaneAddresses + >, + 'provider' + >, + ) { + super({ + ...args, + provider: multiProvider.getProvider(args.chain), + }); + + this.reader = new EvmHookReader(multiProvider, args.chain); + } + + public async read(address: Address): Promise { + return await this.reader.deriveHookConfig(address); + } + + public async update(_config: HookConfig): Promise { + throw new Error('Method not implemented.'); + } + + // manually write static create function + public static create(_config: HookConfig): Promise { + throw new Error('not implemented'); + } +} diff --git a/typescript/sdk/src/crud/EvmIsmModule.ts b/typescript/sdk/src/crud/EvmIsmModule.ts new file mode 100644 index 0000000000..820c4bc84c --- /dev/null +++ b/typescript/sdk/src/crud/EvmIsmModule.ts @@ -0,0 +1,52 @@ +import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; + +import { HyperlaneAddresses } from '../contracts/types.js'; +import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { EvmIsmReader } from '../ism/read.js'; +import { IsmConfig } from '../ism/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { EthersV5Transaction } from '../providers/ProviderType.js'; + +import { CrudModule, CrudModuleArgs } from './AbstractCrudModule.js'; + +// WIP example implementation of EvmIsmModule +export class EvmIsmModule extends CrudModule< + ProtocolType.Ethereum, + IsmConfig, + HyperlaneAddresses +> { + protected logger = rootLogger.child({ module: 'EvmIsmModule' }); + protected reader: EvmIsmReader; + + protected constructor( + protected readonly multiProvider: MultiProvider, + args: Omit< + CrudModuleArgs< + ProtocolType.Ethereum, + IsmConfig, + HyperlaneAddresses + >, + 'provider' + >, + ) { + super({ + ...args, + provider: multiProvider.getProvider(args.chain), + }); + + this.reader = new EvmIsmReader(multiProvider, args.chain); + } + + public async read(address: Address): Promise { + return await this.reader.deriveIsmConfig(address); + } + + public async update(_config: IsmConfig): Promise { + throw new Error('Method not implemented.'); + } + + // manually write static create function + public static create(_config: IsmConfig): Promise { + throw new Error('not implemented'); + } +} diff --git a/typescript/sdk/src/hook/read.ts b/typescript/sdk/src/hook/read.ts index d8b9dafe42..2f49bf9451 100644 --- a/typescript/sdk/src/hook/read.ts +++ b/typescript/sdk/src/hook/read.ts @@ -25,7 +25,7 @@ import { import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { ChainName } from '../types.js'; +import { ChainNameOrId } from '../types.js'; import { AggregationHookConfig, @@ -42,7 +42,7 @@ import { RoutingHookConfig, } from './types.js'; -interface HookReader { +export interface HookReader { deriveHookConfig(address: Address): Promise>; deriveMerkleTreeConfig( address: Address, @@ -74,10 +74,12 @@ export class EvmHookReader implements HookReader { constructor( protected readonly multiProvider: MultiProvider, - protected readonly chain: ChainName, - protected readonly concurrency: number = DEFAULT_CONTRACT_READ_CONCURRENCY, + protected readonly chain: ChainNameOrId, + protected readonly concurrency: number = multiProvider.tryGetRpcConcurrency( + chain, + ) ?? DEFAULT_CONTRACT_READ_CONCURRENCY, ) { - this.provider = this.multiProvider.getProvider(chain); + this.provider = multiProvider.getProvider(chain); } async deriveHookConfig(address: Address): Promise> { diff --git a/typescript/sdk/src/ism/read.ts b/typescript/sdk/src/ism/read.ts index a8898fa8d7..d52c5586ab 100644 --- a/typescript/sdk/src/ism/read.ts +++ b/typescript/sdk/src/ism/read.ts @@ -20,7 +20,7 @@ import { import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/crud.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { ChainName } from '../types.js'; +import { ChainNameOrId } from '../types.js'; import { AggregationIsmConfig, @@ -45,7 +45,7 @@ export type DerivedIsmConfigWithAddress = WithAddress< Exclude >; -interface IsmReader { +export interface IsmReader { deriveIsmConfig(address: Address): Promise; deriveRoutingConfig(address: Address): Promise>; deriveAggregationConfig( @@ -63,10 +63,12 @@ export class EvmIsmReader implements IsmReader { constructor( protected readonly multiProvider: MultiProvider, - protected readonly chain: ChainName, - protected readonly concurrency: number = DEFAULT_CONTRACT_READ_CONCURRENCY, + protected readonly chain: ChainNameOrId, + protected readonly concurrency: number = multiProvider.tryGetRpcConcurrency( + chain, + ) ?? DEFAULT_CONTRACT_READ_CONCURRENCY, ) { - this.provider = this.multiProvider.getProvider(chain); + this.provider = multiProvider.getProvider(chain); } async deriveIsmConfig( diff --git a/typescript/sdk/src/metadata/ChainMetadataManager.ts b/typescript/sdk/src/metadata/ChainMetadataManager.ts index 2dd85a1748..59c7730dae 100644 --- a/typescript/sdk/src/metadata/ChainMetadataManager.ts +++ b/typescript/sdk/src/metadata/ChainMetadataManager.ts @@ -238,14 +238,38 @@ export class ChainMetadataManager { } /** - * Get an RPC URL for a given chain name, chain id, or domain id + * Get the RPC details for a given chain name, chain id, or domain id. + * Optional index for metadata containing more than one RPC. * @throws if chain's metadata has not been set */ - getRpcUrl(chainNameOrId: ChainNameOrId): string { + getRpc( + chainNameOrId: ChainNameOrId, + index = 0, + ): ChainMetadata['rpcUrls'][number] { const { rpcUrls } = this.getChainMetadata(chainNameOrId); - if (!rpcUrls?.length || !rpcUrls[0].http) - throw new Error(`No RPC URl configured for ${chainNameOrId}`); - return rpcUrls[0].http; + if (!rpcUrls?.length || !rpcUrls[index]) + throw new Error( + `No RPC configured at index ${index} for ${chainNameOrId}`, + ); + return rpcUrls[index]; + } + + /** + * Get an RPC URL for a given chain name, chain id, or domain id + * @throws if chain's metadata has not been set + */ + getRpcUrl(chainNameOrId: ChainNameOrId, index = 0): string { + const { http } = this.getRpc(chainNameOrId, index); + if (!http) throw new Error(`No RPC URL configured for ${chainNameOrId}`); + return http; + } + + /** + * Get an RPC concurrency level for a given chain name, chain id, or domain id + */ + tryGetRpcConcurrency(chainNameOrId: ChainNameOrId, index = 0): number | null { + const { concurrency } = this.getRpc(chainNameOrId, index); + return concurrency ?? null; } /** diff --git a/typescript/sdk/src/metadata/agentConfig.ts b/typescript/sdk/src/metadata/agentConfig.ts index e948955420..3c16651f66 100644 --- a/typescript/sdk/src/metadata/agentConfig.ts +++ b/typescript/sdk/src/metadata/agentConfig.ts @@ -190,12 +190,6 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge( } break; - case ProtocolType.Fuel: - if (![AgentSignerKeyType.Hex].includes(signerType)) { - return false; - } - break; - default: // Just accept it if we don't know the protocol } diff --git a/typescript/sdk/src/metadata/chainMetadataTypes.ts b/typescript/sdk/src/metadata/chainMetadataTypes.ts index 1009ec0a65..e8ab6f265b 100644 --- a/typescript/sdk/src/metadata/chainMetadataTypes.ts +++ b/typescript/sdk/src/metadata/chainMetadataTypes.ts @@ -28,6 +28,12 @@ export const RpcUrlSchema = z.object({ .string() .url() .describe('The HTTP URL of the RPC endpoint (preferably HTTPS).'), + concurrency: z + .number() + .int() + .positive() + .optional() + .describe('Maximum number of concurrent RPC requests.'), webSocket: z .string() .optional() diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index 5463b82bf4..fa656766b4 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -32,8 +32,6 @@ export enum ProviderType { SolanaWeb3 = 'solana-web3', CosmJs = 'cosmjs', CosmJsWasm = 'cosmjs-wasm', - // TODO fuel provider types not yet defined below - Fuel = 'fuel', } export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< @@ -43,11 +41,53 @@ export const PROTOCOL_TO_DEFAULT_PROVIDER_TYPE: Record< [ProtocolType.Ethereum]: ProviderType.EthersV5, [ProtocolType.Sealevel]: ProviderType.SolanaWeb3, [ProtocolType.Cosmos]: ProviderType.CosmJsWasm, - [ProtocolType.Fuel]: ProviderType.Fuel, }; export type ProviderMap = Partial>; +type ProtocolTypesMapping = { + [ProtocolType.Ethereum]: { + transaction: EthersV5Transaction; + provider: EthersV5Provider; + contract: EthersV5Contract; + receipt: EthersV5TransactionReceipt; + }; + [ProtocolType.Sealevel]: { + transaction: SolanaWeb3Transaction; + provider: SolanaWeb3Provider; + contract: SolanaWeb3Contract; + receipt: SolanaWeb3TransactionReceipt; + }; + [ProtocolType.Cosmos]: { + transaction: CosmJsWasmTransaction; + provider: CosmJsWasmProvider; + contract: CosmJsWasmContract; + receipt: CosmJsWasmTransactionReceipt; + }; +}; + +type ProtocolTyped< + T extends ProtocolType, + K extends keyof ProtocolTypesMapping[T], +> = ProtocolTypesMapping[T][K]; + +export type ProtocolTypedTransaction = ProtocolTyped< + T, + 'transaction' +>; +export type ProtocolTypedProvider = ProtocolTyped< + T, + 'provider' +>; +export type ProtocolTypedContract = ProtocolTyped< + T, + 'contract' +>; +export type ProtocolTypedReceipt = ProtocolTyped< + T, + 'receipt' +>; + /** * Providers with discriminated union of type */ diff --git a/typescript/sdk/src/providers/providerBuilders.ts b/typescript/sdk/src/providers/providerBuilders.ts index 11cd3d15a0..b3a16762e9 100644 --- a/typescript/sdk/src/providers/providerBuilders.ts +++ b/typescript/sdk/src/providers/providerBuilders.ts @@ -127,7 +127,6 @@ export const defaultProviderBuilderMap: ProviderBuilderMap = { [ProviderType.SolanaWeb3]: defaultSolProviderBuilder, [ProviderType.CosmJs]: defaultCosmJsProviderBuilder, [ProviderType.CosmJsWasm]: defaultCosmJsWasmProviderBuilder, - [ProtocolType.Fuel]: defaultFuelProviderBuilder, }; export const protocolToDefaultProviderBuilder: Record< @@ -136,6 +135,5 @@ export const protocolToDefaultProviderBuilder: Record< > = { [ProtocolType.Ethereum]: defaultEthersV5ProviderBuilder, [ProtocolType.Sealevel]: defaultSolProviderBuilder, - [ProtocolType.Fuel]: defaultFuelProviderBuilder, [ProtocolType.Cosmos]: defaultCosmJsWasmProviderBuilder, }; diff --git a/typescript/sdk/src/test/metadata-check.ts b/typescript/sdk/src/test/metadata-check.ts index 7ed52f5d79..97d704a7dc 100644 --- a/typescript/sdk/src/test/metadata-check.ts +++ b/typescript/sdk/src/test/metadata-check.ts @@ -12,7 +12,6 @@ const PROTOCOL_TO_ADDRESS: Record = { [ProtocolType.Ethereum]: ethers.constants.AddressZero, [ProtocolType.Sealevel]: '11111111111111111111111111111111', [ProtocolType.Cosmos]: 'cosmos100000000000000000000000000000000000000', - [ProtocolType.Fuel]: '', }; const PROTOCOL_TO_TX_HASH: Record = { @@ -21,7 +20,6 @@ const PROTOCOL_TO_TX_HASH: Record = { '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111', [ProtocolType.Cosmos]: '0000000000000000000000000000000000000000000000000000000000000000', - [ProtocolType.Fuel]: '', }; async function main() { diff --git a/typescript/sdk/src/token/Token.test.ts b/typescript/sdk/src/token/Token.test.ts index ebac4bbd6c..a23e2f424b 100644 --- a/typescript/sdk/src/token/Token.test.ts +++ b/typescript/sdk/src/token/Token.test.ts @@ -133,16 +133,12 @@ const STANDARD_TO_TOKEN: Record = { name: 'TIA.n', }, [TokenStandard.CwHypSynthetic]: null, - - // Fuel - [TokenStandard.FuelNative]: null, }; const PROTOCOL_TO_ADDRESS: Partial> = { [ProtocolType.Ethereum]: ethers.constants.AddressZero, [ProtocolType.Cosmos]: 'neutron13we0myxwzlpx8l5ark8elw5gj5d59dl6cjkzmt80c5q5cv5rt54qvzkv2a', - [ProtocolType.Fuel]: '', }; const STANDARD_TO_ADDRESS: Partial> = { diff --git a/typescript/sdk/src/token/TokenStandard.ts b/typescript/sdk/src/token/TokenStandard.ts index 31a31721e5..90fcbbb5e1 100644 --- a/typescript/sdk/src/token/TokenStandard.ts +++ b/typescript/sdk/src/token/TokenStandard.ts @@ -38,9 +38,6 @@ export enum TokenStandard { CwHypNative = 'CwHypNative', CwHypCollateral = 'CwHypCollateral', CwHypSynthetic = 'CwHypSynthetic', - - // Fuel (TODO) - FuelNative = 'FuelNative', } // Allows for omission of protocol field in token args @@ -75,9 +72,6 @@ export const TOKEN_STANDARD_TO_PROTOCOL: Record = { CwHypNative: ProtocolType.Cosmos, CwHypCollateral: ProtocolType.Cosmos, CwHypSynthetic: ProtocolType.Cosmos, - - // Fuel (TODO) - FuelNative: ProtocolType.Fuel, }; export const TOKEN_STANDARD_TO_PROVIDER_TYPE: Record< @@ -149,5 +143,4 @@ export const PROTOCOL_TO_NATIVE_STANDARD: Record = [ProtocolType.Ethereum]: TokenStandard.EvmNative, [ProtocolType.Cosmos]: TokenStandard.CosmosNative, [ProtocolType.Sealevel]: TokenStandard.SealevelNative, - [ProtocolType.Fuel]: TokenStandard.FuelNative, }; diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index e7d2dd76b2..d7f0e52979 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -124,6 +124,7 @@ export { isNullish, isNumeric } from './typeof.js'; export { Address, AddressBytes32, + Annotated, CallData, ChainCaip2Id, ChainId, diff --git a/typescript/utils/src/types.ts b/typescript/utils/src/types.ts index 75d17ae2c5..55e3c46003 100644 --- a/typescript/utils/src/types.ts +++ b/typescript/utils/src/types.ts @@ -3,7 +3,6 @@ import type { BigNumber, ethers } from 'ethers'; export enum ProtocolType { Ethereum = 'ethereum', Sealevel = 'sealevel', - Fuel = 'fuel', Cosmos = 'cosmos', } // A type that also allows for literal values of the enum @@ -103,3 +102,7 @@ export type ParsedLegacyMultisigIsmMetadata = { export enum InterchainSecurityModuleType { MULTISIG = 3, } + +export type Annotated = T & { + annotation?: string; +};