diff --git a/.eslintrc.json b/.eslintrc.json index bd3056e..6bb0b5d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -29,7 +29,10 @@ "hash_or_height", "include_mempool", "template_request", - "fee_delta" + "fee_delta", + "address_type", + "estimate_mode", + "conf_target" ] } ] diff --git a/README.md b/README.md index f99d4ab..edb0bcc 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,70 @@ const state = false; const result = await client.setnetworkactive({ state }); ``` +### Util + +- [`createmultisig`](https://bitcoin.org/en/developer-reference#createmultisig) + +```javascript +const nrequired = 2; +const keys = [ + "03789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd", + "03dbc6764b8884a92e871274b87583e6d5c2a58819473e17e107ef3f6aa5a61626" +]; +const address_type = "bech32"; +const result = await client.createmultisig({ nrequired, keys, address_type }); +``` + +- [`deriveaddresses`](https://bitcoin.org/en/developer-reference#deriveaddresses) + +```javascript +const descriptor = + "wpkh([d34db33f/84'/0'/0']tpubD6NzVbkrYhZ4YTN7usjEzYmfu4JKqnfp9RCbDmdKH78vTyuwgQat8vRw5cX1YaZZvFfQrkHrM2XsyfA8cZE1thA3guTBfTkKqbhCDpcKFLG/0/*)#8gfuh6ex"; +const range = [0, 2]; +const result = await client.deriveaddresses({ descriptor, range }); +``` + +- [`estimatesmartfee`](https://bitcoin.org/en/developer-reference#estimatesmartfee) + +```javascript +const conf_target = 2; +const estimate_mode = "ECONOMICAL"; +const result = await client.estimatesmartfee({ conf_target, estimate_mode }); +``` + +- [`getdescriptorinfo`](https://bitcoin.org/en/developer-reference#getdescriptorinfo) + +```javascript +const descriptor = + "wpkh([d34db33f/84h/0h/0h]0279be667ef9dcbbac55a06295Ce870b07029Bfcdb2dce28d959f2815b16f81798)"; +const result = await client.getdescriptorinfo({ descriptor }); +``` + +- [`signmessagewithprivkey`](https://bitcoin.org/en/developer-reference#signmessagewithprivkey) + +```javascript +const privkey = "yourPrivateKey"; +const message = "Hello World"; +const result = await client.signmessagewithprivkey({ privkey, message }); +``` + +- [`validateaddress`](https://bitcoin.org/en/developer-reference#validateaddress) + +```javascript +const address = "1HLoD9E4SDFFPDiYfNYnkBLQ85Y51J3Zb1"; +const result = await client.validateaddress({ address }); +``` + +- [`verifymessage`](https://bitcoin.org/en/developer-reference#verifymessage) + +```javascript +const address = "myv3xs1BBBhaDVU62LFNBho2zSp4KLBkgK"; +const message = "Hello World"; +const signature = + "H14/QyrMj8e63GyEXBDDWnWrplXK3OORnMc3B+fEOOisbNFEAQuNB9myAH9qs7h1VNJb1xq1ytPQqiLcmSwwPv8="; +const result = await client.verifymessage({ address, message, signature }); +``` + ### ZMQ - [`getzmqnotifications`](https://bitcoincore.org/en/doc/0.17.0/rpc/zmq/getzmqnotifications/) diff --git a/package-lock.json b/package-lock.json index 89e1682..88b0fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rpc-bitcoin", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 44ab22a..1a1cbdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rpc-bitcoin", - "version": "1.7.0", + "version": "1.8.0", "description": "A TypeScript library to make RPC and HTTP REST requests to Bitcoin Core", "main": "build/index.js", "type": "module", diff --git a/src/rpc.ts b/src/rpc.ts index e3d8285..00dbf82 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -107,6 +107,33 @@ export type SetBanParams = { absolute?: boolean; }; +export type CreateMultiSigParams = { + nrequired: number; + keys: string[]; + address_type?: "legacy" | "p2sh-segwit" | "bech32"; +}; + +export type DeriveAddressesParams = { + descriptor: string; + range?: number | [number, number]; +}; + +export type EstimateSmartFeeParams = { + conf_target: number; + estimate_mode?: "UNSET" | "ECONOMICAL" | "CONSERVATIVE"; +}; + +export type SignMessageWithPrivKeyParams = { + privkey: string; + message: string; +}; + +export type VerifyMessageParams = { + address: string; + signature: string; + message: string; +}; + export class RPCClient extends RESTClient { wallet?: string; fullResponse?: boolean; @@ -514,6 +541,65 @@ export class RPCClient extends RESTClient { return this.rpc("setnetworkactive", { state }); } + /** + * @description Creates a multi-signature address with n signature of m keys required. + */ + async createmultisig({ + nrequired, + keys, + address_type = "legacy" + }: CreateMultiSigParams) { + return this.rpc("createmultisig", { nrequired, keys, address_type }); + } + + /** + * @description Derives one or more addresses corresponding to an output descriptor. + */ + async deriveaddresses({ descriptor, range }: DeriveAddressesParams) { + return this.rpc("deriveaddresses", { descriptor, range }); + } + + /** + * @description Estimates the approximate fee per kilobyte needed for a transaction to begin confirmation within `conf_target` blocks if possible and return the number of blocks for which the estimate is valid. + */ + async estimatesmartfee({ + conf_target, + estimate_mode = "CONSERVATIVE" + }: EstimateSmartFeeParams) { + return this.rpc("estimatesmartfee", { conf_target, estimate_mode }); + } + + /** + * @description Analyses a descriptor. + */ + async getdescriptorinfo({ descriptor }: { descriptor: string }) { + return this.rpc("getdescriptorinfo", { descriptor }); + } + + /** + * @description Sign a message with the private key of an address. + */ + async signmessagewithprivkey({ + privkey, + message + }: SignMessageWithPrivKeyParams) { + return this.rpc("signmessagewithprivkey", { privkey, message }); + } + + /** + * @description Return information about the given bitcoin address. + */ + async validateaddress({ address }: { address: string }) { + return this.rpc("validateaddress", { address }); + } + + /** + * @description Verify a signed message + */ + async verifymessage({ address, signature, message }: VerifyMessageParams) { + return this.rpc("verifymessage", { address, signature, message }); + } + /** * @description Returns information about the active ZeroMQ notifications. */ diff --git a/test/rpc.spec.ts b/test/rpc.spec.ts index ed17eb2..c3dba6a 100644 --- a/test/rpc.spec.ts +++ b/test/rpc.spec.ts @@ -1582,6 +1582,141 @@ suite("RPCClient", () => { }); }); + suite("Util", () => { + test(".createmultisig()", async () => { + const nrequired = 2; + const keys = [ + "03789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd", + "03dbc6764b8884a92e871274b87583e6d5c2a58819473e17e107ef3f6aa5a61626" + ]; + const address_type: "bech32" = "bech32"; + const params = { nrequired, keys, address_type }; + const request = { params, method: "createmultisig", id, jsonrpc }; + const result = { + address: + "tb1q0jnggjwnn22a4ywxc2pcw86c0d6tghqkgk3hlryrxl7nmxkylmnqdcdsu7", + redeemScript: + "522103789ed0bb717d88f7d321a368d905e7430207ebbd82bd342cf11ae157a7ace5fd2103dbc6764b8884a92e871274b87583e6d5c2a58819473e17e107ef3f6aa5a6162652ae" + }; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.createmultisig(params); + assert.deepStrictEqual(data, result); + }); + + test(".deriveaddresses()", async () => { + const descriptor = + "wpkh([d34db33f/84'/0'/0']tpubD6NzVbkrYhZ4YTN7usjEzYmfu4JKqnfp9RCbDmdKH78vTyuwgQat8vRw5cX1YaZZvFfQrkHrM2XsyfA8cZE1thA3guTBfTkKqbhCDpcKFLG/0/*)#8gfuh6ex"; + const range: [number, number] = [0, 2]; + const params = { descriptor, range }; + const request = { params, method: "deriveaddresses", id, jsonrpc }; + const result = [ + "tb1q7as9cz0t8rfng5f0xdklfgyp0x6ya0tu6ckaqs", + "tb1q0aducdmz77tfu4dhfez8ayycmp2pz6jwy85hhn", + "tb1qsdqewd8upv66txx48qssr0an5r3llaxtwqzytk" + ]; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.deriveaddresses(params); + assert.deepStrictEqual(data, result); + }); + + test(".estimatesmartfee()", async () => { + const estimate_mode: "ECONOMICAL" = "ECONOMICAL"; + const params = { conf_target: 2, estimate_mode }; + const request = { params, method: "estimatesmartfee", id, jsonrpc }; + const result = { feerate: 0.00001, blocks: 2 }; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.estimatesmartfee(params); + assert.deepStrictEqual(data, result); + }); + + test(".getdescriptorinfo()", async () => { + const descriptor = + "wpkh([d34db33f/84h/0h/0h]0279be667ef9dcbbac55a06295Ce870b07029Bfcdb2dce28d959f2815b16f81798)"; + const params = { descriptor }; + const request = { params, method: "getdescriptorinfo", id, jsonrpc }; + const result = { + descriptor: + "wpkh([d34db33f/84'/0'/0']0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#n9g43y4k", + isrange: false, + issolvable: true, + hasprivatekeys: false + }; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.getdescriptorinfo(params); + assert.deepStrictEqual(data, result); + }); + + test(".signmessagewithprivkey()", async () => { + const privkey = "cPGLkRL4zvHfpcEkjPb93GEHth8WZpTJH2YCCoYWS7kHcFFarn8U"; + const message = "Hello World"; + const params = { privkey, message }; + const request = { params, method: "signmessagewithprivkey", id, jsonrpc }; + const result = + "IIXi7nhOGKbW2uOW2cmV/BbOvlIDzVu0KTZdvntP634/BRL2DmFSvtwifkDMa+pDKd+eRrTbEi6XVAc82JKTiwA="; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.signmessagewithprivkey(params); + assert.deepStrictEqual(data, result); + }); + + test(".validateaddress()", async () => { + const params = { address: "tb1qmv634mfk34sks9z3tcwwncd9ug0why3a0pl4px" }; + const request = { params, method: "validateaddress", id, jsonrpc }; + const result = { + isvalid: true, + address: "tb1qmv634mfk34sks9z3tcwwncd9ug0why3a0pl4px", + scriptPubKey: "0014db351aed368d616814515e1ce9e1a5e21eeb923d", + isscript: false, + iswitness: true, + witness_version: 0, + witness_program: "db351aed368d616814515e1ce9e1a5e21eeb923d" + }; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.validateaddress(params); + assert.deepStrictEqual(data, result); + }); + + test(".verifymessage()", async () => { + const address = "myv3xs1BBBhaDVU62LFNBho2zSp4KLBkgK"; + const signature = + "H14/QyrMj8e63GyEXBDDWnWrplXK3OORnMc3B+fEOOisbNFEAQuNB9myAH9qs7h1VNJb1xq1ytPQqiLcmSwwPv8="; + const message = "Hello World"; + const params = { address, signature, message }; + const request = { params, method: "verifymessage", id, jsonrpc }; + const result = true; + nock(uri) + .post("/", request) + .times(1) + .basicAuth(auth) + .reply(200, { result, error, id }); + const data = await client.verifymessage(params); + assert.deepStrictEqual(data, result); + }); + }); + suite("Zmq", () => { test(".getzmqnotifications()", async () => { const params = {};