Skip to content

Feature: render ZAP1 attestation memos as typed event cards #2172

Description

@Zk-nd3r

What

ZAP1 is an open attestation protocol that writes structured lifecycle events to Zcash shielded memos. There are currently 5 Merkle roots anchored on mainnet, 23 leaves, and 15 event types deployed.

Zodl already decrypts and displays these memos - but they show as raw strings like ZAP1:04:f265b9a06a61b2b8.... This proposal adds formatted rendering so users see what the attestation actually means.

Before / After

Current Proposed
ZAP1:01:075b00df2860... PROGRAM_ENTRY - participant enrolled 075b00df... [Verify]
ZAP1:04:f265b9a06a61... DEPLOYMENT - machine activated f265b9a0... [Verify]
ZAP1:09:024e36515ea3... MERKLE_ROOT - tree anchored at block 3,286,631 024e3651... [Verify]
ZAP1:0d:a487c25f5867... GOVERNANCE_PROPOSAL - proposal submitted a487c25f... [Verify]
Non-ZAP1 memos Unchanged

The Verify link opens the proof page where anyone can independently check the Merkle path back to the Zcash anchor transaction.

Scope

Transaction detail rendering only. No send flow changes. No sync changes. No new permissions.

Detection: check if the decrypted memo string matches ^(ZAP1|NSM1):[0-9a-f]{2}:[0-9a-f]{64}$

Rendering: replace the raw string with an attestation card showing event label, short hash, copy action, and verify link.

Fallback: malformed ZAP1-like strings render as plain text (current behavior).

NSM1: is the legacy prefix - same format, accepted during decode for backward compatibility.

Integration point

The memo string is already resolved before it reaches the UI. The change is at the rendering boundary:

  • TransactionDetailVM.kt lines 273-275 and 315-317: content = stringRes(memo) - intercept here
  • New file Zap1MemoFormatter.kt: prefix detection + type lookup (~30 lines)
  • TransactionDetailInfoMemo.kt: attestation card variant

Parser (complete, 30 lines)

object Zap1MemoFormatter {
    private val PATTERN = Regex("^(ZAP1|NSM1):([0-9a-f]{2}):([0-9a-f]{64})$")

    private val EVENTS = mapOf(
        "01" to "PROGRAM_ENTRY", "02" to "OWNERSHIP_ATTEST",
        "03" to "CONTRACT_ANCHOR", "04" to "DEPLOYMENT",
        "05" to "HOSTING_PAYMENT", "06" to "SHIELD_RENEWAL",
        "07" to "TRANSFER", "08" to "EXIT",
        "09" to "MERKLE_ROOT", "0a" to "STAKING_DEPOSIT",
        "0b" to "STAKING_WITHDRAW", "0c" to "STAKING_REWARD",
        "0d" to "GOVERNANCE_PROPOSAL", "0e" to "GOVERNANCE_VOTE",
        "0f" to "GOVERNANCE_RESULT"
    )

    fun parse(memo: String): Attestation? {
        val m = PATTERN.matchEntire(memo.trim()) ?: return null
        return Attestation(
            prefix = m.groupValues[1],
            typeHex = m.groupValues[2],
            event = EVENTS[m.groupValues[2]] ?: "TYPE_0x${m.groupValues[2]}",
            hash = m.groupValues[3]
        )
    }

    data class Attestation(
        val prefix: String,
        val typeHex: String,
        val event: String,
        val hash: String
    ) {
        val verifyUrl get() = "https://pay.frontiercompute.io/verify/$hash"
        val shortHash get() = hash.take(12) + "..."
        val isLegacy get() = prefix == "NSM1"
    }
}

No new dependencies

The parser is pure Kotlin string matching. No crate bindings, no FFI, no network calls. For projects that want deeper integration, the zcash-memo-decode Rust crate (0 dependencies) classifies all Zcash memo formats including ZAP1, ZIP 302, text, binary, and empty.

Context

Happy to open the PR if there's interest.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions