Skip to content

Commit c85f8cf

Browse files
complete add chains function
1 parent 9bbc273 commit c85f8cf

File tree

6 files changed

+248
-66
lines changed

6 files changed

+248
-66
lines changed

examples/react/src/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,14 @@ const _wallets: BaseWallet[] = [
132132
// mock1Wallet,
133133
// mock2Wallet,
134134
keplrWallet,
135-
leapWallet,
135+
// leapWallet,
136136
// cosmostationWallet,
137137
// stationWallet,
138138
// galaxyStationWallet,
139139
// walletConnect,
140140
// ledgerWallet,
141141
// cosmosExtensionMetaMask,
142-
walletConnect,
142+
// walletConnect,
143143
// ledgerWallet,
144144
// leapCosmosExtensionMetaMask,
145145
// compassWallet,

packages/core/__tests__/wallet-manager.test.ts

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WalletManager } from '../src/wallet-manager';
22
import { Chain, AssetList } from '@chain-registry/v2-types';
33
import { BaseWallet } from '../src/wallets/base-wallet';
44
import { SignerOptions, EndpointOptions, SignType } from '../src/types';
5-
import { WalletNotExist, ChainNameNotExist, NoValidRpcEndpointFound } from '../src/utils';
5+
import { WalletNotExist, ChainNameNotExist, NoValidRpcEndpointFound, getValidRpcEndpoint } from '../src/utils';
66

77
import { createMockAssetList } from './helpers/mock-asset-list-factory';
88
import { createMockChain } from './helpers/mock-chain-factory';
@@ -11,6 +11,13 @@ import { AminoGenericOfflineSigner } from '@interchainjs/cosmos/types/wallet';
1111
import { SigningClient } from '@interchainjs/cosmos/signing-client';
1212
import { createSignerOption } from './helpers/mock-setting-factory';
1313

