Private Guest List Contract
For the complete documentation index, see llms.txt
This Compact contract implements a party with a private guest list. It offers demonstration of the following features:
- Operations on a
Set - Native token (NIGHT) operations
- Privacy boundary in Midnight DApps
pragma language_version 0.23;
import CompactStandardLibrary;
export enum PartyState {
NOT_STARTED,
READY,
STARTED,
DOORS_CLOSED,
FEES_CLAIMED
}
export sealed ledger organizer: Bytes<32>;
export sealed ledger maxListSize: Uint<16>;
export sealed ledger entryFee: Uint<16>;
export ledger partyState: PartyState;
export ledger hashedPartyGoers: Set<Bytes<32>>;
export ledger checkedInParty: Set<UserAddress>;
witness localSecret(): Bytes<32>;
constructor (partySize: Uint<16>, fee: Uint<16>) {
const _secret = localSecret();
const pubKey = getDappPublicKey(_secret);
organizer = disclose(pubKey);
assert(partySize > 0, "The party size must be greater than zero");
assert(fee > 0, "Fee must be greater than zero");
entryFee = disclose(fee);
maxListSize = disclose(partySize);
partyState = PartyState.NOT_STARTED;
}
// called by party goers
export circuit rsvp(_address: UserAddress): [] {
const _secret = localSecret();
const pubKey = getDappPublicKey(_secret);
// caller authentication check
assert(pubKey != organizer, "Organizer cannot RSVP to the party");
// state verification check
assert(partyState == PartyState.NOT_STARTED, "The party has already started");
assert(hashedPartyGoers.size() < maxListSize, "The list is full");
// party goer address remains private
const commitHash = commitAddress(_secret, _address.bytes);
assert(!hashedPartyGoers.member(commitHash), "You are already on the list");
hashedPartyGoers.insert(commitHash);// doesn't need disclose bc persistentCommit
if (hashedPartyGoers.size() == maxListSize) {
// @TODO -- In the future, emit an event to the organizer here (MIP-0002)
partyState = PartyState.READY;
}
}
// start the party (organizer)
export circuit startParty(): [] {
const _secret = localSecret();
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only the organizer can start the party");
assert(partyState == PartyState.READY || partyState == PartyState.NOT_STARTED,
"The party is not in the correct state for this operation");
partyState = PartyState.STARTED;
}
export circuit closeEntry(): [] {
const _secret = localSecret();
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "Only organizer can close the doors");
assert(partyState == PartyState.STARTED, "Party in wrong state");
partyState = PartyState.DOORS_CLOSED;
}
// called by the party goer, so the payment can be prompted to the caller
// after the execution of this circuit, party goers are public
export circuit checkIn(address: UserAddress): [] {
// state verification checks
assert(partyState == PartyState.STARTED, "The party has not been started. Call the party police");
assert(checkedInParty.size() < hashedPartyGoers.size(), "All guests have already checked in");
const _secret = localSecret();
const commitHash = commitAddress(_secret, address.bytes);
// caller verification checks
assert(hashedPartyGoers.member(commitHash), "You are not on the list");
assert(!checkedInParty.member(disclose(address)), "You have already checked in");
// take in unshielded payment, party goers are now public
receiveUnshielded(nativeToken(), entryFee as Uint<128>);
checkedInParty.insert(disclose(address));
if(checkedInParty.size() == maxListSize) {
partyState = PartyState.DOORS_CLOSED;
}
}
export circuit claimFees(address: UserAddress): [] {
const _secret = localSecret();
const pubKey = getDappPublicKey(_secret);
assert(organizer == pubKey, "You are not the organizer");
// state verification checks
assert(partyState == PartyState.DOORS_CLOSED, "The doors are not yet closed");
assert(checkedInParty.size() > 0, "No fees to claim");
// calculate contract balance of NIGHT tokens
const totalCollected = checkedInParty.size() * entryFee;
assert(unshieldedBalanceGte(nativeToken(), totalCollected), "Contract balance wrong");
// send to organizer
sendUnshielded(
nativeToken(),
disclose(totalCollected) as Uint<128>,
right<ContractAddress, UserAddress>(disclose(address))
);
partyState = PartyState.FEES_CLAIMED;
}
circuit commitAddress(_secret: Bytes<32>, _address: Bytes<32>): Bytes<32> {
return persistentCommit<Bytes<32>>(_address, _secret);
}
// hash a publicKey specific to this DApp so that users cannot be tracked
// the _secret should be a highly complex one
circuit getDappPublicKey(_secret: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "private-party:pk:"), _secret]);
}