Skip to content
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

Adds combined/unified bech32+EVM wallet utility functions #195

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions .changeset/nasty-radios-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sei-js/evm': patch
---

Adds wallet utilities and tests
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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",
Expand Down
50 changes: 50 additions & 0 deletions packages/evm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```

<br>
<br>

Expand Down
17 changes: 15 additions & 2 deletions packages/evm/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/evm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './precompiles';
export * from './utils';
1 change: 1 addition & 0 deletions packages/evm/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './walletUtils';
59 changes: 59 additions & 0 deletions packages/evm/src/utils/walletUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
56 changes: 56 additions & 0 deletions packages/evm/src/utils/walletUtils.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 3 additions & 1 deletion packages/evm/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading