User Data
Including additional metadata in your Super Apps
Another powerful component of the Superfluid protocol is the ability to pass in additional user data along with your calls to super agreements. Think of it like metadata that can accompany your streams or IDAs 😎
Before we look at user data, let's take a quick dive into a new element: Context.
Context is used for several key items within the Superfluid protocol such as gas optimization, governance, security, and SuperApp callbacks. One parameter that's also available for use within the context field is userData.
This is from the host interface (ISuperfluid.sol) file inside of our interfaces folder in the Superfluid repo. On line 21, we see userData.
1
​
2
struct Context {
3
//
4
// Call context
5
//
6
// callback level
7
uint8 appLevel;
8
// type of call
9
uint8 callType;
10
// the system timestsamp
11
uint256 timestamp;
12
// The intended message sender for the call
13
address msgSender;
14
​
15
//
16
// Callback context
17
//
18
// For callbacks it is used to know which agreement function selector is called
19
bytes4 agreementSelector;
20
// User provided data for app callbacks
21
bytes userData;
22
​
23
//
24
// App context
25
//
26
// app allowance granted
27
uint256 appAllowanceGranted;
28
// app allowance wanted by the app callback
29
uint256 appAllowanceWanted;
30
// app allowance used, allowing negative values over a callback session
31
int256 appAllowanceUsed;
32
// app address
33
address appAddress;
34
// app allowance in super token
35
ISuperfluidToken appAllowanceToken;
36
}
37
​
Copied!
Whenever you see ctx being moved around within the protocol, this struct is what's under the hood (it's just compiled down to bytes each time it's passed between functions).
As you can see, userData is one of the elements that makes up Context. For the sake of this tutorial, we're going to focus exclusively on userData for the time being.

Quick Review: How Are Super Agreements Called Again?

To call a function in a Super Agreement, you first need to use abi.encode to compile a function call to the super agreement you're looking to trigger. Then, you need to pass the agreement type, the bytecode of the previously compiled function call, and userData to callAgreement (we'll get to userData next). The whole process looks like this:
1
//solidity
2
//Matic Addresses for host and cfa
3
​
4
ISuperfluid host = "0x3E14dC1b13c488a8d5D310918780c983bD5982E7";
5
IConstantFlowAgreementV1 cfa = "0x6EeE6060f715257b970700bc2656De21dEdF074C";
6
//DAIx
7
ISuperToken acceptedToken = "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063";
8
//empty user data
9
bytes userData = "0x";
10
​
11
//$1000 DAI per month
12
int96 flowRate = "385802469135802";
13
​
14
//receiver is arbitrary
15
address receiver = "0x...";
16
​
17
host.callAgreement(
18
cfa,
19
abi.encodeWithSelector(
20
cfa.createFlow.selector,
21
acceptedToken,
22
receiver
23
flowRate,
24
new bytes(0) // placeholder
25
),
26
userData,
27
);
Copied!
Note: userData is always passed into callAgreement as type bytes .
1
//solidity
2
//call agreement interface
3
function callAgreement(
4
ISuperAgreement agreementClass,
5
bytes calldata callData,
6
bytes calldata userData
7
)
8
external
9
//cleanCtx
10
returns(bytes memory returnedData);
Copied!
Behind the scenes, your userData variable is appended onto Context, which is then available to you as a developer in the SuperApp callbacks.
When you execute an operation in the CFA contract for example (and create, update, or delete a flow), you'll have access to the Context that's available after the initial call to the protocol was made. For example, if I pass in userData when a create a flow into a Super App, I can decode the context & this user data inside any of the super app callbacks, and re-use or manipulate this data as I please. For example, if I send a transaction where receiver is a SuperApp, and pass along an encoded string 'hello sir' as userData:
1
//solidity
2
string unformattedUserData = 'hello sir';
3
bytes userData = abi.encode(unformattedUserData);
4
​
5
​
6
host.callAgreement(
7
cfa,
8
abi.encodeWithSelector(
9
cfa.createFlow.selector,
10
acceptedToken,
11
//receiver is a super app...
12
receiver
13
flowRate,
14
new bytes(0) // placeholder
15
),
16
userData,
17
);
Copied!
I can decode the context that's passed into the callback, which will give me the Context struct displayed above. Then, since userData is one of the fields on the struct, we can abi.decode userData get back my value of 'hello sir' on the other side:
1
//inside of the afterAgreementCreated Super App Callback
2
​
3
function afterAgreementCreated(
4
ISuperToken _superToken,
5
address _agreementClass,
6
bytes32, // _agreementId,
7
bytes calldata /*_agreementData*/,
8
bytes calldata ,// _cbdata,
9
bytes calldata _ctx
10
)
11
external override
12
onlyExpected(_superToken, _agreementClass)
13
onlyHost
14
returns (bytes memory newCtx)
15
{
16
17
// decode Contex - this will return the entire Context struct
18
ISuperfluid.Context memory decompiledContext = _host.decodeCtx(_ctx);
19
​
20
//userData is a one of the fields on the Context struct
21
//set decodedUserData variable to decoded value
22
​
23
//this will return 'hello sir'
24
decodedUserData = abi.decode(decompiledContext.userData, (string));
25
26
//do some stuff with your decodedUserData
27
return _doSomeStuff(decodedUserData);
28
}
Copied!
UserData can be any arbitrary piece of data. Think of it as metadata that's associated with anything you do in a Super Agreement.
This metadata could be used for a wide variety of use cases:
  • You could pass in data to accompany a salary or payment stream - perhaps employee info or product info
  • You can send a message along with your distribution in an instant distribution agreement
  • You could even pass in the bytecode for another entire smart contract.
