// Copyright 2017-2025 @pezkuwi/app-claims authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { EcdsaSignature, EthereumAddress, StatementKind } from '@pezkuwi/types/interfaces'; import secp256k1 from 'secp256k1/elliptic.js'; import { statics } from '@pezkuwi/react-api/statics'; import { assert, hexToU8a, stringToU8a, u8aConcat, u8aToBuffer } from '@pezkuwi/util'; import { keccakAsHex, keccakAsU8a } from '@pezkuwi/util-crypto'; interface RecoveredSignature { error: Error | null; ethereumAddress: EthereumAddress | null; signature: EcdsaSignature | null; } interface SignatureParts { recovery: number; signature: Buffer; } // converts an Ethereum address to a checksum representation export function addrToChecksum (_address: string): string { const address = _address.toLowerCase(); const hash = keccakAsHex(address.substring(2)).substring(2); let result = '0x'; for (let n = 0; n < 40; n++) { result = `${result}${ parseInt(hash[n], 16) > 7 ? address[n + 2].toUpperCase() : address[n + 2] }`; } return result; } // convert a give public key to an Ethereum address (the last 20 bytes of an _exapnded_ key keccack) export function publicToAddr (publicKey: Uint8Array): string { return addrToChecksum(`0x${keccakAsHex(publicKey).slice(-40)}`); } // hash a message for use in signature recovery, adding the standard Ethereum header export function hashMessage (message: string): Buffer { const expanded = stringToU8a(`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`); const hashed = keccakAsU8a(expanded); return u8aToBuffer(hashed); } // split is 65-byte signature into the r, s (combined) and recovery number (derived from v) export function sigToParts (_signature: string): SignatureParts { const signature = hexToU8a(_signature); assert(signature.length === 65, `Invalid signature length, expected 65 found ${signature.length}`); let v = signature[64]; if (v < 27) { v += 27; } const recovery = v - 27; assert(recovery === 0 || recovery === 1, 'Invalid signature v value'); return { recovery, signature: u8aToBuffer(signature.slice(0, 64)) }; } // recover an address from a given message and a recover/signature combination export function recoverAddress (message: string, { recovery, signature }: SignatureParts): string { const msgHash = hashMessage(message); const senderPubKey = secp256k1.recover(msgHash, signature, recovery); return publicToAddr( secp256k1.publicKeyConvert(senderPubKey, false).subarray(1) ); } // recover an address from a signature JSON (as supplied by e.g. MyCrypto) export function recoverFromJSON (signatureJson: string | null): RecoveredSignature { try { const { msg, sig } = JSON.parse(signatureJson || '{}') as Record; if (!msg || !sig) { throw new Error('Invalid signature object'); } const parts = sigToParts(sig); return { error: null, ethereumAddress: statics.registry.createType('EthereumAddress', recoverAddress(msg, parts)), signature: statics.registry.createType('EcdsaSignature', u8aConcat(parts.signature, new Uint8Array([parts.recovery]))) }; } catch (error) { console.error(error); return { error: error as Error, ethereumAddress: null, signature: null }; } } export interface Statement { sentence: string; url: string; } function getPezkuwi (kind?: StatementKind | null): Statement | undefined { if (!kind) { return undefined; } const url = kind.isRegular ? 'https://statement.pezkuwichain.io/regular.html' : 'https://statement.pezkuwichain.io/saft.html'; const hash = kind.isRegular ? 'Qmc1XYqT6S39WNp2UeiRUrZichUWUPpGEThDE6dAb3f6Ny' : 'QmXEkMahfhHJPzT3RjkXiZVFi77ZeVeuxtAjhojGRNYckz'; return { sentence: `I hereby agree to the terms of the statement whose SHA-256 multihash is ${hash}. (This may be found at the URL: ${url})`, url }; } export function getStatement (network: string, kind?: StatementKind | null): Statement | undefined { switch (network) { case 'Pezkuwi': case 'Pezkuwi CC1': return getPezkuwi(kind); default: return undefined; } }