Skip to content

Commit

Permalink
Merge branch 'main' into expose-check-in-with-different-client-id
Browse files Browse the repository at this point in the history
  • Loading branch information
shineli1984 authored Dec 11, 2024
2 parents 45409f8 + 6e11807 commit 860cde5
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 61 deletions.
35 changes: 33 additions & 2 deletions packages/checkout/sdk/src/widgets/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,33 @@ export async function getLatestVersionFromNpm(): Promise<string> {
}
}

/**
* Checks if the provided version is available on the CDN.
* @param {string} version - The version to check.
* @returns {Promise<boolean>} A promise resolving to a boolean indicating if the version is available on the CDN.
*/
async function isVersionAvailableOnCDN(version: string): Promise<boolean> {
const files = ['widgets-esm.js', 'widgets.js'];
const baseUrl = `https://cdn.jsdelivr.net/npm/@imtbl/sdk@${version}/dist/browser/checkout/`;

try {
const checks = files.map(async (file) => {
const response = await fetch(`${baseUrl}${file}`, { method: 'HEAD' });
if (!response.ok) {
return false;
}
return true;
});

const results = await Promise.all(checks);
const allFilesAvailable = results.every((isAvailable) => isAvailable);

return allFilesAvailable;
} catch {
return false;
}
}

