Skip to content

Commit 3459b37

Browse files
committed
wip: ethereum sign message
1 parent 445efe6 commit 3459b37

File tree

4 files changed

+193
-25
lines changed

4 files changed

+193
-25
lines changed

templates/chain-template/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"chain-registry": "1.62.3",
3939
"dayjs": "1.11.11",
4040
"interchain-kit": "0.3.36",
41+
"keccak256": "^1.0.6",
4142
"next": "^13",
4243
"node-gzip": "^1.1.2",
4344
"react": "18.2.0",
@@ -46,6 +47,7 @@
4647
"react-dropzone": "^14.2.3",
4748
"react-icons": "5.2.1",
4849
"react-markdown": "9.0.1",
50+
"secp256k1": "^5.0.1",
4951
"zustand": "4.5.2"
5052
},
5153
"devDependencies": {

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

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { NextApiRequest, NextApiResponse } from 'next';
22
import { verifyADR36Amino } from '@keplr-wallet/cosmos';
33
import { decode } from 'bech32';
4+
import { createHash } from 'crypto';
45

56
type RequestBody = {
67
message: string;
78
signature: string;
89
publicKey: string;
910
signer: string;
11+
chainType?: string;
1012
}
1113

1214
type ResponseData = {
@@ -29,7 +31,7 @@ export default function handler(
2931
});
3032
}
3133

32-
const { message, signature, publicKey, signer } = req.body as RequestBody;
34+
const { message, signature, publicKey, signer, chainType } = req.body as RequestBody;
3335

