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

1450 refactor http client #1461

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { VechainSDKError } from '@vechain/sdk-errors';
import { expect } from 'chai';
import { ethers } from 'hardhat';

Expand Down Expand Up @@ -56,9 +55,12 @@ describe('VechainHelloWorldWithNonEmptyConstructor', function () {
fail('should not get here');
} catch (error) {
if (error instanceof Error) {
expect(error.message).to.equal(
`Error on request eth_sendTransaction: HardhatPluginError: Error on request eth_sendRawTransaction: Error: Method 'HttpClient.http()' failed.\n-Reason: 'Request failed with status 403 and message tx rejected: insufficient energy'\n-Parameters: \n\t{\n "method": "POST",\n "url": "https://testnet.vechain.org/transactions"\n}`
);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(
error.message.startsWith(
"Error on request eth_sendTransaction: HardhatPluginError: Error on request eth_sendRawTransaction: Error: Method 'HttpClient.http()' failed."
)
).to.be.true;
}
}
});
Expand Down
30 changes: 30 additions & 0 deletions docs/diagrams/architecture/http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
```mermaid
classDiagram
class SimpleHttpClient {
number DEFAULT_TIMEOUT$
number timeout;
}
class HttpClient {
<<interface>>
string baseUrl
Promise~unknown~ get(string path, HttpParams path)
Promise~unknown~ http(HttpMethod method, string path, HttpParams path)
Promise~unknown~ post(string path, HttpParams path)

}
class HttpMethod {
<<enum>>
GET$
POST$
}
class HttpParams {
<<interface>>
unknown body
Record~string, string~ headers
Record~string, string~ query
validateResponse(Record~string, string~ headers)
}
HttpMethod o-- HttpClient
HttpParams *-- HttpClient
HttpClient <|.. SimpleHttpClient
```
4 changes: 2 additions & 2 deletions docs/examples/thor-client/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HttpClient, TESTNET_URL, ThorClient } from '@vechain/sdk-network';
import { FetchHttpClient, TESTNET_URL, ThorClient } from '@vechain/sdk-network';
import { expect } from 'expect';

// START_SNIPPET: InitializingThorClientSnippet

// First way to initialize thor client
const httpClient = new HttpClient(TESTNET_URL);
const httpClient = new FetchHttpClient(TESTNET_URL);
const thorClient = new ThorClient(httpClient);

// Second way to initialize thor client
Expand Down
2 changes: 1 addition & 1 deletion docs/thor-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ To initialize a Thor client, there are two straightforward methods. The first in