We invite you to be creative with this!
Next up: a tutorial on how to leverage UserData within your applications.
​

Build an NFT Billboard with Superfluid UserData

In this tutorial, we make a small tweak to the contracts used in the TradeableCashflow Super App to test out Superfluid's userData parameters. We set up a Scaffold-eth based repo, remove (comment out) some of the extra front end stuff, and then create a few scripts to allow us to easily create, read, update, and delete flows (we love CRUD).
NOTE: We recommend reading through our Super Apps tutorial before completing this tutorial.
Our dapp will turn the TradeableCashflow into a tradeable NFT billboard that can be rented with streams. The message displayed on our billboard will be the parameter passed in as userData. If the billboard is traded, all rental cashflows will be redirected toward the new owner.
This tutorial is most certainly NOT investment advice, but we'll assume that someone out there will want others to HODL their favorite assets...a decent use case for a billboard
You can follow along with the video version of this tutorial on Youtube, and fork the repo here as well:
protocol-monorepo/examples/nftbillboard-userdata at dev Β· superfluid-finance/protocol-monorepo
GitHub
Before we get started with project setup, you'll also want to head over to the Superfluid Dashboard to claim testnet Super DAI (fDAIx) in at least one account on the network you'd like to use for the tutorial (I would suggest claiming these tokens on Mumbai, the Matic Testnet). To claim test DAI, you can head to the currencies tab in the dashboard and click the plus button on the far right in the DAI row to get your hands on some test Super DAIx.
You'll want to have 2 Ethereum addresses ready: one of which you'll need your private key for (that has our test tokens), the other which we'll just be observing.
​

Scaffold-Eth and Hardhat Configuration

To get your URL, click on your project, select 'View Key' and copy the HTTP URL.
You'll want to create a new .env file and put this URL there - in my case I've called it MUMBAI_ALCHEMY_URL.
I've also gotten the private key associated with the address I'll be using to deploy contracts and create flows. I've set this as an environment variable as well called MUMBAI_DEPLOYER_PRIV_KEY.
NOTE: be VERY careful with your private keys. Do not push them to github or share them publicly. If you need help locating your keys within metamask, you can click on 'Account Details' in the menu and enter your password to 'Export Private Key.'
The standard hardhat.config file will have quite a few network options to choose from. However, in our case, I need to change my default network to "polytest," and include my Alchemy URL and keys under 'accounts' so that hardhat can use my account as a signer when deploying contracts.
Note: if you choose to use a different testnet, you'll need to change the default network to network you wish to use.
1
//inside of hardhat.config on line 29
2
​
3
// Select the network you want to deploy to here:
4
//
5
//using polytest (mumbai)
6
const defaultNetwork = "polytest";
Copied!
Next, scroll down to 'polytest' in the list of potential networks that Scaffold-eth lists in hardhat.config. Add in your URL from Alchemy in 'url' and your private key in 'accounts.' Note: you'll need to prepend '0x' onto your private key. Again - these should be saved as environment variables - don't openly share your private key!
Note: if you choose to use a different testnet, you'll need to add this data for the network you wish to use (i.e. goerli, ropsten, etc.)
1
//inside of hardhat.config on line 163
2
polytest: {
3
url: `${process.env.MUMBAI_ALCHEMY_URL}`,// using alchemy instead of moralis. add your own URL in .env
4
gasPrice: 1000000000,
5
accounts: [`0x${process.env.MUMBAI_DEPLOYER_PRIV_KEY}`]
6
},
7
},
Copied!
Finally, we need to adjust our solidity compiler so that it's compatible with our project.
1
//in hardhat.config on line 280
2
solidity: {
3
compilers: [
4
{
5
//set compiler to version 0.7.0
6
version: "0.7.0",
7
settings: {
8
optimizer: {
9
enabled: true,
10
runs: 200,
11
},
12
},
13
},
14
{
15
version: "0.6.7",
16
settings: {
17
optimizer: {
18
enabled: true,
19
runs: 200,
20
},
21
},
22
},
23
],
24
}
Copied!

Contracts

Next, we'll add in our two key contracts: RedirectAll.sol and TradeableCashflow.sol. As I mentioned, these are very similar to the example in our SuperApps tutorial, but with a few small changes to the RedirectAll.sol contract:
1) We need to add variables which will store the incoming context and userData within the callbacks
1
//public variables which we'll set userData values to
2
ISuperfluid.Context public uData;
3
string public userData;
Copied!
2) We'll add new logic at the bottom of each callback:
Inside of the afterAgreementCreated callback:
1
// decode Context - store full context as uData variable for easy visualization purposes
2
ISuperfluid.Context memory decompiledContext = _host.decodeCtx(_ctx);
3
uData = decompiledContext;
4
​
5
//set userData variable to decoded value
6
//for now, this value is hardcoded as a string - this will be made clear in flow creation scripts within the tutorial
7
//this string will serve as a message on an 'NFT billboard' when a flow is created with recipient = tradeableCashflow
8
//it will be displayed on a front end for assistance in userData explanation
9
​
10
userData = abi.decode(decompiledContext.userData, (string));
Copied!
Inside of the afterAgreementUpdated callback:
1
//update the context with the same logic...
2
​
3
ISuperfluid.Context memory decodedContext = _host.decodeCtx(_ctx);
4
uData = decodedContext;
5
userData = abi.decode(decodedContext.userData, (string));
Copied!
Inside of the afterAgreementTerminated callback:
1
//set the userData (i.e. the billboard message) to an empty string
2
userData = "";
Copied!
This logic will take in the message passed in as userData to calls made to the constant flow agreement which target the flow into this app, decode it, and set it to a storage variable so that we can very easily see what's going on throughout this process. Feel free to make this code your own and do your own gas optimization as you see fit 😁

Contract Deployment

