Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ public struct TransactionDetails {
case .memosLoaded(let memos):
state.areMessagesResolved = true
state.$transactionMemos.withLock {
$0[state.transaction.id] = memos.compactMap { $0.toString() }
$0[state.transaction.id] = memos.compactMap { $0.toString() }.map { Zap1Attestation.format($0) ?? $0 }
}
state.messageStates = state.memos.map {
$0.count < State.Constants.messageExpandThreshold ? .short : .longCollapsed
Expand Down
54 changes: 54 additions & 0 deletions modules/Sources/Features/TransactionDetails/Zap1MemoParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

/// Parses ZAP1 and legacy NSM1 attestation memos into structured data.
struct Zap1Attestation {
let prefix: String
let typeHex: String
let event: String
let hash: String

var shortHash: String { String(hash.prefix(12)) + "..." }
var isLegacy: Bool { prefix == "NSM1" }

// Compiled once, reused across all parse calls
private static let pattern = try! NSRegularExpression(
pattern: #"^(ZAP1|NSM1):([0-9a-fA-F]{2}):([0-9a-fA-F]{64})$"#
)

private static let events: [String: String] = [
"01": "PROGRAM_ENTRY", "02": "OWNERSHIP_ATTEST",
"03": "CONTRACT_ANCHOR", "04": "DEPLOYMENT",
"05": "HOSTING_PAYMENT", "06": "SHIELD_RENEWAL",
"07": "TRANSFER", "08": "EXIT",
"09": "MERKLE_ROOT", "0a": "STAKING_DEPOSIT",
"0b": "STAKING_WITHDRAW", "0c": "STAKING_REWARD",
"0d": "GOVERNANCE_PROPOSAL", "0e": "GOVERNANCE_VOTE",
"0f": "GOVERNANCE_RESULT"
]

static func parse(_ memo: String) -> Zap1Attestation? {
let trimmed = memo.trimmingCharacters(in: .whitespacesAndNewlines)
let range = NSRange(trimmed.startIndex..., in: trimmed)
guard let match = pattern.firstMatch(in: trimmed, range: range),
let prefixRange = Range(match.range(at: 1), in: trimmed),
let typeRange = Range(match.range(at: 2), in: trimmed),
let hashRange = Range(match.range(at: 3), in: trimmed)
else { return nil }

let prefix = String(trimmed[prefixRange])
let typeHex = String(trimmed[typeRange]).lowercased()
let hash = String(trimmed[hashRange])

return Zap1Attestation(
prefix: prefix,
typeHex: typeHex,
event: events[typeHex] ?? "TYPE_0x\(typeHex)",
hash: hash
)
}

static func format(_ memo: String) -> String? {
guard let attestation = parse(memo) else { return nil }
return "ZAP1: \(attestation.event) \(attestation.shortHash)"
}
}
68 changes: 68 additions & 0 deletions secantTests/Zap1MemoParserTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import XCTest

class Zap1MemoParserTests: XCTestCase {
func testParseProgramEntry() {
let att = Zap1Attestation.parse(
"ZAP1:01:075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b"
)

XCTAssertNotNil(att)
XCTAssertEqual(att?.prefix, "ZAP1")
XCTAssertEqual(att?.typeHex, "01")
XCTAssertEqual(att?.event, "PROGRAM_ENTRY")
XCTAssertEqual(att?.shortHash, "075b00df2860...")
XCTAssertEqual(att?.isLegacy, false)
}

func testParseLegacyNSM1() {
let att = Zap1Attestation.parse(
"NSM1:04:f265b9a06a61b2b8c6eeed7fc00c7aa686ad511053467815bf1f1037d460e1f1"
)

XCTAssertNotNil(att)
XCTAssertEqual(att?.prefix, "NSM1")
XCTAssertEqual(att?.event, "DEPLOYMENT")
XCTAssertEqual(att?.isLegacy, true)
}

func testParseGovernanceUppercaseHex() {
let att = Zap1Attestation.parse(
"ZAP1:0D:A487C25F5867A9E3760C45AE7EED24D84E771568F1826A889CCD94B3C7C3A5B5"
)

XCTAssertNotNil(att)
XCTAssertEqual(att?.typeHex, "0d")
XCTAssertEqual(att?.event, "GOVERNANCE_PROPOSAL")
}

func testRejectsInvalidMemos() {
XCTAssertNil(Zap1Attestation.parse("Hello world"))
XCTAssertNil(Zap1Attestation.parse("ZAP1:xx:notahash"))
XCTAssertNil(Zap1Attestation.parse("ZAP1:01:tooshort"))
XCTAssertNil(Zap1Attestation.parse(""))
XCTAssertNil(Zap1Attestation.parse(
"ZAP2:01:075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b"
))
}

func testFormatReturnsReadableString() {
let formatted = Zap1Attestation.format(
"ZAP1:09:024e36515ea30efc15a0a7962dd8f677455938079430b9eab174f46a4328a07a"
)

XCTAssertEqual(formatted, "ZAP1: MERKLE_ROOT 024e36515ea3...")
}

func testFormatReturnsNilForNonZap1() {
XCTAssertNil(Zap1Attestation.format("Just a regular memo"))
}

func testTrimsWhitespaceAndNewlines() {
let att = Zap1Attestation.parse(
" ZAP1:01:075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b\n"
)

XCTAssertNotNil(att)
XCTAssertEqual(att?.event, "PROGRAM_ENTRY")
}
}