Skip to content

Commit

Permalink
[WT-1686] Add on-ramp widget (#770)
Browse files Browse the repository at this point in the history
Co-authored-by: Zach Couchman <zachary.couchman@immutable.com>
Co-authored-by: Sharon Sheah <sharonsheah@gmail.com>
Co-authored-by: Mikhala <mikhala.kurtjak@immutable.com>
  • Loading branch information
4 people authored Sep 13, 2023
1 parent e0c7e16 commit cd62b88
Show file tree
Hide file tree
Showing 68 changed files with 3,751 additions and 330 deletions.
192 changes: 192 additions & 0 deletions packages/checkout/sdk/src/Checkout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { ExternalProvider, Web3Provider } from '@ethersproject/providers';
import { Environment } from '@imtbl/config';
import { BigNumber, ethers } from 'ethers';
import { Passport, UserProfile } from '@imtbl/passport';
import { getNetworkAllowList, getNetworkInfo, switchWalletNetwork } from './network';

import { Checkout } from './Checkout';
Expand All @@ -22,6 +23,9 @@ import {
ItemType,
TransactionOrGasType,
SmartCheckoutParams,
NetworkInfo,
GetTokenAllowListResult,
TokenInfo,
} from './types';
import { getAllBalances, getBalance, getERC20Balance } from './balances';
import { sendTransaction } from './transaction';
Expand All @@ -36,6 +40,8 @@ import { buy } from './smartCheckout/buy';
import { sell } from './smartCheckout/sell';
import { smartCheckout } from './smartCheckout';
import { cancel } from './smartCheckout/cancel';
import { FiatRampService } from './fiatRamp';
import { FiatRampParams, ExchangeType } from './types/fiatRamp';

jest.mock('./connect');
jest.mock('./network');
Expand All @@ -50,6 +56,7 @@ jest.mock('./smartCheckout/buy');
jest.mock('./smartCheckout/sell');
jest.mock('./smartCheckout/cancel');
jest.mock('./smartCheckout');
jest.mock('./fiatRamp');

describe('Connect', () => {
let providerMock: ExternalProvider;
Expand Down Expand Up @@ -629,4 +636,189 @@ describe('Connect', () => {
const result = await Checkout.isWeb3Provider(new Web3Provider(providerMock, ChainId.ETHEREUM));
expect(result).toBeTruthy();
});

describe('createFiatRampUrl', () => {
let createWidgetUrlMock: jest.Mock;
let checkout: Checkout;
let mockProvider: Web3Provider;
let networkInfoResult: NetworkInfo;
let getTokenAllowListResult: GetTokenAllowListResult;

const defaultWidgetUrl = 'https://global-stg.transak.com?apiKey=41ad2da7-ed5a-4d89-a90b-c751865effc2'
+ '&network=immutablezkevm&defaultPaymentMethod=credit_debit_card&disablePaymentMethods='
+ 'sepa_bank_transfer,gbp_bank_transfer,pm_cash_app,pm_jwire,pm_paymaya,pm_bpi,pm_ubp,pm_grabpay,'
+ 'pm_shopeepay,pm_gcash,pm_pix,pm_astropay,pm_pse,inr_bank_transfer&productsAvailed=buy'
+ '&exchangeScreenTitle=Buy&themeColor=0D0D0D';

beforeEach(() => {
createWidgetUrlMock = jest.fn().mockResolvedValue(defaultWidgetUrl);
(FiatRampService as jest.Mock).mockReturnValue({
createWidgetUrl: createWidgetUrlMock,
});

mockProvider = {
getSigner: jest.fn().mockReturnValue({
getAddress: jest.fn().mockResolvedValue('0xADDRESS'),
}),
network: {
chainId: ChainId.ETHEREUM,
},
} as unknown as Web3Provider;

networkInfoResult = {
name: ChainName.ETHEREUM,
chainId: ChainId.ETHEREUM,
nativeCurrency: {
name: 'ETHEREUM',
symbol: 'ETH',
decimals: 18,
},
isSupported: true,
};
(getNetworkInfo as jest.Mock).mockResolvedValue(networkInfoResult);

getTokenAllowListResult = {
tokens: [],
};
(getTokenAllowList as jest.Mock).mockResolvedValue(getTokenAllowListResult);

checkout = new Checkout({
baseConfig: { environment: Environment.PRODUCTION },
});
});

it(`should call FiatRampService.createWidgetUrl with correct params
when only onRampProvider, exchangeType and web3Provider are provided`, async () => {
const params: FiatRampParams = {
exchangeType: ExchangeType.ONRAMP,
web3Provider: mockProvider,
};

await checkout.createFiatRampUrl(params);

expect(createWidgetUrlMock).toBeCalledTimes(1);
expect(createWidgetUrlMock).toBeCalledWith({
exchangeType: ExchangeType.ONRAMP,
isPassport: false,
walletAddress: '0xADDRESS',
tokenAmount: undefined,
tokenSymbol: 'IMX',
email: undefined,
});
});

it(`should call fiatRampService.createWidgetUrl with correct params
when tokenAmount and tokenSymbol are provided`, async () => {
getTokenAllowListResult = {
tokens: [
{
name: 'Immutable X',
address: '0xaddr',
symbol: 'IMX',
decimals: 18,
} as TokenInfo,
{
name: 'Ethereum',
address: '0xethAddr',
symbol: 'ETH',
decimals: 18,
} as TokenInfo,
{
name: 'Matic',
address: '0xmaticAddr',
symbol: 'MATIC',
decimals: '18',
},
],
} as GetTokenAllowListResult;
(getTokenAllowList as jest.Mock).mockResolvedValue(getTokenAllowListResult);

const params: FiatRampParams = {
exchangeType: ExchangeType.ONRAMP,
web3Provider: mockProvider,
tokenAmount: '10',
tokenAddress: '0xethAddr',
};

await checkout.createFiatRampUrl(params);

expect(createWidgetUrlMock).toBeCalledTimes(1);
expect(createWidgetUrlMock).toBeCalledWith({
exchangeType: ExchangeType.ONRAMP,
isPassport: false,
walletAddress: '0xADDRESS',
tokenAmount: '10',
tokenSymbol: 'ETH',
email: undefined,
});
});

it(`should call fiatRampService.createWidgetUrl with correct params
when passport is provided`, async () => {
mockProvider = {
getSigner: jest.fn().mockReturnValue({
getAddress: jest.fn().mockResolvedValue('0xADDRESS'),
}),
network: {
chainId: ChainId.IMTBL_ZKEVM_TESTNET,
},
provider: {
isPassport: true,
},
} as unknown as Web3Provider;
const mockUser: UserProfile = {
sub: 'email|123',
email: 'passport.user@immutable.com',
};
const mockPassport = {
getUserInfo: jest.fn().mockResolvedValue(mockUser),
} as unknown as Passport;

const params: FiatRampParams = {
exchangeType: ExchangeType.ONRAMP,
web3Provider: mockProvider,
passport: mockPassport,
};

await checkout.createFiatRampUrl(params);

expect(createWidgetUrlMock).toBeCalledTimes(1);
expect(createWidgetUrlMock).toBeCalledWith({
exchangeType: ExchangeType.ONRAMP,
isPassport: true,
walletAddress: '0xADDRESS',
tokenAmount: undefined,
tokenSymbol: 'IMX',
email: mockUser.email,
});
});
});

describe('getExchangeFeeEstimate', () => {
let feeEstimateMock: jest.Mock;
let checkout: Checkout;

const feeEstimate = {
minPercentage: '3.5',
maxPercentage: '5.5',
feePercentage: undefined,
};

beforeEach(() => {
feeEstimateMock = jest.fn().mockResolvedValue(feeEstimate);
(FiatRampService as jest.Mock).mockReturnValue({
feeEstimate: feeEstimateMock,
});

checkout = new Checkout({
baseConfig: { environment: Environment.PRODUCTION },
});
});

it('should call fiatRampService.getExchangeFeeEstimate', async () => {
await checkout.getExchangeFeeEstimate();

expect(feeEstimateMock).toBeCalledTimes(1);
});
});
});
50 changes: 50 additions & 0 deletions packages/checkout/sdk/src/Checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ import {
GasEstimateSwapResult,
GasEstimateBridgeToL2Result,
SmartCheckoutParams,
TokenFilterTypes,
OnRampProviderFees,
FiatRampParams,
} from './types';
import { CheckoutConfiguration } from './config';
import { createReadOnlyProviders } from './readOnlyProviders/readOnlyProvider';
import { SellParams } from './types/sell';
import { CancelParams } from './types/cancel';
import { FiatRampService, FiatRampWidgetParams } from './fiatRamp';

const SANDBOX_CONFIGURATION = {
baseConfig: {
Expand All @@ -60,6 +64,8 @@ const SANDBOX_CONFIGURATION = {
export class Checkout {
readonly config: CheckoutConfiguration;

readonly fiatRampService: FiatRampService;

private readOnlyProviders: Map<ChainId, ethers.providers.JsonRpcProvider>;

/**
Expand All @@ -70,6 +76,7 @@ export class Checkout {
config: CheckoutModuleConfiguration = SANDBOX_CONFIGURATION,
) {
this.config = new CheckoutConfiguration(config);
this.fiatRampService = new FiatRampService(this.config);
this.readOnlyProviders = new Map<ChainId, ethers.providers.JsonRpcProvider>();
}

Expand Down Expand Up @@ -397,4 +404,47 @@ export class Checkout {
this.config,
);
}

/**
* Creates and returns a URL for the fiat ramp widget.
* @param {FiatRampParams} params - The parameters for creating the url.
* @returns {Promise<string>} - A promise that resolves to a string url.
*/
public async createFiatRampUrl(params: FiatRampParams): Promise<string> {
let tokenAmount;
let tokenSymbol = 'IMX';
let email;

const walletAddress = await params.web3Provider.getSigner().getAddress();
const isPassport = (params.web3Provider.provider as any)?.isPassport || false;

if (isPassport && params.passport) {
const userInfo = await params.passport.getUserInfo();
email = userInfo?.email;
}

const tokenList = await tokens.getTokenAllowList(this.config, { type: TokenFilterTypes.ONRAMP });
const token = tokenList.tokens.find((t) => t.address?.toLowerCase() === params.tokenAddress?.toLowerCase());
if (token) {
tokenAmount = params.tokenAmount;
tokenSymbol = token.symbol;
}

return await this.fiatRampService.createWidgetUrl({
exchangeType: params.exchangeType,
isPassport,
walletAddress,
tokenAmount,
tokenSymbol,
email,
} as FiatRampWidgetParams);
}

/**
* Fetches fiat ramp fee estimations.
* @returns {Promise<OnRampProviderFees>} - A promise that resolves to OnRampProviderFees.
*/
public async getExchangeFeeEstimate(): Promise<OnRampProviderFees> {
return await this.fiatRampService.feeEstimate();
}
}
Loading

0 comments on commit cd62b88

Please sign in to comment.