function jsonRpc(uri, {method, id, params, fetchMethod=fetch, timeoutMs=30000}) {
  const payload = {id, jsonrpc: '2.0', method, params};
  
  let timeoutId = null;
  
  // Create a promise that will reject after the timeout
  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new Error(`Request timeout after ${timeoutMs}ms`));
    }, timeoutMs);
  });
  
  // Create the fetch promise
  const fetchPromise = fetchMethod(uri, {
    body: JSON.stringify(payload),
    method: 'post',
    mode: 'cors',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json',
    },
  }).then(res => {
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    }
    return res.json();
  }).then(rpcRes => {
    if (rpcRes.id !== id) {
      throw new Error(`Invalid response id: ${rpcRes.id}`);
    }
    if (rpcRes.error) {
      throw new RPCError(rpcRes.error);
    }
    return rpcRes.result;
  });
  
  // Race the fetch against the timeout
  return Promise.race([fetchPromise, timeoutPromise])
    .finally(() => {
      // Clear the timeout to avoid memory leaks
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
    });
}
Makes a JSON-RPC request using `fetch` or a user-provided `fetchMethod`.

@param {string} uri - The URI to the JSON-RPC endpoint.
@param {string} options.method - The remote JSON-RPC method to call.
@param {string} options.id - ID for the request, for matching to a response.
@param {*} options.params  - The params for the remote method.
@param {function} [options.fetchMethod=fetch] - A function with the same
signature as `fetch`, which can be used to make the network request, or for
stubbing in tests.
@param {number} [options.timeoutMs=30000] - Request timeout in milliseconds.