Search Apps Documentation Source Content File Folder Download Copy Actions Download

instance.gno

5.72 Kb · 239 lines
  1package gnorkle
  2
  3import (
  4	"chain/runtime"
  5	"errors"
  6	"strings"
  7
  8	"gno.land/p/demo/gnorkle/agent"
  9	"gno.land/p/demo/gnorkle/feed"
 10	"gno.land/p/demo/gnorkle/message"
 11	"gno.land/p/nt/avl/v0"
 12)
 13
 14// Instance is a single instance of an oracle.
 15type Instance struct {
 16	feeds     *avl.Tree
 17	whitelist agent.Whitelist
 18}
 19
 20// NewInstance creates a new instance of an oracle.
 21func NewInstance() *Instance {
 22	return &Instance{
 23		feeds: avl.NewTree(),
 24	}
 25}
 26
 27func assertValidID(id string) error {
 28	if len(id) == 0 {
 29		return errors.New("feed ids cannot be empty")
 30	}
 31
 32	if strings.Contains(id, ",") {
 33		return errors.New("feed ids cannot contain commas")
 34	}
 35
 36	return nil
 37}
 38
 39func (i *Instance) assertFeedDoesNotExist(id string) error {
 40	if i.feeds.Has(id) {
 41		return errors.New("feed already exists")
 42	}
 43
 44	return nil
 45}
 46
 47// AddFeeds adds feeds to the instance with empty whitelists.
 48func (i *Instance) AddFeeds(feeds ...Feed) error {
 49	for _, feed := range feeds {
 50		if err := assertValidID(feed.ID()); err != nil {
 51			return err
 52		}
 53
 54		if err := i.assertFeedDoesNotExist(feed.ID()); err != nil {
 55			return err
 56		}
 57
 58		i.feeds.Set(
 59			feed.ID(),
 60			FeedWithWhitelist{
 61				Whitelist: new(agent.Whitelist),
 62				Feed:      feed,
 63			},
 64		)
 65	}
 66
 67	return nil
 68}
 69
 70// AddFeedsWithWhitelists adds feeds to the instance with the given whitelists.
 71func (i *Instance) AddFeedsWithWhitelists(feeds ...FeedWithWhitelist) error {
 72	for _, feed := range feeds {
 73		if err := i.assertFeedDoesNotExist(feed.ID()); err != nil {
 74			return err
 75		}
 76		if err := assertValidID(feed.ID()); err != nil {
 77			return err
 78		}
 79
 80		i.feeds.Set(
 81			feed.ID(),
 82			FeedWithWhitelist{
 83				Whitelist: feed.Whitelist,
 84				Feed:      feed,
 85			},
 86		)
 87	}
 88
 89	return nil
 90}
 91
 92// RemoveFeed removes a feed from the instance.
 93func (i *Instance) RemoveFeed(id string) {
 94	i.feeds.Remove(id)
 95}
 96
 97// PostMessageHandler is a type that allows for post-processing of feed state after a feed
 98// ingests a message from an agent.
 99type PostMessageHandler interface {
100	Handle(i *Instance, funcType message.FuncType, feed Feed) error
101}
102
103// HandleMessage handles a message from an agent and routes to either the logic that returns
104// feed definitions or the logic that allows a feed to ingest a message.
105//
106// TODO: Consider further message types that could allow administrative action such as modifying
107// a feed's whitelist without the owner of this oracle having to maintain a reference to it.
108func (i *Instance) HandleMessage(msg string, postHandler PostMessageHandler) (string, error) {
109	caller := string(runtime.OriginCaller())
110
111	funcType, msg := message.ParseFunc(msg)
112
113	switch funcType {
114	case message.FuncTypeRequest:
115		return i.GetFeedDefinitions(caller)
116
117	default:
118		id, msg := message.ParseID(msg)
119		if err := assertValidID(id); err != nil {
120			return "", err
121		}
122
123		feedWithWhitelist, err := i.getFeedWithWhitelist(id)
124		if err != nil {
125			return "", err
126		}
127
128		if !addressIsWhitelisted(&i.whitelist, feedWithWhitelist, caller, nil) {
129			return "", errors.New("caller not whitelisted")
130		}
131
132		if err := feedWithWhitelist.Ingest(funcType, msg, caller); err != nil {
133			return "", err
134		}
135
136		if postHandler != nil {
137			postHandler.Handle(i, funcType, feedWithWhitelist)
138		}
139	}
140
141	return "", nil
142}
143
144func (i *Instance) getFeed(id string) (Feed, error) {
145	untypedFeed, ok := i.feeds.Get(id)
146	if !ok {
147		return nil, errors.New("invalid ingest id: " + id)
148	}
149
150	feed, ok := untypedFeed.(Feed)
151	if !ok {
152		return nil, errors.New("invalid feed type")
153	}
154
155	return feed, nil
156}
157
158func (i *Instance) getFeedWithWhitelist(id string) (FeedWithWhitelist, error) {
159	untypedFeedWithWhitelist, ok := i.feeds.Get(id)
160	if !ok {
161		return FeedWithWhitelist{}, errors.New("invalid ingest id: " + id)
162	}
163
164	feedWithWhitelist, ok := untypedFeedWithWhitelist.(FeedWithWhitelist)
165	if !ok {
166		return FeedWithWhitelist{}, errors.New("invalid feed with whitelist type")
167	}
168
169	return feedWithWhitelist, nil
170}
171
172// GetFeedValue returns the most recently published value of a feed along with a string
173// representation of the value's type and boolean indicating whether the value is
174// okay for consumption.
175func (i *Instance) GetFeedValue(id string) (feed.Value, string, bool, error) {
176	foundFeed, err := i.getFeed(id)
177	if err != nil {
178		return feed.Value{}, "", false, err
179	}
180
181	value, valueType, consumable := foundFeed.Value()
182	return value, valueType, consumable, nil
183}
184
185// GetFeedDefinitions returns a JSON string representing the feed definitions for which the given
186// agent address is whitelisted to provide values for ingestion.
187func (i *Instance) GetFeedDefinitions(forAddress string) (string, error) {
188	instanceHasAddressWhitelisted := !i.whitelist.HasDefinition() || i.whitelist.HasAddress(forAddress)
189
190	buf := new(strings.Builder)
191	buf.WriteString("[")
192	first := true
193	var err error
194
195	// The boolean value returned by this callback function indicates whether to stop iterating.
196	i.feeds.Iterate("", "", func(_ string, value any) bool {
197		feedWithWhitelist, ok := value.(FeedWithWhitelist)
198		if !ok {
199			err = errors.New("invalid feed type")
200			return true
201		}
202
203		// Don't give agents the ability to try to publish to inactive feeds.
204		if !feedWithWhitelist.IsActive() {
205			return false
206		}
207
208		// Skip feeds the address is not whitelisted for.
209		if !addressIsWhitelisted(&i.whitelist, feedWithWhitelist, forAddress, &instanceHasAddressWhitelisted) {
210			return false
211		}
212
213		var taskBytes []byte
214		if taskBytes, err = feedWithWhitelist.Feed.MarshalJSON(); err != nil {
215			return true
216		}
217
218		// Guard against any tasks that shouldn't be returned; maybe they are not active because they have
219		// already been completed.
220		if len(taskBytes) == 0 {
221			return false
222		}
223
224		if !first {
225			buf.WriteString(",")
226		}
227
228		first = false
229		buf.Write(taskBytes)
230		return false
231	})
232
233	if err != nil {
234		return "", err
235	}
236
237	buf.WriteString("]")
238	return buf.String(), nil
239}