Skip to content

Commit 259345e

Browse files
samsiegartmergify-bot
authored and
mergify-bot
committed
feat(wallet): keplr connection
1 parent 0cfddd2 commit 259345e

File tree

7 files changed

+1425
-91
lines changed

7 files changed

+1425
-91
lines changed

packages/wallet/ui/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
"@endo/init": "^0.5.33",
1414
"@agoric/notifier": "^0.3.33",
1515
"@agoric/ui-components": "^0.2.28",
16+
"@cosmjs/launchpad": "^0.27.1",
1617
"@emotion/react": "^11.5.0",
1718
"@emotion/styled": "^11.3.0",
1819
"@mui/icons-material": "^5.1.0",
20+
"@mui/lab": "^5.0.0-alpha.67",
1921
"@mui/material": "^5.1.0",
2022
"@mui/styles": "^5.1.0",
2123
"@testing-library/jest-dom": "^5.11.4",

packages/wallet/ui/src/components/AppBar.jsx

+13-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Tooltip from '@mui/material/Tooltip';
55

66
import WalletConnection from './WalletConnection';
77
import NavDrawer from './NavDrawer';
8+
import ChainConnector from './ChainConnector';
9+
import { withApplicationContext } from '../contexts/Application';
810

911
const logoUrl =
1012
'https://agoric.com/wp-content/themes/agoric_2021_theme/assets/img/logo.svg';
@@ -44,7 +46,7 @@ const useStyles = makeStyles(theme => ({
4446
},
4547
}));
4648

