// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {BitMath} from "@uniswap/v4-core/src/libraries/BitMath.sol";
import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol";
import {Base64} from "openzeppelin-contracts/contracts/utils/Base64.sol";
/// @title SVG
/// @notice Provides a function for generating an SVG associated with a Uniswap NFT
/// @dev Reference: https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/NFTSVG.sol
library SVG {
using Strings for uint256;
// SVG path commands for the curve that represent the steepness of the position
// defined using the Cubic Bezier Curve syntax
// curve1 is the smallest (linear) curve, curve8 is the largest curve
string constant curve1 = "M1 1C41 41 105 105 145 145";
string constant curve2 = "M1 1C33 49 97 113 145 145";
string constant curve3 = "M1 1C33 57 89 113 145 145";
string constant curve4 = "M1 1C25 65 81 121 145 145";
string constant curve5 = "M1 1C17 73 73 129 145 145";
string constant curve6 = "M1 1C9 81 65 137 145 145";
string constant curve7 = "M1 1C1 89 57.5 145 145 145";
string constant curve8 = "M1 1C1 97 49 145 145 145";
struct SVGParams {
string quoteCurrency;
string baseCurrency;
address hooks;
string quoteCurrencySymbol;
string baseCurrencySymbol;
string feeTier;
int24 tickLower;
int24 tickUpper;
int24 tickSpacing;
int8 overRange;
uint256 tokenId;
string color0;
string color1;
string color2;
string color3;
string x1;
string y1;
string x2;
string y2;
string x3;
string y3;
}
/// @notice Generate the SVG associated with a Uniswap v4 NFT
/// @param params The SVGParams struct containing the parameters for the SVG
/// @return svg The SVG string associated with the NFT
function generateSVG(SVGParams memory params) internal pure returns (string memory svg) {
return string(
abi.encodePacked(
generateSVGDefs(params),
generateSVGBorderText(
params.quoteCurrency, params.baseCurrency, params.quoteCurrencySymbol, params.baseCurrencySymbol
),
generateSVGCardMantle(params.quoteCurrencySymbol, params.baseCurrencySymbol, params.feeTier),
generageSvgCurve(params.tickLower, params.tickUpper, params.tickSpacing, params.overRange),
generateSVGPositionDataAndLocationCurve(
params.tokenId.toString(), params.hooks, params.tickLower, params.tickUpper
),
generateSVGRareSparkle(params.tokenId, params.hooks),
""
)
);
}
/// @notice Generate the SVG defs that create the color scheme for the SVG
/// @param params The SVGParams struct containing the parameters to generate the SVG defs
/// @return svg The SVG defs string
function generateSVGDefs(SVGParams memory params) private pure returns (string memory svg) {
svg = string(
abi.encodePacked(
'"
)
)
),
'"/>"
)
)
),
'"/>"
)
)
),
'" />',
'"
)
)
),
'" />',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
' ',
'',
'',
''
)
);
}
/// @notice Generate the SVG for the moving border text displaying the quote and base currency addresses with their symbols
/// @param quoteCurrency The quote currency
/// @param baseCurrency The base currency
/// @param quoteCurrencySymbol The quote currency symbol
/// @param baseCurrencySymbol The base currency symbol
/// @return svg The SVG for the border NFT's border text
function generateSVGBorderText(
string memory quoteCurrency,
string memory baseCurrency,
string memory quoteCurrencySymbol,
string memory baseCurrencySymbol
) private pure returns (string memory svg) {
svg = string(
abi.encodePacked(
'',
'',
baseCurrency,
unicode" • ",
baseCurrencySymbol,
' ',
'',
baseCurrency,
unicode" • ",
baseCurrencySymbol,
' ',
'',
quoteCurrency,
unicode" • ",
quoteCurrencySymbol,
' ',
quoteCurrency,
unicode" • ",
quoteCurrencySymbol,
' '
)
);
}
/// @notice Generate the SVG for the card mantle displaying the quote and base currency symbols and fee tier
/// @param quoteCurrencySymbol The quote currency symbol
/// @param baseCurrencySymbol The base currency symbol
/// @param feeTier The fee tier
/// @return svg The SVG for the card mantle
function generateSVGCardMantle(
string memory quoteCurrencySymbol,
string memory baseCurrencySymbol,
string memory feeTier
) private pure returns (string memory svg) {
svg = string(
abi.encodePacked(
'',
quoteCurrencySymbol,
"/",
baseCurrencySymbol,
'',
feeTier,
"",
''
)
);
}
/// @notice Generate the SVG for the curve that represents the position. Fade up (top is faded) if current price is above your position range, fade down (bottom is faded) if current price is below your position range
/// Circles are generated at the ends of the curve if the position is in range, or at one end of the curve it is on if not in range
/// @param tickLower The lower tick
/// @param tickUpper The upper tick
/// @param tickSpacing The tick spacing
/// @param overRange Whether the current tick is in range, over range, or under range
/// @return svg The SVG for the curve
function generageSvgCurve(int24 tickLower, int24 tickUpper, int24 tickSpacing, int8 overRange)
private
pure
returns (string memory svg)
{
string memory fade = overRange == 1 ? "#fade-up" : overRange == -1 ? "#fade-down" : "#none";
string memory curve = getCurve(tickLower, tickUpper, tickSpacing);
svg = string(
abi.encodePacked(
''
'' '',
'',
'',
'',
generateSVGCurveCircle(overRange)
)
);
}
/// @notice Get the curve based on the tick range
/// The smaller the tick range, the smaller/more linear the curve
/// @param tickLower The lower tick
/// @param tickUpper The upper tick
/// @param tickSpacing The tick spacing
/// @return curve The curve path
function getCurve(int24 tickLower, int24 tickUpper, int24 tickSpacing)
internal
pure
returns (string memory curve)
{
int24 tickRange = (tickUpper - tickLower) / tickSpacing;
if (tickRange <= 4) {
curve = curve1;
} else if (tickRange <= 8) {
curve = curve2;
} else if (tickRange <= 16) {
curve = curve3;
} else if (tickRange <= 32) {
curve = curve4;
} else if (tickRange <= 64) {
curve = curve5;
} else if (tickRange <= 128) {
curve = curve6;
} else if (tickRange <= 256) {
curve = curve7;
} else {
curve = curve8;
}
}
/// @notice Generate the SVG for the circles on the curve
/// @param overRange 0 if the current tick is in range, 1 if the current tick is over range, -1 if the current tick is under range
/// @return svg The SVG for the circles
function generateSVGCurveCircle(int8 overRange) internal pure returns (string memory svg) {
string memory curvex1 = "73";
string memory curvey1 = "190";
string memory curvex2 = "217";
string memory curvey2 = "334";
/// If the position is over or under range, generate one circle at the end of the curve on the side of the range it is on with a larger circle around it
if (overRange == 1 || overRange == -1) {
svg = string(
abi.encodePacked(
''
)
);
} else {
/// If the position is in range, generate two circles at the ends of the curve
svg = string(
abi.encodePacked(
'',
''
)
);
}
}
/// @notice Generate the SVG for the position data (token ID, hooks address, min tick, max tick) and the location curve (where your position falls on the curve)
/// @param tokenId The token ID
/// @param hook The hooks address
/// @param tickLower The lower tick
/// @param tickUpper The upper tick
/// @return svg The SVG for the position data and location curve
function generateSVGPositionDataAndLocationCurve(
string memory tokenId,
address hook,
int24 tickLower,
int24 tickUpper
) private pure returns (string memory svg) {
string memory hookStr = (uint256(uint160(hook))).toHexString(20);
string memory tickLowerStr = tickToString(tickLower);
string memory tickUpperStr = tickToString(tickUpper);
uint256 str1length = bytes(tokenId).length + 4;
string memory hookSlice = hook == address(0)
? "No Hook"
: string(abi.encodePacked(substring(hookStr, 0, 5), "...", substring(hookStr, 39, 42)));
uint256 str2length = bytes(hookSlice).length + 5;
uint256 str3length = bytes(tickLowerStr).length + 10;
uint256 str4length = bytes(tickUpperStr).length + 10;
(string memory xCoord, string memory yCoord) = rangeLocation(tickLower, tickUpper);
svg = string(
abi.encodePacked(
' ',
'',
'ID: ',
tokenId,
"",
' ',
'',
'Hook: ',
hookSlice,
"",
' ',
'',
'Min Tick: ',
tickLowerStr,
"",
' ',
'',
'Max Tick: ',
tickUpperStr,
"" '',
'',
'',
''
)
);
}
function substring(string memory str, uint256 startIndex, uint256 endIndex) internal pure returns (string memory) {
bytes memory strBytes = bytes(str);
bytes memory result = new bytes(endIndex - startIndex);
for (uint256 i = startIndex; i < endIndex; i++) {
result[i - startIndex] = strBytes[i];
}
return string(result);
}
function tickToString(int24 tick) private pure returns (string memory) {
string memory sign = "";
if (tick < 0) {
tick = tick * -1;
sign = "-";
}
return string(abi.encodePacked(sign, uint256(uint24(tick)).toString()));
}
/// @notice Get the location of where your position falls on the curve
/// @param tickLower The lower tick
/// @param tickUpper The upper tick
/// @return The x and y coordinates of the location of the liquidity
function rangeLocation(int24 tickLower, int24 tickUpper) internal pure returns (string memory, string memory) {
int24 midPoint = (tickLower + tickUpper) / 2;
if (midPoint < -125_000) {
return ("8", "7");
} else if (midPoint < -75_000) {
return ("8", "10.5");
} else if (midPoint < -25_000) {
return ("8", "14.25");
} else if (midPoint < -5_000) {
return ("10", "18");
} else if (midPoint < 0) {
return ("11", "21");
} else if (midPoint < 5_000) {
return ("13", "23");
} else if (midPoint < 25_000) {
return ("15", "25");
} else if (midPoint < 75_000) {
return ("18", "26");
} else if (midPoint < 125_000) {
return ("21", "27");
} else {
return ("24", "27");
}
}
/// @notice Generates the SVG for a rare sparkle if the NFT is rare. Else, returns an empty string
/// @param tokenId The token ID
/// @param hooks The hooks address
/// @return svg The SVG for the rare sparkle
function generateSVGRareSparkle(uint256 tokenId, address hooks) private pure returns (string memory svg) {
if (isRare(tokenId, hooks)) {
svg = string(
abi.encodePacked(
'',
'',
''
)
);
} else {
svg = "";
}
}
/// @notice Determines if an NFT is rare based on the token ID and hooks address
/// @param tokenId The token ID
/// @param hooks The hooks address
/// @return Whether the NFT is rare or not
function isRare(uint256 tokenId, address hooks) internal pure returns (bool) {
bytes32 h = keccak256(abi.encodePacked(tokenId, hooks));
return uint256(h) < type(uint256).max / (1 + BitMath.mostSignificantBit(tokenId) * 2);
}
}