In this tutorial, we will give you a taste of a few StarkNet features by putting together an initial skeleton of an NFT marketplace, starting with building a basic application that uses StarkNet.js SDK to interact with an ERC721 contract. Future posts will go into more detail about custom smart contract development with Cairo and building a dapp using MetaMask Flask (with starknet-snap installed).

Before we dive in, some background first. StarkNet is a permissionless decentralized Validity rollup (also known as a β€œZK-Rollup”). It operates as an L2 network over Ethereum, enabling any dapp to achieve unlimited scale for its computation without compromising Ethereum’s composability and security, thanks to StarkNet’s reliance on one of the safest and most scalable cryptographic proof systems – STARK.

ConsenSys and StarkNet partnered early this year, to bring this secure and high-performant technology to the Web3 community by integrating it within Infura and making it compatible with MetaMask.

Infura provides standard JSON-RPC endpoints to communicate seamlessly and in a straightforward manner with the StarkNet network via the Pathfinder client built by Equilibrium.

The StarkNet API maps a subset of Ethereum JSON-RPC methods (with some minor differences), so that eth-savvy users can easily replace eth_ prefixed calls with starknet_ and immediately interact with a StarkNet node or contract.

While StarkNet contracts are written in Cairo, a Turing-complete high-level language and framework different from Solidity, the user is not required to have this knowledge in order to successfully complete the below tutorial.

Luckily for us, the Web3 community (particularly the OpenZeppelin team) has created some ready-to-use presets for the most common ERC standards. In this tutorial, we are going to use the OpenZeppelin Cairo contracts, specifically:

The OpenZeppelin Cairo contracts version used in this tutorial is 0.3.1.

We will be providing the already-compiled contract files containing the definition of all methods and structures (ABI), ready to be deployed on the StarkNet network.

Basic app with StarkNet.js SDK

Prerequisites

Before getting started, make sure that you have all the necessary ingredients for this rich meal:

Getting started

In the section, we will wash, rinse and chop all the ingredients for our delicious stew.

We will start with creating a new Infura Web3 access key (previously, project ID) with StarkNet endpoints enabled, and one IPFS access key for storing our NFT artworks. These two keys will be used by our application to execute transactions against the StarkNet network and retrieve helpful information about account balances and NFT collections.

Create a new Web3 access key with StarkNet endpoints

To access the StarkNet network, we are required to have an endpoint through which all our requests from/to a StarkNet node will be executed. Let’s see how to do that with Infura (or follow this Getting Started guide).

  • Log in to the Infura main page
  • Click on the button on the right-hand side - Create new key
  • From the modal that pops up:
  • Select Network β†’ Web3 API
    • Type anything you like into the Name field πŸ™‚
    • Click on β€œCreate”
  • Scroll down until you spot StarkNet
  • From the network dropdown, select the Goerli testnet endpoint
  • Click on the right icon to copy the content

The output should be:

https://starknet-goerli.infura.io/v3/<API_KEY>

Note this down, as this is going to be useful further ahead on this tutorial.

Create a new IPFS access key and gateway

Storage on the blockchain is expensive and this is why it is a common practice to store NFT media off-chain. IPFS is usually the preferred choice for many developers, for three main reasons: it is free*, it is decentralized and it can guarantee access to a resource for a very, very long time.

We will need to create a new IPFS access key on Infura and set up a dedicated gateway that we will use, later.

  • Log in to the Infura main page
  • Click on the button on the right-hand side - Create new key
  • From the modal that pops up:
    • Select Network β†’ IPFS
    • Type anything you like into the Name field
  • Click on β€œCreate”
  • Under the section, Dedicated Gateways, toggle the button to enable it
  • Insert a favourite Unique subdomain name
  • Copy and save the Gateway URL
  • Copy and save both Project ID and API Key Secret

Now, we have all ingredients ready to start our MasterChef course πŸ§‘β€πŸ³

Building

Let’s start by checking that all our dependencies are installed correctly.

The code for this tutorial has been tested with the following tool version:

  • Node β†’ 16.17.0
  • Npm β†’ 8.18.0

Other versions may work, but it is recommended that you use major versions for full compatibility. (Tip: want to quickly switch between different node versions? Try nvm!)

node --version        
v16.17.0

npm --version         
8.18.0

All in place, perfect! Now let’s create a new Node project.

mkdir my-starknft-world
cd my-starknft-world
npm init -y

The last command will run the npm CLI utility and create a new package.json.

StarkNet.js

Starknet.js is the official Javascript library (SDK) for interacting with StarkNet and is maintained by a community of independent contributors.

The StarkNet.js version used in this tutorial is 4.6.0.

Starknet.js can be installed as a standard node module by running the following npm command

npm install --save starknet@next

We will also need to add "type": "module" to our package.json to enable ES-module and be able to use import.

Once the installation is completed, you should now have a package.json file that looks like this:

{
  "name": "my-starknft-world",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "starknet": "^4.6.0"
  },
  "type": "module"
}

Marvelous! And now it is time to get our hands on the code. πŸ§‘β€πŸ’»

Deploy a new account on StarkNet

In StarkNet, the account model is different from the one found on EVM-based blockchains. It is a more flexible model and one for which the Ethereum community has been waiting a long time, as this excellent article explores.

Here, the concept of a user account becomes a contract_address; StarkNet users will publicly share that address so that someone can send tokens to it, instead of a conventional public address derived directly from the user’s private/public key pair. That contract can contain any code, and the user still interacts with it by signing transactions with its private key.

In other words, StarkNet differentiates between the concept of user wallet and user account.

Furthermore, one could say that a user account is merely a special type of Cairo contract, as long as it contains the necessary logic to forward a transaction that has been signed with a valid private key.

Open the project in your favorite IDE and create a new index.js file.

Initialize a new provider

The Provider API allows you to interact with the StarkNet network, without signing transactions or messages.

import {
  Provider,
} from "starknet";

const infuraEndpoint = "https://starknet-goerli.infura.io/v3/<API_KEY>";
const provider = new Provider({
  rpc: {
    nodeUrl: infuraEndpoint,
  },
});

Download the account contract ABI

As mentioned above, StarkNet contracts are written in Cairo. However, to keep this tutorial super-simple, we are providing the equivalent compiled contract version containing the ABI in a JSON format.

These compiled contracts can be found in the following repository.

Jump on that page and download the OZAccount.json file into a new contracts directory located at the root of your project. Your directory tree should look like this:

.
β”œβ”€β”€ contracts
β”‚   └── OZAccount.json
β”œβ”€β”€ index.js
β”œβ”€β”€ node_modules
β”œβ”€β”€ package-lock.json
└── package.json

Read compiled account contract

Note that the json module used in this tutorial for parsing the compiled contract is part of the starknet package, which provides a custom implementation of the standard json node module.
import fs from "fs";
import {
  Provider,
  json,
} from "starknet";

console.log("Reading OpenZeppelin Account Contract...");
const compiledOZAccount = json.parse(fs.readFileSync("./contracts/OZAccount.json").toString("ascii"));

Generate private and public key pair

Using the stark module, we generate the private and public key pair which are going to be used to sign and execute transactions.

As mentioned above, in StarkNet an account address is computed as a smart contract with no direct relation to the key(s) controlling the account, and that is the reason we cannot derive an account address from these keys and, instead, we will require additional steps to create one.

import {
  defaultProvider,
  ec,
  json,
  stark,
} from "starknet";

const privateKey = stark.randomAddress();
const starkKeyPair = ec.getKeyPair(privateKey);
const starkKeyPub = ec.getStarkKey(starkKeyPair);

console.log(`🚨 DO NOT SHARE !!! 🚨 Private key: ${privateKey}`); // <-- KEEP THIS SECRET! πŸ”
console.log(`Public key: ${starkKeyPub}`);

Note down both keys, as we may want to reuse them later for further operations. Be mindful to store your private keys in a safe place and never share them with anyone.

Deploy a new account as a contract

