infrastructure-upgrade/lib/v3-periphery/test/NFTDescriptor.spec.ts
T-Hax 735546619e
init
Signed-off-by: T-Hax <>
2023-04-08 18:46:18 +00:00

908 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.`,
}
}
})