@unchainedshop/plugins
Version:
Official plugin collection for the Unchained Engine with payment, delivery, and pricing adapters
158 lines (157 loc) • 8.33 kB
JavaScript
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;
},
};