From 58eb0cde3d97750cf8737d119af09e857ad9fcad Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:21:23 +1100 Subject: [PATCH 1/3] Create erc55-utils and tests --- .vscode/settings.json | 2 +- package.json | 1 + .../utils/__tests__/erc55-utils.test.ts | 121 ++++++++++++++++++ src/shared/utils/erc55-utils.ts | 42 ++++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/shared/utils/__tests__/erc55-utils.test.ts create mode 100644 src/shared/utils/erc55-utils.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 48dc0dd0..a0122c82 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,7 +22,7 @@ "eslint.useFlatConfig": true, "githubPullRequests.overrideDefaultBranch": "dev", "githubIssues.issueBranchTitle": "${issueNumber}-${sanitizedIssueTitle}", - "cSpell.words": ["Cbor", "secp"], + "cSpell.words": ["Cbor", "keccak", "secp"], "[dotenv]": { "editor.defaultFormatter": "foxundermoon.shell-format" }, diff --git a/package.json b/package.json index ecbfeced..5037e139 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "format": "prettier .", "icon": "npx iconfont-h5", "test": "vitest", + "test:run": "vitest --run", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:ui:debug": "playwright test --ui --debug", diff --git a/src/shared/utils/__tests__/erc55-utils.test.ts b/src/shared/utils/__tests__/erc55-utils.test.ts new file mode 100644 index 00000000..a726b503 --- /dev/null +++ b/src/shared/utils/__tests__/erc55-utils.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from 'vitest'; + +import { toChecksumAddress } from '../erc55-utils'; + +describe('toChecksumAddress', () => { + it('should handle all official EIP-55 test cases', () => { + const testCases = [ + // All caps + { + input: '0x52908400098527886E0F7030069857D2E4169EE7', + expected: '0x52908400098527886E0F7030069857D2E4169EE7', + }, + { + input: '0x8617E340B3D01FA5F11F306F4090FD50E238070D', + expected: '0x8617E340B3D01FA5F11F306F4090FD50E238070D', + }, + // All Lower + { + input: '0xde709f2102306220921060314715629080e2fb77', + expected: '0xde709f2102306220921060314715629080e2fb77', + }, + { + input: '0x27b1fdb04752bbc536007a920d24acb045561c26', + expected: '0x27b1fdb04752bbc536007a920d24acb045561c26', + }, + // Normal + { + input: '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', + expected: '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', + }, + { + input: '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', + expected: '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', + }, + { + input: '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', + expected: '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', + }, + { + input: '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', + expected: '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', + }, + ]; + + testCases.forEach(({ input, expected }) => { + // Test both with the original input and lowercase input + expect(toChecksumAddress(input)).toBe(expected); + expect(toChecksumAddress(input.toLowerCase())).toBe(expected); + }); + }); + + it('should handle Flow COA addresses', () => { + const testCases = [ + // All lowercase input + { + input: '0x000000000000000000000002aaaaaaaaaaaaaaaa', + expected: '0x000000000000000000000002AaAaAAaAAaAAaaaa', + }, + // Mixed case input + { + input: '0x000000000000000000000002aAaAaAaAaAaAaAaA', + expected: '0x000000000000000000000002AaAaAAaAAaAAaaaa', + }, + // All uppercase input + { + input: '0x000000000000000000000002AAAAAAAAAAAAAAAA', + expected: '0x000000000000000000000002AaAaAAaAAaAAaaaa', + }, + // With numbers + { + input: '0x000000000000000000000002a1b2c3d4e5f6789a', + expected: '0x000000000000000000000002a1b2c3D4e5f6789A', + }, + // Mixed numbers and letters + { + input: '0x0000000000000000000000021234567890abcdef', + expected: '0x0000000000000000000000021234567890ABCDEF', + }, + ]; + + testCases.forEach(({ input, expected }) => { + // Test both with the original input and lowercase input + expect(toChecksumAddress(input)).toBe(expected); + expect(toChecksumAddress(input.toLowerCase())).toBe(expected); + }); + }); + + it('should handle addresses with and without 0x prefix', () => { + const address = '5aaeb6053f3e94c9b9a09f33669435e7ef1beaed'; + const expectedChecksum = '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed'; + + expect(toChecksumAddress(address)).toBe(expectedChecksum); + expect(toChecksumAddress(`0x${address}`)).toBe(expectedChecksum); + }); + + it('should reject invalid addresses', () => { + const invalidAddresses = [ + // Regular Flow addresses (not EVM format) + '0x1234567890', + '0x01', + // Invalid lengths + '0x1234', + '0x' + '0'.repeat(39), + '0x' + '0'.repeat(41), + // Invalid characters + '0xg234567890123456789012345678901234567890', + '0xabcdefghijklmnopqrstuvwxyz1234567890abcd', + // Empty/invalid input + '', + '0x', + 'not an address', + '12345', + ]; + + const expectedError = + 'Invalid EVM address format. Expected 40 hexadecimal characters with optional 0x prefix.'; + invalidAddresses.forEach((address) => { + expect(() => toChecksumAddress(address)).toThrow(expectedError); + }); + }); +}); diff --git a/src/shared/utils/erc55-utils.ts b/src/shared/utils/erc55-utils.ts new file mode 100644 index 00000000..c7cc8dce --- /dev/null +++ b/src/shared/utils/erc55-utils.ts @@ -0,0 +1,42 @@ +// From Vitalik Buterin , Alex Van de Sande , "ERC-55: Mixed-case checksum address encoding," Ethereum Improvement Proposals, no. 55, January 2016. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-55. + +import ethUtil from 'ethereumjs-util'; + +const ERROR_MESSAGE = + 'Invalid EVM address format. Expected 40 hexadecimal characters with optional 0x prefix.'; + +/** + * This is a utility function to convert an Ethereum address to a checksum address. + * @param address - The address to convert. + * @returns The checksum address. + * @throws Error if the address is not a valid format + */ +export const toChecksumAddress = (address: string) => { + if (!address || typeof address !== 'string') { + throw new Error(ERROR_MESSAGE); + } + + // Remove 0x prefix if present + const cleanAddress = address.replace('0x', '').toLowerCase(); + + // Validate the address format (40 hex chars) + const addressRegex = /^[0-9a-f]{40}$/; + if (!addressRegex.test(cleanAddress)) { + throw new Error(ERROR_MESSAGE); + } + + // Create a hash of the address using keccak256 + // Note: We use the address as ASCII string input to match the spec + const hash = ethUtil.keccak256(Buffer.from(cleanAddress, 'ascii')).toString('hex'); + + let ret = '0x'; + + // For each character in the address + for (let i = 0; i < cleanAddress.length; i++) { + const char = cleanAddress[i]; + // If the character is a letter (a-f) we apply checksumming + ret += parseInt(hash[i], 16) >= 8 ? char.toUpperCase() : char; + } + + return ret; +}; From 223a3c1c1033397d64245703ae4e916c74827e42 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:53:53 +1100 Subject: [PATCH 2/3] Moved to using the ethUtil for erc55. Implemented for our evm wallet address --- src/background/controller/wallet.ts | 7 +++++-- src/ui/views/Dashboard/Header.tsx | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index 146ca537..b4d7047e 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -2088,8 +2088,11 @@ export class WalletController extends BaseController { }); if (result) { - await this.setEvmAddress(result); - return result; + // This is the COA address we get straight from the script + // This is where we encode the address in ERC-55 format + const checksummedAddress = ethUtil.toChecksumAddress(ensureEvmAddressPrefix(result)); + await this.setEvmAddress(checksummedAddress); + return checksummedAddress; } else { return ''; } diff --git a/src/ui/views/Dashboard/Header.tsx b/src/ui/views/Dashboard/Header.tsx index f4544fc0..e39e3709 100644 --- a/src/ui/views/Dashboard/Header.tsx +++ b/src/ui/views/Dashboard/Header.tsx @@ -569,7 +569,7 @@ const Header = ({ loading = false }) => { {formatAddress(props.address)} From 2754180c45be8fa8f9e7bb24cc9d2cd068ac06cd Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:54:11 +1100 Subject: [PATCH 3/3] Removing our own implemention --- .../utils/__tests__/erc55-utils.test.ts | 121 ------------------ src/shared/utils/erc55-utils.ts | 42 ------ 2 files changed, 163 deletions(-) delete mode 100644 src/shared/utils/__tests__/erc55-utils.test.ts delete mode 100644 src/shared/utils/erc55-utils.ts diff --git a/src/shared/utils/__tests__/erc55-utils.test.ts b/src/shared/utils/__tests__/erc55-utils.test.ts deleted file mode 100644 index a726b503..00000000 --- a/src/shared/utils/__tests__/erc55-utils.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { toChecksumAddress } from '../erc55-utils'; - -describe('toChecksumAddress', () => { - it('should handle all official EIP-55 test cases', () => { - const testCases = [ - // All caps - { - input: '0x52908400098527886E0F7030069857D2E4169EE7', - expected: '0x52908400098527886E0F7030069857D2E4169EE7', - }, - { - input: '0x8617E340B3D01FA5F11F306F4090FD50E238070D', - expected: '0x8617E340B3D01FA5F11F306F4090FD50E238070D', - }, - // All Lower - { - input: '0xde709f2102306220921060314715629080e2fb77', - expected: '0xde709f2102306220921060314715629080e2fb77', - }, - { - input: '0x27b1fdb04752bbc536007a920d24acb045561c26', - expected: '0x27b1fdb04752bbc536007a920d24acb045561c26', - }, - // Normal - { - input: '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', - expected: '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', - }, - { - input: '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', - expected: '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', - }, - { - input: '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', - expected: '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', - }, - { - input: '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', - expected: '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', - }, - ]; - - testCases.forEach(({ input, expected }) => { - // Test both with the original input and lowercase input - expect(toChecksumAddress(input)).toBe(expected); - expect(toChecksumAddress(input.toLowerCase())).toBe(expected); - }); - }); - - it('should handle Flow COA addresses', () => { - const testCases = [ - // All lowercase input - { - input: '0x000000000000000000000002aaaaaaaaaaaaaaaa', - expected: '0x000000000000000000000002AaAaAAaAAaAAaaaa', - }, - // Mixed case input - { - input: '0x000000000000000000000002aAaAaAaAaAaAaAaA', - expected: '0x000000000000000000000002AaAaAAaAAaAAaaaa', - }, - // All uppercase input - { - input: '0x000000000000000000000002AAAAAAAAAAAAAAAA', - expected: '0x000000000000000000000002AaAaAAaAAaAAaaaa', - }, - // With numbers - { - input: '0x000000000000000000000002a1b2c3d4e5f6789a', - expected: '0x000000000000000000000002a1b2c3D4e5f6789A', - }, - // Mixed numbers and letters - { - input: '0x0000000000000000000000021234567890abcdef', - expected: '0x0000000000000000000000021234567890ABCDEF', - }, - ]; - - testCases.forEach(({ input, expected }) => { - // Test both with the original input and lowercase input - expect(toChecksumAddress(input)).toBe(expected); - expect(toChecksumAddress(input.toLowerCase())).toBe(expected); - }); - }); - - it('should handle addresses with and without 0x prefix', () => { - const address = '5aaeb6053f3e94c9b9a09f33669435e7ef1beaed'; - const expectedChecksum = '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed'; - - expect(toChecksumAddress(address)).toBe(expectedChecksum); - expect(toChecksumAddress(`0x${address}`)).toBe(expectedChecksum); - }); - - it('should reject invalid addresses', () => { - const invalidAddresses = [ - // Regular Flow addresses (not EVM format) - '0x1234567890', - '0x01', - // Invalid lengths - '0x1234', - '0x' + '0'.repeat(39), - '0x' + '0'.repeat(41), - // Invalid characters - '0xg234567890123456789012345678901234567890', - '0xabcdefghijklmnopqrstuvwxyz1234567890abcd', - // Empty/invalid input - '', - '0x', - 'not an address', - '12345', - ]; - - const expectedError = - 'Invalid EVM address format. Expected 40 hexadecimal characters with optional 0x prefix.'; - invalidAddresses.forEach((address) => { - expect(() => toChecksumAddress(address)).toThrow(expectedError); - }); - }); -}); diff --git a/src/shared/utils/erc55-utils.ts b/src/shared/utils/erc55-utils.ts deleted file mode 100644 index c7cc8dce..00000000 --- a/src/shared/utils/erc55-utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -// From Vitalik Buterin , Alex Van de Sande , "ERC-55: Mixed-case checksum address encoding," Ethereum Improvement Proposals, no. 55, January 2016. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-55. - -import ethUtil from 'ethereumjs-util'; - -const ERROR_MESSAGE = - 'Invalid EVM address format. Expected 40 hexadecimal characters with optional 0x prefix.'; - -/** - * This is a utility function to convert an Ethereum address to a checksum address. - * @param address - The address to convert. - * @returns The checksum address. - * @throws Error if the address is not a valid format - */ -export const toChecksumAddress = (address: string) => { - if (!address || typeof address !== 'string') { - throw new Error(ERROR_MESSAGE); - } - - // Remove 0x prefix if present - const cleanAddress = address.replace('0x', '').toLowerCase(); - - // Validate the address format (40 hex chars) - const addressRegex = /^[0-9a-f]{40}$/; - if (!addressRegex.test(cleanAddress)) { - throw new Error(ERROR_MESSAGE); - } - - // Create a hash of the address using keccak256 - // Note: We use the address as ASCII string input to match the spec - const hash = ethUtil.keccak256(Buffer.from(cleanAddress, 'ascii')).toString('hex'); - - let ret = '0x'; - - // For each character in the address - for (let i = 0; i < cleanAddress.length; i++) { - const char = cleanAddress[i]; - // If the character is a letter (a-f) we apply checksumming - ret += parseInt(hash[i], 16) >= 8 ? char.toUpperCase() : char; - } - - return ret; -};