Deno & Fetch

A Deno-based script for minting CIP-68 assets on the Cardano blockchain using the Anvil API. This script generates wallets, creates policies, customizes asset metadata, builds and signs transactions,

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

  • Mint a CIP-68 Asset

  • Create a Policy (an NFT Collection)

  • Create a custom function to customize the asset metadata

  • Create 3 wallets, one for the policy, one for the metadata manager 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

Code

Dependencies

import { Buffer } from "node:buffer";
import {
  FixedTransaction,
  PrivateKey,
} from "npm:@emurgo/[email protected]";

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

Load wallets

We have a Simple CLI tool that can help you generating wallets.

https://github.com/Cardano-Forge/cardano-wallet-cli

// NOTE: Be sure to send ADA in this address, it will be used to pay the tx fee.
const customerWallet = JSON.parse(Deno.readTextFileSync("customer.json"));
const policyWallet = JSON.parse(Deno.readTextFileSync("policy-cip68.json"));
const metaManagerWallet = JSON.parse(
  Deno.readTextFileSync("meta-manager-cip68.json")
);

Cardano Wallets

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

Setup policy and asset template

Don't forget to increase the counter, otherwise you will double mint.

// VARIABLES
const expirationDate = "2026-01-01";
const counter = 202501171652;

// Date before you can interact with the policy
const slot = dateToSlot(new Date(expirationDate));

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);

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

const assets: {
  label: number; // 100, 222
  version: string;
  assetName: string;
  metadata?: {
    name: string;
    image: string | string[];
    mediaType: string;
    description: string;
    epoch: number;
  };
  policyId: string;
  quantity: 1;
  destAddress: string;
}[] = [];

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

Create the transaction

Label 100 must be sent to the metaManagerWallet

Label 222 must be send to the customer (in this case)

assets.push(
  {
    label: 100,
    version: "cip68",
    assetName: `anvilapicip68_${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,
    destAddress: metaManagerWallet.enterprise_address_preprod,
  },
  {
    label: 222,
    version: "cip68",
    assetName: `anvilapicip68_${counter}`,
    policyId: get_policy_id(policyAnvilApi.mint_script),
    quantity: 1,
    destAddress: customerWallet.enterprise_address_preprod,
  }
);

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 output = await fetch(urlTx, {
  method: "POST",
  body: JSON.stringify(data),
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
  },
}).then((res) => res.json());

Sign the transaction with Policy Wallet

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

Sign the transaction with Customer Wallet

// Sign transaction with metaManager key
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

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);
deno run -A index.ts

Example of minted assets using this script: https://preprod.cexplorer.io/policy/18b2ec7f6e805219664e98087528150c95a49ea856caa80d35041903#data

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"
}

The code will override the name key

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]";

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());
}

const customerWallet = JSON.parse(Deno.readTextFileSync("customer.json"));
const policyWallet = JSON.parse(Deno.readTextFileSync("policy-cip68.json"));
const metaManagerWallet = JSON.parse(
  Deno.readTextFileSync("meta-manager-cip68.json")
);

// VARIABLES
const expirationDate = "2026-01-01";
const counter = 202501171643;

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

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

const assets: {
  label: number; // 100, 222
  version: string;
  assetName: string;
  metadata?: {
    name: string;
    image: string | string[];
    mediaType: string;
    description: string;
    epoch: number;
  };
  policyId: string;
  quantity: 1;
  destAddress: string;
}[] = [];

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

assets.push(
  {
    label: 100,
    version: "cip68",
    assetName: `anvilapicip68_${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,
    destAddress: metaManagerWallet.enterprise_address_preprod,
  },
  {
    label: 222,
    version: "cip68",
    assetName: `anvilapicip68_${counter}`,
    policyId: get_policy_id(policyAnvilApi.mint_script),
    quantity: 1,
    destAddress: customerWallet.enterprise_address_preprod,
  }
);

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 output = await fetch(urlTx, {
  method: "POST",
  body: JSON.stringify(data),
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
  },
}).then((res) => res.json());

const transactionToSignWithCustomerKey = FixedTransaction.from_bytes(
  Buffer.from(output.complete, "hex")
);
transactionToSignWithCustomerKey.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(policyWallet.skey)
);

const txToSubmitOnChain = FixedTransaction.from_bytes(
  Buffer.from(transactionToSignWithCustomerKey.to_hex(), "hex")
);
txToSubmitOnChain.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(customerWallet.skey)
);

const urlSubmit =
  "https://preprod.api.ada-anvil.app/v2/services/transactions/submit";
const submitted = await fetch(urlSubmit, {
  method: "POST",
  body: JSON.stringify({
    signatures: [],
    transaction: txToSubmitOnChain.to_hex(),
  }),
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9",
  },
}).then((res) => res.json());

console.debug(submitted);

Last updated

Was this helpful?