Skip to content
Merged
25 changes: 12 additions & 13 deletions packages/checkout/sdk/src/balances/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,19 @@ export const getBalances = async (
});

const balanceResults = await Promise.allSettled(allBalancePromises);
const balances = (balanceResults.filter(
(result) => result.status === 'fulfilled',
) as PromiseFulfilledResult<GetBalanceResult>[]
).map((result) => {
const resp = result;
const { token } = resp.value;
// For some reason isNativeToken always returns undefined.
// We have spent way too much time figuring out why this is happening.
// That we have given up -- keep it as it is for now.
if (!token.address || isMatchingAddress(token.address, NATIVE)) resp.value.token.address = NATIVE;
return resp.value;
});

return { balances };
return balanceResults.reduce((acc, resp) => {
if (resp.status !== 'fulfilled' || resp.value.balance === 0n) return acc;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The change here is to filter out any zero balances. When fetching balances from the RPC it returns all tokens, even zero balance ones. So we need to filter these out to maintain the same behaviour as when we use blockscout.


const { value: result } = resp;

if (!result.token.address || isMatchingAddress(result.token.address, NATIVE)) {
result.token.address = NATIVE;
}

acc.balances.push(result);
return acc;
}, { balances: new Array<GetBalanceResult>() });
};

const getTokenBalances = async (
Expand Down
52 changes: 38 additions & 14 deletions packages/checkout/sdk/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Environment } from '@imtbl/config';
import { CheckoutModuleConfiguration, ChainId, NetworkMap } from '../types';
import {
CheckoutModuleConfiguration, ChainId, NetworkMap, ChainSlug,
} from '../types';
import { RemoteConfigFetcher } from './remoteConfigFetcher';
import {
CHECKOUT_CDN_BASE_URL,
DEFAULT_BRIDGE_ENABLED,
DEFAULT_ON_RAMP_ENABLED,
DEFAULT_SWAP_ENABLED,
DEV_CHAIN_ID_NETWORK_MAP,
ENV_DEVELOPMENT,
globalPackageVersion,
IMMUTABLE_API_BASE_URL,
PRODUCTION_CHAIN_ID_NETWORK_MAP,
SANDBOX_CHAIN_ID_NETWORK_MAP,
} from '../env';
Expand All @@ -28,12 +33,18 @@ const networkMap = (prod: boolean, dev: boolean) => {
return SANDBOX_CHAIN_ID_NETWORK_MAP;
};

// **************************************************** //
// This is duplicated in the widget-lib project. //
// We are not exposing these functions given that this //
// to keep the Checkout SDK interface as minimal as //
// possible. //
// **************************************************** //
const getBaseUrl = (prod: boolean, dev: boolean) => {
if (dev) return IMMUTABLE_API_BASE_URL[ENV_DEVELOPMENT];
if (prod) return IMMUTABLE_API_BASE_URL[Environment.PRODUCTION];
return IMMUTABLE_API_BASE_URL[Environment.SANDBOX];
};

const getChainSlug = (prod: boolean, dev: boolean) => {
if (dev) return ChainSlug.IMTBL_ZKEVM_DEVNET;
if (prod) return ChainSlug.IMTBL_ZKEVM_MAINNET;
return ChainSlug.IMTBL_ZKEVM_TESTNET;
};

