Reconstructing Meridian

Following a delivery of my copy of a Meridian book by Matt DesLauries, I decided to try out and re-create an Art Blocks piece locally following a guide from the book.

In this post, I'll share an in-depth guide on how to re-create our Meridian (or any other Art Blocks piece) by yourself or a way to download your desired piece as a file on your computer.

Image by Vetro Editions

💡 Don’t want to write code by yourself?
You can download any Art Blocks piece from here: https://replit.com/@mavdotso/AB-Contract-reconstruction

Let's get started.

Set-up

If you're running your own Ethereum node — you can connect to it locally and skip the first two steps.

Unfortunately, I do now own 32 ETH, therefore I will employ Infura as a node provider and use it's API to get the data we need.

First, register an account on Infura.

Once you're there, click "Create new API Key" button.

Click "Create".

Now once you've created the API Key, you should see it on the very top of the page.

Click on the small green icon to copy and save it somewhere.

Note: It's a good practice to keep your API keys inaccessible to other people, so don't share the key anywhere.

We are ready to read data from blockchain.

Next, we'll need another API key from Etherscan. Go to https://etherscan.io/ and create your account.

Now, go to the "API Keys" section in menu and click "Add". Give it a name and you should see your key on the dashboard. Copy it and save somewhere safe.

Now that we have all we need, let's write some code!

Writing code

We're going to need a code editor. Usually I use VSCode, but today I'm going to use Repl.it as it's very easy to write and share code snippets and it's perfect for our use case today. Also I'm going to be using JavaScript.

Let's quickly figure out what do we need to do, in order to make it work. A very high-level overview would look something like this:

  1. Connect to a blockchain
  2. Retrieve the contract code from the address
  3. Retrieve the source code of a collection from the contract (Meridian in our case)
  4. Get a token hash for a piece we'd like to download
  5. Combine it all into an html file and download it to our computer

First, let's create a file in our folder, I'll call it script.js.

Now, we need to import some libraries that will help us implement the functionality we need:

import axios from "axios";
import Web3 from "web3";
import fs from "fs";

Next, let's set our API keys from Etherscan and Infura.

If you're working locally or in a private project on Repl.it, you can simply set your API keys like so:

const etherscanApiKey = "YOUR_ETHERSCAN_API_KEY";
const infuraApiKey = "YOUR_INFURA_API_KEY"; 

Replace YOUR_ETHERSCAN_API_KEY and YOUR_INFURA_API_KEY with API keys you generated.

If you're not working locally, you need to hide your API keys from public.

This is how it's done in Repl.it: https://docs.replit.com/programming-ide/workspace-features/secrets

Now, you can import your api keys like so: const etherscanApiKey = process.env["ETHERSCAN_API_KEY"]; const infuraApiKey = process.env["INFURA_API_KEY"];

Next, let's tell our code which contract address we're looking for. Meridians are a part of a legacy Art Blocks Curated contract, and a bunch of the collections share the same contract address: 0xa7d8d9ef8D8Ce8992Df33D8b8CF4Aebabd5bD270

I created a dashboard where you can check which Art Blocks collection is using which contract address: https://dune.com/queries/3394600

Let's create a variable for our contract address:

const contractAddress = "0xa7d8d9ef8D8Ce8992Df33D8b8CF4Aebabd5bD270";

Next, we need to tell our code which collection we're looking for exactly. Every collection on Art Blocks contracts has an ID, in case of Meridians it's 163. You can find the project IDs for each collection in the dune dashboard under "Project id" column.

Let's create a variable:

const projectId = 163;

Let’s pick a piece we want to reconstruct. I chose a Meridian #95 as it's the one we own at Stellars DAO. Let's create a variable:

const mintNumber = 95;

Now, a fun (and a very unnecessary) part.

We need to combine the project id and a piece number into a single number that contains 9 digits and in our case looks like this: 163000095. The project Id goes first, then a bunch of zeroes and then a piece number.

Of course, we can just do it manually, right?

But what if we want to recreate more than one piece? Let's say we want to download hundreds of pieces, or, in my case, we want to write a code so that other people can use it too. It's going to be impossible to do it manually for every piece in every collection. So let's employ a bit of programming magic.

You can totally skip this part and just copy this code, if you don't care what it does:

const totalLength = 9;
const mintNumberString = String(mintNumber);
const missingZeroes = totalLength - mintNumberString.length;
const paddedProjectId = String(projectId).padEnd(missingZeroes, "0");
const tokenId = parseInt(paddedProjectId + mintNumberString);

A brief explanation of what’s going on here:

  1. const totalLength = 9; In this line of code, we tell the code that a length of a token id should be exactly 9 digits.
  2. const mintNumberString = String(mintNumber); In this code, we need to convert the entire number to a string (text) in order to deal with a very specific edge-case. You see, the project id's in Art Blocks contracts start from 0, not from 1. If we take Squiggles, as an example, and want to reconstruct a piece #9450, we will have to construct a token id of 000009450. In JavaScript, we can't have a 000009450 as a number. If we ask JavaScript to to store this value as a number, it will store only 9450 and remove all of the leading zeroes. For this reason, we need to store a number as a text.
  3. const missingZeroes = totalLength - mintNumberString.length; Here, we calculate how many 0 in total we need to add, in order to get to a total of 9 digits.
  4. const paddedProjectId = String(projectId).padEnd(missingZeroes, "0"); In this code, we use padEnd() function to add a number of specified characters (0) to the end of our string. We take the number of 0's from the previous function.
  5. const tokenId = parseInt(paddedProjectId + mintNumberString); Lastly, we add the token id to the end of our number.

After all that, we get a token id that looks like this: 163000095

Quite simple (:

Now, the fun part.

Let's utilise a web3 library to connect to an Infura node with our API and get the data from it:

const web3 = new Web3([https://mainnet.infura.io/v3/${infuraApiKey}](https://mainnet.infura.io/v3/$%7BinfuraApiKey%7D));

Next, we need to create an asynchronous function to fetch the data from the contract. Here is what it looks like:

async function fetchData() {
  try {
    // Fetching the contract ABI
    const abi = await getContractABI(contractAddress);
    const contract = new web3.eth.Contract(abi, contractAddress);

    // Fetching the tokenURI
    const code = await contract.methods
      .projectScriptByIndex(projectId, 0)
      .call();
    const hash = await contract.methods.tokenIdToHash(tokenId).call();

    // Saving into an html file
    const htmlContent = `
<body>
  <script>
    var tokenData = {
      hash: '${hash}',
      code: '${code}'
    };
  </script>
  <script>${code}</script>
</body>
`;

    fs.writeFileSync("output.html", htmlContent);
    console.log(
      "Success! You can download output.html file and open it in your browser."
    );
  } catch (error) {
    console.error("Error:", error.message);
  }
}

Now, we need to create an getContractABI function to fetch the ABI data of the contract from Etherscan:

async function getContractABI(contractAddress) {
  const url = `https://api.etherscan.io/api?module=contract&action=getabi&address=${contractAddress}&apikey=${etherscanApiKey};`;
  try {
    const response = await axios.get(url);
    if (response.data.status === "1") {
      return JSON.parse(response.data.result);
    } else {
      throw new Error("Unable to retrieve contract ABI");
    }
  } catch (error) {
    console.error("Error:", error.message);
  }
}

And finally, let's add a function call to the very end of the file: fetchData();

And, we're done!

Finished code

Here is what the finished code looks like:

import axios from "axios";
import Web3 from "web3";
import fs from "fs";

// Setting API keys for Etherscan and Infura
const etherscanApiKey = process.env["ETHERSCAN_API_KEY"];
const infuraApiKey = process.env["INFURA_API_KEY"];

// AB Curated address
const contractAddress = "0xa7d8d9ef8D8Ce8992Df33D8b8CF4Aebabd5bD270";

// Change to a collection Id
const projectId = 163; // Meridian

// Pick a mint number
const mintNumber = 95;

const totalLength = 9;
const mintNumberString = String(mintNumber);

// Calculate the number of zeroes needed to reach the total length
const missingZeroes = totalLength - mintNumberString.length;

// Add the missing zeroes to the end of the project ID
const paddedProjectId = String(projectId).padEnd(missingZeroes, "0");

// Concatenate the padded project ID and the mint number
const tokenId = parseInt(paddedProjectId + mintNumberString);

console.log("Token ID:", tokenId);

// Create a new instance of the Web3 class using the Infura provider
const web3 = new Web3(`https://mainnet.infura.io/v3/${infuraApiKey}`);

// Getting the contract ABI
async function getContractABI(contractAddress) {
  const url = `https://api.etherscan.io/api?module=contract&action=getabi&address=${contractAddress}&apikey=${etherscanApiKey}`;
  try {
    const response = await axios.get(url);
    if (response.data.status === "1") {
      return JSON.parse(response.data.result);
    } else {
      throw new Error("Unable to retrieve contract ABI");
    }
  } catch (error) {
    console.error("Error:", error.message);
  }
}

// Fetching the data from the contract
async function fetchData() {
  try {
    // Fetching the contract ABI
    const abi = await getContractABI(contractAddress);
    const contract = new web3.eth.Contract(abi, contractAddress);

    // Fetching the tokenURI
    const code = await contract.methods
      .projectScriptByIndex(projectId, 0)
      .call();
    const hash = await contract.methods.tokenIdToHash(tokenId).call();

    // Saving into an html file
    const htmlContent = `
    <body>
      <script>
        var tokenData = {
          hash: '${hash}',
          code: '${code}'
        };
      </script>
      <script>${code}</script>
    </body>
    `;

    fs.writeFileSync("output.html", htmlContent);
    console.log(
      "Success! You can download output.html file and open it in your browser."
    );
  } catch (error) {
    console.error("Error:", error.message);
  }
}

fetchData();

Now, one last thing.

If you work in Repl.it, all you have to do now is just to click "Run" on top of the page, and Repl.it will automatically download and install all dependencies for you.

If you work locally, run this command in your terminal: npm install axios web3

After that, you should see a package.json file in your directory.

Open it, and add "type": "module", on top of the file.

Now, let's run it!