We can deploy the pre-compiled account contract to StarkNet using the deployContract provider method and passing in input the public key previously generated.

console.log("Deployment Tx - Account Contract to StarkNet...");
const accountResponse = await provider.deployContract({
  contract: compiledOZAccount,
  constructorCalldata: [starkKeyPub],
  addressSalt: starkKeyPub,
});
const accountAddress = accountResponse.contract_address;

console.log(`Account address: ${accountAddress}`);

console.log(
  "Waiting for Tx to be Accepted on Starknet - OpenZeppelin Account Deployment..."
);
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${accountResponse.transaction_hash}`
);
await provider.waitForTransaction(accountResponse.transaction_hash);

This operation may require a few minutes (~10 minutes) to be completed - a good time for brewing some β˜•οΈ πŸ™‚

You can always keep an eye on the status of the transaction via the StarkNet block explorer (Voyager) at:

https://goerli.voyager.online/tx/<TRANSACTION_HASH>

And finally, let’s create a new Account object that we will use further ahead in this guide.

An Account extends Provider and inherits all of its methods. It also introduces new methods that allow Accounts to create and verify signatures with a custom Signer. This API is the primary way to interact with an Account contract on StarkNet.

import {
  Account,
  ec,
  json,
  stark,
  Provider,
} from "starknet";

const account = new Account(provider, accountAddress, starkKeyPair);

Bringing it all together, our index.js should look like this:

import fs from "fs";
import {
  Account,
  ec,
  json,
  stark,
  Provider,
} from "starknet";

/*
====================================
🦊 1. Account creation
====================================
*/

// Initialize provider
const infuraEndpoint = "https://starknet-goerli.infura.io/v3/...";
const provider = new Provider({
  rpc: {
    nodeUrl: infuraEndpoint,
  },
});

console.log("Reading OpenZeppelin Account Contract...");
const compiledOZAccount = json.parse(
  fs.readFileSync("./contracts/OZAccount.json").toString("ascii")
);

// Generate public and private key pair.
const privateKey = stark.randomAddress();
const starkKeyPair = ec.getKeyPair(privateKey);
const starkKeyPub = ec.getStarkKey(starkKeyPair);

console.log(`🚨 DO NOT SHARE !!! 🚨 Private key: ${privateKey}`); // <-- KEEP THIS SECRET! πŸ”
console.log(`Public key: ${starkKeyPub}`);

// Deploy the Account contract and wait for it to be verified on StarkNet
console.log("Deployment Tx - Account Contract to StarkNet...");
const accountResponse = await provider.deployContract({
  contract: compiledOZAccount,
  constructorCalldata: [starkKeyPub],
  addressSalt: starkKeyPub,
});
const accountAddress = accountResponse.contract_address;

console.log(`Account address: ${accountAddress}`);

// Wait for the deployment transaction to be accepted on StarkNet
console.log(
  "Waiting for Tx to be Accepted on Starknet - OpenZeppelin Account Deployment..."
);
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${accountResponse.transaction_hash}`
);
await provider.waitForTransaction(accountResponse.transaction_hash);

// Use your new account address
const account = new Account(provider, accountAddress, starkKeyPair);
TIP: This code snippet can be directly executed. However, to save some time when running the subsequent code sections, you can replace the random generation of the private key with the output of your first run. This will reuse the same key pair with no need for a new account deployment.

const privateKey = <PRIVATE_KEY>;

ERC721 NFT contract

As mentioned above, in this tutorial we are going to use a Β ERC721EnumerableMintableBurnable contract based on the OpenZeppelin implementation of the ERC721 standard.

Download the ERC721 contract ABI

Download the ERC721EnumerableMintableBurnable.json file from the following repository into the contracts directory located at the root of your project. Now your directory tree should look like this:

.
β”œβ”€β”€ contracts
β”‚   β”œβ”€β”€ ERC721EnumerableMintableBurnable.json
β”‚   └── OZAccount.json
β”œβ”€β”€ index.js
β”œβ”€β”€ node_modules
β”œβ”€β”€ package-lock.json
└── package.json

