UNPKG

@daimo/pay

Version:

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

377 lines (374 loc) 13.7 kB
import { ethereum, getOrderDestChainId, isCCTPV1Chain, 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 { ROUTES } from '../constants/routes.js'; import { detectPlatform } from '../utils/platform.js'; import { useDaimoPay } from './useDaimoPay.js'; import { useDepositAddressOptions } from './useDepositAddressOptions.js'; import { useExternalPaymentOptions } from './useExternalPaymentOptions.js'; import useIsMobile from './useIsMobile.js'; import { useOrderUsdLimits } from './useOrderUsdLimits.js'; import { useSolanaPaymentOptions } from './useSolanaPaymentOptions.js'; import { useUntronAvailability } from './useUntronAvailability.js'; import { useWalletPaymentOptions } from './useWalletPaymentOptions.js'; 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 destChainId = pay.order == null ? null : getOrderDestChainId(pay.order); const showSolanaPaymentMethod = destChainId != null && isCCTPV1Chain(destChainId); const [buttonProps, setButtonProps] = useState(); const [currPayParams, setCurrPayParams] = useState(); const [paymentWaitingMessage, setPaymentWaitingMessage] = useState(); const [isDepositFlow, setIsDepositFlow] = useState(false); const externalPaymentOptions = useExternalPaymentOptions({ trpc, // allow <DaimoPayButton payId={...} paymentOptions={override} /> filterIds: buttonProps?.paymentOptions ?? pay.order?.metadata.payer?.paymentOptions, platform, usdRequired: pay.order?.destFinalCallTokenAmount.usd, mode: pay.order?.mode }); const walletPaymentOptions = useWalletPaymentOptions({ trpc, address: ethWalletAddress, usdRequired: pay.order?.destFinalCallTokenAmount.usd, destChainId: pay.order?.destFinalCallTokenAmount.token.chainId, destAddress: pay.order?.destFinalCall.to, preferredChains: pay.order?.metadata.payer?.preferredChains, preferredTokens: pay.order?.metadata.payer?.preferredTokens, evmChains: pay.order?.metadata.payer?.evmChains, passthroughTokens: pay.order?.metadata.payer?.passthroughTokens, isDepositFlow, log }); const solanaPaymentOptions = useSolanaPaymentOptions({ trpc, address: solanaPubKey, usdRequired: pay.order?.destFinalCallTokenAmount.usd, isDepositFlow, showSolanaPaymentMethod }); const depositAddressOptions = useDepositAddressOptions({ trpc, usdRequired: pay.order?.destFinalCallTokenAmount.usd, mode: pay.order?.mode }); 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 = () => { const DEFAULT_USD_LIMIT = 2e4; if (pay.order == null || chainOrderUsdLimits.loading) { return DEFAULT_USD_LIMIT; } const destChainId2 = pay.order.destFinalCallTokenAmount.token.chainId; return destChainId2 in chainOrderUsdLimits.limits ? chainOrderUsdLimits.limits[destChainId2] : 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(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 } = useIsMobile(); const openInWalletBrowser = (wallet, amountUsd) => { const paymentState = pay.paymentState; assert( paymentState === "payment_unpaid", `[OPEN IN WALLET BROWSER] paymentState is ${paymentState}, must be payment_unpaid` ); assert( wallet.getDaimoPayDeeplink != null, `openInWalletBrowser: missing deeplink for ${wallet.name}` ); const payId = writeDaimoPayOrderID(pay.order.id); const deeplink = wallet.getDaimoPayDeeplink(payId); 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 (lockPayParams || payId == null) return; const id = readDaimoPayOrderID(payId).toString(); if (pay.order?.id && BigInt(id) == pay.order.id) { return; } pay.reset(); pay.setPayId(payId); setIsDepositFlow(false); }, [lockPayParams, 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"); return { buttonProps, setButtonProps, setPayId, setPayParams, tokenMode, setTokenMode, generatePreviewOrder, isDepositFlow, paymentWaitingMessage, selectedExternalOption, selectedTokenOption, selectedSolanaTokenOption, externalPaymentOptions, showSolanaPaymentMethod, 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