@nftsafe/sdk
Version:
Lend and rent any ERC721s and ERC1155s on supported mainnet and testnet.
368 lines (327 loc) • 11.4 kB
text/typescript
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)));
};