Skip to content

Deprecate / Archive this repo - it's does more harm than good. Prefer fetch + JSDoc. #77

@coolaj86

Description

@coolaj86

After years of submitting bugfixes and refactoring this library a little at a time, I finally found out what it actually does - which is so simple by comparison to how this C++ to JS port (I assume) turned out (I assume by the bitcoin team) that it's difficult to believe.

I'm not even sure if the people who use this regularly know what this does due to all of the complex and abstract metaprogramming obscuring the functionality (otherwise I imagine they wouldn't use it).

It's just a really, really complicated way to do an http call that's actually this simple:

DashRPC, in truth:

curl "$rpc_protocol://$rpc_hostname:$rpc_port/" \
    --user "$rpc_username:$rpc_password" \
    -H 'Content-Type: application/json' \
    --data-binary '{ "id": 37, "method": "getbestblock", "params": [] }'

DashRPC, as a JS function:

Here's the library reimplemented in just a few lines:

Source: DashTx.js

  /**
   * @param {String} basicAuthUrl - ex: https://api:token@trpc.digitalcash.dev/
   *                                    http://user:pass@localhost:19998/
   * @param {String} method - the rpc, such as 'getblockchaininfo',
   *                          'getaddressdeltas', or 'help'
   * @param {...any} params - the arguments for the specific rpc
   *                          ex: rpc(url, 'help', 'getaddressdeltas')
   */
  async function rpc(basicAuthUrl, method, ...params) {
    let url = new URL(basicAuthUrl);
    let baseUrl = `${url.protocol}//${url.host}${url.pathname}`;
    let basicAuth = btoa(`${url.username}:${url.password}`);

    // typically http://localhost:19998/
    let payload = JSON.stringify({ method, params });
    let resp = await fetch(baseUrl, {
      method: "POST",
      headers: {
        Authorization: `Basic ${basicAuth}`,
        "Content-Type": "application/json",
      },
      body: payload,
    });

    let data = await resp.json();
    if (data.error) {
      let err = new Error(data.error.message);
      Object.assign(err, data.error);
      throw err;
    }

    return data.result;
  };

DashRPC, as a JS lib:

Or if you want more of a library feel with a constructor, some options, and few more niceties:

let baseUrl = `${protocol}://${host}:${port}/`;
let rpc = createRpcClient({ baseUrl, username, password });

let result = await rpc.request('getaddressbalance', "yhhZ1o9TsaJzh2YKA7qM5vD2BgjT5XffvK");
function createRpcClient({ baseUrl, username, password }) {
  let basicAuth = btoa(`${username}:${password}`);

  async function request(rpcname, ...args) {
    rpcname = rpcname.toLowerCase();
    let id = getRandomId();
    let body = { id: id, method: rpcname, params: args };
    let payload = JSON.stringify(body);

    let resp = await fetch(rpcBaseUrl, {
      method: 'POST',
      headers: {
        'Authorization': `Basic ${basicAuth}`,
        'Content-Type': 'application/json'
      },
      body: payload,
    });

    let data = await resp.json();
    if (data.error) {
      console.debug(`DEBUG rpcname: ${rpcname}`, args, data.error.code, data.error.message);
      let err = new Error(data.error.message);
      Object.assign(err, data.error);
      throw err;
    }

    let result = data.result || data;
    return result;
  }

  return {
    request,
  };
}

// optional, not required
function getRandomId() {
  let f64 = Math.random() * 100000;
  let i32 = Math.round(f64);
  return i32;
}

Adding some flourish

init()

And if you wanted to make it convenient, you could add an init() method that loops until E_IN_WARMUP disappears:

let baseUrl = `${protocol}://${host}:${port}/`;
let rpc = createRpcClient({ baseUrl, username, password });

await rpc.init();
let result = await rpc.request('getaddressbalance', "yhhZ1o9TsaJzh2YKA7qM5vD2BgjT5XffvK");
function createRpcClient({ baseUrl, username, password }) {

  // ...

  const E_IN_WARMUP = -28;

  async function init() {
    for (;;) {
      let result = await request('getblockchaininfo').catch(function (err) {
        if (err.code === E_IN_WARMUP) {
          return null;
        }
        throw err;
      });
      if (result) {
        return result;
      }
    }
  }

  return {
    init,
    request,
  };
}

Type Hinting

The argument could be made that this provides some type hinting, but it doesn't even work with tsc or vim or VSCode.

It's done in such a bespoke way, that can't be auto-generated to keep up with the actual Dash RPCs, so it's worse to have it than to not having it at all.

If there were some machine-friendly JSON file for type hints, it could very simply be applied to each argument at the time each request is made:

  function request(rpcname, ...args) {
    rpcname = rpcname.toLowerCase();
    assertTypes(rpcname, args);

    // ...
  }

  // the hundred+ different rpc names and types go here 
  let allTypeHints = { 'getfoothing': [ [ 'number' ], [ 'number', 'string', null ] ] };

  function assertTypes(rpcname, args) {
    let typeHints = allTypeHints[rpcname];

    for (let i = 0; i < args.length; i += 1) {
      let arg = args[i];
      let typeHint = typeHints[i];
      assertType(typeHint, arg, i);
    }
  }

  function assertType(typeHint, arg, i) {
    if (!typeHint) { // may be a new extra arg we don't know yet
      return;
    }

    let thisType = typeof arg;
    let isType = typeHint.includes(typeHint);
    if (isType) { // is a known arg of a known type
      return;
    }

    let isNullish = !arg && 'boolean' !== thisType && typeHint.includes(null);
    if (isNullish) { // is an optional arg
      return;
    }

    throw new Error(`expected params[${i}] to be one of [${typeHint}], but got '${thisType}'`);
  }

Alternatively the type hinting could be generated as a build step... but it would result it thousands of extra lines of code (I know because I experimented with it already: https://github.com/dashhive/DashRPC.js/blob/v20.0.0/scripts/generate.js)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions