@smartinvoicexyz/utils
Version:
Unified source for utility functions used across the Smart Invoice protocol.
242 lines (241 loc) • 9.16 kB
JavaScript
import { logDebug } from '@smartinvoicexyz/shared';
import _ from 'lodash';
import { formatUnits } from 'viem';
import { getDateString, parseToDate } from './date';
import { getResolverFee, getResolverInfo, getResolverInfoByAddress, resolverFeeLabel, } from './helpers';
import { chainByName } from './web3';
const totalDeposited = (invoice) => {
const { deposits } = _.pick(invoice, ['deposits']);
if (!deposits)
return undefined;
return deposits.reduce((sum, d) => {
return sum + BigInt(d.amount.toString());
}, BigInt(0));
};
const depositedMilestones = (invoice) => {
const { amounts } = _.pick(invoice, ['amounts']);
let sum = BigInt(0);
return _.map(amounts, (a) => {
sum += BigInt(a);
const amount = totalDeposited(invoice);
if (!amount)
return false;
return amount >= sum;
});
};
const depositedMilestonesString = (invoice) => {
const { amounts } = _.pick(invoice, ['amounts']);
let sum = BigInt(0);
return _.map(amounts, (a) => {
const prevAmount = sum;
sum += BigInt(a);
const deposited = totalDeposited(invoice);
if (deposited && deposited >= sum) {
return 'deposited';
}
if (deposited && deposited > prevAmount) {
return 'partially deposited';
}
return undefined;
});
};
// [2, 2, 2]
// [4, 2]
// assuming possibly multiple milestones paid in one deposit,
// but not catching amounts paid over multiple deposits
const findDepositForAmount = (amount, // total amount to with milestones up to the current amount
deposits) => {
if (!deposits)
return undefined;
const depositsReversed = _.reverse([...deposits]);
let sum = 0n;
return depositsReversed.find((deposit, i) => {
sum += deposit.amount;
return sum >= amount || !depositsReversed[i + 1];
});
};
/**
* Match amounts to deposits
* @param invoice
* @param tokenBalance
* @returns array of deposits
*/
const assignDeposits = (invoice) => {
const { amounts, deposits } = _.pick(invoice, ['amounts', 'deposits']);
if (!amounts || !deposits)
return [];
const deposited = deposits.reduce((sum, d) => {
return sum + d.amount;
}, 0n);
let sum = 0n;
return _.map(amounts, (a) => {
if (sum >= deposited) {
return undefined;
}
sum += a;
const deposit = findDepositForAmount(sum, deposits);
return deposit;
});
};
const assignReleases = (invoice) => {
const { amounts, releases } = _.pick(invoice, ['amounts', 'releases']);
const releasedTxs = amounts?.map((_a, i) => {
const release = releases?.find(r => BigInt(r.milestone) === BigInt(i));
return release;
}) ?? [];
return releasedTxs;
};
const totalAmount = (invoice) => {
const { amounts } = _.pick(invoice, ['amounts']);
return _.reduce(amounts, (sum, a) => {
return sum + BigInt(a);
}, BigInt(0));
};
const totalDue = (invoice) => {
const localTotalDeposited = totalDeposited(invoice);
const total = totalAmount(invoice);
if (!localTotalDeposited || !total)
return undefined;
return localTotalDeposited > total
? BigInt(0)
: BigInt(total) - localTotalDeposited;
};
const isInvoiceExpired = (invoice) => {
const { terminationTime } = _.pick(invoice, ['terminationTime']);
if (!terminationTime)
return false;
return terminationTime <= new Date().getTime() / 1000;
};
const currentMilestoneAmount = (invoice, currentMilestone) => {
const { amounts } = _.pick(invoice, ['amounts']);
if (Number(currentMilestone) < _.size(amounts)) {
return BigInt(amounts?.[Number(currentMilestone)] || 0);
}
return BigInt(0);
};
const isMilestoneReleasable = (invoice, tokenBalance, currentMilestone) => {
const { isLocked } = _.pick(invoice, ['isLocked']);
const amount = currentMilestoneAmount(invoice, currentMilestone);
if (!isLocked && amount && tokenBalance?.value) {
return tokenBalance.value >= BigInt(amount) && BigInt(amount) > BigInt(0);
}
return false;
};
const isLockable = (invoice, tokenBalance) => {
const { isLocked } = _.pick(invoice, ['isLocked']);
if (isLocked || isInvoiceExpired(invoice))
return false;
return (!isLocked && !isInvoiceExpired(invoice) && tokenBalance?.value > BigInt(0));
};
const parseMilestoneAmounts = (invoice, tokenMetadata) => {
const { amounts } = _.pick(invoice, ['amounts']);
const { decimals } = tokenMetadata;
return _.map(amounts, a => _.toNumber(formatUnits(BigInt(a), decimals)));
};
const getDeadlineLabel = (instantDetails, tokenBalance) => {
const { lateFee, lateFeeTimeInterval, deadline } = _.pick(instantDetails, [
'lateFee',
'lateFeeTimeInterval',
'deadline',
]);
if (!lateFee || !deadline || !lateFeeTimeInterval)
return undefined;
const daysPerInterval = lateFeeTimeInterval
? lateFeeTimeInterval / BigInt(1000 * 60 * 60 * 24)
: undefined;
return `${formatUnits(lateFee, tokenBalance?.decimals || 18)} ${tokenBalance?.symbol} every ${daysPerInterval} day${daysPerInterval && daysPerInterval > 1 ? 's' : ''} after ${getDateString(parseToDate(deadline))}`;
};
const getDisputeAndResolution = (invoice) => {
const dispute = invoice?.disputes?.[0];
const resolution = invoice?.resolutions?.[0];
if (_.size(invoice?.disputes) > _.size(invoice?.resolutions)) {
return { dispute, resolution: undefined };
}
return { dispute, resolution };
};
const convertOldMetadata = (oldMetadata) => {
if (!oldMetadata)
return undefined;
const { projectName, title, projectDescription, description, projectAgreement, documents, startDate, endDate, ...metadata } = oldMetadata;
const start = parseToDate(startDate);
const end = parseToDate(endDate);
return {
...metadata,
title: title ?? projectName,
description: description ?? projectDescription,
documents: documents ?? projectAgreement,
startDate: Math.floor(start.getTime() / 1000),
endDate: Math.floor(end.getTime() / 1000),
};
};
export const getInvoiceDetails = async (invoice, tokenMetadata, tokenBalance, nativeBalance, instantDetails, invoiceMetadata) => {
if (!invoice ||
!tokenMetadata ||
!tokenBalance ||
!nativeBalance ||
!invoiceMetadata) {
return null;
}
const { network, currentMilestone, resolver } = _.pick(invoice, [
'resolver',
'network',
'currentMilestone',
]);
const chainId = chainByName(network)?.id;
// current milestone
const currentMilestoneNumber = _.toNumber(currentMilestone?.toString());
const currentMilestoneAmountLabel = currentMilestoneAmount(invoice, currentMilestoneNumber);
// resolver
const resolverInfo = getResolverInfo(invoiceMetadata?.resolverType, chainId) ??
getResolverInfoByAddress(resolver, chainId);
const resolverFee = getResolverFee(invoice, tokenBalance);
const { dispute, resolution } = getDisputeAndResolution(invoice);
try {
const invoiceDetails = {
...invoice,
chainId,
// computed values
total: totalAmount(invoice),
deposited: totalDeposited(invoice),
due: totalDue(invoice),
// milestones
currentMilestoneNumber,
currentMilestoneAmount: currentMilestoneAmount(invoice, currentMilestoneNumber),
currentMilestoneAmountDisplay: formatUnits(currentMilestoneAmountLabel, tokenMetadata?.decimals),
parsedAmounts: parseMilestoneAmounts(invoice, tokenMetadata),
depositedMilestones: depositedMilestones(invoice),
depositedMilestonesDisplay: depositedMilestonesString(invoice),
depositedTxs: assignDeposits(invoice),
releasedTxs: assignReleases(invoice),
// resolver
resolverInfo,
resolverFee,
resolverFeeDisplay: resolverFeeLabel(resolverFee, tokenMetadata),
// instant
deadlineLabel: getDeadlineLabel(instantDetails, tokenBalance),
// dispute
dispute,
resolution,
// flags
isExpired: isInvoiceExpired(invoice),
isReleasable: isMilestoneReleasable(invoice, tokenBalance, currentMilestoneNumber),
isLockable: isLockable(invoice, tokenBalance),
isWithdrawable: isInvoiceExpired(invoice) &&
!!tokenBalance?.value &&
tokenBalance.value > BigInt(0),
// token data
tokenMetadata,
tokenBalance,
nativeBalance,
...instantDetails,
metadata: convertOldMetadata(invoiceMetadata),
};
logDebug({ invoiceDetails, address: invoice.address });
return invoiceDetails;
}
catch (e) {
// eslint-disable-next-line no-console
console.log("Couldn't assemble InvoiceDetails", e);
return null;
}
};