CLI Tool to Mint Assets

Create a custom CLI Tool, using Deno and Fetch to connect to anvil API, taking few configurable parameters, it is possible to mint a collection using a binary with a custom implementation.

This a a very specific example, the goal is to show how flexible and versatile the Anvil API is.

This example will be entirely done into one file named index.ts

Objectives

  • Build a simple CLI tool to mint assets

  • Create a Policy (an NFT Collection)

  • Create a custom function to customize the asset metadata

  • Create 2 wallets, one for the policy and one to act as the customer

  • Submit transaction to the network

  • This is only an example showing the anvil api versatility

  • The example is built on Cardano Preprod

Dependencies

import { Buffer } from "node:buffer";
import {
  Credential,
  type Ed25519KeyHash,
  FixedTransaction,
  NativeScript,
  NativeScripts,
  PrivateKey,
  ScriptAll,
  ScriptPubkey,
  TimelockExpiry,
} from "npm:@emurgo/[email protected]";
import { parseArgs } from "jsr:@std/cli/parse-args";

Cardano Wallets

We have this utility to generate wallets: https://github.com/Cardano-Forge/cardano-wallet-cli/releases

~/Downloads/cardano-wallet-macos-latest --name policy --mnemonic
~/Downloads/cardano-wallet-macos-latest --name customer --mnemonic

You'll need to add some tADA to both wallets—100 should be more than enough.

Import Helper Functions

To reduce the amount of content in this guide, you only have to import all functions defined here:

https://github.com/Cardano-Forge/anvil-api/blob/main/docs/guides/utilities-functions/README.md

CLI

const args: {
  _: [];
  expireDate: string;
  customerWalletPath: string;
  policyWalletPath: string;
  metadataTemplatePath: string;
  counter: number;
  submit: boolean;
} = parseArgs(Deno.args);

Parse and prepare the policy and wallets:

// NOTE: Be sure to send ADA in this address, it will be used to pay the tx fee.
const customerWallet = JSON.parse(
  Deno.readTextFileSync(args.customerWalletPath)
);
// Wallet to create the policy with, no ADA is required for this one.
const policyWallet = JSON.parse(Deno.readTextFileSync(args.policyWalletPath));

// Expiration date, you can interact with the policy until this date is reached.
// After that the policy is locked.
const slot = dateToSlot(new Date(args.expireDate));

const keyhash = get_keyhash(policyWallet.skey);
if (!keyhash) {
  throw new Error("Unable to get key hash for policy, missing or invalid skey");
}
const policyAnvilApi = create_policy_script(keyhash, slot);

Collection configurations

This example requires 2 rules.

const policyAnvilApiScript = {
  type: "all",
  scripts: [
    {
      type: "sig",
      keyHash: keyhash.to_hex(),
    },
    {
      type: "before",
      slot: slot,
    },
  ],
};

Meaning that the policy has to be signed by the wallet defined in the policy wallet path parameter AND all mutations must be done before the date defined.

Create Metadata

This function is where you have to define your own data and configuration per asset.

CIP-25 enforces few fields as mandatory see here: https://cips.cardano.org/cip/CIP-25

You can also use our powerful metadata validator: https://metadraft.io

For example

const assets: {
  version: string;
  assetName: string;
  metadata: {
    name: string;
    image: string | string[];
    mediaType: string;
    description: string;
    epoch: number;
  };
  policyId: string;
  quantity: 1;
}[] = [];
const assetMetadataTemplate = JSON.parse(
  Deno.readTextFileSync(args.metadataTemplatePath)
);
const counter = args.counter;

// Simulate use case
assets.push({
  version: "cip25",
  assetName: `anvilapicip25_${counter}`,
  metadata: {
    ...assetMetadataTemplate,
    // Adding custom data just to test the flow
    name: `anvil-api-${counter}`,
    epoch: new Date().getTime(), // dummy data
  },
  policyId: get_policy_id(policyAnvilApi.mint_script),
  quantity: 1,
});

The counter is used to be sure that the assetName remains unique.

TBD: Show an example output (screenshot or link to preprod url or all of them)

The transaction

