@daimo/pay
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
269 lines (266 loc) • 8.98 kB
JavaScript
import { assert, readDaimoPayOrderID, getOrderDestChainId, DaimoPayOrderMode } from '@daimo/pay-common';
import { formatUnits, getAddress } from 'viem';
import { startPolling } from '../utils/polling.js';
const pollers = /* @__PURE__ */ new Map();
function stopPoller(key) {
pollers.get(key)?.();
pollers.delete(key);
}
function attachPaymentEffectHandlers(store, trpc, log) {
const unsubscribe = store.subscribe(({ prev, next, event }) => {
log(
`[EFFECT] processing effects for event ${event.type} on state transition ${prev.type} -> ${next.type}`
);
if (prev.type !== next.type) {
if (next.type === "payment_unpaid") {
pollFindPayments(store, trpc, next.order.id);
}
if (next.type === "payment_started") {
pollRefreshOrder(store, trpc, next.order.id);
}
if (["payment_completed", "payment_bounced", "error", "idle"].includes(
next.type
)) {
if ("order" in prev && prev.order) {
stopPoller(`${"find_source_payment" /* FIND_SOURCE_PAYMENT */}:${prev.order.id}`);
stopPoller(`${"refresh_order" /* REFRESH_ORDER */}:${prev.order.id}`);
}
}
}
switch (event.type) {
case "set_pay_params":
runSetPayParamsEffects(store, trpc, event);
break;
case "set_pay_id":
runSetPayIdEffects(store, trpc, event);
break;
case "hydrate_order": {
if (prev.type === "preview") {
runHydratePayParamsEffects(store, trpc, prev, event);
} else if (prev.type === "unhydrated") {
runHydratePayIdEffects(store, trpc, prev, event);
} else {
log(`[EFFECT] invalid event ${event.type} on state ${prev.type}`);
}
break;
}
case "pay_source": {
if (prev.type === "payment_unpaid") {
runPaySourceEffects(store, trpc, prev);
} else {
log(`[EFFECT] invalid event ${event.type} on state ${prev.type}`);
}
break;
}
case "pay_ethereum_source": {
if (prev.type === "payment_unpaid") {
runPayEthereumSourceEffects(store, trpc, prev, event);
} else {
log(`[EFFECT] invalid event ${event.type} on state ${prev.type}`);
}
break;
}
case "pay_solana_source": {
if (prev.type === "payment_unpaid") {
runPaySolanaSourceEffects(store, trpc, prev, event);
}
log(`[EFFECT] invalid event ${event.type} on state ${prev.type}`);
break;
}
}
});
const cleanup = () => {
unsubscribe();
pollers.forEach((_, key) => stopPoller(key));
log("[EFFECT] unsubscribed from payment store and stopped all pollers");
};
return cleanup;
}
async function pollFindPayments(store, trpc, orderId) {
const key = `${"find_source_payment" /* FIND_SOURCE_PAYMENT */}:${orderId}`;
const stopPolling = startPolling({
key,
intervalMs: 1e3,
pollFn: () => trpc.findOrderPayments.query({ orderId: orderId.toString() }),
onResult: (order) => {
const state = store.getState();
if (state.type !== "payment_unpaid") {
stopPolling();
return;
}
store.dispatch({ type: "order_refreshed", order });
},
onError: () => {
}
});
pollers.set(key, stopPolling);
}
async function pollRefreshOrder(store, trpc, orderId) {
const key = `${"refresh_order" /* REFRESH_ORDER */}:${orderId}`;
const stopPolling = startPolling({
key,
intervalMs: 300,
pollFn: () => trpc.getOrder.query({ id: orderId.toString() }),
onResult: (res) => {
const state = store.getState();
if (state.type !== "payment_started") {
stopPolling();
return;
}
const order = res.order;
store.dispatch({ type: "order_refreshed", order });
},
onError: () => {
}
});
pollers.set(key, stopPolling);
}
async function runSetPayParamsEffects(store, trpc, event) {
const payParams = event.payParams;
const toUnits = payParams.toUnits == null ? "0" : payParams.toUnits;
assert(payParams.appId.length > 0, "PayParams: appId required");
const isDepositFlow = payParams.toUnits == null;
assert(
!isDepositFlow || payParams.externalId == null,
"PayParams: externalId unsupported in deposit mode"
);
try {
const orderPreview = await trpc.previewOrder.query({
appId: payParams.appId,
toChain: payParams.toChain,
toToken: payParams.toToken,
toUnits,
toAddress: payParams.toAddress,
toCallData: payParams.toCallData,
isAmountEditable: payParams.toUnits == null,
metadata: {
intent: payParams.intent ?? "Pay",
items: [],
payer: {
paymentOptions: payParams.paymentOptions,
preferredChains: payParams.preferredChains,
preferredTokens: payParams.preferredTokens,
evmChains: payParams.evmChains,
passthroughTokens: payParams.passthroughTokens
}
},
externalId: payParams.externalId,
userMetadata: payParams.metadata,
refundAddress: payParams.refundAddress
});
store.dispatch({
type: "preview_generated",
// TODO: Properly type this and fix hacky type casting
order: orderPreview,
payParamsData: {
appId: payParams.appId
}
});
} catch (e) {
store.dispatch({ type: "error", order: void 0, message: e.message });
}
}
async function runSetPayIdEffects(store, trpc, event) {
try {
const { order } = await trpc.getOrder.query({
id: readDaimoPayOrderID(event.payId).toString()
});
store.dispatch({
type: "order_loaded",
order
});
} catch (e) {
store.dispatch({ type: "error", order: void 0, message: e.message });
}
}
async function runHydratePayParamsEffects(store, trpc, prev, event) {
const order = prev.order;
const toUnits = formatUnits(
BigInt(order.destFinalCallTokenAmount.amount),
order.destFinalCallTokenAmount.token.decimals
);
try {
const { hydratedOrder } = await trpc.createOrder.mutate({
appId: prev.payParamsData.appId,
paymentInput: {
id: order.id.toString(),
toChain: getOrderDestChainId(order),
toToken: getAddress(order.destFinalCallTokenAmount.token.token),
toUnits,
toAddress: getAddress(order.destFinalCall.to),
toCallData: order.destFinalCall.data,
isAmountEditable: order.mode === DaimoPayOrderMode.CHOOSE_AMOUNT,
metadata: order.metadata,
userMetadata: order.userMetadata,
externalId: order.externalId ?? void 0
},
// Prefer the refund address passed to this function, if specified. This
// is for cases where the user pays from an EOA. Otherwise, use the refund
// address specified by the dev.
refundAddress: event.refundAddress ?? prev.order.refundAddr ?? void 0
});
store.dispatch({
type: "order_hydrated",
order: hydratedOrder
});
} catch (e) {
store.dispatch({ type: "error", order: prev.order, message: e.message });
}
}
async function runHydratePayIdEffects(store, trpc, prev, event) {
const order = prev.order;
try {
const { hydratedOrder } = await trpc.hydrateOrder.query({
id: order.id.toString(),
refundAddress: event.refundAddress
});
store.dispatch({
type: "order_hydrated",
order: hydratedOrder
});
} catch (e) {
store.dispatch({ type: "error", order: prev.order, message: e.message });
}
}
async function runPaySourceEffects(store, trpc, prev) {
const orderId = prev.order.id;
try {
const order = await trpc.findOrderPayments.query({
orderId: orderId.toString()
});
store.dispatch({ type: "order_refreshed", order });
} catch (e) {
store.dispatch({ type: "error", order: prev.order, message: e.message });
}
}
async function runPayEthereumSourceEffects(store, trpc, prev, event) {
const orderId = prev.order.id;
try {
const order = await trpc.processSourcePayment.mutate({
orderId: orderId.toString(),
sourceInitiateTxHash: event.paymentTxHash,
sourceChainId: event.sourceChainId,
sourceFulfillerAddr: event.payerAddress,
sourceToken: event.sourceToken,
sourceAmount: event.sourceAmount.toString()
});
store.dispatch({ type: "payment_verified", order });
} catch (e) {
store.dispatch({ type: "error", order: prev.order, message: e.message });
}
}
async function runPaySolanaSourceEffects(store, trpc, prev, event) {
const orderId = prev.order.id;
try {
const order = await trpc.processSolanaSourcePayment.mutate({
orderId: orderId.toString(),
startIntentTxHash: event.paymentTxHash,
token: event.sourceToken
});
store.dispatch({ type: "payment_verified", order });
} catch (e) {
store.dispatch({ type: "error", order: prev.order, message: e.message });
}
}
export { attachPaymentEffectHandlers };
//# sourceMappingURL=paymentEffects.js.map