本文是一篇面向开发者的指南,详细介绍了如何使用 TypeScript 和一些关键库(如 BitcoinJS、ecpair、tiny-secp256k1 等)构建一个多功能的加密比特币钱包。文章涵盖了钱包的创建、加密、数据存储和历史记录管理等功能,特别强调了安全和加密的重要性。
钱包是区块链架构中的一个重要工具。这个基本工具或应用程序充当了用户与区块链之间的连接点。对于非开发者来说,他们占据了比特币用户的大部分,他们的大多数与区块链的互动都是通过钱包进行的,因为他们的交易必须使用由钱包控制的账户的私钥进行签名。钱包还在持有和管理用户资产(比特币)方面非常重要,因此,在构建钱包时非常需要确保适当的安全性。在撰写本文时,有许多钱包架构和机制来确保用户资产在钱包中的安全,但本文旨在指导新开发者或比特币爱好者了解钱包创建的一般概念,以及在钱包设计和架构中的安全性、加密、访问和重新访问的重要性。
本文假设你已经了解不同的比特币网络(测试网、回归测试网、主网)以及不同的钱包类型(p2pk、p2pkh 等)。为本文的讨论范围,我们将专注于使用特定库构建一个多功能比特币钱包。
我们的项目将用 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';
上述代码基本上将我们将使用的不同库的内容引入到项目的作用域中。
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;
}
上述代码块包含两个函数,分别用于在我们的比特币钱包创建过程中解码网络和钱包类型参数。
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);
}
上述代码块包含与加密钱包数据有关的函数,并将其存储到项目根目录的钱包文件夹中的各个文件中;
“ 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:
https://github.com/Nonnyjoe/Bitcoin_cli_wallet。
- 原文链接: medium.com/@idogwuchi/bu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!