Skip to content

Commit b7d5cfb

Browse files
committed
feat: add light sdk read and resolver surfaces
1 parent b591ea5 commit b7d5cfb

6 files changed

Lines changed: 624 additions & 220 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect } from 'chai';
2+
3+
import { ProtocolType } from '@hyperlane-xyz/utils';
4+
5+
import type { ChainMap } from '../types.js';
6+
7+
import { createChainMetadataResolver } from './ChainMetadataResolver.js';
8+
import type { ChainMetadata } from './chainMetadataTypes.js';
9+
10+
describe('createChainMetadataResolver', () => {
11+
const metadata = {
12+
ethereum: {
13+
name: 'ethereum',
14+
domainId: 1,
15+
chainId: 1,
16+
protocol: ProtocolType.Ethereum,
17+
rpcUrls: [{ http: 'https://ethereum.invalid' }],
18+
},
19+
cosmoshub: {
20+
name: 'cosmoshub',
21+
domainId: 2,
22+
chainId: 'cosmoshub-4',
23+
protocol: ProtocolType.Cosmos,
24+
rpcUrls: [{ http: 'https://cosmos.invalid' }],
25+
bech32Prefix: 'cosmos',
26+
slip44: 118,
27+
restUrls: [],
28+
grpcUrls: [],
29+
},
30+
} satisfies ChainMap<ChainMetadata>;
31+
32+
it('resolves numeric chain IDs passed as strings', () => {
33+
const resolver = createChainMetadataResolver(metadata);
34+
35+
expect(resolver.tryGetChainMetadata('ethereum')?.name).to.equal('ethereum');
36+
expect(resolver.tryGetChainMetadata('1')?.name).to.equal('ethereum');
37+
expect(resolver.tryGetChainMetadata(1)?.name).to.equal('ethereum');
38+
expect(resolver.tryGetChainName('1')).to.equal('ethereum');
39+
expect(resolver.tryGetChainName(1)).to.equal('ethereum');
40+
});
41+
42+
it('resolves non-numeric string chain IDs', () => {
43+
const resolver = createChainMetadataResolver(metadata);
44+
45+
expect(resolver.tryGetChainMetadata('cosmoshub-4')?.name).to.equal(
46+
'cosmoshub',
47+
);
48+
});
49+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ProtocolType } from '@hyperlane-xyz/utils';
2+
3+
import type { ChainMap, ChainNameOrId } from '../types.js';
4+
5+
import type { ChainMetadata } from './chainMetadataTypes.js';
6+
7+
export interface ChainMetadataResolver<MetaExt = {}> {
8+
readonly metadata: ChainMap<ChainMetadata<MetaExt>>;
9+
getKnownChainNames(): string[];
10+
tryGetChainId(chain: ChainNameOrId): string | number | null;
11+
tryGetChainMetadata(chain: ChainNameOrId): ChainMetadata<MetaExt> | null;
12+
tryGetChainName(chain: ChainNameOrId): string | null;
13+
tryGetDomainId(chain: ChainNameOrId): number | null;
14+
tryGetProtocol(chain: ChainNameOrId): ProtocolType | null;
15+
}
16+
17+
export function createChainMetadataResolver<MetaExt = {}>(
18+
metadata: ChainMap<ChainMetadata<MetaExt>>,
19+
): ChainMetadataResolver<MetaExt> {
20+
const byDomainId = new Map<number, ChainMetadata<MetaExt>>();
21+
const byChainId = new Map<string | number, ChainMetadata<MetaExt>>();
22+
23+
Object.values(metadata).forEach((chainMetadata) => {
24+
byDomainId.set(chainMetadata.domainId, chainMetadata);
25+
26+
if (chainMetadata.chainId !== undefined && chainMetadata.chainId !== null) {
27+
byChainId.set(chainMetadata.chainId, chainMetadata);
28+
29+
const numericChainId = tryNormalizeNumericChainId(chainMetadata.chainId);
30+
if (numericChainId !== null) {
31+
byChainId.set(numericChainId, chainMetadata);
32+
byChainId.set(String(numericChainId), chainMetadata);
33+
}
34+
}
35+
});
36+
37+
const tryGetChainMetadata = (chain: ChainNameOrId) => {
38+
if (typeof chain === 'string') {
39+
return metadata[chain] || byChainId.get(chain) || null;
40+
}
41+
return byDomainId.get(chain) || byChainId.get(chain) || null;
42+
};
43+
44+
return {
45+
metadata,
46+
getKnownChainNames: () => Object.keys(metadata),
47+
tryGetChainId: (chain) => tryGetChainMetadata(chain)?.chainId ?? null,
48+
tryGetChainMetadata,
49+
tryGetChainName: (chain) => tryGetChainMetadata(chain)?.name ?? null,
50+
tryGetDomainId: (chain) => tryGetChainMetadata(chain)?.domainId ?? null,
51+
tryGetProtocol: (chain) => tryGetChainMetadata(chain)?.protocol ?? null,
52+
};
53+
}
54+
55+
function tryNormalizeNumericChainId(chainId: string | number) {
56+
if (typeof chainId === 'number') {
57+
return Number.isSafeInteger(chainId) ? chainId : null;
58+
}
59+
60+
if (!/^\d+$/.test(chainId)) return null;
61+
62+
const numericChainId = Number(chainId);
63+
if (!Number.isSafeInteger(numericChainId)) return null;
64+
if (String(numericChainId) !== chainId) return null;
65+
66+
return numericChainId;
67+
}

typescript/sdk/src/providers/ConfiguredMultiProtocolProvider.ts

Lines changed: 8 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,20 @@
1-
import { Logger } from 'pino';
1+
import { objFilter, objMap, pick } from '@hyperlane-xyz/utils';
22

3-
import {
4-
Address,
5-
HexString,
6-
ProtocolType,
7-
objFilter,
8-
objMap,
9-
pick,
10-
rootLogger,
11-
} from '@hyperlane-xyz/utils';
12-
13-
import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js';
143
import type { ChainMetadata } from '../metadata/chainMetadataTypes.js';
15-
import type { ChainMap, ChainName, ChainNameOrId } from '../types.js';
4+
import type { ChainMap, ChainName } from '../types.js';
165

6+
import {
7+
ConfiguredProviderRegistry,
8+
ConfiguredProviderRegistryOptions,
9+
} from './ConfiguredProviderRegistry.js';
1710
import { MultiProvider, MultiProviderOptions } from './MultiProvider.js';
1811
import {
19-
AleoProvider,
20-
CosmJsNativeProvider,
21-
CosmJsProvider,
22-
CosmJsWasmProvider,
2312
EthersV5Provider,
24-
PROTOCOL_TO_DEFAULT_PROVIDER_TYPE,
25-
ProviderMap,
2613
ProviderType,
27-
RadixProvider,
28-
SolanaWeb3Provider,
29-
StarknetJsProvider,
30-
TronProvider,
3114
TypedProvider,
32-
TypedTransaction,
33-
ViemProvider,
3415
} from './ProviderType.js';
35-
import type {
36-
ProviderBuilderFn,
37-
ProviderBuilderMap,
38-
} from './providerBuilders.js';
39-
import {
40-
TransactionFeeEstimate,
41-
estimateTransactionFee,
42-
} from './transactionFeeEstimators.js';
4316

44-
export interface ConfiguredMultiProtocolProviderOptions {
45-
logger?: Logger;
46-
providers?: ChainMap<ProviderMap<TypedProvider>>;
47-
providerBuilders?: Partial<ProviderBuilderMap>;
48-
}
17+
export interface ConfiguredMultiProtocolProviderOptions extends ConfiguredProviderRegistryOptions {}
4918

5019
export function wrapMultiProviderProviders<MetaExt = {}>(
5120
providers: MultiProvider<MetaExt>['providers'],
@@ -58,24 +27,12 @@ export function wrapMultiProviderProviders<MetaExt = {}>(
5827

5928
export class ConfiguredMultiProtocolProvider<
6029
MetaExt = {},
61-
> extends ChainMetadataManager<MetaExt> {
62-
protected readonly providers: ChainMap<ProviderMap<TypedProvider>>;
63-
protected readonly providerBuilders: Partial<ProviderBuilderMap>;
64-
public readonly logger: Logger;
65-
30+
> extends ConfiguredProviderRegistry<MetaExt> {
6631
constructor(
6732
chainMetadata: ChainMap<ChainMetadata<MetaExt>>,
6833
protected readonly options: ConfiguredMultiProtocolProviderOptions = {},
6934
) {
7035
super(chainMetadata, options);
71-
const loggerModule = new.target?.name || 'ConfiguredMultiProtocolProvider';
72-
this.logger =
73-
options.logger ||
74-
rootLogger.child({
75-
module: loggerModule,
76-
});
77-
this.providers = options.providers || {};
78-
this.providerBuilders = options.providerBuilders || {};
7936
}
8037

8138
static fromMultiProvider<MetaExt = {}>(
@@ -118,175 +75,6 @@ export class ConfiguredMultiProtocolProvider<
11875
});
11976
}
12077

121-
protected getProviderBuilder(
122-
_protocol: ProtocolType,
123-
type: ProviderType,
124-
): ProviderBuilderFn<TypedProvider> | undefined {
125-
return this.providerBuilders[type];
126-
}
127-
128-
tryGetProvider(
129-
chainNameOrId: ChainNameOrId,
130-
type?: ProviderType,
131-
): TypedProvider | null {
132-
const metadata = this.tryGetChainMetadata(chainNameOrId);
133-
if (!metadata) return null;
134-
const { protocol, name, chainId, rpcUrls } = metadata;
135-
if (protocol === ProtocolType.Unknown) return null;
136-
type = type || PROTOCOL_TO_DEFAULT_PROVIDER_TYPE[protocol];
137-
if (!type) return null;
138-
139-
if (this.providers[name]?.[type]) return this.providers[name][type]!;
140-
141-
const builder = this.getProviderBuilder(protocol, type);
142-
if (!rpcUrls.length || !builder) return null;
143-
144-
const provider = builder(rpcUrls, chainId);
145-
this.providers[name] ||= {};
146-
this.providers[name][type] = provider;
147-
return provider;
148-
}
149-
150-
getProvider(
151-
chainNameOrId: ChainNameOrId,
152-
type?: ProviderType,
153-
): TypedProvider {
154-
const provider = this.tryGetProvider(chainNameOrId, type);
155-
if (!provider)
156-
throw new Error(`No provider available for ${chainNameOrId}`);
157-
return provider;
158-
}
159-
160-
protected getSpecificProvider<T>(
161-
chainNameOrId: ChainNameOrId,
162-
type: ProviderType,
163-
): T {
164-
const provider = this.getProvider(chainNameOrId, type);
165-
if (provider.type !== type)
166-
throw new Error(
167-
`Invalid provider type, expected ${type} but found ${provider.type}`,
168-
);
169-
return provider.provider as T;
170-
}
171-
172-
getEthersV5Provider(
173-
chainNameOrId: ChainNameOrId,
174-
): EthersV5Provider['provider'] {
175-
return this.getSpecificProvider<EthersV5Provider['provider']>(
176-
chainNameOrId,
177-
ProviderType.EthersV5,
178-
);
179-
}
180-
181-
getViemProvider(chainNameOrId: ChainNameOrId): ViemProvider['provider'] {
182-
return this.getSpecificProvider<ViemProvider['provider']>(
183-
chainNameOrId,
184-
ProviderType.Viem,
185-
);
186-
}
187-
188-
getSolanaWeb3Provider(
189-
chainNameOrId: ChainNameOrId,
190-
): SolanaWeb3Provider['provider'] {
191-
return this.getSpecificProvider<SolanaWeb3Provider['provider']>(
192-
chainNameOrId,
193-
ProviderType.SolanaWeb3,
194-
);
195-
}
196-
197-
getCosmJsProvider(chainNameOrId: ChainNameOrId): CosmJsProvider['provider'] {
198-
return this.getSpecificProvider<CosmJsProvider['provider']>(
199-
chainNameOrId,
200-
ProviderType.CosmJs,
201-
);
202-
}
203-
204-
getCosmJsWasmProvider(
205-
chainNameOrId: ChainNameOrId,
206-
): CosmJsWasmProvider['provider'] {
207-
return this.getSpecificProvider<CosmJsWasmProvider['provider']>(
208-
chainNameOrId,
209-
ProviderType.CosmJsWasm,
210-
);
211-
}
212-
213-
getCosmJsNativeProvider(
214-
chainNameOrId: ChainNameOrId,
215-
): CosmJsNativeProvider['provider'] {
216-
return this.getSpecificProvider<CosmJsNativeProvider['provider']>(
217-
chainNameOrId,
218-
ProviderType.CosmJsNative,
219-
);
220-
}
221-
222-
getStarknetProvider(
223-
chainNameOrId: ChainNameOrId,
224-
): StarknetJsProvider['provider'] {
225-
return this.getSpecificProvider<StarknetJsProvider['provider']>(
226-
chainNameOrId,
227-
ProviderType.Starknet,
228-
);
229-
}
230-
231-
getRadixProvider(chainNameOrId: ChainNameOrId): RadixProvider['provider'] {
232-
return this.getSpecificProvider<RadixProvider['provider']>(
233-
chainNameOrId,
234-
ProviderType.Radix,
235-
);
236-
}
237-
238-
getAleoProvider(chainNameOrId: ChainNameOrId): AleoProvider['provider'] {
239-
return this.getSpecificProvider<AleoProvider['provider']>(
240-
chainNameOrId,
241-
ProviderType.Aleo,
242-
);
243-
}
244-
245-
getTronProvider(chainNameOrId: ChainNameOrId): TronProvider['provider'] {
246-
return this.getSpecificProvider<TronProvider['provider']>(
247-
chainNameOrId,
248-
ProviderType.Tron,
249-
);
250-
}
251-
252-
setProvider(
253-
chainNameOrId: ChainNameOrId,
254-
provider: TypedProvider,
255-
): TypedProvider {
256-
const chainName = this.getChainName(chainNameOrId);
257-
this.providers[chainName] ||= {};
258-
this.providers[chainName][provider.type] = provider;
259-
return provider;
260-
}
261-
262-
setProviders(providers: ChainMap<TypedProvider>): void {
263-
for (const chain of Object.keys(providers)) {
264-
this.setProvider(chain, providers[chain]);
265-
}
266-
}
267-
268-
estimateTransactionFee({
269-
chainNameOrId,
270-
transaction,
271-
sender,
272-
senderPubKey,
273-
}: {
274-
chainNameOrId: ChainNameOrId;
275-
transaction: TypedTransaction;
276-
sender: Address;
277-
senderPubKey?: HexString;
278-
}): Promise<TransactionFeeEstimate> {
279-
const provider = this.getProvider(chainNameOrId, transaction.type);
280-
const chainMetadata = this.getChainMetadata(chainNameOrId);
281-
return estimateTransactionFee({
282-
transaction,
283-
provider,
284-
chainMetadata,
285-
sender,
286-
senderPubKey,
287-
});
288-
}
289-
29078
override intersect(
29179
chains: ChainName[],
29280
throwIfNotSubset = false,

0 commit comments

Comments
 (0)