Read compiled ERC721 contract

Note that the json module used in this tutorial for parsing the compiled contract is part of the starknet package, which provides a custom implementation of the standard json node module.

// Read NFT contract ABI
console.log(
  "Reading OpenZeppelin ERC721EnumerableMintableBurnable Contract..."
);
const compiledErc721 = json.parse(
  fs
    .readFileSync("./contracts/ERC721EnumerableMintableBurnable.json")
    .toString("ascii")
);

Fund the account

In order to execute transactions on the StarkNet network (as much as on Ethereum), we need to fill our tank with some gas. Funding an account can be done manually by using the official StarkNet Goerli Faucet. However, as our code, once launched, will not stop and the transaction will require some time to be confirmed, we need to add a small snippet of code to pause the execution and restart it, once all of these operations are completed.

Therefore, we are going to add the following function to our index.js:

import readline from "readline";

function prompt(query) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  return new Promise((resolve) =>
    rl.question(query, (ans) => {
      rl.close();
      resolve(ans);
    })
  );
}

And let’s call this method right before the deployment.

await prompt(
  `
  IMPORTANT: you need to fund your newly created account before you use it.
  You can do so by using a faucet: https://faucet.goerli.starknet.io/
  Insert the following account address: ${accountAddress}
  Wait for confirmation before continuing.

  [Press ENTER to continue]`
);

Deploy ERC721 contract

Once we have completed the funding operation and the recipient account is updated with the new balance, we are ready to deploy the ERC721 NFT contract.

import {
  Account,
  ec,
  json,
  number,
  shortString,
  stark,
  Provider,
} from "starknet";

console.log("Deployment Tx - ERC721 Contract to StarkNet...");
const erc721Response = await provider.deployContract({
  contract: compiledErc721,
  constructorCalldata: [
    number.hexToDecimalString(shortString.encodeShortString("MyStarkNFT")),
    number.hexToDecimalString(shortString.encodeShortString("MSN")),
    accountAddress,
  ],
  addressSalt: starkKeyPub,
});

// Wait for the deployment transaction to be accepted on StarkNet
console.log("Waiting for Tx to be Accepted on Starknet - ERC721 Deployment...");
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${erc721Response.transaction_hash}`
);
await provider.waitForTransaction(erc721Response.transaction_hash);

And once the transaction has been confirmed, we can print the contract address and visualize it on the block explorer.

const erc721Address = erc721Response.contract_address;
console.log("ERC721 Address: ", erc721Address);
console.log(`Explorer link: https://goerli.voyager.online/contract/${erc721Address}`);

Connect account to NFT Contract

Contracts can do data transformations in JavaScript based on an ABI. They can also call and invoke to StarkNet through a provided Signer.

By connecting an Account to a Contract, we implicitly define who will be interacting with the network and signing transactions with their private key.

import {
  Account,
  Contract,
  ec,
  json,
  number,
  shortString,
  stark,
  Provider,
} from "starknet";

// Create a new erc721 contract object
const erc721 = new Contract(compiledErc721.abi, erc721Address, provider);

// Connect the current account to execute transactions
erc721.connect(account);

Mint an NFT

We have finally reached the most exciting part of this tutorial: crafting our first Stark-web3 digital art. πŸ‘¨β€πŸŽ¨

The mint transaction, as by standard definition, will take the account address of the recipient and the token id, as input to mint the NFT.

The Contract API allows us to use directly the interface <Contract>.mint as this method is defined in the erc721 ABI.

import {
  Account,
  Contract,
  ec,
  json,
  number,
  shortString,
  stark,
  Provider,
  uint256,
} from "starknet";

const tokenId = 1;
const value = uint256.bnToUint256(tokenId + "000000000000000000");
console.log(
  `Invoke Tx - Minting NFT with tokenId ${tokenId} to ${accountAddress} ...`
);
const { transaction_hash: mintTxHash } = await erc721.mint(
  accountAddress,
  [value.low, value.high],
  {
    maxFee: "999999995330000",
    addressSalt: starkKeyPub,
  }
);

