Treasury Updates Metadata (TS/JS + Fetch)
An example of a CIP-68 NFT where a central treasury wallet updates the on-chain metadata, using TypeScript or JavaScript.
This example requires several utility and configuration files. Create the following files in the same directory as your main script.
https://github.com/Cardano-Forge/anvil-api-examples/blob/main/utils/constant.ts
// See Authentication page for API key details.
export const X_API_KEY = "testnet_EyrkvCWDZqjkfLSe1pxaF0hXxUcByHEhHuXIBjt9";
export const NETWORK = "preprod";
export const API_URL = `https://${NETWORK}.api.ada-anvil.app/v2/services`;
export const HEADERS = {
"Content-Type": "application/json",
"x-api-key": X_API_KEY,
};
You will also need the shared utility functions, which you can find in the utils/shared.ts
file in the examples repository.
https://github.com/Cardano-Forge/anvil-api-examples/blob/main/fetch-utxos-from-the-backend/utxos/blockfrost.ts
import { Buffer } from "node:buffer";
import {
TransactionUnspentOutput,
TransactionInput,
TransactionOutput,
TransactionHash,
MultiAsset,
Value,
Assets,
AssetName,
BigNum,
ScriptHash,
Address,
} from "npm:@emurgo/cardano-serialization-lib-nodejs";
export function hex_to_uint8(input: string): Uint8Array {
return new Uint8Array(Buffer.from(input, "hex"));
}
export function assets_to_value(
multi_asset: MultiAsset,
assets: Asset[],
): Value {
const qt = assets.find((asset) => asset.unit === "lovelace")?.quantity;
if (!qt) {
throw new Error("No lovelace found in the provided utxo");
}
const assets_to_add: Record<string, Assets> = {};
for (const asset of assets) {
if (asset.unit !== "lovelace") {
const policy_hex = asset.unit.slice(0, 56);
const asset_hex = hex_to_uint8(asset.unit.slice(56));
if (!assets_to_add[policy_hex]) {
assets_to_add[policy_hex] = Assets.new();
}
assets_to_add[policy_hex].insert(
AssetName.new(asset_hex),
BigNum.from_str(asset.quantity),
);
}
}
for (const key of Object.keys(assets_to_add)) {
multi_asset.insert(ScriptHash.from_hex(key), assets_to_add[key]);
}
return Value.new_with_assets(BigNum.from_str(qt), multi_asset);
}
export type Asset = {
unit: string;
quantity: string;
};
export type Utxo = {
address: string;
tx_hash: string;
output_index: number;
amount: Asset[];
block?: string;
data_hash?: string | null;
inline_datum?: string | null;
reference_script_hash?: string | null;
};
export async function getUtxos(
blockfrost_base_url: string,
blockfrost_api_key: string,
address: string,): Promise<string[]> {
const utxoHexList: string[] = [];
let page = 1;
// Loop through paginated results from Blockfrost
while (true) {
const res = await fetch(
`${blockfrost_base_url}/addresses/${address}/utxos?page=${page}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
project_id: blockfrost_api_key,
},
},
);
if (!res.ok) {
const errorText = await res.text();
throw new Error(
`Unable to get utxos for specified address. Status: ${res.status}, Response: ${errorText}`,
);
}
const pageUtxos: Utxo[] = await res.json();
// If the page is empty, we've fetched all UTXOs.
if (pageUtxos.length === 0) {
break;
}
// Convert UTXOs to array of CBOR hex strings
for (const utxo of pageUtxos) {
const multi_assets = MultiAsset.new();
const { tx_hash, output_index, amount, address } = utxo;
const txUnspentOutput = TransactionUnspentOutput.new(
TransactionInput.new(TransactionHash.from_hex(tx_hash), output_index),
TransactionOutput.new(
Address.from_bech32(address),
assets_to_value(multi_assets, amount),
),
);
utxoHexList.push(txUnspentOutput.to_hex());
}
page++;
}
return utxoHexList;
}
// Function to find CIP-68 reference token (label 100) UTXO dynamically from script address
export async function findReferenceTokenUTXO(
blockfrost_base_url: string,
blockfrost_api_key: string,
policyId: string,
baseAssetName: string,
scriptAddress: string,
): Promise<{ transaction_id: string; output_index: number }> {
// Fetch all UTXOs from script address
const response = await fetch(
`${blockfrost_base_url}/addresses/${scriptAddress}/utxos`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
project_id: blockfrost_api_key,
},
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch UTXOs: ${response.status} ${response.statusText}. Details: ${errorText}`);
}
const utxos: Utxo[] = await response.json();
// CIP-68 reference token has label 100 (full CIP-67 format: 000643b0)
const referenceTokenLabel = "000643b0"; // 100 with CRC-8 checksum per CIP-67
const expectedAssetUnit = `${policyId}${referenceTokenLabel}${Buffer.from(baseAssetName).toString("hex")}`;
// Find UTXO containing the reference token
for (const utxo of utxos) {
for (const asset of utxo.amount) {
if (asset.unit === expectedAssetUnit && parseInt(asset.quantity) > 0) {
return {
transaction_id: utxo.tx_hash,
output_index: utxo.output_index,
};
}
}
}
throw new Error(
`â Reference token not found at script address.\n` +
`Expected asset: ${expectedAssetUnit}\n` +
`Policy ID: ${policyId}\n` +
`Base asset name: ${baseAssetName}\n` +
`Script address: ${scriptAddress}`
);
}
// Function to find CIP-68 user token (label 222) UTXO dynamically from customer's wallet
export async function findUserTokenUTXO(
blockfrost_base_url: string,
blockfrost_api_key: string,
policyId: string,
baseAssetName: string,
customerAddress: string
): Promise<{ transaction_id: string; output_index: number }> {
// Fetch all UTXOs from customer's address
const response = await fetch(
`${blockfrost_base_url}/addresses/${customerAddress}/utxos`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
project_id: blockfrost_api_key,
},
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch UTXOs: ${response.status} ${response.statusText}. Details: ${errorText}`);
}
const utxos: Utxo[] = await response.json();
// CIP-68 user token has label 222 (full CIP-67 format: 000de140)
const userTokenLabel = "000de140"; // 222 with CRC-8 checksum per CIP-67
const expectedAssetUnit = `${policyId}${userTokenLabel}${Buffer.from(baseAssetName).toString("hex")}`;
// Find UTXO containing the user token
for (const utxo of utxos) {
for (const asset of utxo.amount) {
if (asset.unit === expectedAssetUnit && parseInt(asset.quantity) > 0) {
return {
transaction_id: utxo.tx_hash,
output_index: utxo.output_index,
};
}
}
}
throw new Error(
`â User token not found in wallet.\n` +
`Expected asset: ${expectedAssetUnit}\n` +
`Policy ID: ${policyId}\n` +
`Base asset name: ${baseAssetName}\n` +
`Customer address: ${customerAddress}`
);
}
Now, you can run the main script:
https://github.com/Cardano-Forge/anvil-api-examples/blob/main/documentation-references/cip68-treasury-update-meta.ts
/**
* CIP-68 Metadata Update with Treasury-Paid Fees Example
*
* This script demonstrates how to update the metadata of a CIP-68 asset (specifically the reference token)
* where the transaction fees are paid from a designated treasury wallet.
*
* This involves:
* 1. Loading treasury, policy, and metadata manager wallets.
* 2. Defining the target asset and policy details.
* 3. Fetching UTXOs from the treasury wallet to cover transaction fees.
* 4. Building a transaction payload to update the CIP-68 metadata.
* 5. Building the transaction via the Anvil API.
* 6. Signing the transaction with both the metadata manager's key (as required by the reference token's datum)
* and the treasury wallet's key (to authorize fee payment).
* 7. Submitting the fully signed transaction to the blockchain.
*
* Required files: wallet-treasury.json, wallet-policy.json, wallet-meta-manager.json
* Required environment variables: BLOCKFROST_PROJECT_ID, ANVIL_API_KEY
* Prerequisite: deno run --allow-all --env-file=.env cip68-treasury-update-meta.ts
*/
import { Buffer } from "node:buffer";
import {
FixedTransaction,
PrivateKey,
} from "npm:@emurgo/[email protected]";
import {
createNativeScript,
timeToSlot,
getKeyhash,
} from "../utils/shared.ts";
import { API_URL, HEADERS } from "../utils/constant.ts";
import { getUtxos } from "../fetch-utxos-from-the-backend/utxos/blockfrost.ts";
// 1. Load wallets
const treasuryWallet = JSON.parse(Deno.readTextFileSync("wallet-treasury.json"));
const policyWallet = JSON.parse(Deno.readTextFileSync("wallet-policy.json"));
const metaManagerWallet = JSON.parse(Deno.readTextFileSync("wallet-meta-manager.json"));
// 2. Define Asset and Policy Details
// This is the hex-encoded asset name (policyID + assetName) of the CIP-68 token pair.
// The API will automatically find the reference (100) token to update.
const assetName = "000643b0616e76696c61706963697036385f31373532373032393939373838";
// The native script is needed to be pre-loaded for the API to validate the transaction.
// In a real-world scenario, you might just have the policy ID if it's already on-chain.
const expirationDate = "2026-01-01";
const slot = await timeToSlot(new Date(expirationDate));
const keyhash = await getKeyhash(policyWallet.base_address_preprod);
if (!keyhash) {
throw new Error("Unable to get key hash for policy wallet.");
}
const nativeScript = await createNativeScript(keyhash, slot);
// 3. Fetch Treasury UTXOs
const BLOCKFROST_PROJECT_ID = Deno.env.get("BLOCKFROST_PROJECT_ID");
if (!BLOCKFROST_PROJECT_ID) {
throw new Error("Missing BLOCKFROST_PROJECT_ID env var");
}
const utxos = await getUtxos(
"https://cardano-preprod.blockfrost.io/api/v0",
BLOCKFROST_PROJECT_ID,
treasuryWallet.base_address_preprod,
);
console.log(`Found ${utxos.length} UTXOs in treasury wallet.`);
// 4. Build the transaction payload
const buildBody = {
changeAddress: treasuryWallet.base_address_preprod,
utxos,
cip68MetadataUpdates: [
{
policyId: nativeScript.hash,
assetName: assetName,
metadata: {
name: "New Updated Name",
description: "This is the updated description for the CIP-68 asset.",
image: "ipfs://new-image-hash-goes-here",
},
action: "update",
},
],
preloadedScripts: [
{
type: "simple",
script: nativeScript.script,
hash: nativeScript.hash,
},
],
};
console.log("Build Body: ", buildBody);
// 5. Build the transaction
const buildResponse = await fetch(`${API_URL}/transactions/build`, {
method: "POST",
body: JSON.stringify(buildBody),
headers: HEADERS,
});
const buildJson = await buildResponse.json();
if (!buildResponse.ok) {
console.error("Build failed. Response:", buildJson);
throw new Error(`Failed to build transaction: ${buildResponse.statusText}`);
}
console.log("Build Output:", buildJson);
// 6. Sign the transaction
const transaction = FixedTransaction.from_bytes(
Buffer.from(buildJson.complete, "hex"),
);
// Sign with MetaManager wallet (required by the datum on the reference token)
transaction.sign_and_add_vkey_signature(PrivateKey.from_bech32(metaManagerWallet.skey));
// Sign with Treasury wallet (to authorize spending the UTXO for fees)
transaction.sign_and_add_vkey_signature(PrivateKey.from_bech32(treasuryWallet.skey));
// 7. Submit the transaction
const submitResponse = await fetch(`${API_URL}/transactions/submit`, {
method: "POST",
body: JSON.stringify({
transaction: transaction.to_hex(),
}),
headers: HEADERS,
});
const submitJson = await submitResponse.json();
if (!submitResponse.ok) {
console.error("Submission failed. Response:", submitJson);
throw new Error(`Failed to submit transaction: ${submitResponse.statusText}`);
}
console.log("Submitted Transaction:", JSON.stringify(submitJson, null, 2));
/*
Sample Output:
Found 3 UTXOs in treasury wallet.
Build Body: {
changeAddress: "addr_test1qpxpar5s94wv6n76jvmvkr8uhnrflge6fatjzlkuujw3m33mk0alnas44ptzrkqqp3kdhplmcwys5mlht5w6seefrtls2r748e",
utxos: [
"82825820cbdd371aca5fef27267d5a41d9af6c6d9c9ba46b16aa5501f6fae91d3415a1f700825839004c1e8e902d5ccd4fda9336cb0cfcbcc69fa33a4f57217edce49d1dc63bb3fbf9f615a85621d8000c6cdb87fbc3890a6ff75d1da867291aff1a05f5e100",
"82825820f3c3344a570042a0ad5753bd6af8a6ff360dbc41ea35c77f6502da89427efc9c00825839004c1e8e902d5ccd4fda9336cb0cfcbcc69fa33a4f57217edce49d1dc63bb3fbf9f615a85621d8000c6cdb87fbc3890a6ff75d1da867291aff1a01312d00",
"82825820d6aefd369f3bf1c17398c43a7608d197cba623f7fa60831f77865a9c76ecd56602825839004c1e8e902d5ccd4fda9336cb0cfcbcc69fa33a4f57217edce49d1dc63bb3fbf9f615a85621d8000c6cdb87fbc3890a6ff75d1da867291aff821a097f845ca3581c38fdf893232baf9703e2f051ff9083a2756063c8bce3d80eb8d5eb3fa148000de1407465737401581c5e3817eacf8b6c393307fa61b00d546678e24414380dc7a2d879bc78a148000de1407465737401581cf8e024681ee83f54bd5f9a033464168f619970289017a9f53454d950a148000de1407465737401"
],
cip68MetadataUpdates: [
{
policyId: "4d5bd6249f0d9e4b2762ce334e2973dc7fd414ec1e08b4b0c2159bfb",
assetName: "000643b0616e76696c61706963697036385f31373532373032393939373838",
metadata: {
name: "New Updated Name",
description: "This is the updated description for the CIP-68 asset.",
image: "ipfs://new-image-hash-goes-here"
},
action: "update"
}
],
preloadedScripts: [
{
type: "simple",
script: { type: "all", scripts: [ [Object], [Object] ] },
hash: "4d5bd6249f0d9e4b2762ce334e2973dc7fd414ec1e08b4b0c2159bfb"
}
]
}
Build Output: {
hash: "3bbfdc451b2d12a3388146113b9411da603c205a835579a6e1397e696e04f0df",
complete: "84a700d90102838258209d13e6fdec8ddae0b7e94e27100feb67d30ab13b7f284f8c672f662018e16ff101825820d6aefd369f3bf1c17398c43a7608d197cba623f7fa60831f77865a9c76ecd56600825820d6aefd369f3bf1c17398c43a7608d197cba623f7fa60831f77865a9c76ecd56602018383583900bd2c5bba13a4f58a8a1ffd15e7c4f759f562c2dd928e3bf555fca3cb20106f57def03022a0a9751c4189de03b16730536a231f56df898dd3821a00157084a1581c4d5bd6249f0d9e4b2762ce334e2973dc7fd414ec1e08b4b0c2159bfba1581f000643b0616e76696c61706963697036385f31373532373032393939373838015820a68af340c4deadbc6dfadf635d8e8145d2d5dcdedb06ae4b27a218d0b5823ef8a300581d60ea2cb2c69f72fc8e3c31f836de3d6877267bbdc50311c3e8eecf099c011a00186a00028201d8184a49616e76696c2d746167825839004c1e8e902d5ccd4fda9336cb0cfcbcc69fa33a4f57217edce49d1dc63bb3fbf9f615a85621d8000c6cdb87fbc3890a6ff75d1da867291aff821a0979d543a3581c38fdf893232baf9703e2f051ff9083a2756063c8bce3d80eb8d5eb3fa148000de1407465737401581c5e3817eacf8b6c393307fa61b00d546678e24414380dc7a2d879bc78a148000de1407465737401581cf8e024681ee83f54bd5f9a033464168f619970289017a9f53454d950a148000de1407465737401021a00036529031a05c887fd081a05c86bdd0b5820ff3e6f8c66bcd5a36e6ad36d70bc96873d98e0684e0f1cae68023aa5c20a18e70ed9010281581cea2cb2c69f72fc8e3c31f836de3d6877267bbdc50311c3e8eecf099ca200d90102818258208bc6baaa47db83c0a0d957d559e4b443a9f7a5757ab3c184cf8c2b2ee21bb69558404800f8b668617063b0ecd1cee7760b205d2071ea97b5726c9ec89bcf90db00e2e2b4b8560aa2aae47eab657839e99fe997f17c4703d2ab2da4d778fe0659d40f04d901029fd8799fa3446e616d65504e65772055706461746564204e616d654b6465736372697074696f6e583554686973206973207468652075706461746564206465736372697074696f6e20666f7220746865204349502d36382061737365742e45696d616765581f697066733a2f2f6e65772d696d6167652d686173682d676f65732d6865726501fffff5f6",
stripped: "84a700d90102838258209d13e6fdec8ddae0b7e94e27100feb67d30ab13b7f284f8c672f662018e16ff101825820d6aefd369f3bf1c17398c43a7608d197cba623f7fa60831f77865a9c76ecd56600825820d6aefd369f3bf1c17398c43a7608d197cba623f7fa60831f77865a9c76ecd56602018383583900bd2c5bba13a4f58a8a1ffd15e7c4f759f562c2dd928e3bf555fca3cb20106f57def03022a0a9751c4189de03b16730536a231f56df898dd3821a00157084a1581c4d5bd6249f0d9e4b2762ce334e2973dc7fd414ec1e08b4b0c2159bfba1581f000643b0616e76696c61706963697036385f31373532373032393939373838015820a68af340c4deadbc6dfadf635d8e8145d2d5dcdedb06ae4b27a218d0b5823ef8a300581d60ea2cb2c69f72fc8e3c31f836de3d6877267bbdc50311c3e8eecf099c011a00186a00028201d8184a49616e76696c2d746167825839004c1e8e902d5ccd4fda9336cb0cfcbcc69fa33a4f57217edce49d1dc63bb3fbf9f615a85621d8000c6cdb87fbc3890a6ff75d1da867291aff821a0979d543a3581c38fdf893232baf9703e2f051ff9083a2756063c8bce3d80eb8d5eb3fa148000de1407465737401581c5e3817eacf8b6c393307fa61b00d546678e24414380dc7a2d879bc78a148000de1407465737401581cf8e024681ee83f54bd5f9a033464168f619970289017a9f53454d950a148000de1407465737401021a00036529031a05c887fd081a05c86bdd0b5820ff3e6f8c66bcd5a36e6ad36d70bc96873d98e0684e0f1cae68023aa5c20a18e70ed9010281581cea2cb2c69f72fc8e3c31f836de3d6877267bbdc50311c3e8eecf099ca0f5f6",
witnessSet: "a200d90102818258208bc6baaa47db83c0a0d957d559e4b443a9f7a5757ab3c184cf8c2b2ee21bb69558404800f8b668617063b0ecd1cee7760b205d2071ea97b5726c9ec89bcf90db00e2e2b4b8560aa2aae47eab657839e99fe997f17c4703d2ab2da4d778fe0659d40f04d901029fd8799fa3446e616d65504e65772055706461746564204e616d654b6465736372697074696f6e583554686973206973207468652075706461746564206465736372697074696f6e20666f7220746865204349502d36382061737365742e45696d616765581f697066733a2f2f6e65772d696d6167652d686173682d676f65732d6865726501ffff"
}
Submitted Transaction: {
"txHash": "3bbfdc451b2d12a3388146113b9411da603c205a835579a6e1397e696e04f0df"
}
*/
Learn More
For a step-by-step explanation, see the Update CIP-68 Metadata Guide.
Last updated
Was this helpful?