Advanced Art Case - GasFire

What is GasFire

GasFire is a type of Generative Art that visualizes fire. The colors are generated based on the amount of gas consumed by the address minting the NFT on the Ethereum chain. Users who mint are categorized into predefined Tiers based on their gas consumption, and colors corresponding to these Tiers are assigned. The shape of the fire also incorporates a fluctuation, creating a unique shape for each address.

Let's see how GasFire is implemented on the Phi Protocol.

Overview

First, let's take an overview. The main components that need to be developed are the Verifier API and the Render API. Phi Protocol provides a foundation for issuing NFTs using Advanced Art by preparing these two APIs.

The Verifier API is used to correctly verify whether the created Cred meets the conditions. In this case, the Verifier API checks whether the gas consumption on Ethereum is greater than 0. Other Verifier APIs might verify specific on-chain activities (e.g., issuing a specific NFT or performing a certain number of transactions on a specific chain). Additionally, up to 32 bytes of additional data can be used during verification when issuing Advanced Art. By returning this additional data, it can be passed as an argument during the Render API call at minting time. In this case, the gas consumption value (gas_used) obtained from the Etherscan API is returned.

The Render API generates an image based on the eligible address and additional data returned by the Verifier API when it considers the address eligible. The generated image is uploaded to Arweave as the final NFT image data, and the URL is embedded in the contract.

Next, let's look at the implementation of the Verifier API.

Verifier API Implementation

Here is the implementation of the Verifier API endpoint.

import {NextRequest, NextResponse} from "next/server";  
import {getTokenActivityBy} from "@/client/gasfire/tokenclient";  
import {create_signature} from "@/utils/signature";

export async function GET(req: NextRequest) {  
  return handler(req);  
}  
  
export async function POST(req: NextRequest) {  
  return handler(req);  
}  
  
async function handler(req: NextRequest) {  
  const {searchParams} = new URL(req.url);  
  const address = searchParams.get('address');  
  if (!address) {  
    // If the address is not provided in the query, throw an error  
    throw new Error('Address is required');  
  }  
  const privateKey = process.env.SIGNER_PRIVATE_KEY;  
  if (privateKey === undefined) {  
    throw new Error('SIGNER_PRIVATE_KEY is not defined');  
  }
  
  const tokenActivity = await getTokenActivityBy(address);  
  
  // Check credential 0 ('Complete a transaction on Basechain') for the address  
  const check_result = tokenActivity.gas_used > 0;  
  const counter = String(tokenActivity.gas_used);  
  console.log(`Credential check result: ${check_result}, counter: ${counter}`);  
  
  // If the credential check is successful, create a signature using the address, check result, and counter  
  const signature = await create_signature(privateKey as `0x${string}`, [address as `0x${string}`, check_result, String(counter)]);  
  console.log(`Signature: ${signature}`);  
  // Return a success response with the check result, counter, and signature  
  return NextResponse.json({mint_eligibility: check_result, data: counter, signature});  
}

The flow of this code is as follows:

  1. Retrieve the address from the URL query parameters.

  2. Get the signing private key from environment variables.

  3. Get the gas consumption of the target address via the Etherscan API.

  4. If the gas consumption is greater than 0, mint_eligibility is set to true.

  5. Return the gas consumption value as the value of the data field.

  6. Use the signing private key to create a signature that includes the values of address, mint_eligibility, and the data field, and return it as the value of the signature field.

A sample response in JSON format when mint_eligibility is true and gas_used is 1052390 would be as follows:

{
  "mint_eligibility": true,
  "data": "1052390",
  "signature": "0xe3e2a4...398"
}

These values are used to verify the contract after it is returned. Verification with a signature other than the address set as the Signer when the Verifier API was registered will fail on the contract, and the Art cannot be minted.

For the implementation of the create_signature method for signature verification, please refer to the Phi Protocol specifications.

Once the Verifier API is implemented, proceed to the implementation of the Render API.

Render API Implementation

Here is the implementation of the Render API endpoint.

import {NextRequest, NextResponse} from 'next/server'  
import {createCanvas} from '@napi-rs/canvas';  
import {renderImage} from "@/render/gasfire/render";  
  
export async function GET(req: NextRequest) {
  const {searchParams} = new URL(req.url);
  const address = searchParams.get('address');
  const data = BigInt(searchParams.get('data')!!);
  if (!address || !data) {
    return NextResponse.json({error: 'Missing address or data parameter'}, {status: 400});
  }

  const imageSize = 512;
  const canvas = createCanvas(imageSize, imageSize);
  const ctx = canvas.getContext('2d');

  renderImage(ctx, canvas.width, canvas.height, address, data);
  const headers = new Headers();
  headers.set("Content-Type", "image/png");

  const pngBuffer = await canvas.encode('png');
  return new NextResponse(pngBuffer, {status: 200, statusText: "OK", headers});
}

The flow of this code is as follows:

  1. Retrieve the address and data from the URL query parameters.

  2. Implement the backend using TypeScript/NextJS, and initialize the canvas with a size of 512x512 for image generation.

  3. Call the renderImage method to draw Advanced Art on the canvas.

  4. Encode the drawn image in PNG format and respond with a 200 status.

The renderImage method implements the specific drawing process, which mostly includes the unique processing for Advanced Art.

renderImage overview

Here's an overview of the processes within the renderImage method.

  1. To generate a deterministic and unique Art for the address, a deterministic random number algorithm called XorShift is used. This XorShift initialized with the address will be used as the source of parameters for generating this Art.

  2. The base of the fire image is a predefined set of Bezier curve points. 2 to 4 sets of these points are obtained.

  3. Projection transformations are created and applied to each set of points to add fluctuations to the fire.

  4. The gas consumption passed as an argument is classified into a predefined Tier, and the color corresponding to this Tier is obtained.

  5. Finally, the fire point groups with added fluctuations are combined and drawn using the fire color.

export function renderImage(  
    ctx: SSContext2D | CanvasRenderingContext2D,  
    width: number, height: number,  
    address: string, gasUsed: bigint  
) {  
  const xorShift = XorShift.getDeterministicRandomBy(address);  
  
  const bezierFires: BezierFire[] = [];  
  const fireNum = xorShift.nextIntBet(2, 4);  
  for (let i = 0; i < fireNum; i++) {  
    const seedIdx = xorShift.nextIntBet(0, bezierFireSeeds.length - 1);  
    let fire = bezierFireSeeds[seedIdx];  
    const generator = ProjectionGenerator.createBy(xorShift, fire, width);  
    if (i > 0) {  
      const projection = generator.nextTransform();  
      fire = projection.transformBezier(fire);  
    }  
    bezierFires.push(fire);  
  }  
  
  const tier = checkTierOf(gasUsed);  
  const tieredFireColor = getTieredFireColor(tier);  
  drawBezierFrame(ctx, tieredFireColor, bezierFires);  
}

Conclusion

In this article, we explained how to implement GasFire, an example of Advanced Art, on the Phi Protocol.

This time, the implementation was done using TypeScript/NextJS, but as long as the API conforms to the JSON schema defined by Phi Protocol, the language and technologies used are not restricted.

The complete code for GasFire is available on GitHub for reference.

Last updated