Guides
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#
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";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 --mnemonicYou'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:
Utility FunctionsAnvil Utility Functions
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 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);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 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
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.tsDont 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 \
--submitFile 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/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());
}