├── .github └── build.yaml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── docgraph ├── content_group.go ├── content_item.go ├── docgraph_test.go ├── document.go ├── document_graph.go ├── document_test.go ├── edge.go ├── edge_test.go ├── environment_test.go ├── examples │ ├── compliance-tags.json │ ├── contribution.json │ ├── each-type.json │ └── simplest.json ├── go.mod ├── go.sum ├── graph.go ├── graph_test.go ├── helpers_test.go ├── nodeos.sh └── types_test.go ├── include ├── docs.hpp ├── document_graph │ ├── content.hpp │ ├── content_wrapper.hpp │ ├── document.hpp │ ├── document_graph.hpp │ ├── edge.hpp │ └── util.hpp └── logger │ ├── boost_current_function.hpp │ └── logger.hpp ├── ricardian └── docs.contracts.md └── src ├── CMakeLists.txt ├── docs.cpp ├── document_graph ├── content.cpp ├── content_wrapper.cpp ├── document.cpp ├── document_graph.cpp ├── edge.cpp └── util.cpp └── logger └── logger.cpp /.github/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: "ubuntu-latest" 6 | steps: 7 | - uses: "actions/checkout@master" 8 | with: 9 | submodules: recursive 10 | - name: "Build" 11 | uses: docker://josejulio/hypha-builder:release_1.7.x 12 | with: 13 | args: " -j2 VERBOSE=1 " 14 | env: 15 | CODE: "/github/workspace/" 16 | PATCH_WASM_LD: "1" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.vscode/ 2 | build 3 | **/CMakeFiles 4 | **/cmake_install.cmake 5 | **/Makefile 6 | **/CMakeCache.txt 7 | document_project-prefix 8 | **/*.wasm 9 | **/.nyc* 10 | **/.nyc_output/** 11 | **.DS_Store 12 | .DS_Store 13 | eosc-vault.json 14 | cleos-wallet 15 | **/*node_modules 16 | yarn.lock 17 | package-lock.json 18 | test.key 19 | test.pub 20 | junk.json 21 | docgraph/test_results 22 | nodeos-go.log -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.17) 2 | project (docs) 3 | include(ExternalProject) 4 | 5 | # if no cdt root is given use default path 6 | if(EOSIO_CDT_ROOT STREQUAL "" OR NOT EOSIO_CDT_ROOT) 7 | find_package(eosio.cdt) 8 | endif() 9 | 10 | ExternalProject_Add( 11 | document_project 12 | SOURCE_DIR ${CMAKE_SOURCE_DIR}/src 13 | BINARY_DIR ${CMAKE_BINARY_DIR}/docs 14 | CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=${EOSIO_CDT_ROOT}/lib/cmake/eosio.cdt/EosioWasmToolchain.cmake 15 | UPDATE_COMMAND "" 16 | PATCH_COMMAND "" 17 | TEST_COMMAND "" 18 | INSTALL_COMMAND "" 19 | BUILD_ALWAYS 1 20 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 3 | 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Document Graph data structure 2 | 3 | ## Document 4 | 5 | Each document is comprised of: 6 | 7 | - Header 8 | - creator (account) 9 | - contract (where this is saved) 10 | - created date (timepoint) 11 | - hash of content (not including certificates or header) 12 | - Content 13 | - FlexValue = ```std::variant ``` 14 | - Content = an optionally labeled FlexValue 15 | - ContentGroup = vector 16 | - ContentGroups = vector 17 | - there is a single instance of ContentGroups per document 18 | - this provides enough flexibility to support: 19 | - data of all EOSIO types, 20 | - short clauses of annotated text, 21 | - longer form sequenced data, e.g. chapters 22 | - Certificates 23 | - each document has a list of certificates 24 | - Certificate 25 | - certifier: the 'signer' 26 | - notes: string data provided by signer 27 | - certification_date: time_point 28 | 29 | The simplest example: 30 | ``` 31 | { 32 | "id":4965, 33 | "hash":"50be6cf143050a11e9db3a52ef68e10e07b07cf6cc68007ad46a14baf307c5b9", 34 | "creator":"mem2.hypha", 35 | "content_groups":[ 36 | [ 37 | { 38 | "label":"simplest_label", 39 | "value":[ 40 | "string", 41 | "Simplest" 42 | ] 43 | } 44 | ] 45 | ], 46 | "certificates":[], 47 | "created_date":"2021-01-12T18:21:10.000", 48 | "contract":"dao.hypha" 49 | } 50 | ``` 51 | 52 | The "value" in each content item is a two element array, where the first item is the type and the second item is the data value. The supported values are string, int64, asset, name, time_point, or checksum256. 53 | 54 | This contract uses [content addressing](https://flyingzumwalt.gitbooks.io/decentralized-web-primer/content/avenues-for-access/lessons/power-of-content-addressing.html), meaning the unique identifier of each document is a hash of its contents. Each hash must be unique in the table and this is enforced by the actions. 55 | 56 | # Graph structure 57 | Documents can be linked together with labeled, directional edges to create a graph. For example, one document may be a "member" (vertex) that has an edge (link) to another document for a "role". 58 | 59 | ![image](https://user-images.githubusercontent.com/32852271/90341301-73f54e00-dfcc-11ea-8022-587beaf8fedd.png) 60 | 61 | Certificates are signed notes on documents by any account. Each certificate contains the account, timestamp, and an optional note. 62 | 63 | # Usage 64 | This repo is meant to be used as a library in other smart contracts. It also includes a sample smart contract, a Go package/smart contract test package, and example cleos commands. It also has a nodejs script that does quite a bit but has not been well maintained. 65 | 66 | ## Local Testing 67 | - Install Go (https://golang.org/dl/) 68 | - Install eosio & eosio.cdt 69 | 70 | ``` 71 | git clone https://github.com/hypha-dao/document-graph 72 | cd document-graph 73 | mkdir build 74 | cd build 75 | cmake .. 76 | make -j8 77 | cd ../docgraph 78 | go test -v -timeout 0 79 | ``` 80 | 81 | ## cleos Quickstart 82 | NOTE: Assumes you have relevant environmnent setup.. 83 | ``` bash 84 | # use your key 85 | KEY=EOS696y3uuryxgRRCiajXHBtiX9umXKvhBRGMygPa82HtQDrcDnE6 86 | cleos create account eosio documents $KEY $KEY 87 | cleos create account eosio bob $KEY $KEY 88 | cleos create account eosio alice $KEY $KEY 89 | cleos set contract documents docs 90 | ``` 91 | 92 | You'll need to add the eosio.code permission (use your key) 93 | ``` bash 94 | cleos push action eosio updateauth '{ 95 | "account": "documents", 96 | "permission": "active", 97 | "parent": "owner", 98 | "auth": { 99 | "keys": [ 100 | { 101 | "key": "EOS696y3uuryxgRRCiajXHBtiX9umXKvhBRGMygPa82HtQDrcDnE6", 102 | "weight": 1 103 | } 104 | ], 105 | "threshold": 1, 106 | "accounts": [ 107 | { 108 | "permission": { 109 | "actor": "documents", 110 | "permission": "eosio.code" 111 | }, 112 | "weight": 1 113 | } 114 | ], 115 | "waits": [] 116 | } 117 | }' -p documents@owner 118 | ``` 119 | 120 | ``` bash 121 | # this content just illustrates the various types supported 122 | cleos push action documents create '{ 123 | "creator": "bob", 124 | "content_groups": [ 125 | [ 126 | { 127 | "label": "content_group_name", 128 | "value": [ 129 | "string", 130 | "My Content Group #1" 131 | ] 132 | }, 133 | { 134 | "label": "salary_amount", 135 | "value": [ 136 | "asset", 137 | "130.00 USD" 138 | ] 139 | }, 140 | { 141 | "label": "referrer", 142 | "value": [ 143 | "name", 144 | "friendacct" 145 | ] 146 | }, 147 | { 148 | "label": "vote_count", 149 | "value": [ 150 | "int64", 151 | 67 152 | ] 153 | } 154 | ] 155 | ] 156 | }' -p bob 157 | ``` 158 | 159 | Alice can fork the object. The content must be new or updated or else the action will fail and report back the hash. 160 | Only updated fields and the hash to the parent will be saved within a fork. 161 | ``` bash 162 | cleos push action documents fork '{ 163 | "hash": "", 164 | "creator": "alice", 165 | "content": [ 166 | { 167 | "key": "salary_amount", 168 | "value": [[ 169 | "asset", 170 | "150.00 USD" 171 | ]] 172 | } 173 | ] 174 | }' -p alice 175 | ``` 176 | 177 | 178 | Any account can 'certify' a document, with notes. 179 | ``` bash 180 | cleos push action documents certify '{ 181 | "certifier": "documents", 182 | "hash": "b0477c431b96fa65273cb8a5f60ffb1fd11a42cb05d6e19cf2d66300ad52b8c9", 183 | "notes": "my certification notes" 184 | }' -p documents 185 | ``` 186 | 187 | 188 | ## Javascript Quickstart - DEPRECATED 189 | Some of this will still work, but it's been replaced with the Go libraries and [daoctl](hypha-dao/daoctl). 190 | ``` bash 191 | git clone git@github.com:hypha-dao/document.git 192 | cd js && yarn install && node index.js 193 | ``` 194 | 195 | #### Create a document from a file 196 | ``` bash 197 | $ node index.js --file "examples/each-type.json" --create --auth alice 198 | Transaction Successfull : 7dc613a7c716897f498c95e5973333db5e6a9f5170f604cdcde1b4bb546bdef6 199 | Documents table: [ 200 | { 201 | id: 0, 202 | hash: 'b0477c431b96fa65273cb8a5f60ffb1fd11a42cb05d6e19cf2d66300ad52b8c9', 203 | creator: 'alice', 204 | content: [ [Object], [Object], [Object], [Object], [Object], [Object] ], 205 | certificates: [], 206 | created_date: '2020-08-15T22:39:40.500', 207 | updated_date: '2020-08-15T22:39:40.500' 208 | } 209 | ] 210 | ``` 211 | NOTE: if you tried to recreate the same content a second time, it would fail to enforce in strict deduplication. This is similar to IPFS/IPLD specifications. There are more sample documents in the examples folder. 212 | 213 | #### List documents 214 | ``` 215 | node index.js 216 | ``` 217 | NOTE: use ```--json``` to show the entire document 218 | 219 | #### Certify an existing document 220 | ``` 221 | node index.js --certify 526bbe0d21db98c692559db22a2a32fedbea378ca25a4822d52e1171941401b7 --auth bob 222 | ``` 223 | Certificates are stored in the same table as the content, but it is separate from the hashed content. 224 | 225 | #### Add an edge 226 | Creates a graph edge from a document to another document. 227 | ``` 228 | node js/index.js --link --from e91c036d9f90a9f2dc7ab9767ea4aa19c384431a24e45cf109b4fded0608ec99 --to c0b0e48a9cd1b73ac924cf58a430abd5d3091ca7cbcda6caf5b7e7cebb379327 --edge edger --contract documents --host http://localhost:8888 --auth alice 229 | ``` 230 | 231 | #### Remove Edges 232 | Edges can be removed using any of these options: 233 | 1) one at a time (combination of from, to, and edge name), 234 | 2) all edges for a specific from and to nodes, or 235 | 3) all edges for a specific from node and edge name. 236 | 237 | ### Document fingerprint 238 | The document fingerprinting algorithm creates a data structure like this to hash. 239 | ``` 240 | [ 241 | [children=[ 242 | [checksum256,7b5755ce318c42fc750a754b4734282d1fad08e52c0de04762cb5f159a253c24], 243 | [checksum256,2f5f8a7c18567440b244bcc07ba7bb88cea80ddb3b4cbcb75afe6e15dd9ea33b] 244 | ], 245 | [description=[ 246 | [string,loreum ipsum goes to the store, could also include markdown] 247 | ], 248 | [milestones=[ 249 | [time_point,1597507314], 250 | [time_point,1597852914] 251 | ], 252 | [referrer=[ 253 | [name,friendacct], 254 | [int64,67] 255 | ], 256 | [salary_amount=[ 257 | [asset,130.00 USD] 258 | ], 259 | [vote_count=[ 260 | [int64,69] 261 | ] 262 | ] 263 | ``` 264 | -------------------------------------------------------------------------------- /docgraph/content_group.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | // ContentGroup ... 4 | type ContentGroup []ContentItem 5 | 6 | // GetContent returns a FlexValue of the content with the matching label 7 | // or an instance of ContentNotFoundError 8 | func (cg *ContentGroup) GetContent(label string) (*FlexValue, error) { 9 | for _, content := range *cg { 10 | if content.Label == label { 11 | return content.Value, nil 12 | } 13 | } 14 | 15 | return nil, &ContentNotFoundError{ 16 | Label: label, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docgraph/content_item.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | eos "github.com/eoscanada/eos-go" 9 | ) 10 | 11 | // ContentItem ... 12 | type ContentItem struct { 13 | Label string `json:"label"` 14 | Value *FlexValue `json:"value"` 15 | } 16 | 17 | // IsEqual evalutes if the label, value type and value impl are the same 18 | func (c *ContentItem) IsEqual(c2 ContentItem) bool { 19 | if strings.Compare(c.Label, c2.Label) != 0 { 20 | log.Println("ContentItem labels inequal: ", c.Label, " vs ", c2.Label) 21 | return false 22 | } 23 | 24 | if !c.Value.IsEqual(c2.Value) { 25 | log.Println("ContentItems inequal: ", c.Value, " vs ", c2.Value) 26 | return false 27 | } 28 | 29 | return true 30 | } 31 | 32 | // InvalidTypeError is used the type of a FlexValue doesn't match expectations 33 | type InvalidTypeError struct { 34 | Label string 35 | ExpectedType string 36 | FlexValue *FlexValue 37 | } 38 | 39 | func (c *InvalidTypeError) Error() string { 40 | return fmt.Sprintf("received an unexpected type %T for metadata variant %T", c.ExpectedType, c.FlexValue) 41 | } 42 | 43 | // FlexValueVariant may hold a name, int64, asset, string, or time_point 44 | var FlexValueVariant = eos.NewVariantDefinition([]eos.VariantType{ 45 | {Name: "monostate", Type: int64(0)}, 46 | {Name: "name", Type: eos.Name("")}, 47 | {Name: "string", Type: ""}, 48 | {Name: "asset", Type: eos.Asset{}}, //(*eos.Asset)(nil)}, // Syntax for pointer to a type, could be any struct 49 | {Name: "time_point", Type: eos.TimePoint(0)}, 50 | {Name: "int64", Type: int64(0)}, 51 | {Name: "checksum256", Type: eos.Checksum256([]byte("0"))}, 52 | }) 53 | 54 | // GetVariants returns the definition of types compatible with FlexValue 55 | func GetVariants() *eos.VariantDefinition { 56 | return FlexValueVariant 57 | } 58 | 59 | // FlexValue may hold any of the common EOSIO types 60 | // name, int64, asset, string, time_point, or checksum256 61 | type FlexValue struct { 62 | eos.BaseVariant 63 | } 64 | 65 | func (fv *FlexValue) String() string { 66 | switch v := fv.Impl.(type) { 67 | case eos.Name: 68 | return string(v) 69 | case int64: 70 | return fmt.Sprint(v) 71 | case eos.Asset: 72 | return v.String() 73 | case string: 74 | return v 75 | case eos.TimePoint: 76 | return v.String() 77 | case eos.Checksum256: 78 | return v.String() 79 | default: 80 | return fmt.Sprintf("received an unexpected type %T for variant %T", v, fv) 81 | } 82 | } 83 | 84 | // TimePoint returns a eos.TimePoint value of found content 85 | func (fv *FlexValue) TimePoint() (eos.TimePoint, error) { 86 | switch v := fv.Impl.(type) { 87 | case eos.TimePoint: 88 | return v, nil 89 | default: 90 | return 0, &InvalidTypeError{ 91 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv), 92 | ExpectedType: "eos.TimePoint", 93 | FlexValue: fv, 94 | } 95 | } 96 | } 97 | 98 | // Asset returns a string value of found content or it panics 99 | func (fv *FlexValue) Asset() (eos.Asset, error) { 100 | switch v := fv.Impl.(type) { 101 | case eos.Asset: 102 | return v, nil 103 | default: 104 | return eos.Asset{}, &InvalidTypeError{ 105 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv), 106 | ExpectedType: "eos.Asset", 107 | FlexValue: fv, 108 | } 109 | } 110 | } 111 | 112 | // Name returns a string value of found content or it panics 113 | func (fv *FlexValue) Name() (eos.Name, error) { 114 | switch v := fv.Impl.(type) { 115 | case eos.Name: 116 | return v, nil 117 | case string: 118 | return eos.Name(v), nil 119 | default: 120 | return eos.Name(""), &InvalidTypeError{ 121 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv), 122 | ExpectedType: "eos.Name", 123 | FlexValue: fv, 124 | } 125 | } 126 | } 127 | 128 | // Int64 returns a string value of found content or it panics 129 | func (fv *FlexValue) Int64() (int64, error) { 130 | switch v := fv.Impl.(type) { 131 | case int64: 132 | return v, nil 133 | default: 134 | return -1000000, &InvalidTypeError{ 135 | Label: fmt.Sprintf("received an unexpected type %T for variant %T", v, fv), 136 | ExpectedType: "int64", 137 | FlexValue: fv, 138 | } 139 | } 140 | } 141 | 142 | // IsEqual evaluates if the two FlexValues have the same types and values (deep compare) 143 | func (fv *FlexValue) IsEqual(fv2 *FlexValue) bool { 144 | 145 | if fv.TypeID != fv2.TypeID { 146 | log.Println("FlexValue types inequal: ", fv.TypeID, " vs ", fv2.TypeID) 147 | return false 148 | } 149 | 150 | if fv.String() != fv2.String() { 151 | log.Println("FlexValue Values.String() inequal: ", fv.String(), " vs ", fv2.String()) 152 | return false 153 | } 154 | 155 | return true 156 | } 157 | 158 | // MarshalJSON translates to []byte 159 | func (fv *FlexValue) MarshalJSON() ([]byte, error) { 160 | return fv.BaseVariant.MarshalJSON(FlexValueVariant) 161 | } 162 | 163 | // UnmarshalJSON translates flexValueVariant 164 | func (fv *FlexValue) UnmarshalJSON(data []byte) error { 165 | return fv.BaseVariant.UnmarshalJSON(data, FlexValueVariant) 166 | } 167 | 168 | // UnmarshalBinary ... 169 | func (fv *FlexValue) UnmarshalBinary(decoder *eos.Decoder) error { 170 | return fv.BaseVariant.UnmarshalBinaryVariant(decoder, FlexValueVariant) 171 | } 172 | -------------------------------------------------------------------------------- /docgraph/docgraph_test.go: -------------------------------------------------------------------------------- 1 | package docgraph_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | eostest "github.com/digital-scarcity/eos-go-test" 9 | eos "github.com/eoscanada/eos-go" 10 | "github.com/hypha-dao/document-graph/docgraph" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | const testingEndpoint = "http://localhost:8888" 15 | 16 | var env *Environment 17 | var chainResponsePause time.Duration 18 | 19 | func setupTestCase(t *testing.T) func(t *testing.T) { 20 | t.Log("Bootstrapping testing environment ...") 21 | 22 | cmd, err := eostest.RestartNodeos(true) 23 | assert.NilError(t, err) 24 | 25 | chainResponsePause = time.Second 26 | 27 | t.Log("nodeos PID: ", cmd.Process.Pid) 28 | 29 | return func(t *testing.T) { 30 | folderName := "test_results" 31 | t.Log("Saving graph to : ", folderName) 32 | err := SaveGraph(env.ctx, &env.api, env.Docs, folderName) 33 | assert.NilError(t, err) 34 | } 35 | } 36 | 37 | func TestGetOrNewNew(t *testing.T) { 38 | 39 | teardownTestCase := setupTestCase(t) 40 | defer teardownTestCase(t) 41 | 42 | // var env Environment 43 | env = SetupEnvironment(t) 44 | t.Log("\nEnvironment Setup complete\n") 45 | 46 | _, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 47 | assert.NilError(t, err) 48 | 49 | var ci docgraph.ContentItem 50 | ci.Label = randomString() 51 | ci.Value = &docgraph.FlexValue{ 52 | BaseVariant: eos.BaseVariant{ 53 | TypeID: docgraph.FlexValueVariant.TypeID("name"), 54 | Impl: randomString(), 55 | }, 56 | } 57 | 58 | cg := make([]docgraph.ContentItem, 1) 59 | cg[0] = ci 60 | cgs := make([]docgraph.ContentGroup, 1) 61 | cgs[0] = cg 62 | var randomDoc docgraph.Document 63 | randomDoc.ContentGroups = cgs 64 | 65 | // should be a legit new document 66 | _, err = GetOrNewNew(env.ctx, &env.api, env.Docs, env.Creators[1], randomDoc) 67 | assert.NilError(t, err) 68 | } 69 | 70 | func TestGetOrNewGet(t *testing.T) { 71 | 72 | teardownTestCase := setupTestCase(t) 73 | defer teardownTestCase(t) 74 | 75 | // var env Environment 76 | env = SetupEnvironment(t) 77 | t.Log("\nEnvironment Setup complete\n") 78 | 79 | randomDoc, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 80 | assert.NilError(t, err) 81 | 82 | // should NOT be a legit new document 83 | sameRandomDoc, err := GetOrNewGet(env.ctx, &env.api, env.Docs, env.Creators[1], randomDoc) 84 | assert.NilError(t, err) 85 | 86 | assert.Equal(t, randomDoc.Hash.String(), sameRandomDoc.Hash.String()) 87 | } 88 | 89 | func TestGetLastDocOfEdgeName(t *testing.T) { 90 | 91 | teardownTestCase := setupTestCase(t) 92 | defer teardownTestCase(t) 93 | 94 | // var env Environment 95 | env = SetupEnvironment(t) 96 | t.Log("\nEnvironment Setup complete\n") 97 | 98 | randomDoc1, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 99 | assert.NilError(t, err) 100 | 101 | randomDoc2, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 102 | assert.NilError(t, err) 103 | 104 | _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], randomDoc1.Hash, randomDoc2.Hash, "testlastedge") 105 | assert.NilError(t, err) 106 | 107 | lastDocument, err := docgraph.GetLastDocumentOfEdge(env.ctx, &env.api, env.Docs, "testlastedge") 108 | assert.NilError(t, err) 109 | assert.Equal(t, randomDoc2.Hash.String(), lastDocument.Hash.String()) 110 | } 111 | 112 | func TestManyDocuments(t *testing.T) { 113 | teardownTestCase := setupTestCase(t) 114 | defer teardownTestCase(t) 115 | 116 | env = SetupEnvironment(t) 117 | t.Log("\nEnvironment Setup complete\n") 118 | 119 | for i := 1; i < 1000; i++ { 120 | _, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 121 | assert.NilError(t, err) 122 | } 123 | 124 | docs, err := docgraph.GetAllDocuments(env.ctx, &env.api, env.Docs) 125 | if err != nil { 126 | panic(fmt.Errorf("cannot get all documents: %v", err)) 127 | } 128 | 129 | assert.NilError(t, err) 130 | assert.Assert(t, len(docs) >= 999) 131 | } 132 | 133 | func TestWrongContentError(t *testing.T) { 134 | teardownTestCase := setupTestCase(t) 135 | defer teardownTestCase(t) 136 | 137 | env = SetupEnvironment(t) 138 | t.Log("\nEnvironment Setup complete\n") 139 | 140 | t.Log("\nTesting Worng Content Error:") 141 | 142 | _, err := ContentError(env.ctx, &env.api, env.Docs) 143 | 144 | assert.ErrorContains(t, err, "Content value for label [test_label] is not of expected type") 145 | } 146 | -------------------------------------------------------------------------------- /docgraph/document.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | 10 | eostest "github.com/digital-scarcity/eos-go-test" 11 | eos "github.com/eoscanada/eos-go" 12 | ) 13 | 14 | // ContentNotFoundError is used when content matching 15 | // a specific label is requested but not found in a document 16 | type ContentNotFoundError struct { 17 | Label string 18 | DocumentHash eos.Checksum256 19 | } 20 | 21 | func (c *ContentNotFoundError) Error() string { 22 | return fmt.Sprintf("content label not found: %v in document: %v", c.Label, c.DocumentHash.String()) 23 | } 24 | 25 | // Document is a node in the document graph 26 | // A document may hold any arbitrary, EOSIO compatible data 27 | type Document struct { 28 | ID uint64 `json:"id"` 29 | Hash eos.Checksum256 `json:"hash"` 30 | Creator eos.AccountName `json:"creator"` 31 | ContentGroups []ContentGroup `json:"content_groups"` 32 | Certificates []struct { 33 | Certifier eos.AccountName `json:"certifier"` 34 | Notes string `json:"notes"` 35 | CertificationDate eos.BlockTimestamp `json:"certification_date"` 36 | } `json:"certificates"` 37 | CreatedDate eos.BlockTimestamp `json:"created_date"` 38 | } 39 | 40 | func newDocumentTrx(ctx context.Context, api *eos.API, 41 | contract, creator eos.AccountName, actionName, 42 | fileName string) (Document, error) { 43 | 44 | data, err := ioutil.ReadFile(fileName) 45 | if err != nil { 46 | return Document{}, fmt.Errorf("readfile %v: %v", fileName, err) 47 | } 48 | 49 | action := eos.ActN(actionName) 50 | 51 | var dump map[string]interface{} 52 | err = json.Unmarshal(data, &dump) 53 | if err != nil { 54 | return Document{}, fmt.Errorf("unmarshal %v: %v", fileName, err) 55 | } 56 | 57 | dump["creator"] = creator 58 | 59 | actionBinary, err := api.ABIJSONToBin(ctx, contract, eos.Name(action), dump) 60 | if err != nil { 61 | return Document{}, fmt.Errorf("api json to bin %v: %v", fileName, err) 62 | } 63 | 64 | actions := []*eos.Action{ 65 | { 66 | Account: contract, 67 | Name: action, 68 | Authorization: []eos.PermissionLevel{ 69 | {Actor: creator, Permission: eos.PN("active")}, 70 | }, 71 | ActionData: eos.NewActionDataFromHexData([]byte(actionBinary)), 72 | }} 73 | 74 | _, err = eostest.ExecWithRetry(ctx, api, actions) 75 | if err != nil { 76 | return Document{}, fmt.Errorf("execute transaction %v: %v", fileName, err) 77 | } 78 | 79 | lastDoc, err := GetLastDocument(ctx, api, contract) 80 | if err != nil { 81 | return Document{}, fmt.Errorf("get last document %v: %v", fileName, err) 82 | } 83 | return lastDoc, nil 84 | } 85 | 86 | // CreateDocument creates a new document on chain from the provided file 87 | func CreateDocument(ctx context.Context, api *eos.API, 88 | contract, creator eos.AccountName, 89 | fileName string) (Document, error) { 90 | 91 | return newDocumentTrx(ctx, api, contract, creator, "create", fileName) 92 | } 93 | 94 | // GetContent returns a FlexValue of the content with the matching label 95 | // or an instance of ContentNotFoundError 96 | func (d *Document) GetContent(label string) (*FlexValue, error) { 97 | for _, contentGroup := range d.ContentGroups { 98 | for _, content := range contentGroup { 99 | if content.Label == label { 100 | return content.Value, nil 101 | } 102 | } 103 | } 104 | return nil, &ContentNotFoundError{ 105 | Label: label, 106 | DocumentHash: d.Hash, 107 | } 108 | } 109 | 110 | // GetContentFromGroup returns a FlexValue of the content with the matching label 111 | // or an instance of ContentNotFoundError 112 | func (d *Document) GetContentFromGroup(groupLabel, label string) (*FlexValue, error) { 113 | for _, contentGroup := range d.ContentGroups { 114 | for _, content := range contentGroup { 115 | if content.Label == label { 116 | return content.Value, nil 117 | } 118 | } 119 | } 120 | return nil, &ContentNotFoundError{ 121 | Label: label, 122 | DocumentHash: d.Hash, 123 | } 124 | } 125 | 126 | // GetContentGroup returns a ContentGroup matching the label 127 | // or an instance of ContentNotFoundError 128 | func (d *Document) GetContentGroup(label string) (*ContentGroup, error) { 129 | for _, contentGroup := range d.ContentGroups { 130 | for _, content := range contentGroup { 131 | if content.Label == "content_group_label" { 132 | if content.Value.String() == label { 133 | return &contentGroup, nil 134 | } 135 | break // found label but wrong value, go to next group 136 | } 137 | } 138 | } 139 | return nil, &ContentNotFoundError{ 140 | Label: label, 141 | DocumentHash: d.Hash, 142 | } 143 | } 144 | 145 | // GetNodeLabel returns a string for the node label 146 | func (d *Document) GetNodeLabel() string { 147 | nodeLabel, err := d.GetContentFromGroup("system", "node_label") 148 | if err != nil { 149 | return "" 150 | } 151 | return nodeLabel.String() 152 | } 153 | 154 | // GetType return the document type; fails if it does not exist or is not an eos.Name type 155 | func (d *Document) GetType() (eos.Name, error) { 156 | typeValue, err := d.GetContentFromGroup("system", "type") 157 | if err != nil { 158 | return eos.Name(""), nil 159 | // return eos.Name(""), fmt.Errorf("document type does not exist in system group of document: %v", err) 160 | } 161 | 162 | typeValueName, err := typeValue.Name() 163 | if err != nil { 164 | return eos.Name(""), fmt.Errorf("document type is not an eos.Name value: %v", err) 165 | } 166 | 167 | return typeValueName, nil 168 | } 169 | 170 | // IsEqual is a deep equal comparison of two documents 171 | func (d *Document) IsEqual(d2 Document) bool { 172 | 173 | // ensure the same number of content groups 174 | if len(d.ContentGroups) != len(d2.ContentGroups) { 175 | log.Println("ContentGroups lengths inequal: ", len(d.ContentGroups), " vs ", len(d2.ContentGroups)) 176 | return false 177 | } 178 | 179 | for contentGroupIndex, contentGroup1 := range d.ContentGroups { 180 | contentGroup2 := d2.ContentGroups[contentGroupIndex] 181 | 182 | // ensure these two content groups have the same number of items 183 | if len(contentGroup1) != len(contentGroup2) { 184 | log.Println("ContentGroup lengths inequal for CG index: ", contentGroupIndex, "; ", len(contentGroup1), " vs ", len(contentGroup2)) 185 | return false 186 | } 187 | 188 | for contentIndex, content1 := range contentGroup1 { 189 | content2 := contentGroup2[contentIndex] 190 | 191 | // ensure these content items have the same label and same value 192 | if !content1.IsEqual(content2) { 193 | return false 194 | } 195 | } 196 | } 197 | 198 | // if we got through all the above checks, the documents are equal 199 | return true 200 | } 201 | 202 | // LoadDocument reads a document from the blockchain and creates a Document instance 203 | func LoadDocument(ctx context.Context, api *eos.API, 204 | contract eos.AccountName, 205 | hash string) (Document, error) { 206 | 207 | var documents []Document 208 | var request eos.GetTableRowsRequest 209 | request.Code = string(contract) 210 | request.Scope = string(contract) 211 | request.Table = "documents" 212 | request.Index = "2" 213 | request.KeyType = "sha256" 214 | request.LowerBound = hash 215 | request.UpperBound = hash 216 | request.Limit = 1 217 | request.JSON = true 218 | response, err := api.GetTableRows(ctx, request) 219 | if err != nil { 220 | return Document{}, fmt.Errorf("get table rows %v: %v", hash, err) 221 | } 222 | 223 | err = response.JSONToStructs(&documents) 224 | if err != nil { 225 | return Document{}, fmt.Errorf("json to structs %v: %v", hash, err) 226 | } 227 | 228 | if len(documents) == 0 { 229 | return Document{}, fmt.Errorf("document not found %v: %v", hash, err) 230 | } 231 | return documents[0], nil 232 | } 233 | 234 | type eraseDoc struct { 235 | Hash eos.Checksum256 `json:"hash"` 236 | } 237 | 238 | // EraseDocument ... 239 | func EraseDocument(ctx context.Context, api *eos.API, 240 | contract eos.AccountName, 241 | hash eos.Checksum256) (string, error) { 242 | 243 | actions := []*eos.Action{{ 244 | Account: contract, 245 | Name: eos.ActN("erase"), 246 | Authorization: []eos.PermissionLevel{ 247 | {Actor: contract, Permission: eos.PN("active")}, 248 | }, 249 | ActionData: eos.NewActionData(eraseDoc{ 250 | Hash: hash, 251 | }), 252 | }} 253 | return eostest.ExecWithRetry(ctx, api, actions) 254 | } 255 | -------------------------------------------------------------------------------- /docgraph/document_graph.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | 9 | eos "github.com/eoscanada/eos-go" 10 | "github.com/k0kubun/go-ansi" 11 | "github.com/schollz/progressbar/v3" 12 | ) 13 | 14 | // DocumentGraph is defined by a root node, and is aware of nodes and edges 15 | type DocumentGraph struct { 16 | RootNode Document 17 | } 18 | 19 | func getEdgesByIndex(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeIndex string) ([]Edge, error) { 20 | var edges []Edge 21 | var request eos.GetTableRowsRequest 22 | request.Code = string(contract) 23 | request.Scope = string(contract) 24 | request.Table = "edges" 25 | request.Index = edgeIndex 26 | request.KeyType = "sha256" 27 | request.LowerBound = document.Hash.String() 28 | request.UpperBound = document.Hash.String() 29 | request.Limit = 10000 30 | request.JSON = true 31 | response, err := api.GetTableRows(ctx, request) 32 | if err != nil { 33 | log.Println("Error with GetTableRows: ", err) 34 | return []Edge{}, err 35 | } 36 | 37 | err = response.JSONToStructs(&edges) 38 | if err != nil { 39 | log.Println("Error with JSONToStructs: ", err) 40 | return []Edge{}, err 41 | } 42 | return edges, nil 43 | } 44 | 45 | // GetEdgesFromDocument retrieves a list of edges from this node to other nodes 46 | func GetEdgesFromDocument(ctx context.Context, api *eos.API, contract eos.AccountName, document Document) ([]Edge, error) { 47 | return getEdgesByIndex(ctx, api, contract, document, string("2")) 48 | } 49 | 50 | // GetEdgesToDocument retrieves a list of edges to this node from other nodes 51 | func GetEdgesToDocument(ctx context.Context, api *eos.API, contract eos.AccountName, document Document) ([]Edge, error) { 52 | return getEdgesByIndex(ctx, api, contract, document, string("3")) 53 | } 54 | 55 | // GetEdgesFromDocumentWithEdge retrieves a list of edges from this node to other nodes 56 | func GetEdgesFromDocumentWithEdge(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeName eos.Name) ([]Edge, error) { 57 | edges, err := getEdgesByIndex(ctx, api, contract, document, string("2")) 58 | if err != nil { 59 | log.Println("Error with JSONToStructs: ", err) 60 | return []Edge{}, err 61 | } 62 | 63 | var namedEdges []Edge 64 | for _, edge := range edges { 65 | if edge.EdgeName == edgeName { 66 | namedEdges = append(namedEdges, edge) 67 | } 68 | } 69 | return namedEdges, nil 70 | } 71 | 72 | // GetDocumentsWithEdge retrieves a list of documents connected to the provided document via the provided edge name 73 | func GetDocumentsWithEdge(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeName eos.Name) ([]Document, error) { 74 | edges, err := GetEdgesFromDocumentWithEdge(ctx, api, contract, document, edgeName) 75 | if err != nil { 76 | return []Document{}, fmt.Errorf("error retrieving edges %v", err) 77 | } 78 | 79 | documents := make([]Document, len(edges)) 80 | for index, edge := range edges { 81 | documents[index], err = LoadDocument(ctx, api, contract, edge.ToNode.String()) 82 | if err != nil { 83 | return []Document{}, fmt.Errorf("error loading document %v", err) 84 | } 85 | } 86 | return documents, nil 87 | } 88 | 89 | // GetEdgesToDocumentWithEdge retrieves a list of edges from this node to other nodes 90 | func GetEdgesToDocumentWithEdge(ctx context.Context, api *eos.API, contract eos.AccountName, document Document, edgeName eos.Name) ([]Edge, error) { 91 | edges, err := getEdgesByIndex(ctx, api, contract, document, string("3")) 92 | if err != nil { 93 | log.Println("Error with JSONToStructs: ", err) 94 | return []Edge{}, err 95 | } 96 | 97 | var namedEdges []Edge 98 | for _, edge := range edges { 99 | if edge.EdgeName == edgeName { 100 | namedEdges = append(namedEdges, edge) 101 | } 102 | } 103 | return namedEdges, nil 104 | } 105 | 106 | // GetLastDocument retrieves the last document that was created from the contract 107 | func GetLastDocument(ctx context.Context, api *eos.API, contract eos.AccountName) (Document, error) { 108 | var docs []Document 109 | var request eos.GetTableRowsRequest 110 | request.Code = string(contract) 111 | request.Scope = string(contract) 112 | request.Table = "documents" 113 | request.Reverse = true 114 | request.Limit = 1 115 | request.JSON = true 116 | response, err := api.GetTableRows(ctx, request) 117 | if err != nil { 118 | log.Println("Error with GetTableRows: ", err) 119 | return Document{}, err 120 | } 121 | 122 | err = response.JSONToStructs(&docs) 123 | if err != nil { 124 | log.Println("Error with JSONToStructs: ", err) 125 | return Document{}, err 126 | } 127 | return docs[0], nil 128 | } 129 | 130 | // GetLastDocumentOfEdge ... 131 | func GetLastDocumentOfEdge(ctx context.Context, api *eos.API, contract eos.AccountName, edgeName eos.Name) (Document, error) { 132 | var edges []Edge 133 | var request eos.GetTableRowsRequest 134 | request.Code = string(contract) 135 | request.Scope = string(contract) 136 | request.Table = "edges" 137 | request.Reverse = true 138 | request.Index = "8" 139 | request.KeyType = "i64" 140 | request.Limit = 1000 141 | request.JSON = true 142 | // request.LowerBound = sdtrinedgeName 143 | // request.UpperBound = edgeName 144 | response, err := api.GetTableRows(ctx, request) 145 | if err != nil { 146 | return Document{}, fmt.Errorf("json to struct: %v", err) 147 | } 148 | 149 | err = response.JSONToStructs(&edges) 150 | if err != nil { 151 | return Document{}, fmt.Errorf("json to struct: %v", err) 152 | } 153 | 154 | for _, edge := range edges { 155 | if edge.EdgeName == edgeName { 156 | return LoadDocument(ctx, api, contract, edge.ToNode.String()) 157 | } 158 | } 159 | 160 | return Document{}, fmt.Errorf("no document with edge found: %v", string(edgeName)) 161 | } 162 | 163 | func getRange(ctx context.Context, api *eos.API, contract eos.AccountName, id, count int) ([]Document, bool, error) { 164 | var documents []Document 165 | var request eos.GetTableRowsRequest 166 | if id > 0 { 167 | request.LowerBound = strconv.Itoa(id) 168 | } 169 | request.Code = string(contract) 170 | request.Scope = string(contract) 171 | request.Table = "documents" 172 | request.Limit = uint32(count) 173 | request.JSON = true 174 | response, err := api.GetTableRows(ctx, request) 175 | if err != nil { 176 | return []Document{}, false, fmt.Errorf("get table rows %v", err) 177 | } 178 | 179 | err = response.JSONToStructs(&documents) 180 | if err != nil { 181 | return []Document{}, false, fmt.Errorf("json to structs %v", err) 182 | } 183 | return documents, response.More, nil 184 | } 185 | 186 | // GetAllDocumentsForType reads all documents and returns them in a slice 187 | func GetAllDocumentsForType(ctx context.Context, api *eos.API, contract eos.AccountName, docType string) ([]Document, error) { 188 | 189 | allDocuments, err := GetAllDocuments(ctx, api, contract) 190 | if err != nil { 191 | return []Document{}, fmt.Errorf("cannot get all documents %v", err) 192 | } 193 | 194 | var filteredDocs []Document 195 | for _, doc := range allDocuments { 196 | 197 | typeFV, err := doc.GetContent("type") 198 | if err == nil && 199 | typeFV.Impl.(eos.Name) == eos.Name(docType) { 200 | filteredDocs = append(filteredDocs, doc) 201 | } 202 | } 203 | 204 | return filteredDocs, nil 205 | } 206 | 207 | // GetAllDocuments reads all documents and returns them in a slice 208 | func GetAllDocuments(ctx context.Context, api *eos.API, contract eos.AccountName) ([]Document, error) { 209 | 210 | var allDocuments []Document 211 | batchSize := 75 212 | 213 | bar := DefaultProgressBar("Retrieving graph for cache ... ", -1) // progressbar.Default(-1) 214 | 215 | batch, more, err := getRange(ctx, api, contract, 0, batchSize) 216 | if err != nil { 217 | return []Document{}, fmt.Errorf("json to structs %v", err) 218 | } 219 | allDocuments = append(allDocuments, batch...) 220 | bar.Add(batchSize) 221 | 222 | for more { 223 | batch, more, err = getRange(ctx, api, contract, int(batch[len(batch)-1].ID), batchSize) 224 | if err != nil { 225 | return []Document{}, fmt.Errorf("json to structs %v", err) 226 | } 227 | allDocuments = append(allDocuments, batch...) 228 | bar.Add(batchSize) 229 | } 230 | 231 | bar.Clear() 232 | return allDocuments, nil 233 | } 234 | 235 | func DefaultProgressBar(prefix string, counter int) *progressbar.ProgressBar { 236 | return progressbar.NewOptions(counter, 237 | progressbar.OptionSetWriter(ansi.NewAnsiStdout()), 238 | progressbar.OptionEnableColorCodes(true), 239 | progressbar.OptionSetWidth(90), 240 | // progressbar.OptionShowIts(), 241 | progressbar.OptionSetDescription("[cyan]"+fmt.Sprintf("%20v", prefix)), 242 | progressbar.OptionSetTheme(progressbar.Theme{ 243 | Saucer: "[green]=[reset]", 244 | SaucerHead: "[green]>[reset]", 245 | SaucerPadding: " ", 246 | BarStart: "[", 247 | BarEnd: "]", 248 | })) 249 | } 250 | -------------------------------------------------------------------------------- /docgraph/document_test.go: -------------------------------------------------------------------------------- 1 | package docgraph_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/eoscanada/eos-go" 9 | 10 | "github.com/hypha-dao/document-graph/docgraph" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | func TestDocuments(t *testing.T) { 15 | 16 | teardownTestCase := setupTestCase(t) 17 | defer teardownTestCase(t) 18 | 19 | // var env Environment 20 | env = SetupEnvironment(t) 21 | t.Log("\nEnvironment Setup complete\n") 22 | 23 | t.Run("Test Documents", func(t *testing.T) { 24 | 25 | tests := []struct { 26 | name string 27 | input string 28 | }{ 29 | { 30 | name: "simplest", 31 | input: "examples/simplest.json", 32 | }, 33 | { 34 | name: "each-type", 35 | input: "examples/each-type.json", 36 | }, 37 | { 38 | name: "contribution", 39 | input: "examples/contribution.json", 40 | }, 41 | } 42 | 43 | for _, test := range tests { 44 | t.Run(test.name, func(t *testing.T) { 45 | 46 | lastDoc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], test.input) 47 | assert.NilError(t, err) 48 | 49 | // unmarshal JSON into a Document 50 | data, err := ioutil.ReadFile(test.input) 51 | assert.NilError(t, err) 52 | var documentFromFile docgraph.Document 53 | err = json.Unmarshal(data, &documentFromFile) 54 | assert.NilError(t, err) 55 | 56 | // compare document from chain to document from file 57 | assert.Assert(t, lastDoc.IsEqual(documentFromFile)) 58 | }) 59 | } 60 | }) 61 | } 62 | 63 | func TestLoadDocument(t *testing.T) { 64 | 65 | teardownTestCase := setupTestCase(t) 66 | defer teardownTestCase(t) 67 | 68 | env = SetupEnvironment(t) 69 | t.Log("\nEnvironment Setup complete\n") 70 | 71 | doc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[1], "examples/simplest.json") 72 | assert.NilError(t, err) 73 | 74 | loadedDoc, err := docgraph.LoadDocument(env.ctx, &env.api, env.Docs, doc.Hash.String()) 75 | assert.NilError(t, err) 76 | assert.Equal(t, doc.Hash.String(), loadedDoc.Hash.String()) 77 | assert.Equal(t, doc.Creator, loadedDoc.Creator) 78 | 79 | _, err = docgraph.LoadDocument(env.ctx, &env.api, env.Docs, "ahashthatwillnotexist") 80 | assert.ErrorContains(t, err, "Internal Service Error") 81 | } 82 | 83 | func TestEraseDocument(t *testing.T) { 84 | 85 | teardownTestCase := setupTestCase(t) 86 | defer teardownTestCase(t) 87 | 88 | // var env Environment 89 | env = SetupEnvironment(t) 90 | t.Log("\nEnvironment Setup complete\n") 91 | 92 | randomDoc, err := CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 93 | assert.NilError(t, err) 94 | 95 | _, err = docgraph.EraseDocument(env.ctx, &env.api, env.Docs, randomDoc.Hash) 96 | assert.NilError(t, err) 97 | 98 | _, err = docgraph.LoadDocument(env.ctx, &env.api, env.Docs, randomDoc.Hash.String()) 99 | assert.ErrorContains(t, err, "document not found") 100 | } 101 | 102 | func TestCreateRoot(t *testing.T) { 103 | 104 | teardownTestCase := setupTestCase(t) 105 | defer teardownTestCase(t) 106 | 107 | // var env Environment 108 | env = SetupEnvironment(t) 109 | t.Log("\nEnvironment Setup complete\n") 110 | 111 | rootDoc, err := CreateRoot(env.ctx, &env.api, env.Docs, env.Docs) 112 | assert.NilError(t, err) 113 | 114 | t.Log("Root Document hash: ", rootDoc.Hash.String()) 115 | } 116 | 117 | func TestGetContent(t *testing.T) { 118 | 119 | teardownTestCase := setupTestCase(t) 120 | defer teardownTestCase(t) 121 | 122 | // var env Environment 123 | env = SetupEnvironment(t) 124 | t.Log("\nEnvironment Setup complete\n") 125 | 126 | t.Run("Test Get Content", func(t *testing.T) { 127 | 128 | tests := []struct { 129 | name string 130 | input string 131 | }{ 132 | { 133 | name: "each-type", 134 | input: "examples/each-type.json", 135 | }, 136 | } 137 | 138 | for _, test := range tests { 139 | t.Run(test.name, func(t *testing.T) { 140 | 141 | lastDoc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], test.input) 142 | assert.NilError(t, err) 143 | 144 | // unmarshal JSON into a Document 145 | data, err := ioutil.ReadFile(test.input) 146 | assert.NilError(t, err) 147 | var documentFromFile docgraph.Document 148 | err = json.Unmarshal(data, &documentFromFile) 149 | assert.NilError(t, err) 150 | 151 | // compare document from chain to document from file 152 | assert.Assert(t, lastDoc.IsEqual(documentFromFile)) 153 | 154 | salary, _ := eos.NewAssetFromString("130.00 USD") 155 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label", "salary_amount", salary) 156 | assert.NilError(t, err) 157 | 158 | wrongSalary, _ := eos.NewAssetFromString("131.00 USD") 159 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label", "salary_amount", wrongSalary) 160 | assert.ErrorContains(t, err, "read value does not equal content value") 161 | 162 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label", "wrong_content_label", salary) 163 | assert.ErrorContains(t, err, "contentGroup or contentLabel does not exist") 164 | 165 | _, err = GetAssetTest(env.ctx, &env.api, env.Docs, lastDoc, "Nonexistent Content Group Label", "salary_amount", salary) 166 | assert.ErrorContains(t, err, "contentGroup or contentLabel does not exist") 167 | }) 168 | } 169 | }) 170 | } 171 | 172 | func TestGetGroup(t *testing.T) { 173 | 174 | teardownTestCase := setupTestCase(t) 175 | defer teardownTestCase(t) 176 | 177 | // var env Environment 178 | env = SetupEnvironment(t) 179 | t.Log("\nEnvironment Setup complete\n") 180 | 181 | t.Run("Test Get Content", func(t *testing.T) { 182 | 183 | tests := []struct { 184 | name string 185 | input string 186 | }{ 187 | { 188 | name: "each-type", 189 | input: "examples/each-type.json", 190 | }, 191 | } 192 | 193 | for _, test := range tests { 194 | t.Run(test.name, func(t *testing.T) { 195 | 196 | lastDoc, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], test.input) 197 | assert.NilError(t, err) 198 | 199 | // unmarshal JSON into a Document 200 | data, err := ioutil.ReadFile(test.input) 201 | assert.NilError(t, err) 202 | var documentFromFile docgraph.Document 203 | err = json.Unmarshal(data, &documentFromFile) 204 | assert.NilError(t, err) 205 | 206 | // compare document from chain to document from file 207 | assert.Assert(t, lastDoc.IsEqual(documentFromFile)) 208 | 209 | _, err = GetGroupTest(env.ctx, &env.api, env.Docs, lastDoc, "My Content Group Label") 210 | assert.NilError(t, err) 211 | 212 | _, err = GetGroupTest(env.ctx, &env.api, env.Docs, lastDoc, "Nonexistent Content Group Label") 213 | assert.ErrorContains(t, err, "group was not found") 214 | 215 | }) 216 | } 217 | }) 218 | } 219 | -------------------------------------------------------------------------------- /docgraph/edge.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | 9 | eostest "github.com/digital-scarcity/eos-go-test" 10 | eos "github.com/eoscanada/eos-go" 11 | ) 12 | 13 | // Edge is a directional, named connection from one graph to another 14 | type Edge struct { 15 | ID uint64 `json:"id"` 16 | Creator eos.Name `json:"creator"` 17 | FromNode eos.Checksum256 `json:"from_node"` 18 | ToNode eos.Checksum256 `json:"to_node"` 19 | EdgeName eos.Name `json:"edge_name"` 20 | CreatedDate eos.BlockTimestamp `json:"created_date"` 21 | } 22 | 23 | // RemoveEdges ... 24 | type RemoveEdges struct { 25 | FromNode eos.Checksum256 `json:"from_node"` 26 | EdgeName eos.Name `json:"edge_name"` 27 | Strict bool `json:"strict"` 28 | } 29 | 30 | type removeEdge struct { 31 | FromNode eos.Checksum256 `json:"from_node"` 32 | ToNode eos.Checksum256 `json:"to_node"` 33 | EdgeName eos.Name `json:"edge_name"` 34 | } 35 | 36 | // CreateEdge creates an edge from one document node to another with the specified name 37 | func CreateEdge(ctx context.Context, api *eos.API, 38 | contract, creator eos.AccountName, 39 | fromNode, toNode eos.Checksum256, 40 | edgeName eos.Name) (string, error) { 41 | 42 | actionData := make(map[string]interface{}) 43 | actionData["creator"] = creator 44 | actionData["from_node"] = fromNode 45 | actionData["to_node"] = toNode 46 | actionData["edge_name"] = edgeName 47 | 48 | actionBinary, err := api.ABIJSONToBin(ctx, contract, eos.Name("newedge"), actionData) 49 | if err != nil { 50 | log.Println("Error with ABIJSONToBin: ", err) 51 | return "error", err 52 | } 53 | 54 | actions := []*eos.Action{ 55 | { 56 | Account: contract, 57 | Name: eos.ActN("newedge"), 58 | Authorization: []eos.PermissionLevel{ 59 | {Actor: creator, Permission: eos.PN("active")}, 60 | }, 61 | ActionData: eos.NewActionDataFromHexData([]byte(actionBinary)), 62 | }} 63 | 64 | return eostest.ExecWithRetry(ctx, api, actions) 65 | } 66 | 67 | // RemoveEdge ... 68 | func RemoveEdge(ctx context.Context, api *eos.API, 69 | contract eos.AccountName, 70 | fromHash, toHash eos.Checksum256, edgeName eos.Name) (string, error) { 71 | 72 | actions := []*eos.Action{{ 73 | Account: contract, 74 | Name: eos.ActN("removeedge"), 75 | Authorization: []eos.PermissionLevel{ 76 | {Actor: contract, Permission: eos.PN("active")}, 77 | }, 78 | ActionData: eos.NewActionData(removeEdge{ 79 | FromNode: fromHash, 80 | ToNode: toHash, 81 | EdgeName: edgeName, 82 | }), 83 | }} 84 | return eostest.ExecWithRetry(ctx, api, actions) 85 | } 86 | 87 | // // GetAllEdges retrieves all edges from table 88 | // func GetAllEdges(ctx context.Context, api *eos.API, contract eos.AccountName) ([]Edge, error) { 89 | // var edges []Edge 90 | // var request eos.GetTableRowsRequest 91 | // request.Code = string(contract) 92 | // request.Scope = string(contract) 93 | // request.Table = "edges" 94 | // request.Limit = 100000 95 | // request.JSON = true 96 | // response, err := api.GetTableRows(ctx, request) 97 | // if err != nil { 98 | // log.Println("Error with GetTableRows: ", err) 99 | // return []Edge{}, err 100 | // } 101 | 102 | // err = response.JSONToStructs(&edges) 103 | // if err != nil { 104 | // log.Println("Error with JSONToStructs: ", err) 105 | // return []Edge{}, err 106 | // } 107 | // return edges, nil 108 | // } 109 | 110 | func getEdgeRange(ctx context.Context, api *eos.API, contract eos.AccountName, id, count int) ([]Edge, bool, error) { 111 | var edges []Edge 112 | var request eos.GetTableRowsRequest 113 | 114 | if id > 0 { 115 | request.LowerBound = strconv.Itoa(id) 116 | } 117 | request.Code = string(contract) 118 | request.Scope = string(contract) 119 | request.Table = "edges" 120 | request.Limit = uint32(count) 121 | request.JSON = true 122 | response, err := api.GetTableRows(ctx, request) 123 | if err != nil { 124 | return []Edge{}, false, fmt.Errorf("retrieving edge range %v", err) 125 | } 126 | 127 | err = response.JSONToStructs(&edges) 128 | if err != nil { 129 | return []Edge{}, false, fmt.Errorf("edge json to structs %v", err) 130 | } 131 | return edges, response.More, nil 132 | } 133 | 134 | // GetAllEdges reads all documents and returns them in a slice 135 | func GetAllEdges(ctx context.Context, api *eos.API, contract eos.AccountName) ([]Edge, error) { 136 | 137 | var allEdges []Edge 138 | 139 | batchSize := 1000 140 | 141 | batch, more, err := getEdgeRange(ctx, api, contract, 0, batchSize) 142 | if err != nil { 143 | return []Edge{}, fmt.Errorf("cannot get initial range of edges %v", err) 144 | } 145 | allEdges = append(allEdges, batch...) 146 | 147 | for more { 148 | batch, more, err = getEdgeRange(ctx, api, contract, int(batch[len(batch)-1].ID), batchSize) 149 | if err != nil { 150 | return []Edge{}, fmt.Errorf("cannot get range of edges %v", err) 151 | } 152 | allEdges = append(allEdges, batch...) 153 | } 154 | 155 | return allEdges, nil 156 | } 157 | 158 | // EdgeExists checks to see if the edge exists 159 | func EdgeExists(ctx context.Context, api *eos.API, contract eos.AccountName, 160 | fromNode, toNode Document, edgeName eos.Name) (bool, error) { 161 | 162 | edges, err := GetAllEdges(ctx, api, contract) 163 | if err != nil { 164 | return false, fmt.Errorf("get edges from by name doc: %v err: %v", fromNode.Hash, err) 165 | } 166 | 167 | for _, edge := range edges { 168 | if edge.ToNode.String() == toNode.Hash.String() && 169 | edge.FromNode.String() == fromNode.Hash.String() && 170 | edge.EdgeName == edgeName { 171 | return true, nil 172 | } 173 | } 174 | return false, nil 175 | } 176 | 177 | // RemoveEdgesFromAndName ... 178 | // func RemoveEdgesFromAndName(ctx context.Context, api *eos.API, 179 | // contract eos.AccountName, 180 | // fromHash eos.Checksum256, edgeName eos.Name) (string, error) { 181 | 182 | // actions := []*eos.Action{{ 183 | // Account: contract, 184 | // Name: eos.ActN("remedgesfn"), 185 | // Authorization: []eos.PermissionLevel{ 186 | // {Actor: contract, Permission: eos.PN("active")}, 187 | // }, 188 | // ActionData: eos.NewActionData(RemoveEdges{ 189 | // FromNode: fromHash, 190 | // EdgeName: edgeName, 191 | // Strict: true, 192 | // }), 193 | // }} 194 | // return eostest.ExecWithRetry(ctx, api, actions) 195 | // } 196 | 197 | // RemoveEdgesFromAndTo ... 198 | // func RemoveEdgesFromAndTo(ctx context.Context, api *eos.API, 199 | // contract eos.AccountName, 200 | // fromHash, toHash eos.Checksum256) (string, error) { 201 | 202 | // actions := []*eos.Action{{ 203 | // Account: contract, 204 | // Name: eos.ActN("remedgesft"), 205 | // Authorization: []eos.PermissionLevel{ 206 | // {Actor: contract, Permission: eos.PN("active")}, 207 | // }, 208 | // ActionData: eos.NewActionData(removeEdgesFT{ 209 | // FromNode: fromHash, 210 | // ToNode: toHash, 211 | // Strict: true, 212 | // }), 213 | // }} 214 | // return eostest.ExecWithRetry(ctx, api, actions) 215 | // } 216 | -------------------------------------------------------------------------------- /docgraph/edge_test.go: -------------------------------------------------------------------------------- 1 | package docgraph_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | eostest "github.com/digital-scarcity/eos-go-test" 8 | eos "github.com/eoscanada/eos-go" 9 | "github.com/hypha-dao/document-graph/docgraph" 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | func TestEdges(t *testing.T) { 14 | 15 | teardownTestCase := setupTestCase(t) 16 | defer teardownTestCase(t) 17 | env = SetupEnvironment(t) 18 | t.Log("\nEnvironment Setup complete\n") 19 | 20 | doc1, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], "examples/simplest.json") 21 | assert.NilError(t, err) 22 | 23 | doc2, err := docgraph.CreateDocument(env.ctx, &env.api, env.Docs, env.Creators[0], "examples/each-type.json") 24 | assert.NilError(t, err) 25 | 26 | // doc3, err := CreateDocument(env.ctx, &env.api, env.Docs, suite.Accounts[3], "examples/contribution.json") 27 | // suite.Require().NoError(err) 28 | 29 | tests := []struct { 30 | name string 31 | fromDoc docgraph.Document 32 | toDoc docgraph.Document 33 | creator eos.AccountName 34 | edgeName eos.Name 35 | }{ 36 | { 37 | name: "Test Edge 1", 38 | fromDoc: doc1, 39 | toDoc: doc2, 40 | creator: env.Creators[1], 41 | edgeName: "edge1", 42 | }, 43 | { 44 | name: "Test Edge 2", 45 | fromDoc: doc2, 46 | toDoc: doc1, 47 | creator: env.Creators[2], 48 | edgeName: "edge2", 49 | }, 50 | } 51 | 52 | for testIndex, test := range tests { 53 | t.Run("test edges", func(t *testing.T) { 54 | log.Println(test.name, "... ") 55 | 56 | _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[0], test.fromDoc.Hash, test.toDoc.Hash, test.edgeName) 57 | assert.NilError(t, err) 58 | 59 | // test number of edges 60 | edges, err := docgraph.GetAllEdges(env.ctx, &env.api, env.Docs) 61 | assert.NilError(t, err) 62 | assert.Equal(t, testIndex+1, len(edges)) 63 | 64 | // there should be 1 edge from doc1 to doc2, named edgeName 65 | edgesFrom, err := docgraph.GetEdgesFromDocument(env.ctx, &env.api, env.Docs, test.fromDoc) 66 | assert.NilError(t, err) 67 | assert.Equal(t, 1, len(edgesFrom)) 68 | assert.Equal(t, edgesFrom[0].EdgeName, test.edgeName) 69 | assert.Equal(t, edgesFrom[0].FromNode.String(), test.fromDoc.Hash.String()) 70 | assert.Equal(t, edgesFrom[0].ToNode.String(), test.toDoc.Hash.String()) 71 | 72 | // there should be 0 edges from doc2 to doc1 73 | edgesTo, err := docgraph.GetEdgesToDocument(env.ctx, &env.api, env.Docs, test.toDoc) 74 | assert.NilError(t, err) 75 | assert.Equal(t, 1, len(edgesTo)) 76 | assert.Equal(t, edgesTo[0].EdgeName, test.edgeName) 77 | assert.Equal(t, edgesTo[0].FromNode.String(), test.fromDoc.Hash.String()) 78 | assert.Equal(t, edgesTo[0].ToNode.String(), test.toDoc.Hash.String()) 79 | 80 | // there should be 1 edge from doc1 to doc2, named edgeName 81 | edgesFromByName, err := docgraph.GetEdgesFromDocumentWithEdge(env.ctx, &env.api, env.Docs, test.fromDoc, test.edgeName) 82 | assert.NilError(t, err) 83 | assert.Equal(t, 1, len(edgesFromByName)) 84 | assert.Equal(t, edgesFromByName[0].EdgeName, test.edgeName) 85 | assert.Equal(t, edgesFromByName[0].FromNode.String(), test.fromDoc.Hash.String()) 86 | assert.Equal(t, edgesFromByName[0].ToNode.String(), test.toDoc.Hash.String()) 87 | 88 | // there should be 1 edge from doc1 to doc2, named edgeName 89 | edgesToByName, err := docgraph.GetEdgesToDocumentWithEdge(env.ctx, &env.api, env.Docs, test.toDoc, test.edgeName) 90 | assert.NilError(t, err) 91 | assert.Equal(t, 1, len(edgesToByName)) 92 | assert.Equal(t, edgesToByName[0].EdgeName, test.edgeName) 93 | assert.Equal(t, edgesToByName[0].FromNode.String(), test.fromDoc.Hash.String()) 94 | assert.Equal(t, edgesToByName[0].ToNode.String(), test.toDoc.Hash.String()) 95 | 96 | // there should be 0 edge from doc1 to doc2, named wrongedge 97 | edgesFromByName, err = docgraph.GetEdgesFromDocumentWithEdge(env.ctx, &env.api, env.Docs, test.fromDoc, eos.Name("wrongedge")) 98 | assert.NilError(t, err) 99 | assert.Equal(t, 0, len(edgesFromByName)) 100 | 101 | // there should be 0 edge from doc1 to doc2, named edgeName 102 | edgesToByName, err = docgraph.GetEdgesToDocumentWithEdge(env.ctx, &env.api, env.Docs, test.toDoc, eos.Name("wrongedge")) 103 | assert.NilError(t, err) 104 | assert.Equal(t, 0, len(edgesToByName)) 105 | 106 | doesExist, err := docgraph.EdgeExists(env.ctx, &env.api, env.Docs, test.fromDoc, test.toDoc, test.edgeName) 107 | assert.NilError(t, err) 108 | assert.Assert(t, doesExist) 109 | 110 | doesNotExist, err := docgraph.EdgeExists(env.ctx, &env.api, env.Docs, test.fromDoc, test.toDoc, eos.Name("doesnotexist")) 111 | assert.NilError(t, err) 112 | assert.Assert(t, !doesNotExist) 113 | }) 114 | } 115 | } 116 | 117 | func TestRemoveEdges(t *testing.T) { 118 | 119 | teardownTestCase := setupTestCase(t) 120 | defer teardownTestCase(t) 121 | 122 | // var env Environment 123 | env = SetupEnvironment(t) 124 | t.Log("\nEnvironment Setup complete\n") 125 | 126 | // var docs []Document 127 | var err error 128 | docs := make([]docgraph.Document, 10) 129 | for i := 0; i < 10; i++ { 130 | docs[i], err = CreateRandomDocument(env.ctx, &env.api, env.Docs, env.Creators[1]) 131 | assert.NilError(t, err) 132 | } 133 | 134 | // *************************** BEGIN 135 | // test removal of edges based on the from_node and edge_name 136 | for i := 0; i < 5; i++ { 137 | _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], docs[0].Hash, docs[i].Hash, "test") 138 | assert.NilError(t, err) 139 | eostest.Pause(chainResponsePause, "Build block...", "") 140 | } 141 | 142 | allEdges, err := docgraph.GetAllEdges(env.ctx, &env.api, env.Docs) 143 | assert.NilError(t, err) 144 | assert.Equal(t, len(allEdges), 5) 145 | 146 | for i := 0; i < 5; i++ { 147 | checkEdge(t, env, docs[0], docs[i], eos.Name("test")) 148 | _, err = docgraph.RemoveEdge(env.ctx, &env.api, env.Docs, docs[0].Hash, docs[i].Hash, eos.Name("test")) 149 | assert.NilError(t, err) 150 | } 151 | 152 | allEdges, err = docgraph.GetAllEdges(env.ctx, &env.api, env.Docs) 153 | assert.NilError(t, err) 154 | assert.Equal(t, len(allEdges), 0) 155 | // ***************************** END 156 | 157 | // // ***************************** BEGIN 158 | // // test removal of edges based on the from_node and to_node 159 | // for i := 0; i < 3; i++ { 160 | // _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], docs[0].Hash, docs[1].Hash, eos.Name("test"+strconv.Itoa(i+1))) 161 | // assert.NilError(t, err) 162 | // pause(t, chainResponsePause, "Build block...", "") 163 | // } 164 | 165 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs) 166 | // assert.NilError(t, err) 167 | // assert.Equal(t, len(allEdges), 3) 168 | 169 | // for i := 0; i < 3; i++ { 170 | // checkEdge(t, env, docs[0], docs[1], eos.Name("test"+strconv.Itoa(i+1))) 171 | // } 172 | 173 | // // remove edges based on the from_node and edge_name 174 | // _, err = docgraph.RemoveEdgesFromAndTo(env.ctx, &env.api, env.Docs, docs[0].Hash, docs[1].Hash) 175 | // assert.NilError(t, err) 176 | 177 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs) 178 | // assert.NilError(t, err) 179 | // assert.Equal(t, len(allEdges), 0) 180 | // // ***************************** END 181 | 182 | // // *************************** BEGIN 183 | // // test removal of edges based on the testedge index action 184 | // for i := 0; i < 5; i++ { 185 | // _, err = docgraph.CreateEdge(env.ctx, &env.api, env.Docs, env.Creators[1], docs[0].Hash, docs[i].Hash, "test") 186 | // assert.NilError(t, err) 187 | // pause(t, chainResponsePause, "Build block...", "") 188 | // } 189 | 190 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs) 191 | // assert.NilError(t, err) 192 | // assert.Equal(t, len(allEdges), 5) 193 | 194 | // for i := 0; i < 5; i++ { 195 | // checkEdge(t, env, docs[0], docs[i], eos.Name("test")) 196 | // } 197 | 198 | // // remove edges based on the from_node and edge_name 199 | // _, err = EdgeIdxTest(env.ctx, &env.api, env.Docs, docs[0].Hash, eos.Name("test")) 200 | // assert.NilError(t, err) 201 | 202 | // allEdges, err = GetAllEdges(env.ctx, &env.api, env.Docs) 203 | // assert.NilError(t, err) 204 | // assert.Equal(t, len(allEdges), 0) 205 | // // ***************************** END 206 | } 207 | -------------------------------------------------------------------------------- /docgraph/environment_test.go: -------------------------------------------------------------------------------- 1 | package docgraph_test 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "testing" 7 | 8 | eostest "github.com/digital-scarcity/eos-go-test" 9 | 10 | eos "github.com/eoscanada/eos-go" 11 | "gotest.tools/assert" 12 | ) 13 | 14 | type Environment struct { 15 | ctx context.Context 16 | api eos.API 17 | 18 | Docs eos.AccountName 19 | Creators []eos.AccountName 20 | } 21 | 22 | func SetupEnvironment(t *testing.T) *Environment { 23 | 24 | var env Environment 25 | env.api = *eos.New(testingEndpoint) 26 | // api.Debug = true 27 | env.ctx = context.Background() 28 | 29 | keyBag := &eos.KeyBag{} 30 | err := keyBag.ImportPrivateKey(env.ctx, eostest.DefaultKey()) 31 | assert.NilError(t, err) 32 | 33 | env.api.SetSigner(keyBag) 34 | 35 | env.Docs, err = eostest.CreateAccountFromString(env.ctx, &env.api, "documents", eostest.DefaultKey()) 36 | assert.NilError(t, err) 37 | 38 | _, err = eostest.SetContract(env.ctx, &env.api, env.Docs, "../build/docs/docs.wasm", "../build/docs/docs.abi") 39 | assert.NilError(t, err) 40 | 41 | for i := 1; i < 5; i++ { 42 | 43 | creator, err := eostest.CreateAccountFromString(env.ctx, &env.api, "creator"+strconv.Itoa(i), eostest.DefaultKey()) 44 | assert.NilError(t, err) 45 | 46 | env.Creators = append(env.Creators, creator) 47 | } 48 | return &env 49 | } 50 | -------------------------------------------------------------------------------- /docgraph/examples/compliance-tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_groups": [ 3 | [ 4 | { 5 | "label": "content_group_label", 6 | "value": [ 7 | "string", 8 | "compliance" 9 | ] 10 | }, 11 | { 12 | "label": "compliance_tag", 13 | "value": [ 14 | "string", 15 | "translate-usd-to-tokens-v1.2.1" 16 | ] 17 | } 18 | ] 19 | ] 20 | } -------------------------------------------------------------------------------- /docgraph/examples/contribution.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_groups": [ 3 | [ 4 | { 5 | "label": "document_type", 6 | "value": [ 7 | "string", 8 | "contribution" 9 | ] 10 | }, 11 | { 12 | "label": "proposer", 13 | "value": [ 14 | "name", 15 | "johnnyhypha1" 16 | ] 17 | }, 18 | { 19 | "label": "title", 20 | "value": [ 21 | "string", 22 | "Development of Hypha Space Program" 23 | ] 24 | }, 25 | { 26 | "label": "description", 27 | "value": [ 28 | "string", 29 | "Contribution covers development and deployment of our first satellite" 30 | ] 31 | }, 32 | { 33 | "label": "seeds_amount", 34 | "value": [ 35 | "asset", 36 | "14000.0000 SEEDS" 37 | ] 38 | }, 39 | { 40 | "label": "hypha_amount", 41 | "value": [ 42 | "asset", 43 | "130.00 HYPHA" 44 | ] 45 | }, 46 | { 47 | "label": "hvoice_amount", 48 | "value": [ 49 | "asset", 50 | "130.00 HVOICE" 51 | ] 52 | }, 53 | { 54 | "label": "contribution_date", 55 | "value": [ 56 | "time_point", 57 | "2020-01-30T13:00:00.000" 58 | ] 59 | }, 60 | { 61 | "label": "deferred_perc_x100", 62 | "value": [ 63 | "int64", 64 | 100 65 | ] 66 | }, 67 | { 68 | "label": "", 69 | "value": [ 70 | "string", 71 | "no label string" 72 | ] 73 | }, 74 | { 75 | "label": "circle", 76 | "value": [ 77 | "checksum256", 78 | "d817d5149f3bd842b32868f9d579f83b8157f39ddd88c8c802bed900ce7b8856" 79 | ] 80 | } 81 | ] 82 | ] 83 | } -------------------------------------------------------------------------------- /docgraph/examples/each-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_groups": [ 3 | [ 4 | { 5 | "label": "content_group_label", 6 | "value": [ 7 | "string", 8 | "My Content Group Label" 9 | ] 10 | }, 11 | { 12 | "label": "salary_amount", 13 | "value": [ 14 | "asset", 15 | "130.00 USD" 16 | ] 17 | }, 18 | { 19 | "label": "referrer", 20 | "value": [ 21 | "name", 22 | "friendacct" 23 | ] 24 | }, 25 | { 26 | "label": "vote_count", 27 | "value": [ 28 | "int64", 29 | 67 30 | ] 31 | }, 32 | { 33 | "label": "reference_link", 34 | "value": [ 35 | "checksum256", 36 | "7b5755ce318c42fc750a754b4734282d1fad08e52c0de04762cb5f159a253c24" 37 | ] 38 | } 39 | ], 40 | [ 41 | { 42 | "label": "content_group_name", 43 | "value": [ 44 | "string", 45 | "My Content Group #2" 46 | ] 47 | }, 48 | { 49 | "label": "salary_amount", 50 | "value": [ 51 | "asset", 52 | "130.00 USD" 53 | ] 54 | }, 55 | { 56 | "label": "referrer", 57 | "value": [ 58 | "name", 59 | "friendacct" 60 | ] 61 | }, 62 | { 63 | "label": "vote_count", 64 | "value": [ 65 | "int64", 66 | 67 67 | ] 68 | }, 69 | { 70 | "label": "reference_link", 71 | "value": [ 72 | "checksum256", 73 | "7b5755ce318c42fc750a754b4734282d1fad08e52c0de04762cb5f159a253c24" 74 | ] 75 | } 76 | ] 77 | ] 78 | } -------------------------------------------------------------------------------- /docgraph/examples/simplest.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_groups": [ 3 | [ 4 | { 5 | "label": "simplest_label", 6 | "value": [ 7 | "string", 8 | "Simplest" 9 | ] 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /docgraph/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hypha-dao/document-graph/docgraph 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/digital-scarcity/eos-go-test v0.0.0-20210823154044-8e78e165bb94 7 | github.com/eoscanada/eos-go v0.9.1-0.20200805141443-a9d5402a7bc5 8 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 9 | github.com/schollz/progressbar/v3 v3.7.4 10 | github.com/stretchr/testify v1.7.0 11 | gotest.tools v2.2.0+incompatible 12 | gotest.tools/v3 v3.0.3 13 | ) 14 | -------------------------------------------------------------------------------- /docgraph/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= 5 | contrib.go.opencensus.io/exporter/stackdriver v0.12.6 h1:Y2FTyj0HgOhfjEW6D6ytZNoz1YcPDXmkKr1I478CWKs= 6 | contrib.go.opencensus.io/exporter/stackdriver v0.12.6/go.mod h1:8x999/OcIPy5ivx/wDiV7Gx4D+VUPODf0mWRGRc5kSk= 7 | github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= 8 | github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= 9 | github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4= 10 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 11 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 12 | github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= 13 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 14 | github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= 15 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 16 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 17 | github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= 18 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 19 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 20 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 21 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 22 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 23 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 24 | github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= 25 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 26 | github.com/VictoriaMetrics/fastcache v1.5.3/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE= 27 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 28 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 29 | github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= 30 | github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= 31 | github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 32 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 33 | github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= 34 | github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= 35 | github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ= 36 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 37 | github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= 38 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 39 | github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18/go.mod h1:HD5P3vAIAh+Y2GAxg0PrPN1P8WkepXGpjbUPDHJqqKM= 40 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 41 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 42 | github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U= 43 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 48 | github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 h1:CuJS05R9jmNlUK8GOxrEELPbfXm0EuGh/30LjkjN5vo= 49 | github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70/go.mod h1:EoK/8RFbMEteaCaz89uessDTnCWjbbcr+DXcBh4el5o= 50 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 51 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 52 | github.com/digital-scarcity/eos-go-test v0.0.0-20210823154044-8e78e165bb94 h1:pnDYNzAcP6L0lpfcY5kqhl+00CljL/PjLn9FTVuEV+8= 53 | github.com/digital-scarcity/eos-go-test v0.0.0-20210823154044-8e78e165bb94/go.mod h1:oLjNGc97WVqo8eAt2pTDEAw/7VDz7iwAsOrqSPOwqVA= 54 | github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 55 | github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 56 | github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa/go.mod h1:cdorVVzy1fhmEqmtgqkoE3bYtCfSCkVyjTyCIo22xvs= 57 | github.com/eoscanada/eos-go v0.9.1-0.20200227221642-1b19518201a1/go.mod h1:6RuJFiRU1figWZ39M33o2cERU2MdL6VllElYLHTZNeo= 58 | github.com/eoscanada/eos-go v0.9.1-0.20200805141443-a9d5402a7bc5 h1:5DSf1Fao5UAoX7RaWD/c08JJXjBygQYi549IMVBbFpo= 59 | github.com/eoscanada/eos-go v0.9.1-0.20200805141443-a9d5402a7bc5/go.mod h1:exxz2Fyjqx23FIYF1QlhhhggYZxcbZMGp2H/4h7I34Y= 60 | github.com/ethereum/go-ethereum v1.9.9/go.mod h1:a9TqabFudpDu1nucId+k9S8R9whYaHnGBLKFouA5EAo= 61 | github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 62 | github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= 63 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 64 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= 65 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 66 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 67 | github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= 68 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 69 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 70 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 71 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= 72 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 73 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 74 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 75 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 76 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 78 | github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 79 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 80 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 81 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 82 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 83 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 84 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 85 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 86 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 87 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 88 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 89 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 90 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 91 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 92 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 93 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 94 | github.com/gorilla/websocket v1.4.1-0.20190629185528-ae1634f6a989/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 95 | github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= 96 | github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 97 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 98 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 99 | github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 100 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 101 | github.com/huin/goupnp v0.0.0-20161224104101-679507af18f3/go.mod h1:MZ2ZmwcBpvOoJ22IJsc7va19ZwoheaBk43rKg12SKag= 102 | github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= 103 | github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 104 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 105 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 106 | github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 107 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= 108 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 109 | github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= 110 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 111 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 112 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 113 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 114 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 115 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 116 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 117 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 118 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 119 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 120 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 121 | github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= 122 | github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= 123 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 124 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 125 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 126 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 127 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 128 | github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= 129 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 130 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 131 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 132 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 133 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 134 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 135 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= 136 | github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= 137 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 138 | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 139 | github.com/olekukonko/tablewriter v0.0.2-0.20190409134802-7e037d187b0c/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 140 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 141 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 142 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 143 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 144 | github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= 145 | github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= 146 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 147 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 148 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 149 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 150 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 151 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 152 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 153 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 154 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 155 | github.com/prometheus/tsdb v0.6.2-0.20190402121629-4f204dcbc150/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 156 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 157 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 158 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 159 | github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= 160 | github.com/robertkrimen/otto v0.0.0-20170205013659-6a77b7cbc37d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= 161 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 162 | github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 163 | github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubrpZHtQLnOf6EwhN2hEMusxZOhcW9H3UQ= 164 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 165 | github.com/schollz/progressbar/v3 v3.7.4 h1:G2HfclnGJR2HtTOmFkERQcRqo9J20asOFiuD6AnI5EQ= 166 | github.com/schollz/progressbar/v3 v3.7.4/go.mod h1:1H8m5kMPW6q5fyjpDqtBHW1JT22mu2NwHQ1ApuCPh/8= 167 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 168 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 169 | github.com/spaolacci/murmur3 v1.0.1-0.20190317074736-539464a789e9/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 170 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 171 | github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= 172 | github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570/go.mod h1:8OR4w3TdeIHIh1g6EMY5p0gVNOovcWC+1vpc7naMuAw= 173 | github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3/go.mod h1:hpGUWaI9xL8pRQCTXQgocU38Qw1g0Us7n5PxxTwTCYU= 174 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 175 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 176 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 177 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 178 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 179 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 180 | github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= 181 | github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= 182 | github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= 183 | github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= 184 | github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= 185 | github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= 186 | github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 187 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= 188 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 189 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 190 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 191 | github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg= 192 | github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= 193 | github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= 194 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 195 | github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= 196 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 197 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 198 | go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= 199 | go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= 200 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 201 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 202 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 203 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 204 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 205 | go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 206 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 207 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 208 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 209 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 210 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 211 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 212 | go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 213 | go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= 214 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 215 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 216 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 217 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 218 | golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 219 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 220 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 221 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 222 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 223 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 224 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 225 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 226 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 227 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 228 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 229 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 230 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 231 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 232 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 233 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 234 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 235 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 236 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 237 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 238 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 239 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 240 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 241 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 242 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 243 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 244 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 247 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 248 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 249 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 250 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 251 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 252 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 253 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 254 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 255 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 256 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 257 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 258 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 262 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 263 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 264 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 265 | golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 266 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 267 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 268 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 270 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 272 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= 273 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 274 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 275 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 276 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 277 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 278 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 279 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 280 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 281 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 282 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 283 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 284 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 285 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 286 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 287 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 288 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 289 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 290 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 291 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 292 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 293 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 294 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 295 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 296 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 297 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 298 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 299 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 300 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 301 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 302 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 303 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 304 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 305 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 306 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 307 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 308 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 309 | google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 310 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 311 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 312 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 313 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 314 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 315 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 316 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 317 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 318 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 319 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 320 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 321 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 322 | gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190213234257-ec84240a7772/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= 323 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 324 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 325 | gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= 326 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 327 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 328 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 329 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 330 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 331 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 332 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 333 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 334 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 335 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 336 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 337 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 338 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 339 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 340 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 341 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 342 | -------------------------------------------------------------------------------- /docgraph/graph.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | 9 | eos "github.com/eoscanada/eos-go" 10 | ) 11 | 12 | var ( 13 | documents []Document 14 | edges []Edge 15 | nodes map[string]*Node 16 | ) 17 | 18 | // Graph ... 19 | type Graph struct { 20 | Nodes []*Node 21 | Edges []*Edge 22 | lock sync.RWMutex 23 | } 24 | 25 | // Node ... 26 | type Node struct { 27 | document *Document 28 | OutboundEdges map[eos.Name][]*Node 29 | InboundEdges map[eos.Name][]*Node 30 | } 31 | 32 | func (n *Node) getLabel() string { 33 | return n.document.GetNodeLabel() 34 | } 35 | 36 | // AddNode ... 37 | func (g *Graph) AddNode(n *Node) { 38 | log.Println("Adding node --- " + n.getLabel()) 39 | g.lock.Lock() 40 | g.Nodes = append(g.Nodes, n) 41 | nodes[n.document.Hash.String()] = n 42 | g.lock.Unlock() 43 | } 44 | 45 | // Connect adds an edge to the graph 46 | func (g *Graph) Connect(n1, n2 *Node, edgeName eos.Name) { 47 | log.Println("Connecting ---- " + n1.getLabel() + "--- " + string(edgeName) + " ---> " + n2.getLabel()) 48 | g.lock.Lock() 49 | 50 | if n1.OutboundEdges == nil { 51 | n1.OutboundEdges = make(map[eos.Name][]*Node) 52 | } 53 | 54 | if n2.InboundEdges == nil { 55 | n2.InboundEdges = make(map[eos.Name][]*Node) 56 | } 57 | 58 | n1.OutboundEdges[edgeName] = append(n1.OutboundEdges[edgeName], n2) 59 | n2.InboundEdges[edgeName] = append(n2.InboundEdges[edgeName], n1) 60 | 61 | g.lock.Unlock() 62 | } 63 | 64 | // LoadGraph ... 65 | func LoadGraph(ctx context.Context, api *eos.API, contract eos.AccountName) (*Graph, error) { 66 | 67 | graph := Graph{} 68 | 69 | edges, err := GetAllEdges(ctx, api, contract) 70 | if err != nil { 71 | return &graph, fmt.Errorf("cannot get all edges %v", err) 72 | } 73 | 74 | documents, err := GetAllDocuments(ctx, api, contract) 75 | if err != nil { 76 | return &graph, fmt.Errorf("cannot get all documents %v", err) 77 | } 78 | 79 | graph.Nodes = make([]*Node, len(documents)) 80 | graph.Edges = make([]*Edge, len(edges)) 81 | nodes = make(map[string]*Node) 82 | 83 | for _, doc := range documents { 84 | n := Node{document: &doc} 85 | graph.AddNode(&n) 86 | } 87 | 88 | for _, edge := range edges { 89 | graph.Connect(nodes[edge.FromNode.String()], nodes[edge.ToNode.String()], edge.EdgeName) 90 | } 91 | return &graph, nil 92 | } 93 | -------------------------------------------------------------------------------- /docgraph/graph_test.go: -------------------------------------------------------------------------------- 1 | package docgraph_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hypha-dao/document-graph/docgraph" 7 | ) 8 | 9 | func TestGraph(t *testing.T) { 10 | 11 | teardownTestCase := setupTestCase(t) 12 | defer teardownTestCase(t) 13 | 14 | env = SetupEnvironment(t) 15 | t.Log("\nEnvironment Setup complete\n") 16 | 17 | docgraph.LoadGraph(env.ctx, &env.api, env.Docs) 18 | } 19 | -------------------------------------------------------------------------------- /docgraph/helpers_test.go: -------------------------------------------------------------------------------- 1 | package docgraph_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "testing" 9 | "time" 10 | 11 | eostest "github.com/digital-scarcity/eos-go-test" 12 | eos "github.com/eoscanada/eos-go" 13 | "github.com/hypha-dao/document-graph/docgraph" 14 | "gotest.tools/assert" 15 | ) 16 | 17 | type createDoc struct { 18 | Creator eos.AccountName `json:"creator"` 19 | ContentGroups []docgraph.ContentGroup `json:"content_groups"` 20 | } 21 | 22 | type createRoot struct { 23 | Notes string `json:"notes"` 24 | } 25 | 26 | var seededRand *rand.Rand = rand.New( 27 | rand.NewSource(time.Now().UnixNano())) 28 | 29 | func stringWithCharset(length int, charset string) string { 30 | b := make([]byte, length) 31 | for i := range b { 32 | b[i] = charset[seededRand.Intn(len(charset))] 33 | } 34 | return string(b) 35 | } 36 | 37 | const charset = "abcdefghijklmnopqrstuvwxyz" + "12345" 38 | 39 | func randomString() string { 40 | return stringWithCharset(12, charset) 41 | } 42 | 43 | // GetOrNewNew creates a document with a single random value 44 | func GetOrNewNew(ctx context.Context, api *eos.API, contract, creator eos.AccountName, d docgraph.Document) (docgraph.Document, error) { 45 | 46 | actions := []*eos.Action{{ 47 | Account: contract, 48 | Name: eos.ActN("getornewnew"), 49 | Authorization: []eos.PermissionLevel{ 50 | {Actor: creator, Permission: eos.PN("active")}, 51 | }, 52 | ActionData: eos.NewActionData(createDoc{ 53 | Creator: creator, 54 | ContentGroups: d.ContentGroups, 55 | }), 56 | }} 57 | _, err := eostest.ExecWithRetry(ctx, api, actions) 58 | if err != nil { 59 | return docgraph.Document{}, fmt.Errorf("execute transaction getornewnew: %v", err) 60 | } 61 | 62 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract) 63 | if err != nil { 64 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err) 65 | } 66 | return lastDoc, nil 67 | } 68 | 69 | // GetOrNewGet creates a document with a single random value 70 | func GetOrNewGet(ctx context.Context, api *eos.API, contract, creator eos.AccountName, d docgraph.Document) (docgraph.Document, error) { 71 | 72 | actions := []*eos.Action{{ 73 | Account: contract, 74 | Name: eos.ActN("getornewget"), 75 | Authorization: []eos.PermissionLevel{ 76 | {Actor: creator, Permission: eos.PN("active")}, 77 | }, 78 | ActionData: eos.NewActionData(createDoc{ 79 | Creator: creator, 80 | ContentGroups: d.ContentGroups, 81 | }), 82 | }} 83 | _, err := eostest.ExecWithRetry(ctx, api, actions) 84 | if err != nil { 85 | return docgraph.Document{}, fmt.Errorf("execute transaction getornewnew: %v", err) 86 | } 87 | 88 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract) 89 | if err != nil { 90 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err) 91 | } 92 | return lastDoc, nil 93 | } 94 | 95 | type getAsset struct { 96 | Hash eos.Checksum256 `json:"hash"` 97 | GroupLabel string `json:"groupLabel"` 98 | ContentLabel string `json:"contentLabel"` 99 | ContentValue eos.Asset `json:"contentValue"` 100 | } 101 | 102 | type empty struct { 103 | Test string `json:"test"` 104 | } 105 | 106 | // GetAssetTest creates a document with a single random value 107 | func GetAssetTest(ctx context.Context, api *eos.API, contract eos.AccountName, d docgraph.Document, 108 | groupLabel, contentLabel string, contentValue eos.Asset) (string, error) { 109 | 110 | actions := []*eos.Action{{ 111 | Account: contract, 112 | Name: eos.ActN("testgetasset"), 113 | Authorization: []eos.PermissionLevel{ 114 | {Actor: contract, Permission: eos.PN("active")}, 115 | }, 116 | ActionData: eos.NewActionData(getAsset{ 117 | Hash: d.Hash, 118 | GroupLabel: groupLabel, 119 | ContentLabel: contentLabel, 120 | ContentValue: contentValue, 121 | }), 122 | }} 123 | return eostest.ExecWithRetry(ctx, api, actions) 124 | } 125 | 126 | type getGroup struct { 127 | Hash eos.Checksum256 `json:"hash"` 128 | GroupLabel string `json:"groupLabel"` 129 | } 130 | 131 | func GetGroupTest(ctx context.Context, api *eos.API, contract eos.AccountName, d docgraph.Document, groupLabel string) (string, error) { 132 | 133 | actions := []*eos.Action{{ 134 | Account: contract, 135 | Name: eos.ActN("testgetgroup"), 136 | Authorization: []eos.PermissionLevel{ 137 | {Actor: contract, Permission: eos.PN("active")}, 138 | }, 139 | ActionData: eos.NewActionData(getAsset{ 140 | Hash: d.Hash, 141 | GroupLabel: groupLabel, 142 | }), 143 | }} 144 | return eostest.ExecWithRetry(ctx, api, actions) 145 | } 146 | 147 | func ContentError(ctx context.Context, api *eos.API, contract eos.AccountName) (string, error) { 148 | actions := []*eos.Action{{ 149 | Account: contract, 150 | Name: eos.ActN("testcntnterr"), 151 | Authorization: []eos.PermissionLevel{ 152 | {Actor: contract, Permission: eos.PN("active")}, 153 | }, 154 | ActionData: eos.NewActionData(empty{ 155 | Test: "", 156 | }), 157 | }} 158 | return eostest.ExecTrx(ctx, api, actions) 159 | } 160 | 161 | func CreateRoot(ctx context.Context, api *eos.API, contract, creator eos.AccountName) (docgraph.Document, error) { 162 | actions := []*eos.Action{{ 163 | Account: contract, 164 | Name: eos.ActN("createroot"), 165 | Authorization: []eos.PermissionLevel{ 166 | {Actor: creator, Permission: eos.PN("active")}, 167 | }, 168 | ActionData: eos.NewActionData(createRoot{ 169 | Notes: "notes", 170 | }), 171 | }} 172 | _, err := eostest.ExecWithRetry(ctx, api, actions) 173 | if err != nil { 174 | return docgraph.Document{}, fmt.Errorf("execute create root: %v", err) 175 | } 176 | 177 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract) 178 | if err != nil { 179 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err) 180 | } 181 | return lastDoc, nil 182 | } 183 | 184 | // CreateRandomDocument creates a document with a single random value 185 | func CreateRandomDocument(ctx context.Context, api *eos.API, contract, creator eos.AccountName) (docgraph.Document, error) { 186 | 187 | var ci docgraph.ContentItem 188 | ci.Label = randomString() 189 | ci.Value = &docgraph.FlexValue{ 190 | BaseVariant: eos.BaseVariant{ 191 | TypeID: docgraph.FlexValueVariant.TypeID("name"), 192 | Impl: randomString(), 193 | }, 194 | } 195 | 196 | cg := make([]docgraph.ContentItem, 1) 197 | cg[0] = ci 198 | cgs := make([]docgraph.ContentGroup, 1) 199 | cgs[0] = cg 200 | 201 | actions := []*eos.Action{{ 202 | Account: contract, 203 | Name: eos.ActN("create"), 204 | Authorization: []eos.PermissionLevel{ 205 | {Actor: creator, Permission: eos.PN("active")}, 206 | }, 207 | ActionData: eos.NewActionData(createDoc{ 208 | Creator: creator, 209 | ContentGroups: cgs, 210 | }), 211 | }} 212 | _, err := eostest.ExecWithRetry(ctx, api, actions) 213 | if err != nil { 214 | return docgraph.Document{}, fmt.Errorf("execute transaction random document: %v", err) 215 | } 216 | 217 | lastDoc, err := docgraph.GetLastDocument(ctx, api, contract) 218 | if err != nil { 219 | return docgraph.Document{}, fmt.Errorf("get last document: %v", err) 220 | } 221 | return lastDoc, nil 222 | } 223 | 224 | func SaveGraph(ctx context.Context, api *eos.API, contract eos.AccountName, folderName string) error { 225 | 226 | var request eos.GetTableRowsRequest 227 | request.Code = string(contract) 228 | request.Scope = string(contract) 229 | request.Table = "documents" 230 | request.Limit = 1000 231 | request.JSON = true 232 | response, err := api.GetTableRows(ctx, request) 233 | if err != nil { 234 | return fmt.Errorf("Unable to retrieve rows: %v", err) 235 | } 236 | 237 | data, err := response.Rows.MarshalJSON() 238 | if err != nil { 239 | return fmt.Errorf("Unable to marshal json: %v", err) 240 | } 241 | 242 | documentsFile := folderName + "/documents.json" 243 | err = ioutil.WriteFile(documentsFile, data, 0644) 244 | if err != nil { 245 | return fmt.Errorf("Unable to write file: %v", err) 246 | } 247 | 248 | request = eos.GetTableRowsRequest{} 249 | request.Code = string(contract) 250 | request.Scope = string(contract) 251 | request.Table = "edges" 252 | request.Limit = 1000 253 | request.JSON = true 254 | response, err = api.GetTableRows(ctx, request) 255 | if err != nil { 256 | return fmt.Errorf("Unable to retrieve rows: %v", err) 257 | } 258 | 259 | data, err = response.Rows.MarshalJSON() 260 | if err != nil { 261 | return fmt.Errorf("Unable to marshal json: %v", err) 262 | } 263 | 264 | edgesFile := folderName + "/edges.json" 265 | err = ioutil.WriteFile(edgesFile, data, 0644) 266 | if err != nil { 267 | return fmt.Errorf("Unable to write file: %v", err) 268 | } 269 | 270 | return nil 271 | } 272 | 273 | func checkEdge(t *testing.T, env *Environment, fromEdge, toEdge docgraph.Document, edgeName eos.Name) { 274 | exists, err := docgraph.EdgeExists(env.ctx, &env.api, env.Docs, fromEdge, toEdge, edgeName) 275 | assert.NilError(t, err) 276 | if !exists { 277 | t.Log("Edge does not exist : ", fromEdge.Hash.String(), " -- ", edgeName, " --> ", toEdge.Hash.String()) 278 | } 279 | assert.Check(t, exists) 280 | } 281 | 282 | // this function/action will remove all edges with the from node and edge name 283 | func EdgeIdxTest(ctx context.Context, api *eos.API, 284 | contract eos.AccountName, 285 | fromHash eos.Checksum256, edgeName eos.Name) (string, error) { 286 | 287 | actions := []*eos.Action{{ 288 | Account: contract, 289 | Name: eos.ActN("testedgeidx"), 290 | Authorization: []eos.PermissionLevel{ 291 | {Actor: contract, Permission: eos.PN("active")}, 292 | }, 293 | ActionData: eos.NewActionData(docgraph.RemoveEdges{ 294 | FromNode: fromHash, 295 | EdgeName: edgeName, 296 | Strict: true, 297 | }), 298 | }} 299 | return eostest.ExecWithRetry(ctx, api, actions) 300 | } 301 | -------------------------------------------------------------------------------- /docgraph/nodeos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | nodeos -e -p eosio --plugin eosio::producer_plugin --max-transaction-time 300 --plugin eosio::producer_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::http_plugin --plugin eosio::history_plugin --plugin eosio::history_api_plugin --filter-on='*' --access-control-allow-origin='*' --contracts-console --http-validate-host=false --verbose-http-errors --delete-all-blocks -------------------------------------------------------------------------------- /docgraph/types_test.go: -------------------------------------------------------------------------------- 1 | package docgraph 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "testing" 7 | 8 | eos "github.com/eoscanada/eos-go" 9 | "github.com/stretchr/testify/require" 10 | "gotest.tools/assert" 11 | ) 12 | 13 | const testDocument = `{ 14 | "id": 24, 15 | "hash": "05e81010c4600ed5d978d2ddf22420ffdf6c4094f4b3822711f0596c7c342ccb", 16 | "creator": "johnnyhypha1", 17 | "content_groups": [[{ 18 | "label": "content_group_label", 19 | "value": [ 20 | "string", 21 | "details" 22 | ] 23 | },{ 24 | "label": "title", 25 | "value": [ 26 | "string", 27 | "Healer" 28 | ] 29 | },{ 30 | "label": "description", 31 | "value": [ 32 | "string", 33 | "Holder of indigenous wisdom ready to transfer the knowledge to others willing to receive" 34 | ] 35 | },{ 36 | "label": "icon", 37 | "value": [ 38 | "string", 39 | "https://myiconlink.com/fakelink" 40 | ] 41 | },{ 42 | "label": "seeds_coefficient_x10000", 43 | "value": [ 44 | "int64", 45 | 10010 46 | ] 47 | },{ 48 | "label": "hypha_coefficient_x10000", 49 | "value": [ 50 | "int64", 51 | 10015 52 | ] 53 | },{ 54 | "label": "hvoice_coefficient_x10000", 55 | "value": [ 56 | "int64", 57 | 10000 58 | ] 59 | },{ 60 | "label": "husd_coefficient_x10000", 61 | "value": [ 62 | "int64", 63 | 10100 64 | ] 65 | } 66 | ],[{ 67 | "label": "content_group_label", 68 | "value": [ 69 | "string", 70 | "system" 71 | ] 72 | },{ 73 | "label": "client_version", 74 | "value": [ 75 | "string", 76 | "1.0.13 0c81dde6" 77 | ] 78 | },{ 79 | "label": "contract_version", 80 | "value": [ 81 | "string", 82 | "1.0.1 366e8dfe" 83 | ] 84 | },{ 85 | "label": "ballot_id", 86 | "value": [ 87 | "name", 88 | "hypha1....14i" 89 | ] 90 | },{ 91 | "label": "proposer", 92 | "value": [ 93 | "name", 94 | "johnnyhypha1" 95 | ] 96 | },{ 97 | "label": "type", 98 | "value": [ 99 | "name", 100 | "badge" 101 | ] 102 | } 103 | ] 104 | ], 105 | "certificates": [ 106 | { 107 | "certifier": "dao.hypha", 108 | "notes": "certification notes", 109 | "certification_date": "2020-10-19T14:02:32.500" 110 | } 111 | ], 112 | "created_date": "2020-10-16T14:02:32.500" 113 | }` 114 | 115 | func TestEdgeJSONUnmarshal(t *testing.T) { 116 | input := `{ 117 | "id": 349057277, 118 | "from_node": "7463fa7dda551b9c4bbd2ba17b793931c825cefff9eede14461fd1a5c9f07d15", 119 | "to_node": "d4ec74355830056924c83f20ffb1a22ad0c5145a96daddf6301897a092de951e", 120 | "edge_name": "memberof", 121 | "created_date": "2020-10-16T14:11:37.000" 122 | }` 123 | 124 | var e Edge 125 | err := json.Unmarshal([]byte(input), &e) 126 | 127 | require.NoError(t, err) 128 | assert.Equal(t, e.ID, uint64(349057277), "id") 129 | assert.Equal(t, e.EdgeName, eos.Name("memberof"), "edge_name") 130 | assert.Equal(t, e.FromNode.String(), string("7463fa7dda551b9c4bbd2ba17b793931c825cefff9eede14461fd1a5c9f07d15"), "from_node") 131 | assert.Equal(t, e.ToNode.String(), string("d4ec74355830056924c83f20ffb1a22ad0c5145a96daddf6301897a092de951e"), "to_node") 132 | } 133 | 134 | func TestDocumentJSONUnmarshal(t *testing.T) { 135 | var d Document 136 | 137 | err := json.Unmarshal([]byte(testDocument), &d) 138 | 139 | require.NoError(t, err) 140 | assert.Equal(t, d.ID, uint64(24), "id") 141 | assert.Equal(t, d.Hash.String(), string("05e81010c4600ed5d978d2ddf22420ffdf6c4094f4b3822711f0596c7c342ccb"), "hash") 142 | assert.Equal(t, d.Creator, eos.AN("johnnyhypha1"), "creator") 143 | assert.Equal(t, len(d.ContentGroups), 2, "content groups length") 144 | assert.Equal(t, len(d.ContentGroups[0]), 8, "first content group length") 145 | assert.Equal(t, d.ContentGroups[0][1].Label, string("title"), "title label") 146 | assert.Equal(t, d.ContentGroups[0][1].Value.Impl, string("Healer"), "title value") 147 | } 148 | 149 | func TestGetContent(t *testing.T) { 150 | var d Document 151 | 152 | err := json.Unmarshal([]byte(testDocument), &d) 153 | 154 | require.NoError(t, err) 155 | 156 | content, err := d.GetContent("title") 157 | require.NoError(t, err) 158 | assert.Equal(t, content.Impl, string("Healer"), "get title content") 159 | 160 | content, err = d.GetContent("husd_coefficient_x10000") 161 | require.NoError(t, err) 162 | assert.Equal(t, content.Impl, int64(10100), "husd coefficient get content") 163 | } 164 | 165 | func TestGetContentNotFound(t *testing.T) { 166 | var d Document 167 | 168 | err := json.Unmarshal([]byte(testDocument), &d) 169 | 170 | require.NoError(t, err) 171 | 172 | content, err := d.GetContent("label not found") 173 | require.Error(t, &ContentNotFoundError{ 174 | Label: "label not found", 175 | DocumentHash: d.Hash, 176 | }) 177 | 178 | require.Nil(t, content) 179 | } 180 | 181 | func TestAddContent(t *testing.T) { 182 | var d Document 183 | 184 | err := json.Unmarshal([]byte(testDocument), &d) 185 | require.NoError(t, err) 186 | 187 | require.Equal(t, 8, len(d.ContentGroups[0])) 188 | 189 | hash := eos.Checksum256("7463fa7dda551b9c4bbd2ba17b793931c825cefff9eede14461fd1a5c9f07d15") 190 | 191 | fv := &FlexValue{ 192 | BaseVariant: eos.BaseVariant{ 193 | TypeID: FlexValueVariant.TypeID("checksum256"), 194 | Impl: hash, 195 | }} 196 | 197 | var ci ContentItem 198 | ci.Label = "badge" 199 | ci.Value = fv 200 | 201 | d.ContentGroups[0] = append(d.ContentGroups[0], ci) 202 | require.Equal(t, 9, len(d.ContentGroups[0])) 203 | } 204 | 205 | func TestExamplePayloads(t *testing.T) { 206 | // var d Document 207 | var testFile Document 208 | 209 | tests := []struct { 210 | name string 211 | input string 212 | }{ 213 | { 214 | name: "simplest", 215 | input: "examples/simplest.json", 216 | }, 217 | { 218 | name: "each-type", 219 | input: "examples/each-type.json", 220 | }, 221 | { 222 | name: "contribution", 223 | input: "examples/contribution.json", 224 | }, 225 | } 226 | 227 | for _, test := range tests { 228 | t.Run(test.name, func(t *testing.T) { 229 | 230 | data, err := ioutil.ReadFile(test.input) 231 | require.NoError(t, err) 232 | // log.Println(string(data)) 233 | 234 | err = json.Unmarshal(data, &testFile) 235 | require.NoError(t, err) 236 | }) 237 | } 238 | } 239 | 240 | func TestDocumentEquality(t *testing.T) { 241 | var d1, d2 Document 242 | 243 | tests := []struct { 244 | name string 245 | input string 246 | }{ 247 | { 248 | name: "simplest", 249 | input: "examples/simplest.json", 250 | }, 251 | { 252 | name: "each-type", 253 | input: "examples/each-type.json", 254 | }, 255 | { 256 | name: "contribution", 257 | input: "examples/contribution.json", 258 | }, 259 | } 260 | 261 | for _, test := range tests { 262 | t.Run(test.name, func(t *testing.T) { 263 | 264 | data, err := ioutil.ReadFile(test.input) 265 | require.NoError(t, err) 266 | // log.Println(string(data)) 267 | 268 | err = json.Unmarshal(data, &d1) 269 | require.NoError(t, err) 270 | 271 | err = json.Unmarshal(data, &d2) 272 | require.NoError(t, err) 273 | 274 | require.True(t, d1.IsEqual(d2), "documents are not equal") 275 | }) 276 | } 277 | } 278 | 279 | func TestDocumentInEquality(t *testing.T) { 280 | var d1, d2 Document 281 | 282 | tests := []struct { 283 | name string 284 | input1 string 285 | input2 string 286 | }{ 287 | { 288 | name: "simplest vs each-type", 289 | input1: "examples/simplest.json", 290 | input2: "examples/each-type.json", 291 | }, 292 | { 293 | name: "each-type vs contribution", 294 | input1: "examples/each-type.json", 295 | input2: "examples/contribution.json", 296 | }, 297 | { 298 | name: "contribution vs simplest", 299 | input1: "examples/contribution.json", 300 | input2: "examples/simplest.json", 301 | }, 302 | } 303 | 304 | for _, test := range tests { 305 | t.Run(test.name, func(t *testing.T) { 306 | 307 | data1, err := ioutil.ReadFile(test.input1) 308 | require.NoError(t, err) 309 | 310 | err = json.Unmarshal(data1, &d1) 311 | require.NoError(t, err) 312 | 313 | data2, err := ioutil.ReadFile(test.input2) 314 | require.NoError(t, err) 315 | 316 | err = json.Unmarshal(data2, &d2) 317 | require.NoError(t, err) 318 | 319 | require.False(t, d1.IsEqual(d2), "documents are equal") 320 | }) 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /include/docs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | using namespace eosio; 13 | 14 | namespace hypha 15 | { 16 | CONTRACT docs : public contract 17 | { 18 | public: 19 | docs(name self, name code, datastream ds); 20 | ~docs(); 21 | 22 | DECLARE_DOCUMENT_GRAPH(docs) 23 | 24 | // Any account/member can creator a new document 25 | ACTION create(eosio::name & creator, ContentGroups & content_groups); 26 | ACTION createroot(const std::string ¬es); 27 | 28 | ACTION getornewget(const name &creator, ContentGroups &content_groups); 29 | ACTION getornewnew(const name &creator, ContentGroups &content_groups); 30 | 31 | ACTION newedge(eosio::name & creator, const checksum256 &from_node, const checksum256 &to_node, const name &edge_name); 32 | 33 | ACTION removeedge(const checksum256 &from_node, const checksum256 &to_node, const name &edge_name); 34 | 35 | ACTION erase(const checksum256 &hash); 36 | 37 | ACTION testgetasset(const checksum256 &hash, 38 | const std::string &groupLabel, 39 | const std::string &contentLabel, 40 | const asset &contentValue); 41 | 42 | ACTION testgetgroup(const checksum256 &hash, 43 | const std::string &groupLabel); 44 | 45 | ACTION testcntnterr(string test); 46 | 47 | // // Fork creates a new document (node in a graph) from an existing document. 48 | // // The forked content should contain only new or updated entries to avoid data duplication. (lazily enforced?) 49 | // ACTION fork(const checksum256 &hash, const name &creator, const vector &content_groups); 50 | 51 | // Creates a 'certificate' on a specific fork. 52 | // A certificate can be customized based on the document, but it represents 53 | // the signatures, with notes/timestamp, and of course auth is enforced 54 | // ACTION certify(const name &certifier, const checksum256 &hash, const std::string ¬es); 55 | 56 | // // debug only: deletes all docs 57 | // ACTION reset(); 58 | 59 | private: 60 | DocumentGraph m_dg = DocumentGraph(get_self()); 61 | }; 62 | } // namespace hypha -------------------------------------------------------------------------------- /include/document_graph/content.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace hypha 12 | { 13 | struct Content 14 | { 15 | typedef std::variant 17 | FlexValue; 18 | 19 | public: 20 | Content(); 21 | Content(std::string label, FlexValue value); 22 | ~Content(); 23 | 24 | const bool isEmpty() const; 25 | 26 | const std::string toString() const; 27 | 28 | // NOTE: not using m_ notation because this changes serialization format 29 | std::string label; 30 | FlexValue value; 31 | 32 | //Can return reference to stored type 33 | template 34 | inline decltype(auto) getAs() 35 | { 36 | EOS_CHECK(std::holds_alternative(value), "Content value for label [" + label + "] is not of expected type"); 37 | return std::get(value); 38 | } 39 | 40 | template 41 | inline decltype(auto) getAs() const 42 | { 43 | EOS_CHECK(std::holds_alternative(value), "Content value for label [" + label + "] is not of expected type"); 44 | return std::get(value); 45 | } 46 | 47 | inline bool operator==(const Content& other) 48 | { 49 | return label == other.label && value == other.value; 50 | } 51 | 52 | EOSLIB_SERIALIZE(Content, (label)(value)) 53 | }; 54 | 55 | } // namespace hypha -------------------------------------------------------------------------------- /include/document_graph/content_wrapper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | 6 | using std::string; 7 | using std::string_view; 8 | 9 | namespace hypha 10 | { 11 | using ContentGroup = std::vector; 12 | using ContentGroups = std::vector; 13 | 14 | static const std::string CONTENT_GROUP_LABEL = std::string("content_group_label"); 15 | 16 | class ContentWrapper 17 | { 18 | 19 | public: 20 | ContentWrapper(ContentGroups &cgs); 21 | ~ContentWrapper(); 22 | 23 | // non-static definitions 24 | std::pair getGroup(const std::string &label); 25 | std::pair getGroupOrCreate(const string& label); 26 | ContentGroup *getGroupOrFail(const std::string &label, const std::string &error); 27 | ContentGroup *getGroupOrFail(const std::string &groupLabel); 28 | 29 | std::pair get(const std::string &groupLabel, const std::string &contentLabel); 30 | //Looks for content in an specific group index 31 | std::pair get(size_t groupIndex, const std::string &contentLabel); 32 | Content *getOrFail(const std::string &groupLabel, const std::string &contentLabel, const std::string &error); 33 | Content *getOrFail(const std::string &groupLabel, const std::string &contentLabel); 34 | std::pair getOrFail(size_t groupIndex, const string &contentLabel, string_view error = string_view{}); 35 | 36 | 37 | void removeGroup(const std::string &groupLabel); 38 | void removeGroup(size_t groupIndex); 39 | 40 | //Deletes the first instance of a content with the same value 41 | void removeContent(const std::string& groupLabel, const Content& content); 42 | void removeContent(const std::string &groupLabel, const std::string &contentLabel); 43 | void removeContent(size_t groupIndex, const std::string &contentLabel); 44 | void removeContent(size_t groupIndex, size_t contentIndex); 45 | 46 | void insertOrReplace(size_t groupIndex, const Content &newContent); 47 | 48 | bool exists(const std::string &groupLabel, const std::string &contentLabel); 49 | 50 | string_view getGroupLabel(size_t groupIndex); 51 | 52 | static string_view getGroupLabel(const ContentGroup &contentGroup); 53 | static void insertOrReplace(ContentGroup &contentGroup, const Content &newContent); 54 | 55 | ContentGroups &getContentGroups() { return m_contentGroups; } 56 | 57 | private: 58 | ContentGroups &m_contentGroups; 59 | }; 60 | 61 | } // namespace hypha -------------------------------------------------------------------------------- /include/document_graph/document.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace hypha 15 | { 16 | // unused for now, but leaving in the data structure for the future 17 | struct Certificate 18 | { 19 | Certificate() {} 20 | Certificate(const eosio::name &certifier, const std::string notes) : certifier{certifier}, notes{notes} {} 21 | 22 | eosio::name certifier; 23 | std::string notes; 24 | eosio::time_point certification_date = eosio::current_time_point(); 25 | 26 | EOSLIB_SERIALIZE(Certificate, (certifier)(notes)(certification_date)) 27 | }; 28 | 29 | struct Document 30 | { 31 | public: 32 | Document(); 33 | 34 | // these constructors populate a Document instance and emplace 35 | Document(eosio::name contract, eosio::name creator, ContentGroups contentGroups); 36 | Document(eosio::name contract, eosio::name creator, ContentGroup contentGroup); 37 | Document(eosio::name contract, eosio::name creator, Content content); 38 | Document(eosio::name contract, eosio::name creator, const std::string &label, const Content::FlexValue &value); 39 | 40 | // this constructor reads the hash from the table and populates the object from storage 41 | Document(eosio::name contract, const eosio::checksum256 &hash); 42 | ~Document(); 43 | 44 | void emplace(); 45 | 46 | // returns a document, saves to RAM if it doesn't already exist 47 | static Document getOrNew(eosio::name contract, eosio::name creator, ContentGroups contentGroups); 48 | static Document getOrNew(eosio::name contract, eosio::name creator, ContentGroup contentGroup); 49 | static Document getOrNew(eosio::name contract, eosio::name creator, Content content); 50 | static Document getOrNew(eosio::name contract, eosio::name creator, const std::string &label, const Content::FlexValue &value); 51 | 52 | static bool exists(eosio::name contract, const eosio::checksum256 &hash); 53 | 54 | // certificates are not yet used 55 | void certify(const eosio::name &certifier, const std::string ¬es); 56 | 57 | const void hashContents(); 58 | 59 | // static helpers 60 | static const eosio::checksum256 hashContents(const ContentGroups &contentGroups); 61 | static ContentGroups rollup(ContentGroup contentGroup); 62 | static ContentGroups rollup(Content content); 63 | static void insertOrReplace(ContentGroup &contentGroup, Content &newContent); 64 | 65 | static Document merge(Document original, Document &deltas); 66 | 67 | // vanilla accessors 68 | ContentWrapper getContentWrapper() { return ContentWrapper(content_groups); } 69 | ContentGroups &getContentGroups() { return content_groups; } 70 | const ContentGroups &getContentGroups() const { return content_groups; } 71 | const eosio::checksum256 &getHash() const { return hash; } 72 | const eosio::time_point &getCreated() const { return created_date; } 73 | const eosio::name &getCreator() const { return creator; } 74 | const eosio::name &getContract() const { return contract; } 75 | 76 | // This has to be public in order to be reachable by the abi-generator macro 77 | // indexes for table 78 | uint64_t by_created() const { return created_date.sec_since_epoch(); } 79 | uint64_t by_creator() const { return creator.value; } 80 | eosio::checksum256 by_hash() const { return hash; } 81 | 82 | private: 83 | // members, with names as serialized - these must be public for EOSIO tables 84 | std::uint64_t id; 85 | eosio::checksum256 hash; 86 | eosio::name creator; 87 | ContentGroups content_groups; 88 | std::vector certificates; 89 | eosio::time_point created_date; 90 | eosio::name contract; 91 | 92 | // toString iterates through all content, all levels, concatenating all values 93 | // the resulting string is used for fingerprinting and hashing 94 | const std::string toString(); 95 | static const std::string toString(const ContentGroups &contentGroups); 96 | static const std::string toString(const ContentGroup &contentGroup); 97 | 98 | EOSLIB_SERIALIZE(Document, (id)(hash)(creator)(content_groups)(certificates)(created_date)(contract)) 99 | 100 | public: 101 | // for unknown reason, primary_key() must be public 102 | uint64_t primary_key() const { return id; } 103 | 104 | typedef eosio::multi_index>, 106 | eosio::indexed_by>, 107 | eosio::indexed_by>> 108 | document_table; 109 | }; 110 | 111 | } // namespace hypha -------------------------------------------------------------------------------- /include/document_graph/document_graph.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | namespace hypha 17 | { 18 | class DocumentGraph 19 | { 20 | public: 21 | DocumentGraph(const eosio::name &contract) : m_contract(contract) {} 22 | ~DocumentGraph() {} 23 | 24 | void removeEdges(const eosio::checksum256 &node); 25 | 26 | std::vector getEdges(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode); 27 | std::vector getEdgesOrFail(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode); 28 | 29 | std::vector getEdgesFrom(const eosio::checksum256 &fromNode, const eosio::name &edgeName); 30 | std::vector getEdgesFromOrFail(const eosio::checksum256 &fromNode, const eosio::name &edgeName); 31 | 32 | std::vector getEdgesTo(const eosio::checksum256 &toNode, const eosio::name &edgeName); 33 | std::vector getEdgesToOrFail(const eosio::checksum256 &toNode, const eosio::name &edgeName); 34 | 35 | Edge createEdge(eosio::name &creator, const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode, const eosio::name &edgeName); 36 | 37 | Document updateDocument(const eosio::name &updater, 38 | const eosio::checksum256 &doc_hash, 39 | ContentGroups content_groups); 40 | 41 | bool hasEdges(const eosio::checksum256 &node); 42 | 43 | void replaceNode(const eosio::checksum256 &oldNode, const eosio::checksum256 &newNode); 44 | void eraseDocument(const eosio::checksum256 &document_hash); 45 | void eraseDocument(const eosio::checksum256 &document_hash, const bool includeEdges); 46 | 47 | private: 48 | eosio::name m_contract; 49 | }; 50 | }; // namespace hypha 51 | 52 | #define DECLARE_DOCUMENT_GRAPH(contract)\ 53 | using FlexValue = hypha::Content::FlexValue;\ 54 | using root_doc = hypha::Document;\ 55 | TABLE contract##_document : public root_doc {};\ 56 | using contract_document = contract##_document;\ 57 | using document_table = eosio::multi_index>,\ 59 | eosio::indexed_by>,\ 60 | eosio::indexed_by>>;\ 61 | using root_edge = hypha::Edge;\ 62 | TABLE contract##_edge : public root_edge {};\ 63 | using contract_edge = contract##_edge;\ 64 | using edge_table = eosio::multi_index>,\ 66 | eosio::indexed_by>,\ 67 | eosio::indexed_by>,\ 68 | eosio::indexed_by>,\ 69 | eosio::indexed_by>,\ 70 | eosio::indexed_by>,\ 71 | eosio::indexed_by>,\ 72 | eosio::indexed_by>>; -------------------------------------------------------------------------------- /include/document_graph/edge.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace hypha 9 | { 10 | struct Edge 11 | { 12 | Edge(); 13 | Edge(const eosio::name &contract, const eosio::name &creator, const eosio::checksum256 &fromNode, 14 | const eosio::checksum256 &toNode, const eosio::name &edgeName); 15 | ~Edge(); 16 | 17 | void emplace(); 18 | void erase(); 19 | 20 | const eosio::checksum256 &getFromNode() { return from_node; } 21 | const eosio::checksum256 &getToNode() { return to_node; } 22 | const eosio::name &getEdgeName() { return edge_name; } 23 | const eosio::time_point &getCreated() { return created_date; } 24 | const eosio::name &getCreator() { return creator; } 25 | const eosio::name &getContract() { return contract; } 26 | 27 | static Edge getOrNew(const eosio::name &contract, 28 | const eosio::name &creator, 29 | const eosio::checksum256 &from_node, 30 | const eosio::checksum256 &to_node, 31 | const eosio::name &edge_name); 32 | 33 | static void write(const eosio::name &_contract, 34 | const eosio::name &_creator, 35 | const eosio::checksum256 &_from_node, 36 | const eosio::checksum256 &_to_node, 37 | const eosio::name &_edge_name); 38 | 39 | static Edge get(const eosio::name &contract, 40 | const eosio::checksum256 &from_node, 41 | const eosio::checksum256 &to_node, 42 | const eosio::name &edge_name); 43 | 44 | static std::pair getIfExists(const eosio::name &_contract, 45 | const eosio::checksum256 &_from_node, 46 | const eosio::name &_edge_name); 47 | 48 | static Edge get(const eosio::name &contract, 49 | const eosio::checksum256 &from_node, 50 | const eosio::name &edge_name); 51 | 52 | static Edge getTo(const eosio::name &contract, 53 | const eosio::checksum256 &to_node, 54 | const eosio::name &edge_name); 55 | 56 | static bool exists(const eosio::name &_contract, 57 | const eosio::checksum256 &_from_node, 58 | const eosio::checksum256 &_to_node, 59 | const eosio::name &_edge_name); 60 | 61 | uint64_t id; // hash of from_node, to_node, and edge_name 62 | 63 | // these three additional indexes allow isolating/querying edges more precisely (less iteration) 64 | uint64_t from_node_edge_name_index; 65 | uint64_t from_node_to_node_index; 66 | uint64_t to_node_edge_name_index; 67 | 68 | // these members should be private, but they are used in DocumentGraph for edge replacement logic 69 | eosio::checksum256 from_node; 70 | eosio::checksum256 to_node; 71 | eosio::name edge_name; 72 | eosio::time_point created_date; 73 | eosio::name creator; 74 | eosio::name contract; 75 | 76 | uint64_t primary_key() const; 77 | uint64_t by_from_node_edge_name_index() const; 78 | uint64_t by_from_node_to_node_index() const; 79 | uint64_t by_to_node_edge_name_index() const; 80 | uint64_t by_edge_name() const; 81 | uint64_t by_created() const; 82 | uint64_t by_creator() const; 83 | eosio::checksum256 by_from() const; 84 | eosio::checksum256 by_to() const; 85 | 86 | EOSLIB_SERIALIZE(Edge, (id)(from_node_edge_name_index)(from_node_to_node_index)(to_node_edge_name_index)(from_node)(to_node)(edge_name)(created_date)(creator)(contract)) 87 | 88 | typedef eosio::multi_index>, 90 | eosio::indexed_by>, 91 | eosio::indexed_by>, 92 | eosio::indexed_by>, 93 | eosio::indexed_by>, 94 | eosio::indexed_by>, 95 | eosio::indexed_by>, 96 | eosio::indexed_by>> 97 | edge_table; 98 | }; 99 | 100 | } // namespace hypha -------------------------------------------------------------------------------- /include/document_graph/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace hypha 8 | { 9 | 10 | const std::string toHex(const char *d, std::uint32_t s); 11 | const std::string readableHash(const eosio::checksum256 &hash); 12 | const std::uint64_t toUint64(const std::string &fingerprint); 13 | const std::uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2, const eosio::name label); 14 | const std::uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2); 15 | const std::uint64_t concatHash(const eosio::checksum256 sha, const eosio::name label); 16 | 17 | namespace util 18 | { 19 | namespace detail 20 | { 21 | 22 | template 23 | struct supports_to_string 24 | { 25 | template 26 | static auto can_pass_to_string(const U* arg) -> decltype(std::to_string(*arg), char(0)) 27 | {} 28 | 29 | static std::array can_pass_to_string(...) { } 30 | 31 | static constexpr bool value = (sizeof(can_pass_to_string((T*)0)) == 1); 32 | }; 33 | 34 | template 35 | struct supports_call_to_string 36 | { 37 | template 38 | static auto can_pass_to_string(const U* arg) -> decltype(arg->to_string(), char(0)) 39 | {} 40 | 41 | static std::array can_pass_to_string(...) { } 42 | 43 | static constexpr bool value = (sizeof(can_pass_to_string((T*)0)) == 1); 44 | }; 45 | 46 | template 47 | std::string to_str_h(const T& arg) 48 | { 49 | if constexpr (supports_to_string::value) { 50 | return std::to_string(arg); 51 | } 52 | else if constexpr (supports_call_to_string::value) { 53 | return arg.to_string(); 54 | } 55 | else if constexpr (std::is_same_v) { 56 | return readableHash(arg); 57 | } 58 | else if constexpr (std::is_same_v) { 59 | 60 | std::string s; 61 | 62 | s = "ContentGroup {\n"; 63 | 64 | for (auto& content : arg) { 65 | s += "\tContent " + content.toString() + "\n"; 66 | } 67 | s += "}\n"; 68 | 69 | return s; 70 | } 71 | else { 72 | return arg; 73 | } 74 | } 75 | } 76 | 77 | //Helper function to convert 1+ X type variables to string 78 | template 79 | std::string to_str(const T& first, const Args&... others) 80 | { 81 | return (detail::to_str_h(first) + ... + detail::to_str_h(others)); 82 | } 83 | 84 | } 85 | 86 | } // namespace hypha -------------------------------------------------------------------------------- /include/logger/boost_current_function.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED 2 | #define BOOST_CURRENT_FUNCTION_HPP_INCLUDED 3 | 4 | // MS compatible compilers support #pragma once 5 | 6 | #if defined(_MSC_VER) && (_MSC_VER >= 1020) 7 | # pragma once 8 | #endif 9 | 10 | // 11 | // boost/current_function.hpp - BOOST_CURRENT_FUNCTION 12 | // 13 | // Copyright (c) 2002 Peter Dimov and Multi Media Ltd. 14 | // 15 | // Distributed under the Boost Software License, Version 1.0. 16 | // See accompanying file LICENSE_1_0.txt or copy at 17 | // http://www.boost.org/LICENSE_1_0.txt 18 | // 19 | // http://www.boost.org/libs/assert/current_function.html 20 | // 21 | 22 | namespace boost 23 | { 24 | 25 | namespace detail 26 | { 27 | 28 | inline void current_function_helper() 29 | { 30 | 31 | #if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || (defined(__ICC) && (__ICC >= 600)) || defined(__ghs__) 32 | 33 | # define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__ 34 | 35 | #elif defined(__DMC__) && (__DMC__ >= 0x810) 36 | 37 | # define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__ 38 | 39 | #elif defined(__FUNCSIG__) 40 | 41 | # define BOOST_CURRENT_FUNCTION __FUNCSIG__ 42 | 43 | #elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || (defined(__IBMCPP__) && (__IBMCPP__ >= 500)) 44 | 45 | # define BOOST_CURRENT_FUNCTION __FUNCTION__ 46 | 47 | #elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550) 48 | 49 | # define BOOST_CURRENT_FUNCTION __FUNC__ 50 | 51 | #elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901) 52 | 53 | # define BOOST_CURRENT_FUNCTION __func__ 54 | 55 | #elif defined(__cplusplus) && (__cplusplus >= 201103) 56 | 57 | # define BOOST_CURRENT_FUNCTION __func__ 58 | 59 | #else 60 | 61 | # define BOOST_CURRENT_FUNCTION "(unknown)" 62 | 63 | #endif 64 | 65 | } 66 | 67 | } // namespace detail 68 | 69 | } // namespace boost 70 | 71 | #endif // #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED -------------------------------------------------------------------------------- /include/logger/logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "document_graph/util.hpp" 9 | 10 | namespace hypha 11 | { 12 | 13 | class Logger 14 | { 15 | public: 16 | 17 | static Logger& 18 | instance(); 19 | 20 | void 21 | pushTrace(std::string trace); 22 | 23 | void 24 | popTrace(); 25 | 26 | void 27 | pushMessage(std::string message); 28 | 29 | std::string 30 | generateMessage(); 31 | 32 | private: 33 | 34 | std::vector m_trace; 35 | std::vector m_log; 36 | }; 37 | 38 | #ifdef USE_LOGGING 39 | #include "boost_current_function.hpp" 40 | 41 | class AutoTraceDroper 42 | { 43 | public: 44 | AutoTraceDroper() {} 45 | 46 | ~AutoTraceDroper() { Logger::instance().popTrace(); } 47 | }; 48 | 49 | #define TRACE_FUNCTION() AutoTraceDroper autoDrop##__LINE__{}; Logger::instance().pushTrace(util::to_str(__FILE__, " : ", BOOST_CURRENT_FUNCTION)); 50 | #define TRACE_ERROR(message) AutoTraceDroper autoDrop##__LINE__{}; Logger::instance().pushTrace(util::to_str(__FILE__, ":", __LINE__, ": ", message)); 51 | #define LOG_MESSAGE(message) Logger::instance().pushMessage(util::to_str("[DEBUG]: ", __FILE__, ":", __LINE__, ": ", message)); 52 | #define EOS_CHECK(condition, message)\ 53 | {\ 54 | if (!(condition)) {\ 55 | TRACE_ERROR(message)\ 56 | eosio::check(false, Logger::instance().generateMessage());\ 57 | }\ 58 | } 59 | #else 60 | #define TRACE_FUNCTION() 61 | #define TRACE_ERROR(message) 62 | #define LOG_MESSAGE(message) 63 | #define EOS_CHECK(condition, message) eosio::check((condition), message); 64 | #endif 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /ricardian/docs.contracts.md: -------------------------------------------------------------------------------- 1 |

