Skip to content

Commit

Permalink
implement wallet connect
Browse files Browse the repository at this point in the history
- add wallet connect libraries
- add wallet connect context provider
- add new environment variables
- integrate wallet connect with button
- integrate wallet disconnet with button
  • Loading branch information
Reza Erami authored and rezaerami committed Aug 8, 2023
1 parent 7c17a35 commit e9ae531
Show file tree
Hide file tree
Showing 15 changed files with 582 additions and 54 deletions.
280 changes: 259 additions & 21 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion common/config/rush/repo-state.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "0ce1b55854da10cb6fa1b558bd64743d6c481cdc",
"pnpmShrinkwrapHash": "48c1105dd2167eda84f82ddfcb54cf79f62d3577",
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
4 changes: 4 additions & 0 deletions packages/apps/tools/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ FAUCET_PRIVATE_KEY=

# Default Sender, omit to use default value: not-real
DEFAULT_SENDER=

# wallet connect project info
WALLET_CONNECT_PROJECT_ID=
WALLET_CONNECT_RELAY_URL=
1 change: 1 addition & 0 deletions packages/apps/tools/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"Kadena": "Kadena",
"Back": "Back",
"Connect your wallet": "Connect your wallet",
"Disconnect your wallet": "Disconnect your wallet",
"Kadena Transfer": "Kadena Transfer",
"Generate KeyPair (save to file)": "Generate KeyPair (save to file)",
"Kadena Testnet Faucet": "Kadena Testnet Faucet",
Expand Down
2 changes: 2 additions & 0 deletions packages/apps/tools/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const config = {
GAS_LIMIT: process.env.GAS_LIMIT,
FAUCET_PUBLIC_KEY: process.env.FAUCET_PUBLIC_KEY,
FAUCET_PRIVATE_KEY: process.env.FAUCET_PRIVATE_KEY,
WALLET_CONNECT_PROJECT_ID: process.env.WALLET_CONNECT_PROJECT_ID,
WALLET_CONNECT_RELAY_URL: process.env.WALLET_CONNECT_RELAY_URL,
},
webpack: (config, { isServer }) => {
config.module.rules.push({
Expand Down
7 changes: 6 additions & 1 deletion packages/apps/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@
"react-dom": "^18.2.0",
"react-hook-form": "~7.45.0",
"react-scripts": "5.0.1",
"zod": "~3.18.0"
"zod": "~3.18.0",
"@walletconnect/encoding": "~1.0.2",
"@walletconnect/modal": "~2.6.1",
"@walletconnect/sign-client": "~2.8.1",
"@walletconnect/types": "~2.8.1",
"@walletconnect/utils": "~2.9.2"
},
"devDependencies": {
"@jest/reporters": "~29.5.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
jest.mock('@/components/Common/WalletConnectButton', () =>
jest.fn(() => <button>connect wallet</button>),
);

import Layout from './index';

import { render } from '@testing-library/react';
Expand Down
2 changes: 1 addition & 1 deletion packages/apps/tools/src/components/Common/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FooterWrapper, Header, Sidebar } from './partials';
import { footerStyle, gridItemMainStyle, headerStyle } from './styles.css';

import { WalletConnectButton } from '@/components/Global';
import WalletConnectButton from '@/components/Common/WalletConnectButton';
import routes from '@/constants/routes';
import { useLayoutContext } from '@/context';
import classNames from 'classnames';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button } from '@kadena/react-ui';

import { useWalletConnectClient } from '@/context/connect-wallet-context';
import useTranslation from 'next-translate/useTranslation';
import React, { FC } from 'react';

const WalletConnectButton: FC = () => {
const { connect, isInitializing, disconnect, session } =
useWalletConnectClient();
const { t } = useTranslation();

const handleClick = async (): Promise<void> => {
if (session) {
await disconnect();
return;
}

await connect();
};

const buttonTitle = session
? t('Disconnect your wallet')
: t('Connect your wallet');

return (
<Button
title={buttonTitle}
color="positive"
icon="Link"
iconAlign="right"
onClick={handleClick}
disabled={isInitializing}
loading={isInitializing}
>
{buttonTitle}
</Button>
);
};

export default WalletConnectButton;

This file was deleted.

1 change: 0 additions & 1 deletion packages/apps/tools/src/components/Global/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export { Option } from '@/components/Global/Select/Option';
export { SidebarMenu } from '@/components/Global/SidebarMenu';
export { Container } from '@/components/Global/Container';
export { GridCol, GridRow } from '@/components/Global/Grid';
export { WalletConnectButton } from '@/components/Global/WalletConnectButton';
export {
ChainSelect,
type OnChainSelectChange,
Expand Down
244 changes: 244 additions & 0 deletions packages/apps/tools/src/context/connect-wallet-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { env } from '@/utils/env';
import { WalletConnectModal } from '@walletconnect/modal';
import Client from '@walletconnect/sign-client';
import { PairingTypes, SessionTypes } from '@walletconnect/types';
import { getSdkError } from '@walletconnect/utils';
import React, {
createContext,
FC,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';

/**
* Types
*/
interface IWalletConnectClientContext {
client: Client | undefined;
session: SessionTypes.Struct | undefined;
connect: (pairing?: { topic: string }) => Promise<void>;
disconnect: () => Promise<void>;
isInitializing: boolean;
pairings: PairingTypes.Struct[];
accounts: string[] | undefined;
}

/**
* Context
*/
export const WalletConnectClientContext =
createContext<IWalletConnectClientContext>({} as IWalletConnectClientContext);

/**
* walletConnectModal Config
*/
// eslint-disable-next-line @kadena-dev/typedef-var
const walletConnectModal = new WalletConnectModal({
projectId: env('WALLET_CONNECT_PROJECT_ID', ''),
themeMode: 'light',
});

interface IWalletConnectClientContextProviderProps {
children: ReactNode;
}
/**
* Provider
*/
export const WalletConnectClientContextProvider: FC<
IWalletConnectClientContextProviderProps
> = ({ children }) => {
const [client, setClient] = useState<Client>();
const [pairings, setPairings] = useState<PairingTypes.Struct[]>([]);
const [session, setSession] = useState<SessionTypes.Struct>();
const [accounts, setAccounts] = useState<string[]>();

const [isInitializing, setIsInitializing] = useState(false);

const reset = (): void => {
setSession(undefined as unknown as SessionTypes.Struct);
setAccounts(undefined as unknown as string[]);
};

const onSessionConnected = useCallback(
async (clientSession: SessionTypes.Struct) => {
setSession(clientSession);
setAccounts(clientSession?.namespaces?.kadena?.accounts);
},
[],
);

const connect = useCallback(
async (pairing?: { topic: string }) => {
if (typeof client === 'undefined') {
throw new Error('WalletConnect is not initialized');
}

try {
const { uri, approval } = await client.connect({
pairingTopic: pairing?.topic ?? '',

requiredNamespaces: {
kadena: {
methods: [
'kadena_getAccounts_v1',
'kadena_sign_v1',
'kadena_quicksign_v1',
],
chains: [
'kadena:mainnet01',
'kadena:testnet04',
'kadena:development',
],
events: [],
},
},
});

// Open QRCode modal if a URI was returned (i.e. we're not connecting an existing pairing).
if (uri) {
await walletConnectModal.openModal({ uri });
}

const session = await approval();
await onSessionConnected(session);
// Update known pairings after session is connected.
setPairings(client.pairing.getAll({ active: true }));
} catch (e) {
console.error(e);
// ignore rejection
} finally {
// close modal in case it was open
walletConnectModal.closeModal();
}
},
[client, onSessionConnected],
);

const disconnect = useCallback(async () => {
if (typeof client === 'undefined') {
throw new Error('WalletConnect is not initialized');
}
if (typeof session === 'undefined') {
throw new Error('Session is not connected');
}

try {
await client.disconnect({
topic: session.topic,
reason: getSdkError('USER_DISCONNECTED'),
});
} catch (error) {
console.error('SignClient.disconnect failed:', error);
} finally {
// Reset app state after disconnect.
reset();
}
}, [client, session]);

const subscribeToEvents = useCallback(
async (signClient: Client) => {
if (typeof signClient === 'undefined') {
throw new Error('WalletConnect is not initialized');
}

signClient.on('session_update', ({ topic, params }) => {
const { namespaces } = params;
const clientSession = signClient.session.get(topic);
const updatedSession = { ...clientSession, namespaces };
onSessionConnected(updatedSession)
.then(console.log)
.catch(console.error);
});

signClient.on('session_delete', () => {
reset();
});
},
[onSessionConnected],
);

const checkPersistedState = useCallback(
async (signClient: Client) => {
if (typeof signClient === 'undefined') {
throw new Error('WalletConnect is not initialized');
}
// populates existing pairings to state
setPairings(signClient.pairing.getAll({ active: true }));

if (typeof session !== 'undefined') return;
// populates (the last) existing session to state
if (signClient.session.length) {
const lastKeyIndex = signClient.session.keys.length - 1;
const clientSession = signClient.session.get(
signClient.session.keys[lastKeyIndex],
);
await onSessionConnected(clientSession);
return clientSession;
}
},
[session, onSessionConnected],
);

const createClient = useCallback(async () => {
try {
setIsInitializing(true);

const _client = await Client.init({
relayUrl: env('WALLET_CONNECT_RELAY_URL', ''),
projectId: env('WALLET_CONNECT_PROJECT_ID', ''),
});

setClient(_client);
await subscribeToEvents(_client);
await checkPersistedState(_client);
// eslint-disable-next-line no-useless-catch
} catch (err) {
throw err;
} finally {
setIsInitializing(false);
}
}, [checkPersistedState, subscribeToEvents]);

useEffect(() => {
if (!client) {
createClient().then(console.log).catch(console.error);
}
}, [client, createClient]);

const value = useMemo(
() => ({
pairings,
isInitializing,
accounts,
client,
session,
connect,
disconnect,
}),
[pairings, isInitializing, accounts, client, session, connect, disconnect],
);

return (
<WalletConnectClientContext.Provider
value={{
...value,
}}
>
{children}
</WalletConnectClientContext.Provider>
);
};

export const useWalletConnectClient = (): IWalletConnectClientContext => {
const context = useContext(WalletConnectClientContext);
if (!context) {
throw new Error(
'useWalletConnectClient must be used within a WalletConnectClientContextProvider',
);
}
return context;
};
Loading

1 comment on commit e9ae531

@vercel
Copy link

@vercel vercel bot commented on e9ae531 Aug 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

tools – ./packages/apps/tools

tools.kadena.io
tools-kadena-js.vercel.app
kadena-js-transfer.vercel.app
tools-git-main-kadena-js.vercel.app

Please sign in to comment.