Repository: 0xs34n/starknet.js (ts)

Issue:
Make public Fetch request on Sequencer
Allow users to use starknetjs if missing features or changes to use direct fetch and deal with data themself

Pull request description:
feat: add public sequencer fetch method
## Motivation and Resolution

Close #459 


Patch:
diff --git a/src/provider/sequencer.ts b/src/provider/sequencer.ts
index 074bb7e6..1733db5d 100644
--- a/src/provider/sequencer.ts
+++ b/src/provider/sequencer.ts
@@ -45,10 +45,11 @@ import { GatewayError, HttpError, LibraryError } from './errors';
 import { ProviderInterface } from './interface';
 import { Block, BlockIdentifier } from './utils';
 
-type NetworkName = 'mainnet-alpha' | 'goerli-alpha' | 'goerli-alpha-2';
+export type NetworkName = 'mainnet-alpha' | 'goerli-alpha' | 'goerli-alpha-2';
+export type SequencerHttpMethod = 'POST' | 'GET';
 
 export type SequencerProviderOptions = {
-  headers?: object;
+  headers?: Record<string, string>;
   blockIdentifier?: BlockIdentifier;
 } & (
   | {
@@ -84,14 +85,14 @@ export class SequencerProvider implements ProviderInterface {
 
   public gatewayUrl: string;
 
-  public headers: object | undefined;
+  public headers?: Record<string, string>;
+
+  private blockIdentifier: BlockIdentifier;
 
   private chainId: StarknetChainId;
 
   private responseParser = new SequencerAPIResponseParser();
 
-  private blockIdentifier: BlockIdentifier;
-
   constructor(optionsOrProvider: SequencerProviderOptions = defaultOptions) {
     if ('network' in optionsOrProvider) {
       this.baseUrl = SequencerProvider.getNetworkFromName(optionsOrProvider.network);
@@ -177,7 +178,7 @@ export class SequencerProvider implements ProviderInterface {
     return `?${queryString}`;
   }
 
-  private getHeaders(method: 'POST' | 'GET'): object | undefined {
+  private getHeaders(method: SequencerHttpMethod): Record<string, string> | undefined {
     if (method === 'POST') {
       return {
         'Content-Type': 'application/json',
@@ -202,43 +203,52 @@ export class SequencerProvider implements ProviderInterface {
     const baseUrl = this.getFetchUrl(endpoint);
     const method = this.getFetchMethod(endpoint);
     const queryString = this.getQueryString(query);
-    const headers = this.getHeaders(method);
     const url = urljoin(baseUrl, endpoint, queryString);
 
+    return this.fetch(url, {
+      method,
+      body: request,
+    });
+  }
+
+  public async fetch(
+    endpoint: string,
+    options?: {
+      method?: SequencerHttpMethod;
+      body?: any;
+      parseAlwaysAsBigInt?: boolean;
+    }
+  ): Promise<any> {
+    const url = buildUrl(this.baseUrl, '', endpoint);
+    const method = options?.method ?? 'GET';
+    const headers = this.getHeaders(method);
+
     try {
-      const res = await fetch(url, {
+      const response = await fetch(url, {
         method,
-        body: stringify(request),
-        headers: headers as Record<string, string>,
+        body: stringify(options?.body),
+        headers,
       });
-      const textResponse = await res.text();
-      if (!res.ok) {
-        // This will allow user to handle contract errors
+      const textResponse = await response.text();
+
+      if (!response.ok) {
+        // This will allow the user to handle contract errors
         let responseBody: any;
         try {
           responseBody = parse(textResponse);
         } catch {
-          // if error parsing fails, return an http error
-          throw new HttpError(res.statusText, res.status);
+          throw new HttpError(response.statusText, response.status);
         }
-
-        const errorCode = responseBody.code || ((responseBody as any)?.status_code as string); // starknet-devnet uses status_code instead of code; They need to fix that
-        throw new GatewayError(responseBody.message, errorCode); // Caught locally, and re-thrown for the user
+        throw new GatewayError(responseBody.message, responseBody.code);
       }
 
-      if (endpoint === 'estimate_fee') {
-        return parseAlwaysAsBig(textResponse);
-      }
-      return parse(textResponse) as Sequencer.Endpoints[T]['RESPONSE'];
-    } catch (err) {
-      // rethrow custom errors
-      if (err instanceof GatewayError || err instanceof HttpError) {
-        throw err;
-      }
-      if (err instanceof Error) {
-        throw Error(`Could not ${method} from endpoint \`${url}\`: ${err.message}`);
-      }
-      throw err;
+      const parseChoice = options?.parseAlwaysAsBigInt ? parseAlwaysAsBig : parse;
+      return parseChoice(textResponse);
+    } catch (error) {
+      if (error instanceof Error && !(error instanceof LibraryError))
+        throw Error(`Could not ${method} from endpoint \`${url}\`: ${error.message}`);
+
+      throw error;
     }
   }
 


Test patch:
diff --git a/__tests__/sequencerProvider.test.ts b/__tests__/sequencerProvider.test.ts
index e9a97bbf..ebe26671 100644
--- a/__tests__/sequencerProvider.test.ts
+++ b/__tests__/sequencerProvider.test.ts
@@ -1,4 +1,6 @@
-import { Contract, Provider, SequencerProvider, stark } from '../src';
+import { Contract, GatewayError, HttpError, Provider, SequencerProvider, stark } from '../src';
+import * as fetchModule from '../src/utils/fetchPonyfill';
+import { stringify } from '../src/utils/json';
 import { toBigInt } from '../src/utils/number';
 import { encodeShortString } from '../src/utils/shortString';
 import {
@@ -14,10 +16,42 @@ import {
 describeIfSequencer('SequencerProvider', () => {
   const sequencerProvider = getTestProvider() as SequencerProvider;
   const account = getTestAccount(sequencerProvider);
-  let customSequencerProvider: Provider;
-  let exampleContractAddress: string;
+
+  describe('Generic fetch', () => {
+    const fetchSpy = jest.spyOn(fetchModule, 'default');
+    const generateMockResponse = (ok: boolean, text: any): any => ({
+      ok,
+      text: async () => text,
+    });
+
+    afterAll(() => {
+      fetchSpy.mockRestore();
+    });
+
+    test('fetch unexpected error', async () => {
+      fetchSpy.mockResolvedValueOnce(generateMockResponse(false, null));
+      expect(sequencerProvider.fetch('')).rejects.toThrow(/^Could not GET from endpoint/);
+    });
+
+    test('fetch http error', async () => {
+      fetchSpy.mockResolvedValueOnce(generateMockResponse(false, 'wrong'));
+      expect(sequencerProvider.fetch('')).rejects.toThrow(HttpError);
+    });
+
+    test('fetch gateway error', async () => {
+      fetchSpy.mockResolvedValueOnce(generateMockResponse(false, stringify({})));
+      expect(sequencerProvider.fetch('')).rejects.toThrow(GatewayError);
+    });
+
+    test('fetch success', async () => {
+      fetchSpy.mockResolvedValueOnce(generateMockResponse(true, stringify({ success: '' })));
+      expect(sequencerProvider.fetch('')).resolves.toHaveProperty('success');
+    });
+  });
 
   describe('Gateway specific methods', () => {
+    let exampleContractAddress: string;
+
     let exampleTransactionHash: string;
 
     beforeAll(async () => {
@@ -93,6 +127,7 @@ describeIfSequencer('SequencerProvider', () => {
   });
 
   describeIfDevnet('Test calls with Custom Devnet Sequencer Provider', () => {
+    let customSequencerProvider: Provider;
     let erc20: Contract;
     const wallet = stark.randomAddress();