/**
* Returns the latest compatible version based on the provided checkout version config.
* If no compatible version markers are provided, it returns 'latest'.
Expand Down Expand Up @@ -131,10 +158,14 @@ export async function determineWidgetsVersion(
versionConfig.compatibleVersionMarkers,
);

// If `latest` is returned, query NPM registry for the actual latest version
// If `latest` is returned, query NPM registry for the actual latest version and check if it's available on the CDN
if (compatibleVersion === 'latest') {
const latestVersion = await getLatestVersionFromNpm();
return latestVersion;
const isAvailable = await isVersionAvailableOnCDN(latestVersion);
if (isAvailable) {
return latestVersion;
}
return 'latest';
}

return compatibleVersion;
Expand Down
188 changes: 153 additions & 35 deletions packages/checkout/widgets-lib/src/lib/hooks/useSignOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { useCallback, useState } from 'react';
import { SaleItem } from '@imtbl/checkout-sdk';

import { ChainType, EvmContractCall, SquidCallType } from '@0xsquid/squid-types';
import { ethers } from 'ethers';
import {
SignResponse,
SignOrderInput,
Expand All @@ -19,6 +21,7 @@ import {
SignApiRequest,
SignApiError,
SignCurrencyFilter,
SquidPostHookCall,
} from '../primary-sales';
import { filterAllowedTransactions, hexToText } from '../utils';

Expand Down Expand Up @@ -72,6 +75,7 @@ const toSignResponse = (
return acc;
}, [] as SignedOrderProduct[]),
totalAmount: Number(order.total_amount),
recipientAddress: order.recipient_address,
},
transactions: transactions.map((transaction) => ({
tokenAddress: transaction.contract_address,
Expand Down Expand Up @@ -106,6 +110,9 @@ export const useSignOrder = (input: SignOrderInput) => {
const [signResponse, setSignResponse] = useState<SignResponse | undefined>(
undefined,
);
const [, setPostHooks] = useState<EvmContractCall[] | undefined>(
undefined,
);
const [executeResponse, setExecuteResponse] = useState<ExecuteOrderResponse>({
done: false,
transactions: [],
Expand Down Expand Up @@ -196,6 +203,46 @@ export const useSignOrder = (input: SignOrderInput) => {
[provider, waitFulfillmentSettlements],
);

const signAPI = useCallback(async (
baseUrl: string,
data: SignApiRequest,
): Promise<SignApiResponse> => {
const response = await fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

if (!response.ok) {
const { code } = (await response.json()) as SignApiError;
let errorType: SaleErrorTypes;
switch (response.status) {
case 400:
errorType = SaleErrorTypes.SERVICE_BREAKDOWN;
break;
case 404:
if (code === 'insufficient_stock') {
errorType = SaleErrorTypes.INSUFFICIENT_STOCK;
} else {
errorType = SaleErrorTypes.PRODUCT_NOT_FOUND;
}
break;
case 429:
case 500:
errorType = SaleErrorTypes.DEFAULT;
break;
default:
throw new Error('Unknown error');
}

throw new Error(errorType);
}

return response.json();
}, []);

const sign = useCallback(
async (
paymentType: SignPaymentTypes,
Expand All @@ -218,42 +265,8 @@ export const useSignOrder = (input: SignOrderInput) => {
};

const baseUrl = `${PRIMARY_SALES_API_BASE_URL[environment]}/${environmentId}/order/sign`;
const response = await fetch(baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

const { ok, status } = response;
if (!ok) {
const { code } = (await response.json()) as SignApiError;
let errorType: SaleErrorTypes;
switch (status) {
case 400:
errorType = SaleErrorTypes.SERVICE_BREAKDOWN;
break;
case 404:
if (code === 'insufficient_stock') {
errorType = SaleErrorTypes.INSUFFICIENT_STOCK;
} else {
errorType = SaleErrorTypes.PRODUCT_NOT_FOUND;
}
break;
case 429:
case 500:
errorType = SaleErrorTypes.DEFAULT;
break;
default:
throw new Error('Unknown error');
}

setSignError({ type: errorType });
return undefined;
}
const apiResponse = await signAPI(baseUrl, data);

const apiResponse: SignApiResponse = await response.json();
const apiTokenIds = apiResponse.order.products
.map((product) => product.detail.map(({ token_id }) => token_id))
.flat();
Expand All @@ -280,6 +293,109 @@ export const useSignOrder = (input: SignOrderInput) => {
[items, environmentId, environment, provider],
);

const getPostHooks = (signApiResponse: SignResponse): SquidPostHookCall[] => {
const approvalTxn = signApiResponse.transactions.find((txn) => txn.methodCall.startsWith('approve'));
const transferTxn = signApiResponse.transactions.find((txn) => txn.methodCall.startsWith('execute'));
const postHookCalls: SquidPostHookCall[] = [];

if (approvalTxn) {
postHookCalls.push({
chainType: ChainType.EVM,
callType: SquidCallType.FULL_TOKEN_BALANCE,
target: approvalTxn.tokenAddress,
value: '0',
callData: approvalTxn.rawData,
payload: {
tokenAddress: approvalTxn.tokenAddress,
inputPos: 1,
},
estimatedGas: approvalTxn.gasEstimate.toString(),
});
}

if (transferTxn) {
postHookCalls.push({
chainType: ChainType.EVM,
callType: SquidCallType.DEFAULT,
value: '0',
payload: {
tokenAddress: transferTxn.tokenAddress,
inputPos: 0,
},
target: transferTxn.tokenAddress,
callData: transferTxn.rawData,
estimatedGas: transferTxn.gasEstimate.toString(),
});
}

if (approvalTxn) {
const erc20Interface = new ethers.utils.Interface(['function transfer(address to, uint256 amount)']);
const transferPendingTokensTx = erc20Interface.encodeFunctionData(
'transfer',
[signApiResponse.order.recipientAddress, 0],
);

postHookCalls.push({
chainType: ChainType.EVM,
callType: SquidCallType.FULL_TOKEN_BALANCE,
target: approvalTxn.tokenAddress,
value: '0',
callData: transferPendingTokensTx,
payload: {
tokenAddress: approvalTxn.tokenAddress,
inputPos: 1,
},
estimatedGas: '50000',
});
}

return postHookCalls;
};

const signWithPostHooks = useCallback(
async (
paymentType: SignPaymentTypes,
fromTokenAddress: string,
): Promise<{ signResponse: SignResponse; postHooks: SquidPostHookCall[] } | undefined> => {
try {
const signer = provider?.getSigner();
const address = (await signer?.getAddress()) || '';

const data: SignApiRequest = {
recipient_address: address,
payment_type: paymentType,
currency_filter: SignCurrencyFilter.CONTRACT_ADDRESS,
currency_value: fromTokenAddress,
products: items.map((item) => ({
product_id: item.productId,
quantity: item.qty,
})),
custom_data: customOrderData,
};

const baseUrl = `${PRIMARY_SALES_API_BASE_URL[environment]}/${environmentId}/order/sign`;
const apiResponse = await signAPI(baseUrl, data);

const apiTokenIds = apiResponse.order.products
.map((product) => product.detail.map(({ token_id }) => token_id))
.flat();

const responseData = toSignResponse(apiResponse, items);
const squidPostHooks = getPostHooks(responseData);

setPostHooks(squidPostHooks);
setTokenIds(apiTokenIds);
setSignResponse(responseData);

return { signResponse: responseData, postHooks: squidPostHooks };
} catch (e: any) {
setSignError({ type: SaleErrorTypes.DEFAULT, data: { error: e } });
}
return undefined;
},
[items, environmentId, environment, provider, getPostHooks],
);

const executeTransaction = async (
transaction: SignedTransaction,
onTxnSuccess: (txn: ExecutedTransaction) => void,
Expand Down Expand Up @@ -406,6 +522,7 @@ export const useSignOrder = (input: SignOrderInput) => {

return {
sign,
signWithPostHooks,
signResponse,
signError,
filteredTransactions,
Expand All @@ -414,5 +531,6 @@ export const useSignOrder = (input: SignOrderInput) => {
executeResponse,
tokenIds,
executeNextTransaction,
getPostHooks,
};
};
7 changes: 7 additions & 0 deletions packages/checkout/widgets-lib/src/lib/primary-sales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
FundingItem,
SmartCheckoutResult,
} from '@imtbl/checkout-sdk';
import { EvmContractCall, Hook } from '@0xsquid/squid-types';

export type SignedOrderProduct = {
productId: string;
Expand All @@ -27,6 +28,7 @@ export type SignedOrder = {
};
totalAmount: number;
products: SignedOrderProduct[];
recipientAddress: string;
};

export type SignedTransaction = {
Expand Down Expand Up @@ -236,6 +238,7 @@ export type SignApiResponse = {
currency_symbol: string;
products: SignApiProduct[];
total_amount: string;
recipient_address: string;
};
transactions: SignApiTransaction[];
};
Expand Down Expand Up @@ -264,3 +267,7 @@ export type SignApiError = {
message: string;
trace_id: string;
};

export type SquidPostHook = Omit<Hook, 'fundAmount' | 'fundToken'>;

export type SquidPostHookCall = EvmContractCall;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Web3Provider } from '@ethersproject/providers';
import { Squid } from '@0xsquid/sdk';
import { CosmosBalance, TokenBalance } from '@0xsquid/sdk/dist/types';
import { TokenBalance } from '@0xsquid/sdk/dist/types';
import { Chain } from '../types';

export const fetchBalances = async (
Expand All @@ -11,21 +11,18 @@ export const fetchBalances = async (
const chainIds = chains.map((chain) => chain.id);
const address = await provider?.getSigner().getAddress();

const promises: Promise<{
cosmosBalances?: CosmosBalance[];
evmBalances?: TokenBalance[];
}>[] = [];
const promises: Promise<TokenBalance[]>[] = [];

for (const chainId of chainIds) {
const balancePromise = squid.getAllBalances({
chainIds: [chainId],
evmAddress: address,
const balancePromise = squid.getEvmBalances({
chains: [chainId],
userAddress: address,
});
promises.push(balancePromise);
}

const balances = await Promise.all(promises);
return balances
.flatMap((balance) => balance.evmBalances ?? [])
.flatMap((balance) => balance)
.filter((balance) => balance.balance !== '0');
};
Loading

0 comments on commit 860cde5

Please sign in to comment.