// Wait for the invoke transaction to be accepted on StarkNet
console.log(`Waiting for Tx to be Accepted on Starknet - Minting...`);
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${mintTxHash}`
);
await provider.waitForTransaction(mintTxHash);

Stitching all the threads together:

/*
====================================
πŸ“œ 2. ERC721 NFT contract
====================================
*/

// Read NFT contract ABI
console.log(
  "Reading OpenZeppelin ERC721EnumerableMintableBurnable Contract..."
);
const compiledErc721 = json.parse(
  fs
    .readFileSync("./contracts/ERC721EnumerableMintableBurnable.json")
    .toString("ascii")
);

// Fund the account
await prompt(
  `
  IMPORTANT: you need to fund your newly created account before you use it.
  You can do so by using a faucet: https://faucet.goerli.starknet.io/
  Insert the following account address: ${accountAddress}
  Wait for confirmation before continuing.

  [Press ENTER to continue]`
);

// Deploy an ERC721 contract and wait for it to be verified on StarkNet.
console.log("Deployment Tx - ERC721 Contract to StarkNet...");
const erc721Response = await provider.deployContract({
  contract: compiledErc721,
  constructorCalldata: [
    number.hexToDecimalString(shortString.encodeShortString("MyStarkNFT")),
    number.hexToDecimalString(shortString.encodeShortString("MSN")),
    accountAddress,
  ],
  addressSalt: starkKeyPub,
});

// Wait for the deployment transaction to be accepted on StarkNet
console.log("Waiting for Tx to be Accepted on Starknet - ERC721 Deployment...");
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${erc721Response.transaction_hash}`
);
await provider.waitForTransaction(erc721Response.transaction_hash);

// Get the contract address
const erc721Address = erc721Response.contract_address;
console.log("ERC721 Address: ", erc721Address);
console.log(
  `Explorer link: https://goerli.voyager.online/contract/${erc721Address}`
);

// Create a new erc721 contract object
const erc721 = new Contract(compiledErc721.abi, erc721Address, provider);

// Connect the current account to execute transactions
erc721.connect(account);

// Mint 1 NFT with tokenId to accountAddress
const tokenId = 1;
const value = uint256.bnToUint256(tokenId + "000000000000000000");
console.log(
  `Invoke Tx - Minting NFT with tokenId ${tokenId} to ${accountAddress} ...`
);
const { transaction_hash: mintTxHash } = await erc721.mint(
  accountAddress,
  [value.low, value.high],
  {
    maxFee: "999999995330000",
    addressSalt: starkKeyPub,
  }
);

