UNPKG

@unchainedshop/plugins

Version:

Official plugin collection for the Unchained Engine with payment, delivery, and pricing adapters

158 lines (157 loc) 8.33 kB
import { PaymentAdapter, PaymentError, OrderPricingSheet, } from '@unchainedshop/core'; import { createLogger } from '@unchainedshop/logger'; import {} from "./module.js"; import deriveBtcAddress from "./derive-btc-address.js"; import deriveEthAddress from "./derive-eth-address.js"; import denoteAmount from "./denote-amount.js"; const logger = createLogger('unchained:cryptopay'); const { CRYPTOPAY_SECRET, CRYPTOPAY_BTC_XPUB, CRYPTOPAY_ETH_XPUB } = process.env; const CryptopayCurrencies = { BTC: 'BTC', ETH: 'ETH', }; export const Cryptopay = { ...PaymentAdapter, key: 'shop.unchained.payment.cryptopay', label: 'Cryptopay', version: '1.0.0', typeSupported(type) { return type === 'GENERIC'; }, actions: (config, context) => { const { modules } = context; const setConversionRates = async (currencyCode, existingAddresses) => { const originCurrencyObj = await modules.currencies.findCurrency({ isoCode: currencyCode }); if (!originCurrencyObj?.isActive) throw new Error('Origin currency is not supported'); const updatedAddresses = await Promise.all(existingAddresses.map(async (addressData) => { const targetCurrencyObj = await modules.currencies.findCurrency({ isoCode: addressData.currencyCode, }); if (!targetCurrencyObj?.isActive) return null; const rateData = await modules.products.prices.rates.getRate(originCurrencyObj, targetCurrencyObj); return { ...addressData, currencyConversionRate: rateData?.rate, currencyConversionExpiryDate: rateData?.expiresAt, }; })); return updatedAddresses.filter(Boolean); }; const updateTransactionsWithOrderPaymentId = async (orderPaymentId, cryptoAddresses) => { for (const cryptoAddress of cryptoAddresses) { const { address, currencyCode } = cryptoAddress; await modules.cryptopay.mapOrderPaymentToWalletAddress({ address, currencyCode, orderPaymentId, }); } }; const adapterActions = { ...PaymentAdapter.actions(config, context), configurationError() { if (!CRYPTOPAY_SECRET) { return PaymentError.INCOMPLETE_CONFIGURATION; } if (!CRYPTOPAY_BTC_XPUB && !CRYPTOPAY_ETH_XPUB) { return PaymentError.INCOMPLETE_CONFIGURATION; } return null; }, isActive() { if (adapterActions.configurationError() !== null) return false; if (!context.order) return true; return true; }, isPayLaterAllowed() { return false; }, sign: async () => { const { orderPayment, order } = context; if (!orderPayment) throw new Error('Payment Credential Registration is not yet supported for Cryptopay'); if (!order) throw new Error('Order context is required for signing with Cryptopay'); const existingAddresses = await modules.cryptopay.getWalletAddressesByOrderPaymentId(orderPayment._id); if (existingAddresses.length) { const existingAddressesWithNewExpiration = await setConversionRates(order.currencyCode, existingAddresses.map(({ _id, currencyCode }) => ({ address: _id, currencyCode, }))); return JSON.stringify(existingAddressesWithNewExpiration); } const cryptoAddresses = []; if (CRYPTOPAY_BTC_XPUB) { const btcDerivationNumber = await modules.cryptopay.getNextDerivationNumber(CryptopayCurrencies.BTC); const address = deriveBtcAddress(CRYPTOPAY_BTC_XPUB, btcDerivationNumber); cryptoAddresses.push({ currencyCode: CryptopayCurrencies.BTC, address, }); } if (CRYPTOPAY_ETH_XPUB) { const ethDerivationNumber = await modules.cryptopay.getNextDerivationNumber(CryptopayCurrencies.ETH); const address = deriveEthAddress(CRYPTOPAY_ETH_XPUB, ethDerivationNumber); cryptoAddresses.push({ currencyCode: CryptopayCurrencies.ETH, address, }); } const cryptoAddressesWithExpiration = await setConversionRates(order.currencyCode, cryptoAddresses); await updateTransactionsWithOrderPaymentId(orderPayment._id, cryptoAddresses); return JSON.stringify(cryptoAddressesWithExpiration); }, charge: async () => { const { order, orderPayment } = context; if (!orderPayment) throw new Error('Order Payment context is required for charging with Cryptopay'); if (!order) throw new Error('Order context is required for charging with Cryptopay'); const foundWalletsWithBalances = await modules.cryptopay.getWalletAddressesByOrderPaymentId(orderPayment._id); const walletForOrderPayment = foundWalletsWithBalances.find((wallet) => BigInt(wallet.amount.toString()) > 0n); const pricing = OrderPricingSheet({ calculation: order.calculation, currencyCode: order.currencyCode, }); const totalAmount = BigInt(pricing?.total({ useNetPrice: false }).amount); if (walletForOrderPayment && walletForOrderPayment.currencyCode !== order.currencyCode) { const baseCurrency = await modules.currencies.findCurrency({ isoCode: walletForOrderPayment.currencyCode, }); if (!baseCurrency) throw new Error('Base currency not found'); const quoteCurrency = await modules.currencies.findCurrency({ isoCode: order.currencyCode }); if (!quoteCurrency) throw new Error('Quote currency not found'); const rate = await modules.products.prices.rates.getRateRange(baseCurrency, quoteCurrency, order.confirmed || new Date()); if (!rate) throw new Error('No conversion rate found'); const convertedAmount = denoteAmount(walletForOrderPayment.amount.toString(), walletForOrderPayment.decimals); const maxAmount = parseFloat(convertedAmount.toString()) * rate.max * 1.001; if (maxAmount && maxAmount >= totalAmount) { return { transactionId: walletForOrderPayment._id, }; } logger.info(`Cryptopay Plugin: Wallet ${walletForOrderPayment._id} balance too low (yet): ${maxAmount} < ${totalAmount}`); } if (walletForOrderPayment) { const convertedAmount = denoteAmount(walletForOrderPayment.amount.toString(), walletForOrderPayment.decimals); if (convertedAmount && convertedAmount >= totalAmount) { return { transactionId: walletForOrderPayment._id, }; } logger.info(`Cryptopay Plugin: Wallet ${walletForOrderPayment._id} balance too low (yet): ${convertedAmount} < ${totalAmount}`); } logger.info(`Cryptopay Plugin: No confirmed payments found for currency ${order.currencyCode} and addresses ${JSON.stringify(foundWalletsWithBalances)}`); return false; }, }; return adapterActions; }, };