|
| 1 | +access(all) contract TidalYieldClosedBeta { |
| 2 | + |
| 3 | + access(all) entitlement Admin |
| 4 | + access(all) entitlement Beta |
| 5 | + |
| 6 | + access(all) resource BetaBadge { |
| 7 | + access(all) let assignedTo: Address |
| 8 | + init(_ addr: Address) { |
| 9 | + self.assignedTo = addr |
| 10 | + } |
| 11 | + access(all) view fun getOwner(): Address { |
| 12 | + return self.assignedTo |
| 13 | + } |
| 14 | + } |
| 15 | + |
| 16 | + // --- Paths --- |
| 17 | + access(all) let UserBetaCapStoragePath: StoragePath |
| 18 | + access(all) let AdminHandleStoragePath: StoragePath |
| 19 | + |
| 20 | + // --- Registry: which capability was issued to which address, and revocation flags --- |
| 21 | + access(all) struct AccessInfo { |
| 22 | + access(all) let capID: UInt64 |
| 23 | + access(all) let isRevoked: Bool |
| 24 | + |
| 25 | + init(_ capID: UInt64, _ isRevoked: Bool) { |
| 26 | + self.capID = capID |
| 27 | + self.isRevoked = isRevoked |
| 28 | + } |
| 29 | + } |
| 30 | + access(all) var issuedCapIDs: {Address: AccessInfo} |
| 31 | + |
| 32 | + // --- Events --- |
| 33 | + access(all) event BetaGranted(addr: Address, capID: UInt64) |
| 34 | + access(all) event BetaRevoked(addr: Address, capID: UInt64?) |
| 35 | + |
| 36 | + /// Per-user badge storage path (under the *contract/deployer* account) |
| 37 | + access(contract) fun _badgePath(_ addr: Address): StoragePath { |
| 38 | + return StoragePath(identifier: "TY_BetaBadge_".concat(addr.toString()))! |
| 39 | + } |
| 40 | + |
| 41 | + /// Ensure the admin-owned badge exists for the user |
| 42 | + access(contract) fun _ensureBadge(_ addr: Address) { |
| 43 | + let p = self._badgePath(addr) |
| 44 | + if self.account.storage.type(at: p) == nil { |
| 45 | + self.account.storage.save(<-create BetaBadge(addr), to: p) |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + access(contract) fun _destroyBadge(_ addr: Address) { |
| 50 | + let p = self._badgePath(addr) |
| 51 | + if let badge <- self.account.storage.load<@BetaBadge>(from: p) { |
| 52 | + destroy badge |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + /// Issue a capability from the contract/deployer account and record its ID |
| 57 | + access(contract) fun _issueBadgeCap(_ addr: Address): Capability<auth(Beta) &BetaBadge> { |
| 58 | + let p = self._badgePath(addr) |
| 59 | + let cap: Capability<auth(Beta) &BetaBadge> = |
| 60 | + self.account.capabilities.storage.issue<auth(Beta) &BetaBadge>(p) |
| 61 | + |
| 62 | + self.issuedCapIDs[addr] = AccessInfo(cap.id, false) |
| 63 | + |
| 64 | + if let ctrl = self.account.capabilities.storage.getController(byCapabilityID: cap.id) { |
| 65 | + ctrl.setTag("tidalyield-beta") |
| 66 | + } |
| 67 | + |
| 68 | + emit BetaGranted(addr: addr, capID: cap.id) |
| 69 | + return cap |
| 70 | + } |
| 71 | + |
| 72 | + /// Delete the recorded controller, revoking *all copies* of the capability |
| 73 | + access(contract) fun _revokeByAddress(_ addr: Address) { |
| 74 | + let info = self.issuedCapIDs[addr] ?? panic("No cap recorded for address") |
| 75 | + let ctrl = self.account.capabilities.storage.getController(byCapabilityID: info.capID) |
| 76 | + ?? panic("Missing controller for recorded cap ID") |
| 77 | + ctrl.delete() |
| 78 | + self.issuedCapIDs[addr] = AccessInfo(info.capID, true) |
| 79 | + self._destroyBadge(addr) |
| 80 | + emit BetaRevoked(addr: addr, capID: info.capID) |
| 81 | + } |
| 82 | + |
| 83 | + // 2) A small in-account helper resource that performs privileged ops |
| 84 | + access(all) resource AdminHandle { |
| 85 | + access(Admin) fun grantBeta(addr: Address): Capability<auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge> { |
| 86 | + TidalYieldClosedBeta._ensureBadge(addr) |
| 87 | + return TidalYieldClosedBeta._issueBadgeCap(addr) |
| 88 | + } |
| 89 | + |
| 90 | + access(Admin) fun revokeByAddress(addr: Address) { |
| 91 | + TidalYieldClosedBeta._revokeByAddress(addr) |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + /// Read-only check used by any gated entrypoint |
| 96 | + access(all) view fun getBetaCapID(_ addr: Address): UInt64? { |
| 97 | + if let info = self.issuedCapIDs[addr] { |
| 98 | + if info.isRevoked { |
| 99 | + assert(info.isRevoked, message: "Beta access revoked") |
| 100 | + return nil |
| 101 | + } |
| 102 | + return info.capID |
| 103 | + } |
| 104 | + return nil |
| 105 | + } |
| 106 | + |
| 107 | + access(all) view fun validateBeta(_ addr: Address?, _ betaRef: auth(Beta) &BetaBadge): Bool { |
| 108 | + if (addr == nil) { |
| 109 | + assert(addr == nil, message: "Address is required for Beta verification") |
| 110 | + return false |
| 111 | + } |
| 112 | + let recordedID: UInt64? = self.getBetaCapID(addr!); |
| 113 | + if recordedID == nil { |
| 114 | + assert(recordedID == nil, message: "No Beta access") |
| 115 | + return false |
| 116 | + } |
| 117 | + |
| 118 | + if betaRef.getOwner() != addr { |
| 119 | + assert(betaRef.getOwner() != addr, message: "BetaBadge may only be used by its assigned owner") |
| 120 | + return false |
| 121 | + } |
| 122 | + |
| 123 | + return true |
| 124 | + } |
| 125 | + |
| 126 | + init() { |
| 127 | + self.AdminHandleStoragePath = StoragePath( |
| 128 | + identifier: "TidalYieldClosedBetaAdmin_\(self.account.address)" |
| 129 | + )! |
| 130 | + self.UserBetaCapStoragePath = StoragePath( |
| 131 | + identifier: "TidalYieldUserBetaCap_\(self.account.address)" |
| 132 | + )! |
| 133 | + |
| 134 | + self.issuedCapIDs = {} |
| 135 | + |
| 136 | + // Create and store the admin handle in *this* (deployer) account |
| 137 | + self.account.storage.save(<-create AdminHandle(), to: self.AdminHandleStoragePath) |
| 138 | + } |
| 139 | +} |
0 commit comments