@smartinvoicexyz/utils
Version:
Unified source for utility functions used across the Smart Invoice protocol.
180 lines (179 loc) • 6.76 kB
JavaScript
import { ARWEAVE_ENDPOINT, invoiceFactory, IPFS_ENDPOINT, isSupportedChainId, resolverInfo, resolvers, wrappedNativeToken, } from '@smartinvoicexyz/constants';
import _ from 'lodash';
import { formatUnits } from 'viem';
import { chainById } from './web3';
export const getResolverTypes = (chainId) => {
if (!isSupportedChainId(chainId))
return [];
return resolvers(chainId);
};
export const getResolverInfo = (resolverType, chainId) => {
if (!resolverType || !isSupportedChainId(chainId))
return undefined;
return resolverInfo(chainId)[resolverType];
};
export const getResolverInfoByAddress = (resolverAddress, chainId) => {
if (!isSupportedChainId(chainId))
return undefined;
const resolverTypes = getResolverTypes(chainId);
return resolverTypes
.map(resolverType => getResolverInfo(resolverType, chainId))
.find(info => info?.address.toLowerCase() === resolverAddress.toLowerCase());
};
export const getResolverFee = (invoice, tokenBalance) => {
const { resolutionRate } = _.pick(invoice, ['resolutionRate']);
return tokenBalance?.value && resolutionRate
? formatUnits(!resolutionRate || resolutionRate === BigInt(0)
? BigInt(0)
: tokenBalance.value / BigInt(resolutionRate), 18)
: undefined;
};
export const resolverFeeLabel = (fee, tokenMetadata) => (fee ? `${fee} ${tokenMetadata?.symbol}` : undefined);
export const getWrappedNativeToken = (chainId) => {
if (!isSupportedChainId(chainId))
return undefined;
return wrappedNativeToken(chainId);
};
export const getNativeTokenSymbol = (chainId) => {
if (!isSupportedChainId(chainId))
return undefined;
return chainById(chainId).nativeCurrency.symbol;
};
export const getInvoiceFactoryAddress = (chainId) => {
if (!isSupportedChainId(chainId))
return undefined;
return invoiceFactory(chainId);
};
const getExplorerUrl = (chainId) => {
if (!isSupportedChainId(chainId))
return undefined;
const chain = chainById(chainId);
return _.get(chain, 'blockExplorers.etherscan.url', _.get(chain, 'blockExplorers.default.url'));
};
export const getTxLink = (chainId, hash) => {
if (!chainId || !hash)
return undefined;
return `${getExplorerUrl(chainId)}/tx/${hash}`;
};
export const getAddressLink = (chainId, hash) => {
if (!chainId || !hash)
return undefined;
return `${getExplorerUrl(chainId)}/address/${hash}`;
};
// bytes58 QmNLei78zWmzUdbeRB3CiUfAizWUrbeeZh5K1rhAQKCh51
// is the same as
// bytes64 12200000000000000000000000000000000000000000000000000000000000000000
// which means an all zeros bytes32 was input on the contract
const EMPTY_CID = 'QmNLei78zWmzUdbeRB3CiUfAizWUrbeeZh5K1rhAQKCh51';
export const isEmptyIpfsHash = (hash) => hash === EMPTY_CID;
export const getAccountString = (account) => {
if (!account)
return undefined;
const len = account.length;
return `0x${_.toUpper(account.slice(2, 4))}...${_.toUpper(account.slice(len - 5, len))}`;
};
export const isKnownResolver = (resolverType, chainId) => {
if (!isSupportedChainId(chainId))
return false;
return _.includes(getResolverTypes(chainId), _.toLower(resolverType));
};
export const isKnownResolverAddress = (resolverAddress, chainId) => _.some(getResolverTypes(chainId), resolverType => getResolverInfo(resolverType, chainId)?.address.toLowerCase() ===
resolverAddress.toLowerCase());
export const getResolverString = (resolverType, chainId, resolverAddress) => {
const info = getResolverInfo(resolverType, chainId);
return info ? info.name : getAccountString(resolverAddress);
};
const IPFS_HASH_REGEX = /^(Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})([/?#][-a-zA-Z0-9@:%_+.~#?&//=]*)*$/;
const isValidCID = (hash) => IPFS_HASH_REGEX.test(hash);
const ARWEAVE_TXID_REGEX = /^([a-z0-9-_]{43})$/i;
const isValidArweaveTxID = (hash) => ARWEAVE_TXID_REGEX.test(hash);
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'ipfs:', 'ar:'];
export const PROTOCOL_OPTIONS = ['https://', 'ipfs://', 'ar://'];
export const isValidURL = (url) => {
if (!url)
return false;
try {
// Parse the URL
const parsedUrl = new URL(url);
// Check if the protocol is one of the allowed protocols
if (!ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
return false;
}
if (parsedUrl.protocol === 'ipfs:') {
return isValidCID(parsedUrl.href.slice(7));
}
if (parsedUrl.protocol === 'ar:') {
return isValidArweaveTxID(parsedUrl.href.slice(5));
}
// Additional checks for valid formats, etc.
return true;
}
catch {
// If URL constructor throws, it's not a valid URL
return false;
}
};
export const getDecimals = (value) => {
const [, decimal] = value.split('.');
return decimal?.length || 0;
};
export function commify(x, decimals) {
if (_.toString(x).includes('.')) {
const [whole, decimal] = x.toString().split('.');
return `${commify(whole)}.${decimal.substring(0, Math.min(decimals ?? decimal.length, decimal.length))}`;
}
return _.toString(x).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
export const resolutionFeePercentage = (resolutionRate) => {
if (!resolutionRate || resolutionRate === '0') {
return 0;
}
const feePercentage = 1 / parseInt(resolutionRate, 10);
return feePercentage;
};
export const uriToHttp = (uri) => {
if (!isValidURL(uri)) {
return undefined;
}
const parsedUrl = new URL(uri);
if (parsedUrl.protocol === 'ipfs:') {
return `${IPFS_ENDPOINT}/ipfs/${parsedUrl.href.slice(7)}`;
}
if (parsedUrl.protocol === 'ar:') {
return `${ARWEAVE_ENDPOINT}/${parsedUrl.href.slice(5)}`;
}
return parsedUrl.href;
};
export const getIpfsLink = (hash) => {
if (!isValidCID(hash)) {
return '';
}
return `${IPFS_ENDPOINT}/ipfs/${hash}`;
};
export const documentToHttp = (document) => uriToHttp(document.src);
const protocolToType = (protocol) => {
switch (protocol) {
case 'ipfs:':
return 'ipfs';
case 'ar:':
return 'arweave';
case 'https:':
case 'http:':
default:
return 'https';
}
};
export const uriToDocument = (uri) => {
if (!isValidURL(uri)) {
return undefined;
}
const parsed = new URL(uri);
const now = Math.floor(new Date().getTime() / 1000);
const type = protocolToType(parsed.protocol);
return {
id: parsed.hostname + parsed.pathname + now,
src: uri,
type,
createdAt: now,
};
};