钱包是区块链架构中的一个重要工具。这个基本工具或应用程序充当了用户与区块链之间的连接点。对于非开发者来说,他们占据了比特币用户的大部分,他们的大多数与区块链的互动都是通过钱包进行的,因为他们的交易必须使用由钱包控制的账户的私钥进行签名。钱包还在持有和管理用户资产(比特币)方面非常重要,因此,在构建钱包时非常需要确保适当的安全性。在撰写本文时,有许多钱包架构和机制来确保用户资产在钱包中的安全,但本文旨在指导新开发者或比特币爱好者了解钱包创建的一般概念,以及在钱包设计和架构中的安全性、加密、访问和重新访问的重要性。
本文假设你已经了解不同的比特币网络(测试网、回归测试网、主网)以及不同的钱包类型(p2pk、p2pkh 等)。为本文的讨论范围,我们将专注于使用特定库构建一个多功能比特币钱包。
- BitcoinJs 库: BitcoinJS 库是一个 JavaScript 库,为在 Node.js 和网络应用中处理比特币和类比特币加密货币提供工具和实用程序。它允许开发者进行与比特币相关的各种任务,例如生成比特币地址、创建和签署交易,以及使用比特币脚本。
- ecpair 库: ecpair 库是 BitcoinJS 生态系统的一部分,专门设计用于在 JavaScript 应用中处理椭圆曲线加密(ECC)。它提供创建和管理椭圆曲线密钥对的功能,这对于数字签名和公钥加密等加密操作非常重要。
- tiny-secp256k1 库: tiny-secp256k1 库是一个专门为处理 secp256k1 椭圆曲线而设计的紧凑的 JavaScript 库,该曲线在比特币和其他加密货币中广泛使用。这个库提供与 secp256k1 曲线相关的加密操作的高效和优化实现,包括密钥对生成、数字签名生成和验证以及公钥恢复。
- CryptoJs 库: CryptoJS 库是一个流行的 JavaScript 库,为网络应用提供加密功能。它提供广泛的加密算法和实用工具,允许开发者执行各种加密操作,如加密、解密、哈希和 HMAC(带密钥的哈希消息认证码)生成。
- fs 库: 也称为文件系统库。它为我们提供从 TypeScript 代码创建和读取文件内容的能力。我们将使用它来存储我们创建的每个钱包的详细信息。
我们的项目将用 TypeScript 编写,因此假设你在尝试执行代码之前已经安装了 Node 和 TypeScript。我们的项目结构如下:
我们首先创建一个名为“ Bitcoin_Cli_Wallet”的新文件夹,然后进入该文件夹并运行命令“ npm init”。这将初始化一个新的节点模块,并在目录中创建 package.json 文件,之后我们在根目录下创建一个名为“ wallets”的新文件夹。然后我们运行命令“ npm i -D typescript ts-node”,这将安装 TypeScript 和 TypeScript 执行环境,以便让我们在 Node.js 中执行 TypeScript 代码。接下来,我们通过运行以下代码来安装我们将使用的库:“ npm install bitcoinjs-lib”,“ npm install ecpair”,“ npm install tiny-secp256k1”,“ npm install crypto”,“ npm install dotenv”。这其中除了 dotenv 库外,其他的库已经提到过。dotenv 库帮助我们将代码中需要的私密数据或信息保存在一个单独的文件中,以免其暴露在代码的其他部分。
导入依赖
接下来,我们创建一个 index.ts 文件并填充我们的钱包逻辑。我们将开始导入项目中需要的所有依赖项。import * as bitcoin from 'bitcoinjs-lib' import {ECPairFactory, ECPairInterface} from 'ecpair'; import * as ecc from 'tiny-secp256k1'; import * as fs from 'fs' import * as crypto from 'crypto'; import * as walletTypes from 'bitcoinjs-lib/src/payments/index' import * as dotenv from 'dotenv';
设置全局变量和 .env 变量
上述代码基本上将我们将使用的不同库的内容引入到项目的作用域中。
```
let totalWallets: number;
let allWalets: any;
let allAddresses: any;
interface History {
totalWallets: number;
allWalets: any;
allAddresses: any;
}
interface walletData {
keyPair: ECPairInterface;
walletObject: walletTypes.Payment;
walletType: string;
}
const ECPair = ECPairFactory(ecc);
const TESTNET = bitcoin.networks.testnet;
const REGTEST = bitcoin.networks.regtest;
const BITCOIN = bitcoin.networks.bitcoin;
dotenv.config();
const encryptionKey = process.env.ENCRYPTION_KEY;
const algorithm = process.env.ALGORITHM;接下来,我们定义全局变量以跟踪我们已创建的钱包总数、所有钱包名称的数组和我们创建的所有地址的数组。我们还定义了两个接口。第一个接口用于定义存储钱包历史记录的对象结构。这很重要,因为我们将使用该结构存储钱包活动的历史。最后,我们还有一个接口定义每个钱包数据的结构,我们将在机器上使用该结构存储每个我们创建的钱包。接下来,我们定义用户可以为其创建钱包的不同网络类型;基本上,用户可以使用这个 cli 钱包为上述列出的任何网络创建钱包,最后,我们从dotenv文件中读取加密密钥和算法变量,所以是的,我们需要在根目录中创建一个 “ **.env**” 文件,并将以下内容粘贴到该文件中:
ENCRYPTION_KEY=MySuperSecretKey
ALGORITHM='aes-256-cbc'
```
这表示用于加密和解密我们钱包数据的密钥,以及我们打算使用的加密算法。
创建钱包函数
```
function createWallet(walletname:string, network:string, walletType:string) {
let walletHistory = fetchWalletHistory();
totalWallets = walletHistory.totalWallets;
allWalets = walletHistory.allWalets;
allAddresses = walletHistory.allAddresses;
console.log("创建钱包 .....................");
let keyPair: ECPairInterface = decodeNetwork(network);
let walletData: walletTypes.Payment = decodeWalletType(walletType, keyPair);
let address: string | undefined = walletData.address;
totalWallets += 1;
allWalets.push(walletname);
console.log("初始化地址:", address);
allAddresses.push(address);
console.log("所有地址", allAddresses);
const newWallet: walletData = {
keyPair: keyPair,
walletObject: walletData,
walletType: walletType
}
const newHistory: History = {
totalWallets: totalWallets,
allWalets: allWalets,
allAddresses: allAddresses
}
writeWalletDataToFile(walletname, newWallet);
writeWalletHistoryToFile(newHistory);
console.log("钱包创建成功。你的钱包地址是: " + address);
}`` 上述代码是一个名为
createWallet的函数,用于生成一个新钱包。它开始时通过调用
fetchWalletHistory函数获取钱包历史记录,其中包括钱包总数、名称和地址。随后,它初始化变量以存储检索到的钱包历史数据,然后解码用户传入的新钱包所需的网络和钱包类型参数,以获得所需的密钥对和钱包数据。该函数接着增加钱包总数,并将新钱包名称及其对应地址附加到相应数组。然后,它构建一个表示新钱包的对象,包括其密钥对、钱包对象和类型。此外,它生成一个新的历史对象,包含更新的钱包计数、名称和地址。该函数使用
writeWalletDataToFile将新钱包的数据写入文件,并通过
writeWalletHistoryToFile` 函数更新钱包历史文件。最后,它记录一条消息以确认钱包的创建并显示其地址。上述函数调用了一些辅助函数“ decodeNetwork(network) ” 和 “ decodeWalletType(walletType, keyPair) ”,我们将接下来讨论这些函数。
辅助函数
```
function decodeNetwork(network: string): ECPairInterface {
let keyPair: ECPairInterface;
switch (network) {
case 'testnet':
keyPair = ECPair.makeRandom({ network: TESTNET });
break;
case 'bitcoin':
keyPair = ECPair.makeRandom({ network: BITCOIN });
break;
case 'regtest':
keyPair = ECPair.makeRandom({ network: REGTEST });
break;
default:
throw new Error ('无效的钱包类型');
}
return keyPair;
}
function decodeWalletType(userInput: string, keyPair: ECPairInterface):walletTypes.Payment {
let walletData: walletTypes.Payment;
switch (userInput) {
case 'embed':
walletData = bitcoin.payments.embed({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2ms':
walletData = bitcoin.payments.p2ms({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2pk':
walletData = bitcoin.payments.p2pk({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2pkh':
walletData = bitcoin.payments.p2pkh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2sh':
walletData = bitcoin.payments.p2sh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2wpkh':
walletData = bitcoin.payments.p2wpkh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2wsh':
walletData = bitcoin.payments.p2wsh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2tr':
walletData = bitcoin.payments.p2tr({pubkey: keyPair.publicKey, network: TESTNET,});
break;
default: throw new Error('未知的钱包类型: ' + userInput);
}
return walletData;
}
```
上述代码块包含两个函数,分别用于在我们的比特币钱包创建过程中解码网络和钱包类型参数。
1. decodeNetwork(network: string): ECPairInterface: 此函数接受一个字符串类型的网络参数,代表所需的网络类型(‘testnet’、‘bitcoin’、‘regtest’)。然后它使用 switch 语句根据提供的输入判断相应的网络配置。对于每种情况,它使用 ECPair.makeRandom() 生成一个与指定网络相关的随机密钥对。如果提供了无效的网络类型,则会抛出一个错误。最后,它返回生成的密钥对。
2. decodeWalletType(userInput: string, keyPair: ECPairInterface): 此函数解码用户输入以确定所需的钱包类型(‘p2ms’、‘p2pk’、‘p2pkh’ 等)。它还接受从上一个函数生成的密钥对作为输入。与 decodeNetwork 相似,它利用 switch 语句构建适当的钱包数据,根据提供的输入和网络配置。它使用 bitcoinJs 库生成并返回生成的钱包数据。
在钱包目录中存储和检索加密数据
`` function writeWalletDataToFile(walletname: string, walletData: walletData ): void { let filename: string =
${walletname}.json; let filePath: string =
./wallets/${filename}`;
const encryptedData = encryptData(JSON.stringify(walletData));
fs.writeFileSync(filePath, encryptedData);
console.log("加密的钱包数据成功存储.......");
}
function readWalletDataFromFile(walletname: string): walletData {
let filePath: string = ./wallets/${walletname}
;
console.log("访问钱包数据.............")
try {
let data = fs.readFileSync(filePath, 'utf8');
data = decryptData(data);
console.log("钱包数据成功访问......");
return JSON.parse(data);
} catch (e) {
console.log("访问钱包数据时出错......");
throw new Error (e);
}
}
function writeWalletHistoryToFile(walletHistory: History): void {
let filename: string = 'history.json';
let filePath: string = ./wallets/${filename}
;
const encryptedData = encryptData(JSON.stringify(walletHistory));
fs.writeFileSync(filePath, encryptedData);
}上述代码块包含与加密钱包数据有关的函数,并将其存储到项目根目录的钱包文件夹中的各个文件中; - **“ writeWalletDataToFile(walletname: string, walletData: walletData ): void:”** 此函数接受钱包名称和相应的数据对象作为参数。它根据钱包名称构建文件名和文件路径,然后使用后续将解释的 encryptData 辅助函数对钱包数据进行加密。然后使用 fs.writeFileSync 将加密的数据写入指定的文件路径。此函数确保在加密之后安全地将钱包数据存储在文件中,以便即使程序关闭后也能进行检索。 - “ **readWalletDataFromFile(walletname: string): walletData:**” 此函数根据提供的钱包名称读取文件中的钱包数据。它使用钱包名称构建文件路径,并尝试从文件中读取数据。在成功读取后,它使用 decryptData 函数(后续将解释)对数据进行解密,这与写入时使用的加密算法相对应。然后将解密后的数据解析为 JSON 对象并返回。如果在读取文件或解密时发生错误,则会抛出错误。 - “ **writeWalletHistoryToFile(walletHistory: History): void:**” 此函数负责将钱包历史数据写入文件。它将钱包历史对象作为输入,构建文件路径,加密历史数据,然后使用 fs.writeFileSync 将其写入文件。与 writeWalletDataToFile 类似,此函数确保在加密后安全地存储钱包历史数据,并且可以在用户定义的时间之后重新访问。
function fetchWalletHistory(): History {
console.log("获取钱包历史..............");
const historyFilePath = './wallets/history.json';
try {
if (!fs.existsSync(historyFilePath)) {
// 如果不存在则创建一个新的空文件
console.log("新的钱包历史文件已创建。");
fs.writeFileSync(historyFilePath, '{}');
const newHistory: History = {
totalWallets: 0,
allWalets: [],
allAddresses: []
}
writeWalletHistoryToFile(newHistory);
return newHistory; // 返回一个空对象
}
let data = fs.readFileSync(historyFilePath, 'utf8');
data = decryptData(data);
console.log("钱包历史成功访问......");
return JSON.parse(data);
} catch (e) {
console.log("访问钱包历史时出错......");
throw e;
}
```
上述函数确保从文件中检索钱包历史数据,在必要时创建一个新文件,并处理可能在过程中发生的错误。它在为应用程序中的各种操作提供钱包历史信息访问方面起着至关重要的作用。
首先,它通过使用 fs.existsSync 方法检查历史文件是否存在,以确定文件是否存在于指定路径 (./wallets/history.json)。如果历史文件不存在,函数将在指定路径上创建一个新的空文件,使用 fs.writeFileSync。它初始化一个表示钱包历史的空对象并将其写入新创建的文件,确保始终有一个历史文件可用于存储钱包历史数据。
如果历史文件存在,函数将使用 fs.readFileSync 读取其内容,并指定编码为 UTF-8。然后它使用 decryptData 函数对数据进行解密,该函数对应于写入历史文件时使用的加密算法。在解密数据之后,函数将 JSON 格式的数据解析为表示钱包历史的 JavaScript 对象。它将此对象返回给调用者,提供对钱包历史数据的访问以供进一步处理。
加密和解密钱包数据/历史// 加密数据的函数
```
function encryptData(data: string): string {
console.log("加密数据..........");
const cipher = crypto.createCipher(algorithm, encryptionKey);
let encryptedData = cipher.update(data, 'utf8', 'hex');
encryptedData += cipher.final('hex');
console.log("加密成功..........");
return encryptedData;
}
// 解密数据的函数
function decryptData(encryptedData: string): string {
console.log("解密数据..........");
const decipher = crypto.createDecipher(algorithm, encryptionKey);
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8');
decryptedData += decipher.final('utf8');
console.log("解密成功..........");
return decryptedData;
}
function listAllAddresses(): [string | undefined] {
console.log("访问所有地址...");
let history = fetchWalletHistory();
let allAddresses = history.allAddresses;
console.log("所有可用地址是: " + JSON.stringify(allAddresses));
return allAddresses;
}
```
此最后一段代码包含用于加密和解密存储或检索的数据的主要逻辑。它还实现了从本地机器读取钱包历史数据并记录所有通过我们程序创建的地址的函数。
“ encryptData(data: string): string:” 该函数接受一串数据作为输入并使用指定的加密算法(algorithm)和加密密钥(encryptionKey)进行加密。它使用 crypto.createCipher 方法创建一个加密对象,使用 'utf8' 编码更新数据,并以十六进制格式('hex')生成加密数据。最后,它返回加密数据。
“ decryptData(encryptedData: string): string:” 该函数接受加密数据作为输入,并使用相同的加密算法和加密密钥进行解密。它使用 ‘crypto.createDecipher’ 方法创建一个解密对象,以十六进制格式('hex')更新加密数据,并使用 UTF-8 编码生成解密数据。最后,它返回解密数据。
“listAllAddresses(): [string | undefined]:” 该函数从钱包历史中检索所有钱包地址。它首先使用 fetchWalletHistory 函数获取钱包历史,然后提取所有钱包地址的数组。它将检索到的地址记录到控制台并以字符串或未定义值数组的形式返回。
使用代码库创建钱包
结合上述所有代码块,我们现在能够成功创建一个新钱包,加密钱包细节,并将加密数据存储在本地机器上,以便在需要时检索和解密。我们可以通过在代码库中的最后一行代码下添加以下代码来测试我们代码库的功能。// 以下代码仅用于测试目的。
console.log("运行测试..."); createWallet("myWallet", "regtest", "p2pkh"); console.log("================================================================"); createWallet("BobWallet", "regtest", "p2pkh"); console.log("================================================================"); createWallet("ALiceWallet", "regtest", "p2pkh"); console.log("================================================================"); listAllAddresses();
要测试我们的代码库,我们只需保存的工作流,然后运行命令“ tsc index.ts”。该命令将生成一个名为 index.js 的文件,使其对应于我们 TypeScript 代码库的转录 JavaScript 表示。接下来,我们通过运行命令“ node index.js”来执行 js 文件。该命令运行创建多个钱包的函数,然后返回包含在我们 cli 钱包中的所有地址列表。
结论
的代码库实现了我们在一个极简钱包中所需的大部分功能,除了初始化交易功能;我们将在本文的第二部分讨论该功能,我们将创建一个函数来花费用户的 UTXO。有关我们阶段的完整实现,请访问 GitHub: