pragma language_version >= 0.23;
import CompactStandardLibrary;
// User keypair (witness-derived)
struct UserSecretKey { bytes: Bytes<32>; }
struct UserPublicKey { bytes: Bytes<32>; }
witness getUserSecret(): UserSecretKey;
// Admin keypair (witness-derived) — gates _mint and _burn
struct AdminSecretKey { bytes: Bytes<32>; }
struct AdminPublicKey { bytes: Bytes<32>; }
witness getAdminSecret(): AdminSecretKey;
// Metadata (sealed: settable only in the constructor)
export sealed ledger _name: Opaque<"string">;
export sealed ledger _symbol: Opaque<"string">;
// Admin
export ledger _admin: AdminPublicKey;
// Ownership maps
// _owners[tokenId] = owner pubkey
// _balances[owner] = how many NFTs that owner holds
export ledger _owners: Map<Uint<128>, UserPublicKey>;
export ledger _balances: Map<UserPublicKey, Uint<128>>;
// Approvals
// _tokenApprovals[tokenId] = pubkey approved for that one token
// _operatorApprovals[owner][operator] = operator can manage all of owner's NFTs
export ledger _tokenApprovals: Map<Uint<128>, UserPublicKey>;
export ledger _operatorApprovals: Map<UserPublicKey, Map<UserPublicKey, Boolean>>;
// Per-token metadata URIs
export ledger _tokenURIs: Map<Uint<128>, Opaque<"string">>;
constructor(
initialAdmin: AdminPublicKey,
name_: Opaque<"string">,
symbol_: Opaque<"string">
) {
_admin = disclose(initialAdmin);
_name = disclose(name_);
_symbol = disclose(symbol_);
}
// Key derivation
export circuit deriveUserPublicKey(sk: UserSecretKey): UserPublicKey {
return UserPublicKey {
bytes: persistentHash<Vector<2, Bytes<32>>>([
pad(32, "erc721:user:v1"),
sk.bytes
])
};
}
export circuit deriveAdminPublicKey(sk: AdminSecretKey): AdminPublicKey {
return AdminPublicKey {
bytes: persistentHash<Vector<2, Bytes<32>>>([
pad(32, "erc721:admin:v1"),
sk.bytes
])
};
}
// Metadata
export circuit name(): Opaque<"string"> { return _name; }
export circuit symbol(): Opaque<"string"> { return _symbol; }
export circuit ownerOf(tokenId: Uint<128>): UserPublicKey {
return _requireOwned(tokenId);
}
export circuit balanceOf(owner: UserPublicKey): Uint<128> {
if (!_balances.member(disclose(owner))) { return 0; }
return _balances.lookup(disclose(owner));
}
export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> {
_requireOwned(tokenId);
if (!_tokenURIs.member(disclose(tokenId))) { return default<Opaque<"string">>; }
return _tokenURIs.lookup(disclose(tokenId));
}
// Approve a single token for transfer by `to`
export circuit approve(to: UserPublicKey, tokenId: Uint<128>): [] {
const auth = disclose(deriveUserPublicKey(getUserSecret()));
_approve(to, tokenId, auth);
}
export circuit getApproved(tokenId: Uint<128>): UserPublicKey {
_requireOwned(tokenId);
return _getApproved(tokenId);
}
// Approve operator for all of caller's NFTs
export circuit setApprovalForAll(operator: UserPublicKey, approved: Boolean): [] {
const owner = disclose(deriveUserPublicKey(getUserSecret()));
_setApprovalForAll(owner, operator, approved);
}
export circuit isApprovedForAll(owner: UserPublicKey, operator: UserPublicKey): Boolean {
if (_operatorApprovals.member(disclose(owner)) &&
_operatorApprovals.lookup(disclose(owner)).member(disclose(operator))) {
return _operatorApprovals.lookup(disclose(owner)).lookup(disclose(operator));
}
return false;
}
// Transfer — caller must be owner, approved-for-token, or approved-for-all
export circuit transferFrom(
fromKey: UserPublicKey,
to: UserPublicKey,
tokenId: Uint<128>
): [] {
const caller = disclose(deriveUserPublicKey(getUserSecret()));
const previousOwner = _update(to, tokenId, caller);
assert(previousOwner == fromKey, "Incorrect owner");
}
// Admin: mint a new NFT
export circuit mint(to: UserPublicKey, tokenId: Uint<128>): [] {
assertAdmin();
assert(!_owners.member(disclose(tokenId)), "Token already minted");
_balances.insert(disclose(to), disclose((balanceOf(to) + 1) as Uint<128>));
_owners.insert(disclose(tokenId), disclose(to));
}
// Admin: burn an NFT
export circuit adminBurn(tokenId: Uint<128>): [] {
assertAdmin();
const owner = _requireOwned(tokenId);
// clear single-token approval
if (_tokenApprovals.member(disclose(tokenId))) {
_tokenApprovals.remove(disclose(tokenId));
}
_balances.insert(disclose(owner), disclose(_balances.lookup(disclose(owner)) - 1));
_owners.remove(disclose(tokenId));
}
// Set the URI of a token you own
export circuit setTokenURI(tokenId: Uint<128>, uri: Opaque<"string">): [] {
const caller = disclose(deriveUserPublicKey(getUserSecret()));
const owner = _requireOwned(tokenId);
assert(owner == caller, "Only owner may set URI");
_tokenURIs.insert(disclose(tokenId), disclose(uri));
}
// Internals
circuit assertAdmin(): [] {
assert(_admin == deriveAdminPublicKey(getAdminSecret()), "Not admin");
}
circuit _requireOwned(tokenId: Uint<128>): UserPublicKey {
assert(_owners.member(disclose(tokenId)), "Token does not exist");
return _owners.lookup(disclose(tokenId));
}
circuit _getApproved(tokenId: Uint<128>): UserPublicKey {
if (!_tokenApprovals.member(disclose(tokenId))) {
return UserPublicKey { bytes: pad(32, "") };
}
return _tokenApprovals.lookup(disclose(tokenId));
}
circuit _approve(to: UserPublicKey, tokenId: Uint<128>, auth: UserPublicKey): [] {
const owner = _requireOwned(tokenId);
assert(owner == auth || isApprovedForAll(owner, auth), "Not authorized");
_tokenApprovals.insert(disclose(tokenId), disclose(to));
}
circuit _setApprovalForAll(
owner: UserPublicKey,
operator: UserPublicKey,
approved: Boolean
): [] {
if (!_operatorApprovals.member(disclose(owner))) {
_operatorApprovals.insert(
disclose(owner),
default<Map<UserPublicKey, Boolean>>
);
}
_operatorApprovals.lookup(disclose(owner)).insert(disclose(operator), disclose(approved));
}
circuit _update(to: UserPublicKey, tokenId: Uint<128>, auth: UserPublicKey): UserPublicKey {
const fromKey = _requireOwned(tokenId);
_checkAuthorized(fromKey, auth, tokenId);
// clear single-token approval
if (_tokenApprovals.member(disclose(tokenId))) {
_tokenApprovals.remove(disclose(tokenId));
}
// adjust balances
_balances.insert(
disclose(fromKey),
disclose(_balances.lookup(disclose(fromKey)) - 1)
);
if (!_balances.member(disclose(to))) {
_balances.insert(disclose(to), 0);
}
_balances.insert(
disclose(to),
disclose((_balances.lookup(disclose(to)) + 1) as Uint<128>)
);
_owners.insert(disclose(tokenId), disclose(to));
return fromKey;
}
circuit _checkAuthorized(owner: UserPublicKey, spender: UserPublicKey, tokenId: Uint<128>): [] {
assert(
owner == spender
|| isApprovedForAll(owner, spender)
|| _getApproved(tokenId) == spender,
"Not authorized"
);
}