@mingderwang/wallet 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -5
- package/src/index.ts +105 -0
- package/src/keystore.ts +101 -0
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
2
|
+
"name": "@mingderwang/wallet",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Ethereum wallet + encrypted keystore helpers (scrypt + AES-256-GCM) built on ethers v6.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
11
14
|
},
|
|
12
|
-
"files": ["dist", "README.md"],
|
|
15
|
+
"files": ["dist", "src", "README.md"],
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=18.0.0"
|
|
15
18
|
},
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { Wallet, HDNodeWallet } from 'ethers';
|
|
4
|
+
import {
|
|
5
|
+
encryptPrivateKey,
|
|
6
|
+
decryptPrivateKey,
|
|
7
|
+
verifyPassword,
|
|
8
|
+
randomBytesHex,
|
|
9
|
+
type EncryptedKeystore,
|
|
10
|
+
} from './keystore';
|
|
11
|
+
|
|
12
|
+
export { encryptPrivateKey, decryptPrivateKey, verifyPassword, randomBytesHex };
|
|
13
|
+
export type { EncryptedKeystore };
|
|
14
|
+
|
|
15
|
+
export const NETWORKS = {
|
|
16
|
+
ethereumMainnet: { networkId: 'ethereum-mainnet', chainId: 1 },
|
|
17
|
+
ethereumSepolia: { networkId: 'ethereum-sepolia', chainId: 11155111 },
|
|
18
|
+
base: { networkId: 'base', chainId: 8453 },
|
|
19
|
+
baseSepolia: { networkId: 'base-sepolia', chainId: 84532 },
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export const USDC: Record<string, { address: string; decimals: number; symbol: string }> = {
|
|
23
|
+
'ethereum-sepolia': { address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', decimals: 6, symbol: 'USDC' },
|
|
24
|
+
'ethereum-mainnet': { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6, symbol: 'USDC' },
|
|
25
|
+
'base-sepolia': { address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', decimals: 6, symbol: 'USDC' },
|
|
26
|
+
base: { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, symbol: 'USDC' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function rpcUrl(networkId: string): string {
|
|
30
|
+
const fromEnv = process.env.X402_RPC_URL;
|
|
31
|
+
if (fromEnv) return fromEnv;
|
|
32
|
+
switch (networkId) {
|
|
33
|
+
case 'ethereum-sepolia':
|
|
34
|
+
return 'https://ethereum-sepolia-rpc.publicnode.com';
|
|
35
|
+
case 'ethereum-mainnet':
|
|
36
|
+
return 'https://eth.llamarpc.com';
|
|
37
|
+
case 'base-sepolia':
|
|
38
|
+
return 'https://base-sepolia-rpc.publicnode.com';
|
|
39
|
+
case 'base':
|
|
40
|
+
return 'https://mainnet.base.org';
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`No default RPC for ${networkId}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SigningWallet = Wallet | HDNodeWallet;
|
|
47
|
+
|
|
48
|
+
export function createWallet(): { wallet: HDNodeWallet; privateKey: string; address: string } {
|
|
49
|
+
const wallet = Wallet.createRandom();
|
|
50
|
+
return { wallet, privateKey: wallet.privateKey, address: wallet.address };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function importWallet(secret: string): { wallet: SigningWallet; privateKey: string; address: string } {
|
|
54
|
+
const wallet: SigningWallet = /^[a-f0-9]{64}$/i.test(secret.replace(/^0x/, ''))
|
|
55
|
+
? new Wallet(secret)
|
|
56
|
+
: Wallet.fromPhrase(secret);
|
|
57
|
+
return { wallet, privateKey: wallet.privateKey, address: wallet.address };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadWallet(keystore: EncryptedKeystore, password: string): Wallet {
|
|
61
|
+
const privateKey = decryptPrivateKey(keystore, password);
|
|
62
|
+
const wallet = new Wallet(privateKey);
|
|
63
|
+
if (wallet.address.toLowerCase() !== keystore.address.toLowerCase()) {
|
|
64
|
+
throw new Error('Keystore address does not match its private key.');
|
|
65
|
+
}
|
|
66
|
+
return wallet;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function saveKeystore(keystore: EncryptedKeystore, path: string): void {
|
|
70
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
71
|
+
writeFileSync(path, JSON.stringify(keystore, null, 2), { mode: 0o600 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readKeystore(path: string): EncryptedKeystore {
|
|
75
|
+
return JSON.parse(readFileSync(path, 'utf8')) as EncryptedKeystore;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function deleteFile(path: string): void {
|
|
79
|
+
rmSync(path, { force: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function getBalances(walletOrAddress: string | Wallet, networkId: string): Promise<{ eth: string; usdc: string }> {
|
|
83
|
+
const { JsonRpcProvider, Contract } = await import('ethers');
|
|
84
|
+
const provider = new JsonRpcProvider(rpcUrl(networkId));
|
|
85
|
+
const address = typeof walletOrAddress === 'string' ? walletOrAddress : walletOrAddress.address;
|
|
86
|
+
const usdc = USDC[networkId];
|
|
87
|
+
const eth = await provider.getBalance(address);
|
|
88
|
+
let usdcBalance = 0n;
|
|
89
|
+
if (usdc) {
|
|
90
|
+
const contract = new Contract(usdc.address, ERC20_ABI, provider);
|
|
91
|
+
const balanceOf = (contract as unknown as { balanceOf: (a: string) => Promise<bigint> }).balanceOf;
|
|
92
|
+
usdcBalance = await balanceOf(address);
|
|
93
|
+
}
|
|
94
|
+
return { eth: eth.toString(), usdc: usdcBalance.toString() };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatAmount(amountWei: bigint | string | number, decimals: number): string {
|
|
98
|
+
return (Number(amountWei) / 10 ** decimals).toFixed(decimals);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ERC20_ABI = [
|
|
102
|
+
'function balanceOf(address) view returns (uint256)',
|
|
103
|
+
'function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce)',
|
|
104
|
+
'function nonces(address) view returns (uint256)',
|
|
105
|
+
] as const;
|
package/src/keystore.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, scryptSync, createCipheriv, createDecipheriv } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export interface EncryptedKeystore {
|
|
4
|
+
version: 1;
|
|
5
|
+
id: string;
|
|
6
|
+
type: 'scrypt-aes-256-gcm';
|
|
7
|
+
params: {
|
|
8
|
+
n: number;
|
|
9
|
+
r: number;
|
|
10
|
+
p: number;
|
|
11
|
+
dkLen: number;
|
|
12
|
+
salt: string; // hex
|
|
13
|
+
iv: string; // hex
|
|
14
|
+
tag: string; // hex
|
|
15
|
+
};
|
|
16
|
+
ciphertext: string; // hex
|
|
17
|
+
address: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PARAMS = { n: 2 ** 15, r: 8, p: 1, dkLen: 32 };
|
|
21
|
+
|
|
22
|
+
// Coerce the (pre-generics) Buffer type into the stricter Uint8Array<ArrayBufferLike>
|
|
23
|
+
// the current node crypto typings demand. Runtime-identical to the input buffer.
|
|
24
|
+
function bytes(b: Buffer): Uint8Array<ArrayBufferLike> {
|
|
25
|
+
return b as unknown as Uint8Array<ArrayBufferLike>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function deriveKey(password: string, salt: Buffer): Buffer {
|
|
29
|
+
return scryptSync(password, bytes(salt), PARAMS.dkLen, {
|
|
30
|
+
N: PARAMS.n,
|
|
31
|
+
r: PARAMS.r,
|
|
32
|
+
p: PARAMS.p,
|
|
33
|
+
maxmem: 128 * PARAMS.n * PARAMS.r * 2,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hexString(buf: Buffer): string {
|
|
38
|
+
return buf.toString('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function encryptPrivateKey(privateKey: string, password: string, address: string): EncryptedKeystore {
|
|
42
|
+
const salt = randomBytes(16);
|
|
43
|
+
const iv = randomBytes(12);
|
|
44
|
+
const key = deriveKey(password, salt);
|
|
45
|
+
const cipher = createCipheriv('aes-256-gcm', bytes(key), bytes(iv));
|
|
46
|
+
const data = Buffer.from(privateKey.replace(/^0x/, ''), 'hex');
|
|
47
|
+
const ciphertext = Buffer.concat([cipher.update(bytes(data)), cipher.final()] as unknown as Uint8Array<ArrayBufferLike>[]);
|
|
48
|
+
const tag = cipher.getAuthTag();
|
|
49
|
+
return {
|
|
50
|
+
version: 1,
|
|
51
|
+
id: randomUUID(),
|
|
52
|
+
type: 'scrypt-aes-256-gcm',
|
|
53
|
+
params: {
|
|
54
|
+
n: PARAMS.n,
|
|
55
|
+
r: PARAMS.r,
|
|
56
|
+
p: PARAMS.p,
|
|
57
|
+
dkLen: PARAMS.dkLen,
|
|
58
|
+
salt: hexString(salt),
|
|
59
|
+
iv: hexString(iv),
|
|
60
|
+
tag: hexString(tag),
|
|
61
|
+
},
|
|
62
|
+
ciphertext: hexString(ciphertext),
|
|
63
|
+
address,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function decryptPrivateKey(keystore: EncryptedKeystore, password: string): string {
|
|
68
|
+
if (keystore.version !== 1 || keystore.type !== 'scrypt-aes-256-gcm') {
|
|
69
|
+
throw new Error('Unsupported keystore format');
|
|
70
|
+
}
|
|
71
|
+
const salt = Buffer.from(keystore.params.salt, 'hex');
|
|
72
|
+
const iv = Buffer.from(keystore.params.iv, 'hex');
|
|
73
|
+
const tag = Buffer.from(keystore.params.tag, 'hex');
|
|
74
|
+
const key = deriveKey(password, salt);
|
|
75
|
+
const decipher = createDecipheriv('aes-256-gcm', bytes(key), bytes(iv));
|
|
76
|
+
decipher.setAuthTag(bytes(tag));
|
|
77
|
+
try {
|
|
78
|
+
const data = Buffer.from(keystore.ciphertext, 'hex');
|
|
79
|
+
const plain = Buffer.concat([decipher.update(bytes(data)), decipher.final()] as unknown as Uint8Array<ArrayBufferLike>[]);
|
|
80
|
+
return '0x' + plain.toString('hex');
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error('Incorrect password or corrupted keystore.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function verifyPassword(keystore: EncryptedKeystore, password: string): boolean {
|
|
87
|
+
try {
|
|
88
|
+
decryptPrivateKey(keystore, password);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function bytesToHex(b: Uint8Array): string {
|
|
96
|
+
return '0x' + Buffer.from(b).toString('hex');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function randomBytesHex(len = 32): string {
|
|
100
|
+
return '0x' + randomBytes(len).toString('hex');
|
|
101
|
+
}
|