export const getL1ChainId = (config: CheckoutConfiguration): ChainId => {
// DevMode and Sandbox will both use Sepolia.
if (!config.isProduction) return ChainId.SEPOLIA;
Expand All @@ -45,8 +56,12 @@ export const getL2ChainId = (config: CheckoutConfiguration): ChainId => {
if (config.isProduction) return ChainId.IMTBL_ZKEVM_MAINNET;
return ChainId.IMTBL_ZKEVM_TESTNET;
};
// **************************************************** //
// **************************************************** //

const getRemoteConfigEndpoint = (prod: boolean, dev: boolean) => {
if (dev) return CHECKOUT_CDN_BASE_URL[ENV_DEVELOPMENT];
if (prod) return CHECKOUT_CDN_BASE_URL[Environment.PRODUCTION];
return CHECKOUT_CDN_BASE_URL[Environment.SANDBOX];
};

export class CheckoutConfiguration {
// This is a hidden feature that is only available
Expand All @@ -73,6 +88,10 @@ export class CheckoutConfiguration {

readonly publishableKey: string;

readonly l1ChainId: ChainId;

readonly l2ChainId: ChainId;

readonly overrides: CheckoutModuleConfiguration['overrides'];

constructor(config: CheckoutModuleConfiguration, httpClient: HttpClient) {
Expand All @@ -91,18 +110,23 @@ export class CheckoutConfiguration {
this.isBridgeEnabled = config.bridge?.enable ?? DEFAULT_BRIDGE_ENABLED;
this.publishableKey = config.publishableKey ?? '<no-publishable-key>';

this.networkMap = networkMap(this.isProduction, this.isDevelopment);
this.networkMap = config.overrides?.networkMap ?? networkMap(this.isProduction, this.isDevelopment);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These changes are mostly about reading from the overrides


const remoteConfigEndpoint = config.overrides?.remoteConfigEndpoint
?? getRemoteConfigEndpoint(this.isProduction, this.isDevelopment);

this.remote = new RemoteConfigFetcher(httpClient, {
isDevelopment: this.isDevelopment,
isProduction: this.isProduction,
remoteConfigEndpoint,
});

this.tokens = new TokensFetcher(httpClient, this.remote, {
isDevelopment: this.isDevelopment,
isProduction: this.isProduction,
baseUrl: config.overrides?.baseUrl ?? getBaseUrl(this.isProduction, this.isDevelopment),
chainSlug: config.overrides?.chainSlug ?? getChainSlug(this.isProduction, this.isDevelopment),
});

this.l1ChainId = getL1ChainId(this);
this.l2ChainId = config.overrides?.l2ChainId ?? getL2ChainId(this);

this.overrides = config.overrides ?? {};
}

Expand Down
147 changes: 68 additions & 79 deletions packages/checkout/sdk/src/config/remoteConfigFetcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Environment } from '@imtbl/config';
import { AxiosResponse } from 'axios';
import { ChainId } from '../types';
import { RemoteConfigFetcher } from './remoteConfigFetcher';
import {
CHECKOUT_CDN_BASE_URL,
ENV_DEVELOPMENT,
} from '../env';
import { HttpClient } from '../api/http';

jest.mock('../api/http');
Expand All @@ -22,31 +17,51 @@ describe('RemoteConfig', () => {
mockedHttpClient = new HttpClient() as jest.Mocked<HttpClient>;
});

[Environment.PRODUCTION, Environment.SANDBOX, ENV_DEVELOPMENT].forEach((env) => {
describe('config', () => {
it(`should fetch configs and cache them [${env}]`, async () => {
const mockResponse = {
status: 200,
data: {
connect: {
walletConnect: false,
},
dex: {
overrides: {
rpcURL: 'https://test.com',
},
describe('config', () => {
it('should fetch configs and cache them', async () => {
const mockResponse = {
status: 200,
data: {
connect: {
walletConnect: false,
},
dex: {
overrides: {
rpcURL: 'https://test.com',
},
allowedNetworks: [ChainId.SEPOLIA],
},
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValueOnce(mockResponse);
allowedNetworks: [ChainId.SEPOLIA],
},
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValueOnce(mockResponse);

const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
remoteConfigEndpoint: 'configUrl',
});

expect(await fetcher.getConfig()).toEqual({
connect: {
walletConnect: false,
},
dex: {
overrides: {
rpcURL: 'https://test.com',
},
},
allowedNetworks: [ChainId.SEPOLIA],
});

const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
isDevelopment: env === ENV_DEVELOPMENT,
isProduction: env !== ENV_DEVELOPMENT && env === Environment.PRODUCTION,
});
expect(mockedHttpClient.get).toHaveBeenCalledTimes(1);
expect(mockedHttpClient.get).toHaveBeenNthCalledWith(
1,
`configUrl/${version}/config`,
);
});

expect(await fetcher.getConfig()).toEqual({
it('should fetch config for key', async () => {
const mockResponse = {
status: 200,
data: {
connect: {
walletConnect: false,
},
Expand All @@ -56,70 +71,44 @@ describe('RemoteConfig', () => {
},
},
allowedNetworks: [ChainId.SEPOLIA],
});
},
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValueOnce(mockResponse);

expect(mockedHttpClient.get).toHaveBeenCalledTimes(1);
expect(mockedHttpClient.get).toHaveBeenNthCalledWith(
1,
`${CHECKOUT_CDN_BASE_URL[env]}/${version}/config`,
);
const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
remoteConfigEndpoint: 'abc',
});

it(`should fetch config for key [${env}]`, async () => {
const mockResponse = {
status: 200,
data: {
connect: {
walletConnect: false,
},
dex: {
overrides: {
rpcURL: 'https://test.com',
},
},
allowedNetworks: [ChainId.SEPOLIA],
},
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValueOnce(mockResponse);
expect(await fetcher.getConfig('allowedNetworks')).toEqual([ChainId.SEPOLIA]);
});

const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
isDevelopment: env === ENV_DEVELOPMENT,
isProduction: env !== ENV_DEVELOPMENT && env === Environment.PRODUCTION,
});
it('should return undefined if missing config', async () => {
const mockResponse = {
status: 200,
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValueOnce(mockResponse);

expect(await fetcher.getConfig('allowedNetworks')).toEqual([ChainId.SEPOLIA]);
const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
remoteConfigEndpoint: 'abc',
});

it(`should return undefined if missing config [${env}]`, async () => {
const mockResponse = {
status: 200,
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValueOnce(mockResponse);
expect(await fetcher.getConfig()).toBeUndefined();
});

const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
isDevelopment: env === ENV_DEVELOPMENT,
isProduction: env !== ENV_DEVELOPMENT && env === Environment.PRODUCTION,
});
it('should throw error when configuration is invalid JSON', async () => {
const mockInvalidJSONResponse = {
status: 200,
data: 'invalid json',
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValue(mockInvalidJSONResponse);

expect(await fetcher.getConfig()).toBeUndefined();
const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
remoteConfigEndpoint: 'abc',
});

it('should throw error when configuration is invalid JSON', async () => {
const mockInvalidJSONResponse = {
status: 200,
data: 'invalid json',
} as AxiosResponse;
mockedHttpClient.get.mockResolvedValue(mockInvalidJSONResponse);

const fetcher = new RemoteConfigFetcher(mockedHttpClient, {
isDevelopment: env === ENV_DEVELOPMENT,
isProduction: env !== ENV_DEVELOPMENT && env === Environment.PRODUCTION,
});

await expect(fetcher.getConfig()).rejects.toThrowError(
new Error('Invalid configuration'),
);
});
await expect(fetcher.getConfig()).rejects.toThrowError(
new Error('Invalid configuration'),
);
});
});
});
20 changes: 4 additions & 16 deletions packages/checkout/sdk/src/config/remoteConfigFetcher.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import { Environment } from '@imtbl/config';
import { AxiosResponse } from 'axios';
import { RemoteConfiguration } from '../types';
import { CHECKOUT_CDN_BASE_URL, ENV_DEVELOPMENT } from '../env';
import { HttpClient } from '../api/http';
import { CheckoutError, CheckoutErrorType } from '../errors';

export type RemoteConfigParams = {
isDevelopment: boolean;
isProduction: boolean;
remoteConfigEndpoint: string;
};

export class RemoteConfigFetcher {
private httpClient: HttpClient;

private isDevelopment: boolean;

private isProduction: boolean;
private endpoint: string;

private configCache: RemoteConfiguration | undefined;

private version: string = 'v1';

constructor(httpClient: HttpClient, params: RemoteConfigParams) {
this.isDevelopment = params.isDevelopment;
this.isProduction = params.isProduction;
this.endpoint = params.remoteConfigEndpoint;
this.httpClient = httpClient;
}

private getEndpoint = () => {
if (this.isDevelopment) return CHECKOUT_CDN_BASE_URL[ENV_DEVELOPMENT];
if (this.isProduction) return CHECKOUT_CDN_BASE_URL[Environment.PRODUCTION];
return CHECKOUT_CDN_BASE_URL[Environment.SANDBOX];
};

// eslint-disable-next-line class-methods-use-this
private parseResponse<T>(response: AxiosResponse<any, any>): T {
let responseData: T = response.data;
Expand All @@ -56,7 +44,7 @@ export class RemoteConfigFetcher {
let response: AxiosResponse;
try {
response = await this.httpClient.get(
`${this.getEndpoint()}/${this.version}/config`,
`${this.endpoint}/${this.version}/config`,
);
} catch (err: any) {
throw new CheckoutError(
Expand Down
Loading