dashpay / dashd-rpc

Dash Client Library to connect to Dash Core (dashd) via RPC
MIT License
16 stars 65 forks source link

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

Open coolaj86 opened 1 month ago

coolaj86 commented 1 month ago

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)

coolaj86 commented 1 month ago

For reference, GPT says that the type hinting could be achieved like this:

// Step 1: Define the type mappings for your RPC methods
type RpcMethodMap = {
  foobar: [string, ( string | number), number];
  bar: [number, number];
  // Add more methods as needed
};

// Step 2: Create a type that maps each method name to its corresponding argument tuple
type RpcMethodArgs<T extends keyof RpcMethodMap> = RpcMethodMap[T];

// Step 3: Define a generic function that enforces the correct argument types based on the method name
class MyRpcApi {
  request<T extends keyof RpcMethodMap>(method: T, args: RpcMethodArgs<T>): void {
    // Implement the function logic here
    console.log(`Method: ${method}, Args: ${args}`);
  }
}

// Example usage
const myRpcApi = new MyRpcApi();
myRpcApi.request('foobar', ['a', 'b', 3]); // Correct
myRpcApi.request('bar', [1, 2]); // Correct
// myRpcApi.request('foobar', [1, 2, 3]); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.
// myRpcApi.request('bar', ['a', 'b']); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

That could be translated to JSDoc or .d.ts such that editors would pick it up at dev time rather than bloating the runtime code.

/**
 * @typedef {Object} RpcMethodMap
 * @property {[string, ( string | number), number]} foobar
 * @property {[number, number]} bar
 */

/**
 * @template {keyof RpcMethodMap} T
 * @typedef {RpcMethodMap[T]} RpcMethodArgs
 */

class MyRpcApi {
  /**
   * @template {keyof RpcMethodMap} T
   * @param {T} method
   * @param {RpcMethodArgs<T>} args
   */
  request(method, args) {
    // Implement the function logic here
    console.log(`Method: ${method}, Args: ${args}`);
  }
}

// Example usage
const myRpcApi = new MyRpcApi();
myRpcApi.request('foobar', ['a', 'b', 3]); // Correct
myRpcApi.request('bar', [1, 2]); // Correct
// myRpcApi.request('foobar', [1, 2, 3]); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.
// myRpcApi.request('bar', ['a', 'b']); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
/**
 * Defines the argument types for each RPC method.
 */
type RpcMethodMap = {
  foobar: [string, ( string | number), number];
  bar: [number, number];
  // Add more methods as needed
};

/**
 * Utility type to extract the argument tuple type for a given method name.
 */
type RpcMethodArgs<T extends keyof RpcMethodMap> = RpcMethodMap[T];

declare class MyRpcApi {
  /**
   * Makes a request to the specified RPC method with the given arguments.
   * @param method The name of the RPC method to call.
   * @param args The arguments to pass to the RPC method.
   */
  request<T extends keyof RpcMethodMap>(method: T, args: RpcMethodArgs<T>): void;
}

export { MyRpcApi };