diff --git a/.changeset/nasty-radios-warn.md b/.changeset/nasty-radios-warn.md
new file mode 100644
index 00000000..831cf631
--- /dev/null
+++ b/.changeset/nasty-radios-warn.md
@@ -0,0 +1,5 @@
+---
+'@sei-js/evm': patch
+---
+
+Adds wallet utilities and tests
diff --git a/package.json b/package.json
index dc9d1fee..95920fae 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"dependencies": {
"@changesets/cli": "^2.26.0",
"axios": "^1.6.0",
+ "bech32": "^2.0.0",
"glob": "^10.3.10",
"tslib": "^2.3.0"
},
@@ -43,6 +44,10 @@
"@swc-node/register": "~1.8.0",
"@swc/core": "~1.3.85",
"@swc/helpers": "~0.5.2",
+ "@types/babel__core": "^7.20.5",
+ "@types/babel__generator": "^7.6.8",
+ "@types/babel__template": "^7.4.4",
+ "@types/babel__traverse": "^7.20.6",
"@types/jest": "^29.5.5",
"@types/node": "20.8.2",
"@typescript-eslint/eslint-plugin": "^7.4.0",
diff --git a/packages/evm/README.md b/packages/evm/README.md
index 08b8d421..59602c8e 100644
--- a/packages/evm/README.md
+++ b/packages/evm/README.md
@@ -77,6 +77,56 @@ const amount = parseSei('1000000');
console.log(amount); // 1000000000000000000
```
+### Wallet Utilities
+
+The package provides a set of wallet utilities for generating and validating Sei and EVM addresses.
+
+#### `generateAddressesFromPrivateKey`
+Generates both Sei and EVM addresses from a given private key.
+
+```ts
+import { generateAddressesFromPrivateKey } from '@sei-js/evm';
+
+const privateKey = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
+const { seiAddress, evmAddress } = generateAddressesFromPrivateKey(privateKey);
+
+console.log('Sei Address:', seiAddress);
+console.log('EVM Address:', evmAddress);
+```
+
+#### `deriveAddressesFromPublicKey`
+Derives both Sei and EVM addresses from a given public key.
+
+```ts
+import { deriveAddressesFromPublicKey } from '@sei-js/evm';
+
+const publicKey = new Uint8Array([/* ... */]); // Compressed public key
+const { seiAddress, evmAddress } = deriveAddressesFromPublicKey(publicKey);
+
+console.log('Sei Address:', seiAddress);
+console.log('EVM Address:', evmAddress);
+```
+
+#### `isValidSeiAddress`
+Validates a Sei address.
+
+```ts
+import { isValidSeiAddress } from '@sei-js/evm';
+
+const isValid = isValidSeiAddress('sei1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwp');
+console.log('Is valid Sei address:', isValid);
+```
+
+#### `isValidEvmAddress`
+Validates an EVM address.
+
+```ts
+import { isValidEvmAddress } from '@sei-js/evm';
+
+const isValid = isValidEvmAddress('0x742d35Cc6634C0532925a3b844Bc454e4438f44e');
+console.log('Is valid EVM address:', isValid);
+```
+
diff --git a/packages/evm/package.json b/packages/evm/package.json
index 4cd2a9e5..fd292e96 100644
--- a/packages/evm/package.json
+++ b/packages/evm/package.json
@@ -1,6 +1,6 @@
{
"name": "@sei-js/evm",
- "version": "1.4.0",
+ "version": "1.5.0",
"description": "TypeScript library for EVM interactions on the Sei blockchain",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
@@ -31,13 +31,26 @@
"publishConfig": {
"access": "public"
},
- "dependencies": {},
+ "dependencies": {
+ "@noble/curves": "^1.5.0",
+ "@noble/hashes": "^1.4.0",
+ "bech32": "^2.0.0",
+ "ethers": "^6.0.0",
+ "viem": "2.x"
+ },
"peerDependencies": {
"ethers": "^6.0.0",
"viem": "2.x"
},
"devDependencies": {
+ "@types/babel__core": "^7.20.5",
+ "@types/babel__generator": "^7.6.8",
+ "@types/babel__template": "^7.4.4",
+ "@types/babel__traverse": "^7.20.6",
+ "@types/jest": "^29.5.12",
"ethers": "^6.0.0",
+ "jest": "^29.7.0",
+ "ts-jest": "^29.2.5",
"viem": "2.x"
},
"exports": {
diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts
index ae1a16ab..8ad9fad0 100644
--- a/packages/evm/src/index.ts
+++ b/packages/evm/src/index.ts
@@ -1 +1,2 @@
export * from './precompiles';
+export * from './utils';
\ No newline at end of file
diff --git a/packages/evm/src/utils/index.ts b/packages/evm/src/utils/index.ts
new file mode 100644
index 00000000..738cdfb9
--- /dev/null
+++ b/packages/evm/src/utils/index.ts
@@ -0,0 +1 @@
+export * from './walletUtils';
diff --git a/packages/evm/src/utils/walletUtils.spec.ts b/packages/evm/src/utils/walletUtils.spec.ts
new file mode 100644
index 00000000..5e8f8551
--- /dev/null
+++ b/packages/evm/src/utils/walletUtils.spec.ts
@@ -0,0 +1,59 @@
+import {
+ generateAddressesFromPrivateKey,
+ deriveAddressesFromPublicKey,
+ isValidSeiAddress,
+ isValidEvmAddress
+} from './walletUtils';
+
+describe('Wallet Utilities', () => {
+ const testPrivateKey = 'e7b71175472a74bbd440b2a23a7530adab2ba849e1fe56abaaa303ee8f11e058';
+ const testPublicKey = new Uint8Array([2, 149, 116, 84, 195, 81, 66, 126, 67, 17, 205, 167, 108, 133, 172, 118, 133, 233, 126, 164, 251, 148, 233, 54, 152, 96, 218, 227, 62, 121, 29, 124, 66]);
+
+ describe('generateAddressesFromPrivateKey', () => {
+ it('should generate valid Sei and EVM addresses from a private key', () => {
+ const { seiAddress, evmAddress } = generateAddressesFromPrivateKey(testPrivateKey);
+ expect(isValidSeiAddress(seiAddress)).toBe(true);
+ expect(isValidEvmAddress(evmAddress)).toBe(true);
+ expect(seiAddress.startsWith('sei')).toBe(true);
+ expect(evmAddress.startsWith('0x')).toBe(true);
+ });
+
+ it('should throw an error for an invalid private key', () => {
+ expect(() => generateAddressesFromPrivateKey('invalid')).toThrow('Private key must be 32 bytes long.');
+ });
+ });
+
+ describe('deriveAddressesFromPublicKey', () => {
+ it('should derive valid Sei and EVM addresses from a public key', () => {
+ const { seiAddress, evmAddress } = deriveAddressesFromPublicKey(testPublicKey);
+ expect(isValidSeiAddress(seiAddress)).toBe(true);
+ expect(isValidEvmAddress(evmAddress)).toBe(true);
+ expect(seiAddress.startsWith('sei')).toBe(true);
+ expect(evmAddress.startsWith('0x')).toBe(true);
+ });
+ });
+
+ describe('isValidSeiAddress', () => {
+ it('should return true for a valid Sei address', () => {
+ const { seiAddress } = generateAddressesFromPrivateKey(testPrivateKey);
+ expect(isValidSeiAddress(seiAddress)).toBe(true);
+ });
+
+ it('should return false for an invalid Sei address', () => {
+ const invalidAddress = 'invalid_address';
+ expect(isValidSeiAddress(invalidAddress)).toBe(false);
+ });
+ });
+
+ describe('isValidEvmAddress', () => {
+ it('should return true for a valid EVM address', () => {
+ const { evmAddress } = generateAddressesFromPrivateKey(testPrivateKey);
+ expect(isValidEvmAddress(evmAddress)).toBe(true);
+ });
+
+ it('should return false for an invalid EVM address', () => {
+ const invalidAddress = '0xinvalid_address';
+ expect(isValidEvmAddress(invalidAddress)).toBe(false);
+ });
+ });
+});
\ No newline at end of file
diff --git a/packages/evm/src/utils/walletUtils.ts b/packages/evm/src/utils/walletUtils.ts
new file mode 100644
index 00000000..bb6ba06d
--- /dev/null
+++ b/packages/evm/src/utils/walletUtils.ts
@@ -0,0 +1,56 @@
+import { sha256 } from '@noble/hashes/sha256';
+import { ripemd160 } from '@noble/hashes/ripemd160';
+import { keccak_256 } from '@noble/hashes/sha3';
+import { secp256k1 } from '@noble/curves/secp256k1';
+import { bech32 } from 'bech32'; // Note the import statement
+const encode = bech32.encode;
+const decode = bech32.decode;
+const toWords = bech32.toWords;
+
+
+export interface AddressSet {
+ seiAddress: string;
+ evmAddress: string;
+}
+
+export function generateAddressesFromPrivateKey(privateKeyHex: string): AddressSet {
+ const privateKey = Uint8Array.from(Buffer.from(privateKeyHex.padStart(64, '0'), 'hex'));
+ if (privateKey.length !== 32) {
+ throw new Error('Private key must be 32 bytes long.');
+ }
+
+ const publicKey = secp256k1.getPublicKey(privateKey, true);
+ return deriveAddressesFromPublicKey(publicKey);
+}
+
+export function deriveAddressesFromPublicKey(publicKeyBytes: Uint8Array): AddressSet {
+ // SHA-256 and RIPEMD-160 hashing for Bech32 addresses
+ const sha256Digest = sha256(publicKeyBytes);
+ const ripemd160Digest = ripemd160(sha256Digest);
+
+ // Convert to 5-bit groups for Bech32 encoding
+ const words = toWords(ripemd160Digest);
+
+ // Bech32 address with "sei" prefix
+ const seiAddress = encode('sei', words);
+
+ // Ethereum-style hex address
+ const publicKeyUncompressed = secp256k1.ProjectivePoint.fromHex(publicKeyBytes).toRawBytes(false).slice(1);
+ const keccakHash = keccak_256(publicKeyUncompressed);
+ const evmAddress = `0x${Buffer.from(keccakHash).slice(-20).toString('hex')}`;
+
+ return { seiAddress, evmAddress };
+}
+
+export function isValidSeiAddress(address: string): boolean {
+ try {
+ const decoded = decode(address);
+ return decoded.prefix === 'sei' && decoded.words.length === 32;
+ } catch {
+ return false;
+ }
+}
+
+export function isValidEvmAddress(address: string): boolean {
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
+}
\ No newline at end of file
diff --git a/packages/evm/tsconfig.json b/packages/evm/tsconfig.json
index 5aa365f8..7030218c 100644
--- a/packages/evm/tsconfig.json
+++ b/packages/evm/tsconfig.json
@@ -2,7 +2,9 @@
"extends": "../../tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
- "outDir": "./dist/types"
+ "outDir": "./dist/types",
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true
},
"typedocOptions": {
"readme": "./README.md",
diff --git a/yarn.lock b/yarn.lock
index 129c63e8..9dba7a5c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3211,12 +3211,19 @@
dependencies:
"@noble/hashes" "1.3.2"
+"@noble/curves@^1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.5.0.tgz#7a9b9b507065d516e6dce275a1e31db8d2a100dd"
+ integrity sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==
+ dependencies:
+ "@noble/hashes" "1.4.0"
+
"@noble/hashes@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
-"@noble/hashes@^1", "@noble/hashes@^1.0.0":
+"@noble/hashes@1.4.0", "@noble/hashes@^1", "@noble/hashes@^1.0.0", "@noble/hashes@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==
@@ -3896,7 +3903,7 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
-"@types/babel__core@^7.1.14":
+"@types/babel__core@^7.1.14", "@types/babel__core@^7.20.5":
version "7.20.5"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
@@ -3907,14 +3914,14 @@
"@types/babel__template" "*"
"@types/babel__traverse" "*"
-"@types/babel__generator@*":
+"@types/babel__generator@*", "@types/babel__generator@^7.6.8":
version "7.6.8"
resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab"
integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==
dependencies:
"@babel/types" "^7.0.0"
-"@types/babel__template@*":
+"@types/babel__template@*", "@types/babel__template@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
@@ -3929,6 +3936,13 @@
dependencies:
"@babel/types" "^7.20.7"
+"@types/babel__traverse@^7.20.6":
+ version "7.20.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7"
+ integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==
+ dependencies:
+ "@babel/types" "^7.20.7"
+
"@types/bn.js@*":
version "5.1.5"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0"
@@ -3992,7 +4006,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest@^29.5.5":
+"@types/jest@^29.5.12", "@types/jest@^29.5.5":
version "29.5.12"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544"
integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==
@@ -4902,7 +4916,7 @@ browserslist@^4.22.2, browserslist@^4.23.0:
node-releases "^2.0.14"
update-browserslist-db "^1.0.13"
-bs-logger@0.x:
+bs-logger@0.x, bs-logger@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
@@ -5696,6 +5710,13 @@ eip55@^2.1.1:
dependencies:
keccak "^3.0.3"
+ejs@^3.1.10:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
+ integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
+ dependencies:
+ jake "^10.8.5"
+
ejs@^3.1.7:
version "3.1.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
@@ -7754,7 +7775,7 @@ jest-worker@^29.7.0:
merge-stream "^2.0.0"
supports-color "^8.0.0"
-jest@29.7.0:
+jest@29.7.0, jest@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613"
integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==
@@ -8008,7 +8029,7 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
-lodash.memoize@4.x:
+lodash.memoize@4.x, lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
@@ -8099,7 +8120,7 @@ make-dir@^4.0.0:
dependencies:
semver "^7.5.3"
-make-error@1.x, make-error@^1.1.1:
+make-error@1.x, make-error@^1.1.1, make-error@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
@@ -9416,6 +9437,11 @@ semver@^7.0.0, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
+semver@^7.6.3:
+ version "7.6.3"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
+ integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
+
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -10066,6 +10092,21 @@ ts-jest@29.1.1:
semver "^7.5.3"
yargs-parser "^21.0.1"
+ts-jest@^29.2.5:
+ version "29.2.5"
+ resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.5.tgz#591a3c108e1f5ebd013d3152142cb5472b399d63"
+ integrity sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==
+ dependencies:
+ bs-logger "^0.2.6"
+ ejs "^3.1.10"
+ fast-json-stable-stringify "^2.1.0"
+ jest-util "^29.0.0"
+ json5 "^2.2.3"
+ lodash.memoize "^4.1.2"
+ make-error "^1.3.6"
+ semver "^7.6.3"
+ yargs-parser "^21.1.1"
+
ts-node@10.9.1:
version "10.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"