{"content":{"title":"区块链基础教程 2 # ETH & BTC 节点API","body":"- 说明：本机环境 Mac 12.2.1，不同环境可能略有差异\r\n- 学习节点 API 需要使用一个 RPC 网关，本文以 [Infura](https://app.infura.io/login) 为例，进行举例说明\r\n\r\n\r\n## 1 ETH 节点API交互\r\n首先需要注册 infura 账号，然后申请一个 API KEY，选择 ETH mainnet。这里获取数据有两种方式，一种是 RESTful API 一种是 WebSocket 请求。\r\n```bash\r\nyarn add dotenv\r\nyarn add request\r\nyarn add ws\r\n```\r\n然后新建 app.js，其中的 proxy 是由于众所周知的网络原因单独配置的，这个跟不同网络环境相关。\r\n```js\r\nconst dotenv = require('dotenv').config();\r\nvar request = require('request');\r\n\r\nvar headers = {\r\n\t'Content-Type': 'application/json'\r\n};\r\n\r\nvar dataString = '{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockByNumber\",\"params\":[\"latest\",true], \"id\":1}';\r\n\r\nvar options = {\r\n\turl: `https://mainnet.infura.io/v3/${process.env.PROJECT_ID}`,\r\n\tmethod: 'POST',\r\n\theaders: headers,\r\n\tbody: dataString,\r\n    proxy: {\r\n        host: '127.0.0.1',\r\n        port: 7890,\r\n     }  \r\n};\r\n\r\nfunction callback(error, response, body) {\r\n\tif (!error && response.statusCode == 200) {\r\n\t\tjson = response.body;\r\n\t\tvar obj = JSON.parse(json);\r\n\t\tconsole.log(obj)\r\n\t} else {\r\n        console.log(error)\r\n    }\r\n}\r\n\r\nrequest(options, callback);\r\n```\r\n\r\n![截屏2023-12-12 下午5.00.51.png](https://img.learnblockchain.cn/attachments/2023/12/qgKiXzVD65782176257b6.png)\r\n\r\n\r\n![截屏2023-12-12 下午5.02.03.png](https://img.learnblockchain.cn/attachments/2023/12/70c8NHnD6578218f6c559.png)\r\n\r\nWebSocket 示例：\r\n```js\r\nconst dotenv = require('dotenv').config();\r\nconst WebSocket = require('ws');\r\n\r\nconst ws = new WebSocket(`wss://mainnet.infura.io/v3/${process.env.PROJECT_ID}`);\r\nws.on('open', function open() {\r\n\tws.send('{\"jsonrpc\":\"2.0\",\"method\":\"eth_subscribe\",\"params\":[\"newHeads\"], \"id\":1}');\r\n});\r\n\r\nws.on('message', function incoming(data) {\r\n\tvar obj = JSON.parse(data);\r\n\tconsole.log(obj);\r\n\tws.close()\r\n});\r\n```\r\n# 2 BTC节点API交互\r\n比特币钱包有多种地址形式：\r\n* 以`1`开头的`P2PKH（Pay-to-Public-Key-Hash）`地址，顾名思义是基于公钥哈希进行交易的地址，它是基于公钥私钥的地址形式\r\n* 以`3` 开头的`P2SH（Pay-to-Script-Hash）`地址，与`P2PKH`不同的是它是通过`赎回脚本`进行交易的，注意：这种地址可能是`Segwit（隔离见证）`也可能不是\r\n* 以`bj`开头的`Segwit（隔离见证）`地址，后面会详细介绍\r\n* 以`m、n、2` 开头的地址，一般是测试网络上的地址\r\n\r\n比特币社区推荐大家尽可能使用以`3`开头的地址，因为它比`1`开头的地址具备更多扩展性。\r\n\r\n\r\n\r\nBTC 需要用到 [blockcypher](https://www.blockcypher.com/) API。`BlockCypher`除了可以使用比特币区块链的测试环境外，还可以使用其自身提供的一个测试环境，只需要将 Api 的 url 前缀写成`api.blockcypher.com/v1/bcy/test`就可以了。\r\n\r\n对于比特币中的隔离见证地址，基于 blockcypher 的方式就不能用了，另外一种创建比特币交易的方式——通过`bitcoinjs-lib`来创建交易(本文示例代码基于**bitcoinjs-lib v3.3.2**)。\r\n\r\n## 2.1 生成账号\r\n### 创建 P2PKH 钱包\r\n```js\r\nconst bitcoin = require('bitcoinjs-lib');\r\n// 创建钱包\r\nconst keyPair = bitcoin.ECPair.makeRandom();\r\n// 钱包地址：19AAjaTUbRjQCMuVczepkoPswiZRhjtg31\r\nconsole.log(keyPair.getAddress());\r\n// 钱包私钥：Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct\r\nconsole.log(keyPair.toWIF());\r\n```\r\n![截屏1](https://img.learnblockchain.cn/attachments/2024/01/AslA45Fi6592aa3e1ddb5.png)\r\n\r\n### 私钥导入 P2PKH 钱包\r\n```js\r\nconst bitcoin = require('bitcoinjs-lib');\r\n\r\n// 导入钱包\r\nconst keyPair = bitcoin.ECPair.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct')\r\n// 钱包地址：19AAjaTUbRjQCMuVczepkoPswiZRhjtg31\r\nconsole.log(keyPair.getAddress());\r\n```\r\n\r\n![截屏2](https://img.learnblockchain.cn/attachments/2024/01/OvVQhwsb6592aab04ace7.png)\r\n\r\n### 通过 P2SH 创建 SegWit 钱包\r\n```js\r\nconst bitcoin = require('bitcoinjs-lib');\r\n\r\n// 导入 P2PKH 钱包\r\nconst keyPair = bitcoin.ECPair.fromWIF('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct')\r\nconst pubKey = keyPair.getPublicKeyBuffer()\r\n\r\nconst redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(bitcoin.crypto.hash160(pubKey))\r\nconst scriptPubKey = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(redeemScript))\r\nconst address = bitcoin.address.fromOutputScript(scriptPubKey)\r\n\r\n// 钱包地址：34AgLJhwXrvmkZS1o5TrcdeevMt22Nar53\r\n// 它既是 P2SH 也是 SegWit 钱包\r\nconsole.log(address);\r\n```\r\n\r\n![截屏3](https://img.learnblockchain.cn/attachments/2024/01/Z3s31oc36592ae28cfae8.png)\r\n### 创建测试网络钱包\r\n```js\r\nconst bitcoin = require('bitcoinjs-lib');\r\n\r\n// 测试网络\r\nconst testnet = bitcoin.networks.testnet;\r\n// 测试钱包\r\nconst keyPair = bitcoin.ECPair.makeRandom({ network: testnet });\r\n// 钱包地址：n1HiJCt8YKujJKqBVdBde95ZYw9WLfVQVz\r\nconsole.log(keyPair.getAddress());\r\n// 钱包私钥：cQ18zisWxiXFPuKfHcqKBeKHSukVDj7F9xLPxU2pMz8bCanxF8zD\r\nconsole.log(keyPair.toWIF());\r\n```\r\n![截屏4](https://img.learnblockchain.cn/attachments/2024/01/cg7qJU686592ae4ab999a.png)\r\n\r\n## 2.2 创建交易\r\n### 普通地址的交易\r\n\r\n```js\r\nconst bitcoin = require('bitcoinjs-lib');\r\n\r\n// 创建钱包\r\nconst alice = bitcoin.ECPair.fromWIF('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy');\r\n// 构建交易 builder\r\nconst txb = new bitcoin.TransactionBuilder();\r\n\r\n// 添加交易中的 Inputs，假设这个 UTXO 有 15000 satoshi\r\ntxb.addInput('61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', 0);\r\n// 添加交易中的 Outputs，矿工费用 = 15000 - 12000 = 3000 satoshi\r\n// addOutput 方法的参数分别为收款地址和转账金额\r\ntxb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000);\r\n\r\n// 交易签名\r\ntxb.sign(0, alice);\r\n// 打印签名后的交易 hash\r\nconsole.log(txb.build().toHex());\r\n```\r\n\r\n![截屏5](https://img.learnblockchain.cn/attachments/2024/01/nge9vSSI6592b1275b361.png)\r\n### 隔离见证地址的交易\r\n```js\r\nconst keyPair = bitcoin.ECPair.fromWIF('cMahea7zqjxrtgAbB7LSGbcQUr1uX1ojuat9jZodMN87JcbXMTcA', testnet);\r\nconst pubKey = keyPair.getPublicKeyBuffer();\r\nconst pubKeyHash = bitcoin.crypto.hash160(pubKey);\r\n// 得到隔离见证地址的回执脚本\r\nconst redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(pubKeyHash);\r\n\r\n// 构建交易 builder\r\nconst txb = new bitcoin.TransactionBuilder();\r\n\r\n// 添加交易中的 Inputs，假设这个 UTXO 有 15000 satoshi\r\ntxb.addInput('61d520ccb74288c96bc1a2b20ea1c0d5a704776dd0164a396efec3ea7040349d', 0);\r\n// 添加交易中的 Outputs，矿工费用 = 15000 - 12000 = 3000 satoshi\r\n// addOutput 方法的参数分别为收款地址和转账金额\r\ntxb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000);\r\n\r\n// 交易签名\r\ntxb.sign(0, keyPair, redeemScript, null, 15000);\r\n// 打印签名后的交易 hash\r\nconsole.log(txb.build().toHex());\r\n```\r\n## 2.2 查询账户余额\r\nBTC 采用的不是余额模型，所以这个地方实际查询的是UTXO。Inputs 需要获取转出地址的 UTXO，那有什么简便的方式来获取地址的 UTXO 呢？所幸`BlockChain`提供了一个 Api 可以来获取地址的 UTXO，Api信息如下：\r\n* 网址：<https://blockchain.info/unspent?active=$address>\r\n* 方法：GET\r\n* 参数\r\n  * active: 要查询 UTXO 的地址，多个地址可以用`|`号分隔\r\n  * limit: 返回的记录数限制，默认是 250，最大是 1000\r\n  * confirmations: 查询的 UTXO 必须是大于多少个确认，比如 confirmations=6\r\n\r\n```js\r\n{\r\n    \"unspent_outputs\":[\r\n        {\r\n            \"tx_age\":\"1322659106\",\r\n            \"tx_hash\":\"e6452a2cb71aa864aaa959e647e7a4726a22e640560f199f79b56b5502114c37\",\r\n            \"tx_index\":\"12790219\",\r\n            \"tx_output_n\":\"0\",\r\n            \"script\":\"76a914641ad5051edd97029a003fe9efb29359fcee409d88ac\", (Hex encoded)\r\n            \"value\":\"5000661330\"\r\n        }\r\n    ]\r\n}\r\n```\r\n在返回的结果中有我们需要的交易 hash 和索引值，以及用来计算需要多少个 input 的 value 值，这样就可以算出交易需要哪些 Inputs 了。\r\n在比特币的交易中，如果矿工费用设置过高或者过低，交易都不能成功生成，所以我们还需要计算交易中的矿工费用，这里有一个公式可以大致预估出交易所需的 size，然后将 size 再乘以`每比特的价格`就可以得到矿工费用了。\r\n> size = inputsNum \\* 180 + outputsNum \\* 34 + 10 (+/-) 40\r\n\r\n* inputNum 指交易中的 Input 个数\r\n* outputNum 指交易中的 Output 个数\r\n* 最后一部分是加减 40\r\n\r\n## 2.3 获取块高\r\n```js\r\nconst fetch = require('node-fetch');\r\n\r\nasync function getBtcBlockHeight() {\r\n  const response = await fetch('https://api.blockcypher.com/v1/btc/main');\r\n  const data = await response.json();\r\n  \r\n  return data.height;\r\n}\r\n\r\ngetBtcBlockHeight().then(height => {\r\n  console.log('Current BTC block height:', height);  \r\n});\r\n```\r\n\r\n![截屏6](https://img.learnblockchain.cn/attachments/2024/01/LiA09BHn6592f453d3279.png)\r\n## 2.4 获取块详情\r\n```js\r\nconst fetch = require('node-fetch');\r\n\r\nasync function getBlockDetails(blockHeight) {\r\n    const url = `https://api.blockcypher.com/v1/btc/main/blocks/${blockHeight}`;\r\n    const response = await fetch(url);\r\n    const data = await response.json();\r\n    return data;\r\n}\r\n\r\ngetBlockDetails(700000).then(block => {\r\n    console.log(block)\r\n});\r\n```\r\n\r\n![截屏7](https://img.learnblockchain.cn/attachments/2024/01/qLNQMw2V6592f4e5ed9f9.png)\r\n\r\n## 2.5 获取交易详情\r\n```js\r\nconst fetch = require('node-fetch');\r\n\r\nasync function getTransactionDetails(txid) {\r\n  const url = `https://api.blockcypher.com/v1/btc/main/txs/${txid}`;\r\n  const response = await fetch(url);\r\n  const data = await response.json();\r\n  return data;\r\n}\r\n\r\nconst txid = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098';\r\n\r\ngetTransactionDetails(txid).then(txn => {\r\n  console.log('Transaction Hash:', txn.hash);\r\n  console.log('Confirmations:', txn.confirmations);    \r\n  console.log('Value:', txn.total);\r\n  console.log('Coinbase:', txn.coinbase);\r\n});\r\n```\r\n![截屏8](https://img.learnblockchain.cn/attachments/2024/01/lBnzKsMm6592f55d2cc2a.png)\r\n\r\n## 2.6 交易回执\r\n严格上比特币没有交易回执，比特币因为使用UTXO模型,技术上没有交易回执的概念。与账户模型的以太坊不同,比特币的交易只是从输入到输出的代币转移,没有执行代码生成回执的过程。代码示例试图模拟交易回执的结构,返回交易确认信息。\r\n\r\n```js\r\nconst fetch = require('node-fetch');\r\n\r\nasync function getTransactionReceipt(txid) {\r\n\r\n  const url = `https://api.blockcypher.com/v1/btc/main/txs/${txid}`;\r\n  const response = await fetch(url);  \r\n  const tx = await response.json();  \r\n\r\n  if(!tx.received) {\r\n    throw new Error('Transaction not yet confirmed');\r\n  }\r\n\r\n  const height = tx.received; \r\n  const confirmations = tx.confirmations;\r\n  const url2 = `https://api.blockcypher.com/v1/btc/main/blocks/${height}`;\r\n  const response2 = await fetch(url2);\r\n  const block = await response2.json();\r\n  const timestamp = block.time;\r\n\r\n  const receipt = {\r\n    confirmations,\r\n    timestamp    \r\n  };\r\n\r\n  return receipt;  \r\n}\r\n\r\nconst txid = '0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098';\r\n\r\ngetTransactionReceipt(txid).then(receipt => {  \r\n  console.log(receipt);    \r\n});\r\n```\r\n![截屏9](https://img.learnblockchain.cn/attachments/2024/01/Z3q4nQap6592f5b145d15.png)\r\n\r\n## 2.7 转账 & 广播交易\r\n```js\r\n// 领取测试币\r\nvar data = {\"address\": \"CErXeP7HYZZCMCXRbw8PwW3RpE7mFLECzS\", \"amount\": 100000}\r\naxios.post('https://api.blockcypher.com/v1/bcy/test/faucet?token=a122e0fd87a640e5b411b01f09b5ce3e', JSON.stringify(data))\r\n    .then(function(resp) {console.log(resp.data)});   //水龙头领取测试币\r\n```\r\n\r\n![截屏10](https://img.learnblockchain.cn/attachments/2024/01/YLcEyZyi6592fbea5e569.png)\r\n```js\r\nvar newtx = {\r\n  inputs: [{addresses: ['CErXeP7HYZZCMCXRbw8PwW3RpE7mFLECzS']}],\r\n  outputs: [{addresses: ['C1rGdt7QEPGiwPMFhNKNhHmyoWpa5X92pn'], value: 1000}]\r\n};\r\n\r\n// 创造交易\r\naxios.post('https://api.blockcypher.com/v1/bcy/test/txs/new', JSON.stringify(newtx))\r\n  .then(function(resp) { \r\n    data = resp.data;\r\n    console.log(data)\r\n  }); \r\n```\r\n![截屏11](https://img.learnblockchain.cn/attachments/2024/01/r93gNNkF6592fda1cf946.png)\r\n\r\n```js\r\nvar bitcoin = require(\"bitcoinjs-lib\");\r\nvar secp = require('tiny-secp256k1');\r\nvar ecfacory = require('ecpair');\r\n\r\nvar ECPair = ecfacory.ECPairFactory(secp);\r\n\r\nconst keyBuffer = Buffer.from(my_hex_private_key, 'hex')\r\nvar keys = ECPair.fromPrivateKey(keyBuffer)\r\n\r\nvar newtx = {\r\n  inputs: [{ addresses: ['CEztKBAYNoUEEaPYbkyFeXC5v8Jz9RoZH9'] }],\r\n  outputs: [{ addresses: ['C1rGdt7QEPGiwPMFhNKNhHmyoWpa5X92pn'], value: 100000 }]\r\n};\r\n\r\n// calling the new endpoint, same as above\r\naxios.post('https://api.blockcypher.com/v1/bcy/test/txs/new', JSON.stringify(newtx))\r\n  .then(function (tmptx) {\r\n    // signing each of the hex-encoded string required to finalize the transaction\r\n    tmptx.pubkeys = [];\r\n    tmptx.signatures = tmptx.tosign.map(function (tosign, n) {\r\n      tmptx.pubkeys.push(keys.publicKey.toString('hex'));\r\n      return bitcoin.script.signature.encode(\r\n        keys.sign(Buffer.from(tosign, \"hex\")),\r\n        0x01,\r\n      ).toString(\"hex\").slice(0, -2);\r\n    });\r\n    // sending back the transaction with all the signatures to broadcast\r\n    axios.post('https://api.blockcypher.com/v1/bcy/test/txs/send', JSON.stringify(tmptx))\r\n      .done(function (finaltx) {\r\n        console.log(finaltx);\r\n      })\r\n      .fail(function (xhr) {\r\n        console.log(xhr.responseText);\r\n      });\r\n  });\r\n```\r\n\r\n\r\n\r\n\r\n\r\n# Reference\r\n[1] [Infura 以太坊 API 入门教程](https://learnblockchain.cn/article/1590)\r\n[2] [Using dotenv package to create environment variables](https://medium.com/@thejasonfile/using-dotenv-package-to-create-environment-variables-33da4ac4ea8f)\r\n[3] [比特币交易开发实战（一）](https://zhaozhiming.github.io/2018/07/07/btc-transaction-intro-and-practice-part2/)\r\n[4] [blockcypher api document](https://www.blockcypher.com/dev/bitcoin/?javascript#introduction)"},"author":{"user":"https://learnblockchain.cn/people/17075","address":null},"history":"bafkreiavqh22s3y7sfys6w3xmhp4g3mlhocctyxrizjrfuuf26xsdzngfa","timestamp":1704133141,"version":1}