Skip to content

Commit b532c33

Browse files
works up until calling linkExternalWallets
1 parent 6096fa7 commit b532c33

6 files changed

Lines changed: 287 additions & 70 deletions

File tree

examples/passport/logged-in-user-with-nextjs/src/app/globals.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ body {
1515

1616
.mb-1 {
1717
margin-bottom: 1rem;
18+
}
19+
20+
.mt-1 {
21+
margin-top: 1rem;
1822
}

examples/passport/logged-in-user-with-nextjs/src/app/link-external-wallet/page.tsx

Lines changed: 108 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useState, useEffect } from 'react';
44
import { Button, Heading, Table, Link } from '@biom3/react';
55
import NextLink from 'next/link';
66
import { passportInstance } from '../utils/setupDefault';
77
import { generateNonce } from 'siwe';
8-
import { useConnect, useSignTypedData } from 'wagmi';
9-
import { config } from '../utils/wagmiConfig';
10-
import { metaMask } from 'wagmi/connectors';
8+
import { Provider } from '@imtbl/sdk/passport';
9+
10+
// Define a type for the window.ethereum object
11+
declare global {
12+
interface Window {
13+
ethereum?: any;
14+
}
15+
}
1116

1217
export default function LinkExternalWallet() {
1318
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
@@ -18,12 +23,25 @@ export default function LinkExternalWallet() {
1823
const [linkingStatus, setLinkingStatus] = useState<string>('');
1924
const [linkingError, setLinkingError] = useState<string | null>(null);
2025
const [linkedAddresses, setLinkedAddresses] = useState<string[]>([]);
26+
const [isMetaMaskInstalled, setIsMetaMaskInstalled] = useState<boolean>(false);
27+
28+
// Check if MetaMask is installed
29+
useEffect(() => {
30+
if (typeof window !== 'undefined') {
31+
setIsMetaMaskInstalled(!!window.ethereum);
32+
}
33+
}, []);
2134

22-
const { connectAsync } = useConnect({
23-
config
24-
});
35+
// fetch the Passport provider from the Passport instance
36+
const [passportProvider, setPassportProvider] = useState<Provider>();
2537

26-
const { signTypedDataAsync } = useSignTypedData();
38+
useEffect(() => {
39+
const fetchPassportProvider = async () => {
40+
const passportProvider = await passportInstance.connectEvm();
41+
setPassportProvider(passportProvider);
42+
};
43+
fetchPassportProvider();
44+
}, []);
2745

2846
const loginWithPassport = async () => {
2947
if (!passportInstance) return;
@@ -51,11 +69,20 @@ export default function LinkExternalWallet() {
5169
}
5270

5371
try {
54-
// Use metaMask connector specifically instead of generic injected
55-
const result = await connectAsync({ connector: metaMask() });
56-
if (result?.accounts?.[0]) {
72+
if (typeof window === 'undefined' || !window.ethereum) {
73+
setLinkingError('MetaMask not installed. Please install MetaMask to continue.');
74+
return;
75+
}
76+
77+
const accounts = await window.ethereum.request({
78+
method: 'eth_requestAccounts'
79+
});
80+
81+
if (accounts && accounts.length > 0) {
5782
setWalletConnected(true);
58-
setExternalWalletAddress(result.accounts[0]);
83+
setExternalWalletAddress(accounts[0]);
84+
} else {
85+
throw new Error('No accounts found');
5986
}
6087
} catch (error) {
6188
console.error('Error connecting to wallet:', error);
@@ -80,11 +107,7 @@ export default function LinkExternalWallet() {
80107
const formattedExternalWalletAddress = externalWalletAddress.toLowerCase() as `0x${string}`;
81108
const formattedPassportAddress = accountAddress.toLowerCase() as `0x${string}`;
82109

83-
// Sign the message using EIP-712
84-
const signature = await signTypedDataAsync({
85-
domain: {
86-
chainId: BigInt(1)
87-
},
110+
const dataToSign = {
88111
types: {
89112
EIP712Domain: [
90113
{
@@ -112,15 +135,43 @@ export default function LinkExternalWallet() {
112135
]
113136
},
114137
primaryType: "LinkWallet",
138+
domain: {
139+
chainId: 13473,
140+
},
115141
message: {
116142
walletAddress: formattedExternalWalletAddress,
117143
immutablePassportAddress: formattedPassportAddress,
118144
condition: "I agree to link this wallet to my Immutable Passport account.",
119145
nonce
120146
}
147+
}
148+
149+
if (typeof window === 'undefined' || !window.ethereum) {
150+
throw new Error('MetaMask not installed');
151+
}
152+
153+
// Sign the message using window.ethereum directly
154+
const signature = await window.ethereum.request({
155+
method: 'eth_signTypedData_v4',
156+
params: [formattedExternalWalletAddress, JSON.stringify(dataToSign)]
121157
});
122158

159+
// Import and use our signature validation function
160+
const { validateSignatureComprehensive } = await import('../utils/validateEIP712Signature');
161+
162+
const isValid = await validateSignatureComprehensive(
163+
formattedExternalWalletAddress, // the signer address
164+
dataToSign, // the payload
165+
signature,
166+
window.ethereum // pass the provider for contract wallet validation if needed
167+
);
168+
169+
if (!isValid) {
170+
throw new Error('Invalid signature');
171+
}
172+
123173
setLinkingStatus('Linking wallet...');
174+
124175

125176
// Call the linkExternalWallet method to link the wallet
126177
const result = await passportInstance.linkExternalWallet({
@@ -129,6 +180,8 @@ export default function LinkExternalWallet() {
129180
signature,
130181
nonce
131182
});
183+
184+
console.log('result', result);
132185

133186
const linkedAddresses = await passportInstance.getLinkedAddresses();
134187
setLinkedAddresses(linkedAddresses);
@@ -147,15 +200,39 @@ export default function LinkExternalWallet() {
147200
<Heading size="medium" className="mb-1">
148201
Link External Wallet
149202
</Heading>
150-
<Button
151-
className="mb-1"
152-
size="medium"
153-
onClick={loginWithPassport}
154-
disabled={isLoggedIn}>
155-
{isLoggedIn ? 'Logged In' : 'Login'}
156-
</Button>
157-
158-
<Table>
203+
204+
<div className="mb-1">
205+
{!isLoggedIn ? (
206+
<Button
207+
size="medium"
208+
onClick={loginWithPassport}>
209+
Login with Passport
210+
</Button>
211+
) : (
212+
<>
213+
{!walletConnected && (
214+
<Button
215+
size="medium"
216+
onClick={connectWallet}
217+
disabled={walletConnected || !isMetaMaskInstalled}>
218+
Connect Metamask
219+
</Button>
220+
)}
221+
222+
{walletConnected && (
223+
<Button
224+
size="medium"
225+
onClick={linkWallet}
226+
disabled={isLinking || !walletConnected}>
227+
{isLinking ? 'Linking...' : 'Link Wallet'}
228+
</Button>
229+
)}
230+
</>
231+
)}
232+
</div>
233+
234+
235+
<Table className="mb-1">
159236
<Table.Head>
160237
<Table.Row>
161238
<Table.Cell>Attribute</Table.Cell>
@@ -171,6 +248,10 @@ export default function LinkExternalWallet() {
171248
<Table.Cell><b>Account Address</b></Table.Cell>
172249
<Table.Cell>{accountAddress || 'N/A'}</Table.Cell>
173250
</Table.Row>
251+
<Table.Row>
252+
<Table.Cell><b>MetaMask Available</b></Table.Cell>
253+
<Table.Cell>{isMetaMaskInstalled ? 'Yes' : 'No'}</Table.Cell>
254+
</Table.Row>
174255
<Table.Row>
175256
<Table.Cell><b>External Wallet</b></Table.Cell>
176257
<Table.Cell>{externalWalletAddress || 'Not connected'}</Table.Cell>
@@ -196,26 +277,7 @@ export default function LinkExternalWallet() {
196277
</Table.Body>
197278
</Table>
198279

199-
200-
<Button
201-
size="medium"
202-
onClick={connectWallet}
203-
disabled={!isLoggedIn || walletConnected}>
204-
Connect Wallet
205-
</Button>
206-
207-
{walletConnected && (
208-
<Button
209-
size="medium"
210-
onClick={linkWallet}
211-
className="mt-6"
212-
disabled={isLinking || !isLoggedIn || !walletConnected}>
213-
{isLinking ? 'Linking...' : 'Link Wallet'}
214-
</Button>
215-
)}
216-
217-
218-
<div className="mt-4">
280+
<div className="mt-1">
219281
<Link rc={<NextLink href="/" />}>Return to Examples</Link>
220282
</div>
221283
</>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// #doc passport-wallets-nextjs-sign-isvalidsignature
2+
import { Provider } from '@imtbl/passport';
3+
import { BrowserProvider } from 'ethers';
4+
import { Contract } from 'ethers';
5+
6+
// https://eips.ethereum.org/EIPS/eip-1271#specification
7+
// EIP-1271 states that `isValidSignature` must return the following value if the signature is valid
8+
export const ERC_1271_MAGIC_VALUE = '0x1626ba7e';
9+
10+
export const isValidSignature = async (
11+
address: string, // The Passport wallet address returned from eth_requestAccounts
12+
digest: string | Uint8Array,
13+
signature: string,
14+
zkEvmProvider: Provider, // can be any provider, Passport or not
15+
) => {
16+
const contract = new Contract(
17+
address,
18+
['function isValidSignature(bytes32, bytes) public view returns (bytes4)'],
19+
new BrowserProvider(zkEvmProvider),
20+
);
21+
22+
const isValidSignatureHex = await contract.isValidSignature(digest, signature);
23+
return isValidSignatureHex === ERC_1271_MAGIC_VALUE;
24+
};
25+
// #enddoc passport-wallets-nextjs-sign-eip712-isvalidsignature

0 commit comments

Comments
 (0)