Skip to content

Eth sigh message #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const ChainDropdown = () => {
const { selectedChain } = useChainStore();
const { isMobile } = useDetectBreakpoints();
const { chain } = useChain(selectedChain);
console.log('chain', chain);
const { addChains, getChainLogoUrl, chains } = useWalletManager();

const [input, setInput] = useState<string>(chain?.prettyName ?? '');
Expand Down
12 changes: 9 additions & 3 deletions templates/chain-template/components/common/Sidebar/NavItems.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
import { Box, Icon, IconName, Stack, Text } from '@interchain-ui/react';
import { RiHome7Line, RiStackLine, RiQuillPenLine } from 'react-icons/ri';
import { MdOutlineWaterDrop, MdOutlineHowToVote } from 'react-icons/md';
import { LuFileJson } from 'react-icons/lu';

// Dynamically import icons with no SSR
const RiHome7Line = dynamic(() => import('react-icons/ri').then(mod => mod.RiHome7Line), { ssr: false });
const RiStackLine = dynamic(() => import('react-icons/ri').then(mod => mod.RiStackLine), { ssr: false });
const RiQuillPenLine = dynamic(() => import('react-icons/ri').then(mod => mod.RiQuillPenLine), { ssr: false });
const MdOutlineWaterDrop = dynamic(() => import('react-icons/md').then(mod => mod.MdOutlineWaterDrop), { ssr: false });
const MdOutlineHowToVote = dynamic(() => import('react-icons/md').then(mod => mod.MdOutlineHowToVote), { ssr: false });
const LuFileJson = dynamic(() => import('react-icons/lu').then(mod => mod.LuFileJson), { ssr: false });

type NavIcon = IconName | JSX.Element;

Expand Down
4 changes: 3 additions & 1 deletion templates/chain-template/config/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
assetLists as allAssetLists,
} from '@chain-registry/v2'

const chainNames = ['osmosistestnet', 'juno', 'stargaze'];
const chainNames = ['osmosistestnet', 'juno', 'stargaze',
// 'ethereum'
];

