Skip to main content

Customize Rewards Logic

In this guide, we will walk through two different methods of customizing rewards logic within your metagraph.

Understanding Rewards

Rewards are emitted on every timed snapshot of the metagraph and increase the circulating supply of the metagraph token beyond the initial balances defined in genesis.csv. These special transaction types can be used to distribute your currency to fund node operators, or create fixed pools of tokens over time.

By default, no rewards are distributed by a metagraph using the Currency Framework which results in a static circulating supply. The rewards customizations described below create inflationary currencies - the rate of which can be controlled by the specific logic introduced. Similarly, a maximum token supply can easily be introduced if desired to prevent unlimited inflation.

The Rewards Function

The rewards function includes contextal information from the prior incremental update, including any data produced. Additionally, this function can include customized code capable of invoking any library function of your choice, allowing you to support truly custom use cases and advanced tokenomics structures. The following examples serve as a foundation for typical use cases, which you can expand upon and tailor to your project's needs.

Before You Start

This guide assumes that you have configured your local environment based on the Quick Start Guide and have at least your global-l0, currency-l0, currency-l1 clusters configured.

We will be updating the code within your project in the L0 module. This can be found in:

source/project/<project_name>/modules/l0/src/main/Main.scala

Please note, the examples below show all logic within a single file to make copy/pasting the code as simple as possible. In a production application you would most likely want to split the code into multiple files.

Examples

These examples show different ways that rewards logic can be customized within your metagraph. The concepts displayed can be used independently or combined for further customization based on the business logic of your project.

Example: Distribute Rewards to Fixed Addresses

Add the following code to your L0 Main.scala file.

package com.my.currency.l0

import cats.effect.{Async, IO}
import org.tessellation.BuildInfo
import org.tessellation.currency.dataApplication.BaseDataApplicationL0Service
import org.tessellation.currency.l0.CurrencyL0App
import org.tessellation.currency.schema.currency.{
CurrencyBlock,
CurrencyIncrementalSnapshot,
CurrencySnapshotStateProof,
CurrencyTransaction
}
import org.tessellation.schema.address.Address
import org.tessellation.schema.balance.Balance
import org.tessellation.schema.cluster.ClusterId
import org.tessellation.schema.transaction.{
RewardTransaction,
TransactionAmount
}
import org.tessellation.sdk.domain.rewards.Rewards
import org.tessellation.sdk.infrastructure.consensus.trigger.ConsensusTrigger
import org.tessellation.security.SecurityProvider
import org.tessellation.security.signature.Signed

import eu.timepit.refined.auto._
import cats.syntax.applicative._

import java.util.UUID
import scala.collection.immutable.{SortedSet, SortedMap}

object RewardsMintForPredefinedAddresses {
def make[F[_]: Async] =
new Rewards[
F,
CurrencyTransaction,
CurrencyBlock,
CurrencySnapshotStateProof,
CurrencyIncrementalSnapshot
] {
def distribute(
lastArtifact: Signed[CurrencyIncrementalSnapshot],
lastBalances: SortedMap[Address, Balance],
acceptedTransactions: SortedSet[Signed[CurrencyTransaction]],
trigger: ConsensusTrigger
): F[SortedSet[RewardTransaction]] = SortedSet(
Address("DAG8pkb7EhCkT3yU87B2yPBunSCPnEdmX2Wv24sZ"),
Address("DAG4o41NzhfX6DyYBTTXu6sJa6awm36abJpv89jB")
).map(RewardTransaction(_, TransactionAmount(55_500_0000L))).pure[F]
}
}

object Main
extends CurrencyL0App(
"custom-rewards-l0",
"custom-rewards L0 node",
ClusterId(UUID.fromString("517c3a05-9219-471b-a54c-21b7d72f4ae5")),
version = BuildInfo.version
) {

def dataApplication: Option[BaseDataApplicationL0Service[IO]] = None

def rewards(implicit sp: SecurityProvider[IO]) = Some(
RewardsMintForPredefinedAddresses.make[IO]
)
}

The code distributes 5.55 token rewards on each timed snapshot to two hardcoded addresses:

  • DAG8pkb7EhCkT3yU87B2yPBunSCPnEdmX2Wv24sZ
  • DAG4o41NzhfX6DyYBTTXu6sJa6awm36abJpv89jB

