Last active
October 6, 2022 14:32
-
-
Save ngmachado/4e1349385fc239008761df8d19d2cdb4 to your computer and use it in GitHub Desktop.
CFA Based NFT (onchain SVG)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.8.0; | |
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; | |
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; | |
import {IConstantFlowAgreementV1} from "@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol"; | |
import { ISuperfluid, ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; | |
/* | |
Super721SVGOnChain - Generate NFT based on SF CFA Stream | |
*/ | |
contract Super721SVGOnChain { | |
using Strings for uint256; | |
// Superfluid struct that we get from hook - remove after SF integration | |
struct CFAHookParams { | |
address sender; | |
address receiver; | |
address flowOperator; | |
int96 flowRate; | |
} | |
struct StreamData { | |
address token; | |
address sender; | |
} | |
error NOT_ALLOWED(); | |
error ALREADY_MINTED(); | |
error NOT_MINTED(); | |
error ZERO_ADDRESS(); | |
error EMPTY_DATA(); | |
error NOT_STREAM_USER(); | |
event Transfer(address indexed from, address indexed to, uint256 indexed id); | |
string public name; | |
string public symbol; | |
IConstantFlowAgreementV1 public cfaV1; | |
uint256 private _tokenIds; | |
mapping(uint256 => StreamData) internal _tokenCFAData; | |
mapping(bytes32 => uint256) internal _revertStreamToId; | |
mapping(uint256 => address) internal _ownerOf; | |
function tokenURI(uint256 id) public view returns (string memory) { | |
StreamData memory stream = _tokenCFAData[id]; | |
address receiver = ownerOf(id); | |
int96 flowRate = _getFlowRate(stream.token, stream.sender, receiver); | |
bytes memory dataURI = abi.encodePacked( | |
'{', | |
'"name": "CFA NFT: ', id.toString(), '",', | |
'"description": "Represents a stream on chain",', | |
'"image": "', _generateSVG(stream.token, flowRate, stream.sender, receiver), '"', | |
'}' | |
); | |
return string( | |
abi.encodePacked( | |
"data:application/json;base64,", | |
Base64.encode(dataURI) | |
) | |
); | |
} | |
function _generateSVG( | |
address token, | |
int96 flowRate, | |
address sender, | |
address receiver | |
) internal view returns(string memory) { | |
bytes memory svg = abi.encodePacked( | |
'<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">', | |
'<style>.base { fill: white; font-family: serif; font-size: 14px; }</style>', | |
'<rect width="100%" height="100%" fill="black" />', | |
'<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">', "Token: ", Strings.toHexString(uint256(uint160(token)), 20),'</text>', | |
'<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">', "Sender: ", Strings.toHexString(uint256(uint160(sender)), 20),'</text>', | |
'<text x="50%" y="60%" class="base" dominant-baseline="middle" text-anchor="middle">', "Receiver: ", Strings.toHexString(uint256(uint160(receiver)), 20),'</text>', | |
'<text x="50%" y="70%" class="base" dominant-baseline="middle" text-anchor="middle">', "FlowRate: ", uint256(uint96(flowRate)).toString(),'</text>', | |
'</svg>' | |
); | |
return string( | |
abi.encodePacked( | |
"data:image/svg+xml;base64,", | |
Base64.encode(svg) | |
) | |
); | |
} | |
constructor(address cfa, string memory _name, string memory _symbol) { | |
cfaV1 = IConstantFlowAgreementV1(cfa); | |
name = _name; | |
symbol = _symbol; | |
} | |
function ownerOf(uint256 id) public view virtual returns (address owner) { | |
owner = _ownerOf[id]; | |
if(owner == address(0)) revert NOT_MINTED(); | |
} | |
function balanceOf(address owner) public view virtual returns (uint256) { | |
return 1; | |
} | |
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { | |
return | |
interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 | |
interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 | |
interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata | |
} | |
//Mock until SF integration | |
function onCreate(CFAHookParams memory newFlowData, address token) public returns(bool) { | |
_tokenIds++; | |
_mint(_tokenIds, token, newFlowData.sender, newFlowData.receiver); | |
return true; | |
} | |
function onUpdate(CFAHookParams memory updatedFlowData, address token, int96 oldFlowRate) public returns(bool) { | |
return true; | |
} | |
function onDelete(CFAHookParams memory updatedFlowData, address token, int96 oldFlowRate) public returns(bool) { | |
_burn(token, updatedFlowData.sender, updatedFlowData.receiver); | |
return true; | |
} | |
function _mint(uint256 id, address token, address sender, address receiver) internal { | |
if(receiver == address(0)) revert ZERO_ADDRESS(); | |
bytes32 reverseKey = keccak256(abi.encodePacked( | |
token, | |
sender, | |
receiver | |
)); | |
if(_revertStreamToId[reverseKey] != 0) revert ALREADY_MINTED(); | |
if(_ownerOf[id] != address(0)) revert ALREADY_MINTED(); | |
_ownerOf[id] = receiver; | |
_tokenCFAData[id] = StreamData(token, sender); | |
_revertStreamToId[reverseKey] = id; | |
emit Transfer(address(0), receiver, id); | |
} | |
function _burn(address token, address sender, address receiver) internal { | |
bytes32 reverseKey = keccak256(abi.encodePacked(token, sender, receiver)); | |
uint256 id = _revertStreamToId[reverseKey]; | |
if(id == 0) revert NOT_MINTED(); | |
delete _ownerOf[id]; | |
delete _tokenCFAData[id]; | |
delete _revertStreamToId[reverseKey]; | |
emit Transfer(receiver, address(0), id); | |
} | |
function mint(address token, address sender, address receiver) public { | |
int96 flowRate = _getFlowRate(token, sender, receiver); | |
if(flowRate > 0) { | |
_tokenIds++; | |
_mint(_tokenIds, token, sender, receiver); | |
} | |
} | |
function burn(address token, address sender, address receiver) public { | |
if(msg.sender != receiver) revert NOT_STREAM_USER(); | |
int96 flowRate = _getFlowRate(token, sender, receiver); | |
if(flowRate == 0) { | |
_burn(token, sender, receiver); | |
} | |
} | |
function approve(address spender, uint256 id) public { | |
revert NOT_ALLOWED(); | |
} | |
function setApprovalForAll(address operator, bool approved) public { | |
revert NOT_ALLOWED(); | |
} | |
function transferFrom(address from,address to,uint256 id) public { | |
revert NOT_ALLOWED(); | |
} | |
function safeTransferFrom(address from,address to,uint256 id) public { | |
revert NOT_ALLOWED(); | |
} | |
function safeTransferFrom(address from, address to, uint256 id,bytes calldata data) public { | |
revert NOT_ALLOWED(); | |
} | |
function _getFlowRate(address token, address sender, address receiver) internal view returns(int96 flowRate) { | |
(,flowRate,,) = cfaV1.getFlow(ISuperToken(token), sender, receiver); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment