A very experimental PLC implementation which uses BFT consensus for decentralization
1package abciapp
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "net/url"
9 "time"
10
11 abcitypes "github.com/cometbft/cometbft/abci/types"
12 "github.com/palantir/stacktrace"
13 "github.com/ucarion/urlpath"
14 "tangled.org/gbl08ma.com/didplcbft/plc"
15 "tangled.org/gbl08ma.com/didplcbft/transaction"
16)
17
18// Info implements [types.Application].
19func (d *DIDPLCApplication) Info(ctx context.Context, req *abcitypes.RequestInfo) (*abcitypes.ResponseInfo, error) {
20 return &abcitypes.ResponseInfo{
21 Data: "", // TODO some status report as JSON? Information about CometBFT itself can be obtained from RequestInfo
22 Version: "0.1.1",
23 AppVersion: 0, // TODO, included in the header of every block. move these values to constants
24 LastBlockHeight: d.tree.Version(),
25 LastBlockAppHash: d.tree.Hash(),
26 }, nil
27}
28
29// Query implements [types.Application].
30func (d *DIDPLCApplication) Query(ctx context.Context, req *abcitypes.RequestQuery) (*abcitypes.ResponseQuery, error) {
31 url, err := url.ParseRequestURI(req.Path)
32 if err != nil || url.Host != "" || url.Scheme != "" {
33 return &abcitypes.ResponseQuery{
34 Code: 6000,
35 Info: "Invalid path",
36 }, nil
37 }
38
39 var readTx transaction.Read
40 height := d.tree.Version()
41 if req.Height != 0 {
42 height = req.Height
43 }
44
45 readTx, err = d.txFactory.ReadHeight(time.Now(), height)
46 if err != nil {
47 return &abcitypes.ResponseQuery{
48 Code: 6001,
49 Info: "Unavailable height",
50 }, nil
51 }
52
53 handlers := []struct {
54 matcher urlpath.Path
55 handler func(match urlpath.Match) (*abcitypes.ResponseQuery, error)
56 }{
57 {
58 matcher: urlpath.New("/plc/:did"),
59 handler: func(match urlpath.Match) (*abcitypes.ResponseQuery, error) {
60 did := match.Params["did"]
61 doc, err := d.plc.Resolve(ctx, readTx, did)
62 if err != nil {
63 switch {
64 case errors.Is(err, plc.ErrDIDNotFound):
65 return &abcitypes.ResponseQuery{
66 Key: []byte(did),
67 Code: http.StatusNotFound,
68 Info: "DID not registered: " + did,
69 }, nil
70 case errors.Is(err, plc.ErrDIDGone):
71 return &abcitypes.ResponseQuery{
72 Key: []byte(did),
73 Code: http.StatusGone,
74 Info: "DID not available: " + did,
75 }, nil
76 default:
77 return nil, stacktrace.Propagate(err, "")
78 }
79 }
80
81 docJSON, err := json.Marshal(doc)
82 if err != nil {
83 return nil, stacktrace.Propagate(err, "")
84 }
85
86 return &abcitypes.ResponseQuery{
87 Key: []byte(did),
88 Value: []byte(docJSON),
89 Code: 0,
90 }, nil
91 },
92 },
93 {
94 matcher: urlpath.New("/plc/:did/log"),
95 handler: func(match urlpath.Match) (*abcitypes.ResponseQuery, error) {
96 return &abcitypes.ResponseQuery{
97 Code: 1,
98 Info: "Not implemented",
99 }, nil
100 },
101 },
102 {
103 matcher: urlpath.New("/plc/:did/log/audit"),
104 handler: func(match urlpath.Match) (*abcitypes.ResponseQuery, error) {
105 return &abcitypes.ResponseQuery{
106 Code: 1,
107 Info: "Not implemented",
108 }, nil
109 },
110 },
111 {
112 matcher: urlpath.New("/plc/:did/log/last"),
113 handler: func(match urlpath.Match) (*abcitypes.ResponseQuery, error) {
114 return &abcitypes.ResponseQuery{
115 Code: 1,
116 Info: "Not implemented",
117 }, nil
118 },
119 },
120 {
121 matcher: urlpath.New("/plc/:did/data"),
122 handler: func(match urlpath.Match) (*abcitypes.ResponseQuery, error) {
123 did := match.Params["did"]
124 data, err := d.plc.Data(ctx, readTx, did)
125 if err != nil {
126 switch {
127 case errors.Is(err, plc.ErrDIDNotFound):
128 return &abcitypes.ResponseQuery{
129 Key: []byte(did),
130 Code: http.StatusNotFound,
131 Info: "DID not registered: " + did,
132 }, nil
133 case errors.Is(err, plc.ErrDIDGone):
134 return &abcitypes.ResponseQuery{
135 Key: []byte(did),
136 Code: http.StatusGone,
137 Info: "DID not available: " + did,
138 }, nil
139 default:
140 return nil, stacktrace.Propagate(err, "")
141 }
142 }
143
144 dataJSON, err := json.Marshal(&data)
145 if err != nil {
146 return nil, stacktrace.Propagate(err, "")
147 }
148
149 return &abcitypes.ResponseQuery{
150 Key: []byte(did),
151 Value: []byte(dataJSON),
152 Code: 0,
153 }, nil
154 },
155 },
156 }
157
158 for _, h := range handlers {
159 if match, ok := h.matcher.Match(url.Path); ok {
160 return h.handler(match)
161 }
162 }
163
164 return &abcitypes.ResponseQuery{
165 Code: 6000,
166 Info: "Invalid path",
167 }, nil
168
169}