Guides

User Update Example

Want to understand the smart contract validation logic first? Check out the Smart Contract Logic Guide to learn how the contract validates user updates. Otherwise, you can dive right into the implementation below!

Full Code Example#

Want to see the complete code first? Check out the full working implementation on GitHub, then come back here for the step-by-step breakdown.

Implementation Overview#

This example demonstrates user-driven metadata updates where token holders can update their own NFT metadata by:

  • Proving ownership of the user token (222)
  • Paying a 1 ADA fee to the admin
  • Updating limited metadata fields (e.g., nickname)
  • Following the spend validator's authorization rules

Prerequisites#

This guide assumes you have completed the Minting Example and have:

  1. Existing CIP-68 tokens - Reference and user tokens from a successful mint
  2. User token ownership - The user token (222) must be in the customer's wallet
  3. Reference token location - Current UTXO of the reference token at the script address
  4. Blockfrost API access - For dynamic UTXO discovery
  5. Understanding of user updates - Review update logic for validation details

Step-by-Step Implementation#

Let's build a user-driven CIP-68 metadata update script step by step. Each section explains the concept and shows the corresponding code.

Step 1: Setup and Configuration#

Create update file with the required imports and configuration:

import { Buffer } from "node:buffer";
import {
  FixedTransaction,
  PrivateKey,
} from "npm:@emurgo/cardano-serialization-lib-nodejs@14.1.1";

// Import wallet files and constants
import customer from "./wallet-customer.json" with { type: "json" };
import adminWallet from "./wallet-mint-sc-policy.json" with { type: "json" };
import { API_URL, HEADERS } from "../../../utils/constant.ts";

// Configuration
const CUSTOMER_ADDRESS = customer.base_address_preprod;
const ADMIN_KEY_HASH = adminWallet.key_hash;
const ADMIN_ADDRESS = adminWallet.base_address_preprod;
const assetName = "cip68_1753999309623"; // Your minted asset name
const nickname = "customer_updated"; // New nickname to set

// Import Blockfrost utility for dynamic UTXO fetching
import { 
  getUtxos, 
  findUserTokenUTXO, 
  findReferenceTokenUTXO 
} from "../../../fetch-utxos-from-the-backend/utxos/blockfrost.ts";

// Blockfrost configuration
const BLOCKFROST_BASE_URL = "https://cardano-preprod.blockfrost.io/api/v0";
const BLOCKFROST_API_KEY = Deno.env.get("BLOCKFROST_PROJECT_ID");

if (!BLOCKFROST_API_KEY) {
  console.error("❌ BLOCKFROST_PROJECT_ID environment variable is required");
  Deno.exit(1);
}

Key differences from admin updates:

  • Customer wallet signs the transaction (not admin)
  • Admin address receives the 1 ADA fee payment
  • Blockfrost integration for dynamic UTXO discovery
  • Asset name must match the originally minted token

Step 2: Apply Parameters and Find UTXOs#

Apply parameters to the blueprint and locate the required UTXOs:

// Load blueprint and apply parameters (same as other examples)
import blueprint from "../aiken-mint-cip-68/plutus.json" with { type: "json" };

console.log("🔄 Applying parameters to blueprint...");
const applyParamsResponse = await fetch(`${API_URL}/blueprints/apply-params`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    params: { [blueprint.validators[0].hash]: [ADMIN_KEY_HASH] },
    blueprint: blueprint
  }),
});

const applyParamsResult = await applyParamsResponse.json();
const parameterizedScript = applyParamsResult.preloadedScript;
const POLICY_ID = Object.keys(applyParamsResult.addresses)[0];
const SC_ADDRESS = applyParamsResult.addresses[POLICY_ID].bech32;

console.log("Parameterized script hash:", POLICY_ID);
console.log("Script address:", SC_ADDRESS);

// Find reference token UTXO at script address
const referenceTokenUTXO = await findReferenceTokenUTXO(
  BLOCKFROST_BASE_URL,
  BLOCKFROST_API_KEY,
  POLICY_ID,
  assetName,
  SC_ADDRESS
);

// Find user token UTXO at customer address
const userTokenUTXO = await findUserTokenUTXO(
  BLOCKFROST_BASE_URL,
  BLOCKFROST_API_KEY,
  POLICY_ID,
  assetName,
  CUSTOMER_ADDRESS
);

// Get user token UTXO in string format for requiredInputs
const utxos = await getUtxos(BLOCKFROST_BASE_URL, BLOCKFROST_API_KEY, CUSTOMER_ADDRESS);
const utxoString = `000de140${Buffer.from(assetName).toString("hex")}`;
const userUtxo = utxos.find((utxo) => utxo.includes(utxoString));

if (!userUtxo) {
  console.error("❌ User token UTXO not found at customer address");
  Deno.exit(1);
}

console.log(`Reference token: ${referenceTokenUTXO.transaction_id}#${referenceTokenUTXO.output_index}`);
console.log(`User token: ${userTokenUTXO.transaction_id}#${userTokenUTXO.output_index}`);

What's happening here:

  • Reference token discovery: Finds the current reference token UTXO at the script address
  • User token discovery: Locates the user token in the customer's wallet (proves ownership)
  • UTXO string format: Gets the hex-encoded UTXO string needed for requiredInputs
  • Validation: Ensures both tokens exist before proceeding

Step 3: Build the User Update Transaction#

Now we build the spend transaction with user authorization:

const input = {
  changeAddress: CUSTOMER_ADDRESS,
  message: "CIP-68 Customer Update Example",
  
  // SCRIPT INTERACTIONS: Tell the validator how to spend the reference token
  scriptInteractions: [
    {
      purpose: "spend", // Spending a UTXO locked at smart contract address
      
      // REFERENCE TOKEN UTXO: The UTXO containing the reference token to update
      outputRef: {
        txHash: referenceTokenUTXO.transaction_id,
        index: referenceTokenUTXO.output_index,
      },
      
      // REDEEMER: Authorization data for the spend validator
      redeemer: {
        type: "json",
        value: {
          output_index: 0, // Index of output containing updated reference token
          update: {
            // USER UPDATE: Triggers UserUpdate validation branch
            nickname: Buffer.from(nickname).toString("hex"), // New nickname in hex
            fee_output_index: 1, // Index of output paying 1 ADA fee to admin
            
            // USER TOKEN REFERENCE: Validator uses this to verify ownership
            output_ref_user_token: {
              transaction_id: userTokenUTXO.transaction_id,
              output_index: userTokenUTXO.output_index,
            },
          },
        },
      },
    },
  ],

Key redeemer components:

  • nickname: The updated value in hex format (limited user permission)
  • fee_output_index: Points to the 1 ADA fee payment output
  • output_ref_user_token: References the user token for ownership verification

Step 4: Define Transaction Outputs#

Create the outputs for the updated reference token and fee payment:

  // TRANSACTION OUTPUTS: What the transaction creates
  outputs: [
    {
      // OUTPUT 0: Updated reference token back to script address
      address: SC_ADDRESS,
      assets: [
        {
          assetName: { name: assetName, label: 100, format: "utf8" },
          policyId: POLICY_ID,
          quantity: 1,
        },
      ],
      // UPDATED DATUM: Contains new metadata with updated nickname
      datum: {
        type: "inline",
        shape: {
          validatorHash: POLICY_ID,
          purpose: "spend",
        },
        value: {
          metadata: [
            ["name", assetName], // Must match original minted asset name
            ["nickname", "customer_updated"], // Updated field
          ],
          version: 1,
        },
      },
    },
    {
      // OUTPUT 1: 1 ADA fee payment to admin
      address: ADMIN_ADDRESS,
      lovelace: "1000000", // Exactly 1 ADA - validator verifies this amount
    },
  ],

  requiredSigners: [], // No admin signature required for user updates
  referenceInputs: [],
  requiredInputs: [userUtxo!], // Include user token as input (proves ownership)
  preloadedScripts: [parameterizedScript],
};

Critical output requirements:

  • Output 0: Updated reference token with new metadata at script address
  • Output 1: Exactly 1 ADA fee payment to admin address
  • Datum preservation: Original asset name must be preserved
  • User token input: Must be included via requiredInputs for ownership proof