const data = {
  changeAddress: customerWallet.enterprise_address_preprod,
  mint: assets,
  preloadedScripts: [
    {
      type: "simple",
      script: policyAnvilApiScript,
      hash: get_policy_id(policyAnvilApi.mint_script),
    },
  ],
};

const urlTX =
  "https://preprod.api.ada-anvil.app/v2/services/transactions/build";
const transactionToSignWithPolicyKey = await fetch(urlTX, {
  method: "POST",
  body: JSON.stringify(data),
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
  },
}).then((res) => res.json());

Sign with the policy wallet

// Sign transaction with policy key
const transactionToSignWithCustomerKey = FixedTransaction.from_bytes(
  Buffer.from(transactionToSignWithPolicyKey.complete, "hex")
);
transactionToSignWithCustomerKey.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(policyWallet.skey)
);

Sign with the customer wallet

usually done using the browser extension

const txToSubmitOnChain = FixedTransaction.from_bytes(
  Buffer.from(transactionToSignWithCustomerKey.to_hex(), "hex")
);
// This sign the tx and add vkeys to the txToSubmitOnChain, so in submit we don't need to provide signautres
txToSubmitOnChain.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(customerWallet.skey)
);

Submit Transaction

if (args.submit) {
  const urlSubmit =
    "https://preprod.api.ada-anvil.app/v2/services/transactions/submit";

  const submitted = await fetch(urlSubmit, {
    method: "POST",
    body: JSON.stringify({
      signatures: [], // This empty because the txToSubmitOnChain has the vkeys
      transaction: txToSubmitOnChain.to_hex(),
    }),
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
    },
  }).then((res) => res.json());

  console.debug(submitted);
} else {
  console.log(txToSubmitOnChain.to_hex());
}

Using the CLI

deno compile --allow-read --allow-write --allow-net index.ts

Dont forget to increase the counter, otherwise you will double mint.

./index --expireDate=2025-01-01 \
  --customerWalletPath=customer.json \
  --policyWalletPath=policy.json \
  --metadataTemplatePath=metatemplate.json \
  --counter=2 \
  --submit
File Content - (customer.json, policy.json, metatemplate.json)

customer.json

{
  "skey": "REDACTED",
  "skey_hex": "REDACTED",
  "pkey": "REDACTED",
  "pkey_hex": "REDACTED",
  "key_hash": "REDACTED",
  "base_address_preview": "addr_test1qqc0f6py9qeyp5k2vme7pxct0t0ut5mczmq8yfumx8cfpfx7kdf62usuu92g463pyumktcckmd9x7nmrvfxc9w04dnfqz7ycf9",
  "base_address_preprod": "addr_test1qqc0f6py9qeyp5k2vme7pxct0t0ut5mczmq8yfumx8cfpfx7kdf62usuu92g463pyumktcckmd9x7nmrvfxc9w04dnfqz7ycf9",
  "base_address_mainnet": "addr1qyc0f6py9qeyp5k2vme7pxct0t0ut5mczmq8yfumx8cfpfx7kdf62usuu92g463pyumktcckmd9x7nmrvfxc9w04dnfqpgec96",
  "enterprise_address_mainnet": "addr1vyc0f6py9qeyp5k2vme7pxct0t0ut5mczmq8yfumx8cfpfq5xuxsn",
  "enterprise_address_preview": "addr_test1vqc0f6py9qeyp5k2vme7pxct0t0ut5mczmq8yfumx8cfpfq0wg6lk",
  "enterprise_address_preprod": "addr_test1vqc0f6py9qeyp5k2vme7pxct0t0ut5mczmq8yfumx8cfpfq0wg6lk",
  "reward_address_mainnet": "stake1u80tx5a9wgwwz4y2agsjwdm9uvtdkjn0fa3kynvzh86ke5se4shkt",
  "reward_address_preview": "stake_test1ur0tx5a9wgwwz4y2agsjwdm9uvtdkjn0fa3kynvzh86ke5s7l64jk",
  "reward_address_preprod": "stake_test1ur0tx5a9wgwwz4y2agsjwdm9uvtdkjn0fa3kynvzh86ke5s7l64jk",
  "mnemonic": "REDACTED"
}

