Skip to content

Commit c5a29c3

Browse files
authored
feat: connect-as playground enhancements (#165)
* fix: connect-as playground enhancements * fix: SIWE verification result * fix: format * fix: scwUrl from context
1 parent b46d0b3 commit c5a29c3

File tree

3 files changed

+238
-35
lines changed

3 files changed

+238
-35
lines changed

examples/testapp/src/pages/auto-sub-account/index.page.tsx

Lines changed: 206 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Input,
1111
Radio,
1212
RadioGroup,
13+
Select,
1314
Stack,
1415
Text,
1516
VStack,
@@ -25,7 +26,7 @@ import {
2526
toHex,
2627
} from 'viem';
2728
import { privateKeyToAccount } from 'viem/accounts';
28-
import { baseSepolia } from 'viem/chains';
29+
import * as chains from 'viem/chains';
2930
import { useConfig } from '../../context/ConfigContextProvider';
3031
import { useEIP1193Provider } from '../../context/EIP1193ProviderContextProvider';
3132
import { unsafe_generateOrLoadPrivateKey } from '../../utils/unsafe_generateOrLoadPrivateKey';
@@ -38,6 +39,7 @@ interface WalletConnectResponse {
3839
address: string;
3940
capabilities?: Record<string, unknown>;
4041
}>;
42+
chainIds?: string[];
4143
}
4244

4345
const LOCAL_STORAGE_KEY = 'ba-playground:config';
@@ -53,6 +55,8 @@ export default function AutoSubAccount() {
5355
siwe: false,
5456
addSubAccount: false,
5557
});
58+
const [availableChains, setAvailableChains] = useState<string[]>([]);
59+
const [currentChainId, setCurrentChainId] = useState<string>('');
5660
const { subAccountsConfig, setSubAccountsConfig, config, setConfig } = useConfig();
5761
const { provider } = useEIP1193Provider();
5862

@@ -125,6 +129,31 @@ export default function AutoSubAccount() {
125129
setSubAccountsConfig((prev) => ({ ...prev, toOwnerAccount: getSigner }));
126130
}, [signerType, setSubAccountsConfig]);
127131

132+
const getPublicClient = async () => {
133+
if (!provider) throw new Error('Provider not initialized');
134+
135+
// Get current chain ID if not already set
136+
const chainId =
137+
currentChainId ||
138+
((await provider.request({
139+
method: 'eth_chainId',
140+
params: [],
141+
})) as string);
142+
143+
const chainIdDecimal = Number.parseInt(chainId, 16);
144+
145+
// Find the chain in viem/chains
146+
const chain = Object.values(chains).find((c) => c.id === chainIdDecimal);
147+
if (!chain) {
148+
throw new Error(`Chain with ID ${chainIdDecimal} (${chainId}) not found in viem/chains`);
149+
}
150+
151+
return createPublicClient({
152+
chain,
153+
transport: http(),
154+
});
155+
};
156+
128157
const handleRequestAccounts = async () => {
129158
if (!provider) return;
130159

@@ -191,10 +220,7 @@ export default function AutoSubAccount() {
191220
params: [hexMessage, accounts[0]],
192221
});
193222

194-
const publicClient = createPublicClient({
195-
chain: baseSepolia,
196-
transport: http(),
197-
});
223+
const publicClient = await getPublicClient();
198224

