Pass art
MarkdownWhat each pass shows in wallets and marketplaces. One image is the same for every pass. An IPFS folder gives each pass its own file: pass 1 reads folder/1, pass 2 reads folder/2. A contract builds each pass's metadata itself, so it can change over time.
Dynamic passes
A pass can change with the campaign. Point the campaign at a renderer contract and every pass's tokenURI comes from it, read live from the chain.
The renderer can read what each pass holds: getRemainingLockedEth(tokenId) is what hasn't streamed yet, initialLockedPerToken is where it started, and vestingStartTime, vestingEndDate and vestingComplete place it in the stream. ownerOf(tokenId) is the holder.
- Deploy a contract with
tokenURI(uint256)that returns the metadata, like the example below. - Pick Contract under Pass art when you create the campaign. Later, the owner or manager can change or remove it under Settings on the campaign page, which calls
setTokenRenderer. - To go back to the IPFS metadata, call
setTokenRendererwith the zero address.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
interface IFundingWorks {
function initialLockedPerToken() external view returns (uint256);
function getRemainingLockedEth(uint256 tokenId) external view returns (uint256);
}
/// Each pass fills up as its ETH streams to the creator.
contract StreamRenderer {
using Strings for uint256;
IFundingWorks public immutable campaign;
constructor(IFundingWorks _campaign) {
campaign = _campaign;
}
function tokenURI(uint256 tokenId) external view returns (string memory) {
uint256 start = campaign.initialLockedPerToken();
uint256 left = campaign.getRemainingLockedEth(tokenId);
uint256 streamed = start == 0 ? 100 : 100 - (left * 100) / start;
string memory svg = string.concat(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>",
"<rect width='100' height='100' fill='black'/>",
"<rect y='", (100 - streamed).toString(), "' width='100' height='",
streamed.toString(), "' fill='rgb(34,197,94)'/></svg>"
);
string memory json = string.concat(
'{"name":"Pass #', tokenId.toString(),
'","attributes":[{"trait_type":"Streamed","value":', streamed.toString(),
'}],"image":"data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}'
);
return string.concat("data:application/json;base64,", Base64.encode(bytes(json)));
}
}The campaign doesn't announce metadata changes, so marketplaces may show a cached image until they refresh.