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

feat: Web3 RPC Handler (npm package) #1

Merged

Conversation

Keyrxng
Copy link
Member

@Keyrxng Keyrxng commented Mar 1, 2024

Resolves #3

My package is hosted at yarn @keyrxng/web3-rpc-racer which I'm using in the video where I had left off the now closed pr.

  • I've cleaned up the code
  • Now using tsc to export types properly
  • Cache refresh is configured when initializing
  • Removed ts-template skeleton
  • Few more tests

We should import the Chainlist repository as a submodule to stay in sync.

The URLs should be tested upon initialization (i.e. page load) via Promise.race

The class should rank sort based on latency.

The class should drop failed endpoints.

The class should have a wrapper method to make a network request using the optimal RPC endpoint, and to drop it/automatically rotate to the next best from the list if a network request fails.

Make a standalone and polymorphic (frontend and backend compatible) npm module.

Make sure to have unit tests in Jest.

All covered bar one

The wrapper method I haven't implemented as the spec has described it. The optimalRPC is outdated from call to call so getFastestRpcProvider() will retest either allNetworkUrls or just the successful cached rpcs, returning an updated provider which the invoking logic would handle making the call. This way the requested provider is always the quickest other than when calling getProvider() which will return the provider as-is.

the runtime rpcs are dropped as they fail for whatever reason so there is no need for rotation plus you always receive back the lowest latency provider.

Usage
const rpcHandler = RPCHandler.getInstance(networkId, 5);
return rpcHandler.getProvider();
just shows the bad rpcs being excluded until refresh
cache-refresh-every-5.mp4

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 1, 2024

dont hold back @molecula451

@@ -28,8 +28,8 @@ jobs:
yarn
yarn build
# env: # Set environment variables for the build
# SUPABASE_URL: "https://wfzpewmlyiozupulbuur.supabase.co"
# SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndmenBld21seWlvenVwdWxidXVyIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTU2NzQzMzksImV4cCI6MjAxMTI1MDMzOX0.SKIL3Q0NOBaMehH0ekFspwgcu3afp3Dl9EDzPqs1nKs"
# SUPABASE_URL: "https://wfzpewmlyiozupulbuur.supabase.co"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would be best not to publish these

];
import chainlist from "../lib/chainlist/constants/extraRpcs";