// Wait for the invoke transaction to be accepted on StarkNet
console.log(`Waiting for Tx to be Accepted on Starknet - Minting...`);
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${mintTxHash}`
);
await provider.waitForTransaction(mintTxHash);

The user might have noticed some β€œinteresting” type conversions here and there in this tutorial. The reason why we cannot simply pass a uint256 like we would do in Solidity is that, in Cairo, there is only one data type which is called felt and stands for Field Element. In simple terms, it is an unsigned integer with up to 76 decimals, but it can also be used to store addresses. Do not worry, this is not part of this tutorial, but it was worth a mention. πŸ™‚

Upload NFT data to IPFS

In this section, we are going to use the IPFS gateway that we had previously set up on Infura to upload our NFT media and metadata.

To start, we will need to install the IPFS client package.

npm install --save ipfs-http-client

Initialize IPFS client

Now is the time to recall the information that we noted down before, that is:

  • INFURA_IPFS_PROJECT_ID
  • INFURA_IPFS_SECRET
  • INFURA_IPFS_GATEWAY

And you would then place them in our code, as follows:

const infuraIpfsIdAndSecret = "<INFURA_IPFS_PROJECT_ID>:<INFURA_IPFS_SECRET>";
const infuraIpfsGateway = "https://<CUSTOM_GATEWAY>.infura-ipfs.io/ipfs/";
// Feel free to replace this URL with any image you like on the web - by default we set it to be our logo ❀️ 
const imageUrl = "https://pbs.twimg.com/profile_images/1357501845145485316/yo6M6Y9u_400x400.jpg";

const { create, urlSource } = await import("ipfs-http-client");
const ipfs = await create({
  host: "ipfs.infura.io",
  port: 5001,
  protocol: "https",
  headers: {
    Authorization: `Basic ${Buffer.from(infuraIpfsIdAndSecret).toString(
      "base64"
    )}`,
  },
});

This code snippet will create an instance of an IPFS gateway using our personal Infura information.

Note that you can always replace the imageUrl with anything you like!

Upload media to IPFS

First, we are going to upload the NFT media by sourcing it directly from the web.

let fileUrl;
try {
  const added = await ipfs.add(urlSource(imageUrl));
  console.log("Image", added);
  fileUrl = infuraIpfsGateway + added.cid;
} catch (error) {
  console.log("Error uploading file: ", error);
}
console.log(`IPFS file URL: ${fileUrl}`);

Upload NFT metadata to IPFS

Once we have completed the uploading of our media, we can attach to our NFT some basic information such as a name, a description and the URI of the image - generally referred to as metadata.

const metadata = JSON.stringify({
  name: "StarkNFT",
  description: "My first NFT on StarkNet with Infura! πŸ₯³",
  image: fileUrl,
});
let metadataUrl;
try {
  const added = await ipfs.add(metadata);
  console.log("Metadata", added);
  metadataUrl = infuraIpfsGateway + added.cid;
} catch (error) {
  console.log("Error uploading file: ", error);
}
console.log(`IPFS metadata URL: ${metadataUrl}`);

Set NFT tokenURI

We are going to call the ERC721 standard method setTokenURI to update the information associated with our NFT. However, before doing that there is a little magic trick πŸͺ„ that we need to perform.

As mentioned above, Cairo only uses one data type and when converting string into felt, we need to be sure this value is not higher than 31 characters. As IPFS URIs will probably not match this criteria, we need a workaround to "shorten" this text. The quickest and simplest way to achieve this is to use a free URL shortener available online. For the purpose of this tutorial we are going to use tinyurl.com.

We will call a tinyurl.com API to create a new short-url starting from our Infura IPFS URI. Let's add this snippet into our codebase.

The http package is already part of node, therefore, we will not need to install any new dependency
import https from "http";

function shortenUrl(url) {
  return new Promise((resolve, reject) => {
    const options = `https://tinyurl.com/api-create.php?url=${encodeURIComponent(
      url
    )}`;

    https
      .get(options, (response) => {
        if (response.statusCode >= 400) {
          reject(new Error(`${response.statusCode} ${response.statusMessage}`));
        }

        response.on("data", (data) => {
          resolve(data.toString().replace(/https?:\/\//i, ""));
        });
      })
      .on("error", (error) => {
        reject(error);
      });
  });
}

And then, let's call this method right after the metadata uploading.

metadataUrl = await shortenUrl(metadataUrl);
console.log(`Metadata shortened url is: ${metadataUrl}`);

Magic trick completed!βœ¨πŸ§™πŸΌβ€β™€οΈ Now, everything is ready for us to execute the transaction.

// Update token metadata URI
console.log(`Invoke Tx -  Setting URI for tokenId ${tokenId} to ${metadataUrl} ...`);
const { transaction_hash: tokenUriTxHash } = await erc721.setTokenURI(
  [value.low, value.high],
  number.hexToDecimalString(shortString.encodeShortString(metadataUrl)),
  {
    maxFee: "999999995330000",
    addressSalt: starkKeyPub,
  }
);

// Wait for the invoke transaction to be accepted on StarkNet
console.log(`Waiting for Tx to be Accepted on Starknet - Setting token URI...`);
console.log(
  `Follow the tx status on: https://goerli.voyager.online/tx/${tokenUriTxHash}`
);
await provider.waitForTransaction(tokenUriTxHash);

Retrieve NFT metadata information

