Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added library to extract NFT id from label + tests #2

Merged
merged 3 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/nft-resolver-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Run nft-resolver tests

on:
pull_request:
branches:
- main

jobs:
nft-resolver-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Cache node modules
id: cache-npm
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-

- name: Install Dependencies
run: npm install --frozen-lockfile

- name: Run tests
run: npm run test
53 changes: 53 additions & 0 deletions contracts/LabelUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.25;

/**
* @title Library to perform ENS label manipulation
* @author ConsenSys Software Inc.
*/
library LabelUtils {
/**
* Extract the first label name from a dns encoded ens domain
* @param name the dns encoded ENS domain
* @return label as bytes
*/
function extractFirstLabel(
bytes memory name
) external pure returns (bytes memory) {
uint256 idx = 0;
uint8 labelLength = uint8(name[idx]);
idx++;
bytes memory label = new bytes(labelLength);
for (uint256 i = 0; i < labelLength; i++) {
label[i] = name[idx + i];
}
return label;
}

/**
* Extract the numeric suffix from the dns encoded label
* @param label the dns encoded label
* @return number the numeric suffix
*/
function extractNumericSuffix(
bytes memory label
) external pure returns (uint256) {
uint256 num = 0;
bool hasNumber = false;

for (uint256 i = 0; i < label.length; i++) {
uint8 char = uint8(label[i]);
if (char >= 48 && char <= 57) {
// ASCII for '0' is 48 and '9' is 57
num = num * 10 + (char - 48);
hasNumber = true;
} else if (hasNumber) {
// Break on first non-digit after starting to read numbers
break;
}
}

require(hasNumber, "No numeric suffix found");
return num;
}
}
63 changes: 56 additions & 7 deletions contracts/NFTResolver.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.25;

import {EVMFetcher} from "@consensys/linea-state-verifier/contracts/EVMFetcher.sol";
Expand All @@ -13,6 +13,7 @@ import "@ensdomains/ens-contracts/contracts/resolvers/profiles/IExtendedResolver
import {ITargetResolver} from "./ITargetResolver.sol";
import {IAddrSetter} from "./IAddrSetter.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {LabelUtils} from "./LabelUtils.sol";

contract NFTResolver is
EVMFetchTarget,
Expand All @@ -27,6 +28,8 @@ contract NFTResolver is
ENS public immutable ens;
INameWrapper public immutable nameWrapper;
uint256 public immutable l2ChainId;
// PublicResolver used to resolve a base domain such as xxx.eth when queried
address public immutable publicResolver;
mapping(bytes32 => address) targets;
uint256 constant OWNERS_SLOT = 2;
// To check how old is the value/proof returned and is in the acceptable range
Expand Down Expand Up @@ -54,12 +57,14 @@ contract NFTResolver is
* @param _ens The ENS registry address
* @param _nameWrapper The ENS name wrapper address
* @param _l2ChainId The chainId at which the resolver resolves data from
* @param _publicResolver The PublicResolver address to use to resolve base domains
*/
constructor(
IEVMVerifier _verifier,
ENS _ens,
INameWrapper _nameWrapper,
uint256 _l2ChainId
uint256 _l2ChainId,
address _publicResolver
) {
require(
address(_nameWrapper) != address(0),
Expand All @@ -74,6 +79,7 @@ contract NFTResolver is
ens = _ens;
nameWrapper = _nameWrapper;
l2ChainId = _l2ChainId;
publicResolver = _publicResolver;
}

/**
Expand Down Expand Up @@ -146,19 +152,38 @@ contract NFTResolver is
) external view returns (bytes memory result) {
require(data.length >= 4, "param data too short");

bytes32 node = abi.decode(data[4:], (bytes32));
bool isBaseDomain = targets[node] != address(0);

// If trying to resolve the base domain, we use the PublicResolver
if (isBaseDomain) {
return _resolve(name, data);
}

(, address target) = _getTarget(name, 0);

bytes4 selector = bytes4(data);

if (selector == IAddrResolver.addr.selector) {
bytes32 node = abi.decode(data[4:], (bytes32));
// TODO: Replace node by NFT ID
return _addr(1, target);
// Get NFT Index from the
uint256 nftId = extractNFTId(name);
return _addr(nftId, target);
}

// None selector has been found it reverts
revert("invalid selector");
}

/**
* Get the NFT Id from the ENS name's label
* @param name DNS encoded ENS name
* @return id the NFT id
*/
function extractNFTId(bytes calldata name) public pure returns (uint256) {
bytes memory firstLabel = LabelUtils.extractFirstLabel(name);
return LabelUtils.extractNumericSuffix(firstLabel);
}

/**
* @dev Resolve and throws an EIP 3559 compliant error
* @param name DNS encoded ENS name to query
Expand All @@ -173,13 +198,36 @@ contract NFTResolver is
_writeDeferral(target);
}

/**
* @dev The `PublicResolver` does not implement the `resolve(bytes,bytes)` method.
* This method completes the resolution request by staticcalling `PublicResolver` with the resolve request.
* Implementation matches the ENS `ExtendedResolver:resolve(bytes,bytes)` method with the exception that it `staticcall`s the
* the `rootResolver` instead of `address(this)`.
* @param data The ABI encoded data for the underlying resolution function (Eg, addr(bytes32), text(bytes32,string), etc).
* @return The return data, ABI encoded identically to the underlying function.
*/
function _resolve(
bytes memory,
bytes memory data
) internal view returns (bytes memory) {
(bool success, bytes memory result) = publicResolver.staticcall(data);
if (success) {
return result;
} else {
// Revert with the reason provided by the call
assembly {
revert(add(result, 0x20), mload(result))
}
}
}

function _addr(
uint256 tokenId,
address target
) private view returns (bytes memory) {
EVMFetcher
.newFetchRequest(verifier, target)
.getDynamic(OWNERS_SLOT)
.getStatic(OWNERS_SLOT)
.element(tokenId)
.fetch(this.addrCallback.selector, ""); // recordVersions
}
Expand All @@ -188,7 +236,8 @@ contract NFTResolver is
bytes[] memory values,
bytes memory
) external pure returns (bytes memory) {
return abi.encode(address(bytes20(values[1])));
address addr = abi.decode(values[0], (address));
return abi.encode(addr);
}

function supportsInterface(
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"version": "1.0.0",
"description": "NFT Resolver to resolve names based on NFT ownership on Linea",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "npx hardhat test",
"clean": "npx hardhat clean",
"compile": "npx hardhat compile"
},
"repository": {
"type": "git",
Expand All @@ -30,4 +32,4 @@
"@ensdomains/ens-contracts": "^1.2.2",
"@openzeppelin/contracts": "^4.1.0"
}
}
}
Loading