infrastructure-upgrade/lib/v3-periphery/test/NFTDescriptor.spec.ts

908 lines
32 KiB
TypeScript
Raw Normal View History

2023-04-08 21:46:18 +03:00
import { BigNumber, constants, Wallet } from 'ethers'
import { encodePriceSqrt } from './shared/encodePriceSqrt'
import { waffle, ethers } from 'hardhat'
import { expect } from './shared/expect'
import { TestERC20Metadata, NFTDescriptorTest } from '../typechain'
import { Fixture } from 'ethereum-waffle'
import { FeeAmount, TICK_SPACINGS } from './shared/constants'
import snapshotGasCost from './shared/snapshotGasCost'
import { formatSqrtRatioX96 } from './shared/formatSqrtRatioX96'
import { getMaxTick, getMinTick } from './shared/ticks'
import { randomBytes } from 'crypto'
import { extractJSONFromURI } from './shared/extractJSONFromURI'
import fs from 'fs'
import isSvg from 'is-svg'
const TEN = BigNumber.from(10)
const LOWEST_SQRT_RATIO = 4310618292
const HIGHEST_SQRT_RATIO = BigNumber.from(33849).mul(TEN.pow(34))
describe('NFTDescriptor', () => {
let wallets: Wallet[]
const nftDescriptorFixture: Fixture<{
tokens: [TestERC20Metadata, TestERC20Metadata, TestERC20Metadata, TestERC20Metadata]
nftDescriptor: NFTDescriptorTest
}> = async (wallets, provider) => {
const nftDescriptorLibraryFactory = await ethers.getContractFactory('NFTDescriptor')
const nftDescriptorLibrary = await nftDescriptorLibraryFactory.deploy()
const tokenFactory = await ethers.getContractFactory('TestERC20Metadata')
const NFTDescriptorFactory = await ethers.getContractFactory('NFTDescriptorTest', {
libraries: {
NFTDescriptor: nftDescriptorLibrary.address,
},
})
const nftDescriptor = (await NFTDescriptorFactory.deploy()) as NFTDescriptorTest
const TestERC20Metadata = tokenFactory.deploy(constants.MaxUint256.div(2), 'Test ERC20', 'TEST1')
const tokens: [TestERC20Metadata, TestERC20Metadata, TestERC20Metadata, TestERC20Metadata] = [
(await tokenFactory.deploy(constants.MaxUint256.div(2), 'Test ERC20', 'TEST1')) as TestERC20Metadata, // do not use maxu256 to avoid overflowing
(await tokenFactory.deploy(constants.MaxUint256.div(2), 'Test ERC20', 'TEST2')) as TestERC20Metadata,
(await tokenFactory.deploy(constants.MaxUint256.div(2), 'Test ERC20', 'TEST3')) as TestERC20Metadata,
(await tokenFactory.deploy(constants.MaxUint256.div(2), 'Test ERC20', 'TEST4')) as TestERC20Metadata,
]
tokens.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1))
return {
nftDescriptor,
tokens,
}
}
let nftDescriptor: NFTDescriptorTest
let tokens: [TestERC20Metadata, TestERC20Metadata, TestERC20Metadata, TestERC20Metadata]
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
before('create fixture loader', async () => {
wallets = await (ethers as any).getSigners()
loadFixture = waffle.createFixtureLoader(wallets)
})
beforeEach('load fixture', async () => {
;({ nftDescriptor, tokens } = await loadFixture(nftDescriptorFixture))
})
describe('#constructTokenURI', () => {
let tokenId: number
let baseTokenAddress: string
let quoteTokenAddress: string
let baseTokenSymbol: string
let quoteTokenSymbol: string
let baseTokenDecimals: number
let quoteTokenDecimals: number
let flipRatio: boolean
let tickLower: number
let tickUpper: number
let tickCurrent: number
let tickSpacing: number
let fee: number
let poolAddress: string
beforeEach(async () => {
tokenId = 123
baseTokenAddress = tokens[0].address
quoteTokenAddress = tokens[1].address
baseTokenSymbol = await tokens[0].symbol()
quoteTokenSymbol = await tokens[1].symbol()
baseTokenDecimals = await tokens[0].decimals()
quoteTokenDecimals = await tokens[1].decimals()
flipRatio = false
tickLower = getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM])
tickUpper = getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM])
tickCurrent = 0
tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
fee = 3000
poolAddress = `0x${'b'.repeat(40)}`
})
it('returns the valid JSON string with min and max ticks', async () => {
const json = extractJSONFromURI(
await nftDescriptor.constructTokenURI({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
)
const tokenUri = constructTokenMetadata(
tokenId,
quoteTokenAddress,
baseTokenAddress,
poolAddress,
quoteTokenSymbol,
baseTokenSymbol,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
'0.3%',
'MIN<>MAX'
)
expect(json.description).to.equal(tokenUri.description)
expect(json.name).to.equal(tokenUri.name)
})
it('returns the valid JSON string with mid ticks', async () => {
tickLower = -10
tickUpper = 10
tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
fee = 3000
const json = extractJSONFromURI(
await nftDescriptor.constructTokenURI({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
)
const tokenMetadata = constructTokenMetadata(
tokenId,
quoteTokenAddress,
baseTokenAddress,
poolAddress,
quoteTokenSymbol,
baseTokenSymbol,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
'0.3%',
'0.99900<>1.0010'
)
expect(json.description).to.equal(tokenMetadata.description)
expect(json.name).to.equal(tokenMetadata.name)
})
it('returns valid JSON when token symbols contain quotes', async () => {
quoteTokenSymbol = '"TES"T1"'
const json = extractJSONFromURI(
await nftDescriptor.constructTokenURI({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
)
const tokenMetadata = constructTokenMetadata(
tokenId,
quoteTokenAddress,
baseTokenAddress,
poolAddress,
quoteTokenSymbol,
baseTokenSymbol,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
'0.3%',
'MIN<>MAX'
)
expect(json.description).to.equal(tokenMetadata.description)
expect(json.name).to.equal(tokenMetadata.name)
})
describe('when the token ratio is flipped', () => {
it('returns the valid JSON for mid ticks', async () => {
flipRatio = true
tickLower = -10
tickUpper = 10
const json = extractJSONFromURI(
await nftDescriptor.constructTokenURI({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
)
const tokenMetadata = constructTokenMetadata(
tokenId,
quoteTokenAddress,
baseTokenAddress,
poolAddress,
quoteTokenSymbol,
baseTokenSymbol,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
'0.3%',
'0.99900<>1.0010'
)
expect(json.description).to.equal(tokenMetadata.description)
expect(json.name).to.equal(tokenMetadata.name)
})
it('returns the valid JSON for min/max ticks', async () => {
flipRatio = true
const json = extractJSONFromURI(
await nftDescriptor.constructTokenURI({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
)
const tokenMetadata = constructTokenMetadata(
tokenId,
quoteTokenAddress,
baseTokenAddress,
poolAddress,
quoteTokenSymbol,
baseTokenSymbol,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
'0.3%',
'MIN<>MAX'
)
expect(json.description).to.equal(tokenMetadata.description)
expect(json.name).to.equal(tokenMetadata.name)
})
})
it('gas', async () => {
await snapshotGasCost(
nftDescriptor.getGasCostOfConstructTokenURI({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
)
})
it('snapshot matches', async () => {
// get snapshot with super rare special sparkle
tokenId = 1
poolAddress = `0x${'b'.repeat(40)}`
// get a snapshot with svg fade
tickCurrent = -1
tickLower = 0
tickUpper = 1000
tickSpacing = TICK_SPACINGS[FeeAmount.LOW]
fee = FeeAmount.LOW
quoteTokenAddress = '0xabcdeabcdefabcdefabcdefabcdefabcdefabcdf'
baseTokenAddress = '0x1234567890123456789123456789012345678901'
quoteTokenSymbol = 'UNI'
baseTokenSymbol = 'WETH'
expect(
await nftDescriptor.constructTokenURI({
tokenId,
quoteTokenAddress,
baseTokenAddress,
quoteTokenSymbol,
baseTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
).toMatchSnapshot()
})
})
describe('#addressToString', () => {
it('returns the correct string for a given address', async () => {
let addressStr = await nftDescriptor.addressToString(`0x${'1234abcdef'.repeat(4)}`)
expect(addressStr).to.eq('0x1234abcdef1234abcdef1234abcdef1234abcdef')
addressStr = await nftDescriptor.addressToString(`0x${'1'.repeat(40)}`)
expect(addressStr).to.eq(`0x${'1'.repeat(40)}`)
})
})
describe('#tickToDecimalString', () => {
let tickSpacing: number
let minTick: number
let maxTick: number
describe('when tickspacing is 10', () => {
before(() => {
tickSpacing = TICK_SPACINGS[FeeAmount.LOW]
minTick = getMinTick(tickSpacing)
maxTick = getMaxTick(tickSpacing)
})
it('returns MIN on lowest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false)).to.equal('MIN')
})
it('returns MAX on the highest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false)).to.equal('MAX')
})
it('returns the correct decimal string when the tick is in range', async () => {
expect(await nftDescriptor.tickToDecimalString(1, tickSpacing, 18, 18, false)).to.equal('1.0001')
})
it('returns the correct decimal string when tick is mintick for different tickspace', async () => {
const otherMinTick = getMinTick(TICK_SPACINGS[FeeAmount.HIGH])
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false)).to.equal(
'0.0000000000000000000000000000000000000029387'
)
})
})
describe('when tickspacing is 60', () => {
before(() => {
tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
minTick = getMinTick(tickSpacing)
maxTick = getMaxTick(tickSpacing)
})
it('returns MIN on lowest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false)).to.equal('MIN')
})
it('returns MAX on the highest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false)).to.equal('MAX')
})
it('returns the correct decimal string when the tick is in range', async () => {
expect(await nftDescriptor.tickToDecimalString(-1, tickSpacing, 18, 18, false)).to.equal('0.99990')
})
it('returns the correct decimal string when tick is mintick for different tickspace', async () => {
const otherMinTick = getMinTick(TICK_SPACINGS[FeeAmount.HIGH])
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false)).to.equal(
'0.0000000000000000000000000000000000000029387'
)
})
})
describe('when tickspacing is 200', () => {
before(() => {
tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
minTick = getMinTick(tickSpacing)
maxTick = getMaxTick(tickSpacing)
})
it('returns MIN on lowest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(minTick, tickSpacing, 18, 18, false)).to.equal('MIN')
})
it('returns MAX on the highest tick', async () => {
expect(await nftDescriptor.tickToDecimalString(maxTick, tickSpacing, 18, 18, false)).to.equal('MAX')
})
it('returns the correct decimal string when the tick is in range', async () => {
expect(await nftDescriptor.tickToDecimalString(0, tickSpacing, 18, 18, false)).to.equal('1.0000')
})
it('returns the correct decimal string when tick is mintick for different tickspace', async () => {
const otherMinTick = getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM])
expect(await nftDescriptor.tickToDecimalString(otherMinTick, tickSpacing, 18, 18, false)).to.equal(
'0.0000000000000000000000000000000000000029387'
)
})
})
describe('when token ratio is flipped', () => {
it('returns the inverse of default ratio for medium sized numbers', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(10, tickSpacing, 18, 18, false)).to.eq('1.0010')
expect(await nftDescriptor.tickToDecimalString(10, tickSpacing, 18, 18, true)).to.eq('0.99900')
})
it('returns the inverse of default ratio for large numbers', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(487272, tickSpacing, 18, 18, false)).to.eq(
'1448400000000000000000'
)
expect(await nftDescriptor.tickToDecimalString(487272, tickSpacing, 18, 18, true)).to.eq(
'0.00000000000000000000069041'
)
})
it('returns the inverse of default ratio for small numbers', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(-387272, tickSpacing, 18, 18, false)).to.eq(
'0.000000000000000015200'
)
expect(await nftDescriptor.tickToDecimalString(-387272, tickSpacing, 18, 18, true)).to.eq('65791000000000000')
})
it('returns the correct string with differing token decimals', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
expect(await nftDescriptor.tickToDecimalString(1000, tickSpacing, 18, 18, true)).to.eq('0.90484')
expect(await nftDescriptor.tickToDecimalString(1000, tickSpacing, 18, 10, true)).to.eq('90484000')
expect(await nftDescriptor.tickToDecimalString(1000, tickSpacing, 10, 18, true)).to.eq('0.0000000090484')
})
it('returns MIN for highest tick', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
const lowestTick = getMinTick(TICK_SPACINGS[FeeAmount.HIGH])
expect(await nftDescriptor.tickToDecimalString(lowestTick, tickSpacing, 18, 18, true)).to.eq('MAX')
})
it('returns MAX for lowest tick', async () => {
const tickSpacing = TICK_SPACINGS[FeeAmount.HIGH]
const highestTick = getMaxTick(TICK_SPACINGS[FeeAmount.HIGH])
expect(await nftDescriptor.tickToDecimalString(highestTick, tickSpacing, 18, 18, true)).to.eq('MIN')
})
})
})
describe('#fixedPointToDecimalString', () => {
describe('returns the correct string for', () => {
it('the highest possible price', async () => {
const ratio = encodePriceSqrt(33849, 1 / 10 ** 34)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq(
'338490000000000000000000000000000000000'
)
})
it('large numbers', async () => {
let ratio = encodePriceSqrt(25811, 1 / 10 ** 11)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('2581100000000000')
ratio = encodePriceSqrt(17662, 1 / 10 ** 5)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('1766200000')
})
it('exactly 5 sigfig whole number', async () => {
const ratio = encodePriceSqrt(42026, 1)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('42026')
})
it('when the decimal is at index 4', async () => {
const ratio = encodePriceSqrt(12087, 10)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('1208.7')
})
it('when the decimal is at index 3', async () => {
const ratio = encodePriceSqrt(12087, 100)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('120.87')
})
it('when the decimal is at index 2', async () => {
const ratio = encodePriceSqrt(12087, 1000)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('12.087')
})
it('when the decimal is at index 1', async () => {
const ratio = encodePriceSqrt(12345, 10000)
const bla = await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('1.2345')
})
it('when sigfigs have trailing 0s after the decimal', async () => {
const ratio = encodePriceSqrt(1, 1)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('1.0000')
})
it('when there are exactly 5 numbers after the decimal', async () => {
const ratio = encodePriceSqrt(12345, 100000)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('0.12345')
})
it('very small numbers', async () => {
let ratio = encodePriceSqrt(38741, 10 ** 20)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq('0.00000000000000038741')
ratio = encodePriceSqrt(88498, 10 ** 35)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq(
'0.00000000000000000000000000000088498'
)
})
it('smallest number', async () => {
const ratio = encodePriceSqrt(39000, 10 ** 43)
expect(await nftDescriptor.fixedPointToDecimalString(ratio, 18, 18)).to.eq(
'0.0000000000000000000000000000000000000029387'
)
})
})
describe('when tokens have different decimal precision', () => {
describe('when baseToken has more precision decimals than quoteToken', () => {
it('returns the correct string when the decimal difference is even', async () => {
expect(await nftDescriptor.fixedPointToDecimalString(encodePriceSqrt(1, 1), 18, 16)).to.eq('100.00')
})
it('returns the correct string when the decimal difference is odd', async () => {
const tenRatio = encodePriceSqrt(10, 1)
expect(await nftDescriptor.fixedPointToDecimalString(tenRatio, 18, 17)).to.eq('100.00')
})
it('does not account for higher token0 precision if difference is more than 18', async () => {
expect(await nftDescriptor.fixedPointToDecimalString(encodePriceSqrt(1, 1), 24, 5)).to.eq('1.0000')
})
})
describe('when quoteToken has more precision decimals than baseToken', () => {
it('returns the correct string when the decimal difference is even', async () => {
expect(await nftDescriptor.fixedPointToDecimalString(encodePriceSqrt(1, 1), 10, 18)).to.eq('0.000000010000')
})
it('returns the correct string when the decimal difference is odd', async () => {
expect(await nftDescriptor.fixedPointToDecimalString(encodePriceSqrt(1, 1), 7, 18)).to.eq('0.000000000010000')
})
// TODO: provide compatibility token prices that breach minimum price due to token decimal differences
it.skip('returns the correct string when the decimal difference brings ratio below the minimum', async () => {
const lowRatio = encodePriceSqrt(88498, 10 ** 35)
expect(await nftDescriptor.fixedPointToDecimalString(lowRatio, 10, 20)).to.eq(
'0.000000000000000000000000000000000000000088498'
)
})
it('does not account for higher token1 precision if difference is more than 18', async () => {
expect(await nftDescriptor.fixedPointToDecimalString(encodePriceSqrt(1, 1), 24, 5)).to.eq('1.0000')
})
})
it('some fuzz', async () => {
const random = (min: number, max: number): number => {
return Math.floor(min + ((Math.random() * 100) % (max + 1 - min)))
}
const inputs: [BigNumber, number, number][] = []
let i = 0
while (i <= 20) {
const ratio = BigNumber.from(`0x${randomBytes(random(7, 20)).toString('hex')}`)
const decimals0 = random(3, 21)
const decimals1 = random(3, 21)
const decimalDiff = Math.abs(decimals0 - decimals1)
// TODO: Address edgecase out of bounds prices due to decimal differences
if (
ratio.div(TEN.pow(decimalDiff)).gt(LOWEST_SQRT_RATIO) &&
ratio.mul(TEN.pow(decimalDiff)).lt(HIGHEST_SQRT_RATIO)
) {
inputs.push([ratio, decimals0, decimals1])
i++
}
}
for (let i in inputs) {
let ratio: BigNumber | number
let decimals0: number
let decimals1: number
;[ratio, decimals0, decimals1] = inputs[i]
let result = await nftDescriptor.fixedPointToDecimalString(ratio, decimals0, decimals1)
expect(formatSqrtRatioX96(ratio, decimals0, decimals1)).to.eq(result)
}
}).timeout(300_000)
})
})
describe('#feeToPercentString', () => {
it('returns the correct fee for 0', async () => {
expect(await nftDescriptor.feeToPercentString(0)).to.eq('0%')
})
it('returns the correct fee for 1', async () => {
expect(await nftDescriptor.feeToPercentString(1)).to.eq('0.0001%')
})
it('returns the correct fee for 30', async () => {
expect(await nftDescriptor.feeToPercentString(30)).to.eq('0.003%')
})
it('returns the correct fee for 33', async () => {
expect(await nftDescriptor.feeToPercentString(33)).to.eq('0.0033%')
})
it('returns the correct fee for 500', async () => {
expect(await nftDescriptor.feeToPercentString(500)).to.eq('0.05%')
})
it('returns the correct fee for 2500', async () => {
expect(await nftDescriptor.feeToPercentString(2500)).to.eq('0.25%')
})
it('returns the correct fee for 3000', async () => {
expect(await nftDescriptor.feeToPercentString(3000)).to.eq('0.3%')
})
it('returns the correct fee for 10000', async () => {
expect(await nftDescriptor.feeToPercentString(10000)).to.eq('1%')
})
it('returns the correct fee for 17000', async () => {
expect(await nftDescriptor.feeToPercentString(17000)).to.eq('1.7%')
})
it('returns the correct fee for 100000', async () => {
expect(await nftDescriptor.feeToPercentString(100000)).to.eq('10%')
})
it('returns the correct fee for 150000', async () => {
expect(await nftDescriptor.feeToPercentString(150000)).to.eq('15%')
})
it('returns the correct fee for 102000', async () => {
expect(await nftDescriptor.feeToPercentString(102000)).to.eq('10.2%')
})
it('returns the correct fee for 10000000', async () => {
expect(await nftDescriptor.feeToPercentString(1000000)).to.eq('100%')
})
it('returns the correct fee for 1005000', async () => {
expect(await nftDescriptor.feeToPercentString(1005000)).to.eq('100.5%')
})
it('returns the correct fee for 10000000', async () => {
expect(await nftDescriptor.feeToPercentString(10000000)).to.eq('1000%')
})
it('returns the correct fee for 12300000', async () => {
expect(await nftDescriptor.feeToPercentString(12300000)).to.eq('1230%')
})
})
describe('#tokenToColorHex', () => {
function tokenToColorHex(tokenAddress: string, startIndex: number): string {
return `${tokenAddress.slice(startIndex, startIndex + 6).toLowerCase()}`
}
it('returns the correct hash for the first 3 bytes of the token address', async () => {
expect(await nftDescriptor.tokenToColorHex(tokens[0].address, 136)).to.eq(tokenToColorHex(tokens[0].address, 2))
expect(await nftDescriptor.tokenToColorHex(tokens[1].address, 136)).to.eq(tokenToColorHex(tokens[1].address, 2))
})
it('returns the correct hash for the last 3 bytes of the address', async () => {
expect(await nftDescriptor.tokenToColorHex(tokens[0].address, 0)).to.eq(tokenToColorHex(tokens[0].address, 36))
expect(await nftDescriptor.tokenToColorHex(tokens[1].address, 0)).to.eq(tokenToColorHex(tokens[1].address, 36))
})
})
describe('#rangeLocation', () => {
it('returns the correct coordinates when range midpoint under -125_000', async () => {
const coords = await nftDescriptor.rangeLocation(-887_272, -887_100)
expect(coords[0]).to.eq('8')
expect(coords[1]).to.eq('7')
})
it('returns the correct coordinates when range midpoint is between -125_000 and -75_000', async () => {
const coords = await nftDescriptor.rangeLocation(-100_000, -90_000)
expect(coords[0]).to.eq('8')
expect(coords[1]).to.eq('10.5')
})
it('returns the correct coordinates when range midpoint is between -75_000 and -25_000', async () => {
const coords = await nftDescriptor.rangeLocation(-50_000, -20_000)
expect(coords[0]).to.eq('8')
expect(coords[1]).to.eq('14.25')
})
it('returns the correct coordinates when range midpoint is between -25_000 and -5_000', async () => {
const coords = await nftDescriptor.rangeLocation(-10_000, -5_000)
expect(coords[0]).to.eq('10')
expect(coords[1]).to.eq('18')
})
it('returns the correct coordinates when range midpoint is between -5_000 and 0', async () => {
const coords = await nftDescriptor.rangeLocation(-5_000, -4_000)
expect(coords[0]).to.eq('11')
expect(coords[1]).to.eq('21')
})
it('returns the correct coordinates when range midpoint is between 0 and 5_000', async () => {
const coords = await nftDescriptor.rangeLocation(4_000, 5_000)
expect(coords[0]).to.eq('13')
expect(coords[1]).to.eq('23')
})
it('returns the correct coordinates when range midpoint is between 5_000 and 25_000', async () => {
const coords = await nftDescriptor.rangeLocation(10_000, 15_000)
expect(coords[0]).to.eq('15')
expect(coords[1]).to.eq('25')
})
it('returns the correct coordinates when range midpoint is between 25_000 and 75_000', async () => {
const coords = await nftDescriptor.rangeLocation(25_000, 50_000)
expect(coords[0]).to.eq('18')
expect(coords[1]).to.eq('26')
})
it('returns the correct coordinates when range midpoint is between 75_000 and 125_000', async () => {
const coords = await nftDescriptor.rangeLocation(100_000, 125_000)
expect(coords[0]).to.eq('21')
expect(coords[1]).to.eq('27')
})
it('returns the correct coordinates when range midpoint is above 125_000', async () => {
const coords = await nftDescriptor.rangeLocation(200_000, 100_000)
expect(coords[0]).to.eq('24')
expect(coords[1]).to.eq('27')
})
it('math does not overflow on max value', async () => {
const coords = await nftDescriptor.rangeLocation(887_272, 887_272)
expect(coords[0]).to.eq('24')
expect(coords[1]).to.eq('27')
})
})
describe('#svgImage', () => {
let tokenId: number
let baseTokenAddress: string
let quoteTokenAddress: string
let baseTokenSymbol: string
let quoteTokenSymbol: string
let baseTokenDecimals: number
let quoteTokenDecimals: number
let flipRatio: boolean
let tickLower: number
let tickUpper: number
let tickCurrent: number
let tickSpacing: number
let fee: number
let poolAddress: string
beforeEach(async () => {
tokenId = 123
quoteTokenAddress = '0x1234567890123456789123456789012345678901'
baseTokenAddress = '0xabcdeabcdefabcdefabcdefabcdefabcdefabcdf'
quoteTokenSymbol = 'UNI'
baseTokenSymbol = 'WETH'
tickLower = -1000
tickUpper = 2000
tickCurrent = 40
fee = 500
baseTokenDecimals = await tokens[0].decimals()
quoteTokenDecimals = await tokens[1].decimals()
flipRatio = false
tickSpacing = TICK_SPACINGS[FeeAmount.MEDIUM]
poolAddress = `0x${'b'.repeat(40)}`
})
it('matches the current snapshot', async () => {
const svg = await nftDescriptor.generateSVGImage({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
expect(svg).toMatchSnapshot()
fs.writeFileSync('./test/__snapshots__/NFTDescriptor.svg', svg)
})
it('returns a valid SVG', async () => {
const svg = await nftDescriptor.generateSVGImage({
tokenId,
baseTokenAddress,
quoteTokenAddress,
baseTokenSymbol,
quoteTokenSymbol,
baseTokenDecimals,
quoteTokenDecimals,
flipRatio,
tickLower,
tickUpper,
tickCurrent,
tickSpacing,
fee,
poolAddress,
})
expect(isSvg(svg)).to.eq(true)
})
})
describe('#isRare', () => {
it('returns true sometimes', async () => {
expect(await nftDescriptor.isRare(1, `0x${'b'.repeat(40)}`)).to.eq(true)
})
it('returns false sometimes', async () => {
expect(await nftDescriptor.isRare(2, `0x${'b'.repeat(40)}`)).to.eq(false)
})
})
function constructTokenMetadata(
tokenId: number,
quoteTokenAddress: string,
baseTokenAddress: string,
poolAddress: string,
quoteTokenSymbol: string,
baseTokenSymbol: string,
flipRatio: boolean,
tickLower: number,
tickUpper: number,
tickCurrent: number,
feeTier: string,
prices: string
): { name: string; description: string } {
quoteTokenSymbol = quoteTokenSymbol.replace(/"/gi, '"')
baseTokenSymbol = baseTokenSymbol.replace(/"/gi, '"')
return {
name: `Uniswap - ${feeTier} - ${quoteTokenSymbol}/${baseTokenSymbol} - ${prices}`,
description: `This NFT represents a liquidity position in a Uniswap V3 ${quoteTokenSymbol}-${baseTokenSymbol} pool. The owner of this NFT can modify or redeem the position.\n\
\nPool Address: ${poolAddress}\n${quoteTokenSymbol} Address: ${quoteTokenAddress.toLowerCase()}\n${baseTokenSymbol} Address: ${baseTokenAddress.toLowerCase()}\n\
Fee Tier: ${feeTier}\nToken ID: ${tokenId}\n\n DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure token addresses match the expected tokens, as \
token symbols may be imitated.`,
}
}
})