UNPKG

@nftsafe/sdk

Version:

Lend and rent any ERC721s and ERC1155s on supported mainnet and testnet.

368 lines (327 loc) 11.4 kB
import { BigNumber, ethers } from 'ethers'; import BigNumberJS from 'bignumber.js'; import { NFTStandard, PaymentToken } from './types'; import { NUM_BITS_IN_BYTE, WEI_DECIMAL } from './consts'; // consts that predominantly pertain to this file const BITSIZE_MAX_VALUE = 32; /** * hexchar is 0 to 15 which is 2 ** 4 - 1. * This means that hexchar (aka nibble) is half a byte, * since byte is 8 bits. This function converts number * of bytes to number of nibbles. * * e.g. 2 bytes is 4 nibbles * * @param byteCount * @returns number of nibbles that represent the byteCount bytes */ export const bytesToNibbles = (byteCount: number) => { if (typeof byteCount != 'number') throw new Error('only numbers supported'); if (byteCount < 1) throw new Error('invalid byteCount'); return byteCount * 2; }; /** * (21.42, 32) -> 0x0015002A * * (1.2, 32) -> 0x00010002 * * Notice how the whole decimal part is reprsented by the first 4 nibbles, * whereas the decimal part is represented by the second part, i.e. the * last 4 nibbles * * @param number * @param bitsize * @returns number's padded (of bitsize total length) hex format */ export const toPaddedHex = (number: number, bitsize: number) => { // in node.js this function fails for bitsize above 32 bits if (bitsize > BITSIZE_MAX_VALUE) throw new Error( `bitsize ${bitsize} above maximum value ${BITSIZE_MAX_VALUE}` ); // conversion to unsigned form based on if (number < 0) throw new Error('unsigned number not supported'); // 8 bits = 1 byteCount; 16 bits = 2 byteCount, ... const byteCount = Math.ceil(bitsize / NUM_BITS_IN_BYTE); // shifting 0 bits removes decimals // toString(16) converts into hex // .padStart(byteCount * 2, "0") adds byte return ( '0x' + (number >>> 0) .toString(16) .toUpperCase() // 1 nibble = 4 bits. 1 byte = 2 nibbles .padStart(bytesToNibbles(byteCount), '0') ); }; const sameLength = <T>(a: T[], b: T[]) => a.length === b.length; const validateSameLength = (...args: any[]) => { let prev: any = args[0]; for (const curr of args) { if (!curr) continue; if (!sameLength(prev, curr)) throw new Error('args length variable'); prev = curr; } return true; }; // const decimalToPaddedHexString = (number: number, bitsize: number): string => { // const byteCount = Math.ceil(bitsize / 8); // const maxBinValue = Math.pow(2, bitsize) - 1; // if (bitsize > 32) throw 'number above maximum value'; // if (number < 0) number = maxBinValue + number + 1; // return ( // '0x' + // (number >>> 0) // .toString(16) // .toUpperCase() // .padStart(byteCount * 2, '0') // ); // }; type IObjectKeysValues = | string[] | BigNumber[] | boolean[] | number[] | PaymentToken[] | string[][] | string[][][] | number[][] | [string, number][]; interface IObjectKeys { [key: string]: IObjectKeysValues | undefined; } interface PrepareBatch extends IObjectKeys { nftStandards: NFTStandard[]; nftAddresses: string[]; tokenIds: BigNumber[]; lendAmounts?: BigNumber[]; rentAmounts?: BigNumber[]; maxRentDurations?: number[]; minRentDurations?: number[]; dailyRentPrices?: string[]; collateralPrices?: string[]; paymentOptions?: PaymentToken[]; rentDurations?: number[]; lendingIds?: BigNumber[]; rentingIds?: BigNumber[]; allowedRenters?: string[][][]; } interface PrepareRevenueShareBatch extends IObjectKeys { nftStandards?: NFTStandard[]; nftAddresses: string[]; tokenIds: BigNumber[]; lendAmounts?: BigNumber[]; rentAmounts?: BigNumber[]; maxRentDurations?: number[]; paymentOptions?: PaymentToken[]; rentDurations?: number[]; lendingIds?: BigNumber[]; rentingIds?: BigNumber[]; upfrontFee?: string[]; revenueShareInfo?: string[][] | number[][]; allowedRenters?: string[][]; revenueAmounts?: BigNumber[]; renters?: string[]; revenueTokenAddress?: string[]; } /** * To spend as little gas as possible, arguments must follow a particular format * when passed to the contract. This function prepares whatever inputs you want * to send, and returns the inputs in an optimal format. * * This algorithm's time complexity is pretty awful. But, it will never run on * large arrays, so it doesn't really matter. * @param args */ export const prepareBatch = (args: PrepareBatch) => { if (args.nftAddresses.length === 1) return args; validateSameLength(Object.values(args)); let nfts: Map<string, PrepareBatch> = new Map(); const pb: PrepareBatch = { nftAddresses: [], tokenIds: [], nftStandards: [] }; // O(N), maybe higher because of [...o[k]!, v[i]] const updateNfts = (nftAddresses: string, i: number) => { const o = nfts.get(nftAddresses); for (const [k, v] of Object.entries(args)) { if (!o) throw new Error(`could not find ${nftAddresses}`); if (v) o[k] = [...(o[k] ?? []), v[i]] as IObjectKeysValues; } return nfts; }; const createNft = (nftAddresses: string, i: number) => { nfts.set(nftAddresses, { nftStandards: [args.nftStandards[i]], nftAddresses: [nftAddresses], tokenIds: [args.tokenIds[i]], lendAmounts: args.lendAmounts ? [args.lendAmounts[i]] : undefined, rentAmounts: args.rentAmounts ? [args.rentAmounts[i]] : undefined, maxRentDurations: args.maxRentDurations ? [args.maxRentDurations[i]] : undefined, minRentDurations: args.minRentDurations ? [args.minRentDurations[i]] : undefined, dailyRentPrices: args.dailyRentPrices ? [args.dailyRentPrices[i]] : undefined, collateralPrices: args.collateralPrices ? [args.collateralPrices[i]] : undefined, paymentOptions: args.paymentOptions ? [args.paymentOptions[i]] : undefined, rentDurations: args.rentDurations ? [args.rentDurations[i]] : undefined, lendingIds: args.lendingIds ? [args.lendingIds[i]] : undefined, rentingIds: args.rentingIds ? [args.rentingIds[i]] : undefined, allowedRenters: args.allowedRenters ? [args.allowedRenters[i]] : undefined, }); return nfts; }; // O(2 * N), yikes to 2 const worstArgsort = (tokenIds: BigNumber[]) => { var indices = new Array(tokenIds.length); for (var i = 0; i < tokenIds.length; ++i) indices[i] = i; indices.sort((a, b) => tokenIds[a].lt(tokenIds[b]) ? -1 : tokenIds[a].gt(tokenIds[b]) ? 1 : 0 ); return { sortedTokenID: sortPerIndices(indices, tokenIds), argsort: indices, }; }; const sortPerIndices = (argsort: number[], arr: any[]) => argsort.map(i => arr[i]); // O(N ** M). for each nft loop through all args. M - number of args Object.values(args.nftAddresses).forEach((nft, i) => { if (nfts.has(nft)) nfts = updateNfts(nft, i); else nfts = createNft(nft, i); }); const iterator = nfts.keys(); // O(N * N) while (iterator) { const g = iterator.next().value; if (!g) break; // end of loop const nft = nfts.get(g) as PrepareBatch; const tokenIds = nft.tokenIds as BigNumber[]; const { argsort } = worstArgsort(tokenIds); for (const k of Object.keys(nft)) { if (!nft[k]) continue; const sorted = sortPerIndices(argsort, nft[k] ?? []) as IObjectKeysValues; pb[k] = [...(pb[k] ?? []), ...sorted] as IObjectKeysValues; } } return pb; }; export const prepareRevenueShareBatch = (args: PrepareRevenueShareBatch) => { if (args.nftAddresses.length === 1) return args; validateSameLength(Object.values(args)); let nfts: Map<string, PrepareRevenueShareBatch> = new Map(); const pb: PrepareRevenueShareBatch = { nftAddresses: [], tokenIds: [], nftStandards: [], }; // O(N), maybe higher because of [...o[k]!, v[i]] const updateNfts = (nftAddresses: string, i: number) => { const o = nfts.get(nftAddresses); for (const [k, v] of Object.entries(args)) { if (!o) throw new Error(`could not find ${nftAddresses}`); if (v) o[k] = [...(o[k] ?? []), v[i]] as IObjectKeysValues; } return nfts; }; const createNft = (nftAddresses: string, i: number) => { nfts.set(nftAddresses, { nftStandards: args.nftStandards ? [args.nftStandards[i]] : undefined, nftAddresses: [nftAddresses], tokenIds: [args.tokenIds[i]], lendAmounts: args.lendAmounts ? [args.lendAmounts[i]] : undefined, rentAmounts: args.rentAmounts ? [args.rentAmounts[i]] : undefined, maxRentDurations: args.maxRentDurations ? [args.maxRentDurations[i]] : undefined, paymentOptions: args.paymentOptions ? [args.paymentOptions[i]] : undefined, rentDurations: args.rentDurations ? [args.rentDurations[i]] : undefined, lendingIds: args.lendingIds ? [args.lendingIds[i]] : undefined, rentingIds: args.rentingIds ? [args.rentingIds[i]] : undefined, }); return nfts; }; // O(2 * N), yikes to 2 const worstArgsort = (tokenIds: BigNumber[]) => { var indices = new Array(tokenIds.length); for (var i = 0; i < tokenIds.length; ++i) indices[i] = i; indices.sort((a, b) => tokenIds[a].lt(tokenIds[b]) ? -1 : tokenIds[a].gt(tokenIds[b]) ? 1 : 0 ); return { sortedTokenID: sortPerIndices(indices, tokenIds), argsort: indices, }; }; const sortPerIndices = (argsort: number[], arr: any[]) => argsort.map(i => arr[i]); // O(N ** M). for each nft loop through all args. M - number of args Object.values(args.nftAddresses).forEach((nft, i) => { if (nfts.has(nft)) nfts = updateNfts(nft, i); else nfts = createNft(nft, i); }); const iterator = nfts.keys(); // O(N * N) while (iterator) { const g = iterator.next().value; if (!g) break; // end of loop const nft = nfts.get(g) as PrepareBatch; const tokenIds = nft.tokenIds as BigNumber[]; const { argsort } = worstArgsort(tokenIds); for (const k of Object.keys(nft)) { if (!nft[k]) continue; const sorted = sortPerIndices(argsort, nft[k] ?? []) as IObjectKeysValues; pb[k] = [...(pb[k] ?? []), ...sorted] as IObjectKeysValues; } } return pb; }; // Convert a number into specific byte string export const convertToSpecificByteString = ( number: number, byteSize: number ) => { return ethers.utils.hexZeroPad(ethers.utils.hexlify(number), byteSize); }; // Ex. : 3 to 0x00000003 export const numberToByte4 = (number: number) => { return convertToSpecificByteString(number, 4); }; export const numberToByte8 = (number: number) => { return convertToSpecificByteString(number, 8); }; export const numberToByte16 = (number: number) => { return convertToSpecificByteString(number, 16); }; export const numberToByte32 = (number: number) => { return convertToSpecificByteString(number, 32); }; // Convert Number to Byte4 string // Ex. : 0x00000003 to 3 export const byteToNumber = (number: string) => { return parseInt(number); }; export const bigNumberToWei = ( amount: string | number, decimal: string | number = WEI_DECIMAL ) => { return new BigNumberJS(amount).multipliedBy( new BigNumberJS(10).pow(Number(decimal)) ); }; export const bigNumberToEther = ( amount: string | number, decimal: string | number = WEI_DECIMAL ) => { return new BigNumberJS(amount).div(new BigNumberJS(10).pow(Number(decimal))); };