These addresses could represent treasury wallets or manually distributed rewards pools. Update the number of wallets and amounts to match your use-case.

Rebuild Clusters

Run the following commands to rebuild your clusters with the new code:

scripts/hydra destroy
scripts/hydra build --no_cache

Once built, run hydra start to see your changes take effect.

scripts/hydra start-genesis

View Changes

Using the Developer Dashboard you should see the balances of the two wallets above increase by 5.5 tokens after each snapshot.

Inspecting the snapshot body, you should also see an array of "rewards" transactions present.

Rewards Transactions in Snapshot

Example: Distribute Rewards to Validator Nodes

Add the following code to your L0 Main.scala file.

package com.my.currency.l0

import cats.effect.{Async, IO}
import cats.implicits.{toFoldableOps, toFunctorOps, toTraverseOps}
import org.tessellation.BuildInfo
import org.tessellation.currency.dataApplication.BaseDataApplicationL0Service
import org.tessellation.currency.l0.CurrencyL0App
import org.tessellation.currency.schema.currency.{CurrencyBlock, CurrencyIncrementalSnapshot, CurrencySnapshotStateProof, CurrencyTransaction}
import org.tessellation.schema.address.Address
import org.tessellation.schema.balance.Balance
import org.tessellation.schema.cluster.ClusterId
import org.tessellation.schema.transaction.{RewardTransaction, TransactionAmount}
import org.tessellation.sdk.domain.rewards.Rewards
import org.tessellation.security.SecurityProvider
import org.tessellation.security.signature.Signed
import eu.timepit.refined.auto._
import org.tessellation.sdk.infrastructure.consensus.trigger.ConsensusTrigger

import java.util.UUID
import scala.collection.immutable.{SortedMap, SortedSet}

object RewardsMint1ForEachFacilitator {
def make[F[_]: Async: SecurityProvider] =
new Rewards[F, CurrencyTransaction, CurrencyBlock, CurrencySnapshotStateProof, CurrencyIncrementalSnapshot] {
def distribute(
lastArtifact: Signed[CurrencyIncrementalSnapshot],
lastBalances: SortedMap[Address, Balance],
acceptedTransactions: SortedSet[Signed[CurrencyTransaction]],
trigger: ConsensusTrigger
): F[SortedSet[RewardTransaction]] = {
val facilitatorsToReward = lastArtifact.proofs.map(_.id)
val addresses = facilitatorsToReward.toList.traverse(_.toAddress)
val rewardsTransactions = addresses.map(addresses => {
val addressesAsList = addresses.map(RewardTransaction(_, TransactionAmount(1_000_0000L)))
collection.immutable.SortedSet.empty[RewardTransaction] ++ addressesAsList
})

rewardsTransactions
}
}
}

object Main
extends CurrencyL0App(
"custom-rewards-l0",
"custom-rewards L0 node",
ClusterId(UUID.fromString("517c3a05-9219-471b-a54c-21b7d72f4ae5")),
version = BuildInfo.version
) {

def dataApplication: Option[BaseDataApplicationL0Service[IO]] = None

def rewards(implicit sp: SecurityProvider[IO]) = Some(
RewardsMint1ForEachFacilitator.make[IO]
)
}

The code distributes 1 token reward on each timed snapshot to each validator node that participated in the most recent round of consensus.

Rebuild Clusters

Run the following commands to rebuild your clusters with the new code:

scripts/hydra destroy
scripts/hydra build --no_cache

Once built, run hydra start to see your changes take effect.

scripts/hydra start-genesis

View Changes

Using the Developer Dashboard you should see the balances of the wallets in each node in your L0 cluster above increase by 1 token after each snapshot.

Inspecting the snapshot body, you should also see an array of "rewards" transactions present.

Example: Distribute Rewards Based on API Data

Add the following code to your L0 Main.scala file.

package com.my.currency.l0

import cats.data.NonEmptyList
import cats.effect.{Async, IO}
import cats.implicits.catsSyntaxApplicativeId
import derevo.circe.magnolia.{decoder, encoder}
import derevo.derive
import org.tessellation.BuildInfo
import org.tessellation.currency.dataApplication.BaseDataApplicationL0Service
import org.tessellation.currency.l0.CurrencyL0App
import org.tessellation.currency.schema.currency.{CurrencyBlock, CurrencyIncrementalSnapshot, CurrencySnapshotStateProof, CurrencyTransaction}
import org.tessellation.schema.address.Address
import org.tessellation.schema.balance.Balance
import org.tessellation.schema.cluster.ClusterId
import org.tessellation.schema.transaction.{RewardTransaction, TransactionAmount}
import org.tessellation.sdk.domain.rewards.Rewards
import org.tessellation.security.SecurityProvider
import org.tessellation.security.signature.Signed
import eu.timepit.refined.numeric.Positive
import eu.timepit.refined.refineV
import eu.timepit.refined.types.numeric.PosLong
import org.tessellation.sdk.infrastructure.consensus.trigger.ConsensusTrigger
import io.circe.parser.decode

import java.util.UUID
import scala.collection.immutable.{SortedMap, SortedSet}

object RewardsMintForEachAddressOnApi {
private def getRewardAddresses: List[Address] = {

@derive(decoder, encoder)
case class AddressTimeEntry(address: Address, date: String)

try {
//Using host.docker.internal as host because we will fetch this from a docker container to a API that is on local machine
//You should replace to your url
val response = requests.get("http://host.docker.internal:8000/addresses")
val body = response.text()

println("API response" + body)

decode[List[AddressTimeEntry]](body) match {
case Left(e) => throw e
case Right(addressTimeEntries) => addressTimeEntries.map(_.address)
}
} catch {
case x: Exception => {
println(s"Error when fetching API: ${x.getMessage}")
List[Address]()
}
}
}

private def getAmountPerWallet(addressCount: Int): PosLong = {
val totalAmount: Long = 100_000_0000L
val amountPerWallet: Either[String, PosLong] = refineV[Positive](totalAmount / addressCount)

amountPerWallet.toOption match {
case Some(amount) => amount
case None =>
println("Error getting amount per wallet")
PosLong(1)
}
}

def make[F[_] : Async ] =
new Rewards[F, CurrencyTransaction, CurrencyBlock, CurrencySnapshotStateProof, CurrencyIncrementalSnapshot] {
def distribute(
lastArtifact: Signed[CurrencyIncrementalSnapshot],
lastBalances: SortedMap[Address, Balance],
acceptedTransactions: SortedSet[Signed[CurrencyTransaction]],
trigger: ConsensusTrigger
): F[SortedSet[RewardTransaction]] = {

val rewardAddresses = getRewardAddresses
val foo = NonEmptyList.fromList(rewardAddresses)

foo match {
case Some(addresses) =>
val amountPerWallet = getAmountPerWallet(addresses.size)
val rewardAddressesAsSortedSet = SortedSet(addresses.toList: _*)

rewardAddressesAsSortedSet.map(address => {
val txnAmount = TransactionAmount(amountPerWallet)
RewardTransaction(address, txnAmount)
}).pure[F]

case None =>
println("Could not find reward addresses")
val nodes: SortedSet[RewardTransaction] = SortedSet.empty
nodes.pure[F]
}
}
}
}

object Main
extends CurrencyL0App(
"custom-rewards-l0",
"custom-rewards L0 node",
ClusterId(UUID.fromString("517c3a05-9219-471b-a54c-21b7d72f4ae5")),
version = BuildInfo.version
) {

def dataApplication: Option[BaseDataApplicationL0Service[IO]] = None

def rewards(implicit sp: SecurityProvider[IO]) = Some(
RewardsMintForEachAddressOnApi.make[IO]
)
}

The code distributes token rewards on each timed snapshot to each address that is returned from a custom API.

On this Repository you can take a better look at the template example and the custom API.

In the repository, the code will distribute the amount of 100 tokens between the number of returned wallets (in this case the maximum of 20 latest wallets)

Rebuild Clusters

Run the following commands to rebuild your clusters with the new code:

scripts/hydra destroy
scripts/hydra build --no_cache

Once built, run hydra start to see your changes take effect.

scripts/hydra start-genesis

View Changes

Using the Developer Dashboard you should see the balances of the wallets in each node in your L0 cluster above increase by ( 100 / :number_of_wallets ) tokens after each snapshot.

Inspecting the snapshot body, you should also see an array of "rewards" transactions present.