Skip to content

Commit 0a3888d

Browse files
committed
refactor: adapt how we store subgraph metadata to make it easier to render into base58
1 parent 760a7ea commit 0a3888d

File tree

12 files changed

+238
-53
lines changed

12 files changed

+238
-53
lines changed

contracts/discovery/GNS.sol

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
236236
override
237237
onlySubgraphAuth(_subgraphID)
238238
{
239-
_updateSubgraphMetadata(_subgraphID, _subgraphMetadata);
240-
}
241-
242-
/**
243-
* @dev Internal: Allows a subgraph owner to update the metadata of a subgraph they have published
244-
* @param _subgraphID Subgraph ID
245-
* @param _subgraphMetadata IPFS hash for the subgraph metadata
246-
*/
247-
function _updateSubgraphMetadata(uint256 _subgraphID, bytes32 _subgraphMetadata) internal {
248-
_setSubgraphURI(_subgraphID, string(abi.encodePacked(_subgraphMetadata)));
249-
emit SubgraphMetadataUpdated(_subgraphID, _subgraphMetadata);
239+
_setSubgraphMetadata(_subgraphID, _subgraphMetadata);
250240
}
251241

252242
/**
@@ -276,7 +266,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
276266
emit SubgraphPublished(subgraphID, _subgraphDeploymentID, defaultReserveRatio);
277267

278268
// Set the token metadata
279-
_updateSubgraphMetadata(subgraphID, _subgraphMetadata);
269+
_setSubgraphMetadata(subgraphID, _subgraphMetadata);
280270

281271
emit SubgraphVersionUpdated(subgraphID, _subgraphDeploymentID, _versionMetadata);
282272
}
@@ -706,7 +696,7 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
706696
emit LegacySubgraphClaimed(_graphAccount, _subgraphNumber);
707697

708698
// Set the token metadata
709-
_updateSubgraphMetadata(subgraphID, _subgraphMetadata);
699+
_setSubgraphMetadata(subgraphID, _subgraphMetadata);
710700
}
711701

712702
/**
@@ -789,19 +779,42 @@ contract GNS is GNSV2Storage, GraphUpgradeable, IGNS, Multicall {
789779

790780
// -- NFT --
791781

782+
/**
783+
* @dev Return the owner of a subgraph.
784+
* @param _tokenID Subgraph ID
785+
* @return Owner address
786+
*/
792787
function ownerOf(uint256 _tokenID) public view override returns (address) {
793788
return subgraphNFT.ownerOf(_tokenID);
794789
}
795790

791+
/**
792+
* @dev Mint the NFT for the subgraph.
793+
* @param _owner Owner address
794+
* @param _tokenID Subgraph ID
795+
*/
796796
function _mintNFT(address _owner, uint256 _tokenID) internal {
797797
subgraphNFT.mint(_owner, _tokenID);
798798
}
799799

800+
/**
801+
* @dev Burn the NFT for the subgraph.
802+
* @param _tokenID Subgraph ID
803+
*/
800804
function _burnNFT(uint256 _tokenID) internal {
801805
subgraphNFT.burn(_tokenID);
802806
}
803807

804-
function _setSubgraphURI(uint256 _tokenID, string memory _subgraphURI) internal {
805-
subgraphNFT.setSubgraphURI(_tokenID, _subgraphURI);
808+
/**
809+
* @dev Set the subgraph metadata.
810+
* @param _tokenID Subgraph ID
811+
* @param _subgraphMetadata IPFS hash of the subgraph metadata
812+
*/
813+
function _setSubgraphMetadata(uint256 _tokenID, bytes32 _subgraphMetadata) internal {
814+
subgraphNFT.setSubgraphMetadata(_tokenID, _subgraphMetadata);
815+
816+
// Even if the following event is emitted in the NFT we emit it here to facilitate
817+
// subgraph indexing
818+
emit SubgraphMetadataUpdated(_tokenID, _subgraphMetadata);
806819
}
807820
}

contracts/discovery/ISubgraphNFT.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface ISubgraphNFT is IERC721 {
1919

2020
function burn(uint256 _tokenId) external;
2121

22-
function setSubgraphURI(uint256 _tokenId, string memory _subgraphURI) external;
22+
function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata) external;
2323

2424
function tokenURI(uint256 _tokenId) external view returns (string memory);
2525
}

contracts/discovery/ISubgraphNFTDescriptor.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ pragma solidity ^0.7.6;
55
/// @title Describes subgraph NFT tokens via URI
66
interface ISubgraphNFTDescriptor {
77
/// @notice Produces the URI describing a particular token ID for a Subgraph
8-
/// @dev Note this URI may be a data: URI with the JSON contents directly inlined
8+
/// @dev Note this URI may be data: URI with the JSON contents directly inlined
99
/// @param _minter Address of the allowed minter
1010
/// @param _tokenId The ID of the subgraph NFT for which to produce a description, which may not be valid
11+
/// @param _baseURI The base URI that could be prefixed to the final URI
12+
/// @param _subgraphMetadata Subgraph metadata set for the subgraph
1113
/// @return The URI of the ERC721-compliant metadata
1214
function tokenURI(
1315
address _minter,
1416
uint256 _tokenId,
1517
string calldata _baseURI,
16-
string calldata _tokenURI
18+
bytes32 _subgraphMetadata
1719
) external view returns (string memory);
1820
}

contracts/discovery/SubgraphNFT.sol

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,25 @@ pragma solidity ^0.7.6;
44

55
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
66
import "@openzeppelin/contracts/utils/Address.sol";
7-
import "@openzeppelin/contracts/utils/Strings.sol";
87

98
import "../governance/Governed.sol";
9+
import "../libraries/HexStrings.sol";
1010
import "./ISubgraphNFT.sol";
1111
import "./ISubgraphNFTDescriptor.sol";
1212

1313
/// @title NFT that represents ownership of a Subgraph
1414
contract SubgraphNFT is Governed, ERC721, ISubgraphNFT {
15-
using Strings for uint256;
16-
1715
// -- State --
1816

1917
address public minter;
2018
ISubgraphNFTDescriptor public tokenDescriptor;
21-
mapping(uint256 => string) private _subgraphURIs;
19+
mapping(uint256 => bytes32) private _subgraphMetadataHashes;
2220

2321
// -- Events --
2422

2523
event MinterUpdated(address minter);
2624
event TokenDescriptorUpdated(address tokenDescriptor);
27-
event SubgraphURIUpdated(uint256 indexed tokenID, string subgraphURI);
25+
event SubgraphMetadataUpdated(uint256 indexed tokenID, bytes32 subgraphURI);
2826

2927
// -- Modifiers --
3028

@@ -111,18 +109,19 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT {
111109
}
112110

113111
/**
114-
* @notice Set the URI for a subgraph represented by `_tokenId`.
112+
* @notice Set the metadata for a subgraph represented by `_tokenId`.
115113
* @dev `_tokenId` must exist.
116114
* @param _tokenId ID of the NFT
115+
* @param _subgraphMetadata IPFS hash for the metadata
117116
*/
118-
function setSubgraphURI(uint256 _tokenId, string memory _subgraphURI)
117+
function setSubgraphMetadata(uint256 _tokenId, bytes32 _subgraphMetadata)
119118
external
120119
override
121120
onlyMinter
122121
{
123122
require(_exists(_tokenId), "ERC721Metadata: URI set of nonexistent token");
124-
_subgraphURIs[_tokenId] = _subgraphURI;
125-
emit SubgraphURIUpdated(_tokenId, _subgraphURI);
123+
_subgraphMetadataHashes[_tokenId] = _subgraphMetadata;
124+
emit SubgraphMetadataUpdated(_tokenId, _subgraphMetadata);
126125
}
127126

128127
// -- NFT display --
@@ -137,13 +136,21 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT {
137136
require(_exists(_tokenId), "ERC721Metadata: URI query for nonexistent token");
138137

139138
// Delegates rendering of the metadata to the token descriptor if existing
140-
// This allows for some flexibility in adapting the metadata
139+
// This allows for some flexibility in adapting the token URI
141140
if (address(tokenDescriptor) != address(0)) {
142-
return tokenDescriptor.tokenURI(minter, _tokenId, baseURI(), _subgraphURIs[_tokenId]);
141+
return
142+
tokenDescriptor.tokenURI(
143+
minter,
144+
_tokenId,
145+
baseURI(),
146+
_subgraphMetadataHashes[_tokenId]
147+
);
143148
}
144149

145-
// Default metadata
146-
string memory _subgraphURI = _subgraphURIs[_tokenId];
150+
// Default token URI
151+
uint256 metadata = uint256(_subgraphMetadataHashes[_tokenId]);
152+
153+
string memory _subgraphURI = metadata > 0 ? HexStrings.toString(metadata) : "";
147154
string memory base = baseURI();
148155

149156
// If there is no base URI, return the token URI.
@@ -155,6 +162,6 @@ contract SubgraphNFT is Governed, ERC721, ISubgraphNFT {
155162
return string(abi.encodePacked(base, _subgraphURI));
156163
}
157164
// If there is a baseURI but no tokenURI, concatenate the tokenID to the baseURI.
158-
return string(abi.encodePacked(base, _tokenId.toString()));
165+
return string(abi.encodePacked(base, HexStrings.toString(_tokenId)));
159166
}
160167
}

contracts/discovery/SubgraphNFTDescriptor.sol

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,24 @@
22

33
pragma solidity ^0.7.6;
44