14+
jest.mock('../src/utils/endpoint.ts', () => {
15+
return {
16+
...jest.requireActual('../src/utils/endpoint.ts'),
17+
getValidRpcEndpoint: jest.fn()
18+
};
19+
});
20+
1421
describe('WalletManager', () => {
1522
(global as any).window = {
1623

@@ -22,6 +29,7 @@ describe('WalletManager', () => {
2229
};
2330

2431

32+
2533
let chains: Chain[];
2634
let assetLists: AssetList[];
2735
let wallets: BaseWallet[];
@@ -36,7 +44,11 @@ describe('WalletManager', () => {
3644
let assetList1: AssetList
3745
let assetList2: AssetList
3846

47+
48+
3949
beforeEach(() => {
50+
51+
4052
chain1 = createMockChain({ chainName: 'chain-1', chainId: '1', rpcEndpoint: [{ address: 'http://localhost:26657' }], chainType: 'cosmos' });
4153
chain2 = createMockChain({ chainName: 'chain-2', chainId: '2', rpcEndpoint: [{ address: 'http://localhost:26658' }], chainType: 'cosmos' });
4254
assetList1 = createMockAssetList({
@@ -91,6 +103,8 @@ describe('WalletManager', () => {
91103

92104
walletManager = new WalletManager(chains, assetLists, wallets, signerOptions, endpointOptions);
93105

106+
107+
(getValidRpcEndpoint as jest.Mock).mockClear()
94108
});
95109

96110
it('should set assetLists for wallets, after walletManager constructor', () => {
@@ -103,6 +117,15 @@ describe('WalletManager', () => {
103117
expect(wallet2.chainMap.get(chain1.chainId)).toEqual(chain1);
104118
});
105119

120+
it('should have right signerOptions and endpointOptions for each chain', () => {
121+
expect(walletManager.signerOptionMap[chain1.chainName]).toEqual(signerOptions.signing?.(chain1.chainName));
122+
expect(walletManager.signerOptionMap[chain2.chainName]).toEqual(signerOptions.signing?.(chain2.chainName));
123+
expect(walletManager.endpointOptionsMap[chain1.chainName]).toEqual(endpointOptions.endpoints?.[chain1.chainName]);
124+
expect(walletManager.endpointOptionsMap[chain2.chainName]).toEqual(undefined);
125+
expect(walletManager.preferredSignTypeMap[chain1.chainName]).toEqual('amino');
126+
expect(walletManager.preferredSignTypeMap[chain2.chainName]).toEqual('direct');
127+
});
128+
106129
it('should initialize wallets', async () => {
107130
wallet1.init = jest.fn();
108131
wallet2.init = jest.fn();
@@ -116,10 +139,10 @@ describe('WalletManager', () => {
116139
expect(wm).toBeInstanceOf(WalletManager);
117140
});
118141

119-
it('should add new chains', () => {
142+
it('should add new chains', async () => {
120143
const newChain = createMockChain({ chainName: 'chainToAdd', chainId: '3', rpcEndpoint: [{ address: 'http://localhost:26659' }], chainType: 'cosmos' });
121144
const newAssetList = createMockAssetList({ chainName: 'chainToAdd', assets: [] });
122-
walletManager.addChains([newChain], [newAssetList], signerOptions, endpointOptions);
145+
await walletManager.addChains([newChain], [newAssetList], signerOptions, endpointOptions);
123146
expect(walletManager.chains).toContain(newChain);
124147
expect(walletManager.assetLists).toContain(newAssetList);
125148
expect(walletManager.signerOptionMap[newChain.chainName]).toEqual(signerOptions.signing?.(newChain.chainName));
@@ -185,6 +208,7 @@ describe('WalletManager', () => {
185208
});
186209

187210
it('should throw error if no valid RPC endpoint found', async () => {
211+
(getValidRpcEndpoint as jest.Mock).mockReturnValue(Promise.resolve(''));
188212
if (walletManager.endpointOptions?.endpoints?.['chain-1']) {
189213
walletManager.endpointOptions.endpoints['chain-1'].rpc = [];
190214
}
@@ -253,14 +277,80 @@ describe('WalletManager', () => {
253277
expect(chain).toBeUndefined();
254278
});
255279

256-
it('new chains should be added into chainMaps of wallet, after addChains', () => {
280+
it('new chains should be added into chainMaps of wallet, after addChains', async () => {
257281
const newChain = createMockChain({ chainName: 'chainToAdd', chainId: '3', rpcEndpoint: [{ address: 'http://localhost:26659' }], chainType: 'cosmos' });
258282
const newAssetList = createMockAssetList({ chainName: 'chainToAdd', assets: [] });
259-
walletManager.addChains([newChain], [newAssetList], signerOptions, endpointOptions);
283+
await walletManager.addChains([newChain], [newAssetList], signerOptions, endpointOptions);
260284
expect(wallet1.chainMap.get(newChain.chainId)).toEqual(newChain);
261285
expect(wallet2.chainMap.get(newChain.chainId)).toEqual(newChain);
262286
})
263287

288+
it('addChains should update right signerOptions for new chains', async () => {
289+
const newChain = createMockChain({ chainName: 'chainToAdd', chainId: '3', rpcEndpoint: [{ address: 'http://localhost:26659' }], chainType: 'cosmos' });
290+
const newAssetList = createMockAssetList({ chainName: 'chainToAdd', assets: [] });
291+
292+
const endpointOptions1 = {
293+
endpoints: {
294+
'chainToAdd': {
295+
rpc: ['http://localhost:26679']
296+
}
297+
}
298+
} as EndpointOptions
299+
300+
const signerOptions1 = {
301+
signing: jest.fn().mockImplementation((chainName: string) => {
302+
return {
303+
'chainToAdd': {
304+
gasPrice: '1000uatom',
305+
}
306+
}[chainName]
307+
})
308+
} as SignerOptions
309+
310+
311+
await walletManager.addChains([newChain], [newAssetList], signerOptions1, endpointOptions1);
312+
expect(walletManager.chains).toContain(newChain);
313+
expect(walletManager.assetLists).toContain(newAssetList);
314+
expect(walletManager.signerOptionMap[newChain.chainName]).toEqual({ gasPrice: '1000uatom' });
315+
expect(walletManager.endpointOptionsMap[newChain.chainName]).toEqual({ rpc: ['http://localhost:26679'] });
316+
317+
const endpointOptions2 = {
318+
endpoints: {
319+
'chainToAdd': {
320+
rpc: ['http://localhost:26659']
321+
}
322+
}
323+
} as EndpointOptions
324+
325+
const signerOptions2 = {
326+
signing: jest.fn().mockImplementation((chainName: string) => {
327+
return {
328+
'chainToAdd': {
329+
gasPrice: '2000uatom',
330+
}
331+
}[chainName]
332+
})
333+
} as SignerOptions
334+
335+
336+
await walletManager.addChains([newChain], [newAssetList], signerOptions2, endpointOptions2);
337+
expect(walletManager.signerOptionMap[newChain.chainName]).toEqual({ gasPrice: '2000uatom' });
338+
expect(walletManager.endpointOptionsMap[newChain.chainName]).toEqual({ rpc: ['http://localhost:26659'] });
339+
})
340+
341+
it('addChains should update endpoint from chain rpc list', async () => {
342+
(getValidRpcEndpoint as jest.Mock).mockReturnValue('https://rpc.example.com');
343+
344+
const newChain = createMockChain({ chainName: 'chainToAdd', chainId: '3', rpcEndpoint: [{ address: 'https://rpc.example.com' }], chainType: 'cosmos' });
345+
const newAssetList = createMockAssetList({ chainName: 'chainToAdd', assets: [] });
346+
347+
await walletManager.addChains([newChain], [newAssetList]);
348+
expect(walletManager.chains).toContain(newChain);
349+
expect(walletManager.assetLists).toContain(newAssetList);
350+
expect(walletManager.endpointOptionsMap[newChain.chainName]).toEqual({ rpc: ['https://rpc.example.com'] });
351+
352+
})
353+
264354
});
265355

266356

packages/core/src/wallet-manager.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,46 @@ export class WalletManager {
6363
return wm
6464
}
6565

66-
addChains(chains: Chain[], assetLists: AssetList[], signerOptions?: SignerOptions, endpointOptions?: EndpointOptions) {
66+
async addChains(newChains: Chain[], newAssetLists: AssetList[], signerOptions?: SignerOptions, newEndpointOptions?: EndpointOptions) {
6767
const existChains = this.chains
68-
chains.forEach(newChain => {
69-
const existChain = existChains.find(c => c.chainId === newChain.chainId)
70-
if (!existChain) {
68+
69+
const rpcEndpointTable = new Map<Chain['chainId'], string | HttpEndpoint>()
70+
71+
72+
await Promise.all(newChains.map(async (newChain) => {
73+
74+
75+
const rpcEndpoint = await getValidRpcEndpoint(newChain.apis.rpc.map(url => ({ chainType: newChain.chainType, endpoint: url.address })))
76+
77+
if (rpcEndpoint) {
78+
rpcEndpointTable.set(newChain.chainId, rpcEndpoint)
79+
}
80+
}))
81+
82+
const existChainsTable = new Map(existChains.map(chain => [chain.chainId, chain]))
83+
const newAssetListsTable = new Map(newAssetLists.map(assetList => [assetList.chainName, assetList]))
84+
85+
newChains.forEach(newChain => {
86+
87+
if (!existChainsTable.has(newChain.chainId)) {
88+
const assetList = newAssetListsTable.get(newChain.chainName)
89+
7190
this.chains.push(newChain)
72-
const assetList = assetLists.find(a => a.chainName === newChain.chainName)
7391
this.assetLists.push(assetList)
92+
7493
this.wallets.forEach(wallet => {
7594
wallet.addChain(newChain)
95+
wallet.addAssetList(assetList)
7696
})
7797
}
7898

7999
this.signerOptionMap[newChain.chainName] = signerOptions?.signing?.(newChain.chainName)
80-
this.endpointOptionsMap[newChain.chainName] = endpointOptions?.endpoints?.[newChain.chainName]
100+
101+
this.endpointOptionsMap[newChain.chainName] = {
102+
rpc: [newEndpointOptions?.endpoints?.[newChain.chainName]?.rpc?.[0] || rpcEndpointTable.get(newChain.chainId)],
103+
}
81104
})
105+
82106
}
83107

84108
getChainLogoUrl(chainName: ChainName) {

packages/core/src/wallets/base-wallet.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export abstract class BaseWallet {
2424
setAssetLists(assetLists: AssetList[]) {
2525
this.assetLists = assetLists
2626
}
27+
addAssetList(assetList: AssetList) {
28+
this.assetLists.push(assetList)
29+
}
2730
getChainById(chainId: Chain['chainId']): Chain {
2831
const chain = this.chainMap.get(chainId);
2932
if (!chain) {

packages/react/__tests__/store/store.test.ts

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -189,27 +189,6 @@ describe('InterchainStore', () => {
189189
(walletManager.getRpcEndpoint as jest.Mock).mockResolvedValueOnce(rpcEndpoint);
190190
const result = await useStore.getState().getRpcEndpoint('wallet1', 'chain1');
191191
expect(result).toBe(rpcEndpoint);
192-
const state = useStore.getState().getChainWalletState('wallet1', 'chain1');
193-
expect(state?.rpcEndpoint).toBe(rpcEndpoint);
194-
});
195-
196-
it('should add chains', () => {
197-
const newChains = [{ chainName: 'chain2', chainId: '2' }] as Chain[];
198-
const newAssetLists = [{ chainName: 'chain2', assets: [] }] as AssetList[];
199-
const signingMock = jest.fn().mockReturnValue('prefix');
200-
const newSignerOptions = {
201-
signing: signingMock,
202-
} as SignerOptions;
203-
const newEndpointOptions = {
204-
endpoints: { chain2: { rpc: ['http://localhost:26657'] } },
205-
} as EndpointOptions;
206-
useStore.getState().addChains(newChains, newAssetLists, newSignerOptions, newEndpointOptions);
207-
expect(walletManager.addChains).toHaveBeenCalledWith(newChains, newAssetLists, newSignerOptions, newEndpointOptions);
208-
expect(useStore.getState().chains).toContainEqual(newChains[0]);
209-
expect(useStore.getState().assetLists).toContainEqual(newAssetLists[0]);
210-
expect(useStore.getState().signerOptionMap).toEqual({ chain2: 'prefix' });
211-
expect(useStore.getState().endpointOptionsMap).toEqual({ chain2: newEndpointOptions.endpoints?.chain2 ?? {} });
212-
expect(useStore.getState().getChainWalletState('wallet1', 'chain2')?.rpcEndpoint).toBe('http://localhost:26657');
213192
});
214193

215194
it('should not reconnect WalletConnect if already connected', async () => {
@@ -312,23 +291,39 @@ describe('InterchainStore', () => {
312291
expect(walletManager.addChains).toHaveBeenCalledWith(newChains, newAssetLists, undefined, undefined);
313292
});
314293

315-
it('should update endpoints if use addChains to add newChain', () => {
294+
it('should update endpoints if use addChains to add newChain', async () => {
316295
const newChains = [{ chainName: 'chain2', chainId: '2' }] as Chain[];
317296
const newAssetLists = [{ chainName: 'chain2', assets: [] }] as AssetList[];
318297
const newSignerOptions = {
319-
signing: jest.fn().mockReturnValue('prefix'),
298+
signing: jest.fn().mockReturnValue({ gasPrice: '1000' }),
320299
} as SignerOptions;
321300
const newEndpointOptions = {
322301
endpoints: { chain2: { rpc: ['http://localhost:26657'] } },
323302
} as EndpointOptions;
324-
useStore.getState().addChains(newChains, newAssetLists, newSignerOptions, newEndpointOptions);
325-
expect(useStore.getState().endpointOptionsMap).toEqual({ chain2: newEndpointOptions.endpoints?.chain2 ?? {} });
326-
303+
await useStore.getState().addChains(newChains, newAssetLists, newSignerOptions, newEndpointOptions);
304+
expect(useStore.getState().chains).toContainEqual(newChains[0]);
305+
expect(useStore.getState().assetLists).toContainEqual(newAssetLists[0]);
306+
expect(useStore.getState().signerOptionMap).toEqual(expect.objectContaining({ chain2: { gasPrice: '1000' } }));
307+
expect(useStore.getState().endpointOptionsMap).toEqual(expect.objectContaining({ chain2: newEndpointOptions.endpoints?.chain2 }));
308+
309+
const newSignerOptionss = {
310+
signing: jest.fn().mockImplementation((chainName: string) => {
311+
return {
312+
'chain1': { gasPrice: '3000' },
313+
'chain2': { gasPrice: '4000' },
314+
}[chainName]
315+
}),
316+
} as SignerOptions;
327317
const newEndpointOptionss = {
328-
endpoints: { chain2: { rpc: ['http://localhost:26668'] } },
318+
endpoints: {
319+
chain2: { rpc: ['http://localhost:26668'] },
320+
chain1: { rpc: ['http://localhost:27774'] }
321+
},
329322
} as EndpointOptions;
330-
useStore.getState().addChains(newChains, newAssetLists, newSignerOptions, newEndpointOptionss);
331-
expect(useStore.getState().endpointOptionsMap).toEqual({ chain2: newEndpointOptionss.endpoints?.chain2 ?? {} });
323+
324+
await useStore.getState().addChains(newChains, newAssetLists, newSignerOptionss, newEndpointOptionss);
325+
expect(useStore.getState().signerOptionMap).toEqual(expect.objectContaining({ chain1: { gasPrice: '3000' }, chain2: { gasPrice: '4000' } }));
326+
expect(useStore.getState().endpointOptionsMap).toEqual(expect.objectContaining({ chain2: newEndpointOptionss.endpoints?.chain2, chain1: newEndpointOptionss.endpoints?.chain1 }));
332327
})
333328

334329
it('should update wallet state to Connected and fetch account after connection', async () => {
@@ -342,4 +337,61 @@ describe('InterchainStore', () => {
342337
expect(walletManager.getAccount).toHaveBeenCalledWith('wallet1', 'chain1');
343338
expect(state?.account).toBe(account);
344339
});
340+
341+
it('should add new chain wallet states for new chains and wallets', async () => {
342+
const newChains = [{ chainName: 'chain2', chainId: '2' }] as Chain[];
343+
const newAssetLists = [{ chainName: 'chain2', assets: [] }] as AssetList[];
344+
345+
await useStore.getState().addChains(newChains, newAssetLists);
346+
347+
const chainWalletState = useStore.getState().chainWalletState;
348+
349+
expect(chainWalletState).toEqual([
350+
{
351+
"chainName": "chain1",
352+
"errorMessage": "",
353+
"rpcEndpoint": "",
354+
"walletName": "wallet1",
355+
"walletState": "Disconnected",
356+
},
357+
{
358+
"chainName": "chain1",
359+
"errorMessage": "",
360+
"rpcEndpoint": "",
361+
"walletName": "WalletConnect",
362+
"walletState": "Disconnected",
363+
},
364+
{
365+
"account": undefined,
366+
"chainName": "chain2",
367+
"errorMessage": "",
368+
"rpcEndpoint": "",
369+
"walletName": "wallet1",
370+
"walletState": "Disconnected",
371+
},
372+
{
373+
"account": undefined,
374+
"chainName": "chain2",
375+
"errorMessage": "",
376+
"rpcEndpoint": "",
377+
"walletName": "WalletConnect",
378+
"walletState": "Disconnected",
379+
},
380+
]);
381+
});
382+
383+
it('should not duplicate chain wallet states for existing chains and wallets', async () => {
384+
const newChains = [{ chainName: 'chain1', chainId: '1' }] as Chain[];
385+
const newAssetLists = [{ chainName: 'chain1', assets: [] }] as AssetList[];
386+
387+
await useStore.getState().addChains(newChains, newAssetLists);
388+
389+
const chainWalletState = useStore.getState().chainWalletState;
390+
391+
// Ensure no duplicate entries for chain1 and wallet1
392+
const filteredStates = chainWalletState.filter(
393+
(cws) => cws.chainName === 'chain1' && cws.walletName === 'wallet1'
394+
);
395+
expect(filteredStates.length).toBe(1);
396+
});
345397
});

0 commit comments

Comments
 (0)