> 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

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>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://dev.ada-anvil.io/guides/nft-and-ft/mint-nft-cip-25/cli-tool-to-mint-assets.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
