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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

gnotif.gno

7.85 Kb · 250 lines
  1// Package gnotif is a registry of notification triggers. Each trigger names
  2// a realm and an event type to watch, and the text of the notification that
  3// gnotif servers send when a matching event lands on chain. A later registry
  4// version follows the rules in
  5// https://gnotif.xyz/docs/how-it-works/#rules-for-a-new-registry-version.
  6package gnotif
  7
  8import (
  9	"chain"
 10	"chain/runtime"
 11	"chain/runtime/unsafe"
 12	"strings"
 13
 14	"gno.land/p/g13rqvks880v0hmlfepcj0kkudq0js38d3pc5k6a/gnotif/trigger/v0"
 15	"gno.land/p/nt/bptree/v0"
 16	"gno.land/p/nt/seqid/v0"
 17	"gno.land/r/sys/names"
 18)
 19
 20// Subpath is the subpath of a realm's gnotif identity: me := cur.Sub(gnotif.Subpath).
 21const Subpath = "gnotif"
 22
 23// Trigger is an alias, so a dapp imports only the registry.
 24type Trigger = trigger.Trigger
 25
 26// Handle changes the triggers of the realm that opened it. Each method also
 27// needs that realm's gnotif identity as its caller, so the pointer alone is
 28// useless to any other realm.
 29type Handle struct {
 30	target   string
 31	approver string // the namespace owner who signed the Open
 32}
 33
 34var (
 35	triggers bptree.BPTree // id → record.encode()
 36	byTarget bptree.BPTree // target → []string of its active ids, oldest first
 37	handles  bptree.BPTree // target → its live *Handle
 38	lastID   seqid.ID
 39)
 40
 41// Open returns the handle that changes the triggers of the realm whose gnotif
 42// identity calls it. It panics while the realm holds a handle its namespace
 43// owner has not revoked, and unless that owner signed the transaction and
 44// sent it straight to the realm.
 45func Open(cur realm) *Handle {
 46	me := cur.Previous()
 47	target := targetOf(0, me)
 48	if handles.Has(target) {
 49		panic(target + " already holds a handle: its namespace owner can Revoke it")
 50	}
 51	mustOwnerEntered(0, me, target)
 52	h := &Handle{target: target, approver: unsafe.OriginCaller().String()}
 53	handles.Set(target, h)
 54	return h
 55}
 56
 57// Set stores t under name and returns its id. A new name gets a new id. On a
 58// name in use, t must watch the same event, filter and param: the id stays,
 59// so users who turned the trigger on keep it.
 60func (h *Handle) Set(cur realm, name string, t Trigger) string {
 61	h.mustHold(cur.Previous().PkgPath())
 62	if !validName(name, 1, maxName) {
 63		panic("invalid name")
 64	}
 65	validate(t)
 66	r := record{Target: h.target, Name: name, T: t}
 67	ids := activeOf(h.target)
 68	if id, old, ok := find(ids, name); ok {
 69		if old.T == t {
 70			return id
 71		}
 72		if old.T.Event != t.Event || old.T.Filter != t.Filter || old.T.Param != t.Param {
 73			panic(name + " watches a different event, filter or param: Remove it, then Set it again")
 74		}
 75		store(id, r, h.approver)
 76		return id
 77	}
 78	if len(ids) >= maxPerTarget {
 79		panic("too many triggers for target")
 80	}
 81	id := "v1-" + lastID.Next().String()
 82	store(id, r, h.approver)
 83	byTarget.Set(h.target, append(ids, id))
 84	return id
 85}
 86
 87// Remove removes the trigger under name, and with it every user's choice to
 88// receive it.
 89func (h *Handle) Remove(cur realm, name string) {
 90	h.mustHold(cur.Previous().PkgPath())
 91	if !validName(name, 1, maxName) {
 92		panic("invalid name")
 93	}
 94	id, _, ok := find(activeOf(h.target), name)
 95	if !ok {
 96		panic(name + " is not a trigger of " + h.target)
 97	}
 98	removeIDs(h.target, id)
 99}
100
101// mustHold panics unless h is its realm's live handle and caller is that
102// realm's gnotif identity.
103func (h *Handle) mustHold(caller string) {
104	if handles.Get(h.target) != h {
105		panic("not the live handle of " + h.target)
106	}
107	if caller != h.target+"#"+Subpath {
108		panic("not the gnotif identity of " + h.target + ": " + caller)
109	}
110}
111
112// Drop removes target's listed active triggers, ids separated by commas.
113// Spaces around an id are ignored. Only target's namespace owner may call it,
114// directly, with their own key. A call that lists an id more than once, or one
115// that is not an active trigger of target, removes nothing. Drop leaves the
116// realm's handle live: Revoke stops the realm from setting them again.
117func Drop(cur realm, target, ids string) {
118	mustOwn(cur, target)
119	live := make(map[string]bool)
120	for _, id := range activeOf(target) {
121		live[id] = true
122	}
123	dropped := strings.Split(ids, ",")
124	for i, id := range dropped {
125		id = strings.TrimSpace(id)
126		dropped[i] = id
127		if !live[id] {
128			panic(id + " is not an active trigger of " + target)
129		}
130		live[id] = false
131	}
132	removeIDs(target, dropped...)
133}
134
135// Revoke ends target's handle: its methods panic from then on, and target's
136// triggers stay. The realm can Open a new handle. Only target's namespace
137// owner may call it, directly, with their own key.
138func Revoke(cur realm, target string) {
139	mustOwn(cur, target)
140	if _, removed := handles.Remove(target); !removed {
141		panic(target + " holds no handle")
142	}
143}
144
145// Active lists target's active trigger ids, comma-separated, oldest first.
146func Active(target string) string {
147	return strings.Join(activeOf(target), ",")
148}
149
150func activeOf(target string) []string {
151	ids, _ := byTarget.Get(target).([]string)
152	return ids
153}
154
155// ceiling: decodes up to maxPerTarget records per lookup; index target and
156// name to id before raising maxPerTarget.
157func find(ids []string, name string) (string, record, bool) {
158	for _, id := range ids {
159		if r := decodeRecord(triggers.Get(id)); r.Name == name {
160			return id, r, true
161		}
162	}
163	return "", record{}, false
164}
165
166// targetOf returns the realm whose gnotif identity me is, and panics for any
167// other caller.
168func targetOf(_ int, me realm) string {
169	path := me.PkgPath()
170	host, subpath, _ := chain.SplitPkgSubPath(path)
171	if subpath != Subpath || !validTarget(host) {
172		panic("call with this realm's gnotif identity (me := cur.Sub(gnotif.Subpath), then cross(me)); got " + path)
173	}
174	return host
175}
176
177// mustOwnerEntered panics unless target's namespace owner signed the
178// transaction with its own key and called target's realm directly.
179func mustOwnerEntered(_ int, me realm, target string) {
180	if _, _, _, isSession := runtime.GetSessionInfo(); isSession {
181		panic("signed with a session key: sign with the namespace owner's own key")
182	}
183	signer := unsafe.OriginCaller()
184	if ns := namespaceOf(target); !names.IsAuthorizedAddressForNamespace(signer, ns) {
185		panic(signer.String() + " is not authorized for namespace " + ns)
186	}
187	// The Previous of a gnotif identity is the caller of its host realm.
188	entry := me.Previous()
189	if !entry.IsUserCall() || entry.Address() != signer {
190		panic("the dapp was not entered directly by its namespace owner; entered from " + entry.PkgPath())
191	}
192}
193
194// mustOwn panics unless target's namespace owner called straight into the
195// registry with its own key.
196func mustOwn(cur realm, target string) {
197	caller := cur.Previous()
198	if !caller.IsUserCall() {
199		panic("only the namespace owner may do this, in a call signed with its own key; got " + caller.PkgPath())
200	}
201	if _, _, _, isSession := runtime.GetSessionInfo(); isSession {
202		panic("sign with the namespace owner's own key, not a session key")
203	}
204	if ns := namespaceOf(target); !names.IsAuthorizedAddressForNamespace(caller.Address(), ns) {
205		panic(caller.Address().String() + " is not authorized for namespace " + ns)
206	}
207}
208
209// store writes r under id and emits TriggerDeclared.
210func store(id string, r record, approver string) {
211	triggers.Set(id, r.encode())
212	chain.Emit("TriggerDeclared",
213		"id", id,
214		"target", r.Target,
215		"name", r.Name,
216		"event", r.T.Event,
217		"filter", r.T.Filter,
218		"param", r.T.Param,
219		"title", r.T.Title,
220		"body", r.T.Body,
221		"link", r.T.Link,
222		"declarer", chain.PackageAddress(r.Target).String(),
223		"verified", "true",
224		"approver", approver,
225	)
226}
227
228// removeIDs removes the listed active ids of target and emits TriggerRemoved
229// for each.
230func removeIDs(target string, ids ...string) {
231	gone := make(map[string]bool, len(ids))
232	for _, id := range ids {
233		gone[id] = true
234	}
235	var left []string
236	for _, id := range activeOf(target) {
237		if !gone[id] {
238			left = append(left, id)
239		}
240	}
241	if len(left) == 0 {
242		byTarget.Remove(target)
243	} else {
244		byTarget.Set(target, left)
245	}
246	for _, id := range ids {
247		triggers.Remove(id)
248		chain.Emit("TriggerRemoved", "id", id)
249	}
250}