199225
const isValid = await publicClient.verifyMessage({
200226
address: accounts[0] as `0x${string}`,
@@ -213,6 +239,9 @@ export default function AutoSubAccount() {
213239
if (!provider || !accounts.length) return;
214240

215241
try {
242+
const publicClient = await getPublicClient();
243+
const chainIdDecimal = publicClient.chain.id;
244+
216245
const typedData = {
217246
types: {
218247
EIP712Domain: [
@@ -230,7 +259,7 @@ export default function AutoSubAccount() {
230259
domain: {
231260
name: 'Test Domain',
232261
version: '1',
233-
chainId: baseSepolia.id,
262+
chainId: chainIdDecimal,
234263
verifyingContract: '0x0000000000000000000000000000000000000000' as `0x${string}`,
235264
},
236265
message: {
@@ -244,11 +273,6 @@ export default function AutoSubAccount() {
244273
params: [accounts[0], JSON.stringify(typedData)],
245274
});
246275

247-
const publicClient = createPublicClient({
248-
chain: baseSepolia,
249-
transport: http(),
250-
});
251-
252276
const isValid = await publicClient.verifyTypedData({
253277
address: accounts[0] as `0x${string}`,
254278
domain: typedData.domain,
@@ -311,14 +335,88 @@ export default function AutoSubAccount() {
311335
method: 'wallet_connect',
312336
params,
313337
})) as WalletConnectResponse;
314-
setLastResult(JSON.stringify(response, null, 2));
338+
339+
// Verify SIWE signature if present
340+
let verificationResult = '';
341+
if (response.accounts && response.accounts.length > 0) {
342+
const account = response.accounts[0];
343+
if (account.capabilities && 'signInWithEthereum' in account.capabilities) {
344+
const siweCapability = account.capabilities.signInWithEthereum as {
345+
message: string;
346+
signature: string;
347+
};
348+
349+
try {
350+
// Parse chain ID from SIWE message
351+
const chainIdMatch = siweCapability.message.match(/Chain ID: (\d+)/);
352+
if (!chainIdMatch) {
353+
throw new Error('Could not extract chain ID from SIWE message');
354+
}
355+
const siweChainId = Number.parseInt(chainIdMatch[1], 10);
356+
357+
// Find the chain in viem/chains
358+
const chain = Object.values(chains).find((c) => c.id === siweChainId);
359+
if (!chain) {
360+
throw new Error(`Chain with ID ${siweChainId} not found in viem/chains`);
361+
}
362+
363+
// Create a public client for the SIWE chain
364+
const publicClient = createPublicClient({
365+
chain,
366+
transport: http(),
367+
});
368+
369+
// Verify the SIWE signature
370+
const isValid = await publicClient.verifyMessage({
371+
address: account.address as `0x${string}`,
372+
message: siweCapability.message,
373+
signature: siweCapability.signature as `0x${string}`,
374+
});
375+
376+
verificationResult = `SIWE Signature Verification: ${isValid ? '✓ VALID' : '✗ INVALID'} (Chain ID: ${siweChainId})\n\n`;
377+
} catch (verifyError) {
378+
console.error('SIWE verification error:', verifyError);
379+
verificationResult = `SIWE Signature Verification: ERROR - ${verifyError instanceof Error ? verifyError.message : String(verifyError)}\n\n`;
380+
}
381+
}
382+
}
383+
384+
setLastResult(verificationResult + JSON.stringify(response, null, 2));
385+
386+
// Extract available chains from response
387+
if (response.chainIds && response.chainIds.length > 0) {
388+
setAvailableChains(response.chainIds);
389+
}
315390

316391
// Call eth_accounts to get and set the accounts after successful connection
317392
const accountsResponse = await provider.request({
318393
method: 'eth_accounts',
319394
params: [],
320395
});
321396
setAccounts(accountsResponse as string[]);
397+
398+
// Get current chain ID
399+
const chainId = await provider.request({
400+
method: 'eth_chainId',
401+
params: [],
402+
});
403+
setCurrentChainId(chainId as string);
404+
} catch (e) {
405+
console.error('error', e);
406+
setLastResult(JSON.stringify(e, null, 2));
407+
}
408+
};
409+
410+
const handleSwitchChain = async (chainId: string) => {
411+
if (!provider) return;
412+
413+
try {
414+
await provider.request({
415+
method: 'wallet_switchEthereumChain',
416+
params: [{ chainId }],
417+
});
418+
setCurrentChainId(chainId);
419+
setLastResult(`Switched to chain: ${chainId}`);
322420
} catch (e) {
323421
console.error('error', e);
324422
setLastResult(JSON.stringify(e, null, 2));
@@ -347,13 +445,21 @@ export default function AutoSubAccount() {
347445
],
348446
});
349447
} else {
448+
// Get current chain ID if not already set
449+
const chainId =
450+
currentChainId ||
451+
((await provider.request({
452+
method: 'eth_chainId',
453+
params: [],
454+
})) as string);
455+
350456
// wallet_sendCalls with paymaster support
351457
response = await provider.request({
352458
method: 'wallet_sendCalls',
353459
params: [
354460
{
355461
version: '1.0',
356-
chainId: numberToHex(baseSepolia.id),
462+
chainId: chainId,
357463
from: accounts[0],
358464
calls: [
359465
{
@@ -381,11 +487,31 @@ export default function AutoSubAccount() {
381487
};
382488

383489
const handleUsdcSend = async (amount: string) => {
384-
if (!provider || accounts.length < 2) return;
490+
if (!provider || !accounts.length) return;
385491

386492
try {
387493
setSendingUsdcAmounts((prev) => ({ ...prev, [amount]: true }));
388-
const usdcAddress = '0x036cbd53842c5426634e7929541ec2318f3dcf7e';
494+
495+
// Get current chain ID if not already set
496+
const chainId =
497+
currentChainId ||
498+
((await provider.request({
499+
method: 'eth_chainId',
500+
params: [],
501+
})) as string);
502+
const chainIdDecimal = Number.parseInt(chainId, 16);
503+
504+
// USDC contract addresses by chain ID
505+
const usdcAddresses: Record<number, string> = {
506+
8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base mainnet
507+
84532: '0x036cbd53842c5426634e7929541ec2318f3dcf7e', // Base Sepolia
508+
};
509+
510+
const usdcAddress = usdcAddresses[chainIdDecimal];
511+
if (!usdcAddress) {
512+
throw new Error(`USDC not supported on chain ID ${chainIdDecimal}`);
513+
}
514+
389515
const to = '0x8d25687829d6b85d9e0020b8c89e3ca24de20a89';
390516
const value = parseUnits(amount, 6); // USDC has 6 decimals
391517

@@ -406,17 +532,44 @@ export default function AutoSubAccount() {
406532
args: [to, value],
407533
});
408534

409-
const response = await provider.request({
410-
method: 'eth_sendTransaction',
411-
params: [
412-
{
413-
from: accounts[0],
414-
to: usdcAddress,
415-
value: '0x0',
416-
data,
417-
},
418-
],
419-
});
535+
let response;
536+
if (sendMethod === 'eth_sendTransaction') {
537+
response = await provider.request({
538+
method: 'eth_sendTransaction',
539+
params: [
540+
{
541+
from: accounts[0],
542+
to: usdcAddress,
543+
value: '0x0',
544+
data,
545+
},
546+
],
547+
});
548+
} else {
549+
// wallet_sendCalls with paymaster support
550+
response = await provider.request({
551+
method: 'wallet_sendCalls',
552+
params: [
553+
{
554+
version: '1.0',
555+
chainId: chainId,
556+
from: accounts[0],
557+
calls: [
558+
{
559+
to: usdcAddress,
560+
value: '0x0',
561+
data,
562+
},
563+
],
564+
capabilities: {
565+
paymasterService: {
566+
url: 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O',
567+
},
568+
},
569+
},
570+
],
571+
});
572+
}
420573
setLastResult(JSON.stringify(response, null, 2));
421574
} catch (e) {
422575
console.error('error', e);
@@ -598,6 +751,31 @@ export default function AutoSubAccount() {
598751
</VStack>
599752
</Box>
600753
)}
754+
{availableChains.length > 0 && (
755+
<FormControl>
756+
<FormLabel>Available Chains</FormLabel>
757+
<Select
758+
value={currentChainId}
759+
onChange={(e) => handleSwitchChain(e.target.value)}
760+
placeholder="Select chain"
761+
>
762+
{availableChains.map((chainId) => (
763+
<option key={chainId} value={chainId}>
764+
Chain ID: {chainId}{' '}
765+
{Number.parseInt(chainId, 16) ? `(${Number.parseInt(chainId, 16)})` : ''}
766+
</option>
767+
))}
768+
</Select>
769+
{currentChainId && (
770+
<Text mt={2} fontSize="sm" color="gray.600" _dark={{ color: 'gray.400' }}>
771+
Current Chain: {currentChainId}{' '}
772+
{Number.parseInt(currentChainId, 16)
773+
? `(${Number.parseInt(currentChainId, 16)})`
774+
: ''}
775+
</Text>
776+
)}
777+
</FormControl>
778+
)}
601779
<Box w="full" textAlign="left" fontSize="lg" fontWeight="bold">
602780
RPCs
603781
</Box>
@@ -741,12 +919,12 @@ export default function AutoSubAccount() {
741919
Send USDC
742920
</Box>
743921
<HStack w="full" spacing={4}>
744-
{['0.01', '0.1', '1'].map((amount) => (
922+
{['0.001', '0.01', '0.1', '1'].map((amount) => (
745923
<Button
746924
key={amount}
747925
flex={1}
748926
onClick={() => handleUsdcSend(amount)}
749-
isDisabled={accounts.length < 2 || sendingUsdcAmounts[amount]}
927+
isDisabled={!accounts.length || sendingUsdcAmounts[amount]}
750928
isLoading={sendingUsdcAmounts[amount]}
751929
loadingText="Sending..."
752930
size="lg"

0 commit comments

Comments
 (0)