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.
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
ZAP1:01:075b00df2860...075b00df...[Verify]ZAP1:04:f265b9a06a61...f265b9a0...[Verify]ZAP1:09:024e36515ea3...024e3651...[Verify]ZAP1:0d:a487c25f5867...a487c25f...[Verify]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.ktlines 273-275 and 315-317:content = stringRes(memo)- intercept hereZap1MemoFormatter.kt: prefix detection + type lookup (~30 lines)TransactionDetailInfoMemo.kt: attestation card variantParser (complete, 30 lines)
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.