3436
if (!message || !signature || !publicKey || !signer) {
3537
return res.status(400).json({
@@ -62,18 +64,26 @@ export default function handler(
6264
console.warn('No timestamp found in message, skipping timestamp validation');
6365
}
6466

65-
// Convert base64 public key to Uint8Array
66-
const pubKeyBytes = new Uint8Array(Buffer.from(publicKey, 'base64'));
67-
// Convert base64 signature to Uint8Array
68-
const signatureBytes = new Uint8Array(Buffer.from(signature, 'base64'));
67+
let isValid: boolean;
6968

70-
const isValid = verifyADR36Amino(
71-
decode(signer).prefix,
72-
signer,
73-
message,
74-
pubKeyBytes,
75-
signatureBytes
76-
);
69+
if (chainType === 'eip155') {
70+
// Verify Ethereum personal_sign signature
71+
isValid = verifyEthereumSignature(message, signature, signer);
72+
} else {
73+
// Verify Cosmos signature (default behavior)
74+
// Convert base64 public key to Uint8Array
75+
const pubKeyBytes = new Uint8Array(Buffer.from(publicKey, 'base64'));
76+
// Convert base64 signature to Uint8Array
77+
const signatureBytes = new Uint8Array(Buffer.from(signature, 'base64'));
78+
79+
isValid = verifyADR36Amino(
80+
decode(signer).prefix,
81+
signer,
82+
message,
83+
pubKeyBytes,
84+
signatureBytes
85+
);
86+
}
7787

7888
if (isValid) {
7989
return res.status(200).json({
@@ -95,6 +105,55 @@ export default function handler(
95105
}
96106
}
97107

108+
function verifyEthereumSignature(message: string, signature: string, expectedAddress: string): boolean {
109+
try {
110+
const secp256k1 = require('secp256k1');
111+
const { keccak256 } = require('keccak');
112+
113+
// Ethereum personal sign message format
114+
const prefix = '\x19Ethereum Signed Message:\n';
115+
const prefixedMessage = prefix + message.length + message;
116+
117+
// Hash the prefixed message
118+
const messageHash = keccak256(Buffer.from(prefixedMessage, 'utf8'));
119+
120+
// Remove 0x prefix if present and convert to buffer
121+
const sigHex = signature.startsWith('0x') ? signature.slice(2) : signature;
122+
const sigBuffer = Buffer.from(sigHex, 'hex');
123+
124+
if (sigBuffer.length !== 65) {
125+
throw new Error('Invalid signature length');
126+
}
127+
128+
// Extract r, s, v from signature
129+
const r = sigBuffer.slice(0, 32);
130+
const s = sigBuffer.slice(32, 64);
131+
let v = sigBuffer[64];
132+
133+
// Handle recovery id
134+
if (v < 27) {
135+
v += 27;
136+
}
137+
const recoveryId = v - 27;
138+
139+
// Combine r and s for secp256k1
140+
const signature65 = new Uint8Array([...r, ...s]);
141+
142+
// Recover public key
143+
const publicKey = secp256k1.ecdsaRecover(signature65, recoveryId, new Uint8Array(messageHash));
144+
145+
// Convert public key to address
146+
const publicKeyHash = keccak256(publicKey.slice(1)); // Remove the 0x04 prefix
147+
const address = '0x' + publicKeyHash.slice(-20).toString('hex');
148+
149+
// Compare with expected address (case insensitive)
150+
return address.toLowerCase() === expectedAddress.toLowerCase();
151+
152+
} catch (error) {
153+
console.error('Error verifying Ethereum signature:', error);
154+
return false;
155+
}
156+
}
98157

99158
function extractTimestampFromMessage(message: string): string | null {
100159
// "Please sign this message to complete login authentication.\nTimestamp: 2023-04-30T12:34:56.789Z\nNonce: abc123"

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

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { Container, Button, Stack, Text, useTheme } from '@interchain-ui/react';
33
import { useChain } from '@interchain-kit/react';
44
import { useChainStore } from '@/contexts';
55
import { useToast } from '@/hooks';
6-
import { CosmosWallet, ExtensionWallet } from '@interchain-kit/core';
6+
import { CosmosWallet, ExtensionWallet, EthereumWallet } from '@interchain-kit/core';
77

88
export default function SignMessage() {
99
const [message, setMessage] = useState('');
1010
const [loading, setLoading] = useState(false);
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
1415
const { toast } = useToast();
1516
const { theme } = useTheme();
1617

@@ -54,20 +55,44 @@ export default function SignMessage() {
5455

5556
if (!(wallet instanceof ExtensionWallet)) {
5657
console.log('wallet', wallet, chain.chainType);
57-
return
58+
// return
5859
}
5960

6061
try {
6162
setSigningIn(true);
62-
const cosmosWallet = wallet.getWalletOfType(CosmosWallet)
63-
64-
// Sign the message
65-
const result = await cosmosWallet!.signArbitrary(chain.chainId, address, message);
66-
67-
// Get the public key
68-
const account = await wallet?.getAccount(chain.chainId);
69-
if (!account?.pubkey) {
70-
throw new Error('Failed to get public key');
63+
let result: { signature: string };
64+
let publicKey: string;
65+
66+
if (chain.chainType === 'eip155') {
67+
// Handle Ethereum chains
68+
const ethereumWallet = wallet.getWalletOfType(EthereumWallet);
69+
if (!ethereumWallet) {
70+
throw new Error('Ethereum wallet not found');
71+
}
72+
73+
// Sign the message using personal_sign
74+
const signature = await ethereumWallet.signMessage(message);
75+
result = { signature };
76+
77+
// For Ethereum, we'll derive the public key from the signature during verification
78+
// So we pass the address as publicKey for now
79+
publicKey = address;
80+
} else {
81+
// Handle Cosmos chains
82+
const cosmosWallet = wallet.getWalletOfType(CosmosWallet);
83+
if (!cosmosWallet) {
84+
throw new Error('Cosmos wallet not found');
85+
}
86+
87+
// Sign the message
88+
result = await cosmosWallet.signArbitrary(chain.chainId, address, message);
89+
90+
// Get the public key
91+
const account = await wallet?.getAccount(chain.chainId);
92+
if (!account?.pubkey) {
93+
throw new Error('Failed to get public key');
94+
}
95+
publicKey = Buffer.from(account.pubkey).toString('base64');
7196
}
7297

7398
// Submit to API directly
@@ -79,8 +104,9 @@ export default function SignMessage() {
79104
body: JSON.stringify({
80105
message,
81106
signature: result.signature,
82-
publicKey: Buffer.from(account.pubkey).toString('base64'),
83-
signer: address
107+
publicKey,
108+
signer: address,
109+
chainType: chain.chainType
84110
}),
85111
});
86112

templates/chain-template/yarn.lock

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ __metadata:
614614
eslint-config-next: "npm:13.0.5"
615615
generate-lockfile: "npm:0.0.12"
616616
interchain-kit: "npm:0.3.36"
617+
keccak256: "npm:^1.0.6"
617618
next: "npm:^13"
618619
node-gzip: "npm:^1.1.2"
619620
react: "npm:18.2.0"
@@ -622,6 +623,7 @@ __metadata:
622623
react-dropzone: "npm:^14.2.3"
623624
react-icons: "npm:5.2.1"
624625
react-markdown: "npm:9.0.1"
626+
secp256k1: "npm:^5.0.1"
625627
starshipjs: "npm:^2.4.1"
626628
typescript: "npm:4.9.3"
627629
yaml-loader: "npm:^0.8.1"
@@ -5296,6 +5298,21 @@ __metadata:
52965298
languageName: node
52975299
linkType: hard
52985300

5301+
"elliptic@npm:^6.5.7":
5302+
version: 6.6.1
5303+
resolution: "elliptic@npm:6.6.1"
5304+
dependencies:
5305+
bn.js: "npm:^4.11.9"
5306+
brorand: "npm:^1.1.0"
5307+
hash.js: "npm:^1.0.0"
5308+
hmac-drbg: "npm:^1.0.1"
5309+
inherits: "npm:^2.0.4"
5310+
minimalistic-assert: "npm:^1.0.1"
5311+
minimalistic-crypto-utils: "npm:^1.0.1"
5312+
checksum: 10c0/8b24ef782eec8b472053793ea1e91ae6bee41afffdfcb78a81c0a53b191e715cbe1292aa07165958a9bbe675bd0955142560b1a007ffce7d6c765bcaf951a867
5313+
languageName: node
5314+
linkType: hard
5315+
52995316
"emoji-regex@npm:^8.0.0":
53005317
version: 8.0.0
53015318
resolution: "emoji-regex@npm:8.0.0"
@@ -7145,6 +7162,29 @@ __metadata:
71457162
languageName: node
71467163
linkType: hard
71477164

7165+
"keccak256@npm:^1.0.6":
7166+
version: 1.0.6
7167+
resolution: "keccak256@npm:1.0.6"
7168+
dependencies:
7169+
bn.js: "npm:^5.2.0"
7170+
buffer: "npm:^6.0.3"
7171+
keccak: "npm:^3.0.2"
7172+
checksum: 10c0/2a3f1e281ffd65bcbbae2ee8d62e27f0336efe6f16b7ed9932ad642ed398da62ccbc3d38dcdf43bd2fad9885f02df501dc77a900c358644df296396ed194056f
7173+
languageName: node
7174+
linkType: hard
7175+
7176+
"keccak@npm:^3.0.2":
7177+
version: 3.0.4
7178+
resolution: "keccak@npm:3.0.4"
7179+
dependencies:
7180+
node-addon-api: "npm:^2.0.0"
7181+
node-gyp: "npm:latest"
7182+
node-gyp-build: "npm:^4.2.0"
7183+
readable-stream: "npm:^3.6.0"
7184+
checksum: 10c0/153525c1c1f770beadb8f8897dec2f1d2dcbee11d063fe5f61957a5b236bfd3d2a111ae2727e443aa6a848df5edb98b9ef237c78d56df49087b0ca8a232ca9cd
7185+
languageName: node
7186+
linkType: hard
7187+
71487188
"keypress@npm:0.1.x":
71497189
version: 0.1.0
71507190
resolution: "keypress@npm:0.1.0"
@@ -8085,6 +8125,24 @@ __metadata:
80858125
languageName: node
80868126
linkType: hard
80878127

8128+
"node-addon-api@npm:^2.0.0":
8129+
version: 2.0.2
8130+
resolution: "node-addon-api@npm:2.0.2"
8131+
dependencies:
8132+
node-gyp: "npm:latest"
8133+
checksum: 10c0/ade6c097ba829fa4aee1ca340117bb7f8f29fdae7b777e343a9d5cbd548481d1f0894b7b907d23ce615c70d932e8f96154caed95c3fa935cfe8cf87546510f64
8134+
languageName: node
8135+
linkType: hard
8136+
8137+
"node-addon-api@npm:^5.0.0":
8138+
version: 5.1.0
8139+
resolution: "node-addon-api@npm:5.1.0"
8140+
dependencies:
8141+
node-gyp: "npm:latest"
8142+
checksum: 10c0/0eb269786124ba6fad9df8007a149e03c199b3e5a3038125dfb3e747c2d5113d406a4e33f4de1ea600aa2339be1f137d55eba1a73ee34e5fff06c52a5c296d1d
8143+
languageName: node
8144+
linkType: hard
8145+
80888146
"node-addon-api@npm:^7.0.0":
80898147
version: 7.1.0
80908148
resolution: "node-addon-api@npm:7.1.0"
@@ -8122,6 +8180,17 @@ __metadata:
81228180
languageName: node
81238181
linkType: hard
81248182

8183+
"node-gyp-build@npm:^4.2.0":
8184+
version: 4.8.4
8185+
resolution: "node-gyp-build@npm:4.8.4"
8186+
bin:
8187+
node-gyp-build: bin.js
8188+
node-gyp-build-optional: optional.js
8189+
node-gyp-build-test: build-test.js
8190+
checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1
8191+
languageName: node
8192+
linkType: hard
8193+
81258194
"node-gyp@npm:latest":
81268195
version: 10.1.0
81278196
resolution: "node-gyp@npm:10.1.0"
@@ -9124,6 +9193,18 @@ __metadata:
91249193
languageName: node
91259194
linkType: hard
91269195

9196+
"secp256k1@npm:^5.0.1":
9197+
version: 5.0.1
9198+
resolution: "secp256k1@npm:5.0.1"
9199+
dependencies:
9200+
elliptic: "npm:^6.5.7"
9201+
node-addon-api: "npm:^5.0.0"
9202+
node-gyp: "npm:latest"
9203+
node-gyp-build: "npm:^4.2.0"
9204+
checksum: 10c0/ea977fcd3a21ee10439a546774d4f3f474f065a561fc2247f65cb2a64f09628732fd606c0a62316858abd7c07b41f5aa09c37773537f233590b4cf94d752dbe7
9205+
languageName: node
9206+
linkType: hard
9207+
91279208
"semver@npm:^6.3.1":
91289209
version: 6.3.1
91299210
resolution: "semver@npm:6.3.1"

0 commit comments

Comments
 (0)