Skip to content

Commit

Permalink
[Account Abstraction] Thirdweb Wallet initialization (#2614)
Browse files Browse the repository at this point in the history
* add thirdweb package

* add thirdwebClientId in .env

* render thirdweb wallets (ZERO wallets) in UI

* add enableAccountAbstraction FF

* add `createClient`, `getChain`

* create thirdweb saga

* add core saga logic to initialize thirdweb wallet

* add accountManager to store wallet in memory

* add linkThirdwebWallet ZERO API call

* fix
  • Loading branch information
ratik21 authored Jan 22, 2025
1 parent 15c45e5 commit 9b3a09d
Show file tree
Hide file tree
Showing 20 changed files with 2,984 additions and 591 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ REACT_APP_GOOGLE_PLAY_STORE_PATH='https://play.google.com/store/apps/details?id=
REACT_APP_WALLET_CONNECT_PROJECT_ID=

REACT_APP_WEB_APP_DOWNLOAD_PATH='https://download.zero.tech'

# https://thirdweb.com/team/eafca68a014f457392101c2d57f492305407d96b/ZERO-Development--c39822/connect/in-app-wallets/settings
REACT_APP_THIRDWEB_CLIENT_ID='c3982237d85ae8581aa1c90f551664d4'
3,145 changes: 2,564 additions & 581 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"sass": "^1.49.7",
"stream-browserify": "^3.0.0",
"superagent": "^6.1.0",
"thirdweb": "^5.84.0",
"typescript": "^5.0.4",
"util": "^0.12.5",
"viem": "^2.16.5",
Expand Down
7 changes: 7 additions & 0 deletions src/apps/messenger/Main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { MessengerChat } from '../../components/messenger/chat';
import { MessengerFeed } from '../../components/messenger/feed';
import { JoiningConversationDialog } from '../../components/joining-conversation-dialog';

jest.mock('../../lib/web3/thirdweb/client', () => ({
getThirdWebClient: jest.fn(),
getChain: jest.fn(() => ({
blockExplorers: [{ url: 'https://sepolia.etherscan.io' }],
})),
}));

describe(Main, () => {
const subject = (props: Partial<Properties> = {}) => {
const allProps: Properties = {
Expand Down
7 changes: 7 additions & 0 deletions src/apps/messenger/messenger-main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { Container, Properties } from './messenger-main';
import { Provider as AuthenticationContextProvider } from '../../components/authentication/context';
import { Main } from './Main';

jest.mock('../../lib/web3/thirdweb/client', () => ({
getThirdWebClient: jest.fn(),
getChain: jest.fn(() => ({
blockExplorers: [{ url: 'https://sepolia.etherscan.io' }],
})),
}));

describe('MessengerMain', () => {
const subject = (props: Partial<Properties> = {}) => {
const allProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { Container } from './container';
import { Errors, AccountManagementState, State } from '../../../../store/account-management';
import { RootState } from '../../../../store/reducer';

jest.mock('../../../../lib/web3/thirdweb/client', () => ({
getThirdWebClient: jest.fn(),
getChain: jest.fn(() => ({
blockExplorers: [{ url: 'https://sepolia.etherscan.io' }],
})),
}));

describe('Container', () => {
describe('mapState', () => {
const subject = (inputState: Partial<RootState> = {}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@ import { Button } from '@zero-tech/zui/components/Button';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { State } from '../../../../store/account-management';

const featureFlags = { enableAccountAbstraction: false };
jest.mock('../../../../lib/feature-flags', () => ({
featureFlags: featureFlags,
}));

// Mock the ConnectButton from rainbowkit
jest.mock('@rainbow-me/rainbowkit', () => ({
ConnectButton: {
Custom: ({ children }) => children({ account: { address: '0x123' }, openConnectModal: jest.fn() }),
},
}));

jest.mock('../../../../lib/web3/thirdweb/client', () => ({
getThirdWebClient: jest.fn(),
getChain: jest.fn(() => ({
blockExplorers: [{ url: 'https://sepolia.etherscan.io' }],
})),
}));

const c = bem('.account-management-panel');

describe(AccountManagementPanel, () => {
Expand Down Expand Up @@ -67,23 +79,23 @@ describe(AccountManagementPanel, () => {
expect(props.text()).toEqual('Email added successfully');
});

describe('wallets section', () => {
describe('self-custody wallets section', () => {
it('renders wallets header (no wallet)', () => {
const wrapper = subject({ currentUser: { wallets: [], primaryEmail: '[email protected]' } });

expect(wrapper.find(c('wallets-header')).text()).toEqual('no wallets');
expect(wrapper.find(c('wallets-header')).first().text()).toEqual('no self-custody wallets');
});

it('renders wallets header (1 wallet)', () => {
const wrapper = subject({ currentUser: { wallets: [{ id: 'wallet-id-1' }] } });

expect(wrapper.find(c('wallets-header')).text()).toEqual('1 wallet');
expect(wrapper.find(c('wallets-header')).first().text()).toEqual('1 self-custody wallet');
});

it('renders wallets header (multiple wallets)', () => {
const wrapper = subject({ currentUser: { wallets: [{ id: 'wallet-id-1' }, { id: 'wallet-id-2' }] } });

expect(wrapper.find(c('wallets-header')).text()).toEqual('2 wallets');
expect(wrapper.find(c('wallets-header')).text()).toEqual('2 self-custody wallets');
});

it('renders wallet list items', () => {
Expand Down Expand Up @@ -288,4 +300,104 @@ describe(AccountManagementPanel, () => {
expect(linkWalletModal.prop('isProcessing')).toEqual(true);
});
});

describe('thirdweb wallets section', () => {
beforeEach(() => {
featureFlags.enableAccountAbstraction = true;
});

it('returns null if feature flag is disabled', () => {
featureFlags.enableAccountAbstraction = false;
const wrapper = subject({
currentUser: {
wallets: [{ id: 'wallet-id-1', isThirdWeb: true, publicAddress: '0x123' }],
primaryEmail: '[email protected]',
},
});

const thirdWebWallet = wrapper.find(c('wallets-header')).at(1);
expect(thirdWebWallet.length).toEqual(0);
});

it('does not render thirdweb section when no thirdweb wallets exist', () => {
const wrapper = subject({
currentUser: {
wallets: [],
primaryEmail: '[email protected]',
},
});

const thirdWebWallet = wrapper.find(c('wallets-header')).at(1);
expect(thirdWebWallet.length).toEqual(0);
});

it('renders thirdweb wallets', () => {
const wrapper = subject({
currentUser: {
wallets: [{ id: 'wallet-id-1', isThirdWeb: true, publicAddress: '0x123' }],
primaryEmail: '[email protected]',
},
});

const thirdWebWallet = wrapper.find(c('wallets-header')).at(1);
expect(thirdWebWallet.length).toEqual(1);
expect(thirdWebWallet.text()).toEqual('ZERO Wallet');
expect(wrapper.find('WalletListItem')).toHaveLength(1);
expect(wrapper.find('WalletListItem').at(0).prop('wallet')).toEqual({
id: 'wallet-id-1',
publicAddress: '0x123',
isThirdWeb: true,
});
});

it('renders both self-custody and thirdweb wallets', () => {
const wrapper = subject({
currentUser: {
wallets: [
{ id: 'thirdweb-wallet-id-1', isThirdWeb: true, publicAddress: '0x123' },
{ id: 'self-custody-wallet-id-1', isThirdWeb: false, publicAddress: '0x456' },
],
primaryEmail: '[email protected]',
},
});

// 2 wallets in total
expect(wrapper.find('WalletListItem')).toHaveLength(2);

// 1 self-custody wallet
const selfCustodyWallet = wrapper.find(c('wallets-header')).at(0);
expect(selfCustodyWallet.length).toEqual(1);
expect(selfCustodyWallet.text()).toEqual('1 self-custody wallet');
expect(wrapper.find('WalletListItem').at(0).prop('wallet')).toEqual({
id: 'self-custody-wallet-id-1',
publicAddress: '0x456',
isThirdWeb: false,
});

// 1 thirdweb wallet
const thirdWebWallet = wrapper.find(c('wallets-header')).at(1);
expect(thirdWebWallet.length).toEqual(1);
expect(thirdWebWallet.text()).toEqual('ZERO Wallet');
expect(wrapper.find('WalletListItem').at(1).prop('wallet')).toEqual({
id: 'thirdweb-wallet-id-1',
publicAddress: '0x123',
isThirdWeb: true,
});
});

it('renders wallet with correct etherscan link', () => {
const wallet = { id: 'wallet-id-1', isThirdWeb: true, publicAddress: '0x123' };
const wrapper = subject({
currentUser: {
wallets: [wallet],
primaryEmail: '[email protected]',
},
});

const walletLink = wrapper.find('a');
expect(walletLink.prop('href')).toEqual(`https://etherscan.io/address/${wallet.publicAddress}`);
expect(walletLink.prop('target')).toEqual('_blank');
expect(walletLink.prop('rel')).toEqual('noopener noreferrer');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { ScrollbarContainer } from '../../../scrollbar-container';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { Color, Modal, Variant } from '../../../modal';
import { State as AddWalletState } from '../../../../store/account-management';
import { getChain } from '../../../../lib/web3/thirdweb/client';
import { featureFlags } from '../../../../lib/feature-flags';

const cn = bemClassName('account-management-panel');

Expand Down Expand Up @@ -51,6 +53,12 @@ export class AccountManagementPanel extends React.Component<Properties, State> {
this.props.onBack();
};

getSelfCustodyWallets = () => {
const { currentUser } = this.props;
const wallets = currentUser?.wallets || [];
return wallets.filter((w) => !w.isThirdWeb);
};

renderAddNewWalletButton = () => {
const handleAddWallet = (account, openConnectModal) => {
this.setIsUserLinkingNewWallet(true);
Expand Down Expand Up @@ -83,14 +91,13 @@ export class AccountManagementPanel extends React.Component<Properties, State> {
);
};

renderWalletsSection = () => {
const { currentUser } = this.props;
const wallets = currentUser?.wallets || [];
renderSelfCustodyWalletsSection = () => {
const wallets = this.getSelfCustodyWallets();

return (
<div>
<div {...cn('wallets-header')}>
<span>{wallets.length || 'no'}</span> wallet{wallets.length === 1 ? '' : 's'}
<span>{wallets.length || 'no'}</span> self-custody wallet{wallets.length === 1 ? '' : 's'}
</div>
{wallets.length > 0 ? (
wallets.map((w) => <WalletListItem key={w.id} wallet={w}></WalletListItem>)
Expand All @@ -107,6 +114,38 @@ export class AccountManagementPanel extends React.Component<Properties, State> {
);
};

getThirdWebWallets = () => {
const { currentUser } = this.props;
const wallets = currentUser?.wallets || [];
return wallets.filter((w) => w.isThirdWeb === true);
};

renderThirdWebWalletsSection = () => {
if (!featureFlags.enableAccountAbstraction) {
return null;
}

const wallets = this.getThirdWebWallets();
if (wallets.length === 0) {
return null;
}

const chain = getChain();
const explorerUrl = chain?.blockExplorers[0]?.url || 'https://etherscan.io';
return (
<div {...cn('thirdweb-wallets-container')}>
<div {...cn('wallets-header')}>
<span>ZERO Wallet</span>
</div>
{wallets.map((w) => (
<a key={w.id} href={`${explorerUrl}/address/${w.publicAddress}`} target='_blank' rel='noopener noreferrer'>
<WalletListItem wallet={w}></WalletListItem>
</a>
))}
</div>
);
};

renderEmailSection = () => {
const { currentUser } = this.props;

Expand Down Expand Up @@ -222,8 +261,9 @@ export class AccountManagementPanel extends React.Component<Properties, State> {
<ScrollbarContainer variant='on-hover'>
<div {...cn('panel-content-wrapper')}>
<div {...cn('content')}>
{this.renderWalletsSection()}
{this.renderSelfCustodyWalletsSection()}
{this.renderEmailSection()}
{this.renderThirdWebWalletsSection()}

{this.props.error && (
<Alert variant='error' isFilled>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
text-transform: uppercase;
}

&__thirdweb-wallets-container {
margin-top: 50px;
margin-bottom: 8px;
}

&__email-container {
margin-top: 50px;
margin-bottom: 8px;
Expand Down
8 changes: 8 additions & 0 deletions src/components/messenger/user-profile/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import { SettingsPanelContainer } from './settings-panel/container';
import { AccountManagementContainer } from './account-management-panel/container';
import { DownloadsPanel } from './downloads-panel';
import { LinkedAccountsPanelContainer } from './linked-accounts-panel/container';

jest.mock('../../../lib/web3/thirdweb/client', () => ({
getThirdWebClient: jest.fn(),
getChain: jest.fn(() => ({
blockExplorers: [{ url: 'https://sepolia.etherscan.io' }],
})),
}));

describe(UserProfile, () => {
const subject = (props: Partial<Properties> = {}) => {
const allProps: Properties = {
Expand Down
7 changes: 7 additions & 0 deletions src/components/sidekick/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { Stage as ProfileStage } from '../../store/user-profile';
import { Stage as MessageInfoStage } from '../../store/message-info';
import { MessageInfoContainer } from '../messenger/message-info/container';

jest.mock('../../lib/web3/thirdweb/client', () => ({
getThirdWebClient: jest.fn(),
getChain: jest.fn(() => ({
blockExplorers: [{ url: 'https://sepolia.etherscan.io' }],
})),
}));

describe('Sidekick', () => {
const subject = (props: Partial<Properties> = {}) => {
const allProps = {
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export const config = {
googlePlayStorePath: process.env.REACT_APP_GOOGLE_PLAY_STORE_PATH,
webAppDownloadPath: process.env.REACT_APP_WEB_APP_DOWNLOAD_PATH,
telegramBotUserId: process.env.REACT_APP_TELEGRAM_BOT_USER_ID,
thirwebClientId: process.env.REACT_APP_THIRDWEB_CLIENT_ID,
};
10 changes: 9 additions & 1 deletion src/lib/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,20 @@ export class FeatureFlags {
}

get enableLinkedAccounts() {
return this._getBoolean('enableLinkedAccounts', false);
return this._getBoolean('enableLinkedAccounts', true);
}

set enableLinkedAccounts(value: boolean) {
this._setBoolean('enableLinkedAccounts', value);
}

get enableAccountAbstraction() {
return this._getBoolean('enableAccountAbstraction', false);
}

set enableAccountAbstraction(value: boolean) {
this._setBoolean('enableAccountAbstraction', value);
}
}

export const featureFlags = new FeatureFlags();
Expand Down
19 changes: 19 additions & 0 deletions src/lib/web3/thirdweb/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createThirdwebClient } from 'thirdweb';
import { config } from '../../../config';
import { mainnet, sepolia } from 'thirdweb/chains';

let client;
export function getThirdWebClient() {
client = client ?? createClient();
return client;
}

function createClient() {
return createThirdwebClient({
clientId: config.thirwebClientId,
});
}

export function getChain() {
return process.env.NODE_ENV === 'development' ? sepolia : mainnet;
}
Loading

0 comments on commit 9b3a09d

Please sign in to comment.