@mingderwang/wallet 0.1.0
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/README.md +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +175 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @mingderwang/wallet
|
|
2
|
+
|
|
3
|
+
Ethereum wallet + encrypted keystore helpers for Node.js, built on ethers v6.
|
|
4
|
+
|
|
5
|
+
- Create / import / load wallets (private key + mnemonic)
|
|
6
|
+
- Encrypted keystore: scrypt (N=2^15, r=8, p=1) + AES-256-GCM, stored at a path of your choice
|
|
7
|
+
- Balances for ETH + USDC on `ethereum-sepolia`, `ethereum-mainnet`, `base-sepolia`, `base`
|
|
8
|
+
- Default per-network public RPCs; override any with `X402_RPC_URL`
|
|
9
|
+
|
|
10
|
+
Used by `@mingderwang/xsift` and `@mingderwang/x402`.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @mingderwang/wallet
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createWallet, encryptPrivateKey, saveKeystore, loadWallet, readKeystore, getBalances } from '@mingderwang/wallet';
|
|
22
|
+
|
|
23
|
+
const { wallet, privateKey, address } = createWallet();
|
|
24
|
+
const keystore = encryptPrivateKey(privateKey, 's3cret', address);
|
|
25
|
+
saveKeystore(keystore, '~/.xsift/wallet.json');
|
|
26
|
+
|
|
27
|
+
const loaded = loadWallet(readKeystore('~/.xsift/wallet.json'), 's3cret');
|
|
28
|
+
const { eth, usdc } = await getBalances(loaded, 'ethereum-sepolia');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../src/index';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { Wallet } from "ethers";
|
|
8
|
+
|
|
9
|
+
// src/keystore.ts
|
|
10
|
+
import { randomBytes, randomUUID, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
11
|
+
var PARAMS = { n: 2 ** 15, r: 8, p: 1, dkLen: 32 };
|
|
12
|
+
function bytes(b) {
|
|
13
|
+
return b;
|
|
14
|
+
}
|
|
15
|
+
function deriveKey(password, salt) {
|
|
16
|
+
return scryptSync(password, bytes(salt), PARAMS.dkLen, {
|
|
17
|
+
N: PARAMS.n,
|
|
18
|
+
r: PARAMS.r,
|
|
19
|
+
p: PARAMS.p,
|
|
20
|
+
maxmem: 128 * PARAMS.n * PARAMS.r * 2
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function hexString(buf) {
|
|
24
|
+
return buf.toString("hex");
|
|
25
|
+
}
|
|
26
|
+
function encryptPrivateKey(privateKey, password, address) {
|
|
27
|
+
const salt = randomBytes(16);
|
|
28
|
+
const iv = randomBytes(12);
|
|
29
|
+
const key = deriveKey(password, salt);
|
|
30
|
+
const cipher = createCipheriv("aes-256-gcm", bytes(key), bytes(iv));
|
|
31
|
+
const data = Buffer.from(privateKey.replace(/^0x/, ""), "hex");
|
|
32
|
+
const ciphertext = Buffer.concat([cipher.update(bytes(data)), cipher.final()]);
|
|
33
|
+
const tag = cipher.getAuthTag();
|
|
34
|
+
return {
|
|
35
|
+
version: 1,
|
|
36
|
+
id: randomUUID(),
|
|
37
|
+
type: "scrypt-aes-256-gcm",
|
|
38
|
+
params: {
|
|
39
|
+
n: PARAMS.n,
|
|
40
|
+
r: PARAMS.r,
|
|
41
|
+
p: PARAMS.p,
|
|
42
|
+
dkLen: PARAMS.dkLen,
|
|
43
|
+
salt: hexString(salt),
|
|
44
|
+
iv: hexString(iv),
|
|
45
|
+
tag: hexString(tag)
|
|
46
|
+
},
|
|
47
|
+
ciphertext: hexString(ciphertext),
|
|
48
|
+
address
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function decryptPrivateKey(keystore, password) {
|
|
52
|
+
if (keystore.version !== 1 || keystore.type !== "scrypt-aes-256-gcm") {
|
|
53
|
+
throw new Error("Unsupported keystore format");
|
|
54
|
+
}
|
|
55
|
+
const salt = Buffer.from(keystore.params.salt, "hex");
|
|
56
|
+
const iv = Buffer.from(keystore.params.iv, "hex");
|
|
57
|
+
const tag = Buffer.from(keystore.params.tag, "hex");
|
|
58
|
+
const key = deriveKey(password, salt);
|
|
59
|
+
const decipher = createDecipheriv("aes-256-gcm", bytes(key), bytes(iv));
|
|
60
|
+
decipher.setAuthTag(bytes(tag));
|
|
61
|
+
try {
|
|
62
|
+
const data = Buffer.from(keystore.ciphertext, "hex");
|
|
63
|
+
const plain = Buffer.concat([decipher.update(bytes(data)), decipher.final()]);
|
|
64
|
+
return "0x" + plain.toString("hex");
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error("Incorrect password or corrupted keystore.");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function verifyPassword(keystore, password) {
|
|
70
|
+
try {
|
|
71
|
+
decryptPrivateKey(keystore, password);
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function randomBytesHex(len = 32) {
|
|
78
|
+
return "0x" + randomBytes(len).toString("hex");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/index.ts
|
|
82
|
+
var NETWORKS = {
|
|
83
|
+
ethereumMainnet: { networkId: "ethereum-mainnet", chainId: 1 },
|
|
84
|
+
ethereumSepolia: { networkId: "ethereum-sepolia", chainId: 11155111 },
|
|
85
|
+
base: { networkId: "base", chainId: 8453 },
|
|
86
|
+
baseSepolia: { networkId: "base-sepolia", chainId: 84532 }
|
|
87
|
+
};
|
|
88
|
+
var USDC = {
|
|
89
|
+
"ethereum-sepolia": { address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", decimals: 6, symbol: "USDC" },
|
|
90
|
+
"ethereum-mainnet": { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6, symbol: "USDC" },
|
|
91
|
+
"base-sepolia": { address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", decimals: 6, symbol: "USDC" },
|
|
92
|
+
base: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6, symbol: "USDC" }
|
|
93
|
+
};
|
|
94
|
+
function rpcUrl(networkId) {
|
|
95
|
+
const fromEnv = process.env.X402_RPC_URL;
|
|
96
|
+
if (fromEnv)
|
|
97
|
+
return fromEnv;
|
|
98
|
+
switch (networkId) {
|
|
99
|
+
case "ethereum-sepolia":
|
|
100
|
+
return "https://ethereum-sepolia-rpc.publicnode.com";
|
|
101
|
+
case "ethereum-mainnet":
|
|
102
|
+
return "https://eth.llamarpc.com";
|
|
103
|
+
case "base-sepolia":
|
|
104
|
+
return "https://base-sepolia-rpc.publicnode.com";
|
|
105
|
+
case "base":
|
|
106
|
+
return "https://mainnet.base.org";
|
|
107
|
+
default:
|
|
108
|
+
throw new Error(`No default RPC for ${networkId}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function createWallet() {
|
|
112
|
+
const wallet = Wallet.createRandom();
|
|
113
|
+
return { wallet, privateKey: wallet.privateKey, address: wallet.address };
|
|
114
|
+
}
|
|
115
|
+
function importWallet(secret) {
|
|
116
|
+
const wallet = /^[a-f0-9]{64}$/i.test(secret.replace(/^0x/, "")) ? new Wallet(secret) : Wallet.fromPhrase(secret);
|
|
117
|
+
return { wallet, privateKey: wallet.privateKey, address: wallet.address };
|
|
118
|
+
}
|
|
119
|
+
function loadWallet(keystore, password) {
|
|
120
|
+
const privateKey = decryptPrivateKey(keystore, password);
|
|
121
|
+
const wallet = new Wallet(privateKey);
|
|
122
|
+
if (wallet.address.toLowerCase() !== keystore.address.toLowerCase()) {
|
|
123
|
+
throw new Error("Keystore address does not match its private key.");
|
|
124
|
+
}
|
|
125
|
+
return wallet;
|
|
126
|
+
}
|
|
127
|
+
function saveKeystore(keystore, path) {
|
|
128
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
129
|
+
writeFileSync(path, JSON.stringify(keystore, null, 2), { mode: 384 });
|
|
130
|
+
}
|
|
131
|
+
function readKeystore(path) {
|
|
132
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
133
|
+
}
|
|
134
|
+
function deleteFile(path) {
|
|
135
|
+
rmSync(path, { force: true });
|
|
136
|
+
}
|
|
137
|
+
async function getBalances(walletOrAddress, networkId) {
|
|
138
|
+
const { JsonRpcProvider, Contract } = await import("ethers");
|
|
139
|
+
const provider = new JsonRpcProvider(rpcUrl(networkId));
|
|
140
|
+
const address = typeof walletOrAddress === "string" ? walletOrAddress : walletOrAddress.address;
|
|
141
|
+
const usdc = USDC[networkId];
|
|
142
|
+
const eth = await provider.getBalance(address);
|
|
143
|
+
let usdcBalance = 0n;
|
|
144
|
+
if (usdc) {
|
|
145
|
+
const contract = new Contract(usdc.address, ERC20_ABI, provider);
|
|
146
|
+
const balanceOf = contract.balanceOf;
|
|
147
|
+
usdcBalance = await balanceOf(address);
|
|
148
|
+
}
|
|
149
|
+
return { eth: eth.toString(), usdc: usdcBalance.toString() };
|
|
150
|
+
}
|
|
151
|
+
function formatAmount(amountWei, decimals) {
|
|
152
|
+
return (Number(amountWei) / 10 ** decimals).toFixed(decimals);
|
|
153
|
+
}
|
|
154
|
+
var ERC20_ABI = [
|
|
155
|
+
"function balanceOf(address) view returns (uint256)",
|
|
156
|
+
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce)",
|
|
157
|
+
"function nonces(address) view returns (uint256)"
|
|
158
|
+
];
|
|
159
|
+
export {
|
|
160
|
+
verifyPassword,
|
|
161
|
+
saveKeystore,
|
|
162
|
+
rpcUrl,
|
|
163
|
+
readKeystore,
|
|
164
|
+
randomBytesHex,
|
|
165
|
+
loadWallet,
|
|
166
|
+
importWallet,
|
|
167
|
+
getBalances,
|
|
168
|
+
formatAmount,
|
|
169
|
+
encryptPrivateKey,
|
|
170
|
+
deleteFile,
|
|
171
|
+
decryptPrivateKey,
|
|
172
|
+
createWallet,
|
|
173
|
+
USDC,
|
|
174
|
+
NETWORKS
|
|
175
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mingderwang/wallet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Encrypted Ethereum keystore (scrypt + AES-256-GCM) and wallet signing helpers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": ["dist", "README.md"],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node ./scripts/build.mjs"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ethers": "^6.13.4"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["ethereum", "wallet", "keystore", "usdc", "eip-3009"],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/mingderwang/network-vulnerability-scanner.git"
|
|
26
|
+
}
|
|
27
|
+
}
|