Scaffold-eth makes contract deployment easy: we simply need to use the already installed hardhat plugin, hardhat-deploy, to run our deployments. Each time we want to deploy a new NFT billboard contract, we can simply run yarn deploy to do so.
Inside of the already created deploy folder from Scaffold-eth, we'll pass in a few key addresses from the superfluid protocol as variables, and use them to write a deployment script for our Tradeable Cashflow (NFT billboard) contract.
We get the addresses for host, cfa, and fDAIx from the Superfluid network directory. If you want to deploy to a different testnet, you'll need to make adjustments here.
1
//inside of deploy/00_deploy_tradeable_cashflow.js
2
//mumbai addresses - change if using a different network
3
​
4
const host = '0xEB796bdb90fFA0f28255275e16936D25d3418603';
5
const cfa = '0x49e565Ed1bdc17F3d220f72DF0857C26FA83F873';
6
const fDAIx = '0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f';
7
//your address here...
8
const owner = '0x...';
Copied!
Next, we'll need to add a deployment script for our TradeableCashflow.sol contract. Remember, TradeableCashflow.sol inherits from RedirectAll.sol, so we only need to deploy the single NFT contract.
We'll use the function getNamedAccounts() to get our deployer address. Because we included our account in the 'accounts' property in 'polytest' within hardhat.config, we'll get our address first when calling that function.
We can then deploy our contract fromm that address, and pass in the necessary parameters to the constructor of the TradeableCashflow contract (owner, name, symbol, host, cfa, acceptedToken).
1
//inside of deploy/00_deploy_tradeable_cashflow.js
2
module.exports = async ({ getNamedAccounts, deployments }) => {
3
const { deploy } = deployments;
4
​
5
const { deployer } = await getNamedAccounts();
6
console.log(deployer);
7
​
8
await deploy("TradeableCashflow", {
9
from: deployer,
10
args: [owner, 'nifty_billboard', 'NFTBoard', host, cfa, fDAIx],
11
log: true,
12
})
13
​
14
//...
15
​
16
module.exports.tags = ["YourContract"];
Copied!

Hardhat Scripts For Flow CRUD Functionality

1
//at the top of each script
2
const hre = require("hardhat");
3
require("dotenv");
4
const Web3 = require("web3");
5
​
6
//all addresses hardcoded for mumbai
7
const hostJSON = require("../artifacts/@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol/ISuperfluid.json")
8
const hostABI = hostJSON.abi;
9
const hostAddress = "0xEB796bdb90fFA0f28255275e16936D25d3418603";
10
​
11
const cfaJSON = require("../artifacts/@superfluid-finance/ethereum-contracts/contracts/interfaces/agreements/IConstantFlowAgreementV1.sol/IConstantFlowAgreementV1.json")
12
const cfaABI = cfaJSON.abi;
13
const cfaAddress = "0x49e565Ed1bdc17F3d220f72DF0857C26FA83F873";
14
​
15
const tradeableCashflowJSON = require("../artifacts/contracts/TradeableCashflow.sol/TradeableCashflow.json");
16
const tradeableCashflowABI = tradeableCashflowJSON.abi;
17
​
18
//temporarily hardcode contract address and sender address
19
//need to manually enter contract address and sender address here
20
const deployedTradeableCashflow = require("../deployments/polytest/TradeableCashflow.json");
21
const tradeableCashflowAddress = deployedTradeableCashflow.address;
22
​
23
//your address here
24
const _sender = "0x...";
Copied!
Then, in the bulk of our createFlow, updateFlow, and deleteFlow scripts, we'll create contract objects, define a web3 provider, and submit our call using the pattern outlined in Alchemy's docs on submitting transactions with web3.js.
1
//create a flow
2
async function main() {
3
​
4
​
5
const web3 = new Web3(new Web3.providers.HttpProvider(process.env.MUMBAI_ALCHEMY_URL));
6
​
7
//create contract instances for each of these
8
const host = new web3.eth.Contract(hostABI, hostAddress);
9
const cfa = new web3.eth.Contract(cfaABI, cfaAddress);
10
const tradeableCashflow = new web3.eth.Contract(tradeableCashflowABI, tradeableCashflowAddress);
11
12
const fDAIx = "0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f"
13
const userData = web3.eth.abi.encodeParameter('string', 'HODL BTC');
14
​
15
const nonce = await web3.eth.getTransactionCount(_sender, 'latest'); // nonce starts counting from 0
16
​
17
//create flow by calling host directly in this function
18
//create flow from sender to tradeable cashflow address
19
//pass in userData to the flow as a parameter
20
async function startFlow() {
21
let cfaTx = (await cfa.methods
22
.createFlow(
23
fDAIx,
24
// _sender,
25
tradeableCashflowAddress,
26
"3858024691358",
27
"0x"
28
)
29
.encodeABI())
30
​
31
let txData = (await host.methods.callAgreement(
32
cfaAddress,
33
cfaTx,
34
userData
35
).encodeABI());
36
​
37
let tx = {
38
'to': hostAddress,
39
'gas': 3000000,
40
'nonce': nonce,
41
'data': txData
42
}
43
​
44
let signedTx = await web3.eth.accounts.signTransaction(tx, process.env.MUMBAI_DEPLOYER_PRIV_KEY);
45
​
46
await web3.eth.sendSignedTransaction(signedTx.rawTransaction, function(error, hash) {
47
if (!error) {
48
console.log("πŸŽ‰ The hash of your transaction is: ", hash, "\n Check Alchemy's Mempool to view the status of your transaction!");
49
} else {
50
console.log("❗Something went wrong while submitting your transaction:", error)
51
}
52
});
53
​
54
}
55
56
​
57
await startFlow();
58
​
59
}
60
​
61
// We recommend this pattern to be able to use async/await everywhere
62
// and properly handle errors.
63
main()
64
.then(() => process.exit(0))
65
.catch((error) => {
66
console.error(error);
67
process.exit(1);
68
});
Copied!
Updating a flow is almost identical, but with a change in the userData and flowRate parameters:
1
//update a flow
2
async function main() {
3
​
4
const web3 = new Web3(new Web3.providers.HttpProvider(process.env.MUMBAI_ALCHEMY_URL));
5
​
6
​
7
//create contract instances for each of these
8
const host = new web3.eth.Contract(hostABI, hostAddress);
9
const cfa = new web3.eth.Contract(cfaABI, cfaAddress);
10
const tradeableCashflow = new web3.eth.Contract(tradeableCashflowABI, tradeableCashflowAddress);
11
​
12
const fDAIx = "0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f"
13
const userData = web3.eth.abi.encodeParameter('string', 'HODL ETH');
14
​
15
​
16
const nonce = await web3.eth.getTransactionCount(_sender, 'latest'); // nonce starts counting from 0
17
​
18
//create flow by calling host directly in this function
19
//create flow from sender to tradeable cashflow address
20
//pass in userData to the flow as a parameter
21
async function updateFlow() {
22
let cfaTx = (await cfa.methods
23
.updateFlow(
24
fDAIx,
25
// _sender,
26
tradeableCashflowAddress,
27
"6858024691358",
28
"0x"
29
)
30
.encodeABI())
31
​
32
let txData = (await host.methods.callAgreement(
33
cfaAddress,
34
cfaTx,
35
userData
36
).encodeABI());
37
​
38
let tx = {
39
'to': hostAddress,
40
'gas': 3000000,
41
'nonce': nonce,
42
'data': txData
43
}
44
​
45
let signedTx = await web3.eth.accounts.signTransaction(tx, process.env.MUMBAI_DEPLOYER_PRIV_KEY);
46
​
47
await web3.eth.sendSignedTransaction(signedTx.rawTransaction, function(error, hash) {
48
if (!error) {
49
console.log("πŸŽ‰ The hash of your transaction is: ", hash, "\n Check Alchemy's Mempool to view the status of your transaction!");
50
} else {
51
console.log("❗Something went wrong while submitting your transaction:", error)
52
}
53
});
54
​
55
}
56
​
57
await updateFlow();
58
​
59
}
60
​
61
// We recommend this pattern to be able to use async/await everywhere
62
// and properly handle errors.
63
main()
64
.then(() => process.exit(0))
65
.catch((error) => {
66
console.error(error);
67
process.exit(1);
68
});
Copied!
Our delete flow script has 2 key differences: there is no flowrate passed in, and no userData either. We set the value of userData inside of our contract back to an empty string when an agreement is terminated.
1
//delete a flow
2
async function main() {
3
​
4
const web3 = new Web3(new Web3.providers.HttpProvider(process.env.MUMBAI_ALCHEMY_URL));
5
​
6
//create contract instances for each of these
7
const host = new web3.eth.Contract(hostABI, hostAddress);
8
const cfa = new web3.eth.Contract(cfaABI, cfaAddress);
9
const tradeableCashflow = new web3.eth.Contract(tradeableCashflowABI, tradeableCashflowAddress);
10
​
11
const _sender = "0x9421FE8eCcAfad76C3A9Ec8f9779fAfA05A836B3"
12
​
13
const accts = await web3.eth.getAccounts();
14
​
15
const fDAIx = "0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f"
16
​
17
const nonce = await web3.eth.getTransactionCount(_sender, 'latest'); // nonce starts counting from 0
18
​
19
async function cancelFlow() {
20
let cfaTx = (await cfa.methods
21
.deleteFlow(
22
fDAIx,
23
_sender,
24
tradeableCashflowAddress,
25
"0x"
26
)
27
.encodeABI())
28
//try using callAgreement vs callagreement w context
29
let txData = (await host.methods.callAgreement(
30
cfaAddress,
31
cfaTx,
32
//pass in empty field for userData
33
"0x"
34
).encodeABI());
35
​
36
let tx = {
37
'to': hostAddress,
38
'gas': 3000000,
39
'nonce': nonce,
40
'data': txData
41
}
42
​
43
let signedTx = await web3.eth.accounts.signTransaction(tx, process.env.MUMBAI_DEPLOYER_PRIV_KEY);
44
​
45
await web3.eth.sendSignedTransaction(signedTx.rawTransaction, function(error, hash) {
46
if (!error) {
47
console.log("πŸŽ‰ The hash of your transaction is: ", hash, "\n Check Alchemy's Mempool to view the status of your transaction!");
48
} else {
49
console.log("❗Something went wrong while submitting your transaction:", error)
50
}
51
});
52
​
53
}
54
55
​
56
await cancelFlow();
57
​
58
}
59
​
60
// We recommend this pattern to be able to use async/await everywhere
61
// and properly handle errors.
62
main()
63
.then(() => process.exit(0))
64
.catch((error) => {
65
console.error(error);
66
process.exit(1);
67
});
Copied!
Finally, we'll also create a script which helps us read on chain data that corresponds to our flow. This will allow us to see what Context looks like when passed around within the protocol, and it allows us to see what userData looks like when appended onto it as a bytes value. Once we can identify its place in the Context struct, we just need to decode it to make use of it.
1
//read flowData
2
async function main() {
3
​
4
const web3 = new Web3(new Web3.providers.HttpProvider(process.env.MUMBAI_ALCHEMY_URL));
5
​
6
//create contract instances for each of these
7
const host = new web3.eth.Contract(hostABI, hostAddress);
8
const cfa = new web3.eth.Contract(cfaABI, cfaAddress);
9
const tradeableCashflow = new web3.eth.Contract(tradeableCashflowABI, tradeableCashflowAddress);
10
const fDAIx = "0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f"
11
​
12
​
13
//get data
14
const decodedContext = await tradeableCashflow.methods.uData().call();
15
const decodedUserData = web3.eth.abi.decodeParameter('string', decodedContext.userData);
16
console.log(decodedContext)
17
console.log(decodedUserData)
18
19
//get jail info
20
const jailed = await host.methods.getAppManifest(tradeableCashflowAddress).call()
21
console.log(jailed)
22
const isJailed = await host.methods.isAppJailed(tradeableCashflowAddress).call();
23
console.log(`is jailed: ${isJailed}`);
24
​
25
const flowInfo = await cfa.methods.getFlow(fDAIx, tradeableCashflowAddress, "0x00471Eaad87b91f49b5614D452bd0444499c1bd9").call();
26
const outFlowRate = Number(flowInfo.flowRate);
27
console.log(`Outflow Rate: ${outFlowRate}`);
28
​
29
const netFlow = await cfa.methods.getNetFlow(fDAIx, tradeableCashflowAddress).call();
30
console.log(`Net flow: ${netFlow}`);
31
​
32
const inFlowRate = Number(netFlow) + outFlowRate;
33
console.log(`Inflow rate: ${inFlowRate}`)
34
}
35
​
36
// We recommend this pattern to be able to use async/await everywhere
37
// and properly handle errors.
38
main()
39
.then(() => process.exit(0))
40
.catch((error) => {
41
console.error(error);
42
process.exit(1);
43
});
Copied!
A sample output from readData.js:
1
//decoded context in terminal
2
​
3
appLevel: '1',
4
callType: '3',
5
timestamp: '1634263831',
6
msgSender: '0x9421FE8eCcAfad76C3A9Ec8f9779fAfA05A836B3',
7
agreementSelector: '0x62fc305e',
8
userData: '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008484f444c20425443000000000000000000000000000000000000000000000000',
9
appAllowanceGranted: '13888889148145664',
10
appAllowanceWanted: '0',
11
appAllowanceUsed: '0',
12
appAddress: '0xB04FCcCc7Ef4cfCff6F1693492DdE15BcEbEe71f',
13
appAllowanceToken: '0x5D8B4C2554aeB7e86F387B4d6c00Ac33499Ed01f'
14
}
15
//decoded userData
16
"HODL BTC"
Copied!
If you want, you can also check out the Superfluid dashboard to see these flows being created in real time (you just can't see the userData there)!
​

The Front End - Displaying our Billboard

In this repository, you'll see that you have many of the other components and UI elements from scaffold-eth still there. We left them there to give you the opportunity to continue tinkering with the rest of the framework if you choose to. However, to display our NFT Billboard, we'll need to make a few changes.
First, within the views folder, we'll make a copy of the 'ExampleUI.jsx' file called 'NFTBillboard.jsx' and make a few changes.
We'll add message and billboardOwner to our list of exports, and remove some of the items from ExampleUI:
1
export default function NFTBillboard({
2
message,
3
billboardOwner,
4
mainnetProvider,
5
readContracts,
6
})
Copied!
And we'll also change the content our function component returns to reflect our data:
1
return (
2
<div>
3
{/*
4
βš™οΈ Here is a UI that displays and sets the message in your NFT Billboard:
5
*/}
6
<div style={{ border: "1px solid #cccccc", padding: 16, width: 400, margin: "auto", marginTop: 64 }}>
7
<h1>NFT Billboard</h1>
8
<h2>Message: <b>{message}</b></h2>
9
<Divider />
10
<div style={{ margin: 8 }}>
11
<h3>Owner:</h3>
12
<h4>
13
<Address address={billboardOwner} />
14
</h4>
15
</div>
16
17
</div>
18
​
19
​
20
<div style={{ width: 600, margin: "auto", marginTop: 32, paddingBottom: 256 }}>
21
22
Billboard Contract Address:
23
<Address
24
address={readContracts && readContracts.TradeableCashflow ? readContracts.TradeableCashflow.address : null}
25
ensProvider={mainnetProvider}
26
fontSize={16}
27
/>
28
​
29
</div>
30
</div>
Copied!
If you peer into the original 'ExampleUI.jsx' file, you'll see that we removed a great deal of content. All that's left is the top section and an Address component where we'll display our content's address. Again - you're welcome to utilize the other front end content if you'd like in your own applications 😎
Next, we need to add our NFTBillboard component to our App.jsx file, and make sure that we have logic that will read the message (i.e. userData) and owner of our billboard (the _receiver we pass in when we deployed our contract).
At the top of our file, we'll import the component
1
//in the top section of our App.jsx file
2
//add NFT billboard here
3
import NFTBillboard from "./views/NFTBillboard";
Copied!
We'll then need to make sure that our react app is reading data from the right network. To do this, we'll need to make a change to targetNetwork . In our case, we'll set the network to NETWORKS.mumbai - but if you're using a different network, you'll need to specify that here.
1
//on line 57 within App.jsx
2
/// πŸ“‘ What chain are your contracts deployed to?
3
const targetNetwork = NETWORKS.mumbai; // <------- select your target frontend network (localhost, rinkeby, xdai, mainnet)
Copied!
To get the values for the billboard owner and message, we'll need to use the useContractReader hook that's included in scaffold-eth from eth-hooks. You can read more about eth-hooks here.
The scaffold-eth framework already defines a helpful readContracts variable that we can pass to useContractReader which will allow us to read data from our deployed contracts. If you want to create new contracts that you can write to, you'll use the writeContracts variable instead.
1
//already included for us from Scaffold-eth
2
// Load in your local πŸ“ contract and read a value from it:
3
const readContracts = useContractLoader(localProvider, contractConfig);
Copied!
We can set our message and billboardOwner variables like this:
1
//two new variables that we'll use
2
const message = useContractReader(readContracts, "TradeableCashflow", "userData")
3
const billboardOwner = useContractReader(readContracts, "TradeableCashflow", "_receiver")
Copied!
Finally, we'll include our NFTBillboard component in our body, and set the "/" route to render our billboard component.
1
// Some code
2
//at the top of our Switch statement
3
<Switch>
4
<Route exact path="/">
5
6
<NFTBillboard
7
address={address}
8
mainnetProvider={mainnetProvider}
9
readContracts={readContracts}
10
billboardOwner={billboardOwner}
11
message={message}
12
​
13
/>
14
​
15
//...
16
</Route>
17
</Switch>
Copied!
You'll notice that much of the body in App.jsx is commented out. If you're curious about what the other elements do, feel free to uncomment them and play around.
There you have it! You can run yarn start to run your react app locally, then yarn deploy on your contracts to see things render! To send your message, you can call createFlow to see the message appear on your billboard in real time.
I would also recommend opening up the superfluid dashboard to see how flows are moving into and out of your NFT contract. In the account you're using to deploy the contract, you'll see that you have a flow created into the contract.
Then, if you look at the account you used for the owner of the tradeable cashflow (i.e. billboard) contract, you'll see that you have an incoming stream from the contract itself.
Last modified 1mo ago