Skip to content

Commit 49042f2

Browse files
nachomazzarafrangio
authored andcommitted
feat: add baseTokenURI to ERC721Metadata (#1970)
* feat: add baseTokenURI * fix: tests * chore: dev notation * chore: changelog * chore: typo * Remove extra getters, return empty URI by default * Update docs * Rename baseTokenURI to baseURI * Roll back visibility change of tokenURI * Update changelog entry * Version setBaseURI docs * Improve internal names and comments * Fix compilation errors * Add an external getter for baseURI
1 parent 714f13d commit 49042f2

File tree

4 files changed

+108
-31
lines changed

4 files changed

+108
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### New features
66
* `SafeCast.toUintXX`: new library for integer downcasting, which allows for safe operation on smaller types (e.g. `uint32`) when combined with `SafeMath`. ([#1926](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1926))
7+
* `ERC721Metadata`: added `baseURI`, which can be used for dramatic gas savings when all token URIs share a prefix (e.g. `http://api.myapp.com/tokens/<id>`). ([#1970](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1970))
78

89
### Improvements
910
* `ERC777`: `_burn` is now internal, providing more flexibility and making it easier to create tokens that deflate. ([#1908](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1908))

contracts/mocks/ERC721FullMock.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,8 @@ contract ERC721FullMock is ERC721Full, ERC721Mintable, ERC721MetadataMintable, E
2626
function setTokenURI(uint256 tokenId, string memory uri) public {
2727
_setTokenURI(tokenId, uri);
2828
}
29+
30+
function setBaseURI(string memory baseURI) public {
31+
_setBaseURI(baseURI);
32+
}
2933
}

contracts/token/ERC721/ERC721Metadata.sol

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ contract ERC721Metadata is Context, ERC165, ERC721, IERC721Metadata {
1212
// Token symbol
1313
string private _symbol;
1414

15+
// Base URI
16+
string private _baseURI;
17+
1518
// Optional mapping for token URIs
1619
mapping(uint256 => string) private _tokenURIs;
1720

@@ -52,24 +55,58 @@ contract ERC721Metadata is Context, ERC165, ERC721, IERC721Metadata {
5255
}
5356

5457
/**
55-
* @dev Returns an URI for a given token ID.
56-
* Throws if the token ID does not exist. May return an empty string.
57-
* @param tokenId uint256 ID of the token to query
58+
* @dev Returns the URI for a given token ID. May return an empty string.
59+
*
60+
* If the token's URI is non-empty and a base URI was set (via
61+
* {_setBaseURI}), it will be added to the token ID's URI as a prefix.
62+
*
63+
* Reverts if the token ID does not exist.
5864
*/
5965
function tokenURI(uint256 tokenId) external view returns (string memory) {
6066
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
61-
return _tokenURIs[tokenId];
67+
68+
string memory _tokenURI = _tokenURIs[tokenId];
69+
70+
// Even if there is a base URI, it is only appended to non-empty token-specific URIs
71+
if (bytes(_tokenURI).length == 0) {
72+
return "";
73+
} else {
74+
// abi.encodePacked is being used to concatenate strings
75+
return string(abi.encodePacked(_baseURI, _tokenURI));
76+
}
77+
}
78+
79+
/**
80+
* @dev Returns the base URI set via {_setBaseURI}. This will be
81+
* automatically added as a preffix in {tokenURI} to each token's URI, when
82+
* they are non-empty.
83+
*/
84+
function baseURI() external view returns (string memory) {
85+
return _baseURI;
6286
}
6387

6488
/**
6589
* @dev Internal function to set the token URI for a given token.
90+
*
6691
* Reverts if the token ID does not exist.
67-
* @param tokenId uint256 ID of the token to set its URI
68-
* @param uri string URI to assign
92+
*
93+
* TIP: if all token IDs share a prefix (e.g. if your URIs look like
94+
* `http://api.myproject.com/token/<id>`), use {_setBaseURI} to store
95+
* it and save gas.
6996
*/
70-
function _setTokenURI(uint256 tokenId, string memory uri) internal {
97+
function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal {
7198
require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token");
72-
_tokenURIs[tokenId] = uri;
99+
_tokenURIs[tokenId] = _tokenURI;
100+
}
101+
102+
/**
103+
* @dev Internal function to set the base URI for all token IDs. It is
104+
* automatically added as a prefix to the value returned in {tokenURI}.
105+
*
106+
* _Available since v2.5.0._
107+
*/
108+
function _setBaseURI(string memory baseURI) internal {
109+
_baseURI = baseURI;
73110
}
74111

75112
/**

test/token/ERC721/ERC721Full.test.js

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,6 @@ contract('ERC721Full', function ([
7272
});
7373

7474
describe('metadata', function () {
75-
const sampleUri = 'mock://mytoken';
76-
7775
it('has a name', async function () {
7876
expect(await this.token.name()).to.be.equal(name);
7977
});
@@ -82,31 +80,68 @@ contract('ERC721Full', function ([
8280
expect(await this.token.symbol()).to.be.equal(symbol);
8381
});
8482

85-
it('sets and returns metadata for a token id', async function () {
86-
await this.token.setTokenURI(firstTokenId, sampleUri);
87-
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri);
88-
});
83+
describe('token URI', function () {
84+
const baseURI = 'https://api.com/v1/';
85+
const sampleUri = 'mock://mytoken';
8986

90-
it('reverts when setting metadata for non existent token id', async function () {
91-
await expectRevert(
92-
this.token.setTokenURI(nonExistentTokenId, sampleUri), 'ERC721Metadata: URI set of nonexistent token'
93-
);
94-
});
87+
it('it is empty by default', async function () {
88+
expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
89+
});
9590

96-
it('can burn token with metadata', async function () {
97-
await this.token.setTokenURI(firstTokenId, sampleUri);
98-
await this.token.burn(firstTokenId, { from: owner });
99-
expect(await this.token.exists(firstTokenId)).to.equal(false);
100-
});
91+
it('reverts when queried for non existent token id', async function () {
92+
await expectRevert(
93+
this.token.tokenURI(nonExistentTokenId), 'ERC721Metadata: URI query for nonexistent token'
94+
);
95+
});
10196

102-
it('returns empty metadata for token', async function () {
103-
expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
104-
});
97+
it('can be set for a token id', async function () {
98+
await this.token.setTokenURI(firstTokenId, sampleUri);
99+
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri);
100+
});
105101

106-
it('reverts when querying metadata for non existent token id', async function () {
107-
await expectRevert(
108-
this.token.tokenURI(nonExistentTokenId), 'ERC721Metadata: URI query for nonexistent token'
109-
);
102+
it('reverts when setting for non existent token id', async function () {
103+
await expectRevert(
104+
this.token.setTokenURI(nonExistentTokenId, sampleUri), 'ERC721Metadata: URI set of nonexistent token'
105+
);
106+
});
107+
108+
it('base URI can be set', async function () {
109+
await this.token.setBaseURI(baseURI);
110+
expect(await this.token.baseURI()).to.equal(baseURI);
111+
});
112+
113+
it('base URI is added as a prefix to the token URI', async function () {
114+
await this.token.setBaseURI(baseURI);
115+
await this.token.setTokenURI(firstTokenId, sampleUri);
116+
117+
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + sampleUri);
118+
});
119+
120+
it('token URI can be changed by changing the base URI', async function () {
121+
await this.token.setBaseURI(baseURI);
122+
await this.token.setTokenURI(firstTokenId, sampleUri);
123+
124+
const newBaseURI = 'https://api.com/v2/';
125+
await this.token.setBaseURI(newBaseURI);
126+
expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + sampleUri);
127+
});
128+
129+
it('token URI is empty for tokens with no URI but with base URI', async function () {
130+
await this.token.setBaseURI(baseURI);
131+
132+
expect(await this.token.tokenURI(firstTokenId)).to.be.equal('');
133+
});
134+
135+
it('tokens with URI can be burnt ', async function () {
136+
await this.token.setTokenURI(firstTokenId, sampleUri);
137+
138+
await this.token.burn(firstTokenId, { from: owner });
139+
140+
expect(await this.token.exists(firstTokenId)).to.equal(false);
141+
await expectRevert(
142+
this.token.tokenURI(firstTokenId), 'ERC721Metadata: URI query for nonexistent token'
143+
);
144+
});
110145
});
111146
});
112147

0 commit comments

Comments
 (0)