Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
To install the CLI at your global level and not tinker with the code directly, use:
Commands can now be run using the raindrops-cli commands
If you want to work with the codebase to make updates and interact with the contracts you can clone the github repo and run yarn watch from the js
folder or use ts-node
to execute client scripts directly without transpiling typescript to javascript.
For the time being, as this is partially a Degeniverse product and partially an open source product, you can get help from the developers in the raindrops-protocol
channel of the DTP discord: https://discord.gg/5yjMKVbC.
As mentioned in the Introduction, the raindrops protocol is a series of 5 contracts running on Solana. These contracts govern on-chain or proof-on-chain data specifically for games, currently. This allows any player or item data to be interchangeable with any game client that wishes to process them.
Let's go through the "Who, What, Why, and How" of the raindrops protocol.
Since raindrops is made up of 5 contracts, each of which is specialized to different functions/needs of a game, a game is able to choose any contract(s) that provide just what they need. Or they can decide to use all of them. This allows for games of all sizes and complexities, as well as games that already exist or ones being built from the beginning to benefit from using raindrops.
An existing or established game can decide if they want to move just one feature of their game at a time. Or they can integrate raindrops by using it for an entirely new feature they want to build.
A brand new game will have more freedom to use raindrops in it's entirety from the very beginning. This will greatly reduce the cost and time it takes to get a game into gamers hands, reduce a lot of ongoing headaches with maintaining the same code common to all games, and starting from day one, make their game a part of the raindrops ecosystem of connected games.
What does this mean for gaming and what even is an ecosystem of connected games?
Just like bitcoin introduced a trust-less way for anyone in the world to create and access transaction information for a currency moving from one wallet to another. Raindrops does this for gaming, but it's not specific to a game's economic transactions.
The types of data raindrops makes available consists of items, players, games, matches and staking. Having this data available on-chain enables games to do very cool things that have just never been possible, or were prohibitively expensive to do.
We should first break down why this actually matters for gaming (it's actually applicable beyond gaming, but let's start there) so we can better understand what it actually means.
Why is this information so important and ground-breaking for gaming? Let's reference bitcoin again for a moment.
When bitcoin made transaction data available to anyone in the world, and it was trust-worthy, suddenly the world had a global ledger(database) for anyone to add to and read from. This not only cut out the middle men, it also made it possible to access all the data the middle men might have never even made available, in addition to the start and end state of the data(who owns which bitcoins). Before bitcoin, the only way to track and get data about a currency transaction was to communicate with each banking entity involved, and potentially everything in between. That's assuming everyone involved even provided access to the data and you were allowed to talk to them. That's an enormous undertaking from both a technology and time-cost perspective. Each hand-off from one entity to another can take an undetermined amount of time, as well as be rolled back in the event of a refund or some other event. With bitcoin, you now have one place to look for this information, you can trust it will be accurate, and after enough time has passed, it's very unlikely to ever be rolled back.
So, how does this apply to gaming? And why should you care?
Gaming has the exact same fundamental problem. Before raindrops, every game has been siloed and is separate from one another. Each game has its own database, hidden behind a group of servers, or isn’t even on a server but stored locally on your computer. In addition to that, each game speaks it's own language. That language may even be proprietary and unintelligible without spending a very large amount of time decoding it. This means the only way to share/access game information between two games is to be bilingual and write very specific software that connects to both. That's assuming both games even make their information available. Most don't. This becomes incredibly time consuming and brittle. Therefore, most games never even try to do it. Which is a huge shame and a big loss for gaming as a whole.
It's even more involved then that within the pre-raindrops state of gaming. If you are able to have two games communicate with each other, you're dependent on both games never changing their language. Sometimes that isn't possible if they need to build something new or fix something that's broken. The availability of both game's servers also come into play. If either of the game's servers go down or the language they use changes, you now can't communicate without updating the software connecting them together, or by implementing usually complicated logic that handles these error cases when they happen. This really sucks for game developers and the players that play their game.
Raindrops eliminates all of that and empowers game companies by not forcing them to implement all this common logic themselves, as well as making their game's data available for any other game to consume. This provides a far more immersive gaming experience. Raindrops also provides solutions to common functions many games need to build themselves to just make their games playable. That stops the same bugs from occuring across many different implementations inside each game, as well as the expenses and time each game has to incur to build and maintain their own version. Furthermore, in the blockchain world, without raindrops, each game needs their custom software to be audited for security and data purposes.
Does all this complexity seem error prone, wasteful, and make your head hurt yet? We hate to think about all the time lost and how much further along gaming could be if these problems didn't exist!
Raindrops eliminates these problems. Games can now have one source of truth, one language for communicating with one another, and save a whole lot of time and money in the process of building and maintaining a game. In addition to being more secure.
By using raindrops, a game's data is available on-chain to itself as well as any other game that wants to use it. This data can be read from a contract running on solana or by making an HTTP request.
Even more interestingly, it doesn't have to just be other games that use the data either. Say someone wants to build a dashboard to view the progress of other players/items in a game, or to show how a Legendary Sword has been used across multiple games and what game caused it to take the most damage. They can do all that using the data made available by games using raindrops.
Best of all, the data is verified to be accurate since it uses the same mechanisms used when sending tokens(currency, NFTs, etc...) from one wallet to another. Games can now trust that an item and it's data hasn't been tampered with and the game that updated it is who they claim to be.
In addition, raindrops is compatible with the $RAIN token. The $RAIN token reduces friction for the gamer to play different games.
We don't believe the future of gaming is to have a different token for every game you want to play. We don't know about you, but we don't want to have 10+ different gaming tokens in our wallets to play the games we love.
If a game already has their own token and they want to continue using it, raindrops does support any SPL mint for gameplay costs, rewards, or staking.
With that background, let's ask again, what does raindrops make possible?
For one, gone are the days where your hard work and accomplishments have to be stuck in a single game or lost completely if a game ceases to exist. If a player decides they want to sell their Legendary Sword that they have been leveling up through play in one game, they can, and whomever chooses to buy it will get that sword at it's current level rather then having to start from scratch back at level 1. This means your in-game work using an item will create real-world value.
It's also possible to use that leveled up sword in a completely different game with the same level stats. How crazy is that?! Before raindrops, this would be unheard of to have an item work across games and have the same stats without a significant amount of work and coordination between two games. With raindrops, all the data is just there, and it's in the same format for every game to use. Simple. No surprises.
This also means that a player's progress can be shared across games. That might seem like a more abstract concept, but you can imagine something along the lines of if you were to beat Rainbow Road while playing Mario Kart 1, it could unlock a special level in Mario Kart 2 that you can only unlock by beating Rainbow Road on Mario Kart 1. While this example highlights what is possible within just the Mario Kart universe, it also works within any other game. A game series like Need For Speed could see that you've beaten Rainbow Road and unlock special cars or tracks that are only available to players with that acheivement. No specific coordination would be required between the makers of Mario Kart and Need For Speed besides using the raindrops protocol.
Those are just a few examples of what is possible within an ecosystem of games connected through raindrops. What we're most excited about is what we haven't even imagined is possible when games share both the language and the storage of their game data, and it’s verifiable.
There are several core foundational concepts that are used in some or across all of the Raindrops contracts. This section explains those concepts in detail. You can read them as you run across one you don't understand or read them all at once before digging into any specific part of the protocol.
The Solana ecosystem has a fragmented and very customized way of storing in-game assets on chain, with as many different formats as there are play-2-earn games. However, the methods of storing ordinary NFT data are standardized in the Metaplex Protocol. The aim of Raindrops Protocol is to do the same thing for game assets.
The Raindrops Protocol is a series of five a la carte contracts that govern on-chain or proof-on-chain data specifically for games, allowing any player or item to be interchangeable with any game client that can process them.
Following the composability doctrine, the Raindrops Protocol lives literally on top of the Metaplex Protocol, further decorating existing NFTs with new metadata about whether they are a Player, an Item, or both! All contracts within the Protocol are designed to be composed with other Solana contracts enforcing game-specific rules.
Each contract was built to work together to form a complete protocol that satisfies the needs of blockchain-based games, when using raindrops you retain the choice to include just the parts you need to build your game.
The protocol source is available on GitHub:
Learn about what's possible with the raindrops protocol
Jump in to the quick start docs and start making your first game's objects with raindrops:
There are several powerful concepts used across the raindrops protocol. They are outlined under this section
Explore any contract to get a deep understanding of what it can do
To keep gaming inexpensive, only stateful Items
should be NFTs.
Non-stateful Items
, like single-use potions, should be Semi-Fungible Tokens (supply > 0, decimals = 0, this requires only a metadata struct from the Metaplex Token Metadata Program with no edition struct).
In this way, all users share potions from the same mint, without needing to create new mints for each potion. There are certain limitations on SFTs being used, but the trade-offs in cost are extraordinary.
What this means is that you only need a single NFT defining the ItemClass
for a given SFT item, and then a second mint of semi-fungibles representing the supply that you sell or give to users. It is on top of this mint that you define theItem
. As long as you meet the rules of SFT Items
, you've now got a very efficient gameplay item.
It is a pain to have to redefine the same stats for different player or item NFTs across many unique NFT mints, as well as costly. The Raindrops Protocol avoids this by taking the stance that object inheritance is useful, and an object class should be it’s own NFT. Just as an NFT is simply two structs from the Token Metadata program that live on top of a token (specifically the Metadata
and Master Edition
structs), Raindrops adds additional structs on top of that NFT depending if that NFT represents a Class or an Instance of some Class.
One object class NFT can inherit from another class NFT, and all existing object instances should inherit from an existing object class NFT.
Both the Player and Item contract behave this way, with PlayerClass
structs and ItemClass
storing configurational information about what makes up this kind of Player
or Item
(like BodyParts
, BasicStats
like strength, or what BasicItemEffects
an item should have), and the actual Player
and Item
instance structs are slimmed down data that only contain stateful information specific to that given NFT, like whether the strength is 5, or if the item was used 5 minutes ago.
For an example of this, here is an ItemUsage
(Configurational data from an ItemClass
) and an ItemUsageState
(Stateful information from an Item
that is a child of this class):
An ItemUsage
will have a has_many relationship with an ItemClass
and is embedded within the ItemClass
.
ItemUsage
defines how a given Item
may be used, and an Item
can be used in many ways. However, the ItemClass
doesn’t actually get used - it is a blueprint for creating Item
instances. When made, Items
don’t all contain this massive struct in their PDAs, they instead contain a trimmed down ItemUsageState
for the ItemUsage
defined within their parent ItemClass
class. This keeps the Items
and Players
relatively small as PDAs.
A “inherited field” has the following structure:
Properties/fields that can be inherited in an ItemClass
that come from it’s parent classes are marked appropriately if they came from it’s own definition(NotInherited), came from it’s parent definition(Inherited), or the parent class has permissions to make it illegal for any sub-definitions to be made on the given class(Overridden).
An example of Overridden
would be the parent class saying that the child class cannot override the build permissiveness settings. The build permissiveness array, which can host a variety of different permissions types, must be a 1:1 match with the parent’s copy of it.
Multiple inheritance is explicitly not supported to keep things simple, except in Namespaces, where a single Namespace
can be part of many other parent Namespaces
. This is much more similar to a Mixin however, as namespaces do not inherit from a parent.
Throughout the contracts, with every mint there is often an associated index which is an u64
. The reason for this is that while you may have a single NFT, you may wish to use it across multiple games. These different games may not wish to share a Player
stat sheet or an Item
definition. This is why all NFTs can have multiple Items
and Players
defined on top of them using the index as part of the seed. This index allows a single NFT can be used in different ways by different games.
Another common use-case might be to define index 0 of an NFT as an ItemClass
, and index 1 as the Item
for a true 1/1 Item
so that you don’t need to have two NFTs to define a single legendary sword. Same could be done with a PlayerClass
/Player
.
It should be noted that if one Item
PDA describes a Consumable
that destroys the NFT, and you use the NFT in this way, it will be destroyed, regardless of its other definitions.
When an ItemClass
inherits from another ItemClass
, it can be unknown or undefined how any changes to the parent ItemClass
should be propagated to the child ItemClass
.
Raindrops introduces the concept of permission-less updates(--inheritance-update
) to handle this situation.
This tells the endpoint not to accept any changes to a child ItemClass
except for those also present on the parent ItemClass
. This means anybody can enforce inheritance at any time across any size tree of inheritance.
This updates the Item instance with the new ItemUsageStates
that may arise from changes in ItemUsage
s in the parent class.
Similar logic exists for the Player contract, but instead of using --inheritance-update flag, simply use a null updatePermissivenessToUse inside the config JSON file (the -cp flag entry).
This section defines the 5 contracts that make up the protocol at a deep level. Within this section you will find information on how each contract works and commands you can run to interact with them. You can either read through them one by one or go straight to the one you are interested in. You don't need to read any specific contract first to understand the others. They each can be used individually or combined together to build a larger cohesive system.
There are four different permissions types supported by the five contracts. When speaking here we'll be using the Item
contract for examples, but this works equally well for the other contracts.
TokenHolder
ParentTokenHolder
UpdateAuthority
Anybody
In order to do the action, the token on which the action is being taken must be held by the signer of the transaction.
In order to do the action, the token on which the action is being taken must already have a predefined struct (like an ItemClass
or an Item
) and that struct must have its parent
field set to a valid pubkey. That pubkey will point to the ItemClass
of another NFT. The signer calling this action must be holding the NFT of that parent
ItemClass
.
In order to do the action, the update authority of the Metadata
of the token (as defined by the Token Metadata program) must be the signer on the transaction.
Anybody can do this action. There are no requirements to be met.
These permissiveness types are used to gate every kind of activity there is to do on any object across many of the contracts. In the item contract, for instance, you can set who is allowed to build an instance of an ItemClass
, who can update it, who can stake against it, etc.
Here is an example of an ItemClassSettings
struct that has several fields/properties that use these permissions:
When performing an action against a Raindrops contract, you must specify what type of permissiveness you want to use to do that action and that permissiveness will be checked against the existing array of permissions for the object being manipulated with that contract's endpoint.
The CLI will also use the permissiveness chosen to determine what additional accounts that might be needed to send in the instruction call to prove validity.
Here’s an example configuration file for opening an item escrow, which is the first action one does to create an Item
from an ItemClass
using the Item contract:
Notice that buildPermissivenessToUse
is set to tokenHolder. This means that the ItemClass
allows anybody holding the ItemClass
NFT to use the ItemClass
recipe to turn a non-raindrops NFT into an Item
instance of this ItemClass
. For instance, if you have an NFT that looks like a potion that you just made on a minting website, that does not mean it is an actual Potion that is used during play by Game X. If you hold the NFT representing the ItemClass
for Potion, then you have the right to go through the creation process to turn this freshly minted NFT potion look-alike into an actual Potion that your game client can process during gameplay. At the end of this creation cycle you’ll have an Item PDA seeded to your potion NFT that allows it to be used within the game.
Permissions can be propagated from a parent class to a child class in either a fashion that overwrites the child array or is additive.
To specify which permissions get propagated, use the child_update_propagation_permissiveness
array on the Player
or Item
classes.
The available options are:
If you change this setting on an ItemClass
, a permission-less update_item_class
call can be made to the Item
contract to enforce this down the ItemClass
's inheritance tree. The same goes for PlayerClasses
and Players.
An item in Raindrops is an artifact that has only limited state, mostly for tracking how many times it has been used, the last time it was used, cooldowns, etc. Items are primarily Consumable or Wearable, or both, and the data they contain describes the effect they should have on whatever is using them, as implemented by an enclosing contract.
The ItemClass can be separated into three clear sets of definitions:
Permissions for different functionalities (build/update/staking)
How an Item instance may be used (consumable/wearable, how many times, etc)
What component Items are required (if any) to build an Item instance of this ItemClass.
The Item contract can be separated into three clear functionalities:
Item Class Definitions
Item Recipe Creation
Item Activation (preparing and validating item for use by enclosing contract)
We’ll cover the state diagram and the underlying data structures first to get you warmed up to the concepts, then cover the functionalities.
All Raindrops artifacts that can be cached or assigned to a namespace have an identical first few fields.
Examples:
Note that even Namespace
has a namespaces section that can be used to join it to other namespaces, making it composable. Any artifact can join a namespace if it has this optional array up front with available slots (defined and set on creation).
Joining a namespace is done through the namespace contract.
It is not required to have a namespace for your raindrops artifacts. However, if you use namespaces, your artifacts become eligible to use staking and you can charge entry fees in $RAIN to join those namespaces.
Presently the Item contract is configured to allow for storing proof-of-data through the use of Merkle trees for ItemUsages, ItemUsageState, and Components. While all the actions and structs for using these roots and proofs are present, none of them have been tested and the CLI lacks support for these. Please avoid using this functionality and store all data on chain until this has been properly tested and debugged.
All components must be ItemClasses. Any Item that inherits from those classes directly can be used as this component. Adding support for a top-level ItemClass component that is more than one level up from an Item is on the cards in a future release but was deemed too complex for this iteration.
Each Component requires an amount of that type, and a condition. The allowable conditions are:
When Cooldown or CooldownAndConsume are used, the use_usage_index field is looked at to figure out what ItemUsageState to check in the given item’s usage state array to use when looking to see if the Item is on cooldown or not. In this way you can require a user to first bring an item to cooldown through a use before allowing it to be used as a crafting component.
When Consume or CooldownAndConsume are used, the item is destroyed if used as a component here.
Time_to_build should be the same across all components in a component_scope, and will be used (in seconds) to determine how long to force the user to wait once they enter the build phase for creating an item. A component_scope is an arbitrary grouping of components so that multiple component recipes can be stored in the same array without nesting.
This is a pseudo-diagram of the state defining an ItemClass, with all structs merged together:
As you can see, an ItemClass is made up of its recipe (the Components required to create it), and the ways in which it may be used (the ItemUsage), as well as various permissiveness settings for different kinds of actions. Let’s cover them one at a time.
A Player
in Raindrops is the primary primitive of the entire protocol. Just like Item
, it follows a class-inheritance system and has permissions for different functionalities. Every PlayerClass
represents a "blueprint" NFT that could be used to turn another NFT into a Player
, and once done, that NFT gains certain privileges, such as:
The right to store items and other players in their internal backpack, removing those NFTs/SFTs from your wallet, making for easy transport across your wallets. Never worry about transporting your player + 10 pieces of equipment from hot wallet to hot wallet again!
The right to equip items on various body parts
The right to have body parts!
The right to have different attributes of different data types that can change over time from outside stimuli, all on chain
The right to use items on oneself or on others per recipes encoded on chain
The underlying PlayerClass
for any Player
has permissions for different functionalities that control what a holder may do, such as:
Equipping, adding, or using items
Updating one's stats
Building new Players
Staking
We’ll cover PlayerClass
first, and then look at Player
to see how they connect, and then move on to some Live Fire exercises.
An Item can be used as either a Consumable
or Wearable
. In fact, an Item can have many ItemUsages
describing the different ways in which it may be used. For example, it may be that a legendary sword could both be worn and used in a spell - these are two different ItemUsage
s in the array.
Both ItemUsage
s can have BasicItemEffect
s, which look like this:
The primary difference is in the ItemClassType
enum of your ItemUsage
, which allows for different data storage depending on whether this item is being worn or consumed:
The Wearable
enum is focused on scoping and limiting on a per-body-part basis, which is similar to the component scoping used by the Components
array. It is up to the enclosing contract to enforce this.
The Consumable
enum revolves around setting usage limits, how many players can use it in a given use, warmups and cooldowns.
You may have noticed that both an BasicItemEffect
s, and the ItemUsage
itself, both can have max_uses
. This is so that you can have different effects wear down before the item as a whole does.
There are a few ItemUsageTypes
you can use:
Please note that Exhaustion
is not an allowable ItemUsageType
for SFTs, nor is max_uses
> 1.
Exhaustion
means an Item will not be destroyed once max_uses
hits zero (and max_uses
is required for Exhaustion
), but it will be rendered unusable for this particular ItemUsage
.
Destruction
means when max_uses
hit zero (and it must be set for Destruction
) will cause a token to be burned. For SFTs, with their required max_uses
== 1, this means one token is burned per use.
Infinite
is self-explanatory - max_uses
is ignored or unset, and the item can be used forever. Not applicable to SFTs.
When you define an ItemUsage
, it gets a corresponding ItemUsageState
in any Item
instance to keep track of usage, and cooldowns:
Obviously SFTs cannot have such state as all tokens in the SFT mint share the same Item PDA, which is why there are restrictions on them.
Just a side note, each ItemUsage
can have its own validation and callback (both of type Callback
). Validations are CPIs that are called when the item is being activated, whereas callbacks are intended to be CPI’d from the enclosing contract when it is enacting the Item
’s changes on its target(s). Here’s what a Callback looks like:
An example validation that will work on prod is a dummy endpoint added to the stubbed out Namespace contract. Here’s an example ItemUsage
from an ItemClass
definition JSON used for create_item_class
calls that will make this work:
Feel free to experiment with this. The additional code
is a u64
that is passed in just after the 8 bytes describing the instruction so it can be used for additional filtering. The contract implementing this must have a method called item_validation
in its main
module and must be an Anchor contract.
Okay so you’ve defined your ItemClass
, instantiated an Item
, and now you want to use it. What does that mean? Well, with composability as a central tenet of this protocol, it means that the Item contract should only be responsible for validating that it is possible to use an Item
, and then provide proof that you have decided to use the Item
. It is then the responsibility of the enclosing contract to call the Item contract back to let it know that the Item
has been used, and the activation period is over. Given the “price” of activation is paid up front, not calling back the Item contract simply means you cannot use this Item
again until you do.
To activate an Item
, you issue a begin_item_activation
call with a JSON file like so:
itemMarkerSpace must have a value in the range of 20 to 66, inclusive
This creates an ItemActivationMarker
that acts as a kind of ticket that other programs can use as proof of use:
Note that if you used a validation
key in the ItemUsage
being provided, it will be called during this begin_item_activation
stage. This allows you to hook into item use with custom feedback. It is up to your enclosing program to call the callback
after the enactment. What accounts (writable or otherwise) that you send in a callback is up to you - validation has a preset format that must be followed.
You can provide this PDA address to any enclosing program and they can validate that the Item
is valid for use (i.e. past it’s warmup state if one is specified) and how many of that item is being used (mostly going to be 1 but if it’s an SFT it could potentially be many).
Once your enclosing contract has “enacted” the changes the Item
’s ItemClass
describes, it can make a CPI call to end_item_activation
, or you can use the CLI:
If your Item
contains a warmup period, you may need to use this additional command after beginning item activation to push it into “valid_for_use” after the warmup duration:
To define a class, you’ll need to start with a JSON file:
Now there’s a lot to unpack here, so let’s go step by step.
You’ll also notice two special options which are optional Booleans (a boolean + an inheritance marker), free_build
and children_must_be_editions
. For a traditional ItemClass
, a lack of Components
means that it cannot be built. Setting free_build
to true
indicates that rather than indicating the ItemClass
cannot be used to build an NFT into an Item
, it can be done for free (no components required). children_must_be_editions
is fairly straightforward - all Item
s created from this ItemClass
must be a limited edition of the ItemClass
’s underlying NFT. This requirement is not enforced for child ItemClass
es.
Whenever you are using the JSON files to define Permissivenesses, you’ll notice notInherited
is exclusively used. This is by design. Overridden and Inherited values are provided by the program when class propagation occurs.
The config part of the JSON file covers how an item may be used, and what components are required to build it. You’ll notice that usages are required to have indexes that match up with their position in the array - this is for later use in item activation. You’ll also see that you can configure the way your item is used to have different permissions depending on the type of use.
The top-level of the JSON file lets the CLI know what permissions to use to define or update the class, how much space should be used for the ItemClass
struct, and whether or not you wish to store the metadata and mint fields on the ItemClass
for reverse-lookups.
Once you have this file, you can create an ItemClass
on top of an existing NFT (no SFTs allowed, sorry) like so:
Updating it is just as easy:
Remember, if you want to propagate updates from a parent ItemClass
to your ItemClass
, you must start at the highest parent that has changed, and issue updates in sequence from parent to child via the update_item_class
command until all updates are propagated. If you wish to do this permissionlessly, use the -iu
flag with update_item_class
.
Want to see what you made? Run show_item_class
!
Or from the client side, fetch an item class like this:
All Items start as bare-bones NFTs or SFTs that were previously made before starting the build process. You can either attempt to make them in the same transaction or bring them in from a prior. The Raindrops contracts expect them to pre-exist.
As a reminder, an NFT is a mint with supply = 1, decimals = 0, and both a Metadata and Edition or MasterEdition struct from the Token Metadata program.
An SFT is a mint with supply > 0, decimals = 0, and ONLY a Metadata struct from the Token Metadata program. Ideal uses for SFTs are single-use items like Potions so that you do not need to create a new mint for each potion. SFT-based items cannot have stateful usage types, such as a cooldown period.
To build an Item
from an ItemClass
:
Create an Item Escrow in which to store components from a specific component scope (recipe) for a specific index.
Add or burn or prove absence of required components against the escrow
Start the build phase
If there is no build time, building can be ended immediately. Otherwise, it must be ended once the build time duration has passed.
Now the NFT or SFT provided has a new Item
PDA with seeds [’item’, mint_id, index]
, where index is some u64
> 0 that corresponds to the index used to initialize the Item escrow.
Presently the CLI has the low-level commands for #1-5 but does not have a higher level smart command that can produce all these commands in aggregated transactions. We will be adding a single CLI build command where recipes are read and transactions are built to minimize overhead across all steps.
Let’s walk through how to create an item using the low-level commands. First, you’ll need a JSON file describing what you are trying to do:
You’ll need to give the mints for both the new Item
, and the existing ItemClass
. You’ll need to indicate to the ItemClass
which build permissiveness you want to use as this will determine what accounts the CLI will be passing up to authenticate you.
For every Item
or ItemClass
, it has a specific index offset against the mint to allow the creation of multiple classes and instances from the same mint, and you’ll need to indicate which you want to use for this creation.
The craftEscrowIndex
is used to allow you to have multiple builds progressing for the same ItemClass
against the same mint should you need it.
Components in the recipe list each come tagged with a componentScope
. When you choose a specific scope here, you are required only to provide components that have that tag. Imagine each scope as a different recipe to make the same Item.
If your ItemClass
has itself a parent ItemClass
, the parent
field will need to be set. If not, it can be null.
originator
is an optional field that should be used if those adding components to the escrow will not be the same signer as the one who starts the escrow.
When creating Item
s or ItemClass
es, you get to specify how much space you want to spend. In Solana 1.8 this space is fixed, so think carefully about how much data you plan to store on chain for this Item
before setting this. Depending on ItemUsageState
s, Item
s can be quite inexpensive.
The components
list is a list of actual Item
mints (and their index
offsets) you plan to use when assembling the component
s. This is as opposed to the component
list on the ItemClass
struct itself, which describes what ItemClass
es must be present. This list lets you choose which instance of a given ItemClass
you will use in this build.
Once this is setup, here are the commands, in sequence, that must be run for a single-component recipe:
Some things to note here: the -i
when adding and removing dictate what index
in the component
list you are trying to add (you should always start with 0 and move in order) and the -a
indicates how many of that item you are adding (most of the time, 1.)
If your recipe has a build time, complete_item_escrow_build_phase
will error out until that duration has passed. It is also important, but not required, to remember to run remove_craft_item_from_escrow
even if the Item
was destroyed as part of the crafting, as you will at the very least reclaim lamports for the token account on the escrow side.
The new Item
PDA is setup during the complete_item_escrow_build_phase
step. At this point, the item is now usable.
Want to see what you built? Run this:
You’ll need to supply the mint
and index
offset.
Or from the client side:
Want to cut out before you finish crafting? Simply deactivate and reclaim lamports:
Your settings
contains mostly permissions arrays that use the PermissivenessType
described in to tell the ItemClass
who is allowed to do which things. Examples include who can stake, who can unstake, who can build, and who can update.
While the Item contract has full support for Merklized data structures in the backend(as yet untested), Player does not. In the interests of speed and time, adding this feature was left for future development, if and only if the developer community demands it. As of this writing, we are not fully confident such a feature set is necessary for Players
or PlayerClasses
.
Here's a pseudo-code rendering of the PlayerClass state with all the structs merged together so that you can see it "all in one".
PlayerClass
is cut into two major areas: The settings and the config. The settings mostly covers who may do what to either the class or its children. There is even a permissiveness (child_update_propagation_permissiveness
) that covers which permissions and data propagate to children!
The config focuses on what most devs classically recognize as the "definition" of a Player
. You can define BodyParts
, BasicStats
, which in their blueprint form are called BasicStatTemplates
, and the starting URI for each Player
in this class. Players
will inherit this information as their starting state.
You can describe a starting URI as some third party link, similar to the URI in Token Metadata. Maybe it's JSON, maybe it's an image, it can be anything you like. All instantiated Player
from this PlayerClass
will start with it, and it should contain richer data about the Player
that cannot be contained within BodyParts
or BasicStats
. It works via 'write-on-change' so when you make an update to Player
, you can change it to a new URI that represents a small shift away from the common starting URI all Players
of the same class share.
Any change to a PlayerClass
can be permissionlessly propagated to all Players
via a hit to the update_player endpoint on the Player
contract for each Player
. So feel free to add a new body part to the Warrior class if you need it!
Here is the Player state:
The Player
state is much smaller than the PlayerClass
and is sort of the yin to the class's yang - it contains only the stateful pieces of information that vary from Player
to Player
. For instance, there is an array of equipped items, and for each BasicStatTemplate
, a corresponding BasicStat
that represents the current state of that Stat.
You'll also notice that, as with Items, it's possible for the PlayerClass
and Player
to be either entirely separate NFTs or indeed, the SAME NFT! Due to the index system used with the PDA seeds, you could have the same NFT have a class defined at index 0, and its instance defined at index 1. This would be the Singleton pattern for those of you familiar with OO programming styles.
All PlayerClasses
define an optional vec of BasicStatTemplates
that will map 1:1 to BasicStats
in Players
when instantiated. The Template defines what kind of stat it is, as well as its name, and the actual BasicStat
contains the current state for that stat. An example BasicStat
is "health," which might have a min/max of 0/100, and could be 50 on one Player
and 60 on another.
You'll notice the use of index on both structs. This is to assist in matching across both Player
and PlayerClass
in a more stable way than name
.
The meat of a BasicStatTemplate
is the BasicStatType
, defined below:
It is an enum that defines all the information a Player
would need to know to govern a stat of a given type as that stat moves in value over time. The actual state, called a BasicStatState
, is defined thusly:
With most of the data types the state is only pointing at the current value. The tricky part comes for Integers, which need to keep track of a bunch of intermediate values to track how items, both equipped and used, change the stat over time. When the contract "does math," it uses both structs in memory to figure out where to position the current
value next, however your Player
PDA is much smaller than the PlayerClass
it inherits from because it doesn't need to keep track of mins, maxes, or all possible values.
If you need a more complex type of data field than those provided here, the stats URI provided to every Player
by its PlayerClass
may be the right fit for you. You can store any type of data you want there, but it's up to you to handle changes to it and logic with it.
Let's create a PlayerClass
, and then, for giggles, we'll have it inherit to another PlayerClass
and then we'll cover updating it.
Every Player
may have BodyParts
if it so chooses. In the PlayerClass
, they are defined as:
On the actual Player
there is no corresponding state, as the only stateful part of a BodyPart
is what is equipped on it, and this is contained in the equipped_items
array. You'll notice that each BodyPart
has its own index - this, like BasicStats
, is a required unique field, to help disambiguate parts.
It's possible for a BodyPart
to equip more than one item (see total_item_spots
).
First, let's start with a standard JSON file for a PlayerClass
. As you can see, you don't need to set every field, most are optional. In fact, a lot of the fields we are setting in this example here are optional, but this is a working example so you can play with it as needed.
There's a lot going on here. To skip understanding it and just see how it can be made to "go," let's just run this command:
With this, a PDA has been made on top of your NFT. Check it out with the show_player_class
command:
This produces:
It is not itself a Player
but a blueprint for one. If you want to use it in games, you'll actually need to either instantiate a Player
from this class, or, as we'll do in this example, subclass this PlayerClass
one PlayerClass
further, and then instantiate a Player
. So now this NFT represents a blueprint for an Abstract Warrior. We plan to have a bunch of different Warrior classes, let's move on and subclass it to make a more specific warrior called Concrete Warrior in the next section.
If you're interested in what is going on in the JSON file above, please take a look at this "Breaking down the Magic" section below.
With Players
and PlayerClasses
, you can specify how big you'd like them to be but be aware that if they aren't big enough (totalSpaceBytes
) you run into serialization errors when you equip too many items, for instance. So size appropriately for your game. You can also decide whether you want to store the mint and metadata and edition pubkeys on the PDAs, which take up a lot of space but help with indexing by your front-ends using storeMint
and storeMetadataFields
. If you yourself are not the metadataUpdateAuthority
, make sure to set it. This is a Token Metadata concept from Metaplex - meaning you have the right to update the NFT.
When creating a PlayerClass
, the contract will always use updateAuthority
even if you put, as we did above in updatePermissivenessToUse
, tokenHolder
. The only way it will not use updateAuthority
when creating a class on top of an NFT is if the PlayerClass
being created has a parent
field set in the JSON, meaning it will be inheriting from some other PlayerClass
. In that case, the updatePermissivenessToUse
you chose (in this case, tokenHolder
) is passed along to the parent
, so you'd be trying to prove you have a right to create this PlayerClass
because you are holding the token of the parent PlayerClass
and that PlayerClass
has in it's update_permissiveness
array an entry for tokenHolder
, allowing for such a permission to be used on it.
If you want to have a parent
, parent
can fit the format, if not null, of:
Also take a look at this garbage:
What is all this? What this mess of JSON is saying that any child class inherits all BodyParts
from the parent, and it's defaultCategory
, but nothing else. This system of selective inheritance allows you to decide what parts of your classes filter down to children and what don't.
The overridable
field, when present, determines whether or not the child's values will be completely overridden if present.
So for arrays like BodyParts
, if the parent has a Head and a Neck in its initial JSON file and the child has an Arm in its JSON file, and overridable
is false, then the resultant child will have a Head, Neck, and Arm. If overridable
is true, then the child, despite being created with an Arm in its JSON file, will end up with only a Head and a Neck during the creation call. For single-value types like defaultCategory
, what overridable
= true means is that even if you try to create a child class with an explicit value for defaultCategory
in the initial JSON, it will be ignored, whereas if you try to set it in the initial JSON when overridable
is false, it will be overwrite what was in the parent.
Basically, overridable
true indicates that the parent
crushes everything beneath it, and overridable
false means that the parent
will step aside for the child
if the child has a better or additional idea of what a value should be.
Now that you've made a Player
and two PlayerClasses
, you probably want to clean up and drain them. This is pretty easy, just run the drain commands:
Be careful to run these in order - the protocol won't lead a parent be drained until all children have first been drained. Similarly a Player
cannot be deleted until all Items
have been unequipped, removed from the backpack, and had their effects removed from BasicStats
.
Ok, so now let's build a player. Here's the JSON I will use:
When building players, the JSON provided always needs at least two levels up - the Player's
class, and that class's class, if necessary (notice the parent
block.) Also, you'll notice from the previous page that the buildPermissiveness
array did not allow tokenHolder
permissions, so instead I had to rely on the fact that I had updateAuthority
on the NFT to build the Player
.
Also notice that I did something new here too with the NFTs. While the parent PlayerClass
NFT was located on mint GwB8Zt index 2, and the child was at edo3Dz index 4, I didn't use a whole new NFT for the Player
itself. I reused the second NFT, edo3Dz, just at a different index - 5! This is a very powerful design pattern you can use to create Singletons, if you wish.
Also notice that you get your choice of totalSpaceBytes
. Make sure to set it large enough to accommodate the maximum amount of equipment that can be attached to this Player
(the math is roughly 34 bytes multiplied by the number of BodyParts
multiplied by how many items per part can be applied). At some point in the future we will add dynamic account resizing to item equipping so that the account can grow and shrink as needed.
Here's the command to run to create the Player
:
After running, you can run the show_player
command to check out the state on chain:
Or from the client side:
You'll get something like this:
Note that on mainnet-beta, you'll be charged 1 $RAIN to build a player, and you'll receive that $RAIN back if you drain the player later.
Let's Subclass the Parent with this JSON file:
The key thing to really note here is the parent block in the JSON,
If you note from the prior section, this mint and index is the mint and index used to create the parent class. By referencing it in the JSON file here for the child class, we are telling the contract to consider this class a child. This also means that when we choose an updatePermissivenessToUse
for creation here, we're actually using a permissiveness on the parent, not the child. So that tokenHolder is actually referencing the parent token, not the child. This is a nice way to get around needing to own updateAuthority on a specific NFT if you don't have it but want to make a game with it.
To create the subclass, run the same command as before:
The resulting show_player_class
command will yield the following:
The clever among you will probably be wondering where the BasicStatTemplates
from the parent class are. If it had had BasicStatTemplates
in the childUpdatePropagationPermissivenessArray
, it would have! In this case, we didn't add it in the previous section, so this child will only have the two stats it defines for itself, but if you try this example with that minor tweak to the parent, you can see the inheritance work.
However, we did set the propagation up for BodyParts
, and this resultant class does have both a Leg and an Arm.
Once an Item
has been added to your backpack, if it has within it's list of acceptable ItemUsages
the ability to be equipped, you can equip it using the methodology described here. First, let's create a JSON file like this:
Here you can see that it's possible to equip multiple items using the CLI in a row, or unequip some and equip others, using the equipping
boolean in each object entry of the items
array. You'll also see that we are electing to equip this item at the BodyPart
with index 1 and we're using the ItemUsage
at index 0 of the ItemUsages
array on the ItemClass
of the Item
.
You can figure out what values are at these indices by running show_player_class
on the player-cli
and show_item_class
for the Item
using the item-cli
. Both will display the BodyParts
and ItemUsages
respectively and their indexes so that you can plug them in here.
Save this file and then run
After this, if you run a show_player
call, you'll see something like this:
Indicating that that item is now equipped on the BodyPart
at index 1, through the Wearable ItemUsage
at index 0 of the ItemUsages
array of the ItemClass
of this particular Item
.
When Items
are added/removed to/from backpacks, the Player program checks for the presence of an add_to_pack_validation
on the PlayerClass
of the Player
. If set, this callback is called, and if it fails, then the add_item
or remove_item
call fails. This can be quite useful for making sure users cannot add items that are too powerful to packs, or something like that.
The key layout for this callback's args is:
If the call is add_item
, it will try to call a method on your callback contract called add_item_validation
and if it's remove_item
, it will be remove_item_validation
. The args passed will be:
This Callback expects an Anchor program on the other end, so if it isn't one, God have mercy on your soul.
All Players
come with the ability to add Items
to their backpacks. They can even add other Players
to their backpack too, provided those Players
are also declared to be Items
, something completely possible in the Raindrops framework. Something to think about here would be multiple fighters being stored in a Battlecruiser. Here's how to add an Item
to a Player
. First, you'll need a JSON file that meets the following basic format:
and metadataUpdateAuthority
need only be present if you plan to use updateAuthority
as updatePermissivenessToUse
instead of something else. The playerMint
key is the mint of the Player
you wish to add the item to, and the playerClassMint
is the mint of the class that spawned that Player
. The index is the offset on the playerMint
. You can read more about index offsets here.
Then you can issue call from the player-cli using this json file and a few additional arguments, which we'll break down:
In this command, -m is saying the Item
mint I wish to add is 7lmih, it's index offset is 1, the amount of that Item
is 1(this could be an SFT Item
like a Potion that has many, hence the explicitness), and then finally the -cm stands for "Class Mint", which in this case is 7t5G3x, and points at the ItemClass
mint for this particular Item
. Between the config file passed and the arguments provided, the player-cli
is able to construct the command for the Player contract to have it move the Item
from your wallet to your Player's
backpack!
A similar command called remove_item exists that operates in reverse and takes the same commands.
If you wish to operate this command from your website, you can use the Raindrops SDK, which will look more like this:
When added, the Item
token account is stored as a PDA relative to the Player
PDA with seeds [ PREFIX.as_bytes(), item.key().as_ref(), player.key().as_ref() ]
It is explicitly not an associated token account, as to make it one would require an additional nested CPI which is a waste of CPU.
The benefit of this design is that your Player
can carry many items while appearing as a single NFT in your wallet. Only Items
carried by your Player
can be equipped.
The matches contract is a simple escrow contract that allows for an independent oracle to redistribute tokens added to the match. The idea is to support multiplayer gaming. This contract will be developed further in the future, to add new features such as:
Match defines maximum loss allowed by oracle for joiners to cross-compare
Match has support for player backpacks once player contract comes online, so that items can swap player backpacks without the player having to add those items directly to the match
Match CLI support for Merkle roots
Build out logic for minimum_allowed_entry_time
If an Item
is in your backpack, and it has an ItemUsage
in it's ItemClass
that describes it as a Consumable
, then your Player
may use the item. Before going through a live example, let's walk through the theory.
When a Player
uses an Item
via use_item
, it will do a CPI to begin_item_activation
to the underlying item contract, wherein it will create a ItemActivationMarker
PDA owned by the item contract.
However, this use_item
call does not actually affect the Player
. It just begins the usage. This may mean that the Item
is now in a warmup period or it may simply just mean that the Item
now has a PDA registering its "activated."
Either way, with this PDA in hand, the next step is to add the Item's
effect to the Player
using the add_item_effect
call, wherein the ItemActivationMarker
PDA will be destroyed through a CPI of end_item_activation
to the underlying item contract. It will be exchanged for a PlayerItemActivationMarker
PDA meant to keep track of information the player contract needs about what the Item
is doing to the player throughout the lifecycle of the Item's
use.
After this, the final call in the lifecycle of using an Item
is to subtract_item_effect
which will remove the effect from the Player's
stats. In cases where the Item
has a duration, this subtraction may not be callable until some time in the future. In cases where the Item
has no duration (like a Potion that adds 20 health immediately), this subtraction is a no-op that simply returns lamports to you and deletes the PDA.
TODO
The layout of Match is fairly simple - one defines a match that acts as a state machine, and gives it an oracle that can be controlled by a third party that can define outcomes. The match can define TokenValidations by which people are allowed to enter, and the oracle can define TokenDeltas by which tokens are redistributed to the entrants in the match.
Draft - Nobody can join the match in this state, but it is updatable by its authority.
Initialized - Entrants can join provided they meet at least one TokenValidation. No TokenValidations means anybody can join. Oracles cannot effect a match in this state. Can initialize the match in this state if you wish.
Started - Entrants can join provided the match has join_allowed_during_start is set to true. Entrants can leave if leave_allowed is set to true. This state can only be reached by the match authority updating the match from the Initialized state.
Finalized - This state can only be reached by the finalized boolean on the oracle being flipped to true. Then update_match_from_oracle can be called permissionlessly to get Match into this state. Once here, entrant’s tokens can be paid out to the original entrants, or new recipients, based on the TokenDeltas present in the oracle. Note: An oracle need not be created for a given match until you wish to Finalize the match.
PaidOut - This state is reached once all tokens have been removed from the oracle.
Deactivated - This state can be reached via a direct call to update_match while in Draft or Initialized state, or from PaidOut state. The Match MUST be in this state before drain_match is available to reclaim lamports.
Let’s use this live example JSON to illustrate how to create, enter, and complete a match:
This JSON actually is setup to do a few different things, at different points among the lifetime of the match. We’ll go over each as we do it. First, let’s create the oracle!
This uses the “oracleState” hash to seed and create an oracle. Whatever is in this hash file will be placed in the oracle every time the above command is run. Alternatively, if oracleState is null and winOracle is set, the expectation is that the oracle is not owned by the Matches program and you’re choosing not to use this endpoint to update it, but it still fits the standard.
Now let’s create the match:
Checking the token entry validation in our JSON, or via the show match call, run here, we can see that only one type of token can be added:
We happen to know that the first entry in the tokensToJoin list is the token whose parent matches, see:
We can also see that when this token is added, it will do a CPI call to a specific validation we set. This is entirely optional - removing the callback from the JSON would remove this requirement. It just allows the match author to add additional custom logic beyond the standard filters.
To add this token to the match, we run:
This command uses the “tokensToJoin” array in the JSON file and either iterates through and adds each token to the match escrow OR if -i is provided, only adds that index (as we are doing here). If we then try to add index 1, we will see it fail due to our token entry validation.
After this, we can change the entry for matchState to started in the JSON, then issue the update using the update command.
At this point, now we must use the oracle to determine the outcome of the match. Let’s update the Oracle hash to the following:
Please note that this JSON/update call combo will only work if your oracle is an internally produced one. If it is external, the external party must do their own writing. To issue this oracle state to the on-chain oracle, run
Now that the oracle has been updated, you’ll need to notify the match that the oracle has changed and it should “listen” to it. This will move the match from the Started state to the Finalized state.
Note that this call is permissionless! Anybody can do this. Now if we run show_match, we’ll see that the state has changed. Now it is up to permissionless cranks to follow the TokenTransfers provided and issue out the tokens to either original owners or new owners.
In this case, this will issue a single command to the cluster to move the token to its new owner. Now the match will have entered its final state, PaidOut, because all tokens have left the match escrow. Let’s drain the match and oracle and reclaim lamports!
All Artifacts, including Items, Matches and Players can be joined and then cached to a Namespace. This extends to other Namespaces. Namespaces within namespaces allow you to create hierarchies/groupings of artifacts for fine-grained organization and indexability of your Artifacts. This greatly speeds up query speeds and accessibility within a large collection of Artifacts.
When an Artifact is first created, the creator can define the number of Namespaces an Artifact may join, which can be changed at any time through the UpdateNamespace instruction.
initializeNamespace: Create a Namespace with the desired configuration
updateNamespace: Update the Namespace configuration
cacheArtifact: Add a Raindrops Artifact to the Namespace's cache, remember the Artifact can be another Namespace.
uncacheArtifact: Remove an Artifact from the Namespace cache, this is necessary to remove an Artifact from a Namespace completely.
createNamespaceGatekeeper: Initialize a Gatekeeper which can store Artifact Filters, only one Gatekeeper is associated to a Namespace at a time.
addToNamespaceGatekeeper: Add an Artifact Filter to the Gatekeeper
removeFromNamespaceGatekeeper: Remove an Artifact Filter from a Gatekeeper
joinNamespace: Add an Artifact to a Namespace, this must be done first before caching
leaveNamespace: Remove an Artifact completely from a Namespace, must already be removed from the cache.
Full list of Artifacts that are joinable and cachable to any Namespace.
Item
Match
Namespace
Player
Staking contract is the program that allows users to stake their own items and players. Staking items/players allows for the gamification of artifacts on-chain. For example, you can upgrade an item level, game character level, etc.
Creating a Namespace should be the first step when designing how to store state with Raindrops. During Namespace creation, it's possible to configure many variables, like which Staking mints are allowed to be staked, caching parameters and naming schemes. Creating Namespace Gatekeepers is also a configurable aspect to initialization, these Gatekeepers can have filters that determine which Artifacts are eligible to be joined to your new Namespace.
After a Namespace is created, next create a Raindrops artifact, these can be Item's, Matches or Players. After an Artifact is created, try joining it to a Namespace. Joining artifacts to a namespace allow you to associate and index them in different ways depending on your use case. It's possible to query for all Artifacts that belong to a given Namespace very easily.
Artifacts which are joined to a Namespace can subsequently be cached to the same Namespace. If an Artifact needs to be removed from a Namespace, it's first removed from the namespace's cache. Caching enables easy lookup of an Artifact's Namespaces.
These directions use the CLI, the example configuration is simply to understand the JSON structure you need to provider, use your own payloads.
Setup
Artifact Created
Namespace Initialized
Namespace Gatekeeper created
Artifact Joins a Namespace
Cache Artifact to the Namespace
Artifact Removed from Namespace Cache
Artifact Leaves a Namspace
Staking has two states, warmup and cooldown.
In warmup state, the staking tokens will be transferred from the wallet to a controlled escrow account and then to the controlled internal bank account of artifact. In cooldown state, on the contrary, the tokens will be transferred from the controlled internal back account of artifact to the controlled escrow account and then back to the wallet.
Warmup has two instructions.
begin_artifact_stake_warmup
end_artifact_stake_warmup
Here, begin_artifact_stake_warmup
will transfer tokens from the user wallet to the controlled escrow account and end_artifact_stake_warmup
will transfer tokens from the controlled escrow account to the artifact bank account.
And cooldown has two instructions.
begin_artifact_stake_cooldown
end_artifact_stake_cooldown
Here, begin_artifact_stake_cooldown
will transfer tokens from the artifact bank account to the controlled escrow account and end_artifact_stake_cooldown
will finally transfer tokens back to the user wallet.
To stake artifact, artifact must be joined to the namespace and the staking token must be whitelisted in that namespace.
It is possible to Stake tokens of your choice to a Namespace, when a Namespace is initialized, token mints can be provided which are whitelisted to be eligible to be staked to the Namespace. These mints can be updated at any time over the course of a Namespaces lifecycle.
Staking is a generic term here, because we believe staking tokens can mean a lot of different things depending on the game and situation. Maybe a Namespace is a house, and to build a new floor you would need to have a player gather wood, wood in this case can be tokenized, for example a $WOOD token. Once a Player has staked enough $WOOD tokens to a Namespace, it is possible to build another floor!
Namespace Gatekeepers are responsible for filtering which artifacts are allowed to be joined to a given Namespace. Each Namespace can only have one Gatekeeper but the Gatekeeper can have multiple filters attached, an artifact must pass all filters to be eligible to join a Namespace. To reiterate, Gatekeepers only filter on initial Joining actions to Namespaces, they do not filter on caching artifacts.
There are 2 different filter types and each type can be applied independently to each different type of artifact.
Namespace: Filters on an array of Namespaces
Key: PARTIALLY_IMPLEMENTED, Filters on arbitrary accounts, currently only mint
is implemented.
Permissiveness defines how the Filter Types are applied to a given Artifact when joining a Namespace
All: Automatically passes the filter, nothing is checked
Whitelist: Artifact must match the filter
Blacklist: Artifact must not match the filter
Namespace: Artifact token holder must sign the transaction for it to be joined to the Namespace.
During the initialization of a Namespace, PermissivenessSettings are defined for each artifact that can be joined to a Namespace. This allows granular permissives to be applied. For example, an Item can have a Permissiveness set to Whitelist while a Match can be set to Blacklist, both of these artifacts will run through the same set of filters but evaluated in opposite ways.
ArtifactClass
and Artifact
ArtifactClass
is an alias of ItemClass
/ PlayerClass
and Artifact
is an alias of Item
/ Player
. In other words, ArtifactClass
can be ItemClass
or PlayerClass
and Artifact
can be Item
or Player
. Artifacts and ArtifactClasses are not actually stored on-chain. They are only used for deserialize them to the same structure as they have their own structure in their own contracts and make them easy to manage in the staking contract.
StakingCounter
StakingCounter
records the type and start time of every staking process. If event_type
is 0, the staking process is in warmup state and if event_type
is 1, the staking process is in cooldown state.
Example config JSON: (stake.json)
To begin stake warmup:
To end stake warmup:
To begin stake cooldown:
To end stake cooldown:
Boot-up is a command line interface for making a collection work with the Boots service. If you have a NFT collection currently on-chain, the steps in this guide will "boot up" that collection.
In order to use the Boot-up CLI you will need the following:
An on-chain collection of NFTs.
The NFTs in this project are configured with a Collection NFT. For more on Certified Collections, please visit https://docs.metaplex.com/programs/token-metadata/certified-collections
The Solana command line toolset (https://docs.solana.com/cli/install-solana-cli-tools) - to interact with the Solana network.
1 $RAIN (Mint Id rainH85N1vCoerCi4cQ3w6mCf7oYUdrsTFtFzpaRwjL
) for each player in your collection.
Boots is a proprietary NFT upgrade service that exists on top of the Raindrops Protocol that allows NFT holders to trade and collect traits on their NFT in a controlled and fee-gated environment that produces reliable new revenue streams for NFT companies and the platform and is consistent with web3 principles.
The only way to customize an NFT presently is to trade it for another. The best NFT collections are the ones with enough trait variance that a given user can find a PFP that matches about 80% of their personality. Inevitably, the quest for a "forever PFP" is out of reach for most and more importantly, doesn't lead to much money exchanging hands.
In the traditional secondary sales model, volume and therefore income to NFT companies decreases over time as collectors settle in for the long haul. There is little incentive to trade on existing traits, and speculators lose interest in collections that are no longer new.
Releasing new sub-collections to revitalize revenue is a short-term salve. The long-term effect is that your brand is now diluted across many different collections, all which share the same-sized user base and compete for volume. You may have won a short-term cash prize but in the long term you now have more collections to maintain and a wider roadmap to service.
Royalties on Solana are unenforceable because the Token Program has no idea what they are, and doesn't care what they say when a user initiates a token transfer. There are no plans by the Solana team to merge the Token Metadata program into the Token Program at any point in the future to rectify this. The only real solution to this is to either turn the Token Metadata program into a clone of the Token Program, which is infeasible, or enforce royalties punitively, which does not have 100% efficacy.
All NFTs come with some traits that are customizable and some that are inherent traits, as defined by the Raindrops protocol. Think what shirt an NFT is wearing vs its skin color. The shirt is a token that the NFT is carrying in its backpack and is presently equipped. It's skin-color is a piece of data recorded in an on-chain account similar to Metadata from Metaplex. The token can be unequipped, removed, and like any old semi-fungible token, traded. On removal or addition, the NFT's image is updated to reflect the image change.
Now people can truly customize their NFT, and can get to 100% match without breaking the bank!
With Project Boots, new SFT tokens representing new traits can be dropped by the season or monthly using the Launchpad feature. This can take place through collaborating and splitting revenue/royalties with artists, other major clothing brands, or by releasing your own new traits. There will also be opportunities to use Cupcake technology to couple the clothing to IRL phygital goods.
Fashion brands have used this technique from time immemorial to revitalize their income streams every season and every year. Why trade on last year's trait?
With each new trait collection, the power of expression for your NFT collection grows. Without sub-collections to dilute volume, it remains moving in the base collection, propping up royalties there. Customers will be forced to keep up with each monthly or season installment of traits or their NFT will fall out of cultural relevance. Old traits will trade on a secondary market similar to discount stores found in the real world today.
With the Project Boots system, while our marketplace does enforce royalties when the secondary market is used for trait token sales, this is not intended to be the primary source of revenue. All Boots enabled collections require the authority on the collection to sign off on any addition, removal, equipping, or un-equipping of any trait on the collection. What this means is that the NFT Collection Owners are the gatekeepers of change.
When a user wishes to apply a new trait in their NFT's backpack, they could attempt to sign a transaction that tells the Raindrops protocol to do this, but it would fail because they lack permissions on Boots NFTs. They instead must cosign a transaction with the NFT Collection Owner. The Boots system will provide such a cosigned transaction, but this transaction will come with a standard SOL fee as a side-instruction - which will be split 20% with the platform and 80% among the royalty recipients on the NFT Collections.
This new revenue source is guaranteed by the protocol, cannot be bypassed, and while smaller in magnitude than a secondary sale, is going to happen significantly more frequently. Web2 has already proven the magnitude and power of micro-transactions. This is the future of the NFT ecosystem and the future of your company.
Welcome to Boots.
The boot-up command line interface is used to prime your collection to be used with the Boots protocol.
We will take you through the following steps:
Create Main Class
When you build something, you need a blueprint. We will show you here how to create your blueprint on-chain to allow your project to be booted up.
Create the Items Collection
This step will establish the collection for your items. It establishes this collection of NFTs or SFTs to be able to be equipped on the NFTs of your collection.
Create the Item Classes
Each of the items that you want to equip on your NFTs will need to have its own blueprint. This step creates that for each of your booted items.
Create Players
This last step establishes the NFTs in your collection as Bootsified NFTs.
We will assume that you have an on-chain collection of NFTs, and assume you are familiar with using a command line interface (CLI) to execute commands on your operating system of choice.
The Boot-Up CLI is part of the Raindrops protocol. Install the Raindrops CLI using the instructions found here, the Boot-Up CLI is included in the raindrops-cli
. Once installed, you will have access to a sample configuration file which works with the Degenerate Trash Panda collection.
To find this, use the following commands:
npm list -g
To find the location of the raindrops CLI on your machine. You will get something similar to the following:
/Users/<your user name>/.nvm/versions/node/v18.8.0/lib
Once you are in the directory specified, make your way to the boot-up example-configs directory, located at
node_modules/@raindrops-protocol/raindrops-cli/example-configs/boot-up
Rename the pandaExample.json file to reflect your project name and move it to the desired working directory
mkdir ~/boot-up
cp ./pandaExample.json ~/boot-up/myProject.json
This file establishes the definition of the boots-enabled configuration of the collection. You will want to keep this file safe, saving often as you go through this process because as you boot-up your collection the configuration is updated with on-chain ids and other configuration items that will be important to complete the process.
Let's have a look at the keys in the configuration file and what they mean
This section covers the steps to use the CLI to boot-up.
This page describes the options you can set as part of the configuration
The scope defines what type or set of NFTs you want to establish with Boots. There are currently three options
If this is set to true, the Boot-Up CLI will scan the NFT to make sure that they weren't given more than one of each trait. This process only applies to NFTs that have reached their final state.
*IMPORTANT NOTE* DO NOT set this to true after the collection is being used by your community. This will prune duplicate tokens from the Player class of the NFT, so TAKE CAUTION HERE.
This serves as a key to save the process of the Boots conversion for your NFTs. This will be constantly updated during Step 4 - Create Players, so the initial value for this key should be set to null.
This is a whitelisted array of the body parts you would like to enable for boots. These parts can be changed in your collection.
This array is the set of attributes that you do not need to be able to change through Boots. These are things like the base body for your character. In addition, users will be able to change these items by equipping them as consumable items, and that these will be stored as items on-chain.
This is an array of configuration sets for other non-graphical statistics that you would like to apply to the player class of this collection. The form is described here: Player - Basic Stats
if true, these items will be created as Semi-Fungible Tokens (SFTs).
the name of the on-chain class for this collection
reserved for the namespace for this collection. Namespaces are currently a work in progress, so stay connected to @only_raindrops on Twitter for updates.
Set skipUpdates to True if you do not want the player to be updated with any changes that have happened to the NFT's parent class since the last run. For instance, if a new layer is desired, or if you want a new statistic applied to this player definition, you would want to keep this set to false.
Setting this to true means that Boot-Up will retry configuring your NFT for Boots if an error happens during the run. If you set this to false, the error will propagate up and the next NFT will be tried. It is recommended to keep this set to true.
Set this to true to allow a holder to be able to equip their NFT in this collection. If false, then the collection owner must be the one to equip the collection NFTs. In addition, fees for equipping items can be circumvented if this is set to true.
This is where you are defining the items that will be able to be equipped on your players.
For each item, you will add a default boilerplate entry.
The layer will be populated by Boot-Up with the configuration, so leave it defined as is.
In addition, this is where the bodyPartLayers comes into play. If you want to enable creating NFTs or SFTs for these items you will need to whitelist them in the bodyPartLayers key.
This is the location of the directory that holds the images for the individual items. When the process runs, it will create the NFT or SFT on chain for you, and this is where you have the image files specified.
NAMING CONVENTION FOR THE IMAGE FILES
Because the system needs to be able to find the files in your file system, you will need to follow a character substitution pattern when you name your image files. In general, use an underscore ( _ ) character to substitute for whitespace, and replace any special characters as follows:
So a layer named
"EXALTED_STAT | RUGGED_COUNT: 2"
would end up with an image file named
"EXALTED_STAT__RUGGED_COUNT_2.png"
This is the mint id for the collection that you intend to enable with Boots.
This is an optional path to an image file used when creating the collection for items. If this is not set, the first bodyPartLayer trait image file will be used.
This is the mint that will be used to represent the master PlayerClass for Boots. You can place the collectionMint here if you control it and are the UpdateAuthority but if you don't have those things - no worries as they are not required. You can leave it null, and we will make one automatically when the script runs.
Raindrops allows each NFT in principle to have multiple items and players defined on top of it through the use of indexing. Therefore, for each Boots collection, you must label which index you decided to use for the items and the NFTs themselves.
Raindrops allows each NFT in principle to have multiple items and players defined on top of it through the use of indexing. Therefore, for each Boots collection, you must label which index you decided to use for the items and the NFTs themselves.
This is where the CLI will store the class information for the Items class of the collection. This is automatically populated by the application, so just leave it at null
to start. In general, this specifies the abstract top level class of the ItemsCollection. The ItemClasses for the collection inherit their properties from this ItemsCollection, and then each Item inherits properties from its ItemClass. For more information on how Items are composed, have a look at that section of the Raindrops documentation.
This is where the application will store the class information for the collection's main class. This is populated by the application.
This will be the name of the collection NFT for the class. When the application creates the collection NFT for Boots, this is the name it will apply to the Collection NFT.
This is the public key for the Collection NFT of the items for the collection. This is populated by the application.
Id | Scope | values example | Description |
---|---|---|---|
character | replace with |
---|---|
0
List of NFTs
["GiD1ex1VyEXALniJJvL6KcgQ5mGB4BLzvjM1bQzUXs1T","Cu89qmdY4a6dfUPa9sVZT6W9BTXqZq18d51A5zFGZuG7","4ahpNZJ7pVS85f34ErDXwRmckhmbmvT52ed6kr6J66ZA","ENpscuTJCYReY3NnNcRBJnC7S6pAYbxSfAdcdnJjvxN5","G2fWcgYP49p4J1Db8Jnj3KBJrg1fp1DsfzBo4uJb8u4j"]
A list of NFTs, specified as Mint Ids in the values key
1
Candy Machine Id
["2crTqcmVJVMW6b5z6NEs9HdsKmVc4zDDE2FucNg98kSN"]
Use the Candy Machine Id (i.e. First Creator) to define the collection
2
Collection Id
["CcLrhgWz29JRd3TgxSNEeWwMfMoX9Cz1NQXh27aWUMMx"]
Use the Collection NFT Mint Id
whitespace (spaces, tabs, etc)
"_"
pipe (|)
":"
colon (:)
""
forward slash (/)
":"
Review your Player and Item class enabling with Boots
Once you have completed all 4 steps to enable your collection to work with Boots, you can validate what your player and item classes look like by using the Raindrops CLI. First, go to the configuration file that you have been working with. An example of a finished configuration file is in the Examples, here. You will want to copy the Player Class configuration that was generated by Boot-Up
Search for existingClassDef
in the file and copy the object into a new configuration file. We will name this bootsPlayerConfig.json for the purposes of this example.
2. Make sure you know the location of your keypair file. For our purposes, we will name this bootsUpdateAuth.json and put it in the same location as the config file from step 1.
3. You will also need the masterMint. You can find that in the "masterMint" key from the configuration file you created in step 1. For our purposes here we will be using the Mint Id BNpznqP6Rfy8LhPHEQ23qBYFiAueKiXx9jRaW6WnTo1z
4. Use the following command to show the Player class:
The result of this command is the following:
should match the number of NFTs in your collection
Should show the newBasicStats (i.e. mutationLevel) as well as the stringLayers (i.e. BACKGROUND, FUR) that you have specified in your configuration.
Shows Items that can be equipped for the NFTs in your collection.
For more detail on this output, review "creating a Parent PlayerClass"
Events is a free-form service that allows you to create an event that can be made up of any number of contests or objectives that an individual or group of individuals can participate in. These contests can have a single winner or you can apply rankings based on the performance of the individuals in the contest. Individuals in a contest can be grouped to form teams. In this scenario, an entire team can be declared as the winner.
Entry to an event can require a payment via any SPL-token (USDC, $RAIN, ...), NFT, SFT, or Raindrops artifact(Item, Player). Upon entry to the event, participants can be granted an NFT to signify as their ticket. Holders of the event's entry NFT can then join contests, which may be free, require a specific type of entry NFT, and/or require an additional payment.
Each event can have an "unlimited" amount of prize pools that can be made up of SPL-tokens, NFTs, SFTs, and/or Raindrops artifacts. These prize pools can be distributed to the winners of contests and/or the winners of the entire event.
The entirety of the event is played out on-chain. When the event is declared finalized and all rewards have been distributed, the event data will remain as the permanent history of the outcomes during the event.
Given the highly free-form nature of the events service, we are first providing it's functionality via the opinionated Tournaments service. The Tournaments service is meant to define the typical structure of a competitive series of match based contests. We will be providing more opinionated event services over the coming months. Two other opinionated service examples are Campaigns, and Scavenger Hunts.
This step creates the player class on chain and stores the information needed in the configuration file.
To run, issue the following command:
The command line parameters are as follows:
The output of the help option is the following:
After this command is run, the existingClassDef
entry of the configuration file will be updated with the class definition information.
Once this command finishes successfully, you can move on to the next step
This step creates the classes for the individual items that belong to the collection.
To run, issue the following command:
The command line parameters are as follows:
The output of the help option is the following:
After this command is run, classes for each of the items that you specified in the configuration are created and populated in the configuration file, ready for your NFTs to be equipped.
Once this command finishes successfully, you can move on to the next step.
This step creates the Items collection and stores the information needed in the configuration file.
To run, issue the following command:
The command line parameters are as follows:
The output of the help option is the following:
After this command is run, the existingCollectionForItems
entry of the configuration file will be updated with the item class definition information.
Once this command finishes successfully, you can move on to the next step.
This step uses the configuration file built from the previous three steps to build and equip player classes for your collection NFTs.
To run, issue the following command:
The command line parameters are as follows:
The output of the help option is the following:
PRO TIP: You can break up the running of Step 4 into parallel processes. To do this, do the following:
1) Use an external method to generate your mint list.
2) Divide the mint list into 4 separate lists.
3) Create 4 copies of your configuration file
4) Set the following keys in each of your configuration files:
5) Open up 4 separate terminals and navigate to the folder with your configuration files and wallet. Set up the boot-up step_four_create_players
command in each terminal with a different configuration file for each, like so:
6) Once each job completes, open up the configuration file for that job and update the "runDuplicateChecks" key to true, and then run the job once more to remove excess items.
After you finish "Step 4 - Create Players" and it successfully completes, you can validate using the Raindrops CLI. See the next section for an example of how to verify your Player class.
option | description |
---|---|
option | description |
---|---|
option | description |
---|---|
-e, --env
Which environment to use (devnet, testnet, mainnet-beta, localnet)
-r, --rpc-url
The RPC to use to connect to Solana. This should be a paid RPC, as this application causes heavy use to the RPC.
-l, --loglevel
The log level to use (all, trace, DEBUG, info, warn, error, fatal, off)
-k, --keypair
The keypair which is the update authority for the NFT collection
-cp, --config-path
The path to the configuration file
-h, --help
Get help with this command
-e, --env
Which environment to use (devnet, testnet, mainnet-beta, localnet)
-r, --rpc-url
The RPC to use to connect to Solana. This should be a paid RPC, as this application causes heavy use to the RPC.
-l, --loglevel
The log level to use (all, trace, DEBUG, info, warn, error, fatal, off)
-k, --keypair
The keypair which is the update authority for the NFT collection
-cp, --config-path
The path to the configuration file
-h, --help
Get help with this command
-e, --env
Which environment to use (devnet, testnet, mainnet-beta, localnet)
-r, --rpc-url
The RPC to use to connect to Solana. This should be a paid RPC, as this application causes heavy use to the RPC.
-l, --loglevel
The log level to use (all, trace, DEBUG, info, warn, error, fatal, off)
-k, --keypair
The keypair which is the update authority for the NFT collection
-cp, --config-path
The path to the configuration file
-h, --help
Get help with this command
-e, --env
Which environment to use (devnet, testnet, mainnet-beta, localnet)
-r, --rpc-url
The RPC to use to connect to Solana. This should be a paid RPC, as this application causes heavy use to the RPC.
-l, --loglevel
The log level to use (all, trace, DEBUG, info, warn, error, fatal, off)
-k, --keypair
The keypair which is the update authority for the NFT collection
-cp, --config-path
The path to the configuration file
-h, --help
Get help with this command
Tournaments is an Events service for creating and managing tournament events. You can use an HTTP client to interact with our API. Soon after the initial release of the API you will also be able to use a Web UI to create and run a tournament.
Unlimited matches and participants per tournament
Rewards escrow (aka Prize pool)
Entry fees can be used for rewards
Unlimited number of rewards can be held
Customizable reward distribution
Optionally enforce token gating for tournament entry
Optionally require entry fees to join a tournament
Ban tournament cheaters or revoke any of their rewards for cheating in a match
Data availability, it's simple to query the existing state of the tournament without having to store anything client side
Tournaments users will interact with the Tournaments API via HTTP, this allows flexibility for a variety of user types with the service, we will proxy your requests to the blockchain for you after signing.
We support a typescript HTTP client which will always be compatible with our Tournaments API, we suggest starting here to try things out. The client is designed to be usable from web applications and scripting.
Tournaments is built on top of a generic Events Program, please refer to the Events Reference Errors & State Pages for detail. On those pages, a Tournament is an Event and a Match is a Contest.
Update the NFT with the new equipped items.
First we need to prepare the payload to create the transaction for the user to sign.
Then we send the payload off to the Boots API
If the resultant licenseState
is "SUB_LICENSE_FILLED"
the user will not be able to proceed.
If the resultant licenseState
is "SUB_LICENSE_AVAILABLE"
the user will need to pay the license fee.
However, if the licenseState
is "NONE"
we can continue to sign the transactions and send a call to the API to update the metadata:
An EntryFee can be optionally set at Tournament creation, requires all participants entering the tournament to pay a fee upon entrance. There can only be one tournament entry fee at this time
Multiple Matches can be created within a Tournament, each Match can have an unlimited number of participants, winners of matches accrue Points.
A Participant is an entity which has entered a Tournament and is eligible to play matches
Points are accrued by winners of Matches within a Tournament. These points are used to keep track of which participants are eligible to move onto the next Tournament Round. Throughout the Tournament it's possible to query the point totals of all participating. Tournament Authorities can use these point totals to determine distribution of Tournament Rewards. In addition, if it's determined that one or multiple participants have cheated, their points can be revoked.
Rewards are used to incentivize participants to enter into Tournaments. There can be unlimited number of rewards and the Tournament Authority has complete control over the distribution of these Rewards after the Tournament has been moved to the Ended State. Optionally, the Tournament Entry Fee tokens can be used as a Tournament Reward.
A Round is a grouping of matches within a Tournament, each Round has a unique Round Index. The first round in a Tournament has a Round Index of 0, this means that any participant is eligible to play in the first round of matches (provided they have entered the Tournament of course!).
The winners of each match within a Round are awarded 1 point, this corresponds to the Round Index. For example, the Second Round in this Tournament has a Round Index of 1, which means participants who have been awarded 1 Point from playing in the previous round are eligible for these new Matches.
The farther along a participant makes it within a Tournament, the more points they accrue and the Tournament Authority can query these point totals to make a decision on Reward distribution.
When a Tournament is created it's possible to add a Token Gate, a Token Gate requires that a prospective participant has a particular mint and amount of said mint in their wallet upon joining. One use case of this might be a whitelist token, only allowing participants with the token to be eligible to play in the tournament.
A Tournament is a series of Matches in which participants compete for escrowed Rewards, the Tournament results are recorded on chain and when complete, are immutable and forever public.
A comprehensive list of endpoints exposed by our Tournaments API
Each Endpoint has a GET and a POST operation, the GET will generate a prepared transaction for the endpoint, complete with signatures from newly created accounts and our API signer. Next, this transaction must be signed by the caller and send back in a POST body. We will validate the transaction received via HTTP POST and forward this along to the Solana network.
Transfer tokens collected by a Tournaments Entry Fee to the Reward Escrow, this enables the Entry Fee to be redistributed to tournament participants. This endpoint is only callable by a Tournament Authority.
Transfer tokens from the Tournament Authority to the Reward Escrow. It's important to specify the Reward Amount with the correct decimal precision. This endpoint is only callable by a Tournament Authority.
Example: 1 $USDC would be a reward amount of "100000", USDC has 6 decimals of precision.
Revoke all tournament points gained by a detected cheater in a tournament. This endpoint is only callable by a Tournament Authority.
Create a new round of matches within a Tournament. For each new round, increment the Round Index by 1, the API uses the Participants per Round to determine the number of matches to create for a round. This endpoint is only callable by a Tournament Authority.
Create a new Tournament, the creator mints a Tournament Authority token granting them full control over Tournament operations.
Specify a Name and a Tournament Size, optionally specify an Entry Fee and/or a Token Gate. The Name cannot be longer than 100 characters. The Size sets a limit on number of participants who can join, but this limit does not need to be reached to start the Tournament.
An Entry Fee requires a Mint and an Amount which each participant must pay to enter into your Tournament.
A Token Gate requires a Mint and an Amount which each participant must have in their Wallet to be eligible to enter into your Tournament.
Distribute the tokens stored in a Tournament Reward Escrow to a Tournament Participant
Update the state of a Match to Ended. This endpoint is only callable by a Tournament Authority.
Before a winner of a match can be determined, the Match must be in the Ended State.
Update the state of the Tournament to Ended. This endpoint is only callable by a Tournament Authority.
When a Tournament is moved to Ended, no more Matches or Rounds can be created. Cheaters can still be detected and banned at this stage, this is where rewards would be distributed to eligible participants.
Enter a Tournament Participant into a Match. This endpoint is only callable by a Tournament Authority.
The Tournament Authority is in control of which participants enter which Matches so that the Tournament Authority can control bracketing and matchmaking semantics.
A participant uses this endpoint to enter into a tournament of their choice. If the tournament has an Entry Fee or Token Gate configured, the participant must pay or have in their wallet the necessary tokens.
Finalize the results of a Tournament, all rewards have be distributed, cheaters banned and the results of the Tournament become immutable. This endpoint is only callable by a Tournament Authority.
Retrieve Match Details in JSON format
Retrieve all Matches of a particular Round in a tournament. Use the Round Index to select a particular round. Flip the finalized Boolean if you would like to selectively retrieve Matches which are finalized or not.
Retrieve all participants for a particular Match.
Retrieve all participants who are eligible for a particular Round, regardless if they have entered a Match in that Round or not.
Retrieve current Tournament Standings sorted descending from most points awarded. For each Match win a participant is awarded 1 Tournament Point.
Retrieve Tournament Details in JSON format
Determine a Winner of an Ended Match and move the Match into the Finalized State. This endpoint is only callable by a Tournament Authority.
The Winner will receive 1 Point in their points token account. Tournaments uses the points to determine which participants are eligible to play in subsequent tournament rounds and for participants and managers to keep track of Tournament state.
Currently only 1 winner can be selected but we will make this endpoint more flexible to support multiple winners in the future.
Move a Match within a Tournament into the Started State, this signifies that the Match is beginning to be played out on chain, no other participants may enter this Match. This endpoint is only callable by a Tournament Authority.
Move a Tournament into the Started State, no more participants may enter the tournament and now it's possible to begin creating Tournament Rounds. This endpoint is only callable by a Tournament Authority.
This first script highlights tournaments setup taken by the Tournament Authority, the Authority has full control over configuration, point and reward distribution.
With the Tournament configured, participants can now enter the tournament
Once all participants have entered, it's time to start the tournament. When the tournament moves into the started state, no more participants can join.
Now that the tournament has started, it's time to create a tournament round. A round is simply a collection of matches in which participants face off against each other.
After the round of matches have been created, it's time to select which participants face each other in each match, this is completely up to the Tournament Authority
~Matches are now played off chain~
Once the matches finish, it's time to tell Tournaments who won!
At this point, a full round of a tournament has been played out off-chain and the results have been written on-chain. Repeat this flow until a tournament winner has been determined. For each round, increment the roundIndex
by 1.
Lets say the final tournament standings have been determined, it's time to wrap this up!
Accounts and Data Structures
Errors are returned in hexadecimal format from the Solana Blockchain, convert to decimal then reference this page to determine the error. If the decimal returned is not a 6XXX number, then the error is a built-in error from the Solana blockchain, reference here to troubleshoot: