> For the complete documentation index, see [llms.txt](https://dev.ada-anvil.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://dev.ada-anvil.io/guides/nft-and-ft/mint-nft-cip-25/cli-tool-to-mint-assets.md).

# 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**

{% code overflow="wrap" %}

```typescript
import { Buffer } from "node:buffer";
import {
  Credential,
  type Ed25519KeyHash,
  FixedTransaction,
  NativeScript,
  NativeScripts,
  PrivateKey,
  ScriptAll,
  ScriptPubkey,
  TimelockExpiry,
} from "npm:@emurgo/cardano-serialization-lib-nodejs@13.2.0";
import { parseArgs } from "jsr:@std/cli/parse-args";
```

{% endcode %}

### Cardano Wallets

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

```bash
~/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:

{% content-ref url="<https://github.com/Cardano-Forge/anvil-api/blob/main/docs/guides/utilities-functions/README.md>" %}
<https://github.com/Cardano-Forge/anvil-api/blob/main/docs/guides/utilities-functions/README.md>
{% endcontent-ref %}

### CLI

{% code overflow="wrap" %}

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

{% endcode %}

Parse and prepare the policy and wallets:

{% code overflow="wrap" %}

```typescript
// 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 DateTime is reached.
// After that the policy is locked.
const slot = timeToSlot(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);
```

{% endcode %}

### Collection configurations

This example requires 2 rules.

{% code overflow="wrap" %}

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

{% endcode %}

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 DateTime 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**

{% code overflow="wrap" %}

```typescript
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,
});
```

{% endcode %}

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

<mark style="color:red;">TBD: Show an example output (screenshot or link to preprod url or all of them)</mark>

### The transaction

{% code overflow="wrap" %}

```typescript
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());
```

{% endcode %}

### Sign with the policy wallet

{% code overflow="wrap" %}

```typescript
// 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)
);
```

{% endcode %}

### Sign with the customer wallet

*usually done using the browser extension*

{% code overflow="wrap" %}

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

{% endcode %}

### Submit Transaction

{% code overflow="wrap" %}

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

{% endcode %}

***

### Using the CLI

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

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

```sh
./index --expireDate=2025-01-01 \
  --customerWalletPath=customer.json \
  --policyWalletPath=policy.json \
  --metadataTemplatePath=metatemplate.json \
  --counter=2 \
  --submit
```

<details>

<summary>File Content - (customer.json, policy.json, metatemplate.json)</summary>

**customer.json**

{% code overflow="wrap" %}

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

{% endcode %}

**policy.json**

{% code overflow="wrap" %}

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

{% endcode %}

**metatemplate.json**

{% code overflow="wrap" %}

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

{% endcode %}

*The code will override the name key*

</details>

## Explorer

* <https://preprod.cexplorer.io/policy/fa16f906cb86e6b147683c0bede27215b85550f8b91a3f677292eabb>

## The Whole File (Deno Version)

<details>

<summary>index.ts</summary>

{% code overflow="wrap" %}

```typescript
import { Buffer } from "node:buffer";
import {
  Credential,
  type Ed25519KeyHash,
  FixedTransaction,
  NativeScript,
  NativeScripts,
  PrivateKey,
  ScriptAll,
  ScriptPubkey,
  TimelockExpiry,
} from "npm:@emurgo/cardano-serialization-lib-nodejs@13.2.0";
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 timeToSlot = (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));

// DateTime before you can interact with the policy
const slot = timeToSlot(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());
}
```

{% endcode %}

</details>