5+
import "../libraries/Base58Encoder.sol";
56
import "./ISubgraphNFTDescriptor.sol";
67

78
/// @title Describes subgraph NFT tokens via URI
89
contract SubgraphNFTDescriptor is ISubgraphNFTDescriptor {
910
/// @inheritdoc ISubgraphNFTDescriptor
1011
function tokenURI(
11-
address _minter,
12-
uint256 _tokenId,
12+
address, /* _minter */
13+
uint256, /* _tokenId */
1314
string calldata _baseURI,
14-
string calldata _subgraphURI
15-
) external view override returns (string memory) {
16-
return string(abi.encodePacked(_baseURI, _subgraphURI));
15+
bytes32 _subgraphMetadata
16+
) external pure override returns (string memory) {
17+
bytes memory b58 = Base58Encoder.encode(
18+
abi.encodePacked(Base58Encoder.sha256MultiHash, _subgraphMetadata)
19+
);
20+
if (bytes(_baseURI).length == 0) {
21+
return string(b58);
22+
}
23+
return string(abi.encodePacked(_baseURI, b58));
1724
}
1825
}

contracts/libraries/Base58Encoder.sol

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6;
4+
5+
/// @title Base58Encoder
6+
/// @author Original author - Martin Lundfall ([email protected])
7+
/// Based on https://github.com/MrChico/verifyIPFS
8+
library Base58Encoder {
9+
bytes constant sha256MultiHash = hex"1220";
10+
bytes constant ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11+
12+
/// @dev Converts hex string to base 58
13+
function encode(bytes memory source) internal pure returns (bytes memory) {
14+
if (source.length == 0) return new bytes(0);
15+
uint8[] memory digits = new uint8[](64);
16+
digits[0] = 0;
17+
uint8 digitlength = 1;
18+
for (uint256 i = 0; i < source.length; ++i) {
19+
uint256 carry = uint8(source[i]);
20+
for (uint256 j = 0; j < digitlength; ++j) {
21+
carry += uint256(digits[j]) * 256;
22+
digits[j] = uint8(carry % 58);
23+
carry = carry / 58;
24+
}
25+
26+
while (carry > 0) {
27+
digits[digitlength] = uint8(carry % 58);
28+
digitlength++;
29+
carry = carry / 58;
30+
}
31+
}
32+
return toAlphabet(reverse(truncate(digits, digitlength)));
33+
}
34+
35+
function truncate(uint8[] memory array, uint8 length) internal pure returns (uint8[] memory) {
36+
uint8[] memory output = new uint8[](length);
37+
for (uint256 i = 0; i < length; i++) {
38+
output[i] = array[i];
39+
}
40+
return output;
41+
}
42+
43+
function reverse(uint8[] memory input) internal pure returns (uint8[] memory) {
44+
uint8[] memory output = new uint8[](input.length);
45+
for (uint256 i = 0; i < input.length; i++) {
46+
output[i] = input[input.length - 1 - i];
47+
}
48+
return output;
49+
}
50+
51+
function toAlphabet(uint8[] memory indices) internal pure returns (bytes memory) {
52+
bytes memory output = new bytes(indices.length);
53+
for (uint256 i = 0; i < indices.length; i++) {
54+
output[i] = ALPHABET[indices[i]];
55+
}
56+
return output;
57+
}
58+
}

contracts/libraries/HexStrings.sol

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6;
4+
5+
/// @title HexStrings
6+
/// Based on https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8dd744fc1843d285c38e54e9d439dea7f6b93495/contracts/utils/Strings.sol
7+
library HexStrings {
8+
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
9+
10+
/// @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
11+
function toString(uint256 value) internal pure returns (string memory) {
12+
if (value == 0) {
13+
return "0x00";
14+
}
15+
uint256 temp = value;
16+
uint256 length = 0;
17+
while (temp != 0) {
18+
length++;
19+
temp >>= 8;
20+
}
21+
return toHexString(value, length);
22+
}
23+
24+
/// @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
25+
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
26+
bytes memory buffer = new bytes(2 * length + 2);
27+
buffer[0] = "0";
28+
buffer[1] = "x";
29+
for (uint256 i = 2 * length + 1; i > 1; --i) {
30+
buffer[i] = _HEX_SYMBOLS[value & 0xf];
31+
value >>= 4;
32+
}
33+
require(value == 0, "Strings: hex length insufficient");
34+
return string(buffer);
35+
}
36+
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"@typescript-eslint/typescript-estree": "^4.0.0",
4444
"@urql/core": "^2.1.3",
4545
"axios": "^0.21.1",
46-
"base64-sol": "^1.1.0",
4746
"bignumber.js": "^9.0.0",
4847
"chai": "^4.3.4",
4948
"cli-table": "^0.3.6",

0 commit comments

Comments
 (0)