47-
const AppBar = () => {
49+
const AppBar = ({ useChainBackend }) => {
4850
const theme = useTheme();
4951
const classes = useStyles(theme);
5052

@@ -62,6 +64,13 @@ const AppBar = () => {
6264
</a>
6365
</div>
6466
<div className={classes.appBarSection}>
67+
<div className={classes.connector}>
68+
{useChainBackend ? (
69+
<ChainConnector></ChainConnector>
70+
) : (
71+
<WalletConnection></WalletConnection>
72+
)}
73+
</div>
6574
<div className={classes.connector}>
6675
<Tooltip title="Help">
6776
<IconButton
@@ -74,12 +83,11 @@ const AppBar = () => {
7483
</IconButton>
7584
</Tooltip>
7685
</div>
77-
<div className={classes.connector}>
78-
<WalletConnection></WalletConnection>
79-
</div>
8086
</div>
8187
</header>
8288
);
8389
};
8490

85-
export default AppBar;
91+
export default withApplicationContext(AppBar, context => ({
92+
useChainBackend: context.useChainBackend,
93+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
/* eslint-disable react/display-name */
3+
import React, { useEffect, useState } from 'react';
4+
import clsx from 'clsx';
5+
import { makeStyles } from '@mui/styles';
6+
import LoadingButton from '@mui/lab/LoadingButton';
7+
import List from '@mui/material/List';
8+
import ListItem from '@mui/material/ListItem';
9+
import ListItemText from '@mui/material/ListItemText';
10+
import DialogTitle from '@mui/material/DialogTitle';
11+
import Dialog from '@mui/material/Dialog';
12+
import { NETWORK_CONFIGS, suggestChain } from '../util/SuggestChain';
13+
14+
const useStyles = makeStyles(_ => ({
15+
hidden: {
16+
display: 'none',
17+
},
18+
connector: {
19+
display: 'flex',
20+
flexDirection: 'row',
21+
alignItems: 'center',
22+
height: '100%',
23+
},
24+
centeredText: {
25+
textAlign: 'center',
26+
},
27+
dialog: {
28+
minWidth: 240,
29+
},
30+
}));
31+
32+
const ChainConnector = () => {
33+
const classes = useStyles();
34+
const [dialogOpened, setDialogOpened] = useState(false);
35+
const [networkConfig, setNetworkConfig] = useState(null);
36+
const [connectionInProgress, setConnectionInProgress] = useState(false);
37+
38+
const handleClose = () => {
39+
setDialogOpened(false);
40+
};
41+
42+
const selectNetworkConfig = nc => {
43+
setNetworkConfig(nc);
44+
setDialogOpened(false);
45+
};
46+
47+
useEffect(() => {
48+
if (!networkConfig) {
49+
return () => {};
50+
}
51+
52+
let networkChanged = false;
53+
setConnectionInProgress(true);
54+
55+
const connect = async () => {
56+
if (!window.getOfflineSigner || !window.keplr) {
57+
setNetworkConfig(null);
58+
alert('Please install the Keplr extension');
59+
} else if (window.keplr.experimentalSuggestChain) {
60+
try {
61+
const cosmJS = await suggestChain(networkConfig[0]);
62+
if (!networkChanged) {
63+
setConnectionInProgress(false);
64+
window.cosmJS = cosmJS;
65+
}
66+
} catch {
67+
if (!networkChanged) {
68+
setConnectionInProgress(false);
69+
setNetworkConfig(null);
70+
}
71+
}
72+
} else {
73+
setNetworkConfig(null);
74+
alert('Please use the most recent version of the Keplr extension');
75+
}
76+
};
77+
connect();
78+
79+
return () => {
80+
networkChanged = true;
81+
};
82+
}, [networkConfig]);
83+
84+
return (
85+
<>
86+
<div className={clsx('Connector', classes.connector)}>
87+
<LoadingButton
88+
loading={connectionInProgress}
89+
color="primary"
90+
variant="outlined"
91+
onClick={() => setDialogOpened(true)}
92+
>
93+
{networkConfig ? 'Connected' : 'Connect Wallet'}
94+
</LoadingButton>
95+
</div>
96+
<Dialog onClose={handleClose} open={dialogOpened}>
97+
<div className={classes.dialog}>
98+
<DialogTitle className={classes.centeredText}>
99+
Select Network
100+
</DialogTitle>
101+
<List sx={{ pt: 0 }}>
102+
{NETWORK_CONFIGS.map(nc => (
103+
<ListItem
104+
button
105+
selected={nc === networkConfig}
106+
onClick={() => selectNetworkConfig(nc)}
107+
key={nc[0]}
108+
>
109+
<ListItemText
110+
className={classes.centeredText}
111+
primary={nc[1]}
112+
/>
113+
</ListItem>
114+
))}
115+
</List>
116+
</div>
117+
</Dialog>
118+
</>
119+
);
120+
};
121+
122+
export default ChainConnector;

packages/wallet/ui/src/contexts/Application.jsx

+8
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ const Provider = ({ children }) => {
132132
const [issuers, setIssuers] = useReducer(issuersReducer, null);
133133
const [services, setServices] = useState(null);
134134
const [schemaActions, setSchemaActions] = useState(null);
135+
const [useChainBackend, setUseChainBackend] = useState(false);
136+
137+
useEffect(() => {
138+
const urlParams = new URLSearchParams(window.location.search);
139+
const onChain = urlParams.get('onchain') === 'true';
140+
setUseChainBackend(onChain);
141+
}, []);
135142

136143
const setBackend = backend => {
137144
setSchemaActions(backend.actions);
@@ -201,6 +208,7 @@ const Provider = ({ children }) => {
201208
setDeclinedOffers,
202209
closedOffers,
203210
setClosedOffers,
211+
useChainBackend,
204212
};
205213

206214
useDebugLogging(state, [

packages/wallet/ui/src/tests/App.test.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ jest.mock('../views/Dashboard', () => () => 'Dashboard');
1212
jest.mock('../views/Dapps', () => () => 'Dapps');
1313
jest.mock('../views/Contacts', () => () => 'Contacts');
1414
jest.mock('../views/Issuers', () => () => 'Issuers');
15+
jest.mock('@cosmjs/launchpad', () => () => {
16+
jest.mock();
17+
});
1518

1619
const connectionState = 'connecting';
1720
const withApplicationContext = (Component, _) => ({ ...props }) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { SigningCosmosClient } from '@cosmjs/launchpad';
2+
3+
export const AGORIC_COIN_TYPE = 564;
4+
export const COSMOS_COIN_TYPE = 118;
5+
export const NETWORK_CONFIGS = [
6+
['https://main.agoric.net/network-config', 'Agoric Mainnet'],
7+
['https://testnet.agoric.net/network-config', 'Agoric Testnet'],
8+
['https://devnet.agoric.net/network-config', 'Agoric Devnet'],
9+
['https://stage.agoric.net/network-config', 'Agoric Stage'],
10+
];
11+
12+
export async function suggestChain(networkConfig, caption = undefined) {
13+
const coinType = Number(
14+
new URL(networkConfig).searchParams.get('coinType') || AGORIC_COIN_TYPE,
15+
);
16+
const res = await fetch(networkConfig);
17+
if (!res.ok) {
18+
throw Error(`Cannot fetch network: ${res.status}`);
19+
}
20+
const { chainName: chainId, rpcAddrs } = await res.json();
21+
const hostname = new URL(networkConfig).hostname;
22+
const network = hostname.split('.')[0];
23+
let rpc;
24+
let api;
25+
if (network !== hostname) {
26+
rpc = `https://${network}.rpc.agoric.net`;
27+
api = `https://${network}.api.agoric.net`;
28+
} else {
29+
rpc = `http://${rpcAddrs[Math.floor(Math.random() * rpcAddrs.length)]}`;
30+
api = rpc.replace(/(:\d+)?$/, ':1317');
31+
}
32+
const stakeCurrency = {
33+
coinDenom: 'BLD',
34+
coinMinimalDenom: 'ubld',
35+
coinDecimals: 6,
36+
coinGeckoId: undefined,
37+
};
38+
const stableCurrency = {
39+
coinDenom: 'RUN',
40+
coinMinimalDenom: 'urun',
41+
coinDecimals: 6,
42+
coinGeckoId: undefined,
43+
};
44+
const chainInfo = {
45+
rpc,
46+
rest: api,
47+
chainId,
48+
chainName: caption || `Agoric ${network}`,
49+
stakeCurrency,
50+
walletUrlForStaking: `https://${network}.staking.agoric.app`,
51+
bip44: {
52+
coinType,
53+
},
54+
bech32Config: {
55+
bech32PrefixAccAddr: 'agoric',
56+
bech32PrefixAccPub: 'agoricpub',
57+
bech32PrefixValAddr: 'agoricvaloper',
58+
bech32PrefixValPub: 'agoricvaloperpub',
59+
bech32PrefixConsAddr: 'agoricvalcons',
60+
bech32PrefixConsPub: 'agoricvalconspub',
61+
},
62+
currencies: [stakeCurrency, stableCurrency],
63+
feeCurrencies: [stableCurrency],
64+
features: ['stargate', 'ibc-transfer'],
65+
};
66+
await window.keplr.experimentalSuggestChain(chainInfo);
67+
await window.keplr.enable(chainId);
68+
69+
const offlineSigner = window.getOfflineSigner(chainId);
70+
const accounts = await offlineSigner.getAccounts();
71+
const cosmJS = new SigningCosmosClient(
72+
'https://node-cosmoshub-3.keplr.app/rest', // TODO: Provide correct rest API
73+
accounts[0].address,
74+
offlineSigner,
75+
);
76+
77+
return cosmJS;
78+
}

0 commit comments

Comments
 (0)