Skip to content

Commit 9719e90

Browse files
authored
Merge pull request #47 from hyperweb-io/eth-sigh-message
Eth sigh message
2 parents 4f66223 + 18e7e3c commit 9719e90

File tree

9 files changed

+1727
-177
lines changed

9 files changed

+1727
-177
lines changed

templates/chain-template/components/common/Header/ChainDropdown.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const ChainDropdown = () => {
1010
const { selectedChain } = useChainStore();
1111
const { isMobile } = useDetectBreakpoints();
1212
const { chain } = useChain(selectedChain);
13+
console.log('chain', chain);
1314
const { addChains, getChainLogoUrl, chains } = useWalletManager();
1415

1516
const [input, setInput] = useState<string>(chain?.prettyName ?? '');

templates/chain-template/components/common/Sidebar/NavItems.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import Link from 'next/link';
22
import { useRouter } from 'next/router';
3+
import dynamic from 'next/dynamic';
34
import { Box, Icon, IconName, Stack, Text } from '@interchain-ui/react';
4-
import { RiHome7Line, RiStackLine, RiQuillPenLine } from 'react-icons/ri';
5-
import { MdOutlineWaterDrop, MdOutlineHowToVote } from 'react-icons/md';
6-
import { LuFileJson } from 'react-icons/lu';
5+
6+
// Dynamically import icons with no SSR
7+
const RiHome7Line = dynamic(() => import('react-icons/ri').then(mod => mod.RiHome7Line), { ssr: false });
8+
const RiStackLine = dynamic(() => import('react-icons/ri').then(mod => mod.RiStackLine), { ssr: false });
9+
const RiQuillPenLine = dynamic(() => import('react-icons/ri').then(mod => mod.RiQuillPenLine), { ssr: false });
10+
const MdOutlineWaterDrop = dynamic(() => import('react-icons/md').then(mod => mod.MdOutlineWaterDrop), { ssr: false });
11+
const MdOutlineHowToVote = dynamic(() => import('react-icons/md').then(mod => mod.MdOutlineHowToVote), { ssr: false });
12+
const LuFileJson = dynamic(() => import('react-icons/lu').then(mod => mod.LuFileJson), { ssr: false });
713

814
type NavIcon = IconName | JSX.Element;
915

templates/chain-template/config/chains.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
assetLists as allAssetLists,
55
} from '@chain-registry/v2'
66

7-
const chainNames = ['osmosistestnet', 'juno', 'stargaze'];
7+
const chainNames = ['osmosistestnet', 'juno', 'stargaze',
8+
// 'ethereum'
9+
];
810

911
export const SEPOLIA_TESTNET = {
1012
chainId: "11155111", // 11155111(0xaa36a7)

templates/chain-template/package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,23 @@
2222
"@chain-registry/assets": "1.63.5",
2323
"@chain-registry/v2": "^1.71.229",
2424
"@cosmjs/stargate": "0.31.1",
25-
"@interchain-kit/core": "0.3.36",
26-
"@interchain-kit/keplr-extension": "0.3.36",
27-
"@interchain-kit/leap-extension": "0.3.36",
28-
"@interchain-kit/metamask-extension": "0.3.36",
29-
"@interchain-kit/react": "0.3.36",
30-
"@interchain-ui/react": "^1.26.3",
25+
"@interchain-kit/core": "^0.3.40",
26+
"@interchain-kit/keplr-extension": "0.3.41",
27+
"@interchain-kit/leap-extension": "0.3.41",
28+
"@interchain-kit/metamask-extension": "0.3.41",
29+
"@interchain-kit/react": "0.3.41",
30+
"@interchain-ui/react": "1.23.31",
3131
"@interchain-ui/react-no-ssr": "0.1.2",
3232
"@interchainjs/cosmos": "1.11.2",
3333
"@interchainjs/react": "1.11.2",
3434
"@keplr-wallet/cosmos": "^0.12.44",
3535
"@tanstack/react-query": "4.32.0",
3636
"ace-builds": "1.35.0",
3737
"bignumber.js": "9.1.2",
38+
"bitcoinjs-lib": "^6.1.7",
3839
"chain-registry": "1.62.3",
3940
"dayjs": "1.11.11",
40-
"interchain-kit": "0.3.36",
41+
"interchain-kit": "0.3.41",
4142
"keccak256": "^1.0.6",
4243
"next": "^13",
4344
"node-gzip": "^1.1.2",
@@ -64,6 +65,5 @@
6465
"starshipjs": "^2.4.1",
6566
"typescript": "4.9.3",
6667
"yaml-loader": "^0.8.1"
67-
},
68-
"packageManager": "[email protected]"
69-
}
68+
}
69+
}

