Skip to content

feat: remove WebSocket dependencies #1374

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

Merged
merged 3 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions __tests__/WebSocketChannel.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { WebSocket } from 'isows';

import { Provider, WSSubscriptions, WebSocketChannel } from '../src';
import { StarknetChainId } from '../src/global/constants';
import { getTestAccount, getTestProvider, STRKtokenAddress, TEST_WS_URL } from './config/fixtures';
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/batch.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fetch from '../../src/utils/fetch';
import fetch from '../../src/utils/connect/fetch';
import { BatchClient } from '../../src/utils/batch';
import { createBlockForDevnet, createTestProvider } from '../config/fixtures';
import { initializeMatcher } from '../config/schema';
Expand Down
4 changes: 0 additions & 4 deletions __tests__/utils/stark.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import * as json from '../../src/utils/json';

const { IS_BROWSER } = constants;

// isows has faulty module resolution in the browser emulation environment which prevents test execution
// it is not required for these tests so removing it with a mock circumvents the issue
jest.mock('isows', () => jest.fn());

test('isBrowser', () => {
expect(IS_BROWSER).toBe(true);
});
Expand Down
20 changes: 2 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,14 @@
"@scure/base": "1.2.1",
"@scure/starknet": "1.1.0",
"abi-wan-kanabi": "2.2.4",
"isows": "^1.0.6",
"lossless-json": "^4.0.1",
"pako": "^2.0.4",
"starknet-types-07": "npm:@starknet-io/types-js@~0.7.10",
"starknet-types-08": "npm:@starknet-io/types-js@~0.8.1",
"ts-mixer": "^6.0.3",
"ws": "^8.18.0"
"ts-mixer": "^6.0.3"
},
"engines": {
"node": ">=22"
},
"lint-staged": {
"*.ts": "eslint --cache --fix",
Expand Down
5 changes: 3 additions & 2 deletions src/channel/rpc_0_7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ import { BatchClient } from '../utils/batch';
import { CallData } from '../utils/calldata';
import { isSierra } from '../utils/contract';
import { validateAndParseEthAddress } from '../utils/eth';
import fetch from '../utils/fetch';
import fetch from '../utils/connect/fetch';
import { getSelector, getSelectorFromName } from '../utils/hash';
import { stringify } from '../utils/json';
import { getHexStringArray, toHex, toStorageKey } from '../utils/num';
import { Block, getDefaultNodeUrl, isV3Tx, wait } from '../utils/provider';
import { decompressProgram, signatureToHexArray } from '../utils/stark';
import { getVersionsByType } from '../utils/transaction';
import { logger } from '../global/logger';
import { config } from '../global/config';

const defaultOptions = {
headers: { 'Content-Type': 'application/json' },
Expand Down Expand Up @@ -88,7 +89,7 @@ export class RpcChannel {
} else {
this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default, '0.7');
}
this.baseFetch = baseFetch ?? fetch;
this.baseFetch = baseFetch || config.get('fetch') || fetch;
this.blockIdentifier = blockIdentifier ?? defaultOptions.blockIdentifier;
this.chainId = chainId;
this.headers = { ...defaultOptions.headers, ...headers };
Expand Down
5 changes: 3 additions & 2 deletions src/channel/rpc_0_8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { BatchClient } from '../utils/batch';
import { CallData } from '../utils/calldata';
import { isSierra } from '../utils/contract';
import { validateAndParseEthAddress } from '../utils/eth';
import fetch from '../utils/fetch';
import fetch from '../utils/connect/fetch';
import { getSelector, getSelectorFromName } from '../utils/hash';
import { stringify } from '../utils/json';
import {
Expand All @@ -42,6 +42,7 @@ import { decompressProgram, signatureToHexArray } from '../utils/stark';
import { getVersionsByType } from '../utils/transaction';
import { logger } from '../global/logger';
import { isRPC08_ResourceBounds } from '../provider/types/spec.type';
import { config } from '../global/config';
// TODO: check if we can filet type before entering to this method, as so to specify here only RPC 0.8 types

const defaultOptions = {
Expand Down Expand Up @@ -95,7 +96,7 @@ export class RpcChannel {
} else {
this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default, '0.8');
}
this.baseFetch = baseFetch ?? fetch;
this.baseFetch = baseFetch || config.get('fetch') || fetch;
this.blockIdentifier = blockIdentifier ?? defaultOptions.blockIdentifier;
this.chainId = chainId;
this.headers = { ...defaultOptions.headers, ...headers };
Expand Down
12 changes: 7 additions & 5 deletions src/channel/ws_0_8.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { WebSocket } from 'isows';
import type {
SUBSCRIPTION_ID,
SubscriptionEventsResponse,
Expand All @@ -13,9 +12,11 @@ import type {
import { BigNumberish, SubscriptionBlockIdentifier } from '../types';
import { JRPC } from '../types/api';
import { WebSocketEvent } from '../types/api/jsonrpc';
import WebSocket from '../utils/connect/ws';
import { stringify } from '../utils/json';
import { bigNumberishArrayToHexadecimalStringArray, toHex } from '../utils/num';
import { Block } from '../utils/provider';
import { config } from '../global/config';

export const WSSubscriptions = {
NEW_HEADS: 'newHeads',
Expand All @@ -32,9 +33,10 @@ export type WebSocketOptions = {
*/
nodeUrl?: string;
/**
* You can provide websocket object defined by protocol standard else library will use default 'isows'/'ws' package
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols .
* https://www.rfc-editor.org/rfc/rfc6455.html#section-1 .
* This parameter should be used when working in an environment without native WebSocket support by providing
* an equivalent WebSocket object that conforms to the protocol, e.g. from the 'isows' and/or 'ws' modules
* * https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols .
* * https://www.rfc-editor.org/rfc/rfc6455.html#section-1 .
* @default WebSocket
*/
websocket?: WebSocket;
Expand Down Expand Up @@ -183,7 +185,7 @@ export class WebSocketChannel {
// provided existing websocket
const nodeUrl = options.nodeUrl || 'http://localhost:3000 '; // TODO: implement getDefaultNodeUrl default node when defined by providers?
this.nodeUrl = options.websocket ? options.websocket.url : nodeUrl;
this.websocket = options.websocket ? options.websocket : new WebSocket(nodeUrl);
this.websocket = options.websocket || config.get('websocket') || new WebSocket(nodeUrl);

this.websocket.addEventListener('open', this.onOpen.bind(this));
this.websocket.addEventListener('close', this.onCloseProxy.bind(this));
Expand Down
4 changes: 4 additions & 0 deletions src/global/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const DEFAULT_GLOBAL_CONFIG: {
rpcVersion: _SupportedRpcVersion;
transactionVersion: SupportedTransactionVersion;
feeMarginPercentage: FeeMarginPercentage;
fetch: any;
websocket: any;
} = {
legacyMode: false,
rpcVersion: '0.8',
Expand All @@ -129,6 +131,8 @@ export const DEFAULT_GLOBAL_CONFIG: {
},
maxFee: 50,
},
fetch: undefined,
websocket: undefined,
};

export const RPC_DEFAULT_NODES = {
Expand Down
11 changes: 11 additions & 0 deletions src/utils/connect/fetch.ts

Choose a reason for hiding this comment

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

Has this been tested in browsers?

I get Failed to execute 'fetch' on 'Window': Illegal invocation

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems it's due to the missing .bind(window) as in:

export default (IS_BROWSER && window.fetch.bind(window)) || // use built-in fetch in browser if available

For @penovicp : do you plan on supporting non-fetch-supporting environments? Can we just get rid of these checks that add complexity? All modern environments support native fetch.

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 see why this is mandatory but will add window.fetch.bind(window)

Copy link
Contributor

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

@tabaktoni it's also needed on globalThis and global for the same reason 🙏

  (typeof globalThis !== 'undefined' && globalThis.fetch.bind(globalThis)) ||
  (typeof window !== 'undefined' && window.fetch.bind(window)) ||
  (typeof global !== 'undefined' && global.fetch.bind(global)) ||

and this first line should not exist as it will override all the others as fetch === globalThis.fetch === window.fetch === global.fetch, so this line would always win over the others and the binding won't happen:

(typeof fetch !== 'undefined' && fetch) ||

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LibraryError } from '../errors';

export default (typeof fetch !== 'undefined' && fetch) ||
(typeof globalThis !== 'undefined' && globalThis.fetch) ||
Comment on lines +3 to +4
Copy link
Contributor

@aryzing aryzing May 8, 2025

Choose a reason for hiding this comment

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

Suggested change
export default (typeof fetch !== 'undefined' && fetch) ||
(typeof globalThis !== 'undefined' && globalThis.fetch) ||
export default (typeof globalThis !== 'undefined' && globalThis.fetch.bind(globalThis)) ||

The first check is practically equivalent to the second. The second form is more desirable since it wouldn't make sense to bind to globalThis without first checking for it.

What works and what doesn't:

Works:

const myFetch = fetch;
await myFetch("https://example.com"); // ok

Does not work: Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation

const myObj = {
  myFetch,
  fetch,
};

myObj.myFetch("https://example.com"); // error
myObj.fetch("https://example.com"); // error

Why? the this value in this second case is myObj.

Why does this fail with starknet? The key areas of the library's compiled code look like this:

// src/utils/connect/fetch.ts
var fetch_default =
  (typeof fetch !== "undefined" && fetch) ||
  (typeof globalThis !== "undefined" && globalThis.fetch) ||
  (typeof window !== "undefined" && window.fetch.bind(window)) ||
  (typeof global !== "undefined" && global.fetch) ||
  (() => {
    throw new LibraryError(
      "'fetch()' not detected, use the 'baseFetch' constructor parameter to set it"
    );
  });

and

this.baseFetch = baseFetch || config.get("fetch") || fetch_default;

with a random invocation looking like,

this.baseFetch(this.nodeUrl, {
  method: "POST",
  body: stringify2(rpcRequestBody),
  headers: this.headers,
});

Above, this is RpcChannel, not window or globalThis, so the call fails.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, but why are you suggesting removing support for node.js fetch?

Choose a reason for hiding this comment

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

@tabaktoni plz see this comment on why the global fetch without a context should be removed. As per the comment, the bind also needs to be applied to global and globalThis or they will break in the same way that window broke.

Copy link
Contributor

Choose a reason for hiding this comment

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

@tabaktoni, the changes suggested in the comment above don't remove support for Node.js fetch. If applied, as also suggested by @victorkirov above, the final result would look like

  (typeof globalThis !== 'undefined' && globalThis.fetch.bind(globalThis)) ||
  (typeof window !== 'undefined' && window.fetch.bind(window)) ||
  (typeof global !== 'undefined' && global.fetch.bind(global)) ||
  • globalThis --> all modern clients
  • window --> legacy browsers
  • global --> legacy node.js

(typeof window !== 'undefined' && window.fetch.bind(window)) ||
(typeof global !== 'undefined' && global.fetch) ||
((() => {
throw new LibraryError(
"'fetch()' not detected, use the 'baseFetch' constructor parameter to set it"
);
}) as WindowOrWorkerGlobalScope['fetch']);
13 changes: 13 additions & 0 deletions src/utils/connect/ws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { LibraryError } from '../errors';

export default (typeof WebSocket !== 'undefined' && WebSocket) ||
(typeof globalThis !== 'undefined' && globalThis.WebSocket) ||
(typeof window !== 'undefined' && window.WebSocket.bind(window)) ||
(typeof global !== 'undefined' && global.WebSocket) ||
(class {
constructor() {
throw new LibraryError(
"WebSocket module not detected, use the 'websocket' constructor parameter to set a compatible connection"
);
}
} as unknown as typeof WebSocket);
12 changes: 0 additions & 12 deletions src/utils/fetch.ts

This file was deleted.

10 changes: 10 additions & 0 deletions www/docs/guides/websocket_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ await webSocketChannel.waitForConnection();
// ... use webSocketChannel
```

If the environment doesn't have a detectable global `WebSocket`, an appropriate `WebSocket` implementation should be used and set with the `websocket` constructor parameter.

```typescript
import { WebSocket } from 'ws';

const webSocketChannel = new WebSocketChannel({
websocket: new WebSocket('wss://sepolia-pathfinder-rpc.server.io/rpc/v0_8'),
});
```

### Usage

```typescript
Expand Down