On Chain Generative Art with 8Bidou
A very brief but hopefully informative introduction with some code!
Lacking Definition is a project that I (Landlines Art) created, which uses a custom smart contract to generate 8x8 pixel artwork on 8Bidou in a manner that is completely on-chain. To be clear, this means there is no manual intervention at any point, there are no off-chain dependencies, and EVERYTHING happens in a single transaction. In this post, I briefly outline how one can go about creating a smart contract that does this sort of thing.
A few notes before we begin. To keep things simple, I will be using an uninspired generative algorithm (each pixel is simply assigned a random color) instead of the actual algorithm I wrote for Lacking Definition. This post assumes some familiarity with smart contracts and coding principles. For those unfamiliar with coding smart contracts, check out the resources available here.
Before we start coding, we of course need to import the required dependencies. In our case, there is only one dependency, smartpy.
import smartpy as sp
When writing a basic smart contract, we need to be concerned with two things:
Storing data we need to access throughout the life-cycle of the contract.
Writing entry points that can be called, allowing accounts to interact with the contract
The contract storage is specified in the __init__ function of the contract class, as shown below. The init_type function, defined in the sp.Contract base class, is used to specify the type of each item in the contract storage. The init function is used to initialize the values of each item in the contract storage. We also define a helper function for random number generation, which uses the state variable in the contract storage to generate a stream of random integers via the xor shift algorithm.
class MyContract(sp.Contract):
def __init__(self):
self.init_type(sp.TRecord(
# incremented each time an NFT is minted
token_id=sp.TNat,
# specify the 8bidou FA2 contract address
fa2=sp.TAddress,
# the state for random number generation
state=sp.TNat,
# the creator address on 8bidou
creator=sp.TAddress,
# the creator name on 8bidou
creator_name=sp.TBytes,
# the title prefix on 8bidou
title=sp.TBytes,
# the description on 8bidou
description=sp.TBytes,
# a place to store the actual pixel data
pixel_storage=sp.TString,
# a place to store the formatted title
title_storage=sp.TBytes
))
self.init(
token_id=sp.nat(0),
fa2=sp.address(
"KT1MxDwChiDwd6WBVs24g1NjERUoK622ZEFp"),
state=sp.nat(4728139),
creator=sp.address(
"<YOUR ADDRESS HERE>"),
creator_name=sp.bytes("0x20"),
title=sp.bytes("0x7469746C6520"),
description=sp.bytes("0x20"),
pixel_storage=sp.string(""),
title_storage=sp.bytes("0x")
)
def rng(self):
self.data.state ^= self.data.state << 13
self.data.state ^= self.data.state >> 17
self.data.state ^= self.data.state << 5
self.data.state %= 4294967295
return self.data.state
Now we must create an entry point, which will both generate and mint the artwork. To define an entry point, simply create a method in the MyContract class, and apply the sp.entry_point wrapper. I know it’s a big chunk of code, but I tried to be as descriptive as possible with the comments below. In short, when a collector calls the mint entry point on my custom contract, two things that need to happen:
The generative algorithm is executed, producing artwork encoded as a string of hex values. This is stored in pixels temporarily while the artwork is being constructed, and semi-permanently (it gets overwritten by the next contract call) in the pixel_storage field in the contract storage.
The second chunk of code provides the artwork (a string of hex values), along with some other arguments when it calls the mint entry point in the 8Bidou FA2 contract. This is the part that actually creates the NFT.
@sp.entry_point
def mint(self):
# if you want to collect payment
# or check other things do this here
# or somewhere within this entrypoint
# store the colors we want to use
color_options = [
"21325e",
"3e497a",
"f1d00a"
]
colors = sp.map(
{i:x for i,x in enumerate(color_options)},
tkey=sp.TNat, tvalue=sp.TString)
# create a blank canvas to store pixel data
# we use sp.local variables when we want to manipulate
# the variable while in a loop of some sort
pixels = sp.local(
"pixels",
sp.map({}, tkey=sp.TNat, tvalue=sp.TString))
# set each pixel to a random color
# (i.e. the worst generative algorithm possible)
with sp.for_("i", sp.range(0,64,1)) as i:
pixels.value[i] = colors[self.rng()% sp.len(colors)]
# store the pixels as a single string
self.data.pixel_storage = sp.concat(pixels.value.values())
# double check we have a valid image
# 6 * 64 = 384 characters in hex
sp.verify(
sp.len(self.data.pixel_storage)==384,
message="GENERATION_FAILED")
# this map is used to convert integers [0-9]
# to ascii hex values
int_to_hex = sp.map({i:sp.bytes("0x" + "".join([hex(ord(c))[2:] for c in str(i)])) for i in range(10)})
# we convert each digit at a time
# then we concatenate these parts together
# giving us titles in the form "title 0001"
self.data.title_storage = sp.concat([
self.data.title,
int_to_hex[((self.data.token_id + 1) / 1000) % 10],
int_to_hex[((self.data.token_id + 1) / 100) % 10],
int_to_hex[((self.data.token_id + 1) / 10) % 10],
int_to_hex[(self.data.token_id + 1) % 10]
])
# specify the data type that the bidou expects as input
MINT_TYPE = sp.TRecord(
mint_tx=sp.TRecord(
owner=sp.TAddress,
token_id=sp.TNat,
amount=sp.TNat
).layout(("owner",("token_id","amount"))),
token_meta=sp.TRecord(
token_id=sp.TNat,
token_info=sp.TMap(sp.TString, sp.TBytes)
).layout(("token_id","token_info")),
rgb=sp.TRecord(
token_id=sp.TNat,
creater=sp.TAddress,
creater_name=sp.TBytes,
token_name=sp.TBytes,
token_description=sp.TBytes,
rgb=sp.TString
).layout(("token_id",("creater",("creater_name",("token_name",("token_description","rgb"))))))
).layout(("mint_tx",("token_meta","rgb")))
# prepare the contract call
c = sp.contract(
MINT_TYPE,
self.data.fa2,
entry_point="mint").open_some()
# call the mint entrypoint in the 8bidou FA2
# this effectively creates the NFT
sp.transfer(sp.record(
mint_tx=sp.record(
owner=sp.sender,
token_id=sp.nat(0),
amount=sp.nat(1),
),
token_meta=sp.record(
token_id=sp.nat(0),
token_info=sp.map({"":"<YOUR URI HERE>"})
),
rgb=sp.record(
token_id=sp.nat(0),
creater=self.data.creator,
creater_name=self.data.creator_name,
token_name=self.data.title_storage,
token_description=self.data.description,
rgb=self.data.pixel_storage
)
), sp.mutez(0), c)
# increment the token_id
self.data.token_id += 1
Hope you found this useful in some way!