Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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_rc1
  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}