policy.json

{
  "skey": "REDACTED",
  "skey_hex": "REDACTED",
  "pkey": "REDACTED",
  "pkey_hex": "REDACTED",
  "key_hash": "REDACTED",
  "base_address_preview": "addr_test1qzmdaaddmhut3e3kyjsv5f3nsws0mcxk7tu2gpwql8f8pfschsxexl9yakag0rdpf7m4rgg4dvlwuzklghq7trdr4qkqpckdye",
  "base_address_preprod": "addr_test1qzmdaaddmhut3e3kyjsv5f3nsws0mcxk7tu2gpwql8f8pfschsxexl9yakag0rdpf7m4rgg4dvlwuzklghq7trdr4qkqpckdye",
  "base_address_mainnet": "addr1qxmdaaddmhut3e3kyjsv5f3nsws0mcxk7tu2gpwql8f8pfschsxexl9yakag0rdpf7m4rgg4dvlwuzklghq7trdr4qkqzwtdgx",
  "enterprise_address_mainnet": "addr1vxmdaaddmhut3e3kyjsv5f3nsws0mcxk7tu2gpwql8f8pfsc3x6e8",
  "enterprise_address_testnet": "addr_test1vzmdaaddmhut3e3kyjsv5f3nsws0mcxk7tu2gpwql8f8pfsrejxkz",
  "reward_address_mainnet": "stake1uyvtcrvn0jjwmw583ks5ld635y2kk0hwpt05ts093k36stqfn5cxe",
  "reward_address_testnet": "stake_test1uqvtcrvn0jjwmw583ks5ld635y2kk0hwpt05ts093k36stqwe76zy",
  "mnemonic": "REDACTED"
}

metatemplate.json

{
  "name": "TBD",
  "image": [
    "https://ada-anvil.s3.ca-central-1.amazonaws.com/",
    "logo_pres_V2_3.png"
  ],
  "mediaType": "image/png",
  "description": "Testing CIP-25 using anvil API"
}

The code will override the name key

Explorer

The Whole File (Deno Version)

index.ts
import { Buffer } from "node:buffer";
import {
  Credential,
  type Ed25519KeyHash,
  FixedTransaction,
  NativeScript,
  NativeScripts,
  PrivateKey,
  ScriptAll,
  ScriptPubkey,
  TimelockExpiry,
} from "npm:@emurgo/[email protected]";
import { parseArgs } from "jsr:@std/cli/parse-args";

const args: {
  _: [];
  expireDate: string;
  customerWalletPath: string;
  policyWalletPath: string;
  metadataTemplatePath: string;
  counter: number;
  submit: boolean;
} = parseArgs(Deno.args);
// deno run -A steps.ts \
//  --expireDate=2026-01-01 \
//  --customerWalletPath=customer.json \
//  --policyWalletPath=policy.json \
//  --metadataTemplatePath=metatemplate.json \
//  --counter=20250117 \
//  --submit

const dateToSlot = (date: Date) => {
  return Math.floor(date.getTime() / 1000) - 1596491091 + 4924800;
};

export function get_keyhash(private_key: string): Ed25519KeyHash | undefined {
  return Credential.from_keyhash(
    PrivateKey.from_bech32(private_key).to_public().hash()
  ).to_keyhash();
}

export function create_policy_script(
  policy_key_hash: Ed25519KeyHash,
  ttl: number,
  with_timelock = true
): { mint_script: NativeScript; policy_ttl: number } {
  const scripts = NativeScripts.new();
  const key_hash_script = NativeScript.new_script_pubkey(
    ScriptPubkey.new(policy_key_hash)
  );
  scripts.add(key_hash_script);

  const policy_ttl: number = ttl;

  if (with_timelock) {
    const timelock = TimelockExpiry.new(policy_ttl);
    const timelock_script = NativeScript.new_timelock_expiry(timelock);
    scripts.add(timelock_script);
  }

  const mint_script = NativeScript.new_script_all(ScriptAll.new(scripts));

  return { mint_script, policy_ttl };
}

export function bytes_to_hex(input: Uint8Array): string {
  return Buffer.from(input).toString("hex");
}

