Skip to content

Commit

Permalink
feat(contract): update SpaceOwner token URI methods (#586)
Browse files Browse the repository at this point in the history
Added new methods to manage default URI in `SpaceOwner` and
`SpaceOwnerUriBase`. This includes setting and getting the default URI,
updating the `_render` method to use the default URI, and adding
relevant events and validation checks.

Simplify layout access in `SpaceOwner` with direct getter and setter
methods. Add selectors for `setDefaultUri` and `getDefaultUri` in
deployment script. Extend tests to cover new `setDefaultUri`
functionality and improve `tokenURI` tests.

Streamline the logic for building URIs by prioritizing space URIs and
falling back to the default URI. The test suite has been updated to
reflect these changes, ensuring that URIs with and without trailing
slashes are handled correctly.

Added Solady library to the project and updated `remappings.txt` and
`copyLibs.js` accordingly. Replaced OpenZeppelin's `Strings` with
Solady's `LibString` for string operations in `SpaceOwnerUriBase.sol`
and corresponding test file. Updated `package.json` and `yarn.lock` to
include Solady.

https://linear.app/hnt-labs/issue/HNT-10522/space-metadata-uri

https://linear.app/hnt-labs/issue/HNT-10623/spec-town-metadata-changes-to-support-public-town-image
  • Loading branch information
shuhuiluo authored Aug 1, 2024
1 parent 3552a35 commit 1e84b59
Show file tree
Hide file tree
Showing 68 changed files with 543 additions and 156 deletions.
2 changes: 1 addition & 1 deletion contracts/makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ build:; forge build

test :; forge test --ffi

snapshot :; forge snapshot
snapshot :; forge snapshot --isolate

format :; yarn prettier --write .

Expand Down
3 changes: 2 additions & 1 deletion contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"@openzeppelin/contracts-upgradeable": "^5.0.2",
"@prb/math": "^4.0.3",
"account-abstraction": "github:eth-infinitism/account-abstraction",
"base64": "github:Brechtpd/base64"
"base64": "github:Brechtpd/base64",
"solady": "^0.0.228"
},
"devDependencies": {
"@prb/test": "^0.6.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ contract DeploySpaceOwnerFacet is FacetHelper, Deployer {
constructor() {
addSelector(SpaceOwner.setFactory.selector);
addSelector(SpaceOwner.getFactory.selector);
addSelector(SpaceOwner.setDefaultUri.selector);
addSelector(SpaceOwner.getDefaultUri.selector);
addSelector(SpaceOwner.nextTokenId.selector);
addSelector(SpaceOwner.mintSpace.selector);
addSelector(SpaceOwner.getSpaceInfo.selector);
Expand Down
9 changes: 9 additions & 0 deletions contracts/src/spaces/facets/owner/ISpaceOwner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ interface ISpaceOwnerBase {

error SpaceOwner__OnlyFactoryAllowed();
error SpaceOwner__OnlySpaceOwnerAllowed();
error SpaceOwner__SpaceNotFound();
error SpaceOwner__DefaultUriNotSet();

event SpaceOwner__UpdateSpace(address indexed space);
event SpaceOwner__SetFactory(address factory);
event SpaceOwner__SetDefaultUri(string uri);
}

interface ISpaceOwner is ISpaceOwnerBase {
Expand All @@ -30,6 +33,12 @@ interface ISpaceOwner is ISpaceOwnerBase {
/// @notice Get the factory address
function getFactory() external view returns (address);

/// @notice Set the default URI
function setDefaultUri(string memory uri) external;

/// @notice Get the default URI
function getDefaultUri() external view returns (string memory);

/// @notice Get the next token id that will be used to mint a space
function nextTokenId() external view returns (uint256);

Expand Down
30 changes: 22 additions & 8 deletions contracts/src/spaces/facets/owner/SpaceOwner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,28 @@ contract SpaceOwner is
return _domainSeparatorV4();
}

// =============================================================
// Uri
// =============================================================

/// @inheritdoc ISpaceOwner
function setDefaultUri(string memory uri) external onlyOwner {
_setDefaultUri(uri);
}

/// @inheritdoc ISpaceOwner
function getDefaultUri() external view returns (string memory) {
return _getDefaultUri();
}

function tokenURI(
uint256 tokenId
) public view virtual override returns (string memory) {
if (!_exists(tokenId)) revert URIQueryForNonexistentToken();

return _render(tokenId);
}

// =============================================================
// Overrides
// =============================================================
Expand All @@ -115,14 +137,6 @@ contract SpaceOwner is
super.setApprovalForAll(operator, approved);
}

function tokenURI(
uint256 tokenId
) public view virtual override returns (string memory) {
if (!_exists(tokenId)) revert URIQueryForNonexistentToken();

return _render(tokenId);
}

function _beforeTokenTransfers(
address from,
address to,
Expand Down
4 changes: 2 additions & 2 deletions contracts/src/spaces/facets/owner/SpaceOwnerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ library SpaceOwnerStorage {
mapping(uint256 => address) spaceByTokenId;
mapping(address => Space) spaceByAddress;
mapping(address => SpaceMetadata) spaceMetadata;
string defaultUri;
}

function layout() internal pure returns (Layout storage l) {
bytes32 slot = STORAGE_SLOT;
assembly {
l.slot := slot
l.slot := STORAGE_SLOT
}
}
}
59 changes: 37 additions & 22 deletions contracts/src/spaces/facets/owner/SpaceOwnerUriBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,53 @@ import {ISpaceOwnerBase} from "contracts/src/spaces/facets/owner/ISpaceOwner.sol

// libraries
import {Base64} from "base64/base64.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {LibString} from "solady/utils/LibString.sol";
import {Validator} from "contracts/src/utils/Validator.sol";

// contracts
import {SpaceOwnerStorage} from "contracts/src/spaces/facets/owner/SpaceOwnerStorage.sol";

abstract contract SpaceOwnerUriBase is ISpaceOwnerBase {
using LibString for address;

function _setDefaultUri(string memory uri) internal {
Validator.checkLength(uri, 1);

SpaceOwnerStorage.layout().defaultUri = uri;
emit SpaceOwner__SetDefaultUri(uri);
}

function _getDefaultUri() internal view returns (string memory) {
return SpaceOwnerStorage.layout().defaultUri;
}

/// @dev Returns `${space.uri}` or `${defaultUri}/${spaceAddress}`
function _render(
uint256 tokenId
) internal view virtual returns (string memory) {
SpaceOwnerStorage.Layout storage ds = SpaceOwnerStorage.layout();
address spaceAddress = ds.spaceByTokenId[tokenId];

if (spaceAddress == address(0)) return "";

SpaceOwnerStorage.Space memory space = ds.spaceByAddress[spaceAddress];

return
string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
abi.encodePacked(
'{"name":"',
space.name,
'","image":"',
space.uri,
'","attributes":[{"trait_type":"Created","display_type": "date", "value":',
Strings.toString(space.createdAt),
"}]}"
)
)
)
);
if (spaceAddress == address(0)) revert SpaceOwner__SpaceNotFound();

SpaceOwnerStorage.Space storage space = ds.spaceByAddress[spaceAddress];

// if the space has set a uri, return it
if (bytes(space.uri).length != 0) return space.uri;

string memory defaultUri = ds.defaultUri;

uint256 length = bytes(defaultUri).length;
if (length == 0) revert SpaceOwner__DefaultUriNotSet();

unchecked {
// the ASCII code for "/" is 0x2f
if (bytes(defaultUri)[length - 1] != 0x2f) {
return
string.concat(defaultUri, "/", spaceAddress.toHexStringChecksummed());
} else {
return string.concat(defaultUri, spaceAddress.toHexStringChecksummed());
}
}
}
}
Loading

0 comments on commit 1e84b59

Please sign in to comment.