const typescriptEntries = ["rpc-handler.ts", "constants.ts", "handler.ts", "services/RPCService.ts", "services/StorageService.ts"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised it passed kebab case test?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ts-template doesn't have the kebab workflow in it by default, pay.ubq does tho. Just copy paste it over?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anvil = 31337,
}
export declare enum Tokens {
DAI = "0x6b175474e89094c44da98b954eedeac495271d0f",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make more sense to associate tokens per network in case they aren't matching

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, should I add any more tokens in btw?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for now but I suppose this should be passed in as constructor arguments instead so that the application can handle this?

export class StorageService {
static getLatencies(env: string): Record<string | number, number> {
if (env === "browser") {
return JSON.parse(localStorage.getItem("rpcLatencies") || "{}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think local storage modification should be within this module.

Instead the results should be stored on the object. The end user should manually handle local storage if it's necessary I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's both, I felt that would be better. The invoking logic doesn't need to do anything besides initialize and the rest is taken care of, it saves having to repeat code across multiple UI's and for node envs the state is still saved onto the object

what would you suggest, that I remove this and have each UI handle it's own storage?

I'd need to expose the runtimeRPCSs and latencies, then the UI would pull those from the object after init and then save them into local. Anytime getFastestProvider is called the UI will pull from local, pass them into the handler then save the output to local again?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can consider decoupling the logic if you don't want each UI to handle it redundantly. Just have the localStorage logic in a separate method.

However I would argue that it may not be necessary to use localStorage at all. If we do a Promise.race once upon page load every time that's probably not bad and will accommodate for changing rpc conditions on a sporadic basis.

Since all the RPC endpoints fire off in parallel we shouldn't have any bottle necks initializing like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like a boolean passed into init enabling local storage handling automatically then methods that just set/get local storage?

The only problem I think is, with every page refresh the class is re-initialized and because the class state of latencies is <= 1 then it'll always race all RPCs. On init I pull the latencies and cache refresh count from localStorage and with passing the refresh arg in on init the UI is able to maintain a cache, so I think local is needed unless I'm not seeing something

We already initialize like you say essence just with the cached rpcs (unless refreshing the list), I can't think how we'd persist state across page visits without using local

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I just think its not really necessary and adds complexity to making this polymorphic, or also being able to run in node.

Copy link
Member

@rndquu rndquu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Keyrxng Pls add a proper readme file which describes:

  • what the package does
  • how to install it
  • how to use it
  • how to run tests

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 4, 2024

idk what the official name will be but I just went with rpc-handler

@molecula451
Copy link
Member

build still fails @Keyrxng

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 4, 2024

build still fails @Keyrxng

Added the kebab scripts (forgot to when I added the workflow).

Deploying to cloudflare should be removed as there is nothing to deploy i.e no static folder which fails the run

and I think Knip is still not fully set up yet, is that right?

@molecula451
Copy link
Member

molecula451 commented Mar 4, 2024

and I think Knip is still not fully set up yet, is that right?

yes you can skip/ignore knip

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 4, 2024

Is the run failing maybe because the workflow isn't on the branch it's being merged into? I have no idea why it would fail when it's a 1:1 copy of the stuff on the pay.ubq repo

@molecula451
Copy link
Member

Is the run failing maybe because the workflow isn't on the branch it's being merged into? I have no idea why it would fail when it's a 1:1 copy of the stuff on the pay.ubq repo

you mean the build.yml?

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 4, 2024

Is the run failing maybe because the workflow isn't on the branch it's being merged into? I have no idea why it would fail when it's a 1:1 copy of the stuff on the pay.ubq repo

you mean the build.yml?

The kebab case workflow, the debug run was a big help

@molecula451
Copy link
Member

The kebab case workflow, the debug run was a big help

yeah we are on a permissions issue: cc @gitcoindev
Screenshot from 2024-03-04 11-50-12

This is destructive and renames files so lets not use it.
@rndquu rndquu self-requested a review March 5, 2024 06:55
@rndquu rndquu mentioned this pull request Mar 5, 2024
@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 5, 2024

You are importing the unbuilt version, import from /dist/cjs it should work

@rndquu
Copy link
Member

rndquu commented Mar 5, 2024

@Keyrxng

Trying to run this code:

import { RPCHandler } from "./src";

async function main() {
    const handler = new RPCHandler(100);    
}

main();

Getting this error:

rndquu@User-MBP web3-rpc-racer % npx tsx my.ts
/Users/rndquu/Public/projects/backend/web3-rpc-racer/src/constants.ts:78
  [networkIds.Mainnet]: ["https://rpc-pay.ubq.fi/v1/mainnet", ...(extraRpcs[networkIds.Mainnet] || [])],
                                                                  ^


ReferenceError: extraRpcs is not defined
    at extraRpcs (/Users/rndquu/Public/projects/backend/web3-rpc-racer/src/constants.ts:78:67)
    at Object.<anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/src/constants.ts:85:27)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Object.S (/Users/rndquu/Public/projects/backend/web3-rpc-racer/node_modules/tsx/dist/cjs/index.cjs:1:1292)
    at Module.load (node:internal/modules/cjs/loader:1207:32)
    at Module._load (node:internal/modules/cjs/loader:1023:12)
    at Module.require (node:internal/modules/cjs/loader:1235:19)
    at require (node:internal/modules/helpers:176:18)
    at <anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/src/index.ts:1:131)
    at Object.<anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/src/index.ts:16:28)

Node.js v20.11.1

What am I doing wrong?

@rndquu
Copy link
Member

rndquu commented Mar 5, 2024

You are importing the unbuilt version, import from /dist/cjs it should work

Thanks, I'll try

@molecula451
Copy link
Member

What am I doing wrong?

extraRpcs it's part of chainlist submodule make sure you git recursively clone or init the module

@rndquu
Copy link
Member

rndquu commented Mar 5, 2024

@Keyrxng

I've installed git submodules via git submodule update --init --recursive and rebuilt the project.

Trying to get RPCs for BNB chain via:

import { RPCHandler } from "./dist/cjs/rpc-handler";

async function main() {
    const handler = new RPCHandler(56);    
}

main();

Getting this error:

TypeError: Cannot read properties of undefined (reading '0')
    at new r (/Users/rndquu/Public/projects/backend/web3-rpc-racer/dist/cjs/rpc-handler.js:19:6622)
    at main (/Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts:4:21)
    at <anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts:7:1)
    at Object.<anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts:7:6)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Object.S (/Users/rndquu/Public/projects/backend/web3-rpc-racer/node_modules/tsx/dist/cjs/index.cjs:1:1292)
    at Module.load (node:internal/modules/cjs/loader:1207:32)
    at Module._load (node:internal/modules/cjs/loader:1023:12)
    at cjsLoader (node:internal/modules/esm/translators:356:17)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:305:7)

