@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
JavaScript
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