Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
coolaj86 opened this issue Aug 7, 2024 · 1 comment

Comments

@coolaj86
Copy link
Contributor

coolaj86 commented Aug 7, 2024

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:[email protected]/
   *                                    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
Copy link
Contributor Author

coolaj86 commented Aug 7, 2024

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 };

@coolaj86 coolaj86 changed the title Deprecate / Archive this repo - it's does more harm than good. Just use fetch. Deprecate / Archive this repo - it's does more harm than good. Just use fetch + JSDoc. Aug 7, 2024
@coolaj86 coolaj86 changed the title Deprecate / Archive this repo - it's does more harm than good. Just use fetch + JSDoc. Deprecate / Archive this repo - it's does more harm than good. Prefer fetch + JSDoc. Aug 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant