UNPKG

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