├── .gitattributes ├── tela.png ├── .gitignore ├── tela_tests ├── contracts │ ├── test2.bas │ ├── test4.bas │ ├── test5.bas │ ├── test6.bas │ ├── test3.bas │ └── test1.bas ├── images │ ├── test2.svg │ └── test1.svg ├── app2 │ └── index.html └── app1 │ ├── index.html │ ├── style.css │ └── main.js ├── shards ├── keys.go ├── values.go ├── boltdb.go ├── gravdb.go ├── shards.go └── shards_test.go ├── TELA-MOD-1 ├── vs │ ├── TELA-MOD-1-VSOOIM.bas │ ├── TELA-MOD-1-VSOO.bas │ ├── TELA-MOD-1-VSPUBIM.bas │ ├── TELA-MOD-1-VSPUBOW.bas │ └── TELA-MOD-1-VSPUBSU.bas └── tx │ ├── TELA-MOD-1-TXTO.bas │ ├── TELA-MOD-1-TXDWD.bas │ └── TELA-MOD-1-TXDWA.bas ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── LICENSE ├── TELA-DOC-1 ├── TELA-DOC-1.bas └── README.md ├── CHANGELOG.md ├── TELA-INDEX-1 ├── TELA-INDEX-1.bas └── README.md ├── logger ├── colors.go ├── ascii.go ├── logger.go ├── ascii_test.go └── logger_test.go ├── headers.go ├── go.mod ├── compression.go ├── ratings.go ├── cmd └── tela-cli │ ├── gnomon.go │ └── functions_test.go ├── mods.go └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bas linguist-language=BASIC -------------------------------------------------------------------------------- /tela.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/civilware/tela/HEAD/tela.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | datashards* 2 | mainnet* 3 | testnet* 4 | tela_sim_wallets* -------------------------------------------------------------------------------- /tela_tests/contracts/test2.bas: -------------------------------------------------------------------------------- 1 | // Test code for TELA parser, should error when parsing with dvm.ParseSmartContract 2 | Function Initialize() bool // wrong return type errors dvm.ParseSmartContract() 3 | 10 RETURN 0 4 | End Function -------------------------------------------------------------------------------- /tela_tests/contracts/test4.bas: -------------------------------------------------------------------------------- 1 | // Test code for TELA parser, should be valid to parse with dvm.ParseSmartContract 2 | Function Initialize(u Uint64, s String) String 3 | 10 RETURN 0 4 | End Function 5 | 6 | Function test() 7 | 10 RETURN 0 8 | End Function -------------------------------------------------------------------------------- /tela_tests/contracts/test5.bas: -------------------------------------------------------------------------------- 1 | // Test code for TELA parser, should be valid to parse with dvm.ParseSmartContract 2 | Function Initialize(u Uint64, s String) String 3 | 10 RETURN 0 4 | End Function 5 | 6 | Function test() 7 | 15 RETURN 0 8 | End Function -------------------------------------------------------------------------------- /tela_tests/contracts/test6.bas: -------------------------------------------------------------------------------- 1 | // Test code for TELA parser, should be valid to parse with dvm.ParseSmartContract 2 | Function Initialize(u Uint64, s String) String 3 | 10 RETURN 0 4 | End Function 5 | 6 | Function test2() 7 | 10 RETURN 0 8 | End Function -------------------------------------------------------------------------------- /tela_tests/contracts/test3.bas: -------------------------------------------------------------------------------- 1 | // Test code for TELA parser, should be valid to parse with dvm.ParseSmartContract 2 | Function Initialize(u Uint64, s String) String 3 | 10 RETURN 0 4 | End Function 5 | 6 | Function test() Uint64 7 | 10 IF EXISTS() THEN GOTO 10 8 | End Function -------------------------------------------------------------------------------- /tela_tests/images/test2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tela_tests/contracts/test1.bas: -------------------------------------------------------------------------------- 1 | // Test code for TELA parser, should be valid to parse with dvm.ParseSmartContract 2 | Function Initialize(u Uint64, s String) String 3 | 10 RETURN 0 4 | End Function 5 | 6 | Function test() 7 | 10 RETURN ! // ! hits add single operator 8 | End Function -------------------------------------------------------------------------------- /shards/keys.go: -------------------------------------------------------------------------------- 1 | package shards 2 | 3 | type keys struct{} 4 | 5 | // Common storage keys 6 | var Key keys 7 | 8 | // Endpoint DB storage key 9 | func (keys) Endpoint() []byte { 10 | return []byte("endpoint") 11 | } 12 | 13 | // Network DB storage key 14 | func (keys) Network() []byte { 15 | return []byte("network") 16 | } 17 | -------------------------------------------------------------------------------- /TELA-MOD-1/vs/TELA-MOD-1-VSOOIM.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function SetVar(k String, v String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 IF EXISTS("ivar_"+k) THEN GOTO 20 7 | 12 IF LOAD("owner") == address() THEN GOTO 30 8 | 20 RETURN 1 9 | 30 STORE("ivar_"+k, v) 10 | 40 RETURN 0 11 | End Function -------------------------------------------------------------------------------- /TELA-MOD-1/vs/TELA-MOD-1-VSOO.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function SetVar(k String, v String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 IF LOAD("owner") == address() THEN GOTO 30 7 | 20 RETURN 1 8 | 30 STORE("var_"+k, v) 9 | 40 RETURN 0 10 | End Function 11 | 12 | Function DeleteVar(k String) Uint64 13 | 10 IF EXISTS("var_"+k) == 0 THEN GOTO 20 14 | 11 IF LOAD("owner") == "anon" THEN GOTO 20 15 | 12 IF LOAD("owner") == address() THEN GOTO 30 16 | 20 RETURN 1 17 | 30 DELETE("var_"+k) 18 | 40 RETURN 0 19 | End Function -------------------------------------------------------------------------------- /TELA-MOD-1/vs/TELA-MOD-1-VSPUBIM.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function SetVar(k String, v String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 DIM addr as String 7 | 12 LET addr = address() 8 | 13 IF LOAD("owner") == addr THEN GOTO 30 9 | 14 IF addr == "anon" THEN GOTO 20 10 | 15 IF STRLEN(k) > 256 || STRLEN(v) > 256 THEN GOTO 20 11 | 16 IF EXISTS("ivar_"+addr+"_"+k) == 0 THEN GOTO 50 12 | 20 RETURN 1 13 | 30 IF EXISTS("ivar_"+k) THEN GOTO 20 14 | 31 STORE("ivar_"+k, v) 15 | 40 RETURN 0 16 | 50 STORE("ivar_"+addr+"_"+k, v) 17 | 90 RETURN 0 18 | End Function -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Provide a code example, or steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Enter this '....' 17 | 3. Enter that '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tela_tests/images/test1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TELA-MOD-1/tx/TELA-MOD-1-TXTO.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function TransferOwnership(addr String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 IF IS_ADDRESS_VALID(ADDRESS_RAW(addr)) == 0 THEN GOTO 20 7 | 12 IF LOAD("owner") == address() THEN GOTO 30 // address() comes from TELA base code 8 | 20 RETURN 1 9 | 30 STORE("tmpowner", addr) // Use the string to match address() 10 | 40 RETURN 0 11 | End Function 12 | 13 | Function ClaimOwnership() Uint64 14 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 15 | 11 IF EXISTS("tmpowner") == 0 THEN GOTO 20 16 | 12 IF LOAD("tmpowner") == address() THEN GOTO 30 17 | 20 RETURN 1 18 | 30 STORE("owner", address()) 19 | 40 DELETE("tmpowner") 20 | 50 RETURN 0 21 | End Function -------------------------------------------------------------------------------- /shards/values.go: -------------------------------------------------------------------------------- 1 | package shards 2 | 3 | type network struct{} 4 | 5 | type values struct { 6 | Network network 7 | } 8 | 9 | // Common storage values 10 | var Value values 11 | 12 | // Valid DERO networks 13 | var validNetworks = []string{ 14 | "Mainnet", 15 | "Testnet", 16 | "Simulator", 17 | } 18 | 19 | // Is network a valid DERO network 20 | func isNetworkValid(network string) bool { 21 | for _, n := range validNetworks { 22 | if network == n { 23 | return true 24 | } 25 | } 26 | 27 | return false 28 | } 29 | 30 | // Mainnet network value 31 | func (network) Mainnet() string { 32 | return validNetworks[0] 33 | } 34 | 35 | // Testnet network value 36 | func (network) Testnet() string { 37 | return validNetworks[1] 38 | } 39 | 40 | // Simulator network value 41 | func (network) Simulator() string { 42 | return validNetworks[2] 43 | } 44 | -------------------------------------------------------------------------------- /TELA-MOD-1/vs/TELA-MOD-1-VSPUBOW.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function SetVar(k String, v String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 DIM addr as String 7 | 12 LET addr = address() 8 | 13 IF LOAD("owner") == addr THEN GOTO 30 9 | 14 IF addr == "anon" THEN GOTO 20 10 | 15 IF STRLEN(k) > 256 || STRLEN(v) > 256 THEN GOTO 20 11 | 16 GOTO 50 // Match the flow of vspubsu 12 | 20 RETURN 1 13 | 30 STORE("var_"+k, v) 14 | 40 RETURN 0 15 | 50 STORE("var_"+addr+"_"+k, v) 16 | 90 RETURN 0 17 | End Function 18 | 19 | Function DeleteVar(k String) Uint64 20 | 10 IF EXISTS("var_"+k) == 0 THEN GOTO 20 21 | 11 IF LOAD("owner") == "anon" THEN GOTO 20 22 | 12 IF LOAD("owner") == address() THEN GOTO 30 23 | 20 RETURN 1 24 | 30 DELETE("var_"+k) 25 | 40 RETURN 0 26 | End Function -------------------------------------------------------------------------------- /TELA-MOD-1/tx/TELA-MOD-1-TXDWD.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function DepositDero() Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 IF DEROVALUE() > 0 THEN GOTO 30 7 | 20 RETURN 1 8 | 30 IF EXISTS("balance_dero") THEN GOTO 60 9 | 40 STORE("balance_dero", DEROVALUE()) 10 | 50 GOTO 90 11 | 60 STORE("balance_dero", LOAD("balance_dero")+DEROVALUE()) 12 | 90 RETURN 0 13 | End Function 14 | 15 | Function WithdrawDero(amt Uint64) Uint64 16 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 17 | 12 IF EXISTS("balance_dero") == 0 THEN GOTO 20 18 | 13 IF LOAD("balance_dero") < amt THEN GOTO 20 19 | 14 IF LOAD("owner") == address() THEN GOTO 30 20 | 20 RETURN 1 21 | 30 SEND_DERO_TO_ADDRESS(SIGNER(), amt) 22 | 40 STORE("balance_dero", LOAD("balance_dero")-amt) 23 | 90 RETURN 0 24 | End Function -------------------------------------------------------------------------------- /TELA-MOD-1/vs/TELA-MOD-1-VSPUBSU.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function SetVar(k String, v String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 DIM addr as String 7 | 12 LET addr = address() 8 | 13 IF LOAD("owner") == addr THEN GOTO 30 9 | 14 IF addr == "anon" THEN GOTO 20 10 | 15 IF STRLEN(k) > 256 || STRLEN(v) > 256 THEN GOTO 20 11 | 16 IF EXISTS("var_"+addr+"_"+k) == 0 THEN GOTO 50 12 | 20 RETURN 1 13 | 30 STORE("var_"+k, v) 14 | 40 RETURN 0 15 | 50 STORE("var_"+addr+"_"+k, v) 16 | 90 RETURN 0 17 | End Function 18 | 19 | Function DeleteVar(k String) Uint64 20 | 10 IF EXISTS("var_"+k) == 0 THEN GOTO 20 21 | 11 IF LOAD("owner") == "anon" THEN GOTO 20 22 | 12 IF LOAD("owner") == address() THEN GOTO 30 23 | 20 RETURN 1 24 | 30 DELETE("var_"+k) 25 | 40 RETURN 0 26 | End Function -------------------------------------------------------------------------------- /TELA-MOD-1/tx/TELA-MOD-1-TXDWA.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard Module (TELA-MOD-1) 3 | 4 | Function DepositAsset(scid String) Uint64 5 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 6 | 11 IF ASSETVALUE(HEXDECODE(scid)) > 0 THEN GOTO 30 7 | 20 RETURN 1 8 | 30 IF EXISTS("balance_"+scid) THEN GOTO 60 9 | 40 STORE("balance_"+scid, ASSETVALUE(HEXDECODE(scid))) 10 | 50 GOTO 90 11 | 60 STORE("balance_"+scid, LOAD("balance_"+scid)+ASSETVALUE(HEXDECODE(scid))) 12 | 90 RETURN 0 13 | End Function 14 | 15 | Function WithdrawAsset(scid String, amt Uint64) Uint64 16 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 17 | 12 IF EXISTS("balance_"+scid) == 0 THEN GOTO 20 18 | 13 IF LOAD("balance_"+scid) < amt THEN GOTO 20 19 | 14 IF LOAD("owner") == address() THEN GOTO 30 20 | 20 RETURN 1 21 | 30 SEND_ASSET_TO_ADDRESS(SIGNER(), amt, HEXDECODE(scid)) 22 | 40 STORE("balance_"+scid, LOAD("balance_"+scid)-amt) 23 | 90 RETURN 0 24 | End Function -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Civilware 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the changes and the related issue or feature. 4 | 5 | **NOTE**: The merge process is as follows: 6 | - Your pull request should be directed to `dev` branch. 7 | - When it will be merged in `dev`, we will compile and merge within a `release/` branch and then push into `main` for final release. 8 | 9 | Fixes # (issue) 10 | 11 | ## Type of change 12 | 13 | Please select the right one. 14 | 15 | - [ ] (Patch) Bug fix (non-breaking change which fixes an issue) 16 | - [ ] (Minor) New feature (non-breaking change which adds functionality) 17 | - [ ] (Major) Breaking change (modification that would disrupt existing functionality and require changes to maintain expected behavior) 18 | - [ ] This change requires a documentation update 19 | 20 | ## Which package(s) are impacted ? 21 | 22 | - [ ] cmd 23 | - [ ] tela 24 | - [ ] shards 25 | - [ ] logger 26 | - [ ] Misc (documentation, etc...) 27 | 28 | ## Checklist: 29 | 30 | - [ ] I have performed a self-review of my code 31 | - [ ] I have commented my code 32 | - [ ] I have test coverage for my changes 33 | - [ ] My changes generate no new warnings 34 | 35 | ## License 36 | 37 | I am contributing & releasing the code under the [MIT License](https://raw.githubusercontent.com/civilware/tela/main/LICENSE). -------------------------------------------------------------------------------- /TELA-DOC-1/TELA-DOC-1.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Document (TELA-DOC-1) 3 | 4 | Function InitializePrivate() Uint64 5 | 10 IF init() == 0 THEN GOTO 30 6 | 20 RETURN 1 7 | 30 STORE("var_header_name", "") 8 | 31 STORE("var_header_description", "") 9 | 32 STORE("var_header_icon", "") 10 | 33 STORE("dURL", "") 11 | 34 STORE("docType", "") 12 | 35 STORE("subDir", "") 13 | 36 STORE("fileCheckC", "") 14 | 37 STORE("fileCheckS", "") 15 | 100 RETURN 0 16 | End Function 17 | 18 | Function init() Uint64 19 | 10 IF EXISTS("owner") == 0 THEN GOTO 30 20 | 20 RETURN 1 21 | 30 STORE("owner", address()) 22 | 50 STORE("docVersion", "1.0.0") // DOC SC version 23 | 60 STORE("hash", HEX(TXID())) 24 | 70 STORE("likes", 0) 25 | 80 STORE("dislikes", 0) 26 | 100 RETURN 0 27 | End Function 28 | 29 | Function address() String 30 | 10 DIM s as String 31 | 20 LET s = SIGNER() 32 | 30 IF IS_ADDRESS_VALID(s) THEN GOTO 50 33 | 40 RETURN "anon" 34 | 50 RETURN ADDRESS_STRING(s) 35 | End Function 36 | 37 | Function Rate(r Uint64) Uint64 38 | 10 DIM addr as String 39 | 15 LET addr = address() 40 | 16 IF r < 100 && EXISTS(addr) == 0 && addr != "anon" THEN GOTO 30 41 | 20 RETURN 1 42 | 30 STORE(addr, ""+r+"_"+BLOCK_HEIGHT()) 43 | 40 IF r < 50 THEN GOTO 70 44 | 50 STORE("likes", LOAD("likes")+1) 45 | 60 RETURN 0 46 | 70 STORE("dislikes", LOAD("dislikes")+1) 47 | 100 RETURN 0 48 | End Function 49 | 50 | /* 51 | docType code goes in this comment section 52 | */ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## INDEX v1.1.0 - In Progress 4 | 5 | * Update `TELA-INDEX-1` SC to v1.1.0 6 | * Add `TELA-MOD-1` DVM function modules and related package functions. 7 | * Update `tela` package to handle smart contract versioning. 8 | * Update `tela` package to handle DocShards. 9 | * Update `tela` package to handle DOC compression. 10 | * Add `TELA-GO-1` as explicit golang docType. 11 | * Add CLI `functions_test.go` file 12 | * Add CLI `search exclude` commands. 13 | * Add CLI `set-var` and `delete-var` commands. 14 | * Add CLI `search` by key, value, all variables and line of code. 15 | * Add CLI `balance`, `sc-execute` and `gnomon add` commands. 16 | * Add CLI `file-info`, `file-shard` and `file-construct` commands. 17 | * Add CLI `file-diff` and `scid-diff` commands. 18 | * Add CLI `MODs` commands, search information and install/updating options. 19 | * Add CLI Git clone. 20 | * Fix CLI `--testnet` flag overwrite 21 | * Add CLI close handling on daemon disconnect 22 | * Add CLI file compress option and handling in applicable commands. 23 | * Add CLI gas estimate display. 24 | * Add `.bootstrap` INDEX dURL tag. 25 | * Add validation for on and off chain images. 26 | * Add DOC helper methods for extracting DocCode, SVGs and `` tags. 27 | * Add `var_` standard header support in addition to the existing ART-NFA standard headers. 28 | * Update test site webkit-scrollbar and favIcon 29 | * Update test site websocket methods. 30 | * Update repo documentation. 31 | 32 | 33 | ## TELA v1.0.0 - August 23 2024 34 | 35 | * Publish TELA go package source code along with TELA-CLI cmd. 36 | -------------------------------------------------------------------------------- /TELA-INDEX-1/TELA-INDEX-1.bas: -------------------------------------------------------------------------------- 1 | // Copyright 2024. Civilware. All rights reserved. 2 | // TELA Decentralized Web Standard (TELA-INDEX-1) 3 | 4 | Function InitializePrivate() Uint64 5 | 10 IF init() == 0 THEN GOTO 30 6 | 20 RETURN 1 7 | 30 STORE("var_header_name", "") 8 | 31 STORE("var_header_description", "") 9 | 32 STORE("var_header_icon", "") 10 | 33 STORE("dURL", "") 11 | 34 STORE("mods", "") 12 | 40 STORE("DOC1", "") 13 | // 41 STORE("DOC2", "") 14 | // 42 STORE("DOC3", "") 15 | 1000 RETURN 0 16 | End Function 17 | 18 | Function init() Uint64 19 | 10 IF EXISTS("owner") == 0 THEN GOTO 30 20 | 20 RETURN 1 21 | 30 STORE("owner", address()) 22 | 50 STORE("telaVersion", "1.1.0") // TELA SC version 23 | 60 STORE("commit", 0) // The initial commit 24 | 70 STORE(0, HEX(TXID())) // SCID commit hash 25 | 80 STORE("hash", HEX(TXID())) 26 | 85 STORE("likes", 0) 27 | 90 STORE("dislikes", 0) 28 | 100 RETURN 0 29 | End Function 30 | 31 | Function address() String 32 | 10 DIM s as String 33 | 20 LET s = SIGNER() 34 | 30 IF IS_ADDRESS_VALID(s) THEN GOTO 50 35 | 40 RETURN "anon" 36 | 50 RETURN ADDRESS_STRING(s) 37 | End Function 38 | 39 | Function Rate(r Uint64) Uint64 40 | 10 DIM addr as String 41 | 15 LET addr = address() 42 | 16 IF r < 100 && EXISTS(addr) == 0 && addr != "anon" THEN GOTO 30 43 | 20 RETURN 1 44 | 30 STORE(addr, ""+r+"_"+BLOCK_HEIGHT()) 45 | 40 IF r < 50 THEN GOTO 70 46 | 50 STORE("likes", LOAD("likes")+1) 47 | 60 RETURN 0 48 | 70 STORE("dislikes", LOAD("dislikes")+1) 49 | 100 RETURN 0 50 | End Function 51 | 52 | Function UpdateCode(code String, mods String) Uint64 53 | 10 IF LOAD("owner") == "anon" THEN GOTO 20 54 | 15 IF code == "" THEN GOTO 20 55 | 16 IF LOAD("owner") == address() THEN GOTO 30 56 | 20 RETURN 1 57 | 30 UPDATE_SC_CODE(code) 58 | 40 STORE("commit", LOAD("commit")+1) // New commit 59 | 50 STORE(LOAD("commit"), HEX(TXID())) // New hash 60 | 60 STORE("hash", HEX(TXID())) 61 | 70 STORE("mods", mods) 62 | 100 RETURN 0 63 | End Function -------------------------------------------------------------------------------- /logger/colors.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | // ANSI colors 4 | const ( 5 | ANSIblack = "\033[30m" 6 | ANSIred = "\033[31m" 7 | ANSIgreen = "\033[32m" 8 | ANSIyellow = "\033[33m" 9 | ANSIblue = "\033[34m" 10 | ANSImagenta = "\033[35m" 11 | ANSIcyan = "\033[36m" 12 | ANSIwhite = "\033[37m" 13 | ANSIdefault = "\033[39m" 14 | ANSIgrey = "\033[90m" 15 | ANSIend = "\033[0m" 16 | ) 17 | 18 | type colors struct { 19 | black string 20 | red string 21 | green string 22 | yellow string 23 | blue string 24 | magenta string 25 | cyan string 26 | white string 27 | fallback string 28 | grey string 29 | end string 30 | } 31 | 32 | // Colors for logger package, enabled or disabled with EnableColors() 33 | var Color = colors{ 34 | black: ANSIblack, 35 | red: ANSIred, 36 | green: ANSIgreen, 37 | yellow: ANSIyellow, 38 | blue: ANSIblue, 39 | magenta: ANSImagenta, 40 | cyan: ANSIcyan, 41 | white: ANSIwhite, 42 | fallback: ANSIdefault, 43 | grey: ANSIgrey, 44 | end: ANSIend, 45 | } 46 | 47 | // ANSI color black 48 | func (c *colors) Black() string { 49 | return c.black 50 | } 51 | 52 | // ANSI color red 53 | func (c *colors) Red() string { 54 | return c.red 55 | } 56 | 57 | // ANSI color green 58 | func (c *colors) Green() string { 59 | return c.green 60 | } 61 | 62 | // ANSI color yellow 63 | func (c *colors) Yellow() string { 64 | return c.yellow 65 | } 66 | 67 | // ANSI color blue 68 | func (c *colors) Blue() string { 69 | return c.blue 70 | } 71 | 72 | // ANSI color magenta 73 | func (c *colors) Magenta() string { 74 | return c.magenta 75 | } 76 | 77 | // ANSI color cyan 78 | func (c *colors) Cyan() string { 79 | return c.cyan 80 | } 81 | 82 | // ANSI color white 83 | func (c *colors) White() string { 84 | return c.white 85 | } 86 | 87 | // ANSI color default 88 | func (c *colors) Default() string { 89 | return c.fallback 90 | } 91 | 92 | // ANSI color grey 93 | func (c *colors) Grey() string { 94 | return c.grey 95 | } 96 | 97 | // ANSI color end 98 | func (c *colors) End() string { 99 | return c.end 100 | } 101 | -------------------------------------------------------------------------------- /logger/ascii.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Small TELA ASCII logo 9 | var ASCIISmall = []string{ 10 | Color.red + ` ;;;` + Color.end, 11 | Color.red + ` ;;;` + Color.end, 12 | 13 | ` .WWW`, 14 | `::::::::::kMMM`, 15 | `WMMMMMMMMMMMMM`, 16 | `WMM, 'MMM`, 17 | `WMMx......dMMM`, 18 | `WMMMMMMMMMMMMM`, 19 | `WMM;`, 20 | `NWW'`, 21 | 22 | Color.green + `ooo. ` + Color.end, 23 | Color.green + `''' ` + Color.end, 24 | } 25 | 26 | // Main TELA ASCII logo 27 | var ASCIIMain = []string{ 28 | Color.red + ` ;;;;;` + Color.end, 29 | Color.red + ` ;;;;;` + Color.end, 30 | Color.red + ` ooooo` + Color.end, 31 | ` MMMMM`, 32 | ` MMMMM`, 33 | `NWWWWWWWWWWWWWWMMMMM`, 34 | `WMMMMMMMMMMMMMMMMMMM`, 35 | `WMMMM MMMMM`, 36 | `WMMMM MMMMM`, 37 | `WMMMM;;;;;;;;;;MMMMM`, 38 | `WMMMMMMMMMMMMMMMMMMM`, 39 | `WMMMM`, 40 | `WMMMM`, 41 | `WMMMM`, 42 | 43 | Color.green + `dxxxx ` + Color.end, 44 | Color.green + `loooo ` + Color.end, 45 | Color.green + `''''' ` + Color.end, 46 | } 47 | 48 | // Blend ASCII into existing string for display purposes 49 | func ASCIIBlend(ascii, info []string) { 50 | if info == nil { 51 | return 52 | } 53 | 54 | // find longest line and use it as margin 55 | var marginLen int 56 | for _, line := range info { 57 | l := len(line) 58 | if l > marginLen { 59 | marginLen = l + 4 60 | } 61 | } 62 | 63 | var printed int 64 | asciiLen := len(ascii) 65 | if marginLen < len(ascii[0]) { 66 | marginLen = len(ascii[0]) + 11 67 | } 68 | 69 | for i, line := range info { 70 | var margin string 71 | if i < asciiLen { 72 | linePad := marginLen - len(line) 73 | margin = strings.Repeat(" ", linePad) 74 | fmt.Println(line + margin + ascii[i]) 75 | } else { 76 | fmt.Println(line) 77 | } 78 | printed++ 79 | } 80 | 81 | for printed < asciiLen { 82 | fmt.Println(strings.Repeat(" ", marginLen-1), ascii[printed]) 83 | printed++ 84 | } 85 | } 86 | 87 | // Print TELA ASCII main or small logo 88 | func ASCIIPrint(small bool) { 89 | ascii := ASCIIMain 90 | if small { 91 | ascii = ASCIISmall 92 | } 93 | 94 | for _, line := range ascii { 95 | fmt.Println(line) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tela_tests/app2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TELA Demo Application 2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 | 27 | 29 | 30 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 | dero.io 44 | 45 |
46 |
47 |