```typescript { name=initialize, category=example }
// First way to initialize thor client
const httpClient = new HttpClient(TESTNET_URL);
const httpClient = new FetchHttpClient(TESTNET_URL);
const thorClient = new ThorClient(httpClient);

// Second way to initialize thor client
Expand Down
47 changes: 47 additions & 0 deletions packages/network/src/http/HttpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type HttpMethod } from './HttpMethod';
import { type HttpParams } from './HttpParams';

/**
* Interface representing an HTTP client.
*
* The HttpClient interface provides methods for making HTTP requests
*/
export interface HttpClient {
/**
* The base URL for the API requests.
* This endpoint serves as the root URL for constructing all subsequent API calls.
*/
baseURL: string;

/**
* Makes an HTTP GET request to the specified path with optional query parameters.
*
* @param {string} path - The endpoint path for the GET request.
* @param {HttpParams} [params] - Optional query parameters to include in the request.
* @return {Promise<unknown>} A promise that resolves to the response of the GET request.
*/
get: (path: string, params?: HttpParams) => Promise<unknown>;

/**
* Sends an HTTP request using the specified method, path, and optional parameters.
*
* @param {HttpMethod} method - The HTTP method to be used for the request (e.g., 'GET', 'POST').
* @param {string} path - The endpoint path for the HTTP request.
* @param {HttpParams} [params] - Optional parameters to include in the HTTP request.
* @returns {Promise<unknown>} A promise that resolves with the response of the HTTP request.
*/
http: (
method: HttpMethod,
path: string,
params?: HttpParams
) => Promise<unknown>;

/**
* Sends a POST request to the specified path with the given parameters.
*
* @param {string} path - The endpoint to which the POST request is sent.
* @param {HttpParams} [params] - Optional parameters to be included in the POST request body.
* @returns {Promise<unknown>} - A promise that resolves to the response of the POST request.
*/
post: (path: string, params?: HttpParams) => Promise<unknown>;
}
10 changes: 10 additions & 0 deletions packages/network/src/http/HttpMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Enumeration for HTTP methods.
*
* @property {string} GET - The GET method requests a representation of the specified resource.
* @property {string} POST - The POST method is used to submit data to be processed to a specified resource.
*/
export enum HttpMethod {
GET = 'GET',
POST = 'POST'
}
29 changes: 29 additions & 0 deletions packages/network/src/http/HttpParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Represents the parameters for making an HTTP request.
*
* This interface specifies options for configuring an HTTP request,
* including query parameters, request body, custom headers,
* and a function to validate response headers.
*/
export interface HttpParams {
/**
* The request body, which can be of any type.
*/
body?: unknown;

/**
* Custom headers to be included in the request.
*/
headers?: Record<string, string>;

/**
* Query parameters to include in the request.
*/
query?: Record<string, string>;

/**
* A callback function to validate response headers.
* @param headers - The response headers to validate.
*/
validateResponseHeader?: (headers: Record<string, string>) => void;
}
131 changes: 131 additions & 0 deletions packages/network/src/http/SimpleHttpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { HttpMethod } from './HttpMethod';
import { InvalidHTTPRequest } from '@vechain/sdk-errors';
import { type HttpClient } from './HttpClient';
import { type HttpParams } from './HttpParams';

/**
* This class implements the HttpClient interface using the Fetch API.
*
* The SimpleHttpClient allows making {@link HttpMethod} requests with timeout
* and base URL configuration.
*/
class SimpleHttpClient implements HttpClient {
/**
* Represent the default timeout duration for network requests in milliseconds.
*/
public static readonly DEFAULT_TIMEOUT = 30000;

/**
* Return the root URL for the API endpoints.
*/
public readonly baseURL: string;

/**
* Return the amount of time in milliseconds before a timeout occurs
* when requesting with HTTP methods.
*/
public readonly timeout: number;

/**
* Constructs an instance of SimpleHttpClient with the given base URL and timeout period.
*
* @param {string} baseURL - The base URL for the HTTP client.
* @param {number} [timeout=SimpleHttpClient.DEFAULT_TIMEOUT] - The timeout period for requests in milliseconds.
*/
constructor(
baseURL: string,
timeout: number = SimpleHttpClient.DEFAULT_TIMEOUT
) {
this.baseURL = baseURL;
this.timeout = timeout;
}

/**
* Sends an HTTP GET request to the specified path with optional query parameters.
*
* @param {string} path - The endpoint path to which the HTTP GET request is sent.
* @param {HttpParams} [params] - Optional query parameters to include in the request.
* @return {Promise<unknown>} A promise that resolves with the response of the GET request.
*/
public async get(path: string, params?: HttpParams): Promise<unknown> {
return await this.http(HttpMethod.GET, path, params);
}

/**
* Executes an HTTP request with the specified method, path, and optional parameters.
*
* @param {HttpMethod} method - The HTTP method to use for the request (e.g., GET, POST).
* @param {string} path - The URL path for the request.
* @param {HttpParams} [params] - Optional parameters for the request, including query parameters, headers, body, and response validation.
* @return {Promise<unknown>} A promise that resolves to the response of the HTTP request.
* @throws {InvalidHTTPRequest} Throws an error if the HTTP request fails.
*/
public async http(
method: HttpMethod,
path: string,
params?: HttpParams
): Promise<unknown> {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, this.timeout);
try {
const url = new URL(path, this.baseURL);
if (params?.query != null) {
Object.entries(params.query).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
const response = await fetch(url, {
method,
headers: params?.headers as HeadersInit,
body:
method !== HttpMethod.GET
? JSON.stringify(params?.body)
: undefined,
signal: controller.signal
});
if (response.ok) {
const responseHeaders = Object.fromEntries(
response.headers.entries()
);
if (
params?.validateResponseHeader != null &&
responseHeaders != null
) {
params.validateResponseHeader(responseHeaders);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return await response.json();
}
throw new Error(`HTTP ${response.status} ${response.statusText}`, {
cause: response
});
} catch (error) {
throw new InvalidHTTPRequest(
'HttpClient.http()',
(error as Error).message,
{
method,
url: `${this.baseURL}${path}`
},
error
);
} finally {
clearTimeout(timeoutId);
}
}

/**
* Makes an HTTP POST request to the specified path with optional parameters.
*
* @param {string} path - The endpoint to which the POST request is made.
* @param {HttpParams} [params] - An optional object containing query parameters or data to be sent with the request.
* @return {Promise<unknown>} A promise that resolves with the response from the server.
*/
public async post(path: string, params?: HttpParams): Promise<unknown> {
return await this.http(HttpMethod.POST, path, params);
}
}

export { SimpleHttpClient };
4 changes: 4 additions & 0 deletions packages/network/src/http/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './SimpleHttpClient';
export * from './HttpMethod';
export type * from './HttpClient';
export type * from './HttpParams';
1 change: 1 addition & 0 deletions packages/network/src/network.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './http';
export * from './provider';
export * from './signer';
export * from './thor-client';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
type JsonRpcRequest,
type JsonRpcResponse
} from './types';
import { HttpClient } from '../../../utils';
import { ThorClient } from '../../../thor-client';
import { type ProviderInternalWallet } from '../../helpers';
import { VeChainSDKLogger } from '@vechain/sdk-logging';
import { SimpleHttpClient } from '../../../http';

/**
* This class is a wrapper for the VeChainProvider that Hardhat uses.
Expand Down Expand Up @@ -60,7 +60,7 @@ class HardhatVeChainProvider extends VeChainProvider {
) {
// Initialize the provider with the network configuration.
super(
new ThorClient(new HttpClient(nodeUrl)),
new ThorClient(new SimpleHttpClient(nodeUrl)),
walletToUse,
enableDelegation
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type ResponseStorage
} from './types';
import { type ThorClient } from '../thor-client';
import { HttpMethod } from '../../http';

/**
* Represents detailed account information.
Expand Down Expand Up @@ -112,7 +113,7 @@ class AccountsModule {

return new AccountDetail(
(await this.thor.httpClient.http(
'GET',
HttpMethod.GET,
thorest.accounts.get.ACCOUNT_DETAIL(address),
{
query: buildQuery({ revision: options?.revision })
Expand Down Expand Up @@ -156,7 +157,7 @@ class AccountsModule {
}

const result = (await this.thor.httpClient.http(
'GET',
HttpMethod.GET,
thorest.accounts.get.ACCOUNT_BYTECODE(address),
{
query: buildQuery({ revision: options?.revision })
Expand Down Expand Up @@ -212,7 +213,7 @@ class AccountsModule {
}

const result = (await this.thor.httpClient.http(
'GET',
HttpMethod.GET,
thorest.accounts.get.STORAGE_AT(address, position),
{
query: buildQuery({ position, revision: options?.revision })
Expand Down
5 changes: 3 additions & 2 deletions packages/network/src/thor-client/blocks/blocks-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './types';
import { Revision, type TransactionClause } from '@vechain/sdk-core';
import { type ThorClient } from '../thor-client';
import { HttpMethod } from '../../http';

/** The `BlocksModule` class encapsulates functionality for interacting with blocks
* on the VeChainThor blockchain.
Expand Down Expand Up @@ -94,7 +95,7 @@ class BlocksModule {
);
}
return (await this.thor.httpClient.http(
'GET',
HttpMethod.GET,
thorest.blocks.get.BLOCK_DETAIL(revision)
)) as CompressedBlockDetail | null;
}
Expand Down Expand Up @@ -123,7 +124,7 @@ class BlocksModule {
}

return (await this.thor.httpClient.http(
'GET',
HttpMethod.GET,
thorest.blocks.get.BLOCK_DETAIL(revision),
{
query: buildQuery({ expanded: true })
Expand Down
Loading
Loading