export function get_policy_id(mint_script: NativeScript): string {
  return bytes_to_hex(mint_script.hash().to_bytes());
}

// NOTE: Be sure to send ADA in this address, it will be used to pay the tx fee.
const customerWallet = JSON.parse(
  Deno.readTextFileSync(args.customerWalletPath)
);
const policyWallet = JSON.parse(Deno.readTextFileSync(args.policyWalletPath));

// Date before you can interact with the policy
const slot = dateToSlot(new Date(args.expireDate));
const keyhash = get_keyhash(policyWallet.skey);
if (!keyhash) {
  throw new Error("Unable to get key hash for policy, missing or invalid skey");
}
const policyAnvilApi = create_policy_script(keyhash, slot);

//
// CUSTOM FOR EACH MINT COLLECTION
//
// not the best way to handle this, a config file would be better
const policyAnvilApiScript = {
  type: "all",
  scripts: [
    {
      type: "sig",
      keyHash: keyhash.to_hex(),
    },
    {
      type: "before",
      slot: slot,
    },
  ],
};

const assets: {
  version: string;
  assetName: string;
  metadata: {
    name: string;
    image: string | string[];
    mediaType: string;
    description: string;
    epoch: number;
  };
  policyId: string;
  quantity: 1;
}[] = [];
const assetMetadataTemplate = JSON.parse(
  Deno.readTextFileSync(args.metadataTemplatePath)
);
const counter = args.counter;

// Simulate use case
assets.push({
  version: "cip25",
  assetName: `anvilapicip25_${counter}`,
  metadata: {
    ...assetMetadataTemplate,
    // Adding custom data just to test the flow
    name: `anvil-api-${counter}`,
    epoch: new Date().getTime(), // dummy data
  },
  policyId: get_policy_id(policyAnvilApi.mint_script),
  quantity: 1,
});

//
// Generic calls to create, sign and submit tx
//
const data = {
  changeAddress: customerWallet.enterprise_address_preprod,
  mint: assets,
  preloadedScripts: [
    {
      type: "simple",
      script: policyAnvilApiScript,
      hash: get_policy_id(policyAnvilApi.mint_script),
    },
  ],
};

const urlTX =
  "https://preprod.api.ada-anvil.app/v2/services/transactions/build";
const transactionToSignWithPolicyKey = await fetch(urlTX, {
  method: "POST",
  body: JSON.stringify(data),
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
  },
}).then((res) => res.json());

console.debug(
  "transactionToSignWithPolicyKey: ",
  transactionToSignWithPolicyKey
);
//
// policy signature
//
const transactionToSignWithCustomerKey = FixedTransaction.from_bytes(
  Buffer.from(transactionToSignWithPolicyKey.complete, "hex")
);
transactionToSignWithCustomerKey.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(policyWallet.skey)
);

console.debug(
  "transactionToSignWithCustomerKey: ",
  transactionToSignWithCustomerKey.to_hex()
);

//
// customer signature
//
const txToSubmitOnChain = FixedTransaction.from_bytes(
  Buffer.from(transactionToSignWithCustomerKey.to_hex(), "hex")
);
console.debug("Customer Wallet", customerWallet);
// This sign the tx and add vkeys to the txToSubmitOnChain, so in submit we don't need to provide signautres
txToSubmitOnChain.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(customerWallet.skey)
);

console.debug("txToSubmitOnChain: ", txToSubmitOnChain.to_hex());
console.debug(
  "txToSubmitOnChain JSON: ",
  txToSubmitOnChain.witness_set().to_json()
);

//
// Submit tx
//
if (args.submit) {
  const urlSubmit =
    "https://preprod.api.ada-anvil.app/v2/services/transactions/submit";

  const submitted = await fetch(urlSubmit, {
    method: "POST",
    body: JSON.stringify({
      signatures: [], // This empty because the txToSubmitOnChain has the vkeys
      transaction: txToSubmitOnChain.to_hex(),
    }),
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
    },
  }).then((res) => res.json());

  console.debug(submitted);
} else {
  console.log(txToSubmitOnChain.to_hex());
}

Last updated

Was this helpful?