CryptoVikings
Search…
Technical Deep Dive
A full breakdown of the CryptoViking minting and generation process
The process you begin by clicking mint has our contracts and our API working together to produce a CryptoViking's on-chain representation, its metadata and its final image.
We integrated Chainlink VRF to provision one provably-fair random number per CryptoViking, which is used to break down the Viking's representation. We use Ethereum events to break up the process into a few steps to keep distinct functional concerns separate from one another and to facilitate recovery and data integrity.
We have two contracts, named after the Norse equivalent of the Greek Fates:
  • Nornir - responsible for NFT minting, speaking to VRF, generating statistics, name changes and representation storage
  • NornirResolver - responsible for using a CryptoViking's statistics to resolve its attributes/asset names, implementing the probability of each asset within its set
Here's a full breakdown of the entire generation process, from mint to image generation.

Step 1: Mint

Nornir: mintViking()
function mintViking(uint256 count) public {
doMint(count, false);
}
When you hit the mintViking function with the number you'd like to mint, we first validate that everything seems right with a series of requires, and then action the mint itself.
We take payment, then tokens are minted and random numbers requested from VRF in sequence with requestRandomness(). We store the token ID against the VRF request ID so that we can match the number that comes back later on to the NFT it's for.
Here's a look at the important bits of the minting loop:
Nornir: doMint()
// container for minted IDs for sending to the front end
uint256[] memory mintedIds = new uint256[](count);
// count => requested quantity
for (uint256 i = 0; i < count; i++) {
uint256 id = totalSupply();
_safeMint(msg.sender, id);
_setTokenURI(id, id.toString());
// make the call to VRF, storing the token ID against the VRF request ID
requestIdToVikingId[requestRandomness(keyHash, fee)] = id;
mintedIds[i] = id;
}
// emit VikingsMinted to prompt the front end to move along the walkthrough
emit VikingsMinted(mintedIds);

Step 2: VRF Callback

Nornir: fulfilRandomness()
function fulfillRandomness(bytes32 requestId, uint256 randomNumber) internal override {
// retrieve the token ID this random number is for
uint256 vikingId = requestIdToVikingId[requestId];
// store the random number against the token ID for later
vikingIdToRandomNumber[vikingId] = randomNumber;
// prompt our API to begin the process
emit VikingReady(vikingId);
}
When VRF is done producing a token's number, it calls our fulfilRandomness() override. All we do here is store the random number against the token ID (retrieved by the VRF request ID) and prompt our API to begin the generation process for the Viking in question.

Step 3: Generate

Nornir: generateViking()
function generateViking(uint256 vikingId) public onlyOwner {
// ensure that generation is allowed
require(vikingIdToRandomNumber[vikingId] != 0, 'Viking not minted');
require(vikingStats[vikingId].appearance == 0, 'Viking already generated');
// retrieve the Viking's random number
uint256 randomNumber = vikingIdToRandomNumber[vikingId];
// use some math to take sections of the number for VikingStats
// all numbers are 2-digit, except the last, which is 4-digit
// the VikingStats is stored permanently against the token ID
vikingStats[vikingId] = NornirStructs.VikingStats(
string(abi.encodePacked('Viking #', vikingId.toString())),
(randomNumber % 100),
(randomNumber % 10000) / 100,
(randomNumber % 10**6) / 10**4,
(randomNumber % 10**8) / 10**6,
(randomNumber % 10**10) / 10**8,
(randomNumber % 10**12) / 10**10,
(randomNumber % 10**14) / 10**12,
(randomNumber % 10**16) / 10**14,
(randomNumber % 10**18) / 10**16,
(randomNumber % 10**20) / 10**18,
(randomNumber % 10**28) / 10**20
);
// increment a counter we use to detect data integrity issues for recovery
generatedVikingCount++;
// set the Viking's name in a mapping we use for validating uniqueness
vikingNames[
keccak256(abi.encodePacked('Viking #', vikingId.toString()))
] = true;
// prompt the API to continue the process for this Viking
emit VikingGenerated(vikingId);
}
When the API picks up a VikingReady event, it queues up a call to generateViking() for the ID it received.
In generateViking(), we recover the random number associated with the token ID and do some math to take 2 and 4 digit segments of it before storing the resultant VikingStats struct in a publicly-accessible mapping against the token ID.
This VikingStats will serve as the basis for the next step in the process, which the API is prompted to queue up with a VikingGenerated event.
VikingStats looks like this:
NornirStructs.VikingStats
struct VikingStats {
string name;
// 2-digit selector for boots asset
uint256 boots;
// 2-digit selector for bottoms asset
uint256 bottoms;
// 2-digit selector for helmet asset
uint256 helmet;
// 2-digit selector for shield asset
uint256 shield;
// 2-digit selector for weapon asset
uint256 weapon;
// 2-digit statistic, influencing the condition of the weapon
uint256 attack;
// 2-digit statistic, influencing the condition of the shield
uint256 defence;
// 2-digit statistic, influencing the condition of the helmet
uint256 intelligence;
// 2-digit statistic, influencing the condition of the boots
uint256 speed;
// 2-digit statistic, influencing the condition of the bottoms
uint256 stamina;
// 4-digit combined selectors for beard, body, face and top assets
uint256 appearance;
}

