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}