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

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