hi

2 | 3 | Stub for hi action's ricardian contract -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(docs) 2 | cmake_minimum_required(VERSION 3.17) 3 | 4 | set(EOSIO_WASM_OLD_BEHAVIOR "Off") 5 | find_package(eosio.cdt) 6 | 7 | add_contract( docs docs 8 | docs.cpp 9 | document_graph/util.cpp 10 | document_graph/content.cpp 11 | document_graph/content_wrapper.cpp 12 | document_graph/document.cpp 13 | document_graph/document_graph.cpp 14 | document_graph/edge.cpp ) 15 | 16 | target_include_directories( docs PUBLIC ${CMAKE_SOURCE_DIR}/../include ) 17 | -------------------------------------------------------------------------------- /src/docs.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace hypha 4 | { 5 | 6 | docs::docs(name self, name code, datastream ds) : contract(self, code, ds) {} 7 | docs::~docs() {} 8 | 9 | void docs::create(name &creator, ContentGroups &content_groups) 10 | { 11 | Document document(get_self(), creator, content_groups); 12 | } 13 | 14 | void docs::getornewget(const name &creator, ContentGroups &content_groups) 15 | { 16 | Document document = Document::getOrNew(get_self(), creator, content_groups); 17 | eosio::check(document.getCreated().sec_since_epoch() > 0, "created new instead of reading from existing"); 18 | } 19 | 20 | void docs::getornewnew(const name &creator, ContentGroups &content_groups) 21 | { 22 | bool docExists = Document::exists(get_self(), Document::hashContents(content_groups)); 23 | check(!docExists, "document already exists"); 24 | 25 | Document document = Document::getOrNew(get_self(), creator, content_groups); 26 | eosio::check(document.getCreated().sec_since_epoch() > 0, "created_date not populated when saved"); 27 | } 28 | 29 | void docs::newedge(name &creator, const checksum256 &from_node, const checksum256 &to_node, const name &edge_name) 30 | { 31 | Edge edge(get_self(), creator, from_node, to_node, edge_name); 32 | } 33 | 34 | void docs::removeedge(const checksum256 &from_node, const checksum256 &to_node, const name &edge_name) 35 | { 36 | Edge edge = Edge::get(get_self(), from_node, to_node, edge_name); 37 | edge.erase(); 38 | } 39 | 40 | void docs::erase(const checksum256 &hash) 41 | { 42 | DocumentGraph dg(get_self()); 43 | dg.eraseDocument(hash); 44 | } 45 | 46 | void docs::testgetasset(const checksum256 &hash, 47 | const std::string &groupLabel, 48 | const std::string &contentLabel, 49 | const asset &contentValue) 50 | { 51 | Document document(get_self(), hash); 52 | 53 | eosio::print(" testgetasset:: looking for groupLabel: " + groupLabel + "\n"); 54 | eosio::print(" testgetasset:: looking for contentLabel: " + contentLabel + "\n"); 55 | asset readValue = document.getContentWrapper().getOrFail(groupLabel, contentLabel, "contentGroup or contentLabel does not exist")->getAs(); 56 | 57 | eosio::check(readValue == contentValue, "read value does not equal content value. read value: " + 58 | readValue.to_string() + " expected value: " + contentValue.to_string()); 59 | eosio::print(" testgetasset:: asset found: " + readValue.to_string() + "\n"); 60 | } 61 | 62 | void docs::testgetgroup(const checksum256 &hash, 63 | const std::string &groupLabel) 64 | { 65 | Document document(get_self(), hash); 66 | eosio::print(" testgetasset:: looking for groupLabel: " + groupLabel + "\n"); 67 | 68 | auto [idx, contentGroup] = document.getContentWrapper().getGroup(groupLabel); 69 | check(idx > -1, "group was not found"); 70 | } 71 | 72 | void docs::testcntnterr(string test) 73 | { 74 | ContentGroups cgs{ 75 | ContentGroup{ 76 | Content{CONTENT_GROUP_LABEL, "test"}, 77 | Content{"test_label", string("hello world")} 78 | } 79 | }; 80 | ContentWrapper cw(cgs); 81 | 82 | cw.getOrFail("test", "test_label")->getAs(); 83 | } 84 | 85 | void docs::createroot(const std::string ¬es) 86 | { 87 | require_auth(get_self()); 88 | 89 | Document rootDoc(get_self(), get_self(), Content("root_node", get_self())); 90 | } 91 | 92 | // void docs::fork (const checksum256 &hash, const name &creator, const vector &content_groups ) 93 | // { 94 | // _document_graph.fork_document(hash, creator, content_groups); 95 | // } 96 | 97 | // void docs::certify(const name &certifier, const checksum256 &hash, const std::string ¬es) 98 | // { 99 | // Document document (get_self(), hash); 100 | // document.certify(certifier, notes); 101 | // } 102 | 103 | // void docs::reset() 104 | // { 105 | // require_auth(get_self()); 106 | // document_table d_t(get_self(), get_self().value); 107 | // auto d_itr = d_t.begin(); 108 | // while (d_itr != d_t.end()) 109 | // { 110 | // d_itr = d_t.erase(d_itr); 111 | // } 112 | 113 | // edge_table e_t(get_self(), get_self().value); 114 | // auto e_itr = e_t.begin(); 115 | // while (e_itr != e_t.end()) 116 | // { 117 | // e_itr = e_t.erase(e_itr); 118 | // } 119 | // } 120 | } // namespace hypha -------------------------------------------------------------------------------- /src/document_graph/content.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace hypha 5 | { 6 | 7 | Content::Content(std::string label, FlexValue value) : label{label}, value{value} {} 8 | Content::Content() {} 9 | Content::~Content() {} 10 | 11 | const bool Content::isEmpty () const 12 | { 13 | if (std::holds_alternative(value)) { 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | const std::string Content::toString() const 20 | { 21 | if (isEmpty()) return ""; 22 | 23 | std::string str = "{" + std::string(label) + "="; 24 | if (std::holds_alternative(value)) 25 | { 26 | str += "[int64," + std::to_string(std::get(value)) + "]"; 27 | } 28 | else if (std::holds_alternative(value)) 29 | { 30 | str += "[asset," + std::get(value).to_string() + "]"; 31 | } 32 | else if (std::holds_alternative(value)) 33 | { 34 | str += "[time_point," + std::to_string(std::get(value).sec_since_epoch()) + "]"; 35 | } 36 | else if (std::holds_alternative(value)) 37 | { 38 | str += "[string," + std::get(value) + "]"; 39 | } 40 | else if (std::holds_alternative(value)) 41 | { 42 | eosio::checksum256 cs_value = std::get(value); 43 | auto arr = cs_value.extract_as_byte_array(); 44 | std::string str_value = toHex((const char *)arr.data(), arr.size()); 45 | str += "[checksum256," + str_value + "]"; 46 | } 47 | else 48 | { 49 | str += "[name," + std::get(value).to_string() + "]"; 50 | } 51 | str += "}"; 52 | return str; 53 | } 54 | } // namespace hypha -------------------------------------------------------------------------------- /src/document_graph/content_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace hypha 10 | { 11 | 12 | ContentWrapper::ContentWrapper(ContentGroups& cgs) : m_contentGroups{cgs} {} 13 | 14 | ContentWrapper::~ContentWrapper() {} 15 | 16 | std::pair ContentWrapper::getGroup(const std::string &label) 17 | { 18 | for (std::size_t i = 0; i < getContentGroups().size(); ++i) 19 | { 20 | for (Content &content : getContentGroups()[i]) 21 | { 22 | if (content.label == CONTENT_GROUP_LABEL) 23 | { 24 | EOS_CHECK(std::holds_alternative(content.value), "fatal error: " + CONTENT_GROUP_LABEL + " must be a string"); 25 | if (std::get(content.value) == label) 26 | { 27 | return {(int64_t)i, &getContentGroups()[i]}; 28 | } 29 | } 30 | } 31 | } 32 | return {-1, nullptr}; 33 | } 34 | 35 | std::pair ContentWrapper::getGroupOrCreate(const string& label) 36 | { 37 | TRACE_FUNCTION() 38 | auto [idx, contentGroup] = getGroup(label); 39 | 40 | if (!contentGroup) { 41 | idx = m_contentGroups.size(); 42 | 43 | m_contentGroups.push_back(ContentGroup({ 44 | Content(CONTENT_GROUP_LABEL, label) 45 | })); 46 | 47 | contentGroup = &m_contentGroups[idx]; 48 | } 49 | 50 | return { idx, contentGroup }; 51 | } 52 | 53 | ContentGroup *ContentWrapper::getGroupOrFail(const std::string &label, const std::string &error) 54 | { 55 | TRACE_FUNCTION() 56 | auto [idx, contentGroup] = getGroup(label); 57 | if (idx == -1) 58 | { 59 | EOS_CHECK(false, error); 60 | } 61 | return contentGroup; 62 | } 63 | 64 | ContentGroup *ContentWrapper::getGroupOrFail(const std::string &groupLabel) 65 | { 66 | TRACE_FUNCTION() 67 | return getGroupOrFail(groupLabel, "group: " + groupLabel + " is required but not found"); 68 | } 69 | 70 | std::pair ContentWrapper::get(const std::string &groupLabel, const std::string &contentLabel) 71 | { 72 | TRACE_FUNCTION() 73 | auto [idx, contentGroup] = getGroup(groupLabel); 74 | 75 | return get(static_cast(idx), contentLabel); 76 | } 77 | 78 | Content *ContentWrapper::getOrFail(const std::string &groupLabel, const std::string &contentLabel, const std::string &error) 79 | { 80 | TRACE_FUNCTION() 81 | auto [idx, item] = get(groupLabel, contentLabel); 82 | if (idx == -1) 83 | { 84 | EOS_CHECK(false, error); 85 | } 86 | return item; 87 | } 88 | 89 | Content *ContentWrapper::getOrFail(const std::string &groupLabel, const std::string &contentLabel) 90 | { 91 | TRACE_FUNCTION() 92 | return getOrFail(groupLabel, contentLabel, "group: " + groupLabel + "; content: " + contentLabel + 93 | " is required but not found"); 94 | } 95 | 96 | std::pair ContentWrapper::getOrFail(size_t groupIndex, const std::string &contentLabel, string_view error) 97 | { 98 | EOS_CHECK(groupIndex < m_contentGroups.size(), 99 | "getOrFail(): Can't access invalid group index [Out Of Rrange]: " + 100 | std::to_string(groupIndex)); 101 | 102 | auto [idx, item] = get(groupIndex, contentLabel); 103 | 104 | EOS_CHECK(item, error.empty() ? "group index: " + 105 | std::to_string(groupIndex) + 106 | " content: " + 107 | contentLabel + 108 | " is required but not found" 109 | : string(error)); 110 | 111 | return {idx, item}; 112 | } 113 | 114 | bool ContentWrapper::exists(const std::string &groupLabel, const std::string &contentLabel) 115 | { 116 | auto [idx, item] = get(groupLabel, contentLabel); 117 | if (idx == -1) 118 | { 119 | return false; 120 | } 121 | return true; 122 | } 123 | 124 | std::pair ContentWrapper::get(size_t groupIndex, const std::string &contentLabel) 125 | { 126 | if (groupIndex < m_contentGroups.size()) { 127 | 128 | auto& contentGroup = m_contentGroups[groupIndex]; 129 | 130 | for (size_t i = 0; i < contentGroup.size(); ++i) 131 | { 132 | if (contentGroup.at(i).label == contentLabel) 133 | { 134 | return {(int64_t)i, &contentGroup.at(i)}; 135 | } 136 | } 137 | } 138 | 139 | return {-1, nullptr}; 140 | } 141 | 142 | void ContentWrapper::removeGroup(const std::string &groupLabel) 143 | { 144 | TRACE_FUNCTION() 145 | auto [idx, grp] = getGroup(groupLabel); 146 | EOS_CHECK(idx != -1, 147 | "Can't remove unexisting group: " + groupLabel); 148 | removeGroup(static_cast(idx)); 149 | } 150 | 151 | void ContentWrapper::removeGroup(size_t groupIndex) 152 | { 153 | EOS_CHECK(groupIndex < m_contentGroups.size(), 154 | "Can't remove invalid group index: " + std::to_string(groupIndex)); 155 | 156 | m_contentGroups.erase(m_contentGroups.begin() + groupIndex); 157 | } 158 | 159 | void ContentWrapper::removeContent(const std::string& groupLabel, const Content& content) 160 | { 161 | TRACE_FUNCTION() 162 | 163 | auto [gidx, contentGroup] = getGroup(groupLabel); 164 | 165 | EOS_CHECK(gidx != -1, 166 | "Can't remove content from unexisting group: " + groupLabel); 167 | 168 | //Search for equal content 169 | auto contentIt = std::find(contentGroup->begin(), 170 | contentGroup->end(), content); 171 | 172 | EOS_CHECK(contentIt != contentGroup->end(), 173 | "Can't remove unexisting content [" + content.label + "]"); 174 | 175 | removeContent(static_cast(gidx), 176 | static_cast(std::distance(contentGroup->begin(), contentIt))); 177 | } 178 | 179 | void ContentWrapper::removeContent(const std::string &groupLabel, const std::string &contentLabel) 180 | { 181 | TRACE_FUNCTION() 182 | 183 | auto [gidx, contentGroup] = getGroup(groupLabel); 184 | 185 | EOS_CHECK(gidx != -1, 186 | "Can't remove content from unexisting group: " + groupLabel); 187 | 188 | removeContent(static_cast(gidx), contentLabel); 189 | } 190 | 191 | void ContentWrapper::removeContent(size_t groupIndex, const std::string &contentLabel) 192 | { 193 | TRACE_FUNCTION() 194 | 195 | auto [cidx, content] = get(static_cast(groupIndex), contentLabel); 196 | 197 | EOS_CHECK(cidx != -1, 198 | "Can't remove unexisting content [" + contentLabel + "]"); 199 | 200 | removeContent(groupIndex, cidx); 201 | } 202 | 203 | void ContentWrapper::removeContent(size_t groupIndex, size_t contentIndex) 204 | { 205 | EOS_CHECK(groupIndex < m_contentGroups.size(), 206 | "Can't remove content from invalid group index [Out Of Rrange]: " + std::to_string(groupIndex)); 207 | 208 | auto& contentGroup = m_contentGroups[groupIndex]; 209 | 210 | EOS_CHECK(contentIndex < contentGroup.size(), 211 | "Can't remove invalid content index [Out Of Rrange]: " + std::to_string(contentIndex)); 212 | 213 | contentGroup.erase(contentGroup.begin() + contentIndex); 214 | } 215 | 216 | 217 | void ContentWrapper::insertOrReplace(size_t groupIndex, const Content &newContent) 218 | { 219 | EOS_CHECK(groupIndex < m_contentGroups.size(), 220 | "Can't access invalid group index [Out Of Rrange]: " + std::to_string(groupIndex)); 221 | 222 | auto& contentGroup = m_contentGroups[groupIndex]; 223 | 224 | insertOrReplace(contentGroup, newContent); 225 | } 226 | 227 | string_view ContentWrapper::getGroupLabel(size_t groupIndex) 228 | { 229 | EOS_CHECK(groupIndex < m_contentGroups.size(), 230 | "Can't access invalid group index [Out Of Rrange]: " + std::to_string(groupIndex)); 231 | 232 | TRACE_FUNCTION() 233 | 234 | return getGroupLabel(m_contentGroups[groupIndex]); 235 | } 236 | 237 | string_view ContentWrapper::getGroupLabel(const ContentGroup &contentGroup) 238 | { 239 | for (auto& content : contentGroup) { 240 | if (content.label == CONTENT_GROUP_LABEL) { 241 | EOS_CHECK(std::holds_alternative(content.value), 242 | "fatal error: " + CONTENT_GROUP_LABEL + " must be a string"); 243 | return content.getAs(); 244 | } 245 | } 246 | 247 | return {}; 248 | } 249 | 250 | void ContentWrapper::insertOrReplace(ContentGroup &contentGroup, const Content &newContent) 251 | { 252 | auto is_key = [&newContent](auto &c) { 253 | return c.label == newContent.label; 254 | }; 255 | //First let's check if key already exists 256 | auto content_itr = std::find_if(contentGroup.begin(), contentGroup.end(), is_key); 257 | 258 | if (content_itr == contentGroup.end()) 259 | { 260 | contentGroup.push_back(Content{newContent.label, newContent.value}); 261 | } 262 | else 263 | { 264 | content_itr->value = newContent.value; 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /src/document_graph/document.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace hypha 11 | { 12 | 13 | Document::~Document() {} 14 | Document::Document() {} 15 | 16 | Document::Document(eosio::name contract, eosio::name creator, ContentGroups contentGroups) 17 | : contract{contract}, creator{creator}, content_groups{std::move(contentGroups)} 18 | { 19 | emplace(); 20 | } 21 | 22 | Document::Document(eosio::name contract, eosio::name creator, ContentGroup contentGroup) 23 | : Document(contract, creator, rollup(contentGroup)) 24 | { 25 | } 26 | 27 | Document::Document(eosio::name contract, eosio::name creator, Content content) 28 | : Document(contract, creator, rollup(content)) 29 | { 30 | } 31 | 32 | Document::Document(eosio::name contract, eosio::name creator, const std::string &label, const Content::FlexValue &value) 33 | : Document(contract, creator, rollup(Content(label, value))) 34 | { 35 | } 36 | 37 | Document::Document(eosio::name contract, const eosio::checksum256 &_hash) : contract{contract} 38 | { 39 | TRACE_FUNCTION() 40 | document_table d_t(contract, contract.value); 41 | auto hash_index = d_t.get_index(); 42 | auto h_itr = hash_index.find(_hash); 43 | EOS_CHECK(h_itr != hash_index.end(), "document not found: " + readableHash(_hash)); 44 | 45 | id = h_itr->id; 46 | creator = h_itr->creator; 47 | created_date = h_itr->created_date; 48 | certificates = h_itr->certificates; 49 | content_groups = h_itr->content_groups; 50 | hashContents(); 51 | 52 | // this should never happen, only if hash algorithm somehow changed 53 | EOS_CHECK(hash == _hash, "fatal error: provided and indexed hash does not match newly generated hash"); 54 | } 55 | 56 | bool Document::exists(eosio::name contract, const eosio::checksum256 &_hash) 57 | { 58 | document_table d_t(contract, contract.value); 59 | auto hash_index = d_t.get_index(); 60 | auto h_itr = hash_index.find(_hash); 61 | 62 | if (h_itr != hash_index.end()) 63 | { 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | void Document::emplace() 70 | { 71 | TRACE_FUNCTION() 72 | hashContents(); 73 | 74 | document_table d_t(getContract(), getContract().value); 75 | auto hash_index = d_t.get_index(); 76 | auto h_itr = hash_index.find(hash); 77 | 78 | // if this content exists already, error out and send back the hash of the existing document 79 | EOS_CHECK(h_itr == hash_index.end(), "document exists already: " + readableHash(hash)); 80 | 81 | d_t.emplace(getContract(), [&](auto &d) { 82 | id = d_t.available_primary_key(); 83 | created_date = eosio::current_time_point(); 84 | d = *this; 85 | }); 86 | } 87 | 88 | Document Document::getOrNew(eosio::name _contract, eosio::name _creator, ContentGroups contentGroups) 89 | { 90 | Document document{}; 91 | document.content_groups = contentGroups; 92 | document.hashContents(); 93 | 94 | Document::document_table d_t(_contract, _contract.value); 95 | auto hash_index = d_t.get_index(); 96 | auto h_itr = hash_index.find(document.hash); 97 | 98 | // if this content exists already, return this one 99 | if (h_itr != hash_index.end()) 100 | { 101 | document.contract = _contract; 102 | document.creator = h_itr->creator; 103 | document.created_date = h_itr->created_date; 104 | document.certificates = h_itr->certificates; 105 | document.id = h_itr->id; 106 | return document; 107 | } 108 | 109 | return Document(_contract, _creator, contentGroups); 110 | } 111 | 112 | Document Document::getOrNew(eosio::name contract, eosio::name creator, ContentGroup contentGroup) 113 | { 114 | return getOrNew(contract, creator, rollup(contentGroup)); 115 | } 116 | 117 | Document Document::getOrNew(eosio::name contract, eosio::name creator, Content content) 118 | { 119 | return getOrNew(contract, creator, rollup(content)); 120 | } 121 | 122 | Document Document::getOrNew(eosio::name contract, eosio::name creator, const std::string &label, const Content::FlexValue &value) 123 | { 124 | return getOrNew(contract, creator, rollup(Content(label, value))); 125 | } 126 | 127 | // void Document::certify(const eosio::name &certifier, const std::string ¬es) 128 | // { 129 | // // check if document is already saved?? 130 | // document_table d_t(m_contract, m_contract.value); 131 | // auto h_itr = hash_index.find(id); 132 | // EOS_CHECK(h_itr != d_t.end(), "document not found when attemption to certify: " + readableHash(geash())); 133 | 134 | // require_auth(certifier); 135 | 136 | // // TODO: should a certifier be able to sign the same document fork multiple times? 137 | // d_t.modify(h_itr, m_contract, [&](auto &d) { 138 | // d = std::move(this); 139 | // d.certificates.push_back(new_certificate(certifier, notes)); 140 | // }); 141 | // } 142 | 143 | const void Document::hashContents() 144 | { 145 | // save/cache the hash in the member 146 | hash = hashContents(content_groups); 147 | } 148 | 149 | const std::string Document::toString() 150 | { 151 | return toString(content_groups); 152 | } 153 | 154 | // static version cannot cache the hash in a member 155 | const eosio::checksum256 Document::hashContents(const ContentGroups &contentGroups) 156 | { 157 | std::string string_data = toString(contentGroups); 158 | return eosio::sha256(const_cast(string_data.c_str()), string_data.length()); 159 | } 160 | 161 | const std::string Document::toString(const ContentGroups &contentGroups) 162 | { 163 | std::string results = "["; 164 | bool is_first = true; 165 | 166 | for (const ContentGroup &contentGroup : contentGroups) 167 | { 168 | if (is_first) 169 | { 170 | is_first = false; 171 | } 172 | else 173 | { 174 | results = results + ","; 175 | } 176 | results = results + toString(contentGroup); 177 | } 178 | 179 | results = results + "]"; 180 | return results; 181 | } 182 | 183 | const std::string Document::toString(const ContentGroup &contentGroup) 184 | { 185 | std::string results = "["; 186 | bool is_first = true; 187 | 188 | for (const Content &content : contentGroup) 189 | { 190 | if (is_first) 191 | { 192 | is_first = false; 193 | } 194 | else 195 | { 196 | results = results + ","; 197 | } 198 | results = results + content.toString(); 199 | } 200 | 201 | results = results + "]"; 202 | return results; 203 | } 204 | 205 | ContentGroups Document::rollup(ContentGroup contentGroup) 206 | { 207 | ContentGroups contentGroups; 208 | contentGroups.push_back(contentGroup); 209 | return contentGroups; 210 | } 211 | 212 | ContentGroups Document::rollup(Content content) 213 | { 214 | ContentGroup contentGroup; 215 | contentGroup.push_back(content); 216 | return rollup(contentGroup); 217 | } 218 | 219 | /** Example 220 | * Original Doc { 221 | * content_groups: [ 222 | * [ 223 | * { "label": "content_group_label", "value": "test" }, 224 | * { "label": "epsilon", "value": 22 } 225 | * { "label": "other", "value": "ABCD"} 226 | * ], 227 | * [ 228 | * { "label": "content_group_label", "value": "system" }, 229 | * { "label": "alpha", "value": "lorem" } 230 | * { "label": "date", "value": "2019-08-10"} 231 | * ], 232 | * [ 233 | * { "label": "content_group_label", "value": "common" }, 234 | * { "label": "beta", "value": 12345 } 235 | * { "label": "gamma", "value": "#$#$"} 236 | * ], 237 | * ] 238 | * } 239 | * 240 | * Delta Doc { 241 | * content_groups: [ 242 | * [ 243 | * { "label": "content_group_label", "value": "test" }, 244 | * { "label": "epsilon", "value": 10 } 245 | * { "label": "other", "value": ""} #Monostate values will delete the item 246 | * ], 247 | * [ 248 | * { "label": "content_group_label", "value": "system" }, 249 | * { "label": "alpha", "value": "ipsu" } 250 | * { "label": "date", "value": "2020-08-10"} 251 | * { "label": "skip_from_merge", "value": "" } #This tag will skip this group from the merge (keep original) 252 | * ], 253 | * [ 254 | * { "label": "content_group_label", "value": "common" }, 255 | * { "label": "beta", "value": 0 } 256 | * { "label": "gamma", "value": "....."} 257 | * { "label": "delete_group", "value": "" } #This tag will delete group 258 | * ], 259 | * ] 260 | * } 261 | * 262 | * Merged Doc { 263 | * content_groups: [ 264 | * [ 265 | * { "label": "content_group_label", "value": "test" }, 266 | * { "label": "epsilon", "value": 10 } 267 | * ], 268 | * [ 269 | * { "label": "content_group_label", "value": "system" }, 270 | * { "label": "alpha", "value": "lorem" } 271 | * { "label": "date", "value": "2019-08-10"} 272 | * ], 273 | * ] 274 | * } 275 | */ 276 | Document Document::merge(Document original, Document &deltas) 277 | { 278 | TRACE_FUNCTION() 279 | const auto& deltasGroups = deltas.getContentGroups(); 280 | auto& originalGroups = original.getContentGroups(); 281 | auto deltasWrapper = deltas.getContentWrapper(); 282 | auto originalWrapper = original.getContentWrapper(); 283 | 284 | //unordered_map not available with eosio atm 285 | std::map> groupsByLabel; 286 | 287 | for (size_t i = 0; i < originalGroups.size(); ++i) { 288 | auto label = ContentWrapper::getGroupLabel(originalGroups[i]); 289 | if (!label.empty()) { 290 | groupsByLabel[string(label)] = std::pair{i, &originalGroups[i]}; 291 | } 292 | } 293 | 294 | for (size_t i = 0; i < deltasGroups.size(); ++i) { 295 | 296 | auto label = ContentWrapper::getGroupLabel(deltasGroups[i]); 297 | 298 | //If there is no group label just append it to the original doc 299 | if (label.empty()) { 300 | originalGroups.push_back(deltasGroups[i]); 301 | continue; 302 | } 303 | 304 | //Check if we need to delete the group 305 | if (auto [idx, c] = deltasWrapper.get(i, "delete_group"); 306 | c) { 307 | originalWrapper.removeGroup(string(label)); 308 | continue; 309 | } 310 | 311 | //Check if we need to skip this group from merge 312 | if (auto [_, c] = deltasWrapper.get(i, "skip_from_merge"); 313 | c) { 314 | continue; 315 | } 316 | 317 | //If group is not present on original document we should append it 318 | if (auto groupIt = groupsByLabel.find(string(label)); 319 | groupIt == groupsByLabel.end()) { 320 | originalGroups.push_back(deltasGroups[i]); 321 | } 322 | else { 323 | auto [oriGroupIdx, oriGroup] = groupIt->second; 324 | 325 | //It doesn't matter if it replaces content_group_label as they should be equal 326 | for (auto& deltaContent : deltasGroups[i]) { 327 | // Proposed fix is to use ballot_title & ballot_description as 328 | // a separated item 329 | // if (deltaContent.label == "title") { 330 | // // TODO: fix hack: we need to separate 'ballot title' from the assignment/document title 331 | // continue; 332 | // } 333 | if (std::holds_alternative(deltaContent.value)) { 334 | originalWrapper.removeContent(oriGroupIdx, deltaContent.label); 335 | } 336 | else { 337 | originalWrapper.insertOrReplace(oriGroupIdx, deltaContent); 338 | } 339 | } 340 | } 341 | } 342 | 343 | return original; 344 | } 345 | } // namespace hypha 346 | -------------------------------------------------------------------------------- /src/document_graph/document_graph.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace hypha 7 | { 8 | std::vector DocumentGraph::getEdges(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode) 9 | { 10 | std::vector edges; 11 | 12 | // this index uniquely identifies all edges that share this fromNode and toNode 13 | uint64_t index = concatHash(fromNode, toNode); 14 | Edge::edge_table e_t(m_contract, m_contract.value); 15 | auto from_name_index = e_t.get_index(); 16 | auto itr = from_name_index.find(index); 17 | 18 | while (itr != from_name_index.end() && itr->by_from_node_to_node_index() == index) 19 | { 20 | edges.push_back(*itr); 21 | itr++; 22 | } 23 | 24 | return edges; 25 | } 26 | 27 | std::vector DocumentGraph::getEdgesOrFail(const eosio::checksum256 &fromNode, const eosio::checksum256 &toNode) 28 | { 29 | std::vector edges = getEdges(fromNode, toNode); 30 | EOS_CHECK(edges.size() > 0, "no edges exist: from " + readableHash(fromNode) + " to " + readableHash(toNode)); 31 | return edges; 32 | } 33 | 34 | std::vector DocumentGraph::getEdgesFrom(const eosio::checksum256 &fromNode, const eosio::name &edgeName) 35 | { 36 | std::vector edges; 37 | 38 | // this index uniquely identifies all edges that share this fromNode and edgeName 39 | uint64_t index = concatHash(fromNode, edgeName); 40 | Edge::edge_table e_t(m_contract, m_contract.value); 41 | auto from_name_index = e_t.get_index(); 42 | auto itr = from_name_index.find(index); 43 | 44 | while (itr != from_name_index.end() && itr->by_from_node_edge_name_index() == index) 45 | { 46 | edges.push_back(*itr); 47 | itr++; 48 | } 49 | 50 | return edges; 51 | } 52 | 53 | std::vector DocumentGraph::getEdgesFromOrFail(const eosio::checksum256 &fromNode, const eosio::name &edgeName) 54 | { 55 | std::vector edges = getEdgesFrom(fromNode, edgeName); 56 | EOS_CHECK(edges.size() > 0, "no edges exist: from " + readableHash(fromNode) + " with name " + edgeName.to_string()); 57 | return edges; 58 | } 59 | 60 | std::vector DocumentGraph::getEdgesTo(const eosio::checksum256 &toNode, const eosio::name &edgeName) 61 | { 62 | std::vector edges; 63 | 64 | // this index uniquely identifies all edges that share this toNode and edgeName 65 | uint64_t index = concatHash(toNode, edgeName); 66 | Edge::edge_table e_t(m_contract, m_contract.value); 67 | auto from_name_index = e_t.get_index(); 68 | auto itr = from_name_index.find(index); 69 | 70 | while (itr != from_name_index.end() && itr->by_to_node_edge_name_index() == index) 71 | { 72 | edges.push_back(*itr); 73 | itr++; 74 | } 75 | 76 | return edges; 77 | } 78 | 79 | std::vector DocumentGraph::getEdgesToOrFail(const eosio::checksum256 &toNode, const eosio::name &edgeName) 80 | { 81 | std::vector edges = getEdgesTo(toNode, edgeName); 82 | EOS_CHECK(edges.size() > 0, "no edges exist: to " + readableHash(toNode) + " with name " + edgeName.to_string()); 83 | return edges; 84 | } 85 | 86 | // since we are removing multiple edges here, we do not call erase on each edge, which 87 | // would instantiate the table on each call. This is faster execution. 88 | void DocumentGraph::removeEdges(const eosio::checksum256 &node) 89 | { 90 | Edge::edge_table e_t(m_contract, m_contract.value); 91 | 92 | auto from_node_index = e_t.get_index(); 93 | auto from_itr = from_node_index.find(node); 94 | 95 | while (from_itr != from_node_index.end() && from_itr->from_node == node) 96 | { 97 | from_itr = from_node_index.erase(from_itr); 98 | } 99 | 100 | auto to_node_index = e_t.get_index(); 101 | auto to_itr = to_node_index.find(node); 102 | 103 | while (to_itr != to_node_index.end() && to_itr->to_node == node) 104 | { 105 | to_itr = to_node_index.erase(to_itr); 106 | } 107 | } 108 | 109 | bool DocumentGraph::hasEdges(const eosio::checksum256 &node) 110 | { 111 | Edge::edge_table e_t(m_contract, m_contract.value); 112 | 113 | auto from_node_index = e_t.get_index(); 114 | if (from_node_index.find(node) != from_node_index.end()) 115 | { 116 | return true; 117 | } 118 | 119 | auto to_node_index = e_t.get_index(); 120 | if (to_node_index.find(node) != to_node_index.end()) 121 | { 122 | return true; 123 | } 124 | 125 | return false; 126 | } 127 | 128 | void DocumentGraph::replaceNode(const eosio::checksum256 &oldNode, const eosio::checksum256 &newNode) 129 | { 130 | Edge::edge_table e_t(m_contract, m_contract.value); 131 | 132 | auto from_node_index = e_t.get_index(); 133 | auto from_itr = from_node_index.find(oldNode); 134 | 135 | while (from_itr != from_node_index.end() && from_itr->from_node == oldNode) 136 | { 137 | // create the new edge record 138 | Edge newEdge(m_contract, m_contract, newNode, from_itr->to_node, from_itr->edge_name); 139 | 140 | // erase the old edge record 141 | from_itr = from_node_index.erase(from_itr); 142 | } 143 | 144 | auto to_node_index = e_t.get_index(); 145 | auto to_itr = to_node_index.find(oldNode); 146 | 147 | while (to_itr != to_node_index.end() && to_itr->to_node == oldNode) 148 | { 149 | // create the new edge record 150 | Edge newEdge(m_contract, m_contract, to_itr->from_node, newNode, to_itr->edge_name); 151 | 152 | // erase the old edge record 153 | to_itr = to_node_index.erase(to_itr); 154 | } 155 | } 156 | 157 | Document DocumentGraph::updateDocument(const eosio::name &updater, 158 | const eosio::checksum256 &documentHash, 159 | ContentGroups contentGroups) 160 | { 161 | TRACE_FUNCTION() 162 | Document currentDocument(m_contract, documentHash); 163 | Document newDocument(m_contract, updater, contentGroups); 164 | 165 | replaceNode(documentHash, newDocument.getHash()); 166 | eraseDocument(documentHash, false); 167 | return newDocument; 168 | } 169 | 170 | // for now, permissions should be handled in the contract action rather than this class 171 | void DocumentGraph::eraseDocument(const eosio::checksum256 &documentHash, const bool includeEdges) 172 | { 173 | Document::document_table d_t(m_contract, m_contract.value); 174 | auto hash_index = d_t.get_index(); 175 | auto h_itr = hash_index.find(documentHash); 176 | 177 | EOS_CHECK(h_itr != hash_index.end(), "Cannot erase document; does not exist: " + readableHash(documentHash)); 178 | 179 | if (includeEdges) 180 | { 181 | removeEdges(documentHash); 182 | } 183 | 184 | hash_index.erase(h_itr); 185 | } 186 | 187 | void DocumentGraph::eraseDocument(const eosio::checksum256 &documentHash) 188 | { 189 | TRACE_FUNCTION() 190 | return eraseDocument(documentHash, true); 191 | } 192 | } // namespace hypha -------------------------------------------------------------------------------- /src/document_graph/edge.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace hypha 7 | { 8 | Edge::Edge() {} 9 | Edge::Edge(const eosio::name &contract, 10 | const eosio::name &creator, 11 | const eosio::checksum256 &from_node, 12 | const eosio::checksum256 &to_node, 13 | const eosio::name &edge_name) 14 | : contract{contract}, creator{creator}, from_node{from_node}, to_node{to_node}, edge_name{edge_name} 15 | { 16 | TRACE_FUNCTION() 17 | emplace(); 18 | } 19 | 20 | Edge::~Edge() {} 21 | 22 | // static 23 | void Edge::write(const eosio::name &_contract, 24 | const eosio::name &_creator, 25 | const eosio::checksum256 &_from_node, 26 | const eosio::checksum256 &_to_node, 27 | const eosio::name &_edge_name) 28 | { 29 | edge_table e_t(_contract, _contract.value); 30 | 31 | const int64_t edgeID = concatHash(_from_node, _to_node, _edge_name); 32 | 33 | EOS_CHECK( 34 | e_t.find(edgeID) == e_t.end(), 35 | util::to_str("Edge from: ", _from_node, 36 | " to: ", _to_node, 37 | " with name: ", _edge_name, " already exists") 38 | ); 39 | 40 | std::string edge_name_str = _edge_name.to_string(); 41 | EOS_CHECK(!edge_name_str.empty(), "Edge name cannot be empty"); 42 | EOS_CHECK(!isdigit(edge_name_str[0]), "Edge name cannot start with a number"); 43 | EOS_CHECK(edge_name_str.find('.') == std::string::npos, "Edge name cannot contain '.' characters"); 44 | 45 | e_t.emplace(_contract, [&](auto &e) { 46 | e.id = edgeID; 47 | e.from_node_edge_name_index = concatHash(_from_node, _edge_name); 48 | e.from_node_to_node_index = concatHash(_from_node, _to_node); 49 | e.to_node_edge_name_index = concatHash(_to_node, _edge_name); 50 | e.creator = _creator; 51 | e.contract = _contract; 52 | e.from_node = _from_node; 53 | e.to_node = _to_node; 54 | e.edge_name = _edge_name; 55 | e.created_date = eosio::current_time_point(); 56 | }); 57 | } 58 | 59 | // static 60 | Edge Edge::getOrNew(const eosio::name &_contract, 61 | const eosio::name &creator, 62 | const eosio::checksum256 &_from_node, 63 | const eosio::checksum256 &_to_node, 64 | const eosio::name &_edge_name) 65 | { 66 | edge_table e_t(_contract, _contract.value); 67 | auto itr = e_t.find(concatHash(_from_node, _to_node, _edge_name)); 68 | 69 | if (itr != e_t.end()) 70 | { 71 | return *itr; 72 | } 73 | 74 | return Edge(_contract, creator, _from_node, _to_node, _edge_name); 75 | } 76 | 77 | // static getter 78 | Edge Edge::get(const eosio::name &_contract, 79 | const eosio::checksum256 &_from_node, 80 | const eosio::checksum256 &_to_node, 81 | const eosio::name &_edge_name) 82 | { 83 | edge_table e_t(_contract, _contract.value); 84 | auto itr = e_t.find(concatHash(_from_node, _to_node, _edge_name)); 85 | 86 | EOS_CHECK(itr != e_t.end(), "edge does not exist: from " + readableHash(_from_node) + " to " + readableHash(_to_node) + " with edge name of " + _edge_name.to_string()); 87 | 88 | return *itr; 89 | } 90 | 91 | // static getter 92 | Edge Edge::get(const eosio::name &_contract, 93 | const eosio::checksum256 &_from_node, 94 | const eosio::name &_edge_name) 95 | { 96 | edge_table e_t(_contract, _contract.value); 97 | auto fromEdgeIndex = e_t.get_index(); 98 | auto index = concatHash(_from_node, _edge_name); 99 | auto itr = fromEdgeIndex.find(index); 100 | 101 | EOS_CHECK(itr != fromEdgeIndex.end() && itr->from_node_edge_name_index == index, "edge does not exist: from " + readableHash(_from_node) + " with edge name of " + _edge_name.to_string()); 102 | 103 | return *itr; 104 | } 105 | 106 | // static getter 107 | Edge Edge::getTo(const eosio::name &_contract, 108 | const eosio::checksum256 &_to_node, 109 | const eosio::name &_edge_name) 110 | { 111 | edge_table e_t(_contract, _contract.value); 112 | auto toEdgeIndex = e_t.get_index(); 113 | auto index = concatHash(_to_node, _edge_name); 114 | auto itr = toEdgeIndex.find(index); 115 | 116 | EOS_CHECK(itr != toEdgeIndex.end() && itr->to_node_edge_name_index == index, "edge does not exist: to " + readableHash(_to_node) + " with edge name of " + _edge_name.to_string()); 117 | 118 | return *itr; 119 | } 120 | 121 | // static getter 122 | std::pair Edge::getIfExists(const eosio::name &_contract, 123 | const eosio::checksum256 &_from_node, 124 | const eosio::name &_edge_name) 125 | { 126 | edge_table e_t(_contract, _contract.value); 127 | auto fromEdgeIndex = e_t.get_index(); 128 | auto index = concatHash(_from_node, _edge_name); 129 | auto itr = fromEdgeIndex.find(index); 130 | 131 | if (itr != fromEdgeIndex.end() && itr->from_node_edge_name_index == index) 132 | { 133 | return std::pair (true, *itr); 134 | } 135 | 136 | return std::pair(false, Edge{}); 137 | } 138 | 139 | // static getter 140 | bool Edge::exists(const eosio::name &_contract, 141 | const eosio::checksum256 &_from_node, 142 | const eosio::checksum256 &_to_node, 143 | const eosio::name &_edge_name) 144 | { 145 | edge_table e_t(_contract, _contract.value); 146 | auto itr = e_t.find(concatHash(_from_node, _to_node, _edge_name)); 147 | if (itr != e_t.end()) 148 | return true; 149 | return false; 150 | } 151 | 152 | void Edge::emplace() 153 | { 154 | // update indexes prior to save 155 | id = concatHash(from_node, to_node, edge_name); 156 | 157 | from_node_edge_name_index = concatHash(from_node, edge_name); 158 | from_node_to_node_index = concatHash(from_node, to_node); 159 | to_node_edge_name_index = concatHash(to_node, edge_name); 160 | 161 | edge_table e_t(getContract(), getContract().value); 162 | 163 | EOS_CHECK( 164 | e_t.find(id) == e_t.end(), 165 | util::to_str("Edge from: ", from_node, 166 | " to: ", to_node, 167 | " with name: ", edge_name, " already exists") 168 | ); 169 | 170 | e_t.emplace(getContract(), [&](auto &e) { 171 | e = *this; 172 | e.created_date = eosio::current_time_point(); 173 | }); 174 | } 175 | 176 | void Edge::erase() 177 | { 178 | edge_table e_t(getContract(), getContract().value); 179 | auto itr = e_t.find(id); 180 | 181 | EOS_CHECK(itr != e_t.end(), "edge does not exist: from " + readableHash(from_node) + " to " + readableHash(to_node) + " with edge name of " + edge_name.to_string()); 182 | e_t.erase(itr); 183 | } 184 | 185 | uint64_t Edge::primary_key() const { return id; } 186 | uint64_t Edge::by_from_node_edge_name_index() const { return from_node_edge_name_index; } 187 | uint64_t Edge::by_from_node_to_node_index() const { return from_node_to_node_index; } 188 | uint64_t Edge::by_to_node_edge_name_index() const { return to_node_edge_name_index; } 189 | uint64_t Edge::by_edge_name() const { return edge_name.value; } 190 | uint64_t Edge::by_created() const { return created_date.sec_since_epoch(); } 191 | uint64_t Edge::by_creator() const { return creator.value; } 192 | 193 | eosio::checksum256 Edge::by_from() const { return from_node; } 194 | eosio::checksum256 Edge::by_to() const { return to_node; } 195 | } // namespace hypha -------------------------------------------------------------------------------- /src/document_graph/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | namespace hypha 7 | { 8 | 9 | const std::string toHex(const char *d, std::uint32_t s) 10 | { 11 | std::string r; 12 | const char *to_hex = "0123456789abcdef"; 13 | auto c = reinterpret_cast(d); 14 | for (auto i = 0; i < s; ++i) 15 | (r += to_hex[(c[i] >> 4)]) += to_hex[(c[i] & 0x0f)]; 16 | return r; 17 | } 18 | 19 | const std::string readableHash(const eosio::checksum256 &hash) 20 | { 21 | auto byte_arr = hash.extract_as_byte_array(); 22 | return toHex((const char *)byte_arr.data(), byte_arr.size()); 23 | } 24 | 25 | const std::uint64_t toUint64(const std::string &fingerprint) 26 | { 27 | uint64_t id = 0; 28 | eosio::checksum256 h = eosio::sha256(const_cast(fingerprint.c_str()), fingerprint.size()); 29 | auto hbytes = h.extract_as_byte_array(); 30 | for (int i = 0; i < 4; i++) 31 | { 32 | id <<= 8; 33 | id |= hbytes[i]; 34 | } 35 | return id; 36 | } 37 | 38 | const uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2, const eosio::name label) 39 | { 40 | return toUint64(readableHash(sha1) + readableHash(sha2) + label.to_string()); 41 | } 42 | 43 | const uint64_t concatHash(const eosio::checksum256 sha1, const eosio::checksum256 sha2) 44 | { 45 | return toUint64(readableHash(sha1) + readableHash(sha2)); 46 | } 47 | 48 | const uint64_t concatHash(const eosio::checksum256 sha, const eosio::name label) 49 | { 50 | return toUint64(readableHash(sha) + label.to_string()); 51 | } 52 | 53 | } // namespace hypha -------------------------------------------------------------------------------- /src/logger/logger.cpp: -------------------------------------------------------------------------------- 1 | #include "logger/logger.hpp" 2 | 3 | namespace hypha 4 | { 5 | 6 | Logger& 7 | Logger::instance() 8 | { 9 | static Logger l; 10 | return l; 11 | } 12 | 13 | void 14 | Logger::pushMessage(std::string message) 15 | { 16 | m_log.push_back(std::move(message)); 17 | } 18 | 19 | void 20 | Logger::popTrace() 21 | { 22 | m_trace.pop_back(); 23 | } 24 | 25 | void 26 | Logger::pushTrace(std::string trace) 27 | { 28 | m_trace.push_back(std::move(trace)); 29 | } 30 | 31 | std::string 32 | Logger::generateMessage() 33 | { 34 | std::string ss = "\n------------------- Stack Trace -------------------\n"; 35 | std::string tab = ""; 36 | for (auto& message : m_trace) { 37 | ss += "\n" + tab + message; 38 | tab += "\t"; 39 | } 40 | 41 | ss += "\n\n--------------------------------------------------\n"; 42 | 43 | ss += "\n------------------- Log -------------------\n"; 44 | 45 | for (auto& message : m_log) { 46 | ss += "\n" + message; 47 | } 48 | 49 | ss += "\n\n--------------------------------------------------\n"; 50 | 51 | return ss; 52 | } 53 | 54 | 55 | 56 | } --------------------------------------------------------------------------------