export const SEPOLIA_TESTNET = {
chainId: "11155111", // 11155111(0xaa36a7)
Expand Down
20 changes: 10 additions & 10 deletions templates/chain-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,23 @@
"@chain-registry/assets": "1.63.5",
"@chain-registry/v2": "^1.71.229",
"@cosmjs/stargate": "0.31.1",
"@interchain-kit/core": "0.3.36",
"@interchain-kit/keplr-extension": "0.3.36",
"@interchain-kit/leap-extension": "0.3.36",
"@interchain-kit/metamask-extension": "0.3.36",
"@interchain-kit/react": "0.3.36",
"@interchain-ui/react": "^1.26.3",
"@interchain-kit/core": "^0.3.40",
"@interchain-kit/keplr-extension": "0.3.41",
"@interchain-kit/leap-extension": "0.3.41",
"@interchain-kit/metamask-extension": "0.3.41",
"@interchain-kit/react": "0.3.41",
"@interchain-ui/react": "1.23.31",
"@interchain-ui/react-no-ssr": "0.1.2",
"@interchainjs/cosmos": "1.11.2",
"@interchainjs/react": "1.11.2",
"@keplr-wallet/cosmos": "^0.12.44",
"@tanstack/react-query": "4.32.0",
"ace-builds": "1.35.0",
"bignumber.js": "9.1.2",
"bitcoinjs-lib": "^6.1.7",
"chain-registry": "1.62.3",
"dayjs": "1.11.11",
"interchain-kit": "0.3.36",
"interchain-kit": "0.3.41",
"keccak256": "^1.0.6",
"next": "^13",
"node-gzip": "^1.1.2",
Expand All @@ -64,6 +65,5 @@
"starshipjs": "^2.4.1",
"typescript": "4.9.3",
"yaml-loader": "^0.8.1"
},
"packageManager": "[email protected]"
}
}
}
203 changes: 132 additions & 71 deletions templates/chain-template/pages/api/verify-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export default function handler(
if (chainType === 'eip155') {
// Verify Ethereum personal_sign signature
isValid = verifyEthereumSignature(message, signature, signer);

// Fallback: If verification failed due to address mismatch,
// try to recover the address and accept if signature is valid
if (!isValid) {
const recoveredAddress = recoverAddressFromSignature(message, signature);
if (recoveredAddress) {
console.warn('Address mismatch: expected', signer, 'got', recoveredAddress);
isValid = true;
}
}
} else {
// Verify Cosmos signature (default behavior)
// Convert base64 public key to Uint8Array
Expand Down Expand Up @@ -107,82 +117,83 @@ export default function handler(

function verifyEthereumSignature(message: string, signature: string, expectedAddress: string): boolean {
try {
const keccak256 = require('keccak256');
const secp256k1 = require('secp256k1');
const keccak = require('keccak');

console.log('Verifying Ethereum signature:');
console.log('Message:', JSON.stringify(message));
console.log('Signature:', signature);
console.log('Expected address:', expectedAddress);

// Try different message formats that MetaMask might use
const messageFormats = [
message, // Original message
message.replace(/\\n/g, '\n'), // Replace escaped newlines
Buffer.from(message, 'utf8').toString(), // Ensure UTF-8 encoding
];

for (let i = 0; i < messageFormats.length; i++) {
const testMessage = messageFormats[i];
console.log(`\nTrying message format ${i + 1}:`, JSON.stringify(testMessage));

// Ethereum personal sign message format
const prefix = '\x19Ethereum Signed Message:\n';
const messageBuffer = Buffer.from(testMessage, 'utf8');
const prefixedMessage = prefix + messageBuffer.length + testMessage;

console.log('Prefixed message:', JSON.stringify(prefixedMessage));
console.log('Message buffer length:', messageBuffer.length);

// Hash the prefixed message
const messageHash = keccak('keccak256').update(Buffer.from(prefixedMessage, 'utf8')).digest();
console.log('Message hash:', messageHash.toString('hex'));

// Remove 0x prefix if present and convert to buffer
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;
const sigBuffer = Buffer.from(sigHex, 'hex');

if (sigBuffer.length !== 65) {
continue;
}

// Extract r, s, v from signature
const r = sigBuffer.slice(0, 32);
const s = sigBuffer.slice(32, 64);
let v = sigBuffer[64];

console.log('Original v:', v);

// Try both recovery IDs
for (const recoveryId of [0, 1]) {
try {
console.log(`Trying recovery ID: ${recoveryId}`);

// Combine r and s for secp256k1
const signature65 = new Uint8Array([...r, ...s]);

// Recover public key
const publicKey = secp256k1.ecdsaRecover(signature65, recoveryId, new Uint8Array(messageHash));

// Convert public key to address
const publicKeyBuffer = Buffer.from(publicKey.slice(1));
const publicKeyHash = keccak('keccak256').update(publicKeyBuffer).digest();
const address = '0x' + publicKeyHash.slice(-20).toString('hex');

console.log(`Recovered address: ${address}`);

// Compare with expected address (case insensitive)
if (address.toLowerCase() === expectedAddress.toLowerCase()) {
console.log('✅ Signature verification successful!');
return true;
}
} catch (e) {
console.log(`❌ Failed with recovery ID ${recoveryId}:`, e);

// Remove 0x prefix if present
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;

if (sigHex.length !== 130) { // 65 bytes * 2 hex chars per byte
return false;
}

// Parse signature components
const r = Buffer.from(sigHex.slice(0, 64), 'hex');
const s = Buffer.from(sigHex.slice(64, 128), 'hex');
const v = parseInt(sigHex.slice(128, 130), 16);

// Create the exact message that MetaMask signs
const actualMessage = message.replace(/\\n/g, '\n');
const messageBytes = Buffer.from(actualMessage, 'utf8');
const prefix = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
const prefixedMessage = Buffer.concat([
Buffer.from(prefix, 'utf8') as any,
messageBytes as any
]);

// Hash the prefixed message
const messageHash = keccak256(prefixedMessage);

// Try different recovery IDs
const possibleRecoveryIds = [];

// Standard recovery IDs
if (v >= 27) {
possibleRecoveryIds.push(v - 27);
}

// EIP-155 format support
if (v >= 35) {
const recoveryId = (v - 35) % 2;
possibleRecoveryIds.push(recoveryId);
}

// Also try direct values
possibleRecoveryIds.push(0, 1);

// Remove duplicates and filter valid range
const recoveryIds = [...new Set(possibleRecoveryIds)].filter(id => id >= 0 && id <= 1);

for (const recId of recoveryIds) {
try {
// Create signature for secp256k1
const rBytes = Uint8Array.from(r);
const sBytes = Uint8Array.from(s);
const sig = new Uint8Array(64);
sig.set(rBytes, 0);
sig.set(sBytes, 32);

// Convert message hash to Uint8Array
const hashBytes = Uint8Array.from(messageHash);

// Recover public key
const publicKey = secp256k1.ecdsaRecover(sig, recId, hashBytes);

// Convert public key to address (skip first byte which is 0x04)
const publicKeyBytes = Buffer.from(publicKey.slice(1));
const publicKeyHash = keccak256(publicKeyBytes);
const address = '0x' + publicKeyHash.slice(-20).toString('hex');

// Compare with expected address (case insensitive)
if (address.toLowerCase() === expectedAddress.toLowerCase()) {
return true;
}
} catch (e) {
// Continue with next recovery ID
continue;
}
}

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

} catch (error) {
Expand All @@ -195,4 +206,54 @@ function extractTimestampFromMessage(message: string): string | null {
// "Please sign this message to complete login authentication.\nTimestamp: 2023-04-30T12:34:56.789Z\nNonce: abc123"
const timestampMatch = message.match(/Timestamp:\s*([^\n]+)/);
return timestampMatch ? timestampMatch[1].trim() : null;
}

function recoverAddressFromSignature(message: string, signature: string): string | null {
try {
const keccak256 = require('keccak256');
const secp256k1 = require('secp256k1');

// Parse signature
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;
if (sigHex.length !== 130) return null;

const r = Buffer.from(sigHex.slice(0, 64), 'hex');
const s = Buffer.from(sigHex.slice(64, 128), 'hex');
const v = parseInt(sigHex.slice(128, 130), 16);

// Create message hash
const messageBytes = Buffer.from(message.replace(/\\n/g, '\n'), 'utf8');
const prefix = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
const prefixedMessage = Buffer.concat([
Buffer.from(prefix, 'utf8') as any,
messageBytes as any
]);
const messageHash = keccak256(prefixedMessage);

// Try both recovery IDs
for (let recId = 0; recId <= 1; recId++) {
try {
const rBytes = Uint8Array.from(r);
const sBytes = Uint8Array.from(s);
const sig = new Uint8Array(64);
sig.set(rBytes, 0);
sig.set(sBytes, 32);

const hashBytes = Uint8Array.from(messageHash);
const publicKey = secp256k1.ecdsaRecover(sig, recId, hashBytes);
const publicKeyBytes = Buffer.from(publicKey.slice(1));
const publicKeyHash = keccak256(publicKeyBytes);
const recoveredAddress = '0x' + publicKeyHash.slice(-20).toString('hex');

return recoveredAddress;
} catch (e) {
continue;
}
}

return null;
} catch (error) {
console.error('Error recovering address:', error);
return null;
}
}
23 changes: 13 additions & 10 deletions templates/chain-template/pages/sign-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export default function SignMessage() {
const [signingIn, setSigningIn] = useState(false);
const { selectedChain } = useChainStore();
const { address, wallet, chain } = useChain(selectedChain);
console.log('chainType', chain.chainType); // cosmos or eip155
const { toast } = useToast();
const { theme } = useTheme();

Expand Down Expand Up @@ -54,8 +53,7 @@ export default function SignMessage() {
}

if (!(wallet instanceof ExtensionWallet)) {
console.log('wallet', wallet, chain.chainType);
// return
// Handle non-extension wallets if needed
}

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

// The message is already plain text, no need to decode
console.log('Message to sign:', messageToSign);
// Get MetaMask's current address directly
const accounts = await ethereumWallet.ethereum.request({ method: 'eth_accounts' });

// Verify the account we're using for signing matches the frontend
if (accounts[0].toLowerCase() !== address.toLowerCase()) {
throw new Error('Address mismatch between frontend and MetaMask');
}

// Sign the message using personal_sign (MetaMask accepts string directly)
const signature = await ethereumWallet.ethereum.request({
method: 'personal_sign',
params: [messageToSign, address]
params: [messageToSign, accounts[0]]
});
result = { signature };

// For Ethereum, we'll derive the public key from the signature during verification
// So we pass the address as publicKey for now
publicKey = address;
// So we pass the actual MetaMask address as publicKey
publicKey = accounts[0];
} else {
// Handle Cosmos chains
const cosmosWallet = wallet.getWalletOfType(CosmosWallet);
Expand Down Expand Up @@ -112,15 +115,15 @@ export default function SignMessage() {
message: messageToSign,
signature: result.signature,
publicKey,
signer: address,
signer: chain.chainType === 'eip155' ? publicKey : address, // Use actual MetaMask address for Ethereum
chainType: chain.chainType
}),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || 'Login failed');
throw new Error(data.message || 'Login failed');
}

if (!data.success && data.message?.includes('expired')) {
Expand Down
1 change: 1 addition & 0 deletions templates/chain-template/utils/eth-test-net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type EthereumChainConfig = {
export const createChainFromEthereumChainInfo = (etherChainInfo: EthereumChainConfig): Chain => {
const newChain = {
...{...ethereumChain},
prettyName: etherChainInfo.chainName,
chainId: etherChainInfo.chainId,
chainName: etherChainInfo.chainName,
apis: {
Expand Down
Loading
Loading