What am I doing wrong?

README.md Outdated
- Re-test the cached RPCs

```typescript
const provider = handler.getFastestRpcProvider();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const provider = handler.getFastestRpcProvider();
const provider = await handler.getFastestRpcProvider();

@rndquu
Copy link
Member

rndquu commented Mar 6, 2024

I assume you are just trying to run the script with tsx or something?

Yes

Now I'm getting the Module '"./dist/cjs/src/handler"' has no exported member 'HandlerConstructorConfig' error

Screenshot 2024-03-06 at 16 11 50

How to reproduce:

  1. Checkout to the latest commit from this PR
  2. yarn install
  3. git submodule update --init --recursive
  4. yarn build
  5. npx tsx my.ts
import { HandlerConstructorConfig } from "./dist/cjs/src/handler";
import { RPCHandler } from "./dist/cjs/src/rpc-handler";

const config: HandlerConstructorConfig = {
    autoStorage: true,
    cacheRefreshCycles: 5,
  };

async function main() {
    const networkId = 56;
    const handler = new RPCHandler(networkId, config);
    const provider = await handler.getFastestRpcProvider();
    console.log(provider);
}

main();

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 7, 2024

Your screenshot made me realize two things:

  • type.d out was not right
  • I had added all the ids etc at the type level but no further

I've tested multiple times with fresh vsc instance, yarn, build, test and script and everything is good for me, it should be for you too

  • removed axios and just using built-in fetch
  • config is the only arg passed in now and the only mandatory prop is networkId
  • removed unused stuff like network explorer, currencies, tokens etc as they serve no purpose in racing the RPCs but a good thing to be exported from the package if it's going to be used across many repos
  • fixed the tests
  • found that it's the JsonRpcProvider that is returning the http:/\/localhost:8545"; so I've added the check back in

Copy link
Member

@rndquu rndquu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. If you build the project with yarn build then this line import { HandlerConstructorConfig } from "./dist/cjs/src/handler"; returns the Module '"./dist/cjs/src/handler"' has no exported member 'HandlerConstructorConfig' error. But if you build with yarn build:cjs there is no such error. Pls make sure that yarn build, yarn build:cjs and yarn build:esm are working the same way.
  2. Getting this error:
rndquu@Users-MacBook-Pro web3-rpc-racer % npx tsx my.ts
node:internal/modules/cjs/loader:1144
  const err = new Error(message);
              ^

Error: Cannot find module './dist/cjs/src/rpc-handler'
Require stack:
- /Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts
    at Module._resolveFilename (node:internal/modules/cjs/loader:1144:15)
    at a._resolveFilename (/Users/rndquu/Public/projects/backend/web3-rpc-racer/node_modules/tsx/dist/cjs/index.cjs:1:1729)
    at Module._load (node:internal/modules/cjs/loader:985:27)
    at Module.require (node:internal/modules/cjs/loader:1235:19)
    at require (node:internal/modules/helpers:176:18)
    at <anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts:2:28)
    at Object.<anonymous> (/Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts:16:6)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Object.S (/Users/rndquu/Public/projects/backend/web3-rpc-racer/node_modules/tsx/dist/cjs/index.cjs:1:1292)
    at Module.load (node:internal/modules/cjs/loader:1207:32) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/Users/rndquu/Public/projects/backend/web3-rpc-racer/my.ts' ]
}

Node.js v20.11.1

How to reproduce:

  1. Checkout to the latest commit from this PR
  2. yarn install
  3. git submodule update --init --recursive
  4. yarn build:cjs
  5. npx tsx my.ts
import { HandlerConstructorConfig } from "./dist/cjs/src/handler";
import { RPCHandler } from "./dist/cjs/src/rpc-handler";

const config: HandlerConstructorConfig = {
    networkId: 100,
    autoStorage: false,
    cacheRefreshCycles: 5,
  };

async function main() {
    const handler = new RPCHandler(config);
    const provider = await handler.getFastestRpcProvider();
    console.log(provider);
}

main();

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 10, 2024

Your build dir should look like this after running yarn build

image

yarn build is the only yarn command that should be run as postbuild handles the types

Vid below is me doing a fresh yarn > yarn build > npx tsx ./tests/script-test.ts

I do not understand why you are still getting this error @rndquu

0310.mp4

@0x4007
Copy link
Member

0x4007 commented Mar 10, 2024

Just make a GitHub action to prove its not env related?

@rndquu
Copy link
Member

rndquu commented Mar 11, 2024

@Keyrxng I haven't touched anything in the code since yesterday but today the build is performed without errors

Check this script:

import { HandlerConstructorConfig } from "../dist/cjs/src/handler";
import { RPCHandler } from "../dist/cjs/src/rpc-handler";

const config: HandlerConstructorConfig = {
  networkId: 314,
  autoStorage: false,
};

async function main() {
  const handler = new RPCHandler(config);
  const provider = await handler.getFastestRpcProvider();
  console.log(provider);
}

main();

It returns:

rndquu@User-MBP web3-rpc-racer % npx tsx tests/script-test.ts
[RPCService] Latencies object is empty
[RPCService] Failed to find fastest RPC

Although there are available RPCs for the Filecoin network:
Screenshot 2024-03-11 at 10 03 52

Why is it happening?

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 11, 2024

Happy days we are on the same page with it now tho good stuff

It's failing because I have the request timeout at 500ms, only three rpcs are returned for 314 that have data-tracking-none. Upping the timeout to 1500ms it succeeded, which raises a few questions doesn't it?

We want to keep the timeout low as in the sense that UBQ use it but it might be an idea for me to implement some sort of fallback where if none succeed then try again without the timeout? As we use 100 and 1 and they have lots of RPCS for both those chains the timeout should be kept in as I've seen calls take ^3s, so the timeout keeps things clean when there are lots of endpoints to test but in the case of there being very few endpoints or few good quality endpoints maybe it should be handled? Our use-case and the needs that UBQ have for it are covered anything else is more of a public service than anything I think is it not? :))

Let me know how to handle it and I'll push it today

$ npx tsx ./tests/script-test.ts
chainID 314:  filecoin
chain:  Filecoin
rpcss:  [
  'https://api.node.glif.io',
  'https://node.filutils.com/rpc/v1',
  'https://api.chain.love/rpc/v1'
]
Trace: r {
  _isProvider: true,
Provider available
Block number: 3728033
{
  'https://node.filutils.com/rpc/v1_314': 789.8341999999999,
  'https://api.node.glif.io_314': 1249.8516,
  'https://api.chain.love/rpc/v1_314': 1176.7801
}

@rndquu
Copy link
Member

rndquu commented Mar 11, 2024

It's failing because I have the request timeout at 500ms

that have data-tracking-none

You're right, ubiquity case is covered, all these settings (timeout, data-tracking-none) could be moved to config in a separate issue

Copy link
Member

@rndquu rndquu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works fine

@Keyrxng
Copy link
Member Author

Keyrxng commented Mar 11, 2024

It's failing because I have the request timeout at 500ms
that have data-tracking-none

You're right, ubiquity case is covered, all these settings (timeout, data-tracking-none) could be moved to config in a separate issue

I was going to suggest just that, add timeout etc into config and leave it to the user

package.json Outdated Show resolved Hide resolved
@molecula451 molecula451 linked an issue Mar 14, 2024 that may be closed by this pull request
package.json Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
@molecula451
Copy link
Member

Tested, working good, it looks like https://gnosis-pokt.nodies.app it's making the race at this time of writing, the README can be improved further with all the stuff that was tested here, as it it only available as a NPM installation on the current readme

rpc

@molecula451
Copy link
Member

Code looks good and well structured, the package it's working well, there are maybe things to improve that will only become handful as the package it's use, mergin this, good stuff @Keyrxng

@molecula451 molecula451 merged commit d003d97 into ubiquity:web3-rpc-racer-npm-branch Mar 14, 2024
1 of 2 checks passed
molecula451 added a commit that referenced this pull request Mar 14, 2024
* feat: web3 rpc handler npm package

* feat: web3 rpc handler npm package

* chore: configurable, add kebab, remove dist

* chore: readme

* chore: add workflow scripts

* chore: remove build workflow

* Delete .github/workflows/scripts/kebabalize.sh

This is destructive and renames files so lets not use it.

* chore: improved race, tests and readme, removed ubq rpcs

* chore: script permission

* chore: type the constants remove bad else

* fix: type out path to src

* chore: type the promises, add ts config and jest workflow

* chore: final touch ups

* chore: export all from index

* fix: readme

* Update package.json

* Update README.md

---------

Co-authored-by: Keyrxng <106303466+Keyrxng@users.noreply.github.com>
Co-authored-by: アレクサンダー.eth <4975670+pavlovcik@users.noreply.github.com>
0x4007 pushed a commit that referenced this pull request Oct 10, 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

Successfully merging this pull request may close these issues.

Implement Dynamic RPC pick handler
5 participants