Crafted by Civilware

48 |
49 |

2024

50 |
51 |
52 | 53 | 54 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /headers.go: -------------------------------------------------------------------------------- 1 | package tela 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Standard SC header value stores 9 | type Headers struct { 10 | NameHdr string `json:"nameHdr"` // On-chain name of SC. For TELA-DOCs, they are recreated using this as the file name, it should include the file extension 11 | DescrHdr string `json:"descrHdr"` // On-chain description of DOC, INDEX or Asset SC 12 | IconHdr string `json:"iconHdr"` // On-chain icon URL, (size 100x100) 13 | } 14 | 15 | // DERO signature 16 | type Signature struct { 17 | CheckC string `json:"checkC"` // C signature value 18 | CheckS string `json:"checkS"` // S signature value 19 | } 20 | 21 | // Standard SC header keys 22 | type Header string 23 | 24 | const ( 25 | HEADER_NAME Header = `"nameHdr"` 26 | HEADER_DESCRIPTION Header = `"descrHdr"` 27 | HEADER_ICON_URL Header = `"iconURLHdr"` 28 | HEADER_CHECK_C Header = `"fileCheckC"` 29 | HEADER_CHECK_S Header = `"fileCheckS"` 30 | HEADER_MODS Header = `"mods"` 31 | HEADER_DURL Header = `"dURL"` 32 | HEADER_DOCUMENT Header = `"DOC` // append with Number() 33 | HEADER_SUBDIR Header = `"subDir"` 34 | HEADER_DOCTYPE Header = `"docType"` 35 | HEADER_COLLECTION Header = `"collection"` 36 | HEADER_TYPE Header = `"typeHdr"` 37 | HEADER_TAGS Header = `"tagsHdr"` 38 | HEADER_FILE_URL Header = `"fileURL"` 39 | HEADER_SIGN_URL Header = `"fileSignURL"` 40 | HEADER_COVER_URL Header = `"coverURL"` 41 | HEADER_ART_FEE Header = `"artificerFee"` 42 | HEADER_ROYALTY Header = `"royalty"` 43 | HEADER_OWNER Header = `"owner"` 44 | HEADER_OWNER_UPDATE Header = `"ownerCanUpdate"` 45 | 46 | HEADER_NAME_V2 Header = `"var_header_name"` 47 | HEADER_DESCRIPTION_V2 Header = `"var_header_description"` 48 | HEADER_ICON_URL_V2 Header = `"var_header_icon"` 49 | 50 | LINE_MODS_STORE = `34 STORE("mods", ` 51 | LINE_DOC_VERSION = `50 STORE("docVersion", ` 52 | LINE_INDEX_VERSION = `50 STORE("telaVersion", ` 53 | ) 54 | 55 | // Trim any `"` from Header and return string 56 | func (h Header) Trim() string { 57 | return strings.Trim(string(h), `"`) 58 | } 59 | 60 | // Returns if Header can be appended. Headers ending in `"` or with len < 2 will return false 61 | func (h Header) CanAppend() bool { 62 | i := len(h) - 1 63 | if i < 1 { 64 | return false 65 | } 66 | 67 | return h[i] != byte(0x22) 68 | } 69 | 70 | // Append a number to Header if applicable, otherwise return unchanged Header 71 | func (h Header) Number(i int) Header { 72 | if !h.CanAppend() { 73 | return h 74 | } 75 | 76 | return Header(fmt.Sprintf(`%s%d"`, h, i)) 77 | } 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/civilware/tela 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.5 6 | 7 | require ( 8 | github.com/chzyer/readline v1.5.1 9 | github.com/civilware/Gnomon v0.0.0-20240403103529-8b2fdb2b3106 10 | github.com/creachadair/jrpc2 v0.35.4 11 | github.com/deroproject/derohe v0.0.0-20240405032004-bd300c0e086e 12 | github.com/deroproject/graviton v0.0.0-20220130070622-2c248a53b2e1 13 | github.com/gorilla/websocket v1.5.0 14 | github.com/stretchr/testify v1.8.4 15 | go.etcd.io/bbolt v1.3.7 16 | ) 17 | 18 | replace github.com/deroproject/derohe => github.com/civilware/derohe v0.0.0-20240909003240-fa76d6016cc6 19 | 20 | require ( 21 | github.com/VictoriaMetrics/metrics v1.23.1 // indirect 22 | github.com/beevik/ntp v1.2.0 // indirect 23 | github.com/blang/semver/v4 v4.0.0 // indirect 24 | github.com/caarlos0/env/v6 v6.10.1 // indirect 25 | github.com/cenkalti/hub v1.0.1 // indirect 26 | github.com/cenkalti/rpc2 v0.0.0-20210604223624-c1acbc6ec984 // indirect 27 | github.com/cespare/xxhash v1.1.0 // indirect 28 | github.com/coder/websocket v1.8.12 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/dchest/siphash v1.2.3 // indirect 31 | github.com/dustin/go-humanize v1.0.1 // indirect 32 | github.com/fsnotify/fsnotify v1.7.0 // indirect 33 | github.com/fxamacker/cbor/v2 v2.4.0 // indirect 34 | github.com/go-logr/logr v1.2.3 // indirect 35 | github.com/go-logr/zapr v1.2.3 // indirect 36 | github.com/hashicorp/golang-lru v0.5.1 // indirect 37 | github.com/klauspost/cpuid/v2 v2.2.6 // indirect 38 | github.com/klauspost/reedsolomon v1.11.5 // indirect 39 | github.com/lesismal/llib v1.1.10 // indirect 40 | github.com/lesismal/nbio v1.3.9 // indirect 41 | github.com/mattn/go-colorable v0.1.13 // indirect 42 | github.com/mattn/go-isatty v0.0.16 // indirect 43 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 44 | github.com/minio/sha256-simd v1.0.0 // indirect 45 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 46 | github.com/nxadm/tail v1.4.8 // indirect 47 | github.com/pmezard/go-difflib v1.0.0 // indirect 48 | github.com/robfig/cron/v3 v3.0.1 // indirect 49 | github.com/satori/go.uuid v1.2.0 // indirect 50 | github.com/segmentio/fasthash v1.0.3 // indirect 51 | github.com/sirupsen/logrus v1.9.3 // indirect 52 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect 53 | github.com/x448/float16 v0.8.4 // indirect 54 | github.com/xtaci/kcp-go/v5 v5.6.2 // indirect 55 | go.uber.org/atomic v1.10.0 // indirect 56 | go.uber.org/multierr v1.9.0 // indirect 57 | go.uber.org/zap v1.24.0 // indirect 58 | golang.org/x/crypto v0.17.0 // indirect 59 | golang.org/x/net v0.19.0 // indirect 60 | golang.org/x/sync v0.5.0 // indirect 61 | golang.org/x/sys v0.15.0 // indirect 62 | golang.org/x/term v0.15.0 // indirect 63 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect 64 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 65 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /compression.go: -------------------------------------------------------------------------------- 1 | package tela 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "compress/gzip" 11 | ) 12 | 13 | const ( 14 | COMPRESSION_GZIP = ".gz" 15 | ) 16 | 17 | // Compression formats this package will use 18 | var compressionFormats = []string{ 19 | COMPRESSION_GZIP, 20 | } 21 | 22 | // TrimCompressedExt removes known compression extensions from the filename 23 | func TrimCompressedExt(fileName string) string { 24 | if fileName == "" { 25 | return fileName 26 | } 27 | 28 | for _, comp := range compressionFormats { 29 | fileName = strings.TrimSuffix(fileName, comp) 30 | } 31 | 32 | return fileName 33 | } 34 | 35 | // IsCompressedExt returns true if ext is a valid TELA compression format 36 | func IsCompressedExt(ext string) bool { 37 | if ext == "" { 38 | return false 39 | } 40 | 41 | for _, comp := range compressionFormats { 42 | if ext == comp { 43 | return true 44 | } 45 | } 46 | 47 | return false 48 | } 49 | 50 | // Decompress TELA data using the given compression format, if compression is "" result will return the original data 51 | func Decompress(data []byte, compression string) (result []byte, err error) { 52 | switch compression { 53 | case COMPRESSION_GZIP: 54 | result, err = decompressGzip(data) 55 | if err != nil { 56 | return 57 | } 58 | case "": 59 | result = data 60 | default: 61 | err = fmt.Errorf("unknown decompression format %q", compression) 62 | } 63 | 64 | return 65 | } 66 | 67 | // Compress TELA data using the given compression format 68 | func Compress(data []byte, compression string) (result string, err error) { 69 | switch compression { 70 | case COMPRESSION_GZIP: 71 | result, err = compressGzip(data) 72 | if err != nil { 73 | return 74 | } 75 | default: 76 | err = fmt.Errorf("unknown compression format %q", compression) 77 | } 78 | 79 | return 80 | } 81 | 82 | // Compress data as gzip then encode it in base64 and return the result 83 | func compressGzip(data []byte) (result string, err error) { 84 | var buf bytes.Buffer 85 | var gz *gzip.Writer 86 | gz, err = gzip.NewWriterLevel(&buf, gzip.BestCompression) 87 | if err != nil { 88 | return 89 | } 90 | defer gz.Close() 91 | 92 | _, err = gz.Write(data) 93 | if err != nil { 94 | return 95 | } 96 | 97 | // Ensure all data is written to the buffer 98 | err = gz.Close() 99 | if err != nil { 100 | return 101 | } 102 | 103 | result = base64.StdEncoding.EncodeToString(buf.Bytes()) 104 | 105 | return 106 | } 107 | 108 | // Decompress base64 encoded gzip data and return the result 109 | func decompressGzip(data []byte) (result []byte, err error) { 110 | var decoded []byte 111 | decoded, err = base64.StdEncoding.DecodeString(string(data)) 112 | if err != nil { 113 | return 114 | } 115 | 116 | var gz *gzip.Reader 117 | gz, err = gzip.NewReader(bytes.NewReader(decoded)) 118 | if err != nil { 119 | return 120 | } 121 | defer gz.Close() 122 | 123 | var decompressed []byte 124 | decompressed, err = io.ReadAll(gz) 125 | if err != nil { 126 | return 127 | } 128 | 129 | result = decompressed 130 | 131 | return 132 | } 133 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/deroproject/derohe/globals" 10 | ) 11 | 12 | const ( 13 | timestampFormat = "01/02/2006 15:04:05" 14 | 15 | DEBUG = "DEBUG" 16 | INFO = "INFO" 17 | WARN = "WARN" 18 | ERROR = "ERROR" 19 | FATAL = "FATAL" 20 | ) 21 | 22 | type level struct { 23 | debug string 24 | info string 25 | warn string 26 | err string 27 | fatal string 28 | } 29 | 30 | var tag = level{ 31 | debug: Color.blue + DEBUG + Color.end, 32 | info: Color.green + INFO + Color.end, 33 | warn: Color.yellow + WARN + Color.end, 34 | err: Color.red + ERROR + Color.end, 35 | fatal: Color.red + FATAL + Color.end, 36 | } 37 | 38 | // Enable or disable colors 39 | func EnableColors(b bool) { 40 | if b { 41 | Color.black = ANSIblack 42 | Color.red = ANSIred 43 | Color.green = ANSIgreen 44 | Color.yellow = ANSIyellow 45 | Color.blue = ANSIblue 46 | Color.magenta = ANSImagenta 47 | Color.cyan = ANSIcyan 48 | Color.white = ANSIwhite 49 | Color.fallback = ANSIdefault 50 | Color.grey = ANSIgrey 51 | Color.end = ANSIend 52 | 53 | tag.debug = Color.blue + DEBUG + Color.end 54 | tag.info = Color.green + INFO + Color.end 55 | tag.warn = Color.yellow + WARN + Color.end 56 | tag.err = Color.red + ERROR + Color.end 57 | tag.fatal = Color.red + FATAL + Color.end 58 | } else { 59 | Color.black = "" 60 | Color.red = "" 61 | Color.green = "" 62 | Color.yellow = "" 63 | Color.blue = "" 64 | Color.magenta = "" 65 | Color.cyan = "" 66 | Color.white = "" 67 | Color.fallback = "" 68 | Color.grey = "" 69 | Color.end = "" 70 | 71 | tag.debug = DEBUG 72 | tag.info = INFO 73 | tag.warn = WARN 74 | tag.err = ERROR 75 | tag.fatal = FATAL 76 | } 77 | } 78 | 79 | // Timestamp returns the current local timestamp as a formatted string 80 | func Timestamp() string { 81 | return fmt.Sprintf("%s[%s]%s", Color.grey, time.Now().Format(timestampFormat), Color.end) 82 | } 83 | 84 | // sprint formats a log string with [source] if provided otherwise returns given string 85 | func sprint(s string) string { 86 | start := strings.Index(s, "[") 87 | end := strings.Index(s, "]") 88 | if start != -1 && end != -1 { 89 | if source := s[start+1:end] + ":"; source != ":" { 90 | s = fmt.Sprintf("%s%s%s %s", Color.cyan, source, Color.end, s[end+2:]) 91 | } 92 | } 93 | 94 | return s 95 | } 96 | 97 | // printf formats log text with a message tag and prints to console 98 | func printf(tag, text string) { 99 | fmt.Printf("%s %s %s", Timestamp(), tag, sprint(text)) 100 | } 101 | 102 | // Debugf prints a format specified string with DEBUG message tag if globals.Arguments["--debug"] 103 | func Debugf(format string, a ...any) { 104 | if globals.Arguments["--debug"] != nil && globals.Arguments["--debug"].(bool) { 105 | text := fmt.Sprintf(format, a...) 106 | printf(tag.debug, text) 107 | } 108 | } 109 | 110 | // Printf prints a format specified string with INFO message tag 111 | func Printf(format string, a ...any) { 112 | text := fmt.Sprintf(format, a...) 113 | printf(tag.info, text) 114 | } 115 | 116 | // Warnf prints a format specified string with WARN message tag 117 | func Warnf(format string, a ...any) { 118 | text := fmt.Sprintf(format, a...) 119 | printf(tag.warn, text) 120 | } 121 | 122 | // Errorf prints a format specified string with ERROR message tag 123 | func Errorf(format string, a ...any) { 124 | text := fmt.Sprintf(format, a...) 125 | printf(tag.err, text) 126 | } 127 | 128 | // Fatalf prints a format specified string with FATAL message tag followed by a call to [os.Exit](1) 129 | func Fatalf(format string, a ...any) { 130 | text := fmt.Sprintf(format, a...) 131 | printf(tag.fatal, text) 132 | os.Exit(1) 133 | } 134 | -------------------------------------------------------------------------------- /logger/ascii_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestASCIIBlend(t *testing.T) { 11 | var margin []string 12 | margin = append(margin, "------------------------------") 13 | margin = append(margin, "------------------------") 14 | 15 | defaultSpace := " " 16 | 17 | var info [][]string 18 | info = append(info, []string{margin[0]}) 19 | info = append(info, []string{margin[1]}) 20 | info = append(info, []string{" "}) 21 | info = append(info, []string{""}) 22 | 23 | expectedOutput1 := margin[0] + ` ` + ASCIISmall[0] + "\n" + 24 | `Some info 0 ` + ASCIISmall[1] + "\n" + 25 | `Some info 1 ` + ASCIISmall[2] + "\n" + 26 | `Some info 2 ` + ASCIISmall[3] + "\n" + 27 | defaultSpace + ASCIISmall[4] + "\n" + 28 | defaultSpace + ASCIISmall[5] + "\n" + 29 | defaultSpace + ASCIISmall[6] + "\n" + 30 | defaultSpace + ASCIISmall[7] + "\n" + 31 | defaultSpace + ASCIISmall[8] + "\n" + 32 | defaultSpace + ASCIISmall[9] + "\n" + 33 | defaultSpace + ASCIISmall[10] + "\n" + 34 | defaultSpace + ASCIISmall[11] + "\n" 35 | 36 | // Match info line for expectedOutput1 37 | for i := 0; i < 3; i++ { 38 | info[0] = append(info[0], fmt.Sprintf("Some info %d", i)) 39 | } 40 | 41 | expectedOutput2 := margin[1] + ` ` + ASCIISmall[0] + "\n" + 42 | `Some info 0 ` + ASCIISmall[1] + "\n" + 43 | `Some info 1 ` + ASCIISmall[2] + "\n" + 44 | `Some info 2 ` + ASCIISmall[3] + "\n" + 45 | `Some info 3 ` + ASCIISmall[4] + "\n" + 46 | `Some info 4 ` + ASCIISmall[5] + "\n" + 47 | `Some info 5 ` + ASCIISmall[6] + "\n" + 48 | `Some info 6 ` + ASCIISmall[7] + "\n" + 49 | `Some info 7 ` + ASCIISmall[8] + "\n" + 50 | `Some info 8 ` + ASCIISmall[9] + "\n" + 51 | `Some info 9 ` + ASCIISmall[10] + "\n" + 52 | `Some info 10 ` + ASCIISmall[11] + "\n" + 53 | `Some info 11` + "\n" + `Some info 12` + "\n" + `Some info 13` + "\n" + `Some info 14` + "\n" 54 | 55 | // Match info line for expectedOutput2 56 | for i := 0; i < 15; i++ { 57 | info[1] = append(info[1], fmt.Sprintf("Some info %d", i)) 58 | } 59 | 60 | var expectedOutput3 string 61 | for _, line := range ASCIISmall { 62 | expectedOutput3 = expectedOutput3 + defaultSpace + line + "\n" 63 | } 64 | 65 | var expectedOutput4 string 66 | for _, line := range ASCIISmall { 67 | expectedOutput4 = expectedOutput4 + defaultSpace + line + "\n" 68 | } 69 | 70 | var expectedOutputs []string 71 | expectedOutputs = append(expectedOutputs, expectedOutput1) 72 | expectedOutputs = append(expectedOutputs, expectedOutput2) 73 | expectedOutputs = append(expectedOutputs, expectedOutput3) 74 | expectedOutputs = append(expectedOutputs, expectedOutput4) 75 | 76 | for i, eo := range expectedOutputs { 77 | output := captureOutput(func() { 78 | ASCIIBlend(ASCIISmall, info[i]) 79 | }) 80 | 81 | assert.Equal(t, eo, output, "Blend output %d should be the same", i) 82 | 83 | // Test with nil info 84 | output = captureOutput(func() { 85 | ASCIIBlend(ASCIISmall, nil) 86 | }) 87 | 88 | assert.Empty(t, output, "Blending nil %d should be empty", i) 89 | } 90 | } 91 | 92 | func TestASCIIPrint(t *testing.T) { 93 | var expectedOutputMain string 94 | for _, line := range ASCIIMain { 95 | expectedOutputMain = expectedOutputMain + line + "\n" 96 | } 97 | 98 | var expectedOutputSmall string 99 | for _, line := range ASCIISmall { 100 | expectedOutputSmall = expectedOutputSmall + line + "\n" 101 | } 102 | 103 | output := captureOutput(func() { 104 | ASCIIPrint(false) 105 | }) 106 | 107 | assert.Equal(t, expectedOutputMain, output, "Main output should be the same") 108 | 109 | output = captureOutput(func() { 110 | ASCIIPrint(true) 111 | }) 112 | 113 | assert.Equal(t, expectedOutputSmall, output, "Small output should be the same") 114 | } 115 | -------------------------------------------------------------------------------- /ratings.go: -------------------------------------------------------------------------------- 1 | package tela 2 | 3 | import "fmt" 4 | 5 | // Common detail tags 6 | const ( 7 | detail_Nothing = "Nothing" 8 | detail_Needs_Review = "Needs review" 9 | detail_Needs_Improvement = "Needs improvement" 10 | detail_Bugs = "Bugs" 11 | detail_Errors = "Errors" 12 | ) 13 | 14 | // A TELA SC rating 15 | type Rating struct { 16 | Address string `json:"address"` // Address of the rater 17 | Rating uint64 `json:"rating"` // The 0-99 rating number 18 | Height uint64 `json:"height"` // The block height this rating occurred 19 | } 20 | 21 | // TELA SC rating structure 22 | type Rating_Result struct { 23 | Ratings []Rating `json:"ratings,omitempty"` // All ratings for a SC 24 | Likes uint64 `json:"likes"` // Likes for a SC 25 | Dislikes uint64 `json:"dislikes"` // Dislikes for a SC 26 | Average float64 `json:"average"` // Average category value of all ratings, will be 0-10 27 | } 28 | 29 | // TELA ratings variable structure 30 | type ratings struct { 31 | categories map[uint64]string 32 | negativeDetails map[uint64]string 33 | positiveDetails map[uint64]string 34 | } 35 | 36 | // Access TELA ratings variables and functions 37 | var Ratings ratings 38 | 39 | // Initialize the rating values 40 | func initRatings() { 41 | Ratings.categories = map[uint64]string{ 42 | 0: "Do not use", 43 | 1: "Broken", 44 | 2: "Major issues", 45 | 3: "Minor issues", 46 | 4: "Should be improved", 47 | 5: "Could be improved", 48 | 6: "Average", 49 | 7: "Good", 50 | 8: "Very good", 51 | 9: "Exceptional", 52 | } 53 | 54 | Ratings.negativeDetails = map[uint64]string{ 55 | 0: detail_Nothing, 56 | 1: detail_Needs_Review, 57 | 2: detail_Needs_Improvement, 58 | 3: detail_Bugs, 59 | 4: detail_Errors, 60 | 5: "Inappropriate", 61 | 6: "Incomplete", 62 | 7: "Corrupted", 63 | 8: "Plagiarized", 64 | 9: "Malicious", 65 | } 66 | 67 | Ratings.positiveDetails = map[uint64]string{ 68 | 0: detail_Nothing, 69 | 1: detail_Needs_Review, 70 | 2: detail_Needs_Improvement, 71 | 3: detail_Bugs, 72 | 4: detail_Errors, 73 | 5: "Visually appealing", 74 | 6: "In depth", 75 | 7: "Works well", 76 | 8: "Unique", 77 | 9: "Benevolent", 78 | } 79 | } 80 | 81 | // Returns all TELA rating categories 82 | func (rate *ratings) Categories() (categories map[uint64]string) { 83 | categories = map[uint64]string{} 84 | for u, c := range rate.categories { 85 | categories[u] = c 86 | } 87 | 88 | return 89 | } 90 | 91 | // Returns a TELA rating category if exists, otherwise empty string 92 | func (rate *ratings) Category(r uint64) (category string) { 93 | category = rate.categories[r] 94 | 95 | return 96 | } 97 | 98 | // Returns all negative TELA rating detail tags 99 | func (rate *ratings) NegativeDetails() (details map[uint64]string) { 100 | details = map[uint64]string{} 101 | for u, c := range rate.negativeDetails { 102 | details[u] = c 103 | } 104 | 105 | return 106 | } 107 | 108 | // Returns all positive TELA rating detail tags 109 | func (rate *ratings) PositiveDetails() (details map[uint64]string) { 110 | details = map[uint64]string{} 111 | for u, c := range rate.positiveDetails { 112 | details[u] = c 113 | } 114 | 115 | return 116 | } 117 | 118 | // Returns a TELA detail tag if exists, otherwise empty string 119 | func (rate *ratings) Detail(r uint64, isPositive bool) (detail string) { 120 | if isPositive { 121 | detail = rate.positiveDetails[r] 122 | } else { 123 | detail = rate.negativeDetails[r] 124 | } 125 | 126 | return 127 | } 128 | 129 | // Parse r for its corresponding TELA rating category and detail 130 | func (rate *ratings) Parse(r uint64) (category string, detail string, err error) { 131 | fP := r / 10 132 | sP := r % 10 133 | 134 | var ok bool 135 | if category, ok = rate.categories[fP]; !ok { 136 | err = fmt.Errorf("unknown category") 137 | return 138 | } 139 | 140 | isPositive := fP >= 5 141 | detail = rate.Detail(sP, isPositive) 142 | if detail == "" { 143 | err = fmt.Errorf("unknown detail") 144 | } 145 | 146 | return 147 | } 148 | 149 | // Parse r for its corresponding TELA rating string 150 | func (rate *ratings) ParseString(r uint64) (rating string, err error) { 151 | category, detail, err := rate.Parse(r) 152 | if err != nil { 153 | return 154 | } 155 | 156 | if detail == detail_Nothing { 157 | rating = category 158 | } else { 159 | rating = fmt.Sprintf("%s (%s)", category, detail) 160 | } 161 | 162 | return 163 | } 164 | 165 | // Parse the average rating result for its category string 166 | func (res *Rating_Result) ParseAverage() (category string) { 167 | category, _, _ = Ratings.Parse(uint64(res.Average)) 168 | return 169 | } 170 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "testing" 9 | 10 | "github.com/deroproject/derohe/globals" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // Capture the console output 15 | func captureOutput(f func()) string { 16 | var buf bytes.Buffer 17 | stdout := os.Stdout 18 | r, w, _ := os.Pipe() 19 | os.Stdout = w 20 | 21 | f() 22 | 23 | w.Close() 24 | os.Stdout = stdout 25 | buf.ReadFrom(r) 26 | return buf.String() 27 | } 28 | 29 | func TestEnableColors(t *testing.T) { 30 | EnableColors(false) 31 | assert.Empty(t, Color.Red(), "Color red should be empty when colors are disabled") 32 | assert.Empty(t, Color.End(), "Color end should be empty when colors are disabled") 33 | assert.Equal(t, INFO, tag.info, "INFO tag should match when colors are disabled") 34 | assert.Equal(t, ERROR, tag.err, "ERROR tag should match when colors are disabled") 35 | 36 | EnableColors(true) 37 | expectedInfo := ANSIgreen + INFO + ANSIend 38 | expectedError := ANSIred + ERROR + ANSIend 39 | assert.Equal(t, ANSIblack, Color.Black(), "Color black should match ANSI when colors are enabled") 40 | assert.Equal(t, ANSIred, Color.Red(), "Color red should match ANSI when colors are enabled") 41 | assert.Equal(t, ANSIgreen, Color.Green(), "Color green should match ANSI when colors are enabled") 42 | assert.Equal(t, ANSIyellow, Color.Yellow(), "Color yellow should match ANSI when colors are enabled") 43 | assert.Equal(t, ANSIblue, Color.Blue(), "Color blue should match ANSI when colors are enabled") 44 | assert.Equal(t, ANSImagenta, Color.Magenta(), "Color magenta should match ANSI when colors are enabled") 45 | assert.Equal(t, ANSIcyan, Color.Cyan(), "Color cyan should match ANSI when colors are enabled") 46 | assert.Equal(t, ANSIwhite, Color.White(), "Color white should match ANSI when colors are enabled") 47 | assert.Equal(t, ANSIdefault, Color.Default(), "Color default should match ANSI when colors are enabled") 48 | assert.Equal(t, ANSIgrey, Color.Grey(), "Color grey should match ANSI when colors are enabled") 49 | assert.Equal(t, ANSIend, Color.End(), "Color end should match ANSI when colors are enabled") 50 | assert.Equal(t, expectedInfo, tag.info, "INFO tag should match when colors are enabled") 51 | assert.Equal(t, expectedError, tag.err, "ERROR tag should match when colors are enabled") 52 | } 53 | 54 | func TestTimestamp(t *testing.T) { 55 | stamp := Timestamp() 56 | assert.Contains(t, stamp, "[", "Timestamp missing left format") 57 | assert.Contains(t, stamp, "]", "Timestamp missing right format") 58 | } 59 | 60 | func TestSprint(t *testing.T) { 61 | result := sprint("[source] message") 62 | expected := fmt.Sprintf("%s%s:%s message", Color.Cyan(), "source", Color.End()) 63 | assert.Equal(t, expected, result, "Expected sprint to parse source tag") 64 | 65 | message := "message" 66 | result = sprint(message) 67 | assert.Equal(t, message, result, "Expected message without source to remain the same") 68 | } 69 | 70 | func TestDebugf(t *testing.T) { 71 | format := "Hello %s %d %t" 72 | a := []any{"TELA", 1, true} 73 | 74 | globals.Arguments["--debug"] = true 75 | output := captureOutput(func() { 76 | Debugf(format, a...) 77 | }) 78 | 79 | assert.Contains(t, output, DEBUG, "Expected output to contain DEBUG tag") 80 | assert.Contains(t, output, fmt.Sprintf(format, a...), "Expecting output to match format") 81 | 82 | globals.Arguments["--debug"] = false 83 | output = captureOutput(func() { 84 | Debugf(format, a...) 85 | }) 86 | 87 | assert.Empty(t, output, "Expected empty output when --debug is not enabled") 88 | } 89 | 90 | func TestPrintf(t *testing.T) { 91 | format := "Hello %s %d %t" 92 | a := []any{"TELA", 1, true} 93 | 94 | output := captureOutput(func() { 95 | Printf(format, a...) 96 | }) 97 | 98 | assert.Contains(t, output, INFO, "Expected output to contain INFO tag") 99 | assert.Contains(t, output, fmt.Sprintf(format, a...), "Expecting output to match format") 100 | } 101 | 102 | func TestWarnf(t *testing.T) { 103 | format := "Warning %s %d %t" 104 | a := []any{"issued", 1, true} 105 | 106 | output := captureOutput(func() { 107 | Warnf(format, a...) 108 | }) 109 | 110 | assert.Contains(t, output, WARN, "Expected output to contain WARN tag") 111 | assert.Contains(t, output, fmt.Sprintf(format, a...), "Expecting output to match format") 112 | } 113 | 114 | func TestErrorf(t *testing.T) { 115 | format := "Error %s %d %t" 116 | a := []any{"occurred", 1, true} 117 | 118 | output := captureOutput(func() { 119 | Errorf(format, a...) 120 | }) 121 | 122 | assert.Contains(t, output, ERROR, "Expected output to contain ERROR tag") 123 | assert.Contains(t, output, fmt.Sprintf(format, a...), "Expecting output to match format") 124 | } 125 | 126 | func TestFatalf(t *testing.T) { 127 | if os.Getenv("FLAG") == "1" { 128 | Fatalf("Fatalf test") 129 | } 130 | 131 | // Run the test in a subprocess 132 | cmd := exec.Command(os.Args[0], "-test.run=TestFatalf") 133 | cmd.Env = append(os.Environ(), "FLAG=1") 134 | err := cmd.Run() 135 | 136 | _, ok := err.(*exec.ExitError) 137 | expectedErrorString := "exit status 1" 138 | assert.True(t, ok, "Error should be %T", new(exec.ExitError)) 139 | assert.EqualError(t, err, expectedErrorString, "Error string should be equal") 140 | } 141 | -------------------------------------------------------------------------------- /shards/boltdb.go: -------------------------------------------------------------------------------- 1 | package shards 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/deroproject/derohe/walletapi" 10 | "go.etcd.io/bbolt" 11 | ) 12 | 13 | // Get shard and make directory for DB 14 | func boltGetShard(disk *walletapi.Wallet_Disk) (shard string, err error) { 15 | shard = GetShard(disk) 16 | err = os.MkdirAll(shard, os.ModePerm) 17 | return 18 | } 19 | 20 | // Encrypt a key-value and then store it in a bbolt DB 21 | func boltStoreEncryptedValue(disk *walletapi.Wallet_Disk, bucket string, key []byte, value []byte) (err error) { 22 | var shard string 23 | shard, err = boltGetShard(disk) 24 | if err != nil { 25 | return 26 | } 27 | 28 | var db *bbolt.DB 29 | db, err = bbolt.Open(filepath.Join(shard, filepath.Base(shard)+".db"), 0600, nil) 30 | if err != nil { 31 | return 32 | } 33 | 34 | err = db.Update(func(tx *bbolt.Tx) (err error) { 35 | b, err := tx.CreateBucketIfNotExists([]byte(bucket)) 36 | if err != nil { 37 | return 38 | } 39 | 40 | eValue, err := disk.Encrypt(value) 41 | if err != nil { 42 | return 43 | } 44 | 45 | return b.Put(key, eValue) 46 | }) 47 | 48 | db.Close() 49 | 50 | return 51 | } 52 | 53 | // Store a key-value in a bbolt DB 54 | func boltStoreValue(bucket string, key []byte, value []byte) (err error) { 55 | var shard string 56 | shard, err = boltGetShard(nil) 57 | if err != nil { 58 | return 59 | } 60 | 61 | var db *bbolt.DB 62 | db, err = bbolt.Open(filepath.Join(shard, "settings.db"), 0600, nil) 63 | if err != nil { 64 | return 65 | } 66 | 67 | err = db.Update(func(tx *bbolt.Tx) (err error) { 68 | b, err := tx.CreateBucketIfNotExists([]byte(bucket)) 69 | if err != nil { 70 | return 71 | } 72 | 73 | mar, err := json.Marshal(&value) 74 | if err != nil { 75 | return 76 | } 77 | 78 | return b.Put(key, mar) 79 | }) 80 | 81 | db.Close() 82 | 83 | return 84 | } 85 | 86 | // Get a key-value from a bbolt DB 87 | func boltGetValue(bucket string, key []byte) (result []byte, err error) { 88 | var shard string 89 | shard, err = boltGetShard(nil) 90 | if err != nil { 91 | return 92 | } 93 | 94 | var db *bbolt.DB 95 | db, err = bbolt.Open(filepath.Join(shard, "settings.db"), 0600, nil) 96 | if err != nil { 97 | return 98 | } 99 | 100 | err = db.View(func(tx *bbolt.Tx) (err error) { 101 | b := tx.Bucket([]byte(bucket)) 102 | if b == nil { 103 | return fmt.Errorf("bucket %q not found", bucket) 104 | } 105 | 106 | stored := b.Get(key) 107 | if stored != nil { 108 | return json.Unmarshal(stored, &result) 109 | } 110 | 111 | return fmt.Errorf("key %q not found", key) 112 | }) 113 | 114 | db.Close() 115 | 116 | return 117 | } 118 | 119 | // Store a key-value in bbolt DB settings bucket 120 | func boltStoreSettingsValue(key, value []byte) (err error) { 121 | return boltStoreValue("settings", key, value) 122 | } 123 | 124 | // Get a key-value from bbolt DB settings bucket 125 | func boltGetSettingsValue(key []byte) (result []byte, err error) { 126 | return boltGetValue("settings", key) 127 | } 128 | 129 | // Store endpoint value in bbolt DB settings bucket 130 | func boltStoreEndpoint(value []byte) (err error) { 131 | return boltStoreSettingsValue(Key.Endpoint(), value) 132 | } 133 | 134 | // Get endpoint value from bbolt DB settings bucket 135 | func boltGetEndpoint() (result []byte, err error) { 136 | return boltGetSettingsValue(Key.Endpoint()) 137 | } 138 | 139 | // Store network value in bbolt DB settings bucket 140 | func boltStoreNetwork(value []byte) (err error) { 141 | return boltStoreSettingsValue(Key.Network(), value) 142 | } 143 | 144 | // Get network value from bbolt DB settings bucket 145 | func boltGetNetwork() (result []byte, err error) { 146 | return boltGetSettingsValue(Key.Network()) 147 | } 148 | 149 | // Get an encrypted key-value from a bbolt DB and then decrypt it 150 | func boltGetEncryptedValue(disk *walletapi.Wallet_Disk, bucket string, key []byte) (result []byte, err error) { 151 | var shard string 152 | shard, err = boltGetShard(disk) 153 | if err != nil { 154 | return 155 | } 156 | 157 | var db *bbolt.DB 158 | db, err = bbolt.Open(filepath.Join(shard, filepath.Base(shard)+".db"), 0600, nil) 159 | if err != nil { 160 | return 161 | } 162 | 163 | err = db.View(func(tx *bbolt.Tx) (err error) { 164 | b := tx.Bucket([]byte(bucket)) 165 | if b == nil { 166 | return fmt.Errorf("bucket %q not found", bucket) 167 | } 168 | 169 | stored := b.Get(key) 170 | if stored == nil { 171 | return fmt.Errorf("key %q not found", key) 172 | } 173 | 174 | eValue, err := disk.Decrypt(stored) 175 | if err != nil { 176 | return 177 | } 178 | 179 | result = eValue 180 | 181 | return 182 | }) 183 | 184 | db.Close() 185 | 186 | return 187 | } 188 | 189 | // Delete a key-value in a bbolt DB 190 | func boltDeleteKey(disk *walletapi.Wallet_Disk, bucket string, key []byte) (err error) { 191 | shard := GetShard(disk) 192 | 193 | var db *bbolt.DB 194 | db, err = bbolt.Open(filepath.Join(shard, filepath.Base(shard)+".db"), 0600, nil) 195 | if err != nil { 196 | return 197 | } 198 | 199 | err = db.Update(func(tx *bbolt.Tx) (err error) { 200 | b := tx.Bucket([]byte(bucket)) 201 | if b == nil { 202 | return fmt.Errorf("bucket %q not found", bucket) 203 | } 204 | 205 | return b.Delete(key) 206 | }) 207 | 208 | db.Close() 209 | 210 | return 211 | } 212 | -------------------------------------------------------------------------------- /shards/gravdb.go: -------------------------------------------------------------------------------- 1 | package shards 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/deroproject/derohe/walletapi" 7 | "github.com/deroproject/graviton" 8 | ) 9 | 10 | // Encrypt a key-value and then store it in a Graviton tree 11 | func gravitonStoreEncryptedValue(disk *walletapi.Wallet_Disk, t string, key, value []byte) (err error) { 12 | if disk == nil { 13 | err = fmt.Errorf("no active account found") 14 | return 15 | } 16 | 17 | if t == "" { 18 | err = fmt.Errorf("missing graviton tree input") 19 | return 20 | } 21 | 22 | if key == nil { 23 | err = fmt.Errorf("missing graviton key input") 24 | return 25 | } 26 | 27 | if value == nil { 28 | err = fmt.Errorf("missing graviton value input") 29 | return 30 | } 31 | 32 | eValue, err := disk.Encrypt(value) 33 | if err != nil { 34 | return 35 | } 36 | 37 | store, err := graviton.NewDiskStore(GetShard(disk)) 38 | if err != nil { 39 | return 40 | } 41 | 42 | ss, err := store.LoadSnapshot(0) 43 | if err != nil { 44 | return 45 | } 46 | 47 | tree, err := ss.GetTree(t) 48 | if err != nil { 49 | return 50 | } 51 | 52 | err = tree.Put(key, eValue) 53 | if err != nil { 54 | return 55 | } 56 | 57 | _, err = graviton.Commit(tree) 58 | if err != nil { 59 | return 60 | } 61 | 62 | return 63 | } 64 | 65 | // Store a key-value in a Graviton tree 66 | func gravitonStoreValue(t string, key, value []byte) (err error) { 67 | if t == "" { 68 | err = fmt.Errorf("missing graviton tree input") 69 | return 70 | } 71 | 72 | if key == nil { 73 | err = fmt.Errorf("missing graviton key input") 74 | return 75 | } 76 | 77 | if value == nil { 78 | err = fmt.Errorf("missing graviton value input") 79 | return 80 | } 81 | 82 | store, err := graviton.NewDiskStore(GetShard(nil)) 83 | if err != nil { 84 | return 85 | } 86 | 87 | ss, err := store.LoadSnapshot(0) 88 | if err != nil { 89 | return 90 | } 91 | 92 | tree, err := ss.GetTree(t) 93 | if err != nil { 94 | return 95 | } 96 | 97 | err = tree.Put(key, value) 98 | if err != nil { 99 | return 100 | } 101 | 102 | _, err = graviton.Commit(tree) 103 | if err != nil { 104 | return 105 | } 106 | 107 | return 108 | } 109 | 110 | // Get a key-value from a Graviton tree 111 | func gravitonGetValue(t string, key []byte) (result []byte, err error) { 112 | result = []byte("") 113 | if t == "" { 114 | err = fmt.Errorf("missing graviton tree input") 115 | return 116 | } 117 | 118 | if key == nil { 119 | err = fmt.Errorf("missing graviton key input") 120 | return 121 | } 122 | 123 | store, err := graviton.NewDiskStore(GetShard(nil)) 124 | if err != nil { 125 | return 126 | } 127 | 128 | ss, err := store.LoadSnapshot(0) 129 | if err != nil { 130 | return 131 | } 132 | 133 | tree, err := ss.GetTree(t) 134 | if err != nil { 135 | return 136 | } 137 | 138 | result, err = tree.Get(key) 139 | if err != nil { 140 | return 141 | } 142 | 143 | return 144 | } 145 | 146 | // Store a key-value in Graviton settings tree 147 | func gravitonStoreSettingsValue(key, value []byte) (err error) { 148 | return gravitonStoreValue("settings", key, value) 149 | } 150 | 151 | // Get a key-value from settings Graviton tree 152 | func gravitonGetSettingsValue(key []byte) (result []byte, err error) { 153 | return gravitonGetValue("settings", key) 154 | } 155 | 156 | // Store endpoint value in Graviton settings tree 157 | func gravitonStoreEndpoint(value []byte) (err error) { 158 | return gravitonStoreSettingsValue(Key.Endpoint(), value) 159 | } 160 | 161 | // Get endpoint value from settings Graviton tree 162 | func gravitonGetEndpoint() (result []byte, err error) { 163 | return gravitonGetSettingsValue(Key.Endpoint()) 164 | } 165 | 166 | // Store network value in Graviton settings tree 167 | func gravitonStoreNetwork(value []byte) (err error) { 168 | return gravitonStoreSettingsValue(Key.Network(), value) 169 | } 170 | 171 | // Get network value from settings Graviton tree 172 | func gravitonGetNetwork() (result []byte, err error) { 173 | return gravitonGetSettingsValue(Key.Network()) 174 | } 175 | 176 | // Get an encrypted key-value from a Graviton tree and then decrypt it 177 | func gravitonGetEncryptedValue(disk *walletapi.Wallet_Disk, t string, key []byte) (result []byte, err error) { 178 | result = []byte("") 179 | if disk == nil { 180 | err = fmt.Errorf("no active account found") 181 | return 182 | } 183 | 184 | if t == "" { 185 | err = fmt.Errorf("missing graviton tree input") 186 | return 187 | } 188 | 189 | if key == nil { 190 | err = fmt.Errorf("missing graviton key input") 191 | return 192 | } 193 | 194 | store, err := graviton.NewDiskStore(GetShard(disk)) 195 | if err != nil { 196 | return 197 | } 198 | 199 | ss, err := store.LoadSnapshot(0) 200 | if err != nil { 201 | return 202 | } 203 | 204 | tree, err := ss.GetTree(t) 205 | if err != nil { 206 | return 207 | } 208 | 209 | eValue, err := tree.Get(key) 210 | if err != nil { 211 | return 212 | } 213 | 214 | result, err = disk.Decrypt(eValue) 215 | if err != nil { 216 | return 217 | } 218 | 219 | return 220 | } 221 | 222 | // Delete a key-value in a Graviton tree 223 | func gravitonDeleteKey(disk *walletapi.Wallet_Disk, t string, key []byte) (err error) { 224 | if t == "" { 225 | err = fmt.Errorf("missing graviton tree input") 226 | return 227 | } 228 | 229 | if key == nil { 230 | err = fmt.Errorf("missing graviton key input") 231 | return 232 | } 233 | 234 | store, err := graviton.NewDiskStore(GetShard(disk)) 235 | if err != nil { 236 | return 237 | } 238 | 239 | ss, err := store.LoadSnapshot(0) 240 | if err != nil { 241 | return 242 | } 243 | 244 | tree, err := ss.GetTree(t) 245 | if err != nil { 246 | return 247 | } 248 | 249 | err = tree.Delete(key) 250 | if err != nil { 251 | return 252 | } 253 | 254 | _, err = graviton.Commit(tree) 255 | if err != nil { 256 | return 257 | } 258 | 259 | return 260 | } 261 | -------------------------------------------------------------------------------- /cmd/tela-cli/gnomon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/civilware/Gnomon/indexer" 10 | "github.com/civilware/Gnomon/storage" 11 | "github.com/civilware/Gnomon/structures" 12 | "github.com/civilware/tela" 13 | "github.com/civilware/tela/logger" 14 | "github.com/civilware/tela/shards" 15 | "github.com/deroproject/derohe/globals" 16 | "github.com/deroproject/derohe/walletapi" 17 | ) 18 | 19 | type gnomes struct { 20 | Indexer *indexer.Indexer 21 | fastsync bool 22 | parallelBlocks int 23 | } 24 | 25 | var gnomon gnomes 26 | 27 | const maxParallelBlocks = 10 28 | 29 | const gnomonSearchFilterDOC = `Function init() Uint64 30 | 10 IF EXISTS("owner") == 0 THEN GOTO 30 31 | 20 RETURN 1 32 | 30 STORE("owner", address()) 33 | 50 STORE("docVersion", "1` 34 | 35 | const gnomonSearchFilterINDEX = `Function init() Uint64 36 | 10 IF EXISTS("owner") == 0 THEN GOTO 30 37 | 20 RETURN 1 38 | 30 STORE("owner", address()) 39 | 50 STORE("telaVersion", "1` 40 | 41 | // Stop all indexers and close Gnomon 42 | func stopGnomon() (stopped bool) { 43 | if gnomon.Indexer != nil { 44 | gnomon.Indexer.Close() 45 | gnomon.Indexer = nil 46 | logger.Printf("[Gnomon] Closed all indexers\n") 47 | stopped = true 48 | } 49 | 50 | return 51 | } 52 | 53 | // Start the Gnomon indexer 54 | func startGnomon(endpoint string) { 55 | if walletapi.Connected { 56 | if gnomon.Indexer == nil { 57 | dir, _ := os.Getwd() 58 | path := filepath.Join(dir, "datashards", "gnomon") 59 | 60 | switch getNetworkInfo() { 61 | case shards.Value.Network.Testnet(): 62 | path = filepath.Join(dir, "datashards", "gnomon_testnet") 63 | case shards.Value.Network.Simulator(): 64 | path = filepath.Join(dir, "datashards", "gnomon_simulator") 65 | } 66 | 67 | boltDB, boltErr := storage.NewBBoltDB(path, "gnomon") 68 | 69 | gravDB, gravErr := storage.NewGravDB(path, "25ms") 70 | 71 | dbType := shards.GetDBType() 72 | 73 | var err error 74 | var height int64 75 | switch dbType { 76 | case "boltdb": 77 | if boltErr != nil { 78 | if !strings.HasPrefix(boltErr.Error(), "[") { 79 | boltErr = fmt.Errorf("[NewBBoltDB] %s", boltErr) 80 | } 81 | logger.Errorf("%s\n", boltErr) 82 | return 83 | } 84 | 85 | height, err = boltDB.GetLastIndexHeight() 86 | if err != nil { 87 | height = 0 88 | } 89 | default: 90 | if gravErr != nil { 91 | logger.Errorf("%s\n", gravErr) 92 | return 93 | } 94 | 95 | height, err = gravDB.GetLastIndexHeight() 96 | if err != nil { 97 | height = 0 98 | } 99 | } 100 | 101 | exclusions := []string{"bb43c3eb626ee767c9f305772a6666f7c7300441a0ad8538a0799eb4f12ebcd2"} 102 | filter := []string{gnomonSearchFilterDOC, gnomonSearchFilterINDEX} 103 | 104 | // Fastsync Config 105 | config := &structures.FastSyncConfig{ 106 | Enabled: gnomon.fastsync, 107 | SkipFSRecheck: false, 108 | ForceFastSync: true, 109 | ForceFastSyncDiff: 100, 110 | NoCode: false, 111 | } 112 | 113 | gnomon.Indexer = indexer.NewIndexer(gravDB, boltDB, dbType, filter, height, endpoint, "daemon", false, false, config, exclusions) 114 | 115 | indexer.InitLog(globals.Arguments, os.Stdout) 116 | 117 | go gnomon.Indexer.StartDaemonMode(gnomon.parallelBlocks) 118 | 119 | logger.Printf("[Gnomon] Scan Status: [%d / %d]\n", gnomon.Indexer.LastIndexedHeight, walletapi.Get_Daemon_Height()) 120 | } 121 | } 122 | } 123 | 124 | // Method of Gnomon GetAllOwnersAndSCIDs() where DB type is defined by Indexer.DBType 125 | func (g *gnomes) GetAllOwnersAndSCIDs() (scids map[string]string) { 126 | switch g.Indexer.DBType { 127 | case "gravdb": 128 | return g.Indexer.GravDBBackend.GetAllOwnersAndSCIDs() 129 | case "boltdb": 130 | return g.Indexer.BBSBackend.GetAllOwnersAndSCIDs() 131 | default: 132 | return 133 | } 134 | } 135 | 136 | // Method of Gnomon GetAllSCIDVariableDetails() where DB type is defined by Indexer.DBType 137 | func (g *gnomes) GetAllSCIDVariableDetails(scid string) (vars []*structures.SCIDVariable) { 138 | switch g.Indexer.DBType { 139 | case "gravdb": 140 | return g.Indexer.GravDBBackend.GetAllSCIDVariableDetails(scid) 141 | case "boltdb": 142 | return g.Indexer.BBSBackend.GetAllSCIDVariableDetails(scid) 143 | default: 144 | return 145 | } 146 | } 147 | 148 | // Method of Gnomon GetSCIDValuesByKey() where DB type is defined by Indexer.DBType 149 | func (g *gnomes) GetSCIDValuesByKey(scid string, key interface{}) (valuesstring []string, valuesuint64 []uint64) { 150 | switch g.Indexer.DBType { 151 | case "gravdb": 152 | return g.Indexer.GravDBBackend.GetSCIDValuesByKey(scid, key, g.Indexer.ChainHeight, true) 153 | case "boltdb": 154 | return g.Indexer.BBSBackend.GetSCIDValuesByKey(scid, key, g.Indexer.ChainHeight, true) 155 | default: 156 | return 157 | } 158 | } 159 | 160 | // Method of Gnomon GetSCIDKeysByValue() where DB type is defined by Indexer.DBType 161 | func (g *gnomes) GetSCIDKeysByValue(scid string, key interface{}) (valuesstring []string, valuesuint64 []uint64) { 162 | switch g.Indexer.DBType { 163 | case "gravdb": 164 | return g.Indexer.GravDBBackend.GetSCIDKeysByValue(scid, key, g.Indexer.ChainHeight, true) 165 | case "boltdb": 166 | return g.Indexer.BBSBackend.GetSCIDKeysByValue(scid, key, g.Indexer.ChainHeight, true) 167 | default: 168 | return 169 | } 170 | } 171 | 172 | // Get the "owner" store from a SCID 173 | func (g *gnomes) GetOwnerAddress(scid string) (owner string) { 174 | if g.Indexer == nil { 175 | return 176 | } 177 | 178 | address, _ := g.GetSCIDValuesByKey(scid, "owner") 179 | if address != nil && address[0] != "anon" { 180 | owner = address[0] 181 | } 182 | 183 | return 184 | } 185 | 186 | // Method of Gnomon Indexer.AddSCIDToIndex() configured for TELA-CLI 187 | func (g *gnomes) AddSCIDToIndex(scids map[string]*structures.FastSyncImport) error { 188 | return g.Indexer.AddSCIDToIndex(scids, false, false) 189 | } 190 | 191 | // Manually add SCID(s) to Gnomon index if they can be validated as TELA contracts 192 | func (t *tela_cli) addToIndex(scids []string) (err error) { 193 | for _, scid := range scids { 194 | if len(scid) == 64 { 195 | _, err := tela.GetDOCInfo(scid, t.endpoint) 196 | if err != nil { 197 | _, errr := tela.GetINDEXInfo(scid, t.endpoint) 198 | if errr != nil { 199 | logger.Errorf("[%s] GetDOCInfo: %s\n", appName, err) 200 | logger.Errorf("[%s] GetINDEXInfo: %s\n", appName, errr) 201 | return fmt.Errorf("could not validate %s as TELA INDEX or DOC", scid) 202 | } 203 | } 204 | } 205 | } 206 | 207 | filters := gnomon.Indexer.SearchFilter 208 | gnomon.Indexer.SearchFilter = []string{} 209 | scidsToAdd := map[string]*structures.FastSyncImport{} 210 | 211 | for _, sc := range scids { 212 | scidsToAdd[sc] = &structures.FastSyncImport{} 213 | } 214 | 215 | err = gnomon.AddSCIDToIndex(scidsToAdd) 216 | gnomon.Indexer.SearchFilter = filters 217 | 218 | return 219 | } 220 | 221 | // Get the nameHdr value for a smart contract 222 | func getNameHdr(scid string) (name string) { 223 | nameHdr, _ := gnomon.GetSCIDValuesByKey(scid, tela.HEADER_NAME_V2.Trim()) 224 | if nameHdr == nil { 225 | nameHdr, _ = gnomon.GetSCIDValuesByKey(scid, tela.HEADER_NAME.Trim()) 226 | if nameHdr == nil { 227 | nameHdr = append(nameHdr, "?") 228 | } 229 | } 230 | 231 | return nameHdr[0] 232 | } 233 | -------------------------------------------------------------------------------- /shards/shards.go: -------------------------------------------------------------------------------- 1 | package shards 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | 10 | "github.com/deroproject/derohe/walletapi" 11 | ) 12 | 13 | type dbstore struct { 14 | path string 15 | dbType string 16 | sync.RWMutex 17 | } 18 | 19 | var shards dbstore 20 | var dbTypes = []string{"gravdb", "boltdb"} 21 | 22 | // Initialize package defaults and datashard storage location 23 | func init() { 24 | shards.dbType = "gravdb" 25 | dir, err := os.Getwd() 26 | if err != nil { 27 | fmt.Printf("[shards] %s\n", err) 28 | // Fallback location 29 | shards.path = "datashards" 30 | } else { 31 | shards.path = filepath.Join(dir, "datashards") 32 | } 33 | } 34 | 35 | // SetPath can be used to set a custom path for datashard storage 36 | func SetPath(path string) (datashards string, err error) { 37 | dir := filepath.Dir(path) 38 | if _, err = os.Stat(dir); os.IsNotExist(err) { 39 | err = fmt.Errorf("%s does not exists", path) 40 | return 41 | } 42 | 43 | datashards = filepath.Join(path, "datashards") 44 | shards.Lock() 45 | shards.path = datashards 46 | shards.Unlock() 47 | 48 | return 49 | } 50 | 51 | // Get the current datashards storage path 52 | func GetPath() string { 53 | shards.RLock() 54 | defer shards.RUnlock() 55 | 56 | return shards.path 57 | } 58 | 59 | // Is db a valid type for datashard storage 60 | func IsValidDBType(db string) bool { 61 | shards.RLock() 62 | defer shards.RUnlock() 63 | 64 | for _, t := range dbTypes { 65 | if db == t { 66 | return true 67 | } 68 | } 69 | 70 | return false 71 | } 72 | 73 | // Set datashards DB type if IsValidDBType 74 | func SetDBType(db string) (err error) { 75 | shards.Lock() 76 | defer shards.Unlock() 77 | 78 | switch db { 79 | case "gravdb": 80 | shards.dbType = db 81 | case "boltdb": 82 | shards.dbType = db 83 | default: 84 | return fmt.Errorf("unknown db type %q", db) 85 | } 86 | 87 | return 88 | } 89 | 90 | // Get datashards DB type 91 | func GetDBType() string { 92 | shards.RLock() 93 | defer shards.RUnlock() 94 | 95 | return shards.dbType 96 | } 97 | 98 | // Get a datashard's path, synced with Engram's DB structure 99 | func GetShard(disk *walletapi.Wallet_Disk) (result string) { 100 | dir := GetPath() 101 | if disk == nil { 102 | result = filepath.Join(dir, "settings") 103 | } else { 104 | address := disk.GetAddress().String() 105 | result = filepath.Join(dir, fmt.Sprintf("%x", sha1.Sum([]byte(address)))) 106 | } 107 | 108 | return 109 | } 110 | 111 | // Encrypt a key-value and then store it in datashards 112 | func StoreEncryptedValue(disk *walletapi.Wallet_Disk, t string, key, value []byte) (err error) { 113 | db := GetDBType() 114 | switch db { 115 | case "gravdb": 116 | err = gravitonStoreEncryptedValue(disk, t, key, value) 117 | case "boltdb": 118 | err = boltStoreEncryptedValue(disk, t, key, value) 119 | default: 120 | err = fmt.Errorf("unknown db type %q", db) 121 | } 122 | 123 | return 124 | } 125 | 126 | // Store a key-value in datashards 127 | func StoreValue(t string, key, value []byte) (err error) { 128 | db := GetDBType() 129 | switch db { 130 | case "gravdb": 131 | err = gravitonStoreValue(t, key, value) 132 | case "boltdb": 133 | err = boltStoreValue(t, key, value) 134 | default: 135 | err = fmt.Errorf("unknown db type %q", db) 136 | } 137 | 138 | return 139 | } 140 | 141 | // Get a key-value from datashards 142 | func GetValue(t string, key []byte) (result []byte, err error) { 143 | db := GetDBType() 144 | switch db { 145 | case "gravdb": 146 | result, err = gravitonGetValue(t, key) 147 | case "boltdb": 148 | result, err = boltGetValue(t, key) 149 | default: 150 | err = fmt.Errorf("unknown db type %q", db) 151 | } 152 | 153 | return 154 | } 155 | 156 | // Store a key-value setting in datashards 157 | func StoreSettingsValue(key, value []byte) (err error) { 158 | db := GetDBType() 159 | switch db { 160 | case "gravdb": 161 | err = gravitonStoreSettingsValue(key, value) 162 | case "boltdb": 163 | err = boltStoreSettingsValue(key, value) 164 | default: 165 | err = fmt.Errorf("unknown db type %q", db) 166 | } 167 | 168 | return 169 | } 170 | 171 | // Get a key-value setting from datashards 172 | func GetSettingsValue(key []byte) (result []byte, err error) { 173 | db := GetDBType() 174 | switch db { 175 | case "gravdb": 176 | result, err = gravitonGetSettingsValue(key) 177 | case "boltdb": 178 | result, err = boltGetSettingsValue(key) 179 | default: 180 | err = fmt.Errorf("unknown db type %q", db) 181 | } 182 | 183 | return 184 | } 185 | 186 | // Store endpoint value in datashards 187 | func StoreEndpoint(value string) (err error) { 188 | db := GetDBType() 189 | switch db { 190 | case "gravdb": 191 | err = gravitonStoreEndpoint([]byte(value)) 192 | case "boltdb": 193 | err = boltStoreEndpoint([]byte(value)) 194 | default: 195 | err = fmt.Errorf("unknown db type %q", db) 196 | } 197 | 198 | return 199 | } 200 | 201 | // Get endpoint value from datashards 202 | func GetEndpoint() (endpoint string, err error) { 203 | var result []byte 204 | db := GetDBType() 205 | switch db { 206 | case "gravdb": 207 | result, err = gravitonGetEndpoint() 208 | case "boltdb": 209 | result, err = boltGetEndpoint() 210 | default: 211 | err = fmt.Errorf("unknown db type %q", db) 212 | return 213 | } 214 | 215 | endpoint = string(result) 216 | 217 | return 218 | } 219 | 220 | // Store network value in datashards 221 | func StoreNetwork(value string) (err error) { 222 | if !isNetworkValid(value) { 223 | err = fmt.Errorf("invalid network") 224 | return 225 | } 226 | 227 | db := GetDBType() 228 | switch db { 229 | case "gravdb": 230 | err = gravitonStoreNetwork([]byte(value)) 231 | case "boltdb": 232 | err = boltStoreNetwork([]byte(value)) 233 | default: 234 | err = fmt.Errorf("unknown db type %q", db) 235 | } 236 | 237 | return 238 | } 239 | 240 | // Get network value from datashards 241 | func GetNetwork() (network string, err error) { 242 | var result []byte 243 | db := GetDBType() 244 | switch db { 245 | case "gravdb": 246 | result, err = gravitonGetNetwork() 247 | case "boltdb": 248 | result, err = boltGetNetwork() 249 | default: 250 | err = fmt.Errorf("unknown db type %q", db) 251 | return 252 | } 253 | 254 | network = string(result) 255 | 256 | return 257 | } 258 | 259 | // Get an encrypted key-value from datashards and then decrypt it 260 | func GetEncryptedValue(disk *walletapi.Wallet_Disk, t string, key []byte) (result []byte, err error) { 261 | db := GetDBType() 262 | switch db { 263 | case "gravdb": 264 | result, err = gravitonGetEncryptedValue(disk, t, key) 265 | case "boltdb": 266 | result, err = boltGetEncryptedValue(disk, t, key) 267 | default: 268 | err = fmt.Errorf("unknown db type %q", db) 269 | } 270 | 271 | return 272 | } 273 | 274 | // Delete a key-value from datashards 275 | func DeleteKey(disk *walletapi.Wallet_Disk, t string, key []byte) (err error) { 276 | db := GetDBType() 277 | switch db { 278 | case "gravdb": 279 | err = gravitonDeleteKey(disk, t, key) 280 | case "boltdb": 281 | err = boltDeleteKey(disk, t, key) 282 | default: 283 | err = fmt.Errorf("unknown db type %q", db) 284 | } 285 | 286 | return 287 | } 288 | 289 | // Delete a key-value setting from datashards 290 | func DeleteSettingsKey(key []byte) (err error) { 291 | db := GetDBType() 292 | switch db { 293 | case "gravdb": 294 | err = gravitonDeleteKey(nil, "settings", key) 295 | case "boltdb": 296 | err = boltDeleteKey(nil, "settings", key) 297 | default: 298 | err = fmt.Errorf("unknown db type %q", db) 299 | } 300 | 301 | return 302 | } 303 | -------------------------------------------------------------------------------- /TELA-INDEX-1/README.md: -------------------------------------------------------------------------------- 1 | # TELA-INDEX-1 - TELA Decentralized Web Standard 2 | 3 | ## Introduction 4 | TELA introduces a standard for decentralized browser-based applications that can be executed locally, eliminating the reliance on third-party servers. 5 | 6 | This portion of the documentation will focus on `TELA-INDEX-1`. This is the recommended starting point for learning about installing TELA applications and content. 7 | 8 | ## Getting Started with TELA-INDEX-1 9 | 10 | ### Table of Contents 11 | - [Initialization](#initialization) 12 | - [TELA-INDEX-1 Template](#tela-index-1-template) 13 | - [Utilization](#utilization) 14 | - [Install TELA-INDEX-1](#install-tela-index-1) 15 | - [Update TELA-INDEX-1](#update-tela-index-1) 16 | - [Rate TELA-INDEX-1](#rate-tela-index-1) 17 | - [TELA-DOC-1](../TELA-DOC-1/README.md) 18 | 19 | ### Initialization 20 | It is recommended to use a compliant host application such as [TELA-CLI](../cmd/tela-cli/README.md) when installing a `TELA-INDEX-1`, which will automate the process and help to avoid errors during installation. To deploy a `TELA-INDEX-1` manually, developers can fill out the input fields inside of `InitializePrivate()`. The minimum requirement for deployment is a valid `TELA-DOC-1` SCID, which must be input as the "DOC1" STORE value, and will serve as the entrypoint for the TELA application. 21 | 22 | ```go 23 | Function InitializePrivate() Uint64 24 | ... 25 | // Input fields starts at line 30 26 | 30 STORE("var_header_name", "App Name") // var_header_name defines the name of the TELA application. 27 | 31 STORE("var_header_description", "A TELA App") // var_header_description defines the description of the TELA application. 28 | 32 STORE("var_header_icon", "https://raw.githubusercontent.com/civilware/.github/main/CVLWR.png") // var_header_icon defines the URL or SCID for the icon representing the TELA application. This should be of size 100x100. 29 | 33 STORE("dURL", "app.tela") // dURL is unique identifier for the TELA application, ex myapp.tela. 30 | 34 STORE("mods", "") // mods is an optional store and can be an empty string. Its usage is to store any tags for enabled TELA-MOD-1's within this smart contract, for more information on MODs reference the TELA-MOD-1 documentation. 31 | 40 STORE("DOC1", "a891299086d218840d1eb71ae759ddc08f1e85cbf35801cc34ef64b4b07939c9") // DOC#s are installed TELA-DOC-1 SCIDs to be used in this TELA application. DOC1 will be used as the entrypoint for the application, a valid DOC1 k/v store is the minimum requirement for a TELA application. 32 | 41 STORE("DOC2", "b891299086d218840d1eb71ae759ddc08f1e85cbf35801cc34ef64b4b07939c8") // Further DOCs can be added/removed as required. All DOC stores that are used on the contract must have a valid SCID value otherwise the INDEX will not be validated when serving. 33 | // For any further DOCs needed, make sure the smart contract line number and DOC# increment++ 34 | // 42 STORE("DOC3", "c891299086d218840d1eb71ae759ddc08f1e85cbf35801cc34ef64b4b07939c7") 35 | // 43 STORE("DOC4", "d891299086d218840d1eb71ae759ddc08f1e85cbf35801cc34ef64b4b07939c6") 36 | // .. 37 | // Input fields end at the last DOC# 38 | ... 39 | 1000 RETURN 0 40 | End Function 41 | ``` 42 | It is recommended to keep `TELA-INDEX-1` contracts below 9KB total file size if updating is to be required at any point in the future. If staying within the 9KB size limit, developers are able to embed ~90 (may vary slightly depending on header values) `TELA-DOC-1` SCIDs into a single `TELA-INDEX-1` and maintain its ability to be updated with more DOCs than it was originally installed with. Current test show the maximum limit of DOCs that a `TELA-INDEX-1` can be successfully installed with is ~120 (may vary slightly depending on header values). Library usage and DocShard creation are documented within the `TELA-DOC-1` section and can increase the total capacity beyond these figures. 43 | 44 | ### TELA-INDEX-1 Template 45 | * [TELA-INDEX-1](TELA-INDEX-1.bas) 46 | 47 | The majority of comments on the smart contract template have been removed to utilize the maximum allowable contract space, refer to the above [Initialization](#initialization) documentation for guidance. 48 | 49 | ### Utilization 50 | It is in your best interest to always run a getgasestimate ahead of any direct curls below. This will 1) reduce your errors and 2) ensure you are utilizing the proper amount for fees from your specific parameters and ringsize. 51 | 52 | #### Install TELA-INDEX-1 53 | ```json 54 | curl --request POST --data-binary @TELA-INDEX-1.bas http://127.0.0.1:30000/install_sc; 55 | ``` 56 | 57 | The following manual transaction portions will be split with a getgasestimate example call first, and then the follow-up with the respective fees that were present in that exact scenario. It's up to you to modify the fees parameter to reflect the 'gasstorage' return of getgasestimate. 58 | 59 | #### Update TELA-INDEX-1 60 | Publicly deployed `TELA-INDEX-1` contracts can be updated by their owners. If a `TELA-INDEX-1` is deployed anonymously (*ringsize above 2*) it is immutable. Updating allows for creators to make changes to their applications and content. Each execution of a `TELA-INDEX-1` smart contracts UpdateCode function will store its TXID as a uint64 key in the smart contract allowing for past states to be retrieved from mutable contracts. 61 | 62 | - GetGasEstimate 63 | ```json 64 | curl -X POST\ 65 | http://127.0.0.1:20000/json_rpc\ 66 | -H 'content-type: application/json'\ 67 | -d '{ 68 | "jsonrpc": "2.0", 69 | "id": "1", 70 | "method": "DERO.GetGasEstimate", 71 | "params": { 72 | "transfers": [], 73 | "signer": "deto1qyre7td6x9r88y4cavdgpv6k7lvx6j39lfsx420hpvh3ydpcrtxrxqg8v8e3z", 74 | "sc_rpc": [ 75 | { 76 | "name": "SC_ACTION", 77 | "datatype": "U", 78 | "value": 0 79 | }, 80 | { 81 | "name": "SC_ID", 82 | "datatype": "H", 83 | "value": "ce25b92083f089357d72295f4cf51cc58fed7439500792b94c85244f1067279e" 84 | }, 85 | { 86 | "name": "entrypoint", 87 | "datatype": "S", 88 | "value": "UpdateCode" 89 | }, 90 | { 91 | "name": "mods", 92 | "datatype": "S", 93 | "value": "" 94 | }, 95 | { 96 | "name": "code", 97 | "datatype": "S", 98 | "value": "NEW_TELA_INDEX_SC_CODE_GOES_HERE" 99 | } 100 | ] 101 | } 102 | }' 103 | ``` 104 | 105 | - Txn 106 | ```json 107 | curl -X POST\ 108 | http://127.0.0.1:30000/json_rpc\ 109 | -H 'content-type: application/json'\ 110 | -d '{ 111 | "jsonrpc": "2.0", 112 | "id": "0", 113 | "method": "Transfer", 114 | "params": { 115 | "ringsize": 2, 116 | "fees": 320, 117 | "sc_rpc": [ 118 | { 119 | "name": "entrypoint", 120 | "datatype": "S", 121 | "value": "UpdateCode" 122 | }, 123 | { 124 | "name": "mods", 125 | "datatype": "S", 126 | "value": "" 127 | }, 128 | { 129 | "name": "code", 130 | "datatype": "S", 131 | "value": "NEW_TELA_INDEX_SC_CODE_GOES_HERE" 132 | }, 133 | { 134 | "name": "SC_ACTION", 135 | "datatype": "U", 136 | "value": 0 137 | }, 138 | { 139 | "name": "SC_ID", 140 | "datatype": "H", 141 | "value": "ce25b92083f089357d72295f4cf51cc58fed7439500792b94c85244f1067279e" 142 | } 143 | ] 144 | } 145 | }' 146 | ``` 147 | 148 | #### Rate TELA-INDEX-1 149 | TELA content can be rated by any DERO wallet that executes the Rate function on the smart contract. The following Rate command also applies to `TELA-DOC-1` contracts. 150 | 151 | - GetGasEstimate 152 | ```json 153 | curl -X POST\ 154 | http://127.0.0.1:20000/json_rpc\ 155 | -H 'content-type: application/json'\ 156 | -d '{ 157 | "jsonrpc": "2.0", 158 | "id": "1", 159 | "method": "DERO.GetGasEstimate", 160 | "params": { 161 | "transfers": [], 162 | "signer": "deto1qyre7td6x9r88y4cavdgpv6k7lvx6j39lfsx420hpvh3ydpcrtxrxqg8v8e3z", 163 | "sc_rpc": [ 164 | { 165 | "name": "SC_ACTION", 166 | "datatype": "U", 167 | "value": 0 168 | }, 169 | { 170 | "name": "SC_ID", 171 | "datatype": "H", 172 | "value": "f2815b442d62a055e4bb8913167e3dbce3208f300d7006aaa3a2f127b06de29d" 173 | }, 174 | { 175 | "name": "entrypoint", 176 | "datatype": "S", 177 | "value": "Rate" 178 | }, 179 | { 180 | "name": "r", 181 | "datatype": "U", 182 | "value": 0 183 | } 184 | ] 185 | } 186 | }' 187 | ``` 188 | 189 | - Txn 190 | ```json 191 | curl -X POST\ 192 | http://127.0.0.1:30002/json_rpc\ 193 | -H 'content-type: application/json'\ 194 | -d '{ 195 | "jsonrpc": "2.0", 196 | "id": "0", 197 | "method": "Transfer", 198 | "params": { 199 | "ringsize": 2, 200 | "fees": 90, 201 | "sc_rpc": [ 202 | { 203 | "name": "entrypoint", 204 | "datatype": "S", 205 | "value": "Rate" 206 | }, 207 | { 208 | "name": "r", 209 | "datatype": "U", 210 | "value": 0 211 | }, 212 | { 213 | "name": "SC_ACTION", 214 | "datatype": "U", 215 | "value": 0 216 | }, 217 | { 218 | "name": "SC_ID", 219 | "datatype": "H", 220 | "value": "f2815b442d62a055e4bb8913167e3dbce3208f300d7006aaa3a2f127b06de29d" 221 | } 222 | ] 223 | } 224 | }' 225 | ``` 226 | 227 | ### TELA-DOC-1 228 | * [TELA-DOC-1](../TELA-DOC-1/README.md) -------------------------------------------------------------------------------- /TELA-DOC-1/README.md: -------------------------------------------------------------------------------- 1 | # TELA-DOC-1 - TELA Decentralized Web Standard Document 2 | 3 | ## Introduction 4 | TELA introduces a standard for decentralized browser-based applications that can be executed locally, eliminating the reliance on third-party servers. 5 | 6 | This portion of the documentation will focus on `TELA-DOC-1`. 7 | 8 | `TELA-INDEX-1` info can be found [here](../TELA-INDEX-1/README.md) and it is recommended to have reviewed it prior to `TELA-DOC-1`. 9 | 10 | ## Creating TELA-DOC-1's 11 | 12 | ### Table of Contents 13 | - [Languages](#languages) 14 | - [Preparation](#preparation) 15 | - [Guidelines](#guidelines) 16 | - [JavaScript](#javascript) 17 | - [docTypes](#doctypes) 18 | - [Initialization](#initialization) 19 | - [TELA Libraries](#tela-libraries) 20 | - [Library Usage](#library-usage) 21 | - [Library Creation](#library-creation) 22 | - [DocShards](#docshards) 23 | - [DocShard Usage](#docshard-usage) 24 | - [DocShard Creation](#docshard-creation) 25 | - [Compression](#compression) 26 | - [TELA-DOC-1 Template](#tela-doc-1-template) 27 | - [Utilization](#utilization) 28 | - [Install TELA-DOC-1](#install-tela-doc-1) 29 | - [Rate TELA-DOC-1](#rate-tela-doc-1) 30 | - [TELA-INDEX-1](#tela-index-1) 31 | 32 | ### Languages 33 | The `civilware/tela` go package's accepted languages are: 34 | 35 | - HTML, JSON, JavaScript, CSS and Markdown. 36 | - A Static type is also defined to encompass desired files that are not listed as an accepted language such as asset or build files. 37 | 38 | ### Preparation 39 | Prepare all the code (or text) that will be required for the document. Ensure you are using a language accepted by the host application that will be serving this document. There are some minimal guidelines to be followed when preparing the code to be added to the `TELA-DOC-1` smart contract which will help reduce errors. Outside of these guidelines, syntax and formatting can be defined by the author of the document. References and execution can be assumed to perform as if code were being served from any public server. 40 | 41 | #### Guidelines 42 | - One `TELA-DOC-1` cannot exceed 20KB in total size. 43 | - If `/* */` multiline comments or non ASCII characters are used in the document code it should be encoded to avoid errors during contract installation. See [compression](#compression) for more details. 44 | - Example application code can be found in [tela_tests](../tela_tests/). 45 | - The dURL can be used to help indexers query details beyond the defined contract stores, examples: 46 | - Appending `.lib` to a dURL will mark it as a library for indexes such as in [TELA-CLI](../cmd/tela-cli/README.md). 47 | - DocShards append `.shard` and `.shards` to their DOC and INDEX dURLs respectively to indicate it is a shard or requires reconstruction. 48 | 49 | ##### JavaScript 50 | - Accurate origin URLs for web socket connections can be generated using: 51 | 52 | ```javascript 53 | const applicationData = { 54 | "url": "http://localhost:" + location.port, 55 | }; 56 | ``` 57 | #### docTypes 58 | ```go 59 | "TELA-STATIC-1" 60 | "TELA-HTML-1" 61 | "TELA-JSON-1" 62 | "TELA-CSS-1" 63 | "TELA-JS-1" 64 | "TELA-MD-1" 65 | "TELA-GO-1" 66 | ``` 67 | 68 | ### Initialization 69 | It is recommended to use a compliant host application such as [TELA-CLI](../cmd/tela-cli/README.md) when installing a `TELA-DOC-1`, which will automate the process and help to avoid errors during installation. To deploy a `TELA-DOC-1` manually, developers can fill out the input fields inside of `InitializePrivate()` and input the document code in the designated multiline comment section at the bottom of the smart contract template. 70 | 71 | ```go 72 | Function InitializePrivate() Uint64 73 | ... 74 | // Input fields starts at line 30 75 | 30 STORE("var_header_name", "index.html") // var_header_name defines the name of the TELA document. 76 | 31 STORE("var_header_description", "A HTML index") // var_header_description defines the description of the TELA document. 77 | 32 STORE("var_header_icon", "https://raw.githubusercontent.com/civilware/.github/main/CVLWR.png") // var_header_icon defines the URL or SCID for the icon representing the TELA document. This should be of size 100x100. 78 | 33 STORE("dURL", "app.tela") // dURL is a unique identifier for the TELA document likely linking to the TELA-INDEX-1 where this document is being used or to its corresponding library. 79 | 34 STORE("docType", "TELA-HTML-1") // docType is the language or file type being used, ex TELA-JS-1, TELA-CSS-1... see docTypes list for all store values 80 | 35 STORE("subDir", "") // subDir adds this file to a sub directory, it can be left empty if file location is in root directory, separators should always be / ex: sub1/sub2/sub3 81 | 36 STORE("fileCheckC", "1c37f9e61f15a9526ba680dce0baa567e642ca2cd0ddea71649dab415dad8cb2") // C and S from DERO signature 82 | 37 STORE("fileCheckS", "7e642ca2cd0ddea71649dab415dad8cb21c37f9e61f15a9526ba680dce0baa56") // signature is of the docType code in the multiline comment section 83 | // Input fields ends at line 37 84 | 100 RETURN 0 85 | End Function 86 | ... 87 | /* 88 | 89 | 90 | 91 | 92 | 93 | TELA-DOC-1 Template 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | */ 103 | ``` 104 | 105 | ### TELA Libraries 106 | TELA libraries are in essence any development library that is installed in TELA format. A TELA library consists of `TELA-DOC-1` contracts that have been designed for universal use. Once installed, these libraries are intended to be application-agnostic, allowing their functionality to be leveraged by any TELA application. This promotes code reuse for faster development, helps to reduce chain bloat, drives community-tested solutions, and helps maintain consistency across different projects. To assist developers in discovering and utilizing installed libraries, some indexes like [TELA-CLI](../cmd/tela-cli/README.md) provide specific queries to make it easier to find and propagate universal libraries within the TELA ecosystem. 107 | 108 | #### Library Usage 109 | At its core, a TELA library is a `TELA-DOC-1` contract or a collection of them. This means that using a library follows the same principles as using any `TELA-DOC-1`. 110 | 111 | To consume a library: 112 | - Identify the SCID of the library you want to use in your application. 113 | - While in the development stages, libraries can be cloned to get a local copy of the files so its functions and variables can be referenced and tested in the application before it is installed on-chain. 114 | - When development is finished, input any `TELA-INDEX-1` or `TELA-DOC-1` library SCID(s) used while developing, into your applications `TELA-INDEX-1` contract before it is installed. The application will now contain those libraries when served. 115 | 116 | #### Library Creation 117 | The codebase being installed as a TELA library might exceed the total size of a single `TELA-DOC-1` smart contract. In this case multiple `TELA-DOC-1`'s can be deployed and embedded within a `TELA-INDEX-1` to create a multi-part TELA library which can be referenced by a single SCID for further use. 118 | 119 | To create a library: 120 | - Write all the docType code needed for the `TELA-DOC-1` smart contract or contracts. 121 | - Install the `TELA-DOC-1` contracts, ensuring that all the dURLs match and have a `.lib` suffix. 122 | - Optionally, all the `TELA-DOC-1` contracts can then be embedded within a `TELA-INDEX-1` to reference it with a single SCID. 123 | 124 | ### DocShards 125 | DocShards provide developers with an alternative method for packaging TELA content. They are similar to libraries in their construction; however, embedded DocShards are recreated as a single file when cloned or served. This allows a single piece of TELA content to exceed the smart contract installation maximum size. Additionally, DocShards can be embedded into libraries to enhance their utility. 126 | 127 | #### DocShard Usage 128 | Like TELA libraries, DocShards consist of a collection of `TELA-DOC-1` contracts. It is important to note that a DocShard cannot serve as the entrypoint of a `TELA-INDEX-1`. 129 | 130 | To consume a DocShard: 131 | - Identify the SCID of the DocShard you want to use in your application. 132 | - While in the development stages, DocShards can be cloned to get a local copy of the file so its functions and variables can be referenced and tested in the application before it is installed on-chain. DocShards are cloned to `dURL/nameHdr` in the target directory. 133 | - When development is finished, input any `TELA-INDEX-1` DocShard SCID(s) used while developing, into your applications `TELA-INDEX-1` contract before it is installed. The application will now contain those DocShards when served. 134 | 135 | #### DocShard Creation 136 | To avoid formatting errors during the cloning or serving of content, it is recommended to use the `civilware/tela` go package when creating DocShards. `TELA-CLI` has extended the package's tooling to simplify the creation of DocShards. Creation is limited to files containing only ASCII characters. 137 | 138 | To create a DocShard: 139 | - Write the docType code that will be recreated as a single source file. 140 | - Using the appropriate tools, create the DocShard smart contracts from the source file. 141 | - Install the `TELA-DOC-1` DocShard contracts. The `.shard` tag should be appended to the DOC dURL's to indicate they are DocShards. 142 | - Embed all installed `TELA-DOC-1` DocShard contracts into a `TELA-INDEX-1` and append `.shards` to its dURL to signify it requires reconstruction. 143 | 144 | #### Compression 145 | Document code can be compressed and encoded to maximize the storage capacity of a single DOC. Additionally, applying compression when creating DocShards can reduce the number of shards needed for large files. Adding the compression extension to the end of the nameHdr will signal that the content is compressed, ensuring it is processed appropriately when served. 146 | 147 | For optimal results, it is recommended to use a host application such as `TELA-CLI`, which automates the compression process during DOC installation. The `civilware/tela` package currently supports the following compression formats: 148 | 149 | | Format | Extension | 150 | |--------------|-----------| 151 | | gzip | `.gz` | 152 | 153 | ### TELA-DOC-1 Template 154 | * [TELA-DOC-1](TELA-DOC-1.bas) 155 | 156 | The majority of comments on the smart contract template have been removed to utilize the maximum allowable contract space, refer to the above [Initialization](#initialization) documentation for guidance. 157 | 158 | ### Utilization 159 | It is in your best interest to always run a getgasestimate ahead of any direct curls below. This will 1) reduce your errors and 2) ensure you are utilizing the proper amount for fees from your specific parameters and ringsize. 160 | 161 | #### Install TELA-DOC-1 162 | ```json 163 | curl --request POST --data-binary @TELA-DOC-1.bas http://127.0.0.1:30000/install_sc; 164 | ``` 165 | 166 | #### Rate TELA-DOC-1 167 | Rating `TELA-DOC-1` contracts can be done the same as `TELA-INDEX-1`'s. Manual rating procedures can be found [here](../TELA-INDEX-1/README.md#rate-tela-index-1). 168 | 169 | ### TELA-INDEX-1 170 | * [TELA-INDEX-1](../TELA-INDEX-1/README.md) -------------------------------------------------------------------------------- /tela_tests/app1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TELA Demo Application 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 | 37 | 39 | 40 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |

TELA-DWEB

49 |
50 |
51 |

TELA Decentralized Web Standard Demo

52 |
53 |
54 | This demo shows how a TELA application can be connected to a DERO wallet through XSWD.
55 | Below are various request examples that can be tried if connected to a wallet.
56 | The source code for this application can be found 57 | here. 59 |
60 |
61 | 62 |
63 |
64 |
65 | 66 |
67 | 107 |
108 | 109 |
110 | 111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | 119 |
120 |
121 | 122 | 123 | 124 | 125 | 127 | 128 | 129 |
130 |
131 | 132 | dero.io 133 | 134 |
135 |
136 |

Crafted by Civilware

137 |
138 |

2024

139 |
140 |
141 | 142 |
143 |
144 |
145 |
146 |
147 | 148 |
149 | 151 | 152 | 154 | 155 | 156 |
157 | 158 |
159 | 161 | 162 | 164 | 165 | 166 |
167 | 168 |
169 | 171 | 172 | 174 | 175 | 176 |
177 | 178 | 179 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /tela_tests/app1/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | height: 100vh; 4 | margin: 0; 5 | flex-direction: column; 6 | box-sizing: border-box; 7 | font-family: -apple-system, Arial, Helvetica, sans-serif, Apple Color Emoji, Segoe UI Emoji; 8 | background-color: var(--background); 9 | padding: 0 5px 5px 5px; 10 | } 11 | 12 | a { 13 | color: var(--link); 14 | } 15 | 16 | a:hover { 17 | color: var(--hover); 18 | } 19 | 20 | hr { 21 | color: var(--gray); 22 | } 23 | 24 | select, 25 | input { 26 | background-color: var(--background); 27 | color: var(--text); 28 | border: 3px outset var(--gray); 29 | border-radius: 12px; 30 | font-size: medium; 31 | outline: none; 32 | } 33 | 34 | input { 35 | padding: 2px 5px; 36 | width: 95%; 37 | margin-top: 3px; 38 | } 39 | 40 | select:focus, 41 | input:focus { 42 | outline: none !important; 43 | border-color: var(--hover); 44 | } 45 | 46 | input[type=number] { 47 | -moz-appearance: textfield; 48 | appearance: textfield; 49 | margin: 0; 50 | } 51 | 52 | input[type=number]::-webkit-inner-spin-button, 53 | input[type=number]::-webkit-outer-spin-button { 54 | -webkit-appearance: none; 55 | margin: 0; 56 | } 57 | 58 | footer { 59 | text-align: center; 60 | margin-top: 60px; 61 | color: var(--text); 62 | padding-bottom: 6vh; 63 | } 64 | 65 | code { 66 | font-size: large; 67 | animation: typing 750ms linear infinite; 68 | word-wrap: break-word; 69 | overflow-wrap: break-word; 70 | white-space: normal; 71 | } 72 | 73 | :root { 74 | --green: #46b868; 75 | --yellow: #fffb00; 76 | --red: #ed2024; 77 | } 78 | 79 | #svgCursor { 80 | position: fixed; 81 | pointer-events: none; 82 | width: 25px; 83 | height: 25px; 84 | opacity: 0; 85 | transition: opacity 1s; 86 | z-index: 9999; 87 | } 88 | 89 | .cls-1 { 90 | fill: var(--red); 91 | } 92 | 93 | .cls-1, 94 | .cls-2, 95 | .cls-3 { 96 | stroke-width: 0px; 97 | } 98 | 99 | .cls-2 { 100 | fill: var(--green); 101 | } 102 | 103 | .cls-3 { 104 | fill: var(--text); 105 | } 106 | 107 | .cls-r1 { 108 | fill: var(--rings); 109 | } 110 | 111 | .cls-r3 { 112 | fill: var(--background); 113 | } 114 | 115 | .svg-container { 116 | position: fixed; 117 | width: 100%; 118 | height: 100vh; 119 | display: flex; 120 | justify-content: center; 121 | align-items: center; 122 | z-index: -100; 123 | } 124 | 125 | .ring-image { 126 | width: 120%; 127 | height: 120%; 128 | animation: spin 120s ease-in infinite; 129 | } 130 | 131 | .ring-image2 { 132 | width: 110%; 133 | height: 110%; 134 | animation: spin 96s ease infinite; 135 | } 136 | 137 | .ring-image3 { 138 | width: 115%; 139 | height: 115%; 140 | animation: half-spin 75s ease-in-out infinite; 141 | } 142 | 143 | .marquee { 144 | width: 50vw; 145 | min-width: 280px; 146 | line-height: 50px; 147 | background-color: transparent; 148 | color: var(--green); 149 | white-space: nowrap; 150 | overflow: hidden; 151 | box-sizing: border-box; 152 | margin: auto; 153 | position: absolute; 154 | left: 50%; 155 | transform: translate(-50%, -50%); 156 | padding-bottom: 20px; 157 | } 158 | 159 | .marquee p { 160 | display: inline-block; 161 | padding-left: 100%; 162 | animation: marquee 20s linear infinite; 163 | text-shadow: 0 0 5px var(--background); 164 | } 165 | 166 | .container { 167 | width: 60vw; 168 | min-width: 280px; 169 | border-radius: 12px; 170 | padding: 20px 20px 30px 20px; 171 | margin: 20px auto; 172 | background-color: transparent; 173 | color: var(--text); 174 | font-variant-caps: normal; 175 | line-height: normal; 176 | align-items: center; 177 | overflow-x: visible; 178 | } 179 | 180 | .title, 181 | .icon { 182 | margin: auto; 183 | } 184 | 185 | .title { 186 | max-width: 500px; 187 | } 188 | 189 | .icon { 190 | max-width: 100px; 191 | } 192 | 193 | .description { 194 | margin-top: 3px; 195 | text-align: center; 196 | font-size: 1rem; 197 | line-height: 1.5rem; 198 | display: block; 199 | flex-direction: row; 200 | position: static; 201 | } 202 | 203 | .call-button, 204 | .connect-button { 205 | position: relative; 206 | color: var(--text); 207 | background-color: var(--gray); 208 | margin-top: 3px; 209 | padding: 5px 10px; 210 | font-size: 1rem; 211 | border: 3px outset var(--gray); 212 | border-radius: 12px; 213 | min-width: 150px; 214 | text-align: center; 215 | transition-duration: 0.4s; 216 | overflow: hidden; 217 | cursor: pointer; 218 | box-sizing: border-box; 219 | } 220 | 221 | .call-button:hover, 222 | .connect-button:hover { 223 | border-color: var(--hover); 224 | background-color: var(--hover); 225 | color: var(--background); 226 | transition: 0.2s; 227 | } 228 | 229 | .call-button:after, 230 | .connect-button:after { 231 | content: ""; 232 | border-color: transparent; 233 | background: var(--text); 234 | display: block; 235 | position: absolute; 236 | padding-top: 300%; 237 | padding-left: 350%; 238 | margin-left: -20px !important; 239 | margin-top: -120%; 240 | opacity: 0; 241 | transition: all 0.6s; 242 | } 243 | 244 | .call-button:active:after, 245 | .connect-button:active:after { 246 | padding: 0; 247 | margin: 0; 248 | opacity: 1; 249 | transition: 0s; 250 | } 251 | 252 | .json-container { 253 | display: flex; 254 | justify-content: center; 255 | align-self: center; 256 | min-height: 250px; 257 | max-height: 30%; 258 | width: 75%; 259 | min-width: 60%; 260 | padding-bottom: 30px; 261 | margin: auto; 262 | margin-bottom: 40px; 263 | box-sizing: border-box; 264 | z-index: 10; 265 | } 266 | 267 | .json-display { 268 | min-height: 200px; 269 | min-width: 100px; 270 | background-color: var(--background); 271 | flex: 1; 272 | padding: 10px; 273 | border: 3px outset var(--hover); 274 | border-radius: 12px; 275 | margin: 5px; 276 | white-space: pre-wrap; 277 | overflow: auto; 278 | box-shadow: 279 | 0 0 3px 1px var(--background), 280 | 0 0 20px 10px var(--background); 281 | } 282 | 283 | .arrow-18h, 284 | .arrow-20h { 285 | width: 0; 286 | height: 0; 287 | align-self: center; 288 | z-index: 500; 289 | } 290 | 291 | .arrow-18h { 292 | border-top: 9px solid transparent; 293 | border-bottom: 9px solid transparent; 294 | } 295 | 296 | .arrow-20h { 297 | border-top: 10px solid transparent; 298 | border-bottom: 10px solid transparent; 299 | } 300 | 301 | .connect { 302 | display: flex; 303 | justify-content: flex-end; 304 | align-items: flex-start; 305 | flex-direction: row; 306 | margin: 10px; 307 | margin-top: 20px; 308 | } 309 | 310 | .status { 311 | position: sticky; 312 | line-height: 20px; 313 | margin-top: -20px; 314 | top: 50%; 315 | transform: translate(-50%, -50%); 316 | border: 1px solid transparent; 317 | border-radius: 15px; 318 | } 319 | 320 | #greenIndicator { 321 | background-color: var(--green); 322 | display: none; 323 | animation: pulseGreen 2100ms infinite; 324 | } 325 | 326 | #redIndicator { 327 | background-color: var(--red); 328 | display: none; 329 | animation: pulseRed 2100ms infinite; 330 | } 331 | 332 | #yellowIndicator { 333 | background-color: var(--yellow); 334 | display: none; 335 | animation: pulseYellow 2100ms infinite; 336 | } 337 | 338 | .typing-container { 339 | align-self: center; 340 | text-align: center; 341 | max-height: 200px; 342 | max-width: 80%; 343 | padding-bottom: 10px; 344 | } 345 | 346 | .display { 347 | color: var(--text); 348 | padding: 10px; 349 | } 350 | 351 | .actions { 352 | display: flex; 353 | align-self: center; 354 | flex-direction: column; 355 | width: 80%; 356 | max-width: 300px; 357 | min-width: 260px; 358 | min-height: 180px; 359 | padding-bottom: 10px; 360 | } 361 | 362 | .center-link { 363 | display: flex; 364 | align-self: center; 365 | text-align: center; 366 | margin-top: 100px; 367 | font-size: x-large; 368 | width: auto; 369 | } 370 | 371 | .wave { 372 | background: var(--wave); 373 | border-radius: 1000% 1000% 0 0; 374 | position: fixed; 375 | width: 200%; 376 | height: 12em; 377 | animation: wave 10s -3s linear infinite; 378 | transform: translate3d(0, 0, 0); 379 | opacity: 0.7; 380 | bottom: 0; 381 | left: 0; 382 | z-index: 300; 383 | max-height: 5%; 384 | } 385 | 386 | .wave:nth-of-type(2) { 387 | bottom: -1em; 388 | animation: wave 21s linear reverse infinite; 389 | opacity: 0.5; 390 | } 391 | 392 | .wave:nth-of-type(3) { 393 | bottom: -2em; 394 | animation: wave 24s -1s reverse infinite; 395 | opacity: 0.6; 396 | } 397 | 398 | ::-webkit-scrollbar { 399 | width: 5px; 400 | height: 5px; 401 | } 402 | 403 | ::-webkit-scrollbar-track { 404 | background: transparent; 405 | } 406 | 407 | ::-webkit-scrollbar-thumb { 408 | background: var(--hover); 409 | border-radius: 10px; 410 | border: 2px solid transparent; 411 | } 412 | 413 | ::-webkit-scrollbar-thumb:hover { 414 | background: var(--green); 415 | } 416 | 417 | @keyframes marquee { 418 | 0% { 419 | transform: translate(0, 0); 420 | } 421 | 422 | 100% { 423 | transform: translate(-100%, 0); 424 | } 425 | } 426 | 427 | @keyframes typing { 428 | from { 429 | border-right-color: var(--typing); 430 | } 431 | 432 | to { 433 | border-right-color: transparent; 434 | } 435 | } 436 | 437 | @keyframes spin { 438 | 0% { 439 | transform: rotate(0deg); 440 | } 441 | 442 | 100% { 443 | transform: rotate(360deg); 444 | } 445 | } 446 | 447 | @keyframes half-spin { 448 | 0% { 449 | transform: rotate(0deg); 450 | } 451 | 452 | 40% { 453 | transform: rotate(180deg); 454 | } 455 | 456 | 50% { 457 | transform: rotate(175deg); 458 | } 459 | 460 | 60%, 461 | 65% { 462 | transform: rotate(190deg); 463 | } 464 | 465 | 100% { 466 | transform: rotate(360deg); 467 | } 468 | } 469 | 470 | @keyframes pulseGreen { 471 | 0% { 472 | transform: scale(0.95); 473 | box-shadow: 0 0 0 0 var(--green); 474 | } 475 | 476 | 50% { 477 | transform: scale(1); 478 | box-shadow: 0 0 0 10px rgba(0, 128, 0, 0); 479 | } 480 | 481 | 100% { 482 | transform: scale(0.95); 483 | box-shadow: 0 0 0 0 rgba(0, 128, 0, 0); 484 | } 485 | } 486 | 487 | @keyframes pulseRed { 488 | 0% { 489 | transform: scale(0.95); 490 | box-shadow: 0 0 0 0 var(--red); 491 | } 492 | 493 | 50% { 494 | transform: scale(1); 495 | box-shadow: 0 0 0 10px rgba(0, 128, 0, 0); 496 | } 497 | 498 | 100% { 499 | transform: scale(0.95); 500 | box-shadow: 0 0 0 0 rgba(0, 128, 0, 0); 501 | } 502 | } 503 | 504 | @keyframes pulseYellow { 505 | 0% { 506 | transform: scale(0.95); 507 | box-shadow: 0 0 0 0 var(--yellow); 508 | } 509 | 510 | 50% { 511 | transform: scale(1); 512 | box-shadow: 0 0 0 10px rgba(0, 128, 0, 0); 513 | } 514 | 515 | 100% { 516 | transform: scale(0.95); 517 | box-shadow: 0 0 0 0 rgba(0, 128, 0, 0); 518 | } 519 | } 520 | 521 | @keyframes wave { 522 | 2% { 523 | transform: translateX(1); 524 | } 525 | 526 | 25% { 527 | transform: translateX(-25%); 528 | } 529 | 530 | 50% { 531 | transform: translateX(-50%); 532 | } 533 | 534 | 75% { 535 | transform: translateX(-25%); 536 | } 537 | 538 | 100% { 539 | transform: translateX(1); 540 | } 541 | } 542 | 543 | @media screen and (max-width: 670px) { 544 | .typing-container { 545 | margin-top: -40px; 546 | max-height: 280px; 547 | max-width: 90%; 548 | } 549 | 550 | .display { 551 | word-break: break-word; 552 | } 553 | 554 | .ring-image-2 { 555 | width: 90%; 556 | height: 90%; 557 | } 558 | 559 | .ring-image-3 { 560 | width: 95%; 561 | height: 95%; 562 | } 563 | } 564 | 565 | @media (prefers-color-scheme: dark) { 566 | :root { 567 | --background: #0d1117; 568 | --rings: #46b86815; 569 | --wave: #d4d4d46b; 570 | --typing: #ffffffbf; 571 | --hover: #a5a5a5; 572 | --text: white; 573 | --link: #d4d4d4; 574 | --gray: #7c7c7c; 575 | } 576 | 577 | body { 578 | color: var(--text); 579 | background-color: var(--background); 580 | } 581 | 582 | code { 583 | border-right: solid 5px var(--typing); 584 | color: #ffffffb3; 585 | } 586 | } 587 | 588 | @media (prefers-color-scheme: light) { 589 | :root { 590 | --background: white; 591 | --rings: #46b86823; 592 | --wave: #27272762; 593 | --typing: #000000bf; 594 | --hover: #a0a0a0; 595 | --text: black; 596 | --link: #272727; 597 | --gray: #dfdfdf; 598 | } 599 | 600 | body { 601 | color: var(--text); 602 | background-color: var(--color-canvas-default); 603 | } 604 | 605 | code { 606 | border-right: solid 5px var(--typing); 607 | color: #000000b3; 608 | } 609 | } -------------------------------------------------------------------------------- /cmd/tela-cli/functions_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/civilware/tela" 14 | "github.com/civilware/tela/logger" 15 | "github.com/civilware/tela/shards" 16 | "github.com/deroproject/derohe/globals" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | var mainPath string 21 | var testDir string 22 | var datashards string 23 | 24 | func TestMain(m *testing.M) { 25 | dir, err := os.Getwd() 26 | if err != nil { 27 | logger.Fatalf("[TELA-CLI] %s\n", err) 28 | } 29 | 30 | mainPath = dir 31 | testDir = filepath.Join(mainPath, "cli_tests") 32 | datashards = filepath.Join(testDir, "datashards") 33 | 34 | err = os.MkdirAll(datashards, os.ModePerm) 35 | if err != nil { 36 | logger.Fatalf("[TELA-CLI] %s\n", err) 37 | } 38 | 39 | m.Run() 40 | } 41 | 42 | // Test functions of TELA-CLI 43 | func TestCLIFunctions(t *testing.T) { 44 | var app tela_cli 45 | 46 | t.Cleanup(func() { 47 | os.RemoveAll(testDir) 48 | app.shutdownLocalServer() 49 | }) 50 | 51 | os.RemoveAll(testDir) 52 | err := tela.SetShardPath(testDir) 53 | if err != nil { 54 | t.Fatalf("Could not set shard path for tests: %s", err) 55 | } 56 | 57 | // // Test parseFlags 58 | t.Run("ParseFlags", func(t *testing.T) { 59 | // No flags 60 | app.parseFlags() 61 | assert.False(t, globals.Arguments["--testnet"].(bool), "Network should be mainnet default with no saved preferences") 62 | assert.False(t, globals.Arguments["--simulator"].(bool), "Network should be mainnet default with no saved preferences") 63 | 64 | // Set flags 65 | os.Args = []string{ 66 | "--testnet", 67 | "--simulator", 68 | "--debug", 69 | "--db-type=boltdb", 70 | "--fastsync=true", 71 | "--num-parallel-blocks=4", 72 | } 73 | app.parseFlags() 74 | assert.True(t, globals.Arguments["--testnet"].(bool), "--testnet was not set") 75 | assert.True(t, globals.Arguments["--simulator"].(bool), "--simulator was not set") 76 | assert.True(t, globals.Arguments["--debug"].(bool), "--debug was not set") 77 | assert.Equal(t, "boltdb", shards.GetDBType(), "--db-type was not set") 78 | assert.True(t, gnomon.fastsync, "--fastsync was not set") 79 | assert.Equal(t, 4, gnomon.parallelBlocks, "--num-parallel-blocks was not set") 80 | globals.InitNetwork() 81 | assert.Equal(t, "Simulator", getNetworkInfo(), "getNetworkInfo should return simulator") 82 | 83 | // Set --mainnet override 84 | os.Args = []string{ 85 | "--testnet", 86 | "--simulator", 87 | "--mainnet", 88 | } 89 | app.parseFlags() 90 | assert.False(t, globals.Arguments["--testnet"].(bool), "--mainnet did not override --testnet") 91 | assert.False(t, globals.Arguments["--simulator"].(bool), "--mainnet did not override --simulator") 92 | globals.InitNetwork() 93 | assert.Equal(t, "Mainnet", getNetworkInfo(), "getNetworkInfo should return mainnet") 94 | }) 95 | 96 | // // Test completer functions 97 | t.Run("Completers", func(t *testing.T) { 98 | assert.Empty(t, completerNothing(), "completerNothing should return empty") 99 | assert.Empty(t, app.completerSearchExclusions(), "completerSearchFilters should return empty without exclusions") 100 | app.exclusions = []string{"exclude"} 101 | assert.NotEmpty(t, app.completerSearchExclusions(), "completerSearchFilters should not be empty with exclusions") 102 | assert.NotEmpty(t, completerNetworks(false), "completerNetworks should not be empty") 103 | assert.NotEmpty(t, completerNetworks(true), "completerNetworks should not be empty") 104 | assert.NotEmpty(t, completerTrueFalse(), "completerTrueFalse should not be empty") 105 | assert.NotEmpty(t, completerYesNo(), "completerYesNo should not be empty") 106 | assert.NotEmpty(t, completerDocType(), "completerDocType should not be empty") 107 | assert.NotEmpty(t, completerMODs("tx"), "completerMODs should not be empty") 108 | assert.NotEmpty(t, completerMODClasses(), "completerMODClasses should not be empty") 109 | // completerSCFunctionNames() 110 | assert.NotEmpty(t, completerFiles("."), "completerFiles should not be empty") 111 | // completerServers() 112 | }) 113 | 114 | // // Test error functions 115 | t.Run("ErrorFunctions", func(t *testing.T) { 116 | eofErr := fmt.Errorf("EOF") 117 | interruptErr := fmt.Errorf("Interrupt") 118 | networkErr := fmt.Errorf("Mainnet/TestNet") 119 | 120 | // Test readError 121 | assert.True(t, readError(interruptErr), "readError should return interrupt as true") 122 | assert.False(t, readError(eofErr), "readError should return false when not interrupt") 123 | 124 | // Test networkError 125 | err := networkError(networkErr) 126 | assert.ErrorContains(t, err, "invalid network settings, run command", "Network mismatch should return info error") 127 | err = networkError(interruptErr) 128 | assert.EqualError(t, err, interruptErr.Error(), "networkError should pass through non network errors") 129 | 130 | // Test lineError 131 | assert.EqualError(t, app.lineError(eofErr), eofErr.Error(), "lineError should pass through EOF error") 132 | assert.Nil(t, app.lineError(nil), "lineError should pass through nil error") 133 | }) 134 | 135 | // // Test findDocShardFiles 136 | t.Run("FindDocShards", func(t *testing.T) { 137 | moveTo := filepath.Join(datashards, "clone", "shard") 138 | var docShardFiles = []struct { 139 | Name string 140 | Source string 141 | Path string 142 | }{ 143 | { 144 | Name: "splitMain.go", 145 | Source: "main.go", 146 | Path: filepath.Join(moveTo, "splitMain.go"), 147 | }, 148 | } 149 | 150 | err := os.MkdirAll(moveTo, os.ModePerm) 151 | assert.NoError(t, err, "Creating directories should not error: %s", err) 152 | 153 | // Shard and then reconstruct the files 154 | for _, shardFile := range docShardFiles { 155 | content, _ := readFile(shardFile.Source) 156 | file, err := os.Create(shardFile.Path) 157 | assert.NoError(t, err, "Creating original file should not error: %s", err) 158 | 159 | _, err = file.Write([]byte(content)) 160 | assert.NoError(t, err, "Writing original file should not error: %s", err) 161 | 162 | // Do compressed and raw 163 | err = tela.CreateShardFiles(shardFile.Path, "", nil) 164 | assert.NoError(t, err, "Sharding original file should not error: %s", err) 165 | err = tela.CreateShardFiles(shardFile.Path, tela.COMPRESSION_GZIP, nil) 166 | assert.NoError(t, err, "Sharding compressed original file should not error: %s", err) 167 | 168 | shardEntrypoint := filepath.Join(moveTo, strings.ReplaceAll(shardFile.Name, ".go", "-1.go")) 169 | docShards, recreate, _, err := findDocShardFiles(shardEntrypoint) 170 | assert.NoError(t, err, "Finding shard files should not error: %s", err) 171 | assert.Equal(t, shardFile.Name, recreate, "Recreated file name should be the same") 172 | 173 | err = os.RemoveAll(shardFile.Path) 174 | assert.NoError(t, err, "Removing original file should not error: %s", err) 175 | 176 | err = tela.ConstructFromShards(docShards, recreate, moveTo, "") 177 | assert.NoError(t, err, "Recreating original file should not error: %s", err) 178 | 179 | movePath := filepath.Join("cli_tests", "datashards", "clone", "shard", shardFile.Name) 180 | 181 | newContent, err := readFile(movePath) 182 | assert.NoError(t, err, "Reading the recreated file should not error: %s", err) 183 | assert.Equal(t, content, newContent, "Recreated content should match original") 184 | 185 | // Remove and recreate the compressed version 186 | err = os.RemoveAll(shardFile.Path) 187 | assert.NoError(t, err, "Removing original file should not error: %s", err) 188 | 189 | shardEntrypoint = filepath.Join(moveTo, strings.ReplaceAll(shardFile.Name, ".go", "-1.go"+tela.COMPRESSION_GZIP)) 190 | docShards, recreate, compression, err := findDocShardFiles(shardEntrypoint) 191 | assert.NoError(t, err, "Finding compressed shard files should not error: %s", err) 192 | assert.Equal(t, shardFile.Name, recreate, "Recreated compressed file name should be the same") 193 | 194 | err = tela.ConstructFromShards(docShards, recreate, moveTo, compression) 195 | assert.NoError(t, err, "Recreating compressed original file should not error: %s", err) 196 | 197 | newContent, err = readFile(movePath) 198 | assert.NoError(t, err, "Reading the decompressed recreated file should not error: %s", err) 199 | assert.Equal(t, content, newContent, "Recreated decompressed content should match original") 200 | } 201 | 202 | _, _, _, err = findDocShardFiles("main.go") 203 | assert.Error(t, err, "findDocShardFiles should error with non shard file") 204 | }) 205 | 206 | // Test gitClone if instructed to 207 | t.Run("GitClone", func(t *testing.T) { 208 | if os.Getenv("RUN_GIT_TEST") != "true" { 209 | t.Skipf("Use %q to run Git test", "RUN_GIT_TEST=true go test . -v") 210 | } 211 | 212 | _, err := exec.LookPath("git") 213 | if err != nil { 214 | t.Skipf("Git is not installed for clone tests") 215 | } 216 | 217 | // Valid git clone 218 | err = gitClone("github.com/civilware/tela@main") 219 | assert.NoError(t, err, "Cloning valid repo should not error: %s", err) 220 | 221 | // Invalid git clone 222 | err = gitClone("https://github.com/civilware/tela") 223 | assert.Error(t, err, "Cloning with long form URL should error") 224 | err = gitClone("github.com/civilware/tela.git") 225 | assert.Error(t, err, "Cloning with .git URL suffix should error") 226 | err = gitClone("civilware/tela") 227 | assert.Error(t, err, "Cloning with invalid URL should error") 228 | err = gitClone("/ / /") 229 | assert.Error(t, err, "Cloning with invalid URL parts should error") 230 | }) 231 | 232 | // // Test getFileDiff 233 | t.Run("GetDiff", func(t *testing.T) { 234 | diff, fileNames, err := getFileDiff("gnomon.go", "gnomon.go") 235 | assert.NoError(t, err, "getFileDiff should not error with valid files: %s", err) 236 | assert.Len(t, diff, 2, "getFileDiff should return 2 diffs") 237 | assert.Len(t, fileNames, 2, "getFileDiff should return 2 fileNames") 238 | assert.NotEmpty(t, diff[0], "getFileDiff code1 should not be empty") 239 | assert.NotEmpty(t, diff[1], "getFileDiff code2 should not be empty") 240 | 241 | _, _, err = getFileDiff(" ", " ") 242 | assert.Error(t, err, "getFileDiff should error with invalid files") 243 | }) 244 | 245 | // // Test printDiff 246 | t.Run("PrintDiff", func(t *testing.T) { 247 | var diffTests = []struct { 248 | diff []string // The things to compare 249 | expected string // Expected result of comparison 250 | }{ 251 | // No diff 252 | { 253 | diff: []string{ 254 | `1 255 | 2 256 | 3 257 | 4 258 | 5 259 | 6`, 260 | `1 261 | 2 262 | 3 263 | 4 264 | 5 265 | 6`, 266 | }, 267 | expected: "No diffs found", 268 | }, 269 | // Diff line 4, same file len 270 | { 271 | diff: []string{ 272 | `1 273 | 2 274 | 3 275 | 4 276 | 5 277 | 6`, 278 | `1 279 | 2 280 | 3 281 | 3 282 | 5 283 | 6`, 284 | }, 285 | expected: fmt.Sprintf("%s\n%s", "4 - 4", "4 + 3"), 286 | }, 287 | // Diff line 4 and 9 to hit context, same file len 288 | { 289 | diff: []string{ 290 | `1 291 | 2 292 | 3 293 | 4 294 | 5 295 | 6 296 | 7 297 | 8 298 | 9 299 | 10`, 300 | `1 301 | 2 302 | 3 303 | 3 304 | 5 305 | 6 306 | 7 307 | 8 308 | 8 309 | 10`, 310 | }, 311 | expected: fmt.Sprintf("%s\n%s\n\n%s\n%s", "4 - 4", "4 + 3", "9 - 9", "9 + 8"), 312 | }, 313 | // Diff, file1 longer 314 | { 315 | diff: []string{ 316 | `1 317 | 2 318 | 3 319 | 4 320 | 5 321 | 6`, 322 | `1 323 | 2 324 | 6`, 325 | }, 326 | expected: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", "3 - 3", "3 + 6", "4 - 4", "5 - 5", "6 - 6"), 327 | }, 328 | // Diff, file1 longer same end 329 | { 330 | diff: []string{ 331 | `1 332 | 2 333 | 3 334 | 4 335 | 5 336 | 6`, 337 | `1 338 | 2 339 | 3`, 340 | }, 341 | expected: fmt.Sprintf("%s\n%s\n%s", "4 - 4", "5 - 5", "6 - 6"), 342 | }, 343 | // Diff, file2 longer 344 | { 345 | diff: []string{ 346 | `1 347 | 2 348 | 6`, 349 | `1 350 | 2 351 | 3 352 | 4 353 | 5 354 | 6`, 355 | }, 356 | expected: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", "3 - 6", "3 + 3", "4 + 4", "5 + 5", "6 + 6"), 357 | }, 358 | // Diff, file2 longer same end 359 | { 360 | diff: []string{ 361 | `1 362 | 2 363 | 3`, 364 | `1 365 | 2 366 | 3 367 | 4 368 | 5 369 | 6`, 370 | }, 371 | expected: fmt.Sprintf("%s\n%s\n%s", "4 + 4", "5 + 5", "6 + 6"), 372 | }, 373 | } 374 | 375 | app.pageSize = 20 376 | 377 | logger.EnableColors(false) 378 | outputHeader := "--- a/file1.txt\n+++ b/file2.txt" 379 | 380 | for i, d := range diffTests { 381 | output := captureOutput(func() { 382 | err := app.printDiff(d.diff, []string{"file1.txt", "file2.txt"}) 383 | assert.NoError(t, err, "printDiff with valid inputs should not error: %s", err) 384 | }) 385 | 386 | if i > 0 { 387 | assert.Contains(t, output, outputHeader, "Expected output %d to have matching header", i) 388 | } 389 | assert.Contains(t, output, d.expected, "Output diff %d does not match expected", i) 390 | } 391 | 392 | err := app.printDiff([]string{}, []string{"file1.txt", "file2.txt"}) 393 | assert.Error(t, err, "printDiff with invalid diff should error") 394 | err = app.printDiff([]string{"1", "1"}, []string{}) 395 | assert.Error(t, err, "printDiff with invalid filenames should error") 396 | }) 397 | 398 | t.Run("ServeLocal", func(t *testing.T) { 399 | // Test serveLocal 400 | err := app.serveLocal(".") 401 | assert.NoError(t, err, "serveLocal should not error: %s", err) 402 | 403 | // Test openBrowser 404 | app.os = runtime.GOOS 405 | err = app.openBrowser("http://localhost:8080") 406 | assert.NoError(t, err, "openBrowser should not error opening localhost: %s", err) 407 | 408 | // Test getServerInfo 409 | _, _, localRunning := app.getServerInfo() 410 | assert.True(t, localRunning, "Local server should be running") 411 | 412 | // Test getCLIInfo 413 | assert.NotEmpty(t, app.getCLIInfo(), "getCLIInfo should not be empty") 414 | }) 415 | } 416 | 417 | // Capture the console output 418 | func captureOutput(f func()) string { 419 | var buf bytes.Buffer 420 | stdout := os.Stdout 421 | r, w, _ := os.Pipe() 422 | os.Stdout = w 423 | 424 | f() 425 | 426 | w.Close() 427 | os.Stdout = stdout 428 | buf.ReadFrom(r) 429 | return buf.String() 430 | } 431 | 432 | // Read files for test 433 | func readFile(elem ...string) (string, error) { 434 | path := mainPath 435 | for _, e := range elem { 436 | path = filepath.Join(path, e) 437 | } 438 | 439 | file, err := os.ReadFile(path) 440 | if err != nil { 441 | return "", err 442 | } 443 | 444 | return string(file), nil 445 | } 446 | -------------------------------------------------------------------------------- /tela_tests/app1/main.js: -------------------------------------------------------------------------------- 1 | // Global websocket 2 | let socket; 3 | 4 | // XSWD application data 5 | const applicationData = { 6 | "id": "71605a32e3b0c44298fc1c549afbf4c8496fb92427ae41e4649b934ca495991b", 7 | "name": "TELA Demo Application", 8 | "description": "Basic WS connection parts for TELA application", 9 | "url": "http://localhost:" + location.port, // Get the current port being used by server to set in our XSWD application data, must match origin URL 10 | }; 11 | 12 | let typed = 0; 13 | let typeText = ""; 14 | const typeSpeed = 50; 15 | 16 | const jsonBody = document.getElementById("jsonDisplayBody"); 17 | const jsonResult = document.getElementById("jsonDisplayResult"); 18 | 19 | // Typing text effect 20 | function typeWriter(text) { 21 | const html = document.getElementById("typingLabel"); 22 | if (typed === 0) { 23 | html.innerHTML = ""; 24 | typeText = text; 25 | } 26 | 27 | if (typed < typeText.length) { 28 | html.innerHTML += typeText.charAt(typed); 29 | typed++; 30 | setTimeout(typeWriter, typeSpeed); 31 | } 32 | 33 | if (typed === typeText.length) { 34 | setTimeout(() => { 35 | typed = 0; 36 | }, typeSpeed); 37 | } 38 | } 39 | 40 | // Function to send data 41 | function sendData(d) { 42 | if (socket && socket.readyState === WebSocket.OPEN) { 43 | try { 44 | socket.send(JSON.stringify(d)); 45 | if (d.method) { 46 | console.log(d.method, "request sent to the server"); 47 | if (jsonBody) jsonBody.innerHTML = JSON.stringify(d, null, 2); 48 | if (jsonResult) jsonResult.innerHTML = ""; 49 | } else { 50 | console.log("Connection request sent to the server"); 51 | } 52 | } catch (error) { 53 | console.error("Failed to send data:", error); 54 | toggleIndicators("red"); 55 | } 56 | } else { 57 | console.log("Web socket is not open. State:", socket ? socket.readyState : "N/A"); 58 | toggleIndicators("red"); 59 | } 60 | } 61 | 62 | // Handle web socket connection and listeners 63 | function connectWebSocket() { 64 | // If we are already connected, disconnect 65 | if (document.getElementById("connectButton").textContent === "Disconnect") { 66 | toggleIndicators("red"); 67 | return; 68 | } 69 | 70 | // Connect to the web socket 71 | socket = new WebSocket("ws://localhost:44326/xswd"); 72 | 73 | // Listen for open 74 | socket.addEventListener("open", function (event) { 75 | console.log("Web socket connection established:", event); 76 | toggleIndicators("yellow"); 77 | typed = 0; 78 | typeWriter("Waiting for wallet reply"); 79 | sendData(applicationData); // Send ApplicationData after connection is established 80 | }); 81 | 82 | let address = ""; 83 | let connecting = true; 84 | 85 | // Listen for the messages 86 | socket.addEventListener("message", function (event) { 87 | const response = JSON.parse(event.data); 88 | console.log("Response received:", response); 89 | if (response.accepted) { // If connection is accepted, we will request to get address from wallet 90 | console.log("Connected message received:", response.message); 91 | sendData({ 92 | jsonrpc: "2.0", 93 | id: "1", 94 | method: "GetAddress" 95 | }); 96 | } else if (response.result) { 97 | const res = response.result; 98 | if (jsonResult) jsonResult.innerHTML = JSON.stringify(res, null, 2); 99 | typed = 0; 100 | if (res.address) { // If GetAddress is allowed by user 101 | address = res.address; 102 | console.log("Connected address:", address); 103 | toggleIndicators("green"); 104 | connecting = false; 105 | typeWriter(address); 106 | } else if (res.unlocked_balance) { // Balance response 107 | const bal = "Balance: " + (res.unlocked_balance / 100000).toFixed(5) + " DERO"; 108 | console.log(bal); 109 | typeWriter(bal); 110 | } else if (res.height) { // Height response 111 | const height = "Height: " + res.height; 112 | console.log(height); 113 | typeWriter(height); 114 | } else if (res.telaLinkResult) { // TELA link response 115 | const link = "Opened TELA link: " + res.telaLinkResult; 116 | console.log(link); 117 | typeWriter(link); 118 | } else if (res.lastIndexHeight) { // Gnomon responses 119 | const gnomon = "Gnomon indexed height: " + res.lastIndexHeight; 120 | console.log(gnomon); 121 | typeWriter(gnomon); 122 | } else if (res.epochHashes) { // EPOCH responses 123 | const epoch = "Hashes: " + res.epochHashes + " in " + res.epochDuration + "ms and submitted " + res.epochSubmitted + " as miniblocks"; 124 | console.log(epoch); 125 | typeWriter(epoch); 126 | } else if (res.epochAddress) { 127 | const epoch = "EPOCH address: " + res.epochAddress; 128 | console.log(epoch); 129 | typeWriter(epoch); 130 | } else if (res.maxHashes) { 131 | const epoch = "EPOCH max hashes: " + res.maxHashes; 132 | console.log(epoch); 133 | typeWriter(epoch); 134 | } else if (res.sessionMinis) { 135 | const epoch = "EPOCH session hashes: " + res.sessionHashes + " miniblocks: " + res.sessionMinis; 136 | console.log(epoch); 137 | typeWriter(epoch); 138 | } 139 | } else if (response.error) { // Display error message 140 | console.error("Error:", response.error.message); 141 | typed = 0; 142 | typeWriter(" " + response.error.message); 143 | document.getElementById("typingLabel").innerHTML = ""; 144 | if (connecting) toggleIndicators("red"); 145 | } 146 | }); 147 | 148 | // Listen for errors 149 | socket.addEventListener("error", function (event) { 150 | console.error("Web socket error:", event); 151 | typed = 0; 152 | typeWriter(" Web socket error: " + event.target.url.toString()); 153 | }); 154 | 155 | // Listen for close 156 | socket.addEventListener("close", function (event) { 157 | console.log("Web socket connection closed:", event.code, event.reason); 158 | toggleIndicators("red"); 159 | }); 160 | } 161 | 162 | // Change indictor color based on connection status 163 | function toggleIndicators(color) { 164 | if (color === "green") { 165 | document.getElementById("connectButton").textContent = "Disconnect"; 166 | document.getElementById("greenIndicator").style.display = "block"; 167 | document.getElementById("yellowIndicator").style.display = "none"; 168 | document.getElementById("redIndicator").style.display = "none"; 169 | } else if (color === "yellow") { 170 | document.getElementById("connectButton").textContent = "Disconnect"; 171 | document.getElementById("greenIndicator").style.display = "none"; 172 | document.getElementById("yellowIndicator").style.display = "block"; 173 | document.getElementById("redIndicator").style.display = "none"; 174 | } else { 175 | document.getElementById("connectButton").textContent = "Connect"; 176 | document.getElementById("greenIndicator").style.display = "none"; 177 | document.getElementById("yellowIndicator").style.display = "none"; 178 | document.getElementById("redIndicator").style.display = "block"; 179 | document.getElementById("typingLabel").textContent = ""; 180 | if (socket) socket.close(), socket = null; 181 | } 182 | } 183 | 184 | // Create new text input 185 | function newInput(p, n) { 186 | const input = document.createElement("input"); 187 | if (n) { 188 | input.type = "number"; 189 | input.step = 1; 190 | input.min = 0; 191 | } else { 192 | input.type = "text"; 193 | } 194 | 195 | input.id = p; 196 | input.placeholder = p + ":"; 197 | 198 | return input; 199 | } 200 | 201 | // Create needed params for request 202 | function requestParams() { 203 | const container = document.getElementById("paramsContainer"); 204 | const select = document.getElementById("selectCall"); 205 | container.innerHTML = ""; 206 | switch (select.value) { 207 | case "HandleTELALinks": 208 | container.appendChild(newInput("telaLink")); break; 209 | case "GetTxCount": 210 | container.appendChild(newInput("txType")); break; 211 | case "GetOwner": 212 | case "GetAllNormalTxWithSCIDBySCID": 213 | case "GetAllSCIDInvokeDetails": 214 | case "GetAllSCIDVariableDetails": 215 | case "GetSCIDInteractionHeight": 216 | container.appendChild(newInput("scid")); break; 217 | case "GetAllNormalTxWithSCIDByAddr": 218 | case "GetMiniblockCountByAddress": 219 | case "GetSCIDInteractionByAddr": 220 | container.appendChild(newInput("address")); break; 221 | case "GetAllSCIDInvokeDetailsByEntrypoint": 222 | container.appendChild(newInput("scid")); 223 | container.appendChild(newInput("entrypoint")); break; 224 | case "GetAllSCIDInvokeDetailsBySigner": 225 | container.appendChild(newInput("scid")); 226 | container.appendChild(newInput("signer")); break; 227 | case "GetSCIDVariableDetailsAtTopoheight": 228 | container.appendChild(newInput("scid")); 229 | container.appendChild(newInput("height", true)); break; 230 | case "GetSCIDKeysByValue": 231 | case "GetSCIDValuesByKey": 232 | container.appendChild(newInput("scid")); 233 | container.appendChild(newInput("height", true)); 234 | if (select.value === "GetSCIDKeysByValue") { 235 | container.appendChild(newInput("value")); 236 | } else { 237 | container.appendChild(newInput("key")); 238 | } 239 | break; 240 | case "GetLiveSCIDValuesByKey": 241 | case "GetLiveSCIDKeysByValue": 242 | container.appendChild(newInput("scid")); 243 | if (select.value === "GetLiveSCIDKeysByValue") { 244 | container.appendChild(newInput("value")); 245 | } else { 246 | container.appendChild(newInput("key")); 247 | } 248 | break; 249 | case "GetInteractionIndex": 250 | container.appendChild(newInput("topoheight", true)); 251 | container.appendChild(newInput("height", true)); break; 252 | case "GetMiniblockDetailsByHash": 253 | container.appendChild(newInput("blid")); break; 254 | case "AttemptEPOCH": 255 | container.appendChild(newInput("hashes", true)); break; 256 | case "AttemptEPOCHWithAddr": 257 | container.appendChild(newInput("address")); 258 | container.appendChild(newInput("hashes", true)); break; 259 | } 260 | } 261 | 262 | // Send call with params 263 | function callFor() { 264 | if (!socket) { 265 | typed = 0; 266 | typeWriter("Wallet is not connected"); 267 | return; 268 | } 269 | 270 | typed = 0; 271 | document.getElementById("typingLabel").innerHTML = ""; 272 | 273 | const select = document.getElementById("selectCall"); 274 | let m = ""; 275 | if (select.selectedIndex > 3 && !select.value.includes("EPOCH")) { 276 | m = "Gnomon."; 277 | } else { 278 | typeWriter("Waiting for wallet reply"); 279 | } 280 | 281 | const call = { 282 | jsonrpc: "2.0", 283 | id: "1", 284 | method: m + select.value, 285 | params: {}, 286 | }; 287 | 288 | switch (select.value) { 289 | case "HandleTELALinks": 290 | call.params.telaLink = document.getElementById("telaLink").value; break; 291 | case "GetTxCount": 292 | call.params.txType = document.getElementById("txType").value; break; 293 | case "GetOwner": 294 | case "GetAllNormalTxWithSCIDBySCID": 295 | case "GetAllSCIDInvokeDetails": 296 | case "GetAllSCIDVariableDetails": 297 | case "GetSCIDInteractionHeight": 298 | call.params.scid = document.getElementById("scid").value; break; 299 | case "GetAllNormalTxWithSCIDByAddr": 300 | case "GetMiniblockCountByAddress": 301 | case "GetSCIDInteractionByAddr": 302 | call.params.address = document.getElementById("address").value; break; 303 | case "GetAllSCIDInvokeDetailsByEntrypoint": 304 | call.params.scid = document.getElementById("scid").value; 305 | call.params.entrypoint = document.getElementById("entrypoint").value; break; 306 | case "GetAllSCIDInvokeDetailsBySigner": 307 | call.params.scid = document.getElementById("scid").value; 308 | call.params.signer = document.getElementById("signer").value; break; 309 | case "GetSCIDVariableDetailsAtTopoheight": 310 | call.params.scid = document.getElementById("scid").value; 311 | call.params.height = parseInt(document.getElementById("height").value); break; 312 | case "GetSCIDKeysByValue": 313 | case "GetSCIDValuesByKey": 314 | call.params.scid = document.getElementById("scid").value; 315 | call.params.height = parseInt(document.getElementById("height").value); 316 | if (select.value === "GetSCIDKeysByValue") { 317 | call.params.value = document.getElementById("value").value; 318 | } else { 319 | call.params.value = document.getElementById("key").value; 320 | } 321 | break; 322 | case "GetLiveSCIDValuesByKey": 323 | case "GetLiveSCIDKeysByValue": 324 | call.params.scid = document.getElementById("scid").value; 325 | if (select.value === "GetLiveSCIDKeysByValue") { 326 | call.params.value = document.getElementById("value").value; 327 | } else { 328 | call.params.value = document.getElementById("key").value; 329 | } 330 | break; 331 | case "GetInteractionIndex": 332 | call.params.topoheight = parseInt(document.getElementById("topoheight").value); 333 | call.params.height = parseInt(document.getElementById("height").value); break; 334 | case "GetMiniblockDetailsByHash": 335 | call.params.blid = document.getElementById("blid").value; break; 336 | case "AttemptEPOCH": 337 | call.params.hashes = parseInt(document.getElementById("hashes").value); break; 338 | case "AttemptEPOCHWithAddr": 339 | call.params.address = document.getElementById("address").value; 340 | call.params.hashes = parseInt(document.getElementById("hashes").value); break; 341 | default: 342 | call.params = null; break; 343 | } 344 | 345 | sendData(call); 346 | } -------------------------------------------------------------------------------- /mods.go: -------------------------------------------------------------------------------- 1 | package tela 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/civilware/tela/logger" 8 | "github.com/deroproject/derohe/dvm" 9 | 10 | _ "embed" 11 | ) 12 | 13 | // TELA-MOD structure 14 | type MOD struct { 15 | Name string // The name of the MOD 16 | Tag string // The identifying tag for the MOD 17 | Description string // Description of what the MOD does 18 | FunctionCode func() string // The embedded DVM code 19 | FunctionNames []string // The function names from the DVM code 20 | } 21 | 22 | // MODClass structure is used to group similarly functioned MODs 23 | type MODClass struct { 24 | Name string // The name of the MODClass 25 | Tag string // The identifying tag for the MODClass 26 | Rules []MODClassRule // Any rules for the MODClass 27 | } 28 | 29 | // MODClassRule structure contains data for the rules used within a MODClass 30 | type MODClassRule struct { 31 | Name string // Name of the rule 32 | Description string // Description of what the rule is enforcing 33 | Verify func([]string, MODClass) error // Verification function checking that the parameters of the rule are met 34 | } 35 | 36 | // MODs contains the package's TELA-MOD data structures and access, 37 | // all MODClasses are held in the mods variable and can be separated by its index 38 | type MODs struct { 39 | mods []MOD // MOD data 40 | classes []MODClass // classes hold the MODClass info 41 | index []int // index is the index for the MODClasses: vs, tx... 42 | rules []MODClassRule 43 | } 44 | 45 | // Access the package's initialized TELA-MOD data 46 | var Mods MODs 47 | 48 | // Tag returns a TELA-MOD tag string by index 49 | func (m *MODs) Tag(i int) (tag string) { 50 | if i < len(m.mods) { 51 | tag = m.mods[i].Tag 52 | } 53 | 54 | return 55 | } 56 | 57 | // Functions returns the TELA-MOD function code and names for the given tag 58 | func (m *MODs) Functions(tag string) (functionCode string, functionNames []string) { 59 | for _, m := range m.mods { 60 | if m.Tag == tag { 61 | return m.FunctionCode(), m.FunctionNames 62 | } 63 | } 64 | 65 | return 66 | } 67 | 68 | // GetAllMods returns all TELA-MODs 69 | func (m *MODs) GetAllMods() (mods []MOD) { 70 | mods = m.mods 71 | return 72 | } 73 | 74 | // GetAllClasses returns all MODClasses 75 | func (m *MODs) GetAllClasses() (classes []MODClass) { 76 | classes = m.classes 77 | return 78 | } 79 | 80 | // GetMod returns a TELA-MOD by its tag string 81 | func (m *MODs) GetMod(tag string) (mod MOD) { 82 | for _, m := range m.mods { 83 | if m.Tag == tag { 84 | mod = m 85 | return 86 | } 87 | } 88 | 89 | return 90 | } 91 | 92 | // GetClass returns a MODClass from a MOD tag 93 | func (m *MODs) GetClass(modTag string) (class MODClass) { 94 | for _, c := range m.classes { 95 | if strings.HasPrefix(modTag, c.Tag) { 96 | return c 97 | } 98 | } 99 | 100 | return 101 | } 102 | 103 | // GetRules returns all of the MODClassesRules 104 | func (m *MODs) GetRules() (rules []MODClassRule) { 105 | rules = m.rules 106 | return 107 | } 108 | 109 | // Index returns the MODClass index 110 | func (m *MODs) Index() (ci []int) { 111 | ci = m.index 112 | return 113 | } 114 | 115 | // Function HowToAddMODs() Uint64 116 | // 10 Create the function set bas file in /TELA-MOD-1/ 117 | // 20 Embed the function set bas file below as a new variable 118 | // 30 In initMods() create and append the new MOD in its MODClass's []MOD 119 | // 35 If a new MODClass is required, it can be added similarly to the existing MODClasses 120 | // 50 RETURN 0 121 | // End Function 122 | 123 | // // Embed the TELA-MOD smart contract function sets 124 | 125 | //go:embed */vs/TELA-MOD-1-VSOO.bas 126 | var TELA_MOD_1_VSOO string 127 | 128 | //go:embed */vs/TELA-MOD-1-VSOOIM.bas 129 | var TELA_MOD_1_VSOOIM string 130 | 131 | //go:embed */vs/TELA-MOD-1-VSPUBSU.bas 132 | var TELA_MOD_1_VSPUBSU string 133 | 134 | //go:embed */vs/TELA-MOD-1-VSPUBOW.bas 135 | var TELA_MOD_1_VSPUBOW string 136 | 137 | //go:embed */vs/TELA-MOD-1-VSPUBIM.bas 138 | var TELA_MOD_1_VSPUBIM string 139 | 140 | //go:embed */tx/TELA-MOD-1-TXDWA.bas 141 | var TELA_MOD_1_TXDWA string 142 | 143 | //go:embed */tx/TELA-MOD-1-TXDWD.bas 144 | var TELA_MOD_1_TXDWD string 145 | 146 | //go:embed */tx/TELA-MOD-1-TXTO.bas 147 | var TELA_MOD_1_TXTO string 148 | 149 | // Initialize TELA-MOD data 150 | func initMods() { 151 | // Initialize all the package's MODClass rules 152 | { 153 | rules := []MODClassRule{ 154 | { 155 | Name: "Single MOD", 156 | Description: "Only one MOD from this MODClass can be used at a time", 157 | Verify: func(tags []string, c MODClass) (err error) { 158 | if hasMultipleClassTags(tags, c.Tag) { 159 | err = fmt.Errorf("conflicting tags for %q MODClass", strings.ToLower(c.Name)) 160 | } 161 | return 162 | }, 163 | }, 164 | { 165 | Name: "Multi MOD", 166 | Description: "Multiple MODs from this MODClass can be used simultaneously", 167 | Verify: func(tags []string, c MODClass) (err error) { 168 | // All MODs are valid so return nil 169 | return 170 | }, 171 | }, 172 | } 173 | 174 | // When new rules are added, any conflict handling will go in verifyRules() 175 | Mods.rules = append(Mods.rules, rules...) 176 | } 177 | 178 | // // Variable Store MODClass 179 | { 180 | // Initialize the MODClass 181 | variableStoreMODClass := MODClass{ 182 | Name: "Variable store", 183 | Tag: "vs", 184 | Rules: []MODClassRule{Mods.rules[0]}, 185 | } 186 | 187 | variableStoreMODs := []MOD{ 188 | { 189 | Name: fmt.Sprintf("%s %s", variableStoreMODClass.Name, "owner only"), 190 | Tag: variableStoreMODClass.NewTag("oo"), 191 | Description: "Allows the owner of the smart contract to set and delete custom variable stores", 192 | FunctionCode: func() string { return TELA_MOD_1_VSOO }, 193 | FunctionNames: []string{"SetVar", "DeleteVar"}, 194 | }, 195 | { 196 | Name: fmt.Sprintf("%s %s", variableStoreMODClass.Name, "owner only immutable"), 197 | Tag: variableStoreMODClass.NewTag("ooim"), 198 | Description: "Allows the owner of the smart contract to set custom variable stores which cannot be changed or deleted", 199 | FunctionCode: func() string { return TELA_MOD_1_VSOOIM }, 200 | FunctionNames: []string{"SetVar"}, 201 | }, 202 | { 203 | Name: fmt.Sprintf("%s %s", variableStoreMODClass.Name, "public single use"), 204 | Tag: variableStoreMODClass.NewTag("pubsu"), 205 | Description: "Allows all wallets to store variables which the wallet cannot change, the owner of the smart contract can set and delete all variables", 206 | FunctionCode: func() string { return TELA_MOD_1_VSPUBSU }, 207 | FunctionNames: []string{"SetVar", "DeleteVar"}, 208 | }, 209 | { 210 | Name: fmt.Sprintf("%s %s", variableStoreMODClass.Name, "public overwrite"), 211 | Tag: variableStoreMODClass.NewTag("pubow"), 212 | Description: "Allows all wallets to store variables which the wallet can overwrite, the owner of the smart contract can set and delete all variables", 213 | FunctionCode: func() string { return TELA_MOD_1_VSPUBOW }, 214 | FunctionNames: []string{"SetVar", "DeleteVar"}, 215 | }, 216 | { 217 | Name: fmt.Sprintf("%s %s", variableStoreMODClass.Name, "public immutable"), 218 | Tag: variableStoreMODClass.NewTag("pubim"), 219 | Description: "Allows all wallets to store variables which cannot be changed or deleted from the smart contract", 220 | FunctionCode: func() string { return TELA_MOD_1_VSPUBIM }, 221 | FunctionNames: []string{"SetVar"}, 222 | }, 223 | // New MOD for variable store MODClass would go here 224 | } 225 | 226 | // Add the variable store MODClass and its MODs 227 | Mods.Add(variableStoreMODClass, variableStoreMODs) 228 | } 229 | 230 | // // Transfers MODClass 231 | { 232 | transferMODClass := MODClass{ 233 | Name: "Transfers", 234 | Tag: "tx", 235 | Rules: []MODClassRule{Mods.rules[1]}, 236 | } 237 | 238 | transferMODs := []MOD{ 239 | { 240 | Name: "Deposit and withdraw DERO", 241 | Tag: transferMODClass.NewTag("dwd"), 242 | Description: "Stores DERO deposits and owner can withdraw DERO from the smart contract", 243 | FunctionCode: func() string { return TELA_MOD_1_TXDWD }, 244 | FunctionNames: []string{"DepositDero", "WithdrawDero"}, 245 | }, 246 | { 247 | Name: "Deposit and withdraw assets", 248 | Tag: transferMODClass.NewTag("dwa"), 249 | Description: "Stores asset deposits and owner can withdraw tokens from the smart contract", 250 | FunctionCode: func() string { return TELA_MOD_1_TXDWA }, 251 | FunctionNames: []string{"DepositAsset", "WithdrawAsset"}, 252 | }, 253 | { 254 | Name: "Transfer ownership", 255 | Tag: transferMODClass.NewTag("to"), 256 | Description: "Transfer the ownership of the smart contract to another wallet", 257 | FunctionCode: func() string { return TELA_MOD_1_TXTO }, 258 | FunctionNames: []string{"TransferOwnership", "ClaimOwnership"}, 259 | }, 260 | // New MODs for transfers MODClass would go here 261 | } 262 | 263 | // Add the transfers MODClass and its MODs 264 | Mods.Add(transferMODClass, transferMODs) 265 | } 266 | 267 | // This is checked each Add but we should ensure again there is no conflicts with the initialized MODClasses and MODs 268 | err := Mods.Verify() 269 | if err != nil { 270 | logger.Fatalf("[TELA] MODs: %s\n", err) 271 | } 272 | } 273 | 274 | // verifyRules is used to check that the initialized MODClassRules do not conflict with each other 275 | func (mc *MODClass) verifyRules() (err error) { 276 | var hasSingleMODRule bool 277 | for _, r := range mc.Rules { 278 | if r.Name == "Single MOD" { 279 | hasSingleMODRule = true 280 | continue 281 | } 282 | 283 | if hasSingleMODRule && r.Name == "Multi MOD" { 284 | err = fmt.Errorf("conflicting rules for MODClass %q", mc.Name) 285 | return 286 | } 287 | } 288 | 289 | return 290 | } 291 | 292 | // Verify that all MODs present have the appropriate data and that there are no conflicting MODs or MODClasses 293 | func (m *MODs) Verify() (err error) { 294 | var classTags, modTags, modNames []string 295 | for _, c := range m.classes { 296 | err = c.verifyRules() 297 | if err != nil { 298 | return 299 | } 300 | 301 | classTags = append(classTags, c.Tag) 302 | } 303 | 304 | duplicate, found := hasDuplicateString(classTags) 305 | if found { 306 | err = fmt.Errorf("class tag %q cannot be duplicated", duplicate) 307 | return 308 | } 309 | 310 | if len(m.classes) != len(m.index) { 311 | err = fmt.Errorf("invalid MODClass index: %d/%d", len(m.classes), len(m.index)) 312 | return 313 | } 314 | 315 | for _, mod := range m.mods { 316 | class := m.GetClass(mod.Tag) 317 | if class.Tag == "" { 318 | err = fmt.Errorf("could not get MODClass tag for %q", mod.Tag) 319 | return 320 | } 321 | 322 | if mod.FunctionCode == nil { 323 | err = fmt.Errorf("missing function code for %q", mod.Name) 324 | return 325 | } 326 | 327 | var sc dvm.SmartContract 328 | sc, _, err = dvm.ParseSmartContract(mod.FunctionCode()) 329 | if err != nil { 330 | err = fmt.Errorf("function code for %q is invalid: %s", mod.Name, err) 331 | return 332 | } 333 | 334 | if len(mod.FunctionNames) != len(sc.Functions) { 335 | err = fmt.Errorf("missing function names for %q %d/%d", mod.Name, len(mod.FunctionNames), len(sc.Functions)) 336 | return 337 | } 338 | 339 | modTags = append(modTags, mod.Tag) 340 | modNames = append(modNames, mod.Name) 341 | } 342 | 343 | duplicate, found = hasDuplicateString(modTags) 344 | if found { 345 | err = fmt.Errorf("tag %q cannot be duplicated", duplicate) 346 | return 347 | } 348 | 349 | duplicate, found = hasDuplicateString(modNames) 350 | if found { 351 | err = fmt.Errorf("name %q cannot be duplicated", duplicate) 352 | return 353 | } 354 | 355 | return 356 | } 357 | 358 | // Add a new MODClass and its MODs if they are valid 359 | func (m *MODs) Add(class MODClass, mods []MOD) (err error) { 360 | addMod := MODs{ 361 | mods: append(mods, m.mods...), 362 | classes: append([]MODClass{class}, m.classes...), 363 | index: append(m.index, len(mods)+len(m.mods)), 364 | } 365 | 366 | err = addMod.Verify() 367 | if err != nil { 368 | return 369 | } 370 | 371 | // Add the new MODClass 372 | m.classes = append(m.classes, class) 373 | // Add the new MODs 374 | m.mods = append(m.mods, mods...) 375 | // Complete the MODClass by storing its index 376 | m.index = append(m.index, len(m.mods)) 377 | 378 | return 379 | } 380 | 381 | // NewModTag returns a modTag string from the given tag elements 382 | func NewModTag(tags []string) (modTag string) { 383 | modTag = strings.Join(tags, ",") 384 | return 385 | } 386 | 387 | // NewTag returns a MOD tag prefixed with the MODClass tag 388 | func (mc *MODClass) NewTag(tag string) string { 389 | return fmt.Sprintf("%s%s", mc.Tag, tag) 390 | } 391 | 392 | // Check if any duplicate elements exists 393 | func hasDuplicateString(check []string) (duplicate string, found bool) { 394 | have := map[string]bool{} 395 | for _, ele := range check { 396 | if have[ele] { 397 | found = true 398 | duplicate = ele 399 | return 400 | } 401 | 402 | have[ele] = true 403 | } 404 | 405 | return 406 | } 407 | 408 | // Check tag prefixes ensuring that only one tag from the MODClass is used within the tags 409 | func hasMultipleClassTags(tags []string, prefix string) bool { 410 | have := 0 411 | for _, tag := range tags { 412 | if strings.HasPrefix(tag, prefix) { 413 | have++ 414 | } 415 | 416 | if have > 1 { 417 | return true 418 | } 419 | } 420 | 421 | return false 422 | } 423 | 424 | // ModTagsAreValid parses a modTag string formatted as "tag,tag,tag" returning its tags if all tags are valid 425 | // and there is no conflicting tags. If an empty modTag is passed it will return no error and nil tags 426 | func (m *MODs) TagsAreValid(modTag string) (tags []string, err error) { 427 | checkTags := strings.Split(modTag, ",") 428 | if checkTags[0] == "" { 429 | return 430 | } 431 | 432 | duplicate, found := hasDuplicateString(checkTags) 433 | if found { 434 | err = fmt.Errorf("found duplicate tag %q", duplicate) 435 | return 436 | } 437 | 438 | var modExists bool 439 | for _, tag := range checkTags { 440 | modExists = false 441 | for _, m := range m.mods { 442 | if m.Tag == tag { 443 | modExists = true 444 | break 445 | } 446 | } 447 | 448 | if !modExists { 449 | err = fmt.Errorf("tag %q does not exist", tag) 450 | return 451 | } 452 | } 453 | 454 | // Go through all MODClasses ensuring that the tags abide by MODClassRules 455 | for _, c := range m.classes { 456 | for _, r := range c.Rules { 457 | err = r.Verify(checkTags, c) 458 | if err != nil { 459 | return 460 | } 461 | } 462 | } 463 | 464 | tags = checkTags 465 | 466 | return 467 | } 468 | 469 | // injectMOD injects a TELA-MOD's functions into the code of a smart contract and returns the new smart contract and new code 470 | func (m *MODs) injectMOD(mod, code string) (modSC dvm.SmartContract, modCode string, err error) { 471 | modSC, _, err = dvm.ParseSmartContract(code) 472 | if err != nil { 473 | err = fmt.Errorf("could not parse MOD base code: %s", err) 474 | return 475 | } 476 | 477 | // Find the MOD code to be injected 478 | modCodeSet, functionNames := m.Functions(mod) 479 | if modCodeSet == "" { 480 | err = fmt.Errorf("could not find MOD %q code: %s", mod, err) 481 | return 482 | } 483 | 484 | sc, _, err := dvm.ParseSmartContract(modCodeSet) 485 | if err != nil { 486 | err = fmt.Errorf("could not parse MOD %q code: %s", mod, err) 487 | return 488 | } 489 | 490 | // Inject the new MOD functions into the smart contract 491 | for _, name := range functionNames { 492 | modSC.Functions[name] = sc.Functions[name] 493 | } 494 | 495 | modCode = fmt.Sprintf("%s\n%s", code, modCodeSet) 496 | 497 | return 498 | } 499 | 500 | // InjectMODs parses the modTag ensuring all tags are valid before injecting the TELA-MOD code for the given MODs in the modTag 501 | func (m *MODs) InjectMODs(modTag, code string) (modSC dvm.SmartContract, modCode string, err error) { 502 | tags, err := m.TagsAreValid(modTag) 503 | if err != nil { 504 | err = fmt.Errorf("modTag has invalid MODs: %s", err) 505 | return 506 | } 507 | 508 | modCode = code // tags could be nil otherwise start injecting mods 509 | for _, mod := range tags { 510 | _, modCode, err = m.injectMOD(mod, modCode) 511 | if err != nil { 512 | return 513 | } 514 | } 515 | 516 | return 517 | } 518 | -------------------------------------------------------------------------------- /shards/shards_test.go: -------------------------------------------------------------------------------- 1 | package shards 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/deroproject/derohe/cryptography/crypto" 12 | "github.com/deroproject/derohe/walletapi" 13 | "github.com/deroproject/graviton" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestShards(t *testing.T) { 18 | mainPath, err := os.Getwd() 19 | if err != nil { 20 | t.Fatalf("Error: %s", err) 21 | } 22 | 23 | endpoint := "127.0.0.1:20000" 24 | walletName := "tela-store" 25 | testPath := filepath.Join(mainPath, "tela_tests") 26 | datashards := filepath.Join(testPath, "datashards") 27 | walletPath := filepath.Join(testPath, "tela_sim_wallets") 28 | walletSeeds := []string{ 29 | "2af6630905d73ee40864bd48339f297908a0731a6c4c6fa0a27ea574ac4e4733", 30 | "193faf64d79e9feca5fce8b992b4bb59b86c50f491e2dc475522764ca6666b6b", 31 | } 32 | 33 | t.Cleanup(func() { 34 | os.RemoveAll(testPath) 35 | }) 36 | 37 | os.RemoveAll(testPath) 38 | 39 | // Create simulator wallets to use for encrypt/decrypt tests 40 | var wallets []*walletapi.Wallet_Disk 41 | for i, seed := range walletSeeds { 42 | w, err := createTestWallet(fmt.Sprintf("%s%d", walletName, i), walletPath, seed) 43 | if err != nil { 44 | t.Fatalf("Failed to create wallet %d: %s", i, err) 45 | } 46 | 47 | wallets = append(wallets, w) 48 | assert.NotNil(t, wallets[i], "Wallet %s should not be nil", i) 49 | } 50 | 51 | t.Run("Store", func(t *testing.T) { 52 | // Test SetPath 53 | testPath := "likely/A/invalid/Path" 54 | _, err = SetPath(testPath) 55 | assert.Error(t, err, "Invalid path should not be set") 56 | defaultPath := filepath.Join(mainPath, "datashards") 57 | assert.Equal(t, defaultPath, GetPath(), "Datashard path should have remained as default") 58 | _, err = SetPath(datashards) // set test path 59 | assert.NoError(t, err, "Valid path should not error: %s", err) 60 | 61 | // Values to store 62 | tree := "user" 63 | key := []byte("other_network") 64 | value := []byte(endpoint + endpoint) 65 | 66 | for _, dbType := range dbTypes { 67 | err := SetDBType(dbType) 68 | assert.NoError(t, err, "Setting db type should not error: %s", err) 69 | 70 | t.Run("StoreValue", func(t *testing.T) { 71 | err := StoreValue(tree, key, value) 72 | assert.NoError(t, err, "Storing value should not error: %s", err) 73 | }) 74 | 75 | t.Run("GetValue", func(t *testing.T) { 76 | result, err := GetValue(tree, key) 77 | assert.NoError(t, err, "Getting stored value should not error: %s", err) 78 | assert.Equal(t, value, result, "Stored endpoints should be equal") 79 | }) 80 | 81 | t.Run("SettingsValue", func(t *testing.T) { 82 | err := StoreSettingsValue(key, value) 83 | assert.NoError(t, err, "Storing value should not error: %s", err) 84 | result, err := GetSettingsValue(key) 85 | assert.NoError(t, err, "Getting stored value should not error: %s", err) 86 | assert.Equal(t, value, result, "Stored endpoints should be equal") 87 | }) 88 | 89 | t.Run("Endpoint", func(t *testing.T) { 90 | endpoint := "endpoint" 91 | err := StoreEndpoint(endpoint) 92 | assert.NoError(t, err, "Storing endpoint value should not error: %s", err) 93 | result, err := GetEndpoint() 94 | assert.NoError(t, err, "Getting endpoint should not error: %s", err) 95 | assert.Equal(t, endpoint, result, "Stored endpoints should be equal") 96 | }) 97 | 98 | t.Run("Network", func(t *testing.T) { 99 | network := "Testnet" 100 | err := StoreNetwork(network) 101 | assert.NoError(t, err, "Storing network value should not error: %s", err) 102 | result, err := GetNetwork() 103 | assert.NoError(t, err, "Getting network should not error: %s", err) 104 | assert.Equal(t, network, result, "Stored networks should be equal") 105 | }) 106 | 107 | t.Run("StoreEncryptedValue", func(t *testing.T) { 108 | err := StoreEncryptedValue(wallets[0], tree, key, value) 109 | assert.NoError(t, err, "Storing encrypted value should not error: %s", err) 110 | }) 111 | 112 | t.Run("GetEncryptedValue", func(t *testing.T) { 113 | result, err := GetEncryptedValue(wallets[0], tree, key) 114 | assert.NoError(t, err, "Getting encrypted store value should not error: %s", err) 115 | assert.Equal(t, value, result, "Stored decrypted values should be equal") 116 | }) 117 | 118 | t.Run("DeleteKey", func(t *testing.T) { 119 | err = DeleteKey(wallets[0], tree, key) 120 | assert.NoError(t, err, "Deleting valid key should not error: %s", err) 121 | }) 122 | 123 | t.Run("DeleteSettingsKey", func(t *testing.T) { 124 | err = DeleteSettingsKey(key) 125 | assert.NoError(t, err, "Deleting valid settings key should not error: %s", err) 126 | }) 127 | } 128 | 129 | for _, db := range dbTypes { 130 | assert.True(t, IsValidDBType(db), "%s should be a valid DB type", db) 131 | } 132 | 133 | // Test invalid DB types 134 | invalidDB := "invalidDB" 135 | assert.False(t, IsValidDBType(invalidDB), "%s should not be a valid DB type", invalidDB) 136 | 137 | err = SetDBType(invalidDB) 138 | assert.Error(t, err, "Setting invalid db type should error") 139 | assert.Equal(t, "boltdb", GetDBType(), "DB type should be boltdb: %s", GetDBType()) 140 | // SetDBType should not allow this to occur 141 | shards.dbType = "unlikely" 142 | err := StoreValue(tree, key, value) 143 | assert.Error(t, err, "Storing value with invalid DB type should error") 144 | _, err = GetValue(tree, key) 145 | assert.Error(t, err, "Getting value with invalid DB type should error") 146 | err = StoreSettingsValue(key, value) 147 | assert.Error(t, err, "Storing settings value with invalid DB type should error") 148 | _, err = GetSettingsValue(key) 149 | assert.Error(t, err, "Getting settings value with invalid DB type should error") 150 | err = StoreEndpoint("endpoint") 151 | assert.Error(t, err, "Storing endpoint with invalid DB type should error") 152 | _, err = GetEndpoint() 153 | assert.Error(t, err, "Getting endpoint with invalid DB type should error") 154 | err = StoreNetwork("Mainnet") 155 | assert.Error(t, err, "Storing valid network with invalid DB type should error") 156 | err = StoreNetwork("invalid") 157 | assert.Error(t, err, "Storing invalid network with invalid DB type should error") 158 | _, err = GetNetwork() 159 | assert.Error(t, err, "Getting network with invalid DB type should error") 160 | err = StoreEncryptedValue(wallets[0], tree, key, value) 161 | assert.Error(t, err, "Storing encrypted value with invalid DB type should error") 162 | _, err = GetEncryptedValue(wallets[0], tree, key) 163 | assert.Error(t, err, "Getting encrypted value with invalid DB type should error") 164 | err = DeleteKey(wallets[0], tree, key) 165 | assert.Error(t, err, "Deleting key value with invalid DB type should error") 166 | err = DeleteSettingsKey(key) 167 | assert.Error(t, err, "Deleting settings key value with invalid DB type should error") 168 | }) 169 | 170 | // // Test bbolt functions 171 | t.Run("Bolt", func(t *testing.T) { 172 | // Values to store 173 | bucket := "settings" 174 | key := []byte("network") 175 | value := []byte(endpoint) 176 | 177 | // Test boltStoreValue 178 | t.Run("boltStoreValue", func(t *testing.T) { 179 | err := boltStoreValue(bucket, key, value) 180 | assert.NoError(t, err, "Storing value should not error: %s", err) 181 | // boltStoreValue errors 182 | err = boltStoreValue("", key, value) 183 | assert.Error(t, err, "Storing empty bucket should error") 184 | err = boltStoreValue(bucket, nil, value) 185 | assert.Error(t, err, "Storing invalid key should error") 186 | }) 187 | 188 | // Test boltGetValue 189 | t.Run("boltGetValue", func(t *testing.T) { 190 | result, err := boltGetValue(bucket, key) 191 | assert.NoError(t, err, "Getting stored value should not error: %s", err) 192 | assert.Equal(t, value, result, "Stored endpoints should be equal") 193 | // boltGetValue errors 194 | _, err = boltGetValue("", key) 195 | assert.Error(t, err, "Getting empty bucket should error") 196 | _, err = boltGetValue(bucket, nil) 197 | assert.Error(t, err, "Getting empty value should error") 198 | }) 199 | 200 | // Test boltStoreEncryptedValue 201 | t.Run("boltStoreEncryptedValue", func(t *testing.T) { 202 | err := boltStoreEncryptedValue(wallets[0], bucket, key, value) 203 | assert.NoError(t, err, "Storing encrypted value should not error: %s", err) 204 | // boltStoreEncryptedValue errors 205 | err = boltStoreEncryptedValue(wallets[0], "", key, value) 206 | assert.Error(t, err, "Storing encrypted value with empty bucket should error") 207 | err = boltStoreEncryptedValue(wallets[0], bucket, nil, value) 208 | assert.Error(t, err, "Storing encrypted value with nil key should error") 209 | }) 210 | 211 | // Test boltGetEncryptedValue 212 | t.Run("boltGetEncryptedValue", func(t *testing.T) { 213 | result, err := boltGetEncryptedValue(wallets[0], bucket, key) 214 | assert.NoError(t, err, "Getting encrypted store value should not error: %s", err) 215 | assert.Equal(t, value, result, "Stored decrypted values should be equal") 216 | // boltGetEncryptedValue errors 217 | _, err = boltGetEncryptedValue(wallets[0], "", key) 218 | assert.Error(t, err, "Getting encrypted value with empty bucket should error") 219 | _, err = boltGetEncryptedValue(wallets[0], bucket, nil) 220 | assert.Error(t, err, "Getting encrypted value with nil key should error") 221 | }) 222 | 223 | // Test boltDeleteKey 224 | t.Run("boltDeleteKey", func(t *testing.T) { 225 | err = boltDeleteKey(nil, bucket, key) 226 | assert.NoError(t, err, "Deleting settings with valid key should not error: %s", err) 227 | err = boltDeleteKey(nil, "", key) 228 | assert.Error(t, err, "Deleting settings with empty bucket error") 229 | // boltDeleteKey errors 230 | err = boltDeleteKey(wallets[0], "", key) 231 | assert.Error(t, err, "Deleting shard with empty bucket should error") 232 | err = boltDeleteKey(wallets[0], bucket, key) 233 | assert.NoError(t, err, "Deleting shard with valid key should not error: %s", err) 234 | err = boltDeleteKey(wallets[1], bucket, key) 235 | assert.Error(t, err, "Deleting non existent bucket should error") 236 | }) 237 | }) 238 | 239 | // // Test Graviton functions 240 | t.Run("Graviton", func(t *testing.T) { 241 | // Values to store 242 | tree := "settings" 243 | key := []byte("network") 244 | value := []byte(endpoint) 245 | 246 | // For errors 247 | treeToBig := strings.Repeat("t", graviton.TREE_NAME_LIMIT+1) 248 | valueToBIg := []byte(strings.Repeat("t", graviton.MAX_VALUE_SIZE+1)) 249 | 250 | // Test gravitonStoreValue 251 | t.Run("gravitonStoreValue", func(t *testing.T) { 252 | err := gravitonStoreValue(tree, key, value) 253 | assert.NoError(t, err, "Storing settings value should not error: %s", err) 254 | // gravitonStoreValue errors 255 | err = gravitonStoreValue("", key, value) 256 | assert.Error(t, err, "Storing empty tree should error") 257 | err = gravitonStoreValue(tree, nil, nil) 258 | assert.Error(t, err, "Storing nil key should error") 259 | err = gravitonStoreValue(tree, key, nil) 260 | assert.Error(t, err, "Storing nil value should error") 261 | err = gravitonStoreValue(treeToBig, key, value) 262 | assert.Error(t, err, "Storing value with tree exceeding %d should error", graviton.TREE_NAME_LIMIT) 263 | err = gravitonStoreValue(tree, key, valueToBIg) 264 | assert.Error(t, err, "Storing value with value exceeding %d should error", graviton.MAX_VALUE_SIZE) 265 | }) 266 | 267 | // Test gravitonGetValue 268 | t.Run("gravitonGetValue", func(t *testing.T) { 269 | result, err := gravitonGetValue(tree, key) 270 | assert.NoError(t, err, "Getting settings value should not error: %s", err) 271 | assert.Equal(t, value, result, "Stored endpoints should be equal") 272 | // gravitonGetValue errors 273 | _, err = gravitonGetValue("", key) 274 | assert.Error(t, err, "Getting empty tree should error") 275 | _, err = gravitonGetValue(tree, nil) 276 | assert.Error(t, err, "Getting nil key should error") 277 | _, err = gravitonGetValue(treeToBig, key) 278 | assert.Error(t, err, "Getting value with tree to big should error") 279 | _, err = gravitonGetValue(tree, []byte("not here")) 280 | assert.Error(t, err, "Getting value non existent key should error") 281 | }) 282 | 283 | // Test gravitonStoreEncryptedValue 284 | t.Run("gravitonStoreEncryptedValue", func(t *testing.T) { 285 | err := gravitonStoreEncryptedValue(wallets[0], tree, key, value) 286 | assert.NoError(t, err, "Storing encrypted value should not error: %s", err) 287 | // gravitonStoreEncryptedValue errors 288 | err = gravitonStoreEncryptedValue(wallets[0], "", key, value) 289 | assert.Error(t, err, "Storing encrypted value with empty tree should error") 290 | err = gravitonStoreEncryptedValue(wallets[0], tree, nil, nil) 291 | assert.Error(t, err, "Storing encrypted value with nil key should error") 292 | err = gravitonStoreEncryptedValue(wallets[0], tree, key, nil) 293 | assert.Error(t, err, "Storing encrypted value with nil value should error") 294 | err = gravitonStoreEncryptedValue(nil, tree, key, nil) 295 | assert.Error(t, err, "Storing encrypted value should error with nil wallet") 296 | err = gravitonStoreEncryptedValue(wallets[0], treeToBig, key, value) 297 | assert.Error(t, err, "Storing encrypted value with tree exceeding %d should error", graviton.TREE_NAME_LIMIT) 298 | err = gravitonStoreEncryptedValue(wallets[0], tree, key, valueToBIg) 299 | assert.Error(t, err, "Storing encrypted value with value exceeding %d should error", graviton.MAX_VALUE_SIZE) 300 | }) 301 | 302 | // Test gravitonGetEncryptedValue 303 | t.Run("gravitonGetEncryptedValue", func(t *testing.T) { 304 | result, err := gravitonGetEncryptedValue(wallets[0], tree, key) 305 | assert.NoError(t, err, "Getting encrypted store value should not error: %s", err) 306 | assert.Equal(t, value, result, "Stored decrypted values should be equal") 307 | // gravitonGetEncryptedValue errors 308 | _, err = gravitonGetEncryptedValue(wallets[0], "", key) 309 | assert.Error(t, err, "Getting empty tree should error") 310 | _, err = gravitonGetEncryptedValue(wallets[0], treeToBig, key) 311 | assert.Error(t, err, "Getting encrypted value with tree to big should error") 312 | _, err = gravitonGetEncryptedValue(wallets[0], tree, nil) 313 | assert.Error(t, err, "Getting nil key should error") 314 | _, err = gravitonGetEncryptedValue(nil, tree, key) 315 | assert.Error(t, err, "Getting encrypted value should error with nil wallet") 316 | _, err = gravitonGetEncryptedValue(wallets[1], tree, key) 317 | assert.Error(t, err, "Encrypted value should not exists for this wallet") 318 | }) 319 | 320 | // Test gravitonDeleteKey 321 | t.Run("gravitonDeleteKey", func(t *testing.T) { 322 | err := gravitonDeleteKey(wallets[0], tree, key) 323 | assert.NoError(t, err, "Deleting valid key should not error: %s", err) 324 | // gravitonDeleteKey errors 325 | err = gravitonDeleteKey(wallets[0], "", key) 326 | assert.Error(t, err, "Deleting with empty tree should error") 327 | err = gravitonDeleteKey(wallets[0], tree, nil) 328 | assert.Error(t, err, "Deleting with nil key should error") 329 | err = gravitonDeleteKey(nil, tree, nil) 330 | assert.Error(t, err, "Deleting with nil wallet should error") 331 | err = gravitonDeleteKey(wallets[0], treeToBig, key) 332 | assert.Error(t, err, "Deleting with with value exceeding %d should error", graviton.TREE_NAME_LIMIT) 333 | }) 334 | }) 335 | 336 | t.Run("Keys", func(t *testing.T) { 337 | assert.Equal(t, []byte("endpoint"), Key.Endpoint(), "Endpoint keys should be equal") 338 | assert.Equal(t, []byte("network"), Key.Network(), "Network keys should be equal") 339 | }) 340 | 341 | t.Run("Values", func(t *testing.T) { 342 | assert.Equal(t, "Mainnet", Value.Network.Mainnet(), "Mainnet values should be equal") 343 | assert.Equal(t, "Testnet", Value.Network.Testnet(), "Testnet values should be equal") 344 | assert.Equal(t, "Simulator", Value.Network.Simulator(), "Simulator values should be equal") 345 | }) 346 | } 347 | 348 | // Create test wallet 349 | func createTestWallet(name, dir, seed string) (wallet *walletapi.Wallet_Disk, err error) { 350 | seed_raw, err := hex.DecodeString(seed) 351 | if err != nil { 352 | return 353 | } 354 | 355 | err = os.MkdirAll(dir, os.ModePerm) 356 | if err != nil { 357 | return 358 | } 359 | 360 | filename := filepath.Join(dir, name) 361 | 362 | os.Remove(filename) 363 | 364 | wallet, err = walletapi.Create_Encrypted_Wallet(filename, "", new(crypto.BNRed).SetBytes(seed_raw)) 365 | if err != nil { 366 | return 367 | } 368 | 369 | wallet.SetNetwork(false) 370 | wallet.Save_Wallet() 371 | 372 | return 373 | } 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/civilware/tela/blob/main/tela.png?raw=true) 2 | 3 | ## TELA: Decentralized Web Standard 4 | 5 | ### Table of Contents 6 | - [What Is TELA?](#what-is-tela) 7 | - [How It Works](#how-it-works) 8 | - [Additional Features](#additional-features) 9 | - [Get Started](#get-started) 10 | - [TELA-INDEX-1](TELA-INDEX-1/README.md) 11 | - [TELA-DOC-1](TELA-DOC-1/README.md) 12 | - [TELA-MOD-1](TELA-MOD-1/README.md) 13 | - [Accessing TELA Content](#accessing-tela-content) 14 | - [Compliant Host Applications](#compliant-host-applications) 15 | - [Content Rating System](#content-rating-system) 16 | - [Package Use](#package-use) 17 | - [Serving](#serving) 18 | - [Installing](#installing) 19 | - [Updating](#updating) 20 | - [Variables](#variables) 21 | - [Rating](#rating) 22 | - [Parse](#parse) 23 | - [TELA-CLI](cmd/tela-cli/README.md) 24 | - [Changelog](CHANGELOG.md) 25 | - [License](LICENSE) 26 | 27 | ### What Is TELA? 28 | TELA enables the secure and decentralized storage of application files on [DERO's](https://dero.io) blockchain using smart contracts. This innovative standard ensures the integrity and authenticity of stored files, allowing them to be retrieved from the blockchain and executed locally in a browser. By doing so, TELA enhances user privacy and control over browser-based applications, eliminating the reliance on third-party servers. 29 | 30 | ### How It Works 31 | TELA applications are built on two key smart contract components: 32 | 33 | `TELA-INDEX-1` Contract: This serves as the entrypoint for TELA applications. Users can simply input the SCID (Smart Contract ID) of any deployed `TELA-INDEX-1` contract to retrieve the necessary files to run the application locally. This process can be demonstrated using tools like the `civilware/tela` [go package](https://pkg.go.dev/github.com/Civilware/tela) or similar methods to parse a `TELA-INDEX-1` according to this standard, creating a host app for all TELA content. 34 | 35 | `TELA-DOC-1` Contract: This contains the essential code required by the application. TELA supports common programming languages such as, but not limited to: 36 | 37 | - HTML 38 | - JSON 39 | - JavaScript 40 | - CSS 41 | - Markdown 42 | 43 | Multiple `TELA-DOC-1` contracts can be installed and embedded within a `TELA-INDEX-1` application, allowing the use of extensive codebases beyond DERO’s [DVM-BASIC](https://docs.dero.io/developer/dvm.html) smart contract language. These contracts can also install necessary libraries and tools on the blockchain, facilitating modular development through reusable code. 44 | 45 | ### Additional Features 46 | - File Management: TELA ensures application code remains in an unalterable state using a combination of mutable (`TELA-INDEX-1`) and immutable (`TELA-DOC-1`) contracts. This structure provides a commit-based system that allows for application updates, code verification, and retrieval of previous contract states. 47 | 48 | - Connectivity: TELA supports DERO's XSWD protocol, enabling permissioned web socket interactions with DERO wallets, enhancing connectivity and user interaction. 49 | 50 | - Contract Identification: TELA contracts utilize the header structure from the [ART-NFA Standard](https://github.com/civilware/artificer-nfa-standard/blob/main/Headers/README.md), making them easily identifiable and ensuring consistent integration within the DERO ecosystem. 51 | 52 | By leveraging these components and features, TELA represents a significant advancement in secure, decentralized web applications, fostering an environment where privacy, security, and user autonomy are paramount. 53 | 54 | ### Get Started 55 | See the following for more information on how to get started creating and installing your TELA application: 56 | 57 | * [TELA-INDEX-1](TELA-INDEX-1/README.md) 58 | 59 | Entrypoint and manifest for TELA applications 60 | 61 | * [TELA-DOC-1](TELA-DOC-1/README.md) 62 | 63 | TELA application files and libraries 64 | 65 | * [TELA-MOD-1](TELA-MOD-1/README.md) 66 | 67 | Additional TELA building blocks which extend the standard's features. 68 | 69 | ### Accessing TELA Content 70 | The minimum requirements to access TELA content are: 71 | * Connection to a DERO node 72 | * Any host application that can serve TELA content 73 | 74 | The `civilware/tela` go package aims to be a simple entrypoint for hosting and managing TELA content and can be used as the foundation for creating compliant host applications. 75 | 76 | #### Compliant Host Applications 77 | * [Engram](https://github.com/DEROFDN/Engram/) 78 | * [TELA-CLI](cmd/tela-cli/README.md) 79 | 80 | ### Content Rating System 81 | TELA smart contracts have a rating system integrated into each contract to help developers and users navigate content. Ratings can be interpreted for a quick judgment, or can be used to gather further details about content. A guide to the content rating system is as follows: 82 | - One rating per DERO account, per contract. 83 | - A rating is a positive number < 100. 84 | - Ringsize of 2 is required for a Rate transaction. 85 | - The smart contract stores the rating number and the address of the rater. 86 | - The smart contract also adds to a likes or dislikes value store based on the given rating, dislike is a rating < 50. 87 | - Standard gas fees apply when making a Rate transaction. 88 | 89 | A quick tally of the like and dislike values or gathering the average rating number can give a brief overview of how the content has been received by others. For a more detailed review of content, TELA has provided a structure for the 0-99 rating numbers, attaching them to a wider range of subjective comments while still supporting the basic "higher number is better" mentality for overall ease of use. The `civilware/tela` package can generate rating strings using the following interpretation: 90 | 91 | Numbers are broken down into place values. 92 | - Ex, 24 = first place 2 and second place 4. 93 | - Ex, 8 = first place 0 and second place 8. 94 | 95 | Each place is given different values. The first place represents the rating category. 96 | 97 | Rating Categories 98 | 99 | | First Place | Category | 100 | |-------------|--------------------| 101 | | 0 | Do not use | 102 | | 1 | Broken | 103 | | 2 | Major issues | 104 | | 3 | Minor issues | 105 | | 4 | Should be improved | 106 | | 5 | Could be improved | 107 | | 6 | Average | 108 | | 7 | Good | 109 | | 8 | Very good | 110 | | 9 | Exceptional | 111 | 112 | The second place represents a detail tag. Positive and negative rating categories each have their own set of detail tags, sharing some common ones between them. 113 | 114 | Detail Tags 115 | | Second Place | Negative Detail Tags | Positive Detail Tags | 116 | |--------------|----------------------|----------------------| 117 | | 0 | Nothing | Nothing | 118 | | 1 | Needs review | Needs review | 119 | | 2 | Needs improvement | Needs improvement | 120 | | 3 | Bugs | Bugs | 121 | | 4 | Errors | Errors | 122 | | 5 | Inappropriate | Visually appealing | 123 | | 6 | Incomplete | In depth | 124 | | 7 | Corrupted | Works well | 125 | | 8 | Plagiarized | Unique | 126 | | 9 | Malicious | Benevolent | 127 | 128 | This format would produce the following strings given some example rating numbers: 129 | 130 | | Rating | String | 131 | |--------------|-----------------------------| 132 | | 80 | Very good | 133 | | 77 | Good, Works well | 134 | | 43 | Should be improved, Bugs | 135 | | 7 | Do not use, Corrupted | 136 | 137 | ### Package Use 138 | The main usage of the `civilware/tela` go package is to query a deployed `TELA-INDEX-1` SCID from a connected node and serve the content on a URL such as `localhost:8081/tela/` 139 | 140 | #### Serving 141 | ```go 142 | import ( 143 | "github.com/civilware/tela" 144 | ) 145 | 146 | func main() { 147 | endpoint := "127.0.0.1:20000" 148 | scid := "a842dac04587000b019a7aeee55d7e3e5df40f959b0bd36a474cda67936e9399" 149 | url, err := tela.ServeTELA(scid, endpoint) 150 | if err != nil { 151 | // Handle error 152 | } 153 | // Code to open url in local browser 154 | // .. 155 | // Shutdown all TELA servers when done 156 | tela.ShutdownTELA() 157 | } 158 | ``` 159 | 160 | #### Installing 161 | TELA content can be installed in a manner of ways. The `civilware/tela` package takes the necessary data for the type of smart contract and creates the applicable transfer arguments and code to easily install the new contract. For manual installation see [here](TELA-DOC-1/README.md#install-tela-doc-1). 162 | ```go 163 | import ( 164 | "fmt" 165 | 166 | "github.com/civilware/tela" 167 | "github.com/deroproject/derohe/rpc" 168 | "github.com/deroproject/derohe/walletapi" 169 | ) 170 | 171 | func main() { 172 | // Create DOC (or INDEX) with all relevant data 173 | doc := &tela.DOC{ 174 | DocType: tela.DOC_HTML, 175 | Code: "", 176 | SubDir: "", 177 | DURL: "app.tela", 178 | Headers: tela.Headers{ 179 | NameHdr: "index.html", 180 | DescrHdr: "HTML index file", 181 | IconHdr: "ICON_URL", 182 | }, 183 | Signature: tela.Signature{ 184 | CheckC: "c4d7bbdaaf9344f4c351e72d0b2145b4235402c89510101e0500f43969fd1387", 185 | CheckS: "b879b0ff01d78841d61e9770fd18436d8b9afce59302c77a786272e7422c15f6", 186 | }, 187 | } 188 | 189 | // Pass doc to NewInstallArgs() to create smart contract and transfer arguments 190 | args, err := tela.NewInstallArgs(doc) 191 | if err != nil { 192 | // Handle error 193 | } 194 | fmt.Printf("SC Code: %s\n", args.Value(rpc.SCCODE, rpc.DataString)) 195 | // Code to make transfer call with WS/RPC and install args 196 | 197 | // // // 198 | // // 199 | // Alternatively, Installer() takes a DOC or INDEX and installs it with the given walletapi 200 | ringsize := uint64(2) 201 | // ringsize 2 allows installed INDEX contracts to be updated (public) 202 | // ringsize > 2 will make the installed INDEX contract immutable (anonymous) 203 | txid, err := tela.Installer(&walletapi.Wallet_Disk{}, ringsize, doc) 204 | if err != nil { 205 | // Handle error 206 | } 207 | fmt.Printf("Installed TELA SCID: %s\n", txid) 208 | } 209 | ``` 210 | 211 | #### Updating 212 | Updating `TELA-INDEX-1`'s can be managed similarly to new installs. The values provided for updating the smart contract will be embedded into its new code making them available when the code is parsed post update, while the original variable stores for those values will remain unchanged preserving the contract's origin. The TXID generated by each update execution is stored in the smart contract, allowing for reference to the code changes that have taken place. For manual update procedures see [here](TELA-INDEX-1/README.md#update-tela-index-1). 213 | ```go 214 | import ( 215 | "fmt" 216 | 217 | "github.com/civilware/tela" 218 | "github.com/deroproject/derohe/walletapi" 219 | ) 220 | 221 | func main() { 222 | // Create the INDEX to be updated, including all its relevant data 223 | scid := "8dd839608e584f75b64c0ca7ff2c274879677ac3aaf60159c78797ee518946c2" 224 | 225 | index := &tela.INDEX{ 226 | SCID: scid, 227 | DURL: "app.tela", 228 | DOCs: []string{"", ""}, 229 | Headers: tela.Headers{ 230 | NameHdr: "TELA App", 231 | DescrHdr: "A TELA Application", 232 | IconHdr: "ICON_URL", 233 | }, 234 | } 235 | 236 | // Pass index to NewUpdateArgs() to create transfer arguments for update call 237 | args, err := tela.NewUpdateArgs(index) 238 | if err != nil { 239 | // Handle error 240 | } 241 | // Code to make transfer call with WS/RPC and update args 242 | 243 | // // // 244 | // // 245 | // Alternatively, Updater() takes an INDEX and updates it with the given walletapi 246 | txid, err := tela.Updater(&walletapi.Wallet_Disk{}, index) 247 | if err != nil { 248 | // Handle error 249 | } 250 | fmt.Printf("Update TXID: %s %s\n", txid, err) 251 | 252 | // // // 253 | // // 254 | // GetINDEXInfo() can be used to preform INDEX data from an existing SCID 255 | endpoint := "127.0.0.1:20000" 256 | liveIndex, _ := tela.GetINDEXInfo(scid, endpoint) 257 | liveIndex.DOCs = []string{"", ""} 258 | args, _ = tela.NewUpdateArgs(&liveIndex) 259 | } 260 | ``` 261 | 262 | #### Variables 263 | Publicly deployed `TELA-INDEX-1` contracts that have enabled `TELA-MOD-1`'s can store arbitrary data. DERO wallets can set or delete key-value pairs. When executing a variable store operation it does not affect the commit state of the `TELA-INDEX-1`. The various `TELA-MOD-1`'s offer developers a wide range of versatility for making dynamic TELA content by allowing the variables stores to be view only, give them overwrite permissions or make them immutable. 264 | ```go 265 | import ( 266 | "github.com/civilware/tela" 267 | "github.com/deroproject/derohe/walletapi" 268 | ) 269 | 270 | func main() { 271 | scid := "e2a7384d2a1a6a99d9899d0db10891e5c3466823eadb039c18ec92fbcf2f8b89" 272 | key := "key" 273 | value := "value" 274 | 275 | txid, err := tela.SetVar(&walletapi.Wallet_Disk{}, scid, key, value) 276 | if err != nil { 277 | // Handle error 278 | } 279 | // Variable is stored in SC, the SC stores keys with a "var_" prefix 280 | 281 | txid, err = tela.DeleteVar(&walletapi.Wallet_Disk{}, scid, key) 282 | if err != nil { 283 | // Handle error 284 | } 285 | // Key is deleted 286 | 287 | // Check if a TELA key exists on a SC 288 | endpoint := "127.0.0.1:20000" 289 | _, exists, _ := tela.KeyExists(scid, fmt.Sprintf("var_%s", key), endpoint) 290 | if exists { 291 | // Found key 292 | } 293 | } 294 | ``` 295 | 296 | #### Rating 297 | Rating TELA content is a straightforward process, a DERO wallet submits a Rate transaction to a TELA SCID which gets stored in the smart contract's public ratings. The `civilware/tela` package's extended [content rating system](#content-rating-system) components are exported to make integrating the same TELA interpretations an easy task. For manual rating procedures see [here](TELA-INDEX-1/README.md#rate-tela-index-1). 298 | ```go 299 | import ( 300 | "fmt" 301 | 302 | "github.com/civilware/tela" 303 | "github.com/deroproject/derohe/walletapi" 304 | ) 305 | 306 | func main() { 307 | // Define rating and scid 308 | rating := uint64(0) 309 | scid := "c4d7bbdaaf9344f4c351e72d0b2145b4235402c89510101e0500f43969fd1387" 310 | 311 | // Pass params to NewRateArgs() to create transfer arguments for Rate call 312 | args, err := tela.NewRateArgs(scid, rating) 313 | if err != nil { 314 | // Handle error 315 | } 316 | // Code to make transfer call with WS/RPC and rate args 317 | 318 | // // // 319 | // // 320 | // Alternatively, Rate() takes a TELA scid and rating and rates the contract with the given walletapi 321 | txid, err := tela.Rate(&walletapi.Wallet_Disk{}, scid, rating) 322 | if err != nil { 323 | // Handle error 324 | } 325 | fmt.Printf("Rate TXID: %s\n", txid) 326 | 327 | // // // 328 | // // 329 | // GetRating() gets the rating results from a scid 330 | endpoint := "127.0.0.1:20000" 331 | height := uint64(0) // Results can be filtered by height showing ratings that occurred >= height 332 | result, err := tela.GetRating(scid, endpoint, height) 333 | if err != nil { 334 | // Handle error 335 | } 336 | fmt.Printf("Likes: %d, Dislikes: %d Average: %d\n", result.Likes, result.Dislikes, result.Average) 337 | 338 | // // // 339 | // // 340 | // The package's rating structures can be accessed using the Rating variable 341 | category, detail, _ := tela.Ratings.Parse(rating) 342 | 343 | // Get a rating string formatted as "Category (detail)" 344 | ratingString, _ := tela.Ratings.ParseString(rating) 345 | 346 | // Get the category of a rating 347 | category = tela.Ratings.Category(rating) 348 | 349 | // Get the detail tag from a rating 350 | detail = tela.Ratings.Detail(rating, false) 351 | 352 | // Get all TELA rating categories 353 | categories := tela.Ratings.Categories() 354 | 355 | // Get all TELA negative details 356 | negativeDetails := tela.Ratings.NegativeDetails() 357 | 358 | // Get all TELA positive details 359 | positiveDetails := tela.Ratings.PositiveDetails() 360 | } 361 | ``` 362 | 363 | #### Parse 364 | The `civilware/tela` package has exported much of the functionality used in the necessary components to create TELA content stored on the DERO blockchain. The parsing and header tools can be of great use to developers working with any DVM smart contract. 365 | ```go 366 | package main 367 | 368 | import ( 369 | "github.com/civilware/tela" 370 | ) 371 | 372 | func main() { 373 | // Parse a file name for its respective TELA docType 374 | fileName := "index.html" 375 | docType := tela.ParseDocType(fileName) 376 | 377 | // Parse a TELA-INDEX-1 for any embedded DOCs 378 | scCode := tela.TELA_INDEX_1 379 | docSCID, _ := tela.ParseINDEXForDOCs(scCode) 380 | 381 | // Parse a DERO signature for its address, C and S values 382 | signature := []byte("-----BEGIN DERO SIGNED MESSAGE-----") 383 | address, c, s, _ := tela.ParseSignature(signature) 384 | 385 | // Get all function names from DERO smart contract code 386 | functionNames := tela.GetSmartContractFuncNames(scCode) 387 | 388 | // Compare equality between two DERO smart contracts 389 | dvmCode, _ := tela.EqualSmartContracts(scCode, scCode) 390 | 391 | // Format DERO smart contract code removing whitespace and comments 392 | formattedCode, _ := tela.FormatSmartContract(dvmCode, scCode) 393 | 394 | // Create DocShard files from a source file 395 | tela.CreateShardFiles(fileName, "", nil) 396 | 397 | // Recreate a file in path from its DocShards 398 | path := "dir1/" 399 | docShards := [][]byte{} 400 | tela.ConstructFromShards(docShards, fileName, path, "") 401 | 402 | // Parse and inject headers into a DERO smart contract 403 | headers1 := &tela.Headers{ 404 | NameHdr: "myNameHdr", 405 | DescrHdr: "myDescrHdr", 406 | IconHdr: "myIconURL", 407 | } 408 | formattedCode, _ = tela.ParseHeaders(scCode, headers1) 409 | 410 | // ParseHeaders takes various input formats for a wide range of use 411 | headers2 := &tela.INDEX{ 412 | DURL: "", 413 | DOCs: []string{""}, 414 | Headers: *headers1, 415 | } 416 | formattedCode, _ = tela.ParseHeaders(scCode, headers2) 417 | 418 | // Including any custom headers 419 | headers3 := map[tela.Header]interface{}{ 420 | tela.HEADER_NAME: "myNameHdr", 421 | tela.HEADER_ICON_URL: "myIconURL", 422 | tela.Header(`"customHdr"`): "myCustomHdr", 423 | } 424 | formattedCode, _ = tela.ParseHeaders(scCode, headers3) 425 | 426 | headers4 := map[string]interface{}{ 427 | `"nameHdr"`: "myNameHdr", 428 | `"iconURLHdr"`: "myIconURL", 429 | `"customHdr"`: "myCustomHdr", 430 | } 431 | formattedCode, _ = tela.ParseHeaders(scCode, headers4) 432 | } 433 | ``` 434 | ### TELA-CLI 435 | * [TELA-CLI](cmd/tela-cli/README.md) 436 | 437 | ### Changelog 438 | * [Changelog](CHANGELOG.md) 439 | 440 | ### License 441 | * [License](LICENSE) 442 | --------------------------------------------------------------------------------