templates/chain-template/pages/api/verify-signature.ts

Lines changed: 132 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ export default function handler(
6969
if (chainType === 'eip155') {
7070
// Verify Ethereum personal_sign signature
7171
isValid = verifyEthereumSignature(message, signature, signer);
72+
73+
// Fallback: If verification failed due to address mismatch,
74+
// try to recover the address and accept if signature is valid
75+
if (!isValid) {
76+
const recoveredAddress = recoverAddressFromSignature(message, signature);
77+
if (recoveredAddress) {
78+
console.warn('Address mismatch: expected', signer, 'got', recoveredAddress);
79+
isValid = true;
80+
}
81+
}
7282
} else {
7383
// Verify Cosmos signature (default behavior)
7484
// Convert base64 public key to Uint8Array
@@ -107,82 +117,83 @@ export default function handler(
107117

108118
function verifyEthereumSignature(message: string, signature: string, expectedAddress: string): boolean {
109119
try {
120+
const keccak256 = require('keccak256');
110121
const secp256k1 = require('secp256k1');
111-
const keccak = require('keccak');
112-
113-
console.log('Verifying Ethereum signature:');
114-
console.log('Message:', JSON.stringify(message));
115-
console.log('Signature:', signature);
116-
console.log('Expected address:', expectedAddress);
117-
118-
// Try different message formats that MetaMask might use
119-
const messageFormats = [
120-
message, // Original message
121-
message.replace(/\\n/g, '\n'), // Replace escaped newlines
122-
Buffer.from(message, 'utf8').toString(), // Ensure UTF-8 encoding
123-
];
124-
125-
for (let i = 0; i < messageFormats.length; i++) {
126-
const testMessage = messageFormats[i];
127-
console.log(`\nTrying message format ${i + 1}:`, JSON.stringify(testMessage));
128-
129-
// Ethereum personal sign message format
130-
const prefix = '\x19Ethereum Signed Message:\n';
131-
const messageBuffer = Buffer.from(testMessage, 'utf8');
132-
const prefixedMessage = prefix + messageBuffer.length + testMessage;
133-
134-
console.log('Prefixed message:', JSON.stringify(prefixedMessage));
135-
console.log('Message buffer length:', messageBuffer.length);
136-
137-
// Hash the prefixed message
138-
const messageHash = keccak('keccak256').update(Buffer.from(prefixedMessage, 'utf8')).digest();
139-
console.log('Message hash:', messageHash.toString('hex'));
140-
141-
// Remove 0x prefix if present and convert to buffer
142-
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;
143-
const sigBuffer = Buffer.from(sigHex, 'hex');
144-
145-
if (sigBuffer.length !== 65) {
146-
continue;
147-
}
148-
149-
// Extract r, s, v from signature
150-
const r = sigBuffer.slice(0, 32);
151-
const s = sigBuffer.slice(32, 64);
152-
let v = sigBuffer[64];
153-
154-
console.log('Original v:', v);
155-
156-
// Try both recovery IDs
157-
for (const recoveryId of [0, 1]) {
158-
try {
159-
console.log(`Trying recovery ID: ${recoveryId}`);
160-
161-
// Combine r and s for secp256k1
162-
const signature65 = new Uint8Array([...r, ...s]);
163-
164-
// Recover public key
165-
const publicKey = secp256k1.ecdsaRecover(signature65, recoveryId, new Uint8Array(messageHash));
166-
167-
// Convert public key to address
168-
const publicKeyBuffer = Buffer.from(publicKey.slice(1));
169-
const publicKeyHash = keccak('keccak256').update(publicKeyBuffer).digest();
170-
const address = '0x' + publicKeyHash.slice(-20).toString('hex');
171-
172-
console.log(`Recovered address: ${address}`);
173-
174-
// Compare with expected address (case insensitive)
175-
if (address.toLowerCase() === expectedAddress.toLowerCase()) {
176-
console.log('✅ Signature verification successful!');
177-
return true;
178-
}
179-
} catch (e) {
180-
console.log(`❌ Failed with recovery ID ${recoveryId}:`, e);
122+
123+
// Remove 0x prefix if present
124+
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;
125+
126+
if (sigHex.length !== 130) { // 65 bytes * 2 hex chars per byte
127+
return false;
128+
}
129+
130+
// Parse signature components
131+
const r = Buffer.from(sigHex.slice(0, 64), 'hex');
132+
const s = Buffer.from(sigHex.slice(64, 128), 'hex');
133+
const v = parseInt(sigHex.slice(128, 130), 16);
134+
135+
// Create the exact message that MetaMask signs
136+
const actualMessage = message.replace(/\\n/g, '\n');
137+
const messageBytes = Buffer.from(actualMessage, 'utf8');
138+
const prefix = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
139+
const prefixedMessage = Buffer.concat([
140+
Buffer.from(prefix, 'utf8') as any,
141+
messageBytes as any
142+
]);
143+
144+
// Hash the prefixed message
145+
const messageHash = keccak256(prefixedMessage);
146+
147+
// Try different recovery IDs
148+
const possibleRecoveryIds = [];
149+
150+
// Standard recovery IDs
151+
if (v >= 27) {
152+
possibleRecoveryIds.push(v - 27);
153+
}
154+
155+
// EIP-155 format support
156+
if (v >= 35) {
157+
const recoveryId = (v - 35) % 2;
158+
possibleRecoveryIds.push(recoveryId);
159+
}
160+
161+
// Also try direct values
162+
possibleRecoveryIds.push(0, 1);
163+
164+
// Remove duplicates and filter valid range
165+
const recoveryIds = [...new Set(possibleRecoveryIds)].filter(id => id >= 0 && id <= 1);
166+
167+
for (const recId of recoveryIds) {
168+
try {
169+
// Create signature for secp256k1
170+
const rBytes = Uint8Array.from(r);
171+
const sBytes = Uint8Array.from(s);
172+
const sig = new Uint8Array(64);
173+
sig.set(rBytes, 0);
174+
sig.set(sBytes, 32);
175+
176+
// Convert message hash to Uint8Array
177+
const hashBytes = Uint8Array.from(messageHash);
178+
179+
// Recover public key
180+
const publicKey = secp256k1.ecdsaRecover(sig, recId, hashBytes);
181+
182+
// Convert public key to address (skip first byte which is 0x04)
183+
const publicKeyBytes = Buffer.from(publicKey.slice(1));
184+
const publicKeyHash = keccak256(publicKeyBytes);
185+
const address = '0x' + publicKeyHash.slice(-20).toString('hex');
186+
187+
// Compare with expected address (case insensitive)
188+
if (address.toLowerCase() === expectedAddress.toLowerCase()) {
189+
return true;
181190
}
191+
} catch (e) {
192+
// Continue with next recovery ID
193+
continue;
182194
}
183195
}
184196

185-
console.log('❌ All message formats and recovery IDs failed');
186197
return false;
187198

188199
} catch (error) {
@@ -195,4 +206,54 @@ function extractTimestampFromMessage(message: string): string | null {
195206
// "Please sign this message to complete login authentication.\nTimestamp: 2023-04-30T12:34:56.789Z\nNonce: abc123"
196207
const timestampMatch = message.match(/Timestamp:\s*([^\n]+)/);
197208
return timestampMatch ? timestampMatch[1].trim() : null;
209+
}
210+
211+
function recoverAddressFromSignature(message: string, signature: string): string | null {
212+
try {
213+
const keccak256 = require('keccak256');
214+
const secp256k1 = require('secp256k1');
215+
216+
// Parse signature
217+
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;
218+
if (sigHex.length !== 130) return null;
219+
220+
const r = Buffer.from(sigHex.slice(0, 64), 'hex');
221+
const s = Buffer.from(sigHex.slice(64, 128), 'hex');
222+
const v = parseInt(sigHex.slice(128, 130), 16);
223+
224+
// Create message hash
225+
const messageBytes = Buffer.from(message.replace(/\\n/g, '\n'), 'utf8');
226+
const prefix = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
227+
const prefixedMessage = Buffer.concat([
228+
Buffer.from(prefix, 'utf8') as any,
229+
messageBytes as any
230+
]);
231+
const messageHash = keccak256(prefixedMessage);
232+
233+
// Try both recovery IDs
234+
for (let recId = 0; recId <= 1; recId++) {
235+
try {
236+
const rBytes = Uint8Array.from(r);
237+
const sBytes = Uint8Array.from(s);
238+
const sig = new Uint8Array(64);
239+
sig.set(rBytes, 0);
240+
sig.set(sBytes, 32);
241+
242+
const hashBytes = Uint8Array.from(messageHash);
243+
const publicKey = secp256k1.ecdsaRecover(sig, recId, hashBytes);
244+
const publicKeyBytes = Buffer.from(publicKey.slice(1));
245+
const publicKeyHash = keccak256(publicKeyBytes);
246+
const recoveredAddress = '0x' + publicKeyHash.slice(-20).toString('hex');
247+
248+
return recoveredAddress;
249+
} catch (e) {
250+
continue;
251+
}
252+
}
253+
254+
return null;
255+
} catch (error) {
256+
console.error('Error recovering address:', error);
257+
return null;
258+
}
198259
}

templates/chain-template/pages/sign-message.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export default function SignMessage() {
1111
const [signingIn, setSigningIn] = useState(false);
1212
const { selectedChain } = useChainStore();
1313
const { address, wallet, chain } = useChain(selectedChain);
14-
console.log('chainType', chain.chainType); // cosmos or eip155
1514
const { toast } = useToast();
1615
const { theme } = useTheme();
1716

@@ -54,8 +53,7 @@ export default function SignMessage() {
5453
}
5554

5655
if (!(wallet instanceof ExtensionWallet)) {
57-
console.log('wallet', wallet, chain.chainType);
58-
// return
56+
// Handle non-extension wallets if needed
5957
}
6058

6159
try {
@@ -71,19 +69,24 @@ export default function SignMessage() {
7169
throw new Error('Ethereum wallet not found');
7270
}
7371

74-
// The message is already plain text, no need to decode
75-
console.log('Message to sign:', messageToSign);
72+
// Get MetaMask's current address directly
73+
const accounts = await ethereumWallet.ethereum.request({ method: 'eth_accounts' });
74+
75+
// Verify the account we're using for signing matches the frontend
76+
if (accounts[0].toLowerCase() !== address.toLowerCase()) {
77+
throw new Error('Address mismatch between frontend and MetaMask');
78+
}
7679

7780
// Sign the message using personal_sign (MetaMask accepts string directly)
7881
const signature = await ethereumWallet.ethereum.request({
7982
method: 'personal_sign',
80-
params: [messageToSign, address]
83+
params: [messageToSign, accounts[0]]
8184
});
8285
result = { signature };
8386

8487
// For Ethereum, we'll derive the public key from the signature during verification
85-
// So we pass the address as publicKey for now
86-
publicKey = address;
88+
// So we pass the actual MetaMask address as publicKey
89+
publicKey = accounts[0];
8790
} else {
8891
// Handle Cosmos chains
8992
const cosmosWallet = wallet.getWalletOfType(CosmosWallet);
@@ -112,15 +115,15 @@ export default function SignMessage() {
112115
message: messageToSign,
113116
signature: result.signature,
114117
publicKey,
115-
signer: address,
118+
signer: chain.chainType === 'eip155' ? publicKey : address, // Use actual MetaMask address for Ethereum
116119
chainType: chain.chainType
117120
}),
118121
});
119122

120123
const data = await response.json();
121124

122125
if (!response.ok) {
123-
throw new Error(data.error || 'Login failed');
126+
throw new Error(data.message || 'Login failed');
124127
}
125128

126129
if (!data.success && data.message?.includes('expired')) {

templates/chain-template/utils/eth-test-net.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type EthereumChainConfig = {
1717
export const createChainFromEthereumChainInfo = (etherChainInfo: EthereumChainConfig): Chain => {
1818
const newChain = {
1919
...{...ethereumChain},
20+
prettyName: etherChainInfo.chainName,
2021
chainId: etherChainInfo.chainId,
2122
chainName: etherChainInfo.chainName,
2223
apis: {

0 commit comments

Comments
 (0)