From 47796e96b5deff67a4499b2732da5f17a86522ce Mon Sep 17 00:00:00 2001 From: Nik <2661899+CodeSchwert@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:37:22 +1300 Subject: [PATCH 1/4] disable publish workflow triggers (#2462) --- .github/workflows/publish.yaml | 58 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ae3ec4c145..0e5c8ca03e 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,34 +1,34 @@ name: Publish to NPM -on: - workflow_dispatch: - inputs: - release_type: - type: choice - description: Release Type - options: - - alpha - - release - required: true - default: alpha - upgrade_type: - type: choice - description: Upgrade Type - options: - - none - - patch - - minor - # - major - required: false - default: none - dry_run: - type: boolean - description: "(Optional) Dry run" - required: false - default: false - push: - branches: - - main +# on: +# workflow_dispatch: +# inputs: +# release_type: +# type: choice +# description: Release Type +# options: +# - alpha +# - release +# required: true +# default: alpha +# upgrade_type: +# type: choice +# description: Upgrade Type +# options: +# - none +# - patch +# - minor +# # - major +# required: false +# default: none +# dry_run: +# type: boolean +# description: "(Optional) Dry run" +# required: false +# default: false +# push: +# branches: +# - main env: RELEASE_TYPE: ${{ github.event.inputs.release_type || 'alpha' }} From 107de467b8ce2ddc47d04177f0bcdf9ae2aca58d Mon Sep 17 00:00:00 2001 From: zaidarain1 Date: Wed, 11 Dec 2024 10:45:29 +1100 Subject: [PATCH 2/4] chore: re-enable publish workflow triggers (#2463) --- .github/workflows/publish.yaml | 58 +++++++++++++++++----------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 0e5c8ca03e..ae3ec4c145 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,34 +1,34 @@ name: Publish to NPM -# on: -# workflow_dispatch: -# inputs: -# release_type: -# type: choice -# description: Release Type -# options: -# - alpha -# - release -# required: true -# default: alpha -# upgrade_type: -# type: choice -# description: Upgrade Type -# options: -# - none -# - patch -# - minor -# # - major -# required: false -# default: none -# dry_run: -# type: boolean -# description: "(Optional) Dry run" -# required: false -# default: false -# push: -# branches: -# - main +on: + workflow_dispatch: + inputs: + release_type: + type: choice + description: Release Type + options: + - alpha + - release + required: true + default: alpha + upgrade_type: + type: choice + description: Upgrade Type + options: + - none + - patch + - minor + # - major + required: false + default: none + dry_run: + type: boolean + description: "(Optional) Dry run" + required: false + default: false + push: + branches: + - main env: RELEASE_TYPE: ${{ github.event.inputs.release_type || 'alpha' }} From 3dc041a55610efffd3910086867b4861c5375016 Mon Sep 17 00:00:00 2001 From: Mimi Tran <80493680+mimi-imtbl@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:30:46 +1100 Subject: [PATCH 3/4] Check CDN availability before loading latest version (#2464) --- packages/checkout/sdk/src/widgets/version.ts | 35 ++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/checkout/sdk/src/widgets/version.ts b/packages/checkout/sdk/src/widgets/version.ts index 950c1374d2..0e16aab7e4 100644 --- a/packages/checkout/sdk/src/widgets/version.ts +++ b/packages/checkout/sdk/src/widgets/version.ts @@ -84,6 +84,33 @@ export async function getLatestVersionFromNpm(): Promise { } } +/** + * Checks if the provided version is available on the CDN. + * @param {string} version - The version to check. + * @returns {Promise} A promise resolving to a boolean indicating if the version is available on the CDN. + */ +async function isVersionAvailableOnCDN(version: string): Promise { + 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'. @@ -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; From 6e11807c80902a4dc1a3cb4263968db82af237fd Mon Sep 17 00:00:00 2001 From: Alejandro Loaiza Date: Wed, 11 Dec 2024 15:23:59 +1100 Subject: [PATCH 4/4] ID-3085 - Purchase add post hooks logic (#2454) --- .../widgets-lib/src/lib/hooks/useSignOrder.ts | 188 ++++++++++++++---- .../widgets-lib/src/lib/primary-sales.ts | 7 + .../src/lib/squid/functions/fetchBalances.ts | 15 +- .../src/lib/squid/functions/fetchChains.ts | 28 +-- .../src/lib/squid/hooks/useRoutes.ts | 10 + .../widgets/add-tokens/AddTokensWidget.tsx | 3 +- 6 files changed, 192 insertions(+), 59 deletions(-) diff --git a/packages/checkout/widgets-lib/src/lib/hooks/useSignOrder.ts b/packages/checkout/widgets-lib/src/lib/hooks/useSignOrder.ts index f7e4d22846..11b7ea6948 100644 --- a/packages/checkout/widgets-lib/src/lib/hooks/useSignOrder.ts +++ b/packages/checkout/widgets-lib/src/lib/hooks/useSignOrder.ts @@ -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, @@ -19,6 +21,7 @@ import { SignApiRequest, SignApiError, SignCurrencyFilter, + SquidPostHookCall, } from '../primary-sales'; import { filterAllowedTransactions, hexToText } from '../utils'; @@ -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, @@ -106,6 +110,9 @@ export const useSignOrder = (input: SignOrderInput) => { const [signResponse, setSignResponse] = useState( undefined, ); + const [, setPostHooks] = useState( + undefined, + ); const [executeResponse, setExecuteResponse] = useState({ done: false, transactions: [], @@ -196,6 +203,46 @@ export const useSignOrder = (input: SignOrderInput) => { [provider, waitFulfillmentSettlements], ); + const signAPI = useCallback(async ( + baseUrl: string, + data: SignApiRequest, + ): Promise => { + 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, @@ -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(); @@ -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, @@ -406,6 +522,7 @@ export const useSignOrder = (input: SignOrderInput) => { return { sign, + signWithPostHooks, signResponse, signError, filteredTransactions, @@ -414,5 +531,6 @@ export const useSignOrder = (input: SignOrderInput) => { executeResponse, tokenIds, executeNextTransaction, + getPostHooks, }; }; diff --git a/packages/checkout/widgets-lib/src/lib/primary-sales.ts b/packages/checkout/widgets-lib/src/lib/primary-sales.ts index 83c956e20e..75e637ff27 100644 --- a/packages/checkout/widgets-lib/src/lib/primary-sales.ts +++ b/packages/checkout/widgets-lib/src/lib/primary-sales.ts @@ -6,6 +6,7 @@ import { FundingItem, SmartCheckoutResult, } from '@imtbl/checkout-sdk'; +import { EvmContractCall, Hook } from '@0xsquid/squid-types'; export type SignedOrderProduct = { productId: string; @@ -27,6 +28,7 @@ export type SignedOrder = { }; totalAmount: number; products: SignedOrderProduct[]; + recipientAddress: string; }; export type SignedTransaction = { @@ -236,6 +238,7 @@ export type SignApiResponse = { currency_symbol: string; products: SignApiProduct[]; total_amount: string; + recipient_address: string; }; transactions: SignApiTransaction[]; }; @@ -264,3 +267,7 @@ export type SignApiError = { message: string; trace_id: string; }; + +export type SquidPostHook = Omit; + +export type SquidPostHookCall = EvmContractCall; diff --git a/packages/checkout/widgets-lib/src/lib/squid/functions/fetchBalances.ts b/packages/checkout/widgets-lib/src/lib/squid/functions/fetchBalances.ts index 2d900a6d91..cbe81d4575 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/functions/fetchBalances.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/functions/fetchBalances.ts @@ -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 ( @@ -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[] = []; 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'); }; diff --git a/packages/checkout/widgets-lib/src/lib/squid/functions/fetchChains.ts b/packages/checkout/widgets-lib/src/lib/squid/functions/fetchChains.ts index 35a2441cb6..13e1b8c4d1 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/functions/fetchChains.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/functions/fetchChains.ts @@ -1,4 +1,5 @@ import { Squid } from '@0xsquid/sdk'; +import { ChainType } from '@0xsquid/squid-types'; import { Chain } from '../types'; type SquidChain = { @@ -18,17 +19,18 @@ export type SquidNativeCurrency = { export const fetchChains = (squid: Squid): Chain[] => { const { chains } = squid; - - return chains.map((chain: SquidChain) => ({ - id: chain.chainId.toString(), - name: chain.networkName, - iconUrl: chain.chainIconURI, - type: chain.chainType, - nativeCurrency: { - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - decimals: chain.nativeCurrency.decimals, - iconUrl: chain.nativeCurrency.icon, - }, - })); + return chains + .filter((chain: SquidChain) => chain.chainType === ChainType.EVM) + .map((chain: SquidChain) => ({ + id: chain.chainId.toString(), + name: chain.networkName, + iconUrl: chain.chainIconURI, + type: chain.chainType, + nativeCurrency: { + name: chain.nativeCurrency.name, + symbol: chain.nativeCurrency.symbol, + decimals: chain.nativeCurrency.decimals, + iconUrl: chain.nativeCurrency.icon, + }, + })); }; diff --git a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts index 713c39ecb9..8d4b5ea81e 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts @@ -13,6 +13,7 @@ import { isPassportProvider } from '../../provider'; import { AmountData, RouteData, RouteResponseData, Token, } from '../types'; +import { SquidPostHook } from '../../primary-sales'; import { SQUID_NATIVE_TOKEN } from '../config'; const BASE_SLIPPAGE = 0.02; @@ -156,6 +157,7 @@ export const useRoutes = () => { fromAmount: string, fromAddress?: string, quoteOnly = true, + postHook?: SquidPostHook, ): Promise => { try { return await retry( @@ -170,6 +172,7 @@ export const useRoutes = () => { quoteOnly, enableBoost: true, receiveGasOnDestination: !isPassportProvider(toProvider), + postHook, }), { retryIntervalMs: 1000, @@ -219,6 +222,7 @@ export const useRoutes = () => { toAmount: string, fromAddress?: string, quoteOnly = true, + postHook?: SquidPostHook, ): Promise => { try { const routeResponse = await getRouteWithRetry( @@ -229,6 +233,7 @@ export const useRoutes = () => { fromAmount, fromAddress, quoteOnly, + postHook, ); if (!routeResponse?.route) { @@ -252,6 +257,7 @@ export const useRoutes = () => { newFromAmount, fromAddress, quoteOnly, + postHook, ); if (!newRoute?.route) { @@ -299,6 +305,7 @@ export const useRoutes = () => { toTokenAddress: string, balances: TokenBalance[], fromAmountArray: AmountData[], + postHook?: SquidPostHook, ): Promise => { const getGasCost = ( route: RouteResponseData, @@ -356,6 +363,9 @@ export const useRoutes = () => { toTokenAddress, data.fromAmount, data.toAmount, + undefined, + true, + postHook, ); if (!routeResponse?.route) return null; diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx index 6334ba7e5b..9d70e77abc 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/AddTokensWidget.tsx @@ -172,8 +172,7 @@ export default function AddTokensWidget({ (async () => { try { fetchingBalances.current = true; - const evmChains = chains.filter((chain) => chain.type === 'evm'); - const balances = await fetchBalances(squid, evmChains, fromProvider); + const balances = await fetchBalances(squid, chains, fromProvider); addTokensDispatch({ payload: {