Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

requests.gno

5.89 Kb · 213 lines
  1package memba_notes_v1
  2
  3import (
  4	"chain/runtime"
  5	"gno.land/p/samcrew/avl"
  6	keys "gno.land/r/samcrew/enckeys_v1"
  7	"strconv"
  8	"strings"
  9)
 10
 11const MaxAccessRequests = 256
 12const RequestTTLBlocks int64 = 10000
 13
 14type accessRequest struct {
 15	Who     address
 16	Expires int64
 17	Active  bool
 18	Status  string
 19	Last    receipt
 20}
 21
 22var requests = avl.NewTree()
 23
 24func requestKey(id [16]byte, who address) string { return idText(id) + ":" + string(who) }
 25func RequestAccess(cur realm, id [16]byte, opID [16]byte) {
 26	who := enter(cur, false)
 27	defer leave()
 28	n := live(id)
 29	if !cur.Previous().IsUserCall() || !encrypted(n.Mode) || who == n.Owner || contains(n.Readers, who) {
 30		panic("notes: request not applicable")
 31	}
 32	if opID == ([16]byte{}) {
 33		panic("notes: empty operation id")
 34	}
 35	h := runtime.ChainHeight()
 36	if h > int64(9223372036854775807)-RequestTTLBlocks {
 37		panic("notes: request height overflow")
 38	}
 39	key := requestKey(id, who)
 40	if v, ok := requests.Get(key); ok {
 41		r := v.(*accessRequest)
 42		if r.Last.Operation == opID {
 43			panic("notes: operation id reused")
 44		}
 45		if !r.Active && r.Status == "rejected" && r.Expires > h {
 46			panic("notes: request rejected recently")
 47		}
 48		if r.Active {
 49			if r.Expires > h {
 50				panic("notes: request pending")
 51			}
 52			n.RequestCount--
 53		}
 54	}
 55	if n.RequestCount >= MaxAccessRequests {
 56		panic("notes: request capacity; purge expired requests")
 57	}
 58	n.RequestCount++
 59	requests.Set(key, &accessRequest{who, h + RequestTTLBlocks, true, "pending", receipt{opID, who, h}})
 60}
 61func closeRequest(n *note, who address, status string, actor address, opID [16]byte) {
 62	v, ok := requests.Get(requestKey(n.ID, who))
 63	if !ok {
 64		panic("notes: request missing")
 65	}
 66	r := v.(*accessRequest)
 67	if !r.Active {
 68		panic("notes: request already closed")
 69	}
 70	if opID == ([16]byte{}) || opID == r.Last.Operation {
 71		panic("notes: invalid request operation")
 72	}
 73	h := runtime.ChainHeight()
 74	r.Active = false
 75	r.Status = status
 76	r.Last = receipt{opID, actor, h}
 77	n.RequestCount--
 78	// A rejection keeps the record until one full TTL after it, so neither the
 79	// requester nor PurgeExpiredRequests can end the re-request cooldown early.
 80	if status == "rejected" {
 81		if h > int64(9223372036854775807)-RequestTTLBlocks {
 82			panic("notes: request height overflow")
 83		}
 84		r.Expires = h + RequestTTLBlocks
 85	}
 86}
 87func CancelAccessRequest(cur realm, id [16]byte, opID [16]byte) {
 88	who := enter(cur, false)
 89	defer leave()
 90	n := find(id)
 91	closeRequest(n, who, "cancelled", who, opID)
 92}
 93func RejectAccessRequest(cur realm, id [16]byte, to address, opID [16]byte) {
 94	who := enter(cur, false)
 95	defer leave()
 96	mutateRejectAccessRequest(cur, who, id, to, opID)
 97}
 98func mutateRejectAccessRequest(cur realm, who address, id [16]byte, to address, opID [16]byte) {
 99	n := live(id)
100	owner(n, who)
101	closeRequest(n, keys.CanonicalAddress(to), "rejected", who, opID)
102}
103func acknowledgeRequests(n *note, actor address, opID [16]byte) {
104	for _, who := range n.Readers {
105		if v, ok := requests.Get(requestKey(n.ID, who)); ok {
106			r := v.(*accessRequest)
107			if r.Active {
108				r.Active = false
109				r.Status = "granted"
110				r.Last = receipt{opID, actor, runtime.ChainHeight()}
111				n.RequestCount--
112			}
113		}
114	}
115}
116func requestJSON(r *accessRequest) string {
117	return `{"address":` + q(string(r.Who)) + `,"expires_height":` + q(strconv.FormatInt(r.Expires, 10)) + `,"active":` + strconv.FormatBool(r.Active) + `,"status":` + q(r.Status) + `,"op_id":` + q(idText(r.Last.Operation)) + `,"actor":` + q(string(r.Last.Actor)) + `,"height":` + q(strconv.FormatInt(r.Last.Height, 10)) + `}`
118}
119func AccessRequestJSON(idHex string, who address) string {
120	v, ok := requests.Get(requestKey(readID(idHex), keys.CanonicalAddress(who)))
121	if !ok {
122		return "null"
123	}
124	return requestJSON(v.(*accessRequest))
125}
126func requestStart(id [16]byte, cursor string) string {
127	if cursor != "" && string(keys.CanonicalAddress(address(cursor))) != cursor {
128		panic("notes: bad request cursor")
129	}
130	return idText(id) + ":" + cursor
131}
132func AccessRequestsJSON(idHex, cursor string, limit int) string {
133	id := readID(idHex)
134	n := find(id)
135	if limit < 1 || limit > MaxPage {
136		panic("notes: page limit")
137	}
138	start := requestStart(id, cursor)
139	prefix := idText(id) + ":"
140	items := []string{}
141	next := ""
142	requests.Iterate(start, prefix+"\xff", func(k string, v interface{}) bool {
143		if cursor != "" && k == start {
144			return false
145		}
146		items = append(items, requestJSON(v.(*accessRequest)))
147		next = strings.TrimPrefix(k, prefix)
148		return len(items) == limit
149	})
150	if len(items) < limit {
151		next = ""
152	}
153	return `{"items":[` + strings.Join(items, ",") + `],"next_cursor":` + q(next) + `,"active_count":` + u(uint64(n.RequestCount)) + `}`
154}
155
156// At most limit records are inspected, not just removed. The returned cursor
157// lets a caller sweep the full request set without an unbounded scan.
158func PurgeExpiredRequests(cur realm, id [16]byte, cursor string, limit int) string {
159	enter(cur, false)
160	defer leave()
161	n := find(id)
162	if limit < 1 || limit > MaxPage {
163		panic("notes: purge limit")
164	}
165	start := requestStart(id, cursor)
166	prefix := idText(id) + ":"
167	doomed := []string{}
168	next := ""
169	inspected := 0
170	h := runtime.ChainHeight()
171	requests.Iterate(start, prefix+"\xff", func(k string, v interface{}) bool {
172		if cursor != "" && k == start {
173			return false
174		}
175		r := v.(*accessRequest)
176		if r.Expires <= h {
177			doomed = append(doomed, k)
178			if r.Active {
179				n.RequestCount--
180			}
181		}
182		inspected++
183		next = strings.TrimPrefix(k, prefix)
184		return inspected == limit
185	})
186	for _, key := range doomed {
187		requests.Remove(key)
188	}
189	if inspected < limit {
190		next = ""
191	}
192	return next
193}
194
195func purgeRequests(n *note, limit int) int {
196	if limit == 0 {
197		return 0
198	}
199	prefix := idText(n.ID) + ":"
200	doomed := []string{}
201	requests.Iterate(prefix, prefix+"\xff", func(k string, v interface{}) bool {
202		r := v.(*accessRequest)
203		if r.Active {
204			n.RequestCount--
205		}
206		doomed = append(doomed, k)
207		return len(doomed) == limit
208	})
209	for _, k := range doomed {
210		requests.Remove(k)
211	}
212	return len(doomed)
213}