Finally, we are going to retrieve the NFT metadata by calling the standard ERC721 method tokenURI.

console.log(`Retrieving metadata for tokenId ${tokenId} ...`);
const tokenURI = await erc721.tokenURI([value.low, value.high]);
const resultDecoded = shortString.decodeShortString(number.toHex(number.toBN(tokenURI[0])));
console.log(
  `Token URI for ${tokenId} is`,
  resultDecoded
);
console.log(`Direct link --> https://${resultDecoded}`);

Putting it all together:

/*
====================================
πŸ–Ό 3. Upload NFT artwork & metadata 
====================================
*/

// Initialize IPFS client
const infuraIpfsIdAndSecret = "<INFURA_IPFS_PROJECT_ID>:<INFURA_IPFS_SECRET>";
const infuraIpfsGateway = "https://<CUSTOM_GATEWAY>.infura-ipfs.io/ipfs/";
// Feel free to replace this URL with any image you like on the web - by default we set it to be our logo ❀️ 
const imageUrl = "https://pbs.twimg.com/profile_images/1357501845145485316/yo6M6Y9u_400x400.jpg";

const { create, urlSource } = await import("ipfs-http-client");
const ipfs = await create({
  host: "ipfs.infura.io",
  port: 5001,
  protocol: "https",
  headers: {
    Authorization: `Basic ${Buffer.from(infuraIpfsIdAndSecret).toString(
      "base64"
    )}`,
  },
});

// Upload image to IPFS
let fileUrl;
try {
  const added = await ipfs.add(urlSource(imageUrl));
  console.log("Image", added);
  fileUrl = infuraIpfsGateway + added.cid;
} catch (error) {
  console.log("Error uploading file: ", error);
}
console.log(`IPFS file URL: ${fileUrl}`);

// Upload NFT metadata to IPFS
const metadata = JSON.stringify({
  name: "StarkNFT",
  description: "My first NFT on StarkNet with Infura! πŸ₯³",
  image: fileUrl,
});
let metadataUrl;
try {
  const added = await ipfs.add(metadata);
  console.log("Metadata", added);
  metadataUrl = infuraIpfsGateway + added.cid;
} catch (error) {
  console.log("Error uploading file: ", error);
}
console.log(`IPFS metadata URL: ${metadataUrl}`);

// Shorten the URI to a compatible shortString format
metadataUrl = await shortenUrl(metadataUrl);
console.log(`Metadata shortened url is: ${metadataUrl}`);

// Update token metadata URI
console.log(`Invoke Tx -  Setting URI for tokenId ${tokenId} to ${metadataUrl} ...`);
const { transaction_hash: tokenUriTxHash } = await erc721.setTokenURI(
  [value.low, value.high],
  number.hexToDecimalString(shortString.encodeShortString(metadataUrl)),
  {
    maxFee: "999999995330000",
    addressSalt: starkKeyPub,
  }
);

// Wait for the invoke transaction to be accepted on StarkNet
console.log(`Waiting for Tx to be Accepted on Starknet - Setting token URI...`);
console.log(`Follow the tx status on: https://goerli.voyager.online/tx/${tokenUriTxHash}`);
await provider.waitForTransaction(tokenUriTxHash);

// Retrieve NFT metadata information
console.log(`Retrieving metadata for tokenId ${tokenId} ...`);
const tokenURI = await erc721.tokenURI([value.low, value.high]);
const resultDecoded = shortString.decodeShortString(number.toHex(number.toBN(tokenURI[0])));
console.log(
  `Token URI for ${tokenId} is`,
  resultDecoded
);
console.log(`Direct link --> https://${resultDecoded}`);

console.log("\nCongratulations! You minted your first NFT on StarkNet with Infura πŸ₯³");

Congratulations, you made it! πŸ‘ You have minted your first NFT on StarkNet with Infura. πŸ₯³

You can find the complete code of this tutorial here: https://github.com/czar0/my-starknft-world.

Resources

And last but not least, if you want to dive deeper into the awesome StarkNet world, here are some resources for you: