// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.26;
import {ITokenURIProvider} from "lens-modules/contracts/core/interfaces/ITokenURIProvider.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import {Events} from "lens-modules/contracts/core/types/Events.sol";
import {IERC721Namespace} from "lens-modules/contracts/core/interfaces/IERC721Namespace.sol";
contract LensUsernameTokenURIProvider is ITokenURIProvider {
using Strings for uint256;
uint256 constant CHARS_PER_MULTILINE_LINE = 17;
constructor() {
_emitLensContractDeployedEvent();
}
function tokenURI(uint256 tokenId) external view override returns (string memory) {
string memory lowercasedUsername = _toLowercase(IERC721Namespace(msg.sender).getUsernameByTokenId(tokenId));
uint256 usernameLength = bytes(lowercasedUsername).length;
string memory scapedUsernameWithAtSymbol = string.concat("@", _escapeForJson(lowercasedUsername));
string memory namespace = IERC721Namespace(msg.sender).getNamespace();
return string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
abi.encodePacked(
'{"name":"',
scapedUsernameWithAtSymbol,
'","description":"',
_escapeForJson(_slice(namespace, 0, bytes(namespace).length)),
" - ",
scapedUsernameWithAtSymbol,
'","image":"data:image/svg+xml;base64,',
Base64.encode(bytes(_svgImage(namespace, lowercasedUsername))),
'","attributes":[{"trait_type":"length","value":"',
usernameLength.toString(),
'"}]}'
)
)
)
);
}
function _emitLensContractDeployedEvent() internal virtual {
emit Events.Lens_Contract_Deployed({
contractType: "lens.contract.TokenURIProvider",
flavour: "lens.contract.TokenURIProvider.LensUsernameTokenURIProvider"
});
}
function _svgImage(string memory namespace, string memory lowercasedUsername)
internal
view
returns (string memory)
{
return string.concat(
'"
);
}
function _namespaceAndUsernameTexts(string memory namespace, string memory lowercasedUsername)
internal
view
virtual
returns (string memory)
{
(string memory usernameText, uint256 usernameYCoordinate, uint256 usernameAmountOfLines) =
_usernameText(lowercasedUsername);
string memory namespaceText = _namespaceText(namespace, usernameYCoordinate, usernameAmountOfLines);
return string.concat(namespaceText, usernameText);
}
function _namespaceText(string memory namespace, uint256 usernameYCoordinate, uint256 usernameAmountOfLines)
internal
view
virtual
returns (string memory)
{
uint256 namespaceYCoordinate;
if (usernameAmountOfLines > 1) {
namespaceYCoordinate = usernameYCoordinate - 49;
} else {
namespaceYCoordinate = usernameYCoordinate - 68;
}
namespace = _toLowercase(namespace);
if (bytes(namespace).length > 18) {
namespace = string.concat(_slice(namespace, 0, 17), "...");
}
return string.concat(
'',
_escapeForSvg(namespace),
""
);
}
// Returns the SVG text for the username, and the lines of text used
function _usernameText(string memory username) internal view virtual returns (string memory, uint256, uint256) {
uint256 usernameLength = bytes(username).length;
uint256 fontSize;
uint256 amountOfLines;
uint256 yCoordinateStartOfText;
string memory usernameTextSpans;
if (usernameLength <= 17) {
// Use single line
if (usernameLength <= 11) {
fontSize = 52;
} else if (usernameLength == 12) {
fontSize = 48;
} else if (usernameLength == 13) {
fontSize = 44;
} else if (usernameLength == 14) {
fontSize = 41;
} else if (usernameLength == 15) {
fontSize = 38;
} else if (usernameLength == 16) {
fontSize = 36;
} else {
// usernameLength == 17
fontSize = 34;
}
usernameTextSpans = string.concat('', _escapeForSvg(username), "");
yCoordinateStartOfText = 468;
amountOfLines = 1;
} else {
uint256 charsToBorrow = 0;
// More than 17 characters, use multiple lines
fontSize = 33;
// Break into multiple lines
amountOfLines = usernameLength / CHARS_PER_MULTILINE_LINE;
if (usernameLength % CHARS_PER_MULTILINE_LINE != 0) {
amountOfLines++;
}
if (amountOfLines > 4) {
// Fix to 4 lines and put '...' at the end
amountOfLines = 4;
username = string.concat(_slice(username, 0, 17 * 4 - 2), "...");
} else {
uint256 charsInLastLine = usernameLength % CHARS_PER_MULTILINE_LINE;
if (charsInLastLine != 0 && charsInLastLine < 3) {
// Borrow chars from previous line to make last line have 3 chars
charsToBorrow = 3 - charsInLastLine;
}
}
// Last line at 471, then each line above goes up -42 Y coordinates up
yCoordinateStartOfText = 471 - 42 * (amountOfLines - 1);
for (uint256 i = 0; i < amountOfLines - 1; i++) {
uint256 sliceEnd = (i + 1) * CHARS_PER_MULTILINE_LINE;
if (i == amountOfLines - 2) {
sliceEnd -= charsToBorrow;
}
usernameTextSpans = string.concat(
usernameTextSpans,
'',
_escapeForSvg(_slice(username, i * CHARS_PER_MULTILINE_LINE, sliceEnd)),
""
);
}
usernameTextSpans = string.concat(
usernameTextSpans,
'',
_escapeForSvg(
_slice(
username, (amountOfLines - 1) * CHARS_PER_MULTILINE_LINE - charsToBorrow, bytes(username).length
)
),
""
);
}
string memory usernameTextSvg = string.concat(
'',
usernameTextSpans,
""
);
return (usernameTextSvg, yCoordinateStartOfText, amountOfLines);
}
function _slice(string memory str, uint256 start, uint256 end) internal pure virtual returns (string memory) {
bytes memory strBytes = bytes(str);
bytes memory result = new bytes(end - start);
assembly {
let length := sub(end, start)
let resultPtr := add(result, 0x20)
let strPtr := add(add(strBytes, 0x20), start)
// Copy memory using mload and mstore in 32-byte chunks
for { let i := 0 } lt(i, length) { i := add(i, 0x20) } { mstore(add(resultPtr, i), mload(add(strPtr, i))) }
// Handle any remaining bytes (if length is not a multiple of 32)
let remainder := mod(length, 0x20)
if gt(remainder, 0) {
let mask := sub(shl(mul(8, sub(0x20, remainder)), 1), 1)
let data := and(mload(add(strPtr, length)), not(mask))
mstore(add(resultPtr, length), data)
}
}
return string(result);
}
function _escapeForSvg(string memory str) internal pure virtual returns (string memory) {
uint256 i = 0;
uint256 length = bytes(str).length;
while (i < length) {
bytes1 char = bytes(str)[i];
if (char == "&") {
// & -> &
str = string.concat(_slice(str, 0, i), "&", _slice(str, i + 1, length));
length += 4;
i += 5;
} else if (char == "<") {
// < -> <
str = string.concat(_slice(str, 0, i), "<", _slice(str, i + 1, length));
length += 3;
i += 4;
} else if (char == ">") {
// > -> >
str = string.concat(_slice(str, 0, i), ">", _slice(str, i + 1, length));
length += 3;
i += 4;
} else if (char == '"') {
// " -> "
str = string.concat(_slice(str, 0, i), """, _slice(str, i + 1, length));
length += 5;
i += 6;
} else if (char == "'") {
// ' -> '
str = string.concat(_slice(str, 0, i), "'", _slice(str, i + 1, length));
length += 4;
i += 5;
} else {
i++;
}
}
return str;
}
function _escapeForJson(string memory str) internal pure virtual returns (string memory) {
uint256 i = 0;
uint256 length = bytes(str).length;
while (i < length) {
bytes1 char = bytes(str)[i];
if (char == '"') {
// " -> \"
str = string.concat(_slice(str, 0, i), "\\", '"', _slice(str, i + 1, length));
length += 1;
i += 2;
} else if (char == "\\") {
// \ -> \\
str = string.concat(_slice(str, 0, i), "\\", "\\", _slice(str, i + 1, length));
length += 1;
i += 2;
} else if (char == "\x08") {
// \b -> \\b
str = string.concat(_slice(str, 0, i), "\\", "b", _slice(str, i + 1, length));
length += 1;
i += 2;
} else if (char == "\x0c") {
// \f -> \\f
str = string.concat(_slice(str, 0, i), "\\", "f", _slice(str, i + 1, length));
length += 1;
i += 2;
} else if (char == "\n") {
// \n -> \\n
str = string.concat(_slice(str, 0, i), "\\", "n", _slice(str, i + 1, length));
length += 1;
i += 2;
} else if (char == "\r") {
// \r -> \\r
str = string.concat(_slice(str, 0, i), "\\", "r", _slice(str, i + 1, length));
length += 1;
i += 2;
} else if (char == "\t") {
// \t -> \\t
str = string.concat(_slice(str, 0, i), "\\", "t", _slice(str, i + 1, length));
length += 1;
i += 2;
} else {
i++;
}
}
return str;
}
function _asString(uint256 value) internal pure virtual returns (string memory) {
return value.toString();
}
function _toLowercase(string memory str) public pure returns (string memory) {
bytes memory strBytes = bytes(str);
for (uint256 i = 0; i < strBytes.length; i++) {
bytes1 char = strBytes[i];
// Check if character is uppercase (A-Z)
if (char >= "A" && char <= "Z") {
// Convert to lowercase by adding 32
strBytes[i] = bytes1(uint8(char) + 32);
}
}
return string(strBytes);
}
}