Step 4: Resolve

Nornir: resolveViking()
function resolveViking(uint256 vikingId) public onlyOwner {
// ensure that resolution is allowed
require(vikingStats[vikingId].appearance != 0, 'Viking not generated');
require(bytes(vikingComponents[vikingId].weapon).length == 0, 'components already resolved');
require(bytes(vikingConditions[vikingId].weapon).length == 0, 'Conditions already resolved');
// call out to NornirResolver to resolve the VikingConditions for the Viking
// the VikingConditions is stored permanently against the token ID
vikingConditions[vikingId] = nornirResolverContract.resolveConditions(vikingStats[vikingId]);
// call out to NornirResolver to resolve the VikingComponents for the Viking
// the VikingComponents is stored permanently against the token ID
vikingComponents[vikingId] = nornirResolverContract.resolveComponents(vikingStats[vikingId], vikingConditions[vikingId]);
// increment a counter we use to detect data integrity issues for recovery
resolvedVikingCount++;
// prompt the API to generate the Viking's image
emit VikingResolved(vikingId, vikingStats[vikingId], vikingComponents[vikingId], vikingConditions[vikingId]);
}
When the API picks up a VikingGenerated event, it queues up a call to resolveViking() for the ID it received.
In resolveViking(), we recover the VikingStats associated with the token ID and ask NornirResolver to resolve the Viking's item conditions and asset names, storing that information in two structs - VikingConditions and VikingComponents - publicly against the token ID.
The on-chain representation breakdown is now complete - we prompt the API to generate an image with a VikingResolved event.
VikingConditions and VikingComponents look like this:
NornirStructs.VikingConditions
NornirStructs.VikingComponents
// VikingConditions contains resolved condition names for each of a Viking's items
// conditions modify an associated asset visually
struct VikingConditions {
string boots;
string bottoms;
string helmet;
string shield;
string weapon;
}
// VikingComponents contains resolved asset names for each of a Viking's parts
struct VikingComponents {
string beard;
string body;
string face;
string top;
string boots;
string bottoms;
string helmet;
string shield;
string weapon;
}
And here's how NornirResolver resolves them. The actual resolution functions and full list of asset names are hidden until we open source the system.
NornirResolver: resolveConditions()
NornirResolver: resolveComponents()
function resolveConditions(NornirStructs.VikingStats memory stats) external pure returns (NornirStructs.VikingConditions memory) {
return NornirStructs.VikingConditions(
// speed statistic conditions the boots
resolveClothesCondition(stats.speed),
// stamina statistic conditions the bottoms
resolveClothesCondition(stats.stamina),
// intelligence statistic conditions the helmet
resolveItemCondition(stats.intelligence),
// defence statistic conditions the shield
resolveItemCondition(stats.defence),
// attack statistic conditions the weapon
resolveItemCondition(stats.attack)
);
}
// conditions are split by Clothes and Items
// they're selected based on the value of the statistic
/*
Item Conditions (helmet, shield, weapon):
None
Destroyed
Battered
War Torn
Battle Ready
Flawless
Clothes Conditions (boots, bottoms):
Standard
Ragged
Rough
Used
Good
Perfect
*/
function resolveComponents(NornirStructs.VikingStats memory stats, NornirStructs.VikingConditions memory conditions) external pure returns (NornirStructs.VikingComponents memory) {
return NornirStructs.VikingComponents(
// first two digits of 'appearance' select the beard
resolveBeard(stats.appearance / 1000000),
// second 2 digits of 'appearance' select the body
resolveBody((stats.appearance / 10000) % 100),
// third 2 digits of 'appearance' select the face
resolveFace((stats.appearance / 100) % 100),
// final 2 digits of 'appearance' select the top
resolveTop(stats.appearance % 100),
// boots selected by boots stat, modified by boots condition
resolveBoots(stats.boots, conditions.boots),
// bottoms selected by bottoms stat, modified by bottoms condition
resolveBottoms(stats.bottoms, conditions.bottoms),
// helmet selected by helmet stat, modified by helmet condition
resolveHelmet(stats.helmet, conditions.helmet),
// shield selected by shield stat, modified by weapon condition
resolveShield(stats.shield, conditions.shield),
// weapon selected by weapon stat, modified by weapon condition
resolveWeapon(stats.weapon, conditions.weapon)
);
}

