diff --git a/__tests__/rpcChannel.test.ts b/__tests__/rpcChannel.test.ts index f82ee7ab5..ac535a543 100644 --- a/__tests__/rpcChannel.test.ts +++ b/__tests__/rpcChannel.test.ts @@ -1,18 +1,31 @@ -import { RPC07 } from '../src'; +import { RPC06, RPC07 } from '../src'; import { createBlockForDevnet, getTestProvider } from './config/fixtures'; import { initializeMatcher } from './config/schema'; -describe('RPC 0.7.0', () => { - const rpcProvider = getTestProvider(false); - const channel = rpcProvider.channel as RPC07.RpcChannel; +describe('RpcChannel', () => { + const { nodeUrl } = getTestProvider(false).channel; + const channel07 = new RPC07.RpcChannel({ nodeUrl }); initializeMatcher(expect); beforeAll(async () => { await createBlockForDevnet(); }); - test('getBlockWithReceipts', async () => { - const response = await channel.getBlockWithReceipts('latest'); - expect(response).toMatchSchemaRef('BlockWithTxReceipts'); + test('baseFetch override', async () => { + const baseFetch = jest.fn(); + const fetchChannel06 = new RPC06.RpcChannel({ nodeUrl, baseFetch }); + const fetchChannel07 = new RPC07.RpcChannel({ nodeUrl, baseFetch }); + (fetchChannel06.fetch as any)(); + expect(baseFetch).toHaveBeenCalledTimes(1); + baseFetch.mockClear(); + (fetchChannel07.fetch as any)(); + expect(baseFetch).toHaveBeenCalledTimes(1); + }); + + describe('RPC 0.7.0', () => { + test('getBlockWithReceipts', async () => { + const response = await channel07.getBlockWithReceipts('latest'); + expect(response).toMatchSchemaRef('BlockWithTxReceipts'); + }); }); }); diff --git a/__tests__/rpcProvider.test.ts b/__tests__/rpcProvider.test.ts index 6aa109f3d..7502a1a6a 100644 --- a/__tests__/rpcProvider.test.ts +++ b/__tests__/rpcProvider.test.ts @@ -46,6 +46,14 @@ describeIfRpc('RPCProvider', () => { await createBlockForDevnet(); }); + test('baseFetch override', async () => { + const { nodeUrl } = rpcProvider.channel; + const baseFetch = jest.fn(); + const fetchProvider = new RpcProvider({ nodeUrl, baseFetch }); + (fetchProvider.fetch as any)(); + expect(baseFetch.mock.calls.length).toBe(1); + }); + test('getChainId', async () => { const fetchSpy = jest.spyOn(rpcProvider.channel as any, 'fetchEndpoint'); (rpcProvider as any).chainId = undefined as unknown as StarknetChainId; diff --git a/package-lock.json b/package-lock.json index 96000893e..208edf9e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", "fetch-cookie": "^3.0.0", - "isomorphic-fetch": "^3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types": "^0.0.5", @@ -50,6 +49,7 @@ "fetch-intercept": "^2.4.0", "husky": "^9.0.11", "import-sort-style-module": "^6.0.0", + "isomorphic-fetch": "^3.0.0", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "jest-json-schema": "^6.1.0", @@ -10143,6 +10143,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" @@ -13077,6 +13078,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -13095,17 +13097,20 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -18993,7 +18998,8 @@ "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true }, "node_modules/whatwg-mimetype": { "version": "3.0.0", diff --git a/package.json b/package.json index 9b2f50624..b433f7b01 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "fetch-intercept": "^2.4.0", "husky": "^9.0.11", "import-sort-style-module": "^6.0.0", + "isomorphic-fetch": "^3.0.0", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "jest-json-schema": "^6.1.0", @@ -95,7 +96,6 @@ "@scure/starknet": "~1.0.0", "abi-wan-kanabi": "^2.2.2", "fetch-cookie": "^3.0.0", - "isomorphic-fetch": "^3.0.0", "lossless-json": "^4.0.1", "pako": "^2.0.4", "starknet-types": "^0.0.5", diff --git a/src/channel/rpc_0_6.ts b/src/channel/rpc_0_6.ts index a4c66ca00..42f5a5312 100644 --- a/src/channel/rpc_0_6.ts +++ b/src/channel/rpc_0_6.ts @@ -21,7 +21,7 @@ import { JRPC, RPCSPEC06 as RPC } from '../types/api'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; import { validateAndParseEthAddress } from '../utils/eth'; -import fetch from '../utils/fetchPonyfill'; +import fetch from '../utils/fetch'; import { getSelector, getSelectorFromName } from '../utils/hash'; import { stringify } from '../utils/json'; import { getHexStringArray, toHex, toStorageKey } from '../utils/num'; @@ -40,21 +40,31 @@ export class RpcChannel { public headers: object; - readonly retries: number; - public requestId: number; readonly blockIdentifier: BlockIdentifier; + readonly retries: number; + + readonly waitMode: boolean; // behave like web2 rpc and return when tx is processed + private chainId?: StarknetChainId; private specVersion?: string; - readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed + private baseFetch: NonNullable; constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, specVersion, waitMode } = - optionsOrProvider || {}; + const { + baseFetch, + blockIdentifier, + chainId, + headers, + nodeUrl, + retries, + specVersion, + waitMode, + } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); } else if (nodeUrl) { @@ -62,12 +72,14 @@ export class RpcChannel { } else { this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default); } - this.retries = retries || defaultOptions.retries; - this.headers = { ...defaultOptions.headers, ...headers }; - this.blockIdentifier = blockIdentifier || defaultOptions.blockIdentifier; + this.baseFetch = baseFetch ?? fetch; + this.blockIdentifier = blockIdentifier ?? defaultOptions.blockIdentifier; this.chainId = chainId; + this.headers = { ...defaultOptions.headers, ...headers }; + this.retries = retries ?? defaultOptions.retries; this.specVersion = specVersion; - this.waitMode = waitMode || false; + this.waitMode = waitMode ?? false; + this.requestId = 0; } @@ -82,7 +94,7 @@ export class RpcChannel { method, ...(params && { params }), }; - return fetch(this.nodeUrl, { + return this.baseFetch(this.nodeUrl, { method: 'POST', body: stringify(rpcRequestBody), headers: this.headers as Record, diff --git a/src/channel/rpc_0_7.ts b/src/channel/rpc_0_7.ts index 53e51aed2..e578daf88 100644 --- a/src/channel/rpc_0_7.ts +++ b/src/channel/rpc_0_7.ts @@ -21,7 +21,7 @@ import { JRPC, RPCSPEC07 as RPC } from '../types/api'; import { CallData } from '../utils/calldata'; import { isSierra } from '../utils/contract'; import { validateAndParseEthAddress } from '../utils/eth'; -import fetch from '../utils/fetchPonyfill'; +import fetch from '../utils/fetch'; import { getSelector, getSelectorFromName } from '../utils/hash'; import { stringify } from '../utils/json'; import { getHexStringArray, toHex, toStorageKey } from '../utils/num'; @@ -40,21 +40,31 @@ export class RpcChannel { public headers: object; - readonly retries: number; - public requestId: number; readonly blockIdentifier: BlockIdentifier; + readonly retries: number; + + readonly waitMode: boolean; // behave like web2 rpc and return when tx is processed + private chainId?: StarknetChainId; private specVersion?: string; - readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed + private baseFetch: NonNullable; constructor(optionsOrProvider?: RpcProviderOptions) { - const { nodeUrl, retries, headers, blockIdentifier, chainId, specVersion, waitMode } = - optionsOrProvider || {}; + const { + baseFetch, + blockIdentifier, + chainId, + headers, + nodeUrl, + retries, + specVersion, + waitMode, + } = optionsOrProvider || {}; if (Object.values(NetworkName).includes(nodeUrl as NetworkName)) { this.nodeUrl = getDefaultNodeUrl(nodeUrl as NetworkName, optionsOrProvider?.default); } else if (nodeUrl) { @@ -62,12 +72,14 @@ export class RpcChannel { } else { this.nodeUrl = getDefaultNodeUrl(undefined, optionsOrProvider?.default); } - this.retries = retries || defaultOptions.retries; - this.headers = { ...defaultOptions.headers, ...headers }; - this.blockIdentifier = blockIdentifier || defaultOptions.blockIdentifier; + this.baseFetch = baseFetch ?? fetch; + this.blockIdentifier = blockIdentifier ?? defaultOptions.blockIdentifier; this.chainId = chainId; + this.headers = { ...defaultOptions.headers, ...headers }; + this.retries = retries ?? defaultOptions.retries; this.specVersion = specVersion; - this.waitMode = waitMode || false; + this.waitMode = waitMode ?? false; + this.requestId = 0; } @@ -82,7 +94,7 @@ export class RpcChannel { method, ...(params && { params }), }; - return fetch(this.nodeUrl, { + return this.baseFetch(this.nodeUrl, { method: 'POST', body: stringify(rpcRequestBody), headers: this.headers as Record, diff --git a/src/types/provider/configuration.ts b/src/types/provider/configuration.ts index 71eaf534f..20b5a5f03 100644 --- a/src/types/provider/configuration.ts +++ b/src/types/provider/configuration.ts @@ -12,6 +12,7 @@ export type RpcProviderOptions = { specVersion?: string; default?: boolean; waitMode?: boolean; + baseFetch?: WindowOrWorkerGlobalScope['fetch']; feeMarginPercentage?: { l1BoundMaxAmount: number; l1BoundMaxPricePerUnit: number; diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 000000000..ab11882bb --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,15 @@ +// the ts-ignore suppresses an esm to cjs import error that is resolved with entry point resolution +// @ts-ignore +import makeFetchCookie from 'fetch-cookie'; +import { LibraryError } from '../provider/errors'; + +// use built-in fetch in browser if available +export default (typeof window !== 'undefined' && window.fetch) || + // use built-in fetch in node, react-native and service worker if available + (typeof global !== 'undefined' && makeFetchCookie(global.fetch)) || + // throw with instructions when no fetch is detected + ((() => { + throw new LibraryError( + "'fetch()' not detected, use the 'baseFetch' constructor parameter to set it" + ); + }) as WindowOrWorkerGlobalScope['fetch']); diff --git a/src/utils/fetchPonyfill.ts b/src/utils/fetchPonyfill.ts deleted file mode 100644 index 35db800e9..000000000 --- a/src/utils/fetchPonyfill.ts +++ /dev/null @@ -1,8 +0,0 @@ -// the ts-ignore suppresses an esm to cjs import error that is resolved with entry point resolution -// @ts-ignore -import makeFetchCookie from 'fetch-cookie'; -import isomorphicFetch from 'isomorphic-fetch'; - -export default (typeof window !== 'undefined' && window.fetch) || // use buildin fetch in browser if available - (typeof global !== 'undefined' && makeFetchCookie(global.fetch)) || // use buildin fetch in node, react-native and service worker if available - isomorphicFetch; // ponyfill fetch in node and browsers that don't have it