Step 5: Build, Sign, and Submit Transaction#

Build and submit the user update transaction:

// Build the transaction
const contractDeployed = await fetch(`${API_URL}/transactions/build`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify(input),
});

if (!contractDeployed.ok) {
  const errorText = await contractDeployed.text();
  console.error("❌ Transaction build failed:", errorText);
  Deno.exit(1);
}

const transaction = await contractDeployed.json();
console.log("✅ Transaction built successfully");

// Sign with customer's private key (not admin)
const txToSubmitOnChain = FixedTransaction.from_bytes(
  Buffer.from(transaction.complete, "hex")
);
txToSubmitOnChain.sign_and_add_vkey_signature(
  PrivateKey.from_bech32(customer.skey)
);

// Submit to network
const submitted = await fetch(`${API_URL}/transactions/submit`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ transaction: txToSubmitOnChain.to_hex() }),
});

const output = await submitted.json();
if (!submitted.ok) {
  console.error("❌ Transaction submission failed:", output);
  Deno.exit(1);
}

console.log("✅ Customer update transaction submitted successfully");
console.log("Transaction hash:", output.transactionId);

What's happening here:

  • Customer signature: Only the customer signs (proves they own the user token)
  • No admin signature: Admin authorization comes from fee payment, not signature
  • Validation on-chain: Smart contract validates ownership and fee payment

Running the Example#

Prerequisites Setup#

  1. Complete the minting example first to have CIP-68 tokens available

  2. Set up environment variables:

    # PowerShell/Windows
    $env:BLOCKFROST_PROJECT_ID='your_blockfrost_api_key'
    
    # Bash/macOS/Linux
    export BLOCKFROST_PROJECT_ID='your_blockfrost_api_key'
  3. Update configuration:

    • Set assetName to match your minted token
    • Modify nickname to your desired update value
    • Ensure wallet files are properly configured
  4. Run the user update script:

    deno run --allow-all user-update.ts

Expected Output#

🔄 Applying parameters to blueprint...
✅ Parameters applied successfully
Parameterized script hash: 4983663c2b236161ad8e26c36dff9aee709a6adef53be2...
Script address: addr_test1wpycxe3u9v3kzcdd3cnvxm0lnth8pxn2mm6nhch9f6pagxq...
Reference token: 1750a1c191646ade084c911fd9fd8a7c6b8372923e2305c4f4...#0
User token: 1750a1c191646ade084c911fd9fd8a7c6b8372923e2305c4f4...#1
✅ Transaction built successfully
✅ Customer update transaction submitted successfully
Transaction hash: b2cb92868e58238edb5646775847fca911c46c4d2206bb66b64c4df0af06187b

User vs Admin Updates#

This user update approach differs from admin updates in three key ways:

Aspect User Updates Admin Updates
Authorization Token ownership + 1 ADA fee Admin signature only
Permissions Limited fields (e.g., nickname) Full metadata access
Cost 1 ADA fee to admin Free for admin

The economic barrier prevents spam while giving users control over their metadata.

Troubleshooting#

Common Validation Failures#

If your transaction fails validation, check these common issues:

  • user_token_quantity != 1 - User token not included in transaction inputs
  • lovelace_paid != 1000000 - Fee amount is not exactly 1 ADA
  • !is_paid_to_admin - Fee payment sent to wrong address
  • current_datum != expected_datum - Attempting to update restricted metadata fields

For detailed validator logic, see the Update Logic Guide.

Key Requirements#

  • Include user token in requiredInputs for ownership proof
  • Pay exactly 1 ADA (1,000,000 lovelace) to admin address
  • Update only permitted fields as defined by the smart contract
  • Track UTXO locations dynamically (tokens move with each update)

Next Steps#

You now have a complete user-driven update implementation. For production applications, consider:

  • Batch Updates: Multiple metadata fields in one transaction
  • Update History: Track metadata changes for audit trails
  • Fee Estimation: Display costs before transaction submission
  • User Interface: Build a frontend for non-technical users

References#