FundingWorks
CampaignsActivityHow it works
New campaign
FundingWorks
CampaignsActivityHow it works
New campaign
FundingWorks
  • Campaigns
  • Activity
  • How it works
  • X
OverviewSupportStreamExitLaunchPass artContracts
How it works

Pass art

Markdown

What 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.

  1. Deploy a contract with tokenURI(uint256) that returns the metadata, like the example below.
  2. 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.
  3. To go back to the IPFS metadata, call setTokenRenderer with 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.

PreviousLaunchNextContracts