UNPKG

@daimo/pay

Version:

Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.

400 lines (397 loc) 14.5 kB
import { ethereum, readDaimoPayOrderID, assert, debugJson, isNativeToken, assertNotNull, writeDaimoPayOrderID } from '@daimo/pay-common'; import { useWallet, useConnection } from '@solana/wallet-adapter-react'; import { VersionedTransaction } from '@solana/web3.js'; import { useState, useEffect, useCallback } from 'react'; import { getAddress, erc20Abi, isHex, hexToBytes } from 'viem'; import { useAccount, useEnsName, useSendTransaction, useWriteContract } from 'wagmi'; import { DEFAULT_USD_LIMIT } from '../constants/limits.js'; import { inferTopLevelFromArray, DEFAULT_TOP_OPTIONS_ORDER, TOP_LEVEL_PAYMENT_OPTIONS } from '../constants/paymentOptions.js'; import { ROUTES } from '../constants/routes.js'; import { detectPlatform } from '../utils/platform.js'; import { useDaimoPay } from './useDaimoPay.js'; import useIsMobile from './useIsMobile.js'; import { useOrderUsdLimits } from './useOrderUsdLimits.js'; import { usePaymentOptions } from './usePaymentOptions.js'; import { useUntronAvailability } from './useUntronAvailability.js'; function getTopLevelOptions(paymentOptions) { if (!paymentOptions || paymentOptions.length === 0) return []; const topLevelOptions = TOP_LEVEL_PAYMENT_OPTIONS; const isString = (opt) => typeof opt === "string"; const stringOptions = paymentOptions.filter(isString); const topLevel = stringOptions.filter( (opt) => topLevelOptions.includes(opt) ); const specific = stringOptions.filter( (opt) => !topLevelOptions.includes(opt) ); if (topLevel.length && specific.length) { throw new Error( `invalid paymentOptions: cannot mix top-level options ${JSON.stringify(topLevel)} with specific options ${JSON.stringify(specific)}. use either ["AllWallets", "AllExchanges", ...] or ["MiniPay", "Binance", ...], not both` ); } const flattened = paymentOptions.map( (opt) => Array.isArray(opt) ? inferTopLevelFromArray(opt) ?? "AllWallets" : opt ); return flattened.filter( (opt) => topLevelOptions.includes(opt) ); } function usePaymentState({ trpc, lockPayParams, setRoute, log, redirectReturnUrl }) { const pay = useDaimoPay(); const [platform, setPlatform] = useState(); useEffect(() => { setPlatform(detectPlatform(window.navigator.userAgent)); }, []); const { address: ethWalletAddress } = useAccount(); const { data: senderEnsName } = useEnsName({ chainId: ethereum.chainId, address: ethWalletAddress }); const { sendTransactionAsync } = useSendTransaction(); const { writeContractAsync } = useWriteContract(); const solanaWallet = useWallet(); const { connection } = useConnection(); const solanaPubKey = solanaWallet.publicKey?.toBase58(); const [buttonProps, setButtonProps] = useState(); const [currPayParams, setCurrPayParams] = useState(); const [paymentWaitingMessage, setPaymentWaitingMessage] = useState(); const [isDepositFlow, setIsDepositFlow] = useState(false); const orderId = pay.order?.createdAt != null ? pay.order.id : void 0; const { externalPaymentOptions, walletPaymentOptions, solanaPaymentOptions, depositAddressOptions } = usePaymentOptions({ trpc, appId: currPayParams?.appId, orderId, isDepositFlow, usdRequired: pay.order?.destFinalCallTokenAmount.usd, solanaPubKey, ethWalletAddress, platform, filterIds: buttonProps?.paymentOptions ?? pay.order?.metadata.payer?.paymentOptions, preferredChains: pay.order?.metadata.payer?.preferredChains, preferredTokens: pay.order?.metadata.payer?.preferredTokens, evmChains: pay.order?.metadata.payer?.evmChains, destChainId: pay.order?.destFinalCallTokenAmount.token.chainId, passthroughTokens: pay.order?.metadata.payer?.passthroughTokens, destAddress: pay.order?.destFinalCall.to, log }); const { available: untronAvailable } = useUntronAvailability({ trpc }); const chainOrderUsdLimits = useOrderUsdLimits({ trpc }); const [selectedExternalOption, setSelectedExternalOption] = useState(); const [selectedTokenOption, setSelectedTokenOption] = useState(); const [selectedSolanaTokenOption, setSelectedSolanaTokenOption] = useState(); const [selectedDepositAddressOption, setSelectedDepositAddressOption] = useState(); const [selectedWallet, setSelectedWallet] = useState(); const [selectedWalletDeepLink, setSelectedWalletDeepLink] = useState(); const getOrderUsdLimit = () => { if (pay.order == null || chainOrderUsdLimits.loading) { return DEFAULT_USD_LIMIT; } const destChainId = pay.order.destFinalCallTokenAmount.token.chainId; return destChainId in chainOrderUsdLimits.limits ? chainOrderUsdLimits.limits[destChainId] : DEFAULT_USD_LIMIT; }; const payWithToken = async (walletOption) => { assert( ethWalletAddress != null, `[PAY TOKEN] null ethWalletAddress when paying on ethereum` ); assert( pay.paymentState === "preview" || pay.paymentState === "unhydrated" || pay.paymentState === "payment_unpaid", `[PAY TOKEN] paymentState is ${pay.paymentState}, must be preview or unhydrated or payment_unpaid` ); let hydratedOrder; const { required, fees } = walletOption; const paymentAmount = BigInt(required.amount) + BigInt(fees.amount); if (pay.paymentState !== "payment_unpaid") { assert( required.token.token === fees.token.token, `[PAY TOKEN] required token ${debugJson(required)} does not match fees token ${debugJson(fees)}` ); const res = await pay.hydrateOrder(ethWalletAddress); hydratedOrder = res.order; log( `[PAY TOKEN] hydrated order: ${debugJson( hydratedOrder )}, paying ${paymentAmount} of token ${required.token.token}` ); } else { hydratedOrder = pay.order; } const paymentTxHash = await (async () => { const dest = walletOption.passthroughAddress ?? hydratedOrder.intentAddr; try { if (isNativeToken( required.token.chainId, getAddress(required.token.token) )) { return await sendTransactionAsync({ to: dest, value: paymentAmount }); } else { return await writeContractAsync({ abi: erc20Abi, address: getAddress(required.token.token), functionName: "transfer", args: [dest, paymentAmount] }); } } catch (e) { console.error(`[PAY TOKEN] error sending token: ${e}`); throw e; } })(); if (!isHex(paymentTxHash) || paymentTxHash.length !== 66) { log( `[PAY TOKEN] wallet bug detected. ignoring invalid payment txHash: ${paymentTxHash}` ); return { success: true }; } try { await pay.payEthSource({ paymentTxHash, sourceChainId: required.token.chainId, payerAddress: ethWalletAddress, sourceToken: getAddress(required.token.token), sourceAmount: paymentAmount }); return { txHash: paymentTxHash, success: true }; } catch { console.error( `[PAY TOKEN] could not verify payment tx on chain: ${paymentTxHash}` ); return { txHash: paymentTxHash, success: false }; } }; const payWithSolanaToken = async (inputToken) => { const payerPublicKey = solanaWallet.publicKey; assert( payerPublicKey != null, "[PAY SOLANA] null payerPublicKey when paying on solana" ); assert( pay.order?.id != null, "[PAY SOLANA] null orderId when paying on solana" ); assert( pay.paymentState === "preview" || pay.paymentState === "unhydrated" || pay.paymentState === "payment_unpaid", `[PAY SOLANA] paymentState is ${pay.paymentState}, must be preview or unhydrated or payment_unpaid` ); let hydratedOrder; if (pay.paymentState !== "payment_unpaid") { const res = await pay.hydrateOrder(); hydratedOrder = res.order; log( `[PAY SOLANA] Hydrated order: ${JSON.stringify( hydratedOrder )}, checking out with Solana ${inputToken}` ); } else { hydratedOrder = pay.order; } const paymentTxHash = await (async () => { try { const serializedTx = await trpc.getSolanaSwapAndBurnTx.query({ orderId: pay.order.id.toString(), userPublicKey: assertNotNull( payerPublicKey, "[PAY SOLANA] wallet.publicKey cannot be null" ).toString(), inputTokenMint: inputToken }); const tx = VersionedTransaction.deserialize(hexToBytes(serializedTx)); const txHash = await solanaWallet.sendTransaction(tx, connection); return txHash; } catch (e) { console.error(e); throw e; } })(); try { await pay.paySolanaSource({ paymentTxHash, sourceToken: inputToken }); return { txHash: paymentTxHash, success: true }; } catch { console.error( `[PAY SOLANA] could not verify payment tx on chain: ${paymentTxHash}` ); return { txHash: paymentTxHash, success: false }; } }; const payWithExternal = async (option) => { assert(pay.order != null, "[PAY EXTERNAL] order cannot be null"); assert(platform != null, "[PAY EXTERNAL] platform cannot be null"); const { order } = await pay.hydrateOrder(); const externalPaymentOptionData = await trpc.getExternalPaymentOptionData.query({ id: order.id.toString(), externalPaymentOption: option, platform, redirectReturnUrl }); assert( externalPaymentOptionData != null, "[PAY EXTERNAL] missing externalPaymentOptionData" ); log( `[PAY EXTERNAL] hydrated order: ${debugJson( order )}, checking out with external payment: ${option}` ); setPaymentWaitingMessage(externalPaymentOptionData.waitingMessage); return externalPaymentOptionData.url; }; const payWithDepositAddress = async (option) => { const { order } = await pay.hydrateOrder(); log( `[PAY DEPOSIT ADDRESS] hydrated order ${order.id} for ${order.usdValue} USD, checking out with deposit address: ${option}` ); const result = await trpc.getDepositAddressForOrder.query({ orderId: order.id.toString(), option }); return "error" in result ? null : result; }; const { isIOS, isAndroid } = useIsMobile(); const openInWalletBrowser = async (wallet, amountUsd) => { const paymentState = pay.paymentState; assert( paymentState === "preview" || paymentState === "unhydrated" || paymentState === "payment_unpaid", `[OPEN IN WALLET BROWSER] paymentState is ${paymentState}, must be preview or unhydrated or payment_unpaid` ); assert( wallet.getDaimoPayDeeplink != null, `openInWalletBrowser: missing deeplink for ${wallet.name}` ); if (pay.paymentState !== "payment_unpaid") { await pay.hydrateOrder(); } const payId = writeDaimoPayOrderID(pay.order.id); const platform2 = isIOS ? "ios" : isAndroid ? "android" : "other"; const deeplink = wallet.getDaimoPayDeeplink(payId, platform2); if (!isIOS) { window.open(deeplink, "_blank"); } setSelectedWallet(wallet); setSelectedWalletDeepLink(deeplink); setRoute(ROUTES.WAITING_WALLET, { amountUsd, payId, wallet_name: wallet.name }); }; const setChosenUsd = (usd) => { assert( pay.paymentState === "preview", "[SET CHOSEN USD] paymentState is not preview" ); pay.setChosenUsd(usd); }; const setPayId = useCallback( async (payId) => { if (payId == null) return; const id = readDaimoPayOrderID(payId).toString(); if (pay.order?.id && BigInt(id) == pay.order.id) { return; } pay.reset(); await pay.setPayId(payId); setIsDepositFlow(false); }, [pay] ); const setPayParams = async (payParams) => { if (lockPayParams) return; assert(payParams != null, "[SET PAY PARAMS] payParams cannot be null"); log("[SET PAY PARAMS] setting payParams", payParams); pay.reset(); setCurrPayParams(payParams); setIsDepositFlow(payParams.toUnits == null); await pay.createPreviewOrder(payParams); }; const generatePreviewOrder = async () => { pay.reset(); if (currPayParams == null) return; await pay.createPreviewOrder(currPayParams); }; const resetOrder = useCallback( async (payParams) => { const mergedPayParams = payParams != null && currPayParams != null ? { ...currPayParams, ...payParams } : currPayParams; pay.reset(); setSelectedExternalOption(void 0); setSelectedTokenOption(void 0); setSelectedSolanaTokenOption(void 0); setSelectedDepositAddressOption(void 0); setSelectedWallet(void 0); setSelectedWalletDeepLink(void 0); setPaymentWaitingMessage(void 0); if (mergedPayParams) { setCurrPayParams(mergedPayParams); setIsDepositFlow(mergedPayParams.toUnits == null); await pay.createPreviewOrder(mergedPayParams); } setRoute(ROUTES.SELECT_METHOD); }, [setRoute, pay, currPayParams] ); const [tokenMode, setTokenMode] = useState("evm"); const topOptionsOrder = (() => { const defaultOrder = DEFAULT_TOP_OPTIONS_ORDER; const paymentOptions = buttonProps?.paymentOptions ?? pay.order?.metadata.payer?.paymentOptions; const found = getTopLevelOptions(paymentOptions); return found.length ? found : defaultOrder; })(); return { buttonProps, setButtonProps, topOptionsOrder, setPayId, setPayParams, tokenMode, setTokenMode, generatePreviewOrder, isDepositFlow, paymentWaitingMessage, selectedExternalOption, selectedTokenOption, selectedSolanaTokenOption, externalPaymentOptions, selectedWallet, selectedWalletDeepLink, walletPaymentOptions, solanaPaymentOptions, depositAddressOptions, selectedDepositAddressOption, getOrderUsdLimit, resetOrder, setSelectedWallet, setSelectedWalletDeepLink, setPaymentWaitingMessage, setSelectedExternalOption, setSelectedTokenOption, setSelectedSolanaTokenOption, setSelectedDepositAddressOption, setChosenUsd, payWithToken, payWithExternal, payWithDepositAddress, payWithSolanaToken, openInWalletBrowser, senderEnsName: senderEnsName ?? void 0, untronAvailable }; } export { usePaymentState }; //# sourceMappingURL=usePaymentState.js.map