Step 5: Image Generation

Once the API receives the complete VikingStats, VikingConditions and VikingComponents, it can generate the final Viking image.
First it massages the information into a structure called VikingSpecification, which designates among other things the file paths for each of the selected assets, and then it supplies the VikingSpecification to the image generator.
Here's a snippet of the file path production:
API: VikingSpecificationHelper.resolveFilePaths()
// VikingComponents and VikingConditions are 1-1 reflections of their on-chain counterparts
private static resolveFilePaths(components: VikingComponents, conditions: VikingConditions): VikingSpecification['filePaths'] {
// component + condition name sanitizer for use in filenames
const cleanName = (name: string): string => name.replace(/[\s-]/g, '_').toLocaleLowerCase();
// resolve the file path for the beard - just run the chain-provided beard component through cleanName() for the file name
const beardFile = path.join(VikingSpecificationHelper.directories.beards, `beard_${cleanName(components.beard)}.png`);
// ...
// resolve the file path for the helmet
let helmetFile = undefined;
// if the condition was 'None', do not select a helmet
if (conditions.helmet !== 'None') {
// the style and the conditions are cleanName()'d chain-provided data
const style = cleanName(components.helmet);
const condition = cleanName(conditions.helmet);
// the file name is constructed with both the style and the condition to select the right variation
helmetFile = path.join(VikingSpecificationHelper.directories.helmets, style, `helmet_${style}_${condition}.png`);
}
// ...
return {
beard: beardFile,
// ...
helmet: helmetFile
// ...
};
}
And here's the actual image generation routine in full:
API: ImageHelper.generateVikingImage()
public static async generateVikingImage(vikingSpecification: VikingSpecification): Promise<void> {
// wrap the GM process into a Promise so that it can be awaited
return new Promise((resolve, reject) => {
// images are named numerically so as to decouple storage + retrieval from the Viking's actual name
const filePath = path.join(ImageHelper.VIKING_OUT, `viking_${vikingSpecification.number}.png`);
// initialize an empty gm()
const image = gm('');
// pass in the asset parts in a specific layering order
image
.in(vikingSpecification.filePaths.body)
.in(vikingSpecification.filePaths.face)
.in(vikingSpecification.filePaths.top)
.in(vikingSpecification.filePaths.beard)
.in(vikingSpecification.filePaths.bottoms)
.in(vikingSpecification.filePaths.boots);
// only pass in helmet, shield and weapon if their associated conditions weren't 'None'
if (vikingSpecification.filePaths.helmet) {
image.in(vikingSpecification.filePaths.helmet);
}
if (vikingSpecification.filePaths.shield) {
image.in(vikingSpecification.filePaths.shield);
}
if (vikingSpecification.filePaths.weapon) {
image.in(vikingSpecification.filePaths.weapon);
}
// configure and output the composed image
image
.background('transparent')
.mosaic()
.resize(1024, 1024)
.write(filePath, ((err) => {
err ? reject(`${err.message} : ${JSON.stringify(vikingSpecification.filePaths)}`) : resolve()
}));
});
}
The full API source will become available when we open source the system as part of our roadmap.
Copy link
On this page
Step 1: Mint
Step 2: VRF Callback
Step 3: Generate
Step 4: Resolve
Step 5: Image Generation