├── .gitignore ├── LICENSE ├── README.md ├── mintcoin ├── Makefile ├── README.md ├── cmd │ └── mintcoin │ │ ├── commands │ │ ├── init.go │ │ └── mint.go │ │ └── main.go ├── glide.lock ├── glide.yaml ├── plugins.go ├── plugins_test.go ├── types.go └── types_test.go ├── paytovote ├── Makefile ├── README.md ├── cmd │ └── paytovote │ │ ├── commands │ │ ├── init.go │ │ └── paytovote.go │ │ └── main.go ├── glide.lock ├── glide.yaml ├── paytovote.go └── paytovote_test.go └── trader ├── Makefile ├── README.md ├── accountant.go ├── cmd └── trader │ ├── commands │ ├── escrow.go │ ├── init.go │ └── options.go │ └── main.go ├── glide.lock ├── glide.yaml ├── plugins ├── escrow │ ├── plugin.go │ ├── tx.go │ └── tx_test.go └── options │ ├── plugin.go │ ├── tx.go │ └── tx_test.go ├── prefix_store.go └── types ├── escrow.go ├── escrow_test.go ├── options.go └── options_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | merkleeyes.db 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basecoin Examples 2 | 3 | **DEPRECATED:** See [Cosmos Academy](https://github.com/cosmos/cosmos-academy) 4 | 5 | This repository contains example code and new ideas for cryptocurrency design with basecoin. 6 | Each subdirectory is thought out as a stand-alone application, and all code is under Apache 2.0 license. 7 | You can use them as 8 | It's easy to plug these modules into your own cryptocurrency design - see our [docs on Basecoin](https://github.com/tendermint/basecoin/tree/develop) for lots more details. 9 | 10 | **NOTE**: all this code is currently based on a non-master branch of basecoin to make use of the newest functionality, so make sure to update dependencies properly here, using `make get_vendor_deps` **prior** to running any code. It is important to copy the glide files to any clone as well. 11 | 12 | -------------------------------------------------------------------------------- /mintcoin/Makefile: -------------------------------------------------------------------------------- 1 | all: get_vendor_deps test install 2 | 3 | test: 4 | go test . 5 | 6 | install: 7 | go install ./cmd/... 8 | 9 | get_vendor_deps: 10 | go get github.com/Masterminds/glide 11 | glide install 12 | -------------------------------------------------------------------------------- /mintcoin/README.md: -------------------------------------------------------------------------------- 1 | # Mintcoin - minting your own crypto-cash 2 | 3 | Mintcoin is a Basecoin plugin that allows more coins to be created in the network 4 | by registering some accounts as "Central Bankers" that can issue more money. 5 | 6 | For more details about Basecoin, the tools, and the plugin architecture, [see the docs](https://github.com/tendermint/basecoin). 7 | 8 | ## Install 9 | 10 | Run `make all` in this directory. 11 | This will update all dependencies, run the test suite, and install the `mintcoin` binary to your `$GOPATH/bin`. 12 | 13 | ## Setting Initial State 14 | 15 | The state is initialized using a `genesis.json` containing a list of issuers. 16 | These are the accounts that can issue new coins. 17 | An example can be found in `data/genesis.json`. 18 | 19 | `mintcoin` uses the `SetOption` plugin method to enable new issuers to be added or removed with the `add` and `remove` keys, respectively. The value must be the hex-encoded address of the issuer to add or remove. 20 | 21 | Once an address is added, the private key that belongs to that address can sign MintTx transactions 22 | that create money. 23 | 24 | ## Minting Money 25 | 26 | The `mintcoin` plugin expects the `Data` in the `AppTx` to contain a serialized `MintTx`: 27 | 28 | ``` 29 | type MintTx struct { 30 | Credits Credits 31 | } 32 | 33 | type Credits []Credit 34 | 35 | type Credit struct { 36 | Addr []byte 37 | Amount types.Coins 38 | } 39 | ``` 40 | 41 | If the sender of the `AppTx` is a registered issuer, 42 | the corresponding amounts in the embedded `MintTx` will be credited to the listed accounts. 43 | 44 | ## Testing with a CLI 45 | 46 | Alright, now let's set ourselves up as issuers and send some shiny new bills to our friends! 47 | 48 | First we do the usual reset routine: 49 | 50 | ``` 51 | mintcoin init 52 | mintcoin unsafe_reset_all 53 | ``` 54 | 55 | Now we can start Basecoin with the mintcoin plugin and the default genesis: 56 | 57 | ``` 58 | mintcoin start 59 | ``` 60 | 61 | In another window, we can run the client tool: 62 | 63 | ``` 64 | mintcoin account 0x1B1BE55F969F54064628A63B9559E7C21C925165 65 | ``` 66 | 67 | This was the account registered in the genesis; it has the right number of coins. 68 | 69 | Let's mint some new coins: 70 | 71 | ``` 72 | mintcoin tx mint --chain_id mint_chain_id --amount 1mycoin --mintto 0x1B1BE55F969F54064628A63B9559E7C21C925165 --mint 1000BTC 73 | mintcoin tx mint --chain_id mint_chain_id --amount 1mycoin --mintto 0x1B1BE55F969F54064628A63B9559E7C21C925165 --mint 5cosmo 74 | mintcoin tx mint --chain_id mint_chain_id --amount 1mycoin --mintto 0x1B1BE55F969F54064628A63B9559E7C21C925165 --mint 5000FOOD 75 | ``` 76 | 77 | Here, we're sending `1000 BTC`, `5 cosmo`, and `5000 FOOD` to the account with address `0x1B1BE55F969F54064628A63B9559E7C21C925165`. 78 | Note that we have to provide some non-zero `--amount` for the transaction, and we have to specify the `--chain_id`, 79 | which must match the `chain_id` in the `genesis.json`. 80 | 81 | Let's take another look at the account: 82 | 83 | ``` 84 | mintcoin account 0x1B1BE55F969F54064628A63B9559E7C21C925165 85 | ``` 86 | 87 | It's got all the coins! 88 | 89 | Alright, let's issue some coins to our friend: 90 | 91 | ``` 92 | mintcoin tx mint --chain_id mint_chain_id --amount 1mycoin --mintto 0x1DA7C74F9C219229FD54CC9F7386D5A3839F0090 --mint 1234BTC 93 | mintcoin account 0x1DA7C74F9C219229FD54CC9F7386D5A3839F0090 94 | ``` 95 | 96 | Now they can send us some coins for our labour: 97 | 98 | ``` 99 | mintcoin tx send --chain_id mint_chain_id --from key2.json --to 0x1B1BE55F969F54064628A63B9559E7C21C925165 --amount 333BTC 100 | mintcoin account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 101 | mintcoin account 0x1B1BE55F969F54064628A63B9559E7C21C925165 102 | ``` 103 | 104 | If we try to issue coins from the wrong account, we'll get an error: 105 | 106 | ``` 107 | mintcoin tx mint --from key2.json --chain_id mint_chain_id --amount 1BTC --mintto 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 --mint 1234BTC 108 | ``` 109 | 110 | ## Attaching a GUI 111 | 112 | Coming soon! For now, see [the repository](https://github.com/tendermint/js-basecoin) 113 | -------------------------------------------------------------------------------- /mintcoin/cmd/mintcoin/commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | bcmd "github.com/tendermint/basecoin/cmd/commands" 5 | ) 6 | 7 | func init() { 8 | //Change the GenesisJSON 9 | bcmd.GenesisJSON = `{ 10 | "app_hash": "", 11 | "chain_id": "mint_chain_id", 12 | "genesis_time": "0001-01-01T00:00:00.000Z", 13 | "validators": [ 14 | { 15 | "amount": 10, 16 | "name": "", 17 | "pub_key": { 18 | "type": "ed25519", 19 | "data": "7B90EA87E7DC0C7145C8C48C08992BE271C7234134343E8A8E8008E617DE7B30" 20 | } 21 | } 22 | ], 23 | "app_options": { 24 | "accounts": [{ 25 | "pub_key": { 26 | "type": "ed25519", 27 | "data": "619D3678599971ED29C7529DDD4DA537B97129893598A17C82E3AC9A8BA95279" 28 | }, 29 | "coins": [ 30 | { 31 | "denom": "mycoin", 32 | "amount": 9007199254740992 33 | } 34 | ] 35 | }], 36 | "plugin_options": ["mint/add", "1B1BE55F969F54064628A63B9559E7C21C925165"] 37 | } 38 | }` 39 | 40 | } 41 | -------------------------------------------------------------------------------- /mintcoin/cmd/mintcoin/commands/mint.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/tendermint/basecoin-examples/mintcoin" 11 | bcmd "github.com/tendermint/basecoin/cmd/commands" 12 | "github.com/tendermint/basecoin/types" 13 | wire "github.com/tendermint/go-wire" 14 | ) 15 | 16 | const MintName = "mint" 17 | 18 | var ( 19 | //flags 20 | MintToFlag string 21 | MintAmountFlag string 22 | 23 | //Commands 24 | MintTxCmd = &cobra.Command{ 25 | Use: "mint", 26 | Short: "Craft a transaction to mint some more currency", 27 | RunE: mintTxCmd, 28 | } 29 | ) 30 | 31 | func init() { 32 | 33 | //register flags 34 | flags := []bcmd.Flag2Register{ 35 | {&MintToFlag, "mintto", "", "Where to send the newly minted coins"}, 36 | {&MintAmountFlag, "mint", "", "Amount of coins to mint in format ,,..."}, 37 | } 38 | bcmd.RegisterFlags(MintTxCmd, flags) 39 | 40 | bcmd.RegisterTxSubcommand(MintTxCmd) 41 | bcmd.RegisterStartPlugin(MintName, func() types.Plugin { return mintcoin.New(MintName) }) 42 | } 43 | 44 | func mintTxCmd(cmd *cobra.Command, args []string) error { 45 | 46 | // convert destination address to bytes 47 | to, err := hex.DecodeString(bcmd.StripHex(MintToFlag)) 48 | if err != nil { 49 | return errors.Errorf("To address is invalid hex: %v\n", err) 50 | } 51 | 52 | amountCoins, err := types.ParseCoins(MintAmountFlag) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | mintTx := mintcoin.MintTx{ 58 | Credits: []mintcoin.Credit{ 59 | { 60 | Addr: to, 61 | Amount: amountCoins, 62 | }, 63 | }, 64 | } 65 | fmt.Println("MintTx:", string(wire.JSONBytes(mintTx))) 66 | data := wire.BinaryBytes(mintTx) 67 | 68 | return bcmd.AppTx(MintName, data) 69 | } 70 | -------------------------------------------------------------------------------- /mintcoin/cmd/mintcoin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/tendermint/basecoin/cmd/commands" 9 | "github.com/tendermint/tmlibs/cli" 10 | 11 | // import _ to register the mint plugin to apptx 12 | _ "github.com/tendermint/basecoin-examples/mintcoin/cmd/mintcoin/commands" 13 | ) 14 | 15 | func main() { 16 | 17 | var RootCmd = &cobra.Command{ 18 | Use: "mintcoin", 19 | } 20 | 21 | RootCmd.AddCommand( 22 | commands.InitCmd, 23 | commands.StartCmd, 24 | commands.TxCmd, 25 | commands.QueryCmd, 26 | commands.KeyCmd, 27 | commands.VerifyCmd, 28 | commands.BlockCmd, 29 | commands.AccountCmd, 30 | commands.UnsafeResetAllCmd, 31 | commands.QuickVersionCmd("0.2.0"), 32 | ) 33 | 34 | cmd := cli.PrepareMainCmd(RootCmd, "MT", os.ExpandEnv("$HOME/.mintcoin")) 35 | cmd.Execute() 36 | } 37 | -------------------------------------------------------------------------------- /mintcoin/glide.lock: -------------------------------------------------------------------------------- 1 | hash: 997e4cc3339141ee01aa2adf656425a49ebf117e6ca9e81ba72b8f94fee3e86e 2 | updated: 2017-05-17T12:25:00.580569867+02:00 3 | imports: 4 | - name: github.com/bgentry/speakeasy 5 | version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd 6 | - name: github.com/btcsuite/btcd 7 | version: 1ae306021e323ae11c71ffb8546fbd9019e6cb6f 8 | subpackages: 9 | - btcec 10 | - name: github.com/BurntSushi/toml 11 | version: b26d9c308763d68093482582cea63d69be07a0f0 12 | - name: github.com/ebuchman/fail-test 13 | version: 95f809107225be108efcf10a3509e4ea6ceef3c4 14 | - name: github.com/fsnotify/fsnotify 15 | version: 4da3e2cfbabc9f751898f250b49f2439785783a1 16 | - name: github.com/go-kit/kit 17 | version: d67bb4c202e3b91377d1079b110a6c9ce23ab2f8 18 | subpackages: 19 | - log 20 | - log/level 21 | - log/term 22 | - name: github.com/go-logfmt/logfmt 23 | version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 24 | - name: github.com/go-playground/locales 25 | version: 1e5f1161c6416a5ff48840eb8724a394e48cc534 26 | subpackages: 27 | - currency 28 | - name: github.com/go-playground/universal-translator 29 | version: 71201497bace774495daed26a3874fd339e0b538 30 | - name: github.com/go-stack/stack 31 | version: 7a2f19628aabfe68f0766b59e74d6315f8347d22 32 | - name: github.com/golang/protobuf 33 | version: b50ceb1fa9818fa4d78b016c2d4ae025593a7ce3 34 | subpackages: 35 | - proto 36 | - ptypes/any 37 | - name: github.com/golang/snappy 38 | version: 553a641470496b2327abcac10b36396bd98e45c9 39 | - name: github.com/gorilla/context 40 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 41 | - name: github.com/gorilla/handlers 42 | version: 3a5767ca75ece5f7f1440b1d16975247f8d8b221 43 | - name: github.com/gorilla/mux 44 | version: 392c28fe23e1c45ddba891b0320b3b5df220beea 45 | - name: github.com/gorilla/websocket 46 | version: a91eba7f97777409bc2c443f5534d41dd20c5720 47 | - name: github.com/hashicorp/hcl 48 | version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca 49 | subpackages: 50 | - hcl/ast 51 | - hcl/parser 52 | - hcl/scanner 53 | - hcl/strconv 54 | - hcl/token 55 | - json/parser 56 | - json/scanner 57 | - json/token 58 | - name: github.com/inconshreveable/mousetrap 59 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 60 | - name: github.com/jmhodges/levigo 61 | version: c42d9e0ca023e2198120196f842701bb4c55d7b9 62 | - name: github.com/kr/logfmt 63 | version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 64 | - name: github.com/magiconair/properties 65 | version: 51463bfca2576e06c62a8504b5c0f06d61312647 66 | - name: github.com/mitchellh/mapstructure 67 | version: cc8532a8e9a55ea36402aa21efdf403a60d34096 68 | - name: github.com/pelletier/go-buffruneio 69 | version: c37440a7cf42ac63b919c752ca73a85067e05992 70 | - name: github.com/pelletier/go-toml 71 | version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a 72 | - name: github.com/pkg/errors 73 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 74 | - name: github.com/spf13/afero 75 | version: 9be650865eab0c12963d8753212f4f9c66cdcf12 76 | subpackages: 77 | - mem 78 | - name: github.com/spf13/cast 79 | version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 80 | - name: github.com/spf13/cobra 81 | version: 3454e0e28e69c1b8effa6b5123c8e4185e20d696 82 | - name: github.com/spf13/jwalterweatherman 83 | version: 8f07c835e5cc1450c082fe3a439cf87b0cbb2d99 84 | - name: github.com/spf13/pflag 85 | version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 86 | - name: github.com/spf13/viper 87 | version: 0967fc9aceab2ce9da34061253ac10fb99bba5b2 88 | - name: github.com/syndtr/goleveldb 89 | version: 8c81ea47d4c41a385645e133e15510fc6a2a74b4 90 | subpackages: 91 | - leveldb 92 | - leveldb/cache 93 | - leveldb/comparer 94 | - leveldb/errors 95 | - leveldb/filter 96 | - leveldb/iterator 97 | - leveldb/journal 98 | - leveldb/memdb 99 | - leveldb/opt 100 | - leveldb/storage 101 | - leveldb/table 102 | - leveldb/util 103 | - name: github.com/tendermint/abci 104 | version: 5dabeffb35c027d7087a12149685daa68989168b 105 | subpackages: 106 | - client 107 | - example/dummy 108 | - server 109 | - types 110 | - name: github.com/tendermint/basecoin 111 | version: bd62b21d6e2f7faa6dfeed62d5ac4b9bb3553031 112 | - name: github.com/tendermint/ed25519 113 | version: 1f52c6f8b8a5c7908aff4497c186af344b428925 114 | subpackages: 115 | - edwards25519 116 | - extra25519 117 | - name: github.com/tendermint/go-crypto 118 | version: 438b16f1f84ef002d7408ecd6fc3a3974cbc9559 119 | subpackages: 120 | - cmd 121 | - keys 122 | - keys/cryptostore 123 | - keys/server 124 | - keys/server/types 125 | - keys/storage/filestorage 126 | - name: github.com/tendermint/go-wire 127 | version: 97beaedf0f4dbc035309157c92be3b30cc6e5d74 128 | subpackages: 129 | - data 130 | - data/base58 131 | - name: github.com/tendermint/light-client 132 | version: 478876ca34b360df62f941d5e20cdd608fa0a466 133 | subpackages: 134 | - certifiers 135 | - certifiers/client 136 | - certifiers/files 137 | - commands 138 | - commands/proofs 139 | - commands/proxy 140 | - commands/seeds 141 | - commands/txs 142 | - proofs 143 | - name: github.com/tendermint/merkleeyes 144 | version: c722818b460381bc5b82e38c73ff6e22a9df624d 145 | subpackages: 146 | - app 147 | - client 148 | - iavl 149 | - name: github.com/tendermint/tendermint 150 | version: 11b5d11e9eec170e1d3dce165f0270d5c0759d69 151 | subpackages: 152 | - blockchain 153 | - config 154 | - consensus 155 | - mempool 156 | - node 157 | - p2p 158 | - p2p/upnp 159 | - proxy 160 | - rpc/client 161 | - rpc/core 162 | - rpc/core/types 163 | - rpc/grpc 164 | - rpc/lib 165 | - rpc/lib/client 166 | - rpc/lib/server 167 | - rpc/lib/types 168 | - state 169 | - state/txindex 170 | - state/txindex/kv 171 | - state/txindex/null 172 | - types 173 | - version 174 | - name: github.com/tendermint/tmlibs 175 | version: 8af1c70a8be17543eb33e9bfbbcdd8371e3201cc 176 | subpackages: 177 | - autofile 178 | - cli 179 | - clist 180 | - common 181 | - db 182 | - events 183 | - flowrate 184 | - log 185 | - logger 186 | - merkle 187 | - name: golang.org/x/crypto 188 | version: ab89591268e0c8b748cbe4047b00197516011af5 189 | subpackages: 190 | - curve25519 191 | - nacl/box 192 | - nacl/secretbox 193 | - openpgp/armor 194 | - openpgp/errors 195 | - poly1305 196 | - ripemd160 197 | - salsa20/salsa 198 | - name: golang.org/x/net 199 | version: c9b681d35165f1995d6f3034e61f8761d4b90c99 200 | subpackages: 201 | - context 202 | - http2 203 | - http2/hpack 204 | - idna 205 | - internal/timeseries 206 | - lex/httplex 207 | - trace 208 | - name: golang.org/x/sys 209 | version: 9c9d83fe39ed3fd2d9249fcf6b755891fff54b03 210 | subpackages: 211 | - unix 212 | - name: golang.org/x/text 213 | version: 470f45bf29f4147d6fbd7dfd0a02a848e49f5bf4 214 | subpackages: 215 | - secure/bidirule 216 | - transform 217 | - unicode/bidi 218 | - unicode/norm 219 | - name: google.golang.org/genproto 220 | version: 411e09b969b1170a9f0c467558eb4c4c110d9c77 221 | subpackages: 222 | - googleapis/rpc/status 223 | - name: google.golang.org/grpc 224 | version: a0c3e72252b6fbf4826bb143e450eb05588a9d6d 225 | subpackages: 226 | - codes 227 | - credentials 228 | - grpclb/grpc_lb_v1 229 | - grpclog 230 | - internal 231 | - keepalive 232 | - metadata 233 | - naming 234 | - peer 235 | - stats 236 | - status 237 | - tap 238 | - transport 239 | - name: gopkg.in/go-playground/validator.v9 240 | version: 6d8c18553ea1ac493d049edd6f102f52e618f085 241 | - name: gopkg.in/yaml.v2 242 | version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 243 | testImports: 244 | - name: github.com/davecgh/go-spew 245 | version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 246 | subpackages: 247 | - spew 248 | - name: github.com/pmezard/go-difflib 249 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 250 | subpackages: 251 | - difflib 252 | - name: github.com/stretchr/testify 253 | version: 4d4bfba8f1d1027c4fdbe371823030df51419987 254 | subpackages: 255 | - assert 256 | - require 257 | -------------------------------------------------------------------------------- /mintcoin/glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/tendermint/basecoin-examples/mintcoin 2 | import: 3 | - package: github.com/gorilla/websocket 4 | - package: github.com/pkg/errors 5 | - package: github.com/spf13/cobra 6 | - package: github.com/spf13/pflag 7 | - package: github.com/spf13/viper 8 | - package: github.com/tendermint/abci 9 | version: develop 10 | subpackages: 11 | - server 12 | - types 13 | - package: github.com/tendermint/basecoin 14 | version: develop 15 | - package: github.com/tendermint/go-crypto 16 | version: develop 17 | subpackages: 18 | - cmd 19 | - keys 20 | - package: github.com/tendermint/go-wire 21 | version: develop 22 | subpackages: 23 | - data 24 | - package: github.com/tendermint/light-client 25 | version: develop 26 | subpackages: 27 | - commands 28 | - commands/proofs 29 | - commands/seeds 30 | - commands/txs 31 | - proofs 32 | - package: github.com/tendermint/merkleeyes 33 | version: develop 34 | subpackages: 35 | - client 36 | - iavl 37 | - package: github.com/tendermint/tendermint 38 | version: develop 39 | subpackages: 40 | - config 41 | - node 42 | - proxy 43 | - rpc/client 44 | - rpc/core/types 45 | - rpc/lib/client 46 | - rpc/lib/types 47 | - types 48 | - package: github.com/tendermint/tmlibs 49 | version: develop 50 | subpackages: 51 | - cli 52 | - common 53 | - events 54 | - log 55 | - logger 56 | testImport: 57 | - package: github.com/stretchr/testify 58 | subpackages: 59 | - assert 60 | - require 61 | -------------------------------------------------------------------------------- /mintcoin/plugins.go: -------------------------------------------------------------------------------- 1 | package mintcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | abci "github.com/tendermint/abci/types" 8 | "github.com/tendermint/basecoin/state" 9 | "github.com/tendermint/basecoin/types" 10 | wire "github.com/tendermint/go-wire" 11 | ) 12 | 13 | const ( 14 | AddIssuer = "add" 15 | RemoveIssuer = "remove" 16 | ) 17 | 18 | // MintPlugin is a plugin, storing all state prefixed with it's unique name 19 | type MintPlugin struct { 20 | name string 21 | } 22 | 23 | func New(name string) MintPlugin { 24 | return MintPlugin{name: name} 25 | } 26 | 27 | func (mp MintPlugin) Name() string { 28 | return mp.name 29 | } 30 | 31 | // Set initial minters 32 | func (mp MintPlugin) SetOption(store types.KVStore, key string, value string) (log string) { 33 | 34 | // value is always a hex-encoded address 35 | addr, err := hex.DecodeString(value) 36 | if err != nil { 37 | return fmt.Sprintf("Invalid address: %s: %v", addr, err) 38 | } 39 | 40 | switch key { 41 | case AddIssuer: 42 | s := mp.loadState(store) 43 | s.AddIssuer(addr) 44 | mp.saveState(store, s) 45 | mp.saveState(store, s) 46 | return fmt.Sprintf("Added: %s", addr) 47 | case RemoveIssuer: 48 | s := mp.loadState(store) 49 | s.RemoveIssuer(addr) 50 | mp.saveState(store, s) 51 | return fmt.Sprintf("Removed: %s", addr) 52 | default: 53 | return fmt.Sprintf("Unknown key: %s", key) 54 | } 55 | } 56 | 57 | // This allows 58 | func (mp MintPlugin) RunTx(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) { 59 | // parse transaction 60 | var tx MintTx 61 | err := wire.ReadBinaryBytes(txBytes, &tx) 62 | if err != nil { 63 | return abci.ErrEncodingError 64 | } 65 | 66 | // make sure it was signed by an Issuer 67 | s := mp.loadState(store) 68 | if !s.IsIssuer(ctx.CallerAddress) { 69 | return abci.ErrUnauthorized 70 | } 71 | 72 | // now, send all this money! 73 | for _, credit := range tx.Credits { 74 | // load or create account 75 | acct := state.GetAccount(store, credit.Addr) 76 | if acct == nil { 77 | acct = &types.Account{ 78 | Sequence: 0, 79 | } 80 | } 81 | 82 | // add the money 83 | acct.Balance = acct.Balance.Plus(credit.Amount) 84 | 85 | // and save the new balance 86 | state.SetAccount(store, credit.Addr, acct) 87 | } 88 | 89 | return abci.Result{} 90 | } 91 | 92 | // placeholders empty to fulfill interface 93 | func (mp MintPlugin) InitChain(store types.KVStore, vals []*abci.Validator) {} 94 | func (mp MintPlugin) BeginBlock(store types.KVStore, hash []byte, header *abci.Header) {} 95 | func (mp MintPlugin) EndBlock(store types.KVStore, height uint64) abci.ResponseEndBlock { 96 | return abci.ResponseEndBlock{} 97 | } 98 | 99 | func (mp MintPlugin) assertPlugin() types.Plugin { 100 | return mp 101 | } 102 | 103 | /*** implementation ***/ 104 | 105 | func (mp MintPlugin) stateKey() []byte { 106 | key := fmt.Sprintf("*%s*", mp.name) 107 | return []byte(key) 108 | } 109 | 110 | func (mp MintPlugin) loadState(store types.KVStore) *MintState { 111 | var s MintState 112 | data := store.Get(mp.stateKey()) 113 | // here return an uninitialized state 114 | if len(data) == 0 { 115 | return &s 116 | } 117 | 118 | err := wire.ReadBinaryBytes(data, &s) 119 | // this should never happen, but we should also never panic.... 120 | if err != nil { 121 | panic(err) 122 | } 123 | return &s 124 | } 125 | 126 | func (mp MintPlugin) saveState(store types.KVStore, state *MintState) { 127 | value := wire.BinaryBytes(*state) 128 | store.Set(mp.stateKey(), value) 129 | } 130 | -------------------------------------------------------------------------------- /mintcoin/plugins_test.go: -------------------------------------------------------------------------------- 1 | package mintcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/tendermint/basecoin/state" 10 | "github.com/tendermint/basecoin/types" 11 | wire "github.com/tendermint/go-wire" 12 | ) 13 | 14 | func TestSaveLoad(t *testing.T) { 15 | assert := assert.New(t) 16 | store := types.NewMemKVStore() 17 | plugin := New("cash") 18 | addr1, addr2 := []byte("bigmoney"), []byte("litlefish") 19 | 20 | s := plugin.loadState(store) 21 | assert.NotNil(s) 22 | assert.False(s.IsIssuer(addr1)) 23 | s.AddIssuer(addr1) 24 | plugin.saveState(store, s) 25 | 26 | s2 := plugin.loadState(store) 27 | assert.NotNil(s2) 28 | assert.True(s2.IsIssuer(addr1)) 29 | assert.False(s2.IsIssuer(addr2)) 30 | } 31 | 32 | func TestSetOptions(t *testing.T) { 33 | assert := assert.New(t) 34 | store := types.NewMemKVStore() 35 | plugin := New("cash") 36 | 37 | addr1, addr2 := []byte("bigmoney"), []byte("litlefish") 38 | hex1 := hex.EncodeToString(addr1) 39 | hex2 := hex.EncodeToString(addr2) 40 | assert.Equal("cash", plugin.Name()) 41 | 42 | plugin.SetOption(store, AddIssuer, hex1) 43 | st := plugin.loadState(store) 44 | assert.True(st.IsIssuer(addr1)) 45 | assert.False(st.IsIssuer(addr2)) 46 | 47 | plugin.SetOption(store, RemoveIssuer, hex2) 48 | st = plugin.loadState(store) 49 | assert.True(st.IsIssuer(addr1)) 50 | assert.False(st.IsIssuer(addr2)) 51 | 52 | plugin.SetOption(store, AddIssuer, hex2) 53 | plugin.SetOption(store, RemoveIssuer, hex1) 54 | st = plugin.loadState(store) 55 | assert.False(st.IsIssuer(addr1)) 56 | assert.True(st.IsIssuer(addr2)) 57 | } 58 | 59 | func TestTransactions(t *testing.T) { 60 | assert := assert.New(t) 61 | store := types.NewMemKVStore() 62 | plugin := New("cash") 63 | 64 | addr1, addr2 := []byte("bigmoney"), []byte("litlefish") 65 | assert.Nil(state.GetAccount(store, addr1)) 66 | 67 | tx := MintTx{ 68 | Credits{ 69 | { 70 | Addr: addr1, 71 | Amount: types.Coins{ 72 | {Denom: "BTC", Amount: 5}, 73 | {Denom: "EUR", Amount: 100}, 74 | }, 75 | }, 76 | { 77 | Addr: addr2, 78 | Amount: types.Coins{ 79 | {Denom: "USD", Amount: 75}, 80 | }, 81 | }, 82 | }, 83 | } 84 | txBytes := wire.BinaryBytes(tx) 85 | ctx := types.CallContext{CallerAddress: addr1} 86 | res := plugin.RunTx(store, ctx, txBytes) 87 | 88 | // this won't work, cuz bigmoney isn't a Issuer yet 89 | assert.True(res.IsErr()) 90 | assert.Nil(state.GetAccount(store, addr1)) 91 | 92 | // let's set the options and watch the cash flow! 93 | hex1 := hex.EncodeToString(addr1) 94 | plugin.SetOption(store, AddIssuer, hex1) 95 | res = plugin.RunTx(store, ctx, txBytes) 96 | assert.True(res.IsOK()) 97 | acct1 := state.GetAccount(store, addr1) 98 | assert.NotNil(acct1) 99 | assert.True(acct1.Balance.IsPositive()) 100 | assert.Equal(2, len(acct1.Balance)) 101 | btc := acct1.Balance[0] 102 | assert.Equal("BTC", btc.Denom) 103 | assert.Equal(int64(5), btc.Amount) 104 | 105 | acct2 := state.GetAccount(store, addr2) 106 | assert.NotNil(acct2) 107 | assert.True(acct2.Balance.IsPositive()) 108 | assert.Equal(1, len(acct2.Balance)) 109 | usd := acct2.Balance[0] 110 | assert.Equal("USD", usd.Denom) 111 | assert.Equal(int64(75), usd.Amount) 112 | } 113 | -------------------------------------------------------------------------------- /mintcoin/types.go: -------------------------------------------------------------------------------- 1 | package mintcoin 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/tendermint/basecoin/types" 7 | wire "github.com/tendermint/go-wire" 8 | ) 9 | 10 | type MintState struct { 11 | Issuers Issuers 12 | } 13 | 14 | type Issuer []byte 15 | 16 | type Issuers []Issuer 17 | 18 | func (s *MintState) AddIssuer(addr []byte) { 19 | if !s.IsIssuer(addr) { 20 | s.Issuers = append(s.Issuers, addr) 21 | } 22 | } 23 | 24 | func (s *MintState) RemoveIssuer(addr []byte) { 25 | b := s.Issuers 26 | for i := range b { 27 | if bytes.Equal(addr, b[i]) { 28 | s.Issuers = append(b[:i], b[i+1:]...) 29 | return 30 | } 31 | } 32 | } 33 | 34 | func (s *MintState) IsIssuer(addr []byte) bool { 35 | for _, b := range s.Issuers { 36 | if bytes.Equal(b, addr) { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | type MintTx struct { 44 | Credits Credits 45 | } 46 | 47 | type Credits []Credit 48 | 49 | type Credit struct { 50 | Addr []byte 51 | Amount types.Coins 52 | } 53 | 54 | func (tx MintTx) Serialize() []byte { 55 | return wire.BinaryBytes(tx) 56 | } 57 | -------------------------------------------------------------------------------- /mintcoin/types_test.go: -------------------------------------------------------------------------------- 1 | package mintcoin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestState(t *testing.T) { 10 | assert := assert.New(t) 11 | addr1 := []byte("foobar") 12 | addr2 := []byte("biggie") 13 | 14 | s := MintState{} 15 | assert.False(s.IsIssuer(addr1)) 16 | assert.False(s.IsIssuer(addr2)) 17 | 18 | s.AddIssuer(addr1) 19 | assert.True(s.IsIssuer(addr1)) 20 | assert.False(s.IsIssuer(addr2)) 21 | 22 | s.AddIssuer(addr2) 23 | assert.True(s.IsIssuer(addr1)) 24 | assert.True(s.IsIssuer(addr2)) 25 | 26 | // make sure multiple adds don't lead to multiple entries 27 | s.AddIssuer(addr1) 28 | s.AddIssuer(addr1) 29 | s.RemoveIssuer(addr1) 30 | assert.False(s.IsIssuer(addr1)) 31 | assert.True(s.IsIssuer(addr2)) 32 | } 33 | -------------------------------------------------------------------------------- /paytovote/Makefile: -------------------------------------------------------------------------------- 1 | all: get_vendor_deps test install 2 | 3 | test: 4 | go test . 5 | 6 | install: 7 | go install ./cmd/... 8 | 9 | get_vendor_deps: 10 | go get github.com/Masterminds/glide 11 | glide install 12 | -------------------------------------------------------------------------------- /paytovote/README.md: -------------------------------------------------------------------------------- 1 | # PayToVote Plugin 2 | 3 | ### Description 4 | Paytovote is a basic application which demonstrates how to create an instance 5 | of the basecoin system which utilizes a custom paytovote plugin. The premise of 6 | this plugin is to allow users to pay a fee to create or vote for user-specified 7 | issues. Unique fees are applied when voting or creating a new issue. Fees may 8 | use coin types (for example "voteToken" or "issueToken"). Currently, the 9 | fee to cast a vote is decided by the user when the issue is being generated, 10 | and the fee to create a new issue is defined globally within the plugin CLI 11 | commands (cmd/commands) 12 | 13 | 14 | ### Install 15 | Run `make all` in this directory. This will update all dependencies, run the 16 | test suite, and install the `paytovote` binary to your `$GOPATH/bin`. 17 | 18 | ### General Usage 19 | - create issues with `paytovote tx paytovote create-issue` 20 | - mandatory flags 21 | - --from string Path to a private key to sign the transaction (default "key.json") 22 | - --amount string Coins to send in transaction of the format ,,... (eg: 1btc,2gold,5silver}, 23 | - --issue string name of the issue to generate or vote for (default "default issue") 24 | - --voteFee string the fees required to vote on this new issue, uses the format ,,... (eg: 1gold,2silver,5btc) (default "1voteToken") 25 | - optional flags 26 | - --node string Tendermint RPC address (default "tcp://localhost:46657") 27 | - --chain_id string ID of the chain for replay protection (default "test_chain_id") 28 | - --coin value Specify a coin denomination (default: "blank") 29 | - --gas int The amount of gas for the transaction 30 | - --fee string Coins for the transaction fee of the format 31 | - --sequence int Sequence number for the account (-1 to autocalculate}, (default -1) 32 | - vote for issues with `paytovote tx paytovote vote` and flags listed below 33 | - mandatory flags 34 | - --from string Path to a private key to sign the transaction (default "key.json") 35 | - --amount string Coins to send in transaction of the format ,,... (eg: 1btc,2gold,5silver}, 36 | - --issue string name of the issue to generate or vote for (default "default issue") 37 | - --voteFor if present vote will be a vote-for, if absent a vote-against 38 | - optional flags 39 | - --node string Tendermint RPC address (default "tcp://localhost:46657") 40 | - --chain_id string ID of the chain for replay protection (default "test_chain_id") 41 | - --coin value Specify a coin denomination (default: "blank") 42 | - --gas int The amount of gas for the transaction 43 | - --fee string Coins for the transaction fee of the format 44 | - --sequence int Sequence number for the account (-1 to autocalculate}, (default -1) 45 | - query the state of an issue using the command `paytovote query p2vIssue [yourissuename]` 46 | 47 | ### Example CLI Usage 48 | First perform the initialization commands: 49 | 50 | ``` 51 | paytovote unsafe_reset_all 52 | paytovote init 53 | ``` 54 | 55 | For the default genesis file provided (~/.basecoin/genesis.json) we have specified a starting account at the hex 56 | address 0x1B1BE55F969F54064628A63B9559E7C21C925165 to have 1000 coins of "issueToken", and "voteToken". 57 | The address, public key, and private key for this account are also stored under ~/.basecoin/key.json 58 | Now we can start paytovote: 59 | 60 | ``` 61 | paytovote start 62 | ``` 63 | 64 | In another terminal window (or tab: ctrl-shift-t), we can run the client tool: 65 | 66 | ``` 67 | paytovote account 0x1B1BE55F969F54064628A63B9559E7C21C925165 68 | ``` 69 | The above transaction will check for an account with the given hex address and 70 | list any coins within that account. We should see the initialized amount of 71 | 1000 issueToken, and voteToken. The default cost of generating a new issue 1 72 | issueToken and is currently hard coded into paytovote, let's create an issue 73 | which can be voted on. Notice the flags that are used in this proceedure: 74 | - `--from key.json` the transaction is coming from the account described within the key.json file under our current directory 75 | - `--voteFee 1voteToken` set the future cost of voting for this issue to 1 voteToken 76 | - `--amount 1issueToken` the amount of coins we are sending in with this transaction, in this case 1 issueToken 77 | - `--issue freeFoobar` name of the issue we will be generating with this transaction 78 | 79 | ``` 80 | paytovote tx paytovote create-issue --from key.json --voteFee 1voteToken --amount 1issueToken --issue freeFoobar 81 | ``` 82 | 83 | Now we can query for our issue as see that it has been created and that no votes have yet been cast: 84 | 85 | ``` 86 | paytovote query p2vIssue freeFoobar 87 | ``` 88 | 89 | Next let's make a few votes, first we will vote for the issue once, and then against the issue twice 90 | 91 | ``` 92 | paytovote tx paytovote vote --from key.json --amount 1voteToken --issue freeFoobar --voteFor 93 | paytovote tx paytovote vote --from key.json --amount 1voteToken --issue freeFoobar 94 | paytovote tx paytovote vote --from key.json --amount 1voteToken --issue freeFoobar 95 | ``` 96 | 97 | To view the votes that have been cast query the transaction once more 98 | 99 | ``` 100 | paytovote query p2vIssue freeFoobar 101 | ``` 102 | 103 | Lastly we can verify that we have in fact spent 1 issueToken and 3 voteToken, 104 | 105 | ``` 106 | paytovote account 0x1B1BE55F969F54064628A63B9559E7C21C925165 107 | ``` 108 | 109 | ### Thoughts for future development 110 | - Creating multiple vote options at issue generation (such as options or candidate name) 111 | - Alternative voting methods (for example ranked voting system) 112 | - Determine the type of voting mechanism when creating the issue 113 | - Allow votes to 'write in' their own candidate or spoil their ballot 114 | - Methods for the distributions of vote tokens 115 | 116 | -------------------------------------------------------------------------------- /paytovote/cmd/paytovote/commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | bcmd "github.com/tendermint/basecoin/cmd/commands" 5 | ) 6 | 7 | func init() { 8 | //Change the GenesisJSON 9 | bcmd.GenesisJSON = `{ 10 | "app_hash": "", 11 | "chain_id": "test_chain_id", 12 | "genesis_time": "0001-01-01T00:00:00.000Z", 13 | "validators": [ 14 | { 15 | "amount": 10, 16 | "name": "", 17 | "pub_key": { 18 | "type": "ed25519", 19 | "data": "7B90EA87E7DC0C7145C8C48C08992BE271C7234134343E8A8E8008E617DE7B30" 20 | } 21 | } 22 | ], 23 | "app_options": { 24 | "accounts": [{ 25 | "pub_key": { 26 | "type": "ed25519", 27 | "data": "619D3678599971ED29C7529DDD4DA537B97129893598A17C82E3AC9A8BA95279" 28 | }, 29 | "coins": [ 30 | { 31 | "denom": "issueToken", 32 | "amount": 1000 33 | }, 34 | { 35 | "denom": "voteToken", 36 | "amount": 1000 37 | } 38 | ] 39 | }] 40 | } 41 | }` 42 | 43 | } 44 | -------------------------------------------------------------------------------- /paytovote/cmd/paytovote/commands/paytovote.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/tendermint/basecoin-examples/paytovote" 10 | bcmd "github.com/tendermint/basecoin/cmd/commands" 11 | "github.com/tendermint/basecoin/types" 12 | "github.com/tendermint/go-wire" 13 | ) 14 | 15 | const PaytovoteName = "paytovote" 16 | 17 | var ( 18 | //flags 19 | issueFlag string 20 | voteFeeFlag string 21 | voteForFlag bool 22 | 23 | //commands 24 | P2VTxCmd = &cobra.Command{ 25 | Use: "paytovote", 26 | Short: "Send transactions to the paytovote plugin", 27 | } 28 | 29 | P2VQueryIssueCmd = &cobra.Command{ 30 | Use: "p2vIssue", 31 | Short: "Query a paytovote issue", 32 | RunE: queryIssueCmd, 33 | } 34 | 35 | P2VCreateIssueCmd = &cobra.Command{ 36 | Use: "create-issue", 37 | Short: "Create an issue which can be voted for", 38 | RunE: createIssueCmd, 39 | } 40 | 41 | P2VVoteCmd = &cobra.Command{ 42 | Use: "vote", 43 | Short: "Vote for an existing issue", 44 | RunE: voteCmd, 45 | } 46 | ) 47 | 48 | func init() { 49 | 50 | //register flags 51 | 52 | issueFlag2Reg := bcmd.Flag2Register{&issueFlag, "issue", "default issue", "name of the issue to generate or vote for"} 53 | 54 | createIssueFlags := []bcmd.Flag2Register{ 55 | issueFlag2Reg, 56 | {&voteFeeFlag, "voteFee", "1voteToken", 57 | "the fees required to vote on this new issue, uses the format ,,... (eg: 1gold,2silver,5btc)"}, 58 | } 59 | 60 | voteFlags := []bcmd.Flag2Register{ 61 | issueFlag2Reg, 62 | {&voteForFlag, "voteFor", false, "if present vote will be a vote-for, if absent a vote-against"}, 63 | } 64 | 65 | bcmd.RegisterFlags(P2VCreateIssueCmd, createIssueFlags) 66 | bcmd.RegisterFlags(P2VVoteCmd, voteFlags) 67 | 68 | //register commands 69 | P2VTxCmd.AddCommand(P2VCreateIssueCmd, P2VVoteCmd) 70 | 71 | bcmd.RegisterTxSubcommand(P2VTxCmd) 72 | bcmd.RegisterQuerySubcommand(P2VQueryIssueCmd) 73 | bcmd.RegisterStartPlugin(PaytovoteName, func() types.Plugin { return paytovote.New() }) 74 | } 75 | 76 | func createIssueCmd(cmd *cobra.Command, args []string) error { 77 | 78 | voteFee, err := types.ParseCoins(voteFeeFlag) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | createIssueFee := types.Coins{{"issueToken", 1}} //manually set the cost to create a new issue here 84 | 85 | txBytes := paytovote.NewCreateIssueTxBytes(issueFlag, voteFee, createIssueFee) 86 | 87 | fmt.Println("Issue creation transaction sent") 88 | return bcmd.AppTx(PaytovoteName, txBytes) 89 | } 90 | 91 | func voteCmd(cmd *cobra.Command, args []string) error { 92 | 93 | var voteTB byte = paytovote.TypeByteVoteFor 94 | if !voteForFlag { 95 | voteTB = paytovote.TypeByteVoteAgainst 96 | } 97 | 98 | txBytes := paytovote.NewVoteTxBytes(issueFlag, voteTB) 99 | 100 | fmt.Println("Vote transaction sent") 101 | return bcmd.AppTx(PaytovoteName, txBytes) 102 | } 103 | 104 | func queryIssueCmd(cmd *cobra.Command, args []string) error { 105 | 106 | //get the parent context 107 | parentContext := cmd.Parent() 108 | 109 | //get the issue, generate issue key 110 | if len(args) != 1 { 111 | return fmt.Errorf("query command requires an argument ([issue])") //never stack trace 112 | } 113 | issue := args[0] 114 | issueKey := paytovote.IssueKey(issue) 115 | 116 | //perform the query, get response 117 | resp, err := bcmd.Query(parentContext.Flag("node").Value.String(), issueKey) 118 | if err != nil { 119 | return err 120 | } 121 | if !resp.Code.IsOK() { 122 | return errors.Errorf("Query for issueKey (%v) returned non-zero code (%v): %v", 123 | string(issueKey), resp.Code, resp.Log) 124 | } 125 | 126 | //get the paytovote issue object and print it 127 | p2vIssue, err := paytovote.GetIssueFromWire(resp.Value) 128 | if err != nil { 129 | return err 130 | } 131 | fmt.Println(string(wire.JSONBytes(p2vIssue))) 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /paytovote/cmd/paytovote/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/tendermint/basecoin/cmd/commands" 9 | "github.com/tendermint/tmlibs/cli" 10 | 11 | _ "github.com/tendermint/basecoin-examples/paytovote/cmd/paytovote/commands" 12 | ) 13 | 14 | func main() { 15 | 16 | var RootCmd = &cobra.Command{ 17 | Use: "paytovote", 18 | } 19 | 20 | RootCmd.AddCommand( 21 | commands.InitCmd, 22 | commands.StartCmd, 23 | commands.TxCmd, 24 | commands.QueryCmd, 25 | commands.KeyCmd, 26 | commands.VerifyCmd, 27 | commands.BlockCmd, 28 | commands.AccountCmd, 29 | commands.UnsafeResetAllCmd, 30 | commands.QuickVersionCmd("0.2.0"), 31 | ) 32 | 33 | cmd := cli.PrepareMainCmd(RootCmd, "PV", os.ExpandEnv("$HOME/.paytovote")) 34 | cmd.Execute() 35 | } 36 | -------------------------------------------------------------------------------- /paytovote/glide.lock: -------------------------------------------------------------------------------- 1 | hash: 997e4cc3339141ee01aa2adf656425a49ebf117e6ca9e81ba72b8f94fee3e86e 2 | updated: 2017-05-17T12:25:00.580569867+02:00 3 | imports: 4 | - name: github.com/bgentry/speakeasy 5 | version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd 6 | - name: github.com/btcsuite/btcd 7 | version: 1ae306021e323ae11c71ffb8546fbd9019e6cb6f 8 | subpackages: 9 | - btcec 10 | - name: github.com/BurntSushi/toml 11 | version: b26d9c308763d68093482582cea63d69be07a0f0 12 | - name: github.com/ebuchman/fail-test 13 | version: 95f809107225be108efcf10a3509e4ea6ceef3c4 14 | - name: github.com/fsnotify/fsnotify 15 | version: 4da3e2cfbabc9f751898f250b49f2439785783a1 16 | - name: github.com/go-kit/kit 17 | version: d67bb4c202e3b91377d1079b110a6c9ce23ab2f8 18 | subpackages: 19 | - log 20 | - log/level 21 | - log/term 22 | - name: github.com/go-logfmt/logfmt 23 | version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 24 | - name: github.com/go-playground/locales 25 | version: 1e5f1161c6416a5ff48840eb8724a394e48cc534 26 | subpackages: 27 | - currency 28 | - name: github.com/go-playground/universal-translator 29 | version: 71201497bace774495daed26a3874fd339e0b538 30 | - name: github.com/go-stack/stack 31 | version: 7a2f19628aabfe68f0766b59e74d6315f8347d22 32 | - name: github.com/golang/protobuf 33 | version: b50ceb1fa9818fa4d78b016c2d4ae025593a7ce3 34 | subpackages: 35 | - proto 36 | - ptypes/any 37 | - name: github.com/golang/snappy 38 | version: 553a641470496b2327abcac10b36396bd98e45c9 39 | - name: github.com/gorilla/context 40 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 41 | - name: github.com/gorilla/handlers 42 | version: 3a5767ca75ece5f7f1440b1d16975247f8d8b221 43 | - name: github.com/gorilla/mux 44 | version: 392c28fe23e1c45ddba891b0320b3b5df220beea 45 | - name: github.com/gorilla/websocket 46 | version: a91eba7f97777409bc2c443f5534d41dd20c5720 47 | - name: github.com/hashicorp/hcl 48 | version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca 49 | subpackages: 50 | - hcl/ast 51 | - hcl/parser 52 | - hcl/scanner 53 | - hcl/strconv 54 | - hcl/token 55 | - json/parser 56 | - json/scanner 57 | - json/token 58 | - name: github.com/inconshreveable/mousetrap 59 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 60 | - name: github.com/jmhodges/levigo 61 | version: c42d9e0ca023e2198120196f842701bb4c55d7b9 62 | - name: github.com/kr/logfmt 63 | version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 64 | - name: github.com/magiconair/properties 65 | version: 51463bfca2576e06c62a8504b5c0f06d61312647 66 | - name: github.com/mitchellh/mapstructure 67 | version: cc8532a8e9a55ea36402aa21efdf403a60d34096 68 | - name: github.com/pelletier/go-buffruneio 69 | version: c37440a7cf42ac63b919c752ca73a85067e05992 70 | - name: github.com/pelletier/go-toml 71 | version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a 72 | - name: github.com/pkg/errors 73 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 74 | - name: github.com/spf13/afero 75 | version: 9be650865eab0c12963d8753212f4f9c66cdcf12 76 | subpackages: 77 | - mem 78 | - name: github.com/spf13/cast 79 | version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 80 | - name: github.com/spf13/cobra 81 | version: 3454e0e28e69c1b8effa6b5123c8e4185e20d696 82 | - name: github.com/spf13/jwalterweatherman 83 | version: 8f07c835e5cc1450c082fe3a439cf87b0cbb2d99 84 | - name: github.com/spf13/pflag 85 | version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 86 | - name: github.com/spf13/viper 87 | version: 0967fc9aceab2ce9da34061253ac10fb99bba5b2 88 | - name: github.com/syndtr/goleveldb 89 | version: 8c81ea47d4c41a385645e133e15510fc6a2a74b4 90 | subpackages: 91 | - leveldb 92 | - leveldb/cache 93 | - leveldb/comparer 94 | - leveldb/errors 95 | - leveldb/filter 96 | - leveldb/iterator 97 | - leveldb/journal 98 | - leveldb/memdb 99 | - leveldb/opt 100 | - leveldb/storage 101 | - leveldb/table 102 | - leveldb/util 103 | - name: github.com/tendermint/abci 104 | version: 5dabeffb35c027d7087a12149685daa68989168b 105 | subpackages: 106 | - client 107 | - example/dummy 108 | - server 109 | - types 110 | - name: github.com/tendermint/basecoin 111 | version: bd62b21d6e2f7faa6dfeed62d5ac4b9bb3553031 112 | - name: github.com/tendermint/ed25519 113 | version: 1f52c6f8b8a5c7908aff4497c186af344b428925 114 | subpackages: 115 | - edwards25519 116 | - extra25519 117 | - name: github.com/tendermint/go-crypto 118 | version: 438b16f1f84ef002d7408ecd6fc3a3974cbc9559 119 | subpackages: 120 | - cmd 121 | - keys 122 | - keys/cryptostore 123 | - keys/server 124 | - keys/server/types 125 | - keys/storage/filestorage 126 | - name: github.com/tendermint/go-wire 127 | version: 97beaedf0f4dbc035309157c92be3b30cc6e5d74 128 | subpackages: 129 | - data 130 | - data/base58 131 | - name: github.com/tendermint/light-client 132 | version: 478876ca34b360df62f941d5e20cdd608fa0a466 133 | subpackages: 134 | - certifiers 135 | - certifiers/client 136 | - certifiers/files 137 | - commands 138 | - commands/proofs 139 | - commands/proxy 140 | - commands/seeds 141 | - commands/txs 142 | - proofs 143 | - name: github.com/tendermint/merkleeyes 144 | version: c722818b460381bc5b82e38c73ff6e22a9df624d 145 | subpackages: 146 | - app 147 | - client 148 | - iavl 149 | - name: github.com/tendermint/tendermint 150 | version: 11b5d11e9eec170e1d3dce165f0270d5c0759d69 151 | subpackages: 152 | - blockchain 153 | - config 154 | - consensus 155 | - mempool 156 | - node 157 | - p2p 158 | - p2p/upnp 159 | - proxy 160 | - rpc/client 161 | - rpc/core 162 | - rpc/core/types 163 | - rpc/grpc 164 | - rpc/lib 165 | - rpc/lib/client 166 | - rpc/lib/server 167 | - rpc/lib/types 168 | - state 169 | - state/txindex 170 | - state/txindex/kv 171 | - state/txindex/null 172 | - types 173 | - version 174 | - name: github.com/tendermint/tmlibs 175 | version: 8af1c70a8be17543eb33e9bfbbcdd8371e3201cc 176 | subpackages: 177 | - autofile 178 | - cli 179 | - clist 180 | - common 181 | - db 182 | - events 183 | - flowrate 184 | - log 185 | - logger 186 | - merkle 187 | - name: golang.org/x/crypto 188 | version: ab89591268e0c8b748cbe4047b00197516011af5 189 | subpackages: 190 | - curve25519 191 | - nacl/box 192 | - nacl/secretbox 193 | - openpgp/armor 194 | - openpgp/errors 195 | - poly1305 196 | - ripemd160 197 | - salsa20/salsa 198 | - name: golang.org/x/net 199 | version: c9b681d35165f1995d6f3034e61f8761d4b90c99 200 | subpackages: 201 | - context 202 | - http2 203 | - http2/hpack 204 | - idna 205 | - internal/timeseries 206 | - lex/httplex 207 | - trace 208 | - name: golang.org/x/sys 209 | version: 9c9d83fe39ed3fd2d9249fcf6b755891fff54b03 210 | subpackages: 211 | - unix 212 | - name: golang.org/x/text 213 | version: 470f45bf29f4147d6fbd7dfd0a02a848e49f5bf4 214 | subpackages: 215 | - secure/bidirule 216 | - transform 217 | - unicode/bidi 218 | - unicode/norm 219 | - name: google.golang.org/genproto 220 | version: 411e09b969b1170a9f0c467558eb4c4c110d9c77 221 | subpackages: 222 | - googleapis/rpc/status 223 | - name: google.golang.org/grpc 224 | version: a0c3e72252b6fbf4826bb143e450eb05588a9d6d 225 | subpackages: 226 | - codes 227 | - credentials 228 | - grpclb/grpc_lb_v1 229 | - grpclog 230 | - internal 231 | - keepalive 232 | - metadata 233 | - naming 234 | - peer 235 | - stats 236 | - status 237 | - tap 238 | - transport 239 | - name: gopkg.in/go-playground/validator.v9 240 | version: 6d8c18553ea1ac493d049edd6f102f52e618f085 241 | - name: gopkg.in/yaml.v2 242 | version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 243 | testImports: 244 | - name: github.com/davecgh/go-spew 245 | version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 246 | subpackages: 247 | - spew 248 | - name: github.com/pmezard/go-difflib 249 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 250 | subpackages: 251 | - difflib 252 | - name: github.com/stretchr/testify 253 | version: 4d4bfba8f1d1027c4fdbe371823030df51419987 254 | subpackages: 255 | - assert 256 | - require 257 | -------------------------------------------------------------------------------- /paytovote/glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/tendermint/basecoin-examples/mintcoin 2 | import: 3 | - package: github.com/gorilla/websocket 4 | - package: github.com/pkg/errors 5 | - package: github.com/spf13/cobra 6 | - package: github.com/spf13/pflag 7 | - package: github.com/spf13/viper 8 | - package: github.com/tendermint/abci 9 | version: develop 10 | subpackages: 11 | - server 12 | - types 13 | - package: github.com/tendermint/basecoin 14 | version: develop 15 | - package: github.com/tendermint/go-crypto 16 | version: develop 17 | subpackages: 18 | - cmd 19 | - keys 20 | - package: github.com/tendermint/go-wire 21 | version: develop 22 | subpackages: 23 | - data 24 | - package: github.com/tendermint/light-client 25 | version: develop 26 | subpackages: 27 | - commands 28 | - commands/proofs 29 | - commands/seeds 30 | - commands/txs 31 | - proofs 32 | - package: github.com/tendermint/merkleeyes 33 | version: develop 34 | subpackages: 35 | - client 36 | - iavl 37 | - package: github.com/tendermint/tendermint 38 | version: develop 39 | subpackages: 40 | - config 41 | - node 42 | - proxy 43 | - rpc/client 44 | - rpc/core/types 45 | - rpc/lib/client 46 | - rpc/lib/types 47 | - types 48 | - package: github.com/tendermint/tmlibs 49 | version: develop 50 | subpackages: 51 | - cli 52 | - common 53 | - events 54 | - log 55 | - logger 56 | testImport: 57 | - package: github.com/stretchr/testify 58 | subpackages: 59 | - assert 60 | - require 61 | -------------------------------------------------------------------------------- /paytovote/paytovote.go: -------------------------------------------------------------------------------- 1 | package paytovote 2 | 3 | import ( 4 | "fmt" 5 | 6 | abci "github.com/tendermint/abci/types" 7 | "github.com/tendermint/basecoin/state" 8 | "github.com/tendermint/basecoin/types" 9 | "github.com/tendermint/go-wire" 10 | ) 11 | 12 | type P2VPlugin struct { 13 | name string 14 | } 15 | 16 | func New() *P2VPlugin { 17 | return &P2VPlugin{ 18 | name: "paytovote", 19 | } 20 | } 21 | 22 | /////////////////////////////////////////////////// 23 | 24 | const ( 25 | TypeByteTxCreate byte = 0x01 26 | TypeByteTxVote byte = 0x02 27 | 28 | TypeByteVoteFor byte = 0x01 29 | TypeByteVoteAgainst byte = 0x02 30 | ) 31 | 32 | type createIssueTx struct { 33 | Issue string //Issue to be created 34 | FeePerVote types.Coins //Cost to vote for the issue 35 | Fee2CreateIssue types.Coins //Cost to create a new issue 36 | } 37 | 38 | type voteTx struct { 39 | Issue string //Issue being voted for 40 | VoteTypeByte byte //How is the vote being cast 41 | } 42 | 43 | func NewCreateIssueTxBytes(issue string, feePerVote, fee2CreateIssue types.Coins) []byte { 44 | data := wire.BinaryBytes( 45 | createIssueTx{ 46 | Issue: issue, 47 | FeePerVote: feePerVote, 48 | Fee2CreateIssue: fee2CreateIssue, 49 | }) 50 | data = append([]byte{TypeByteTxCreate}, data...) 51 | return data 52 | } 53 | 54 | func NewVoteTxBytes(issue string, voteTypeByte byte) []byte { 55 | data := wire.BinaryBytes( 56 | voteTx{ 57 | Issue: issue, 58 | VoteTypeByte: voteTypeByte, 59 | }) 60 | data = append([]byte{TypeByteTxVote}, data...) 61 | return data 62 | } 63 | 64 | /////////////////////////////////////////////////// 65 | 66 | type P2VIssue struct { 67 | Issue string 68 | FeePerVote types.Coins 69 | VotesFor int 70 | VotesAgainst int 71 | } 72 | 73 | func newP2VIssue(issue string, feePerVote types.Coins) P2VIssue { 74 | return P2VIssue{ 75 | Issue: issue, 76 | FeePerVote: feePerVote, 77 | VotesFor: 0, 78 | VotesAgainst: 0, 79 | } 80 | } 81 | 82 | func IssueKey(issue string) []byte { 83 | //The state key is defined as only being affected by effected issue 84 | // aka. if multiple paytovote plugins are initialized 85 | // then all will have access to the same issue vote counts 86 | return []byte(fmt.Sprintf("P2VPlugin,issue=%v", issue)) 87 | } 88 | 89 | //get the issue from store bytes 90 | func GetIssueFromWire(issueBytes []byte) (p2vIssue P2VIssue, err error) { 91 | 92 | //Determine if the issue already exists and load 93 | if len(issueBytes) > 0 { //is there a record of the issue existing? 94 | err = wire.ReadBinaryBytes(issueBytes, &p2vIssue) 95 | if err != nil { 96 | err = abci.ErrInternalError.AppendLog("Error decoding state: " + err.Error()) 97 | } 98 | } else { 99 | err = abci.ErrInternalError.AppendLog("Tx Issue not found") 100 | } 101 | return 102 | } 103 | 104 | func getIssue(store types.KVStore, issue string) (p2vIssue P2VIssue, err error) { 105 | p2vIssueBytes := store.Get(IssueKey(issue)) 106 | 107 | return GetIssueFromWire(p2vIssueBytes) 108 | } 109 | 110 | /////////////////////////////////////////////////// 111 | 112 | func (p2v *P2VPlugin) Name() string { 113 | return p2v.name 114 | } 115 | 116 | func (p2v *P2VPlugin) SetOption(store types.KVStore, key string, value string) (log string) { 117 | return "" 118 | } 119 | 120 | func (p2v *P2VPlugin) RunTx(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) { 121 | 122 | defer func() { 123 | //Return the ctx coins to the wallet if there is an error 124 | if res.IsErr() { 125 | acc := ctx.CallerAccount 126 | acc.Balance = acc.Balance.Plus(ctx.Coins) // add the context transaction coins 127 | state.SetAccount(store, ctx.CallerAddress, acc) // save the new balance 128 | } 129 | }() 130 | 131 | //Determine the transaction type and then send to the appropriate transaction function 132 | if len(txBytes) < 1 { 133 | return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: no tx bytes") 134 | } 135 | 136 | //Note that the zero position of txBytes contains the type-byte for the tx type 137 | switch txBytes[0] { 138 | case TypeByteTxCreate: 139 | return p2v.runTxCreateIssue(store, ctx, txBytes[1:]) 140 | case TypeByteTxVote: 141 | return p2v.runTxVote(store, ctx, txBytes[1:]) 142 | default: 143 | return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: bad prepended bytes") 144 | } 145 | } 146 | 147 | func chargeFee(store types.KVStore, ctx types.CallContext, fee types.Coins) { 148 | 149 | //Charge the Fee from the context coins 150 | leftoverCoins := ctx.Coins.Minus(fee) 151 | if !leftoverCoins.IsZero() { 152 | acc := ctx.CallerAccount 153 | //return leftover coins 154 | acc.Balance = acc.Balance.Plus(leftoverCoins) // subtract fees 155 | state.SetAccount(store, ctx.CallerAddress, acc) // save the new balance 156 | } 157 | } 158 | 159 | func (p2v *P2VPlugin) runTxCreateIssue(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) { 160 | 161 | // Decode tx 162 | var tx createIssueTx 163 | err := wire.ReadBinaryBytes(txBytes, &tx) 164 | if err != nil { 165 | return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error()) 166 | } 167 | 168 | //Validate Tx 169 | switch { 170 | case len(tx.Issue) == 0: 171 | return abci.ErrInternalError.AppendLog("P2VTx.Issue must have a length greater than 0") 172 | case !tx.FeePerVote.IsValid(): 173 | return abci.ErrInternalError.AppendLog("P2VTx.Fee2Vote is not sorted or has zero amounts") 174 | case !tx.FeePerVote.IsNonnegative(): 175 | return abci.ErrInternalError.AppendLog("P2VTx.Fee2Vote must be nonnegative") 176 | case !tx.Fee2CreateIssue.IsValid(): 177 | return abci.ErrInternalError.AppendLog("P2VTx.Fee2CreateIssue is not sorted or has zero amounts") 178 | case !tx.Fee2CreateIssue.IsNonnegative(): 179 | return abci.ErrInternalError.AppendLog("P2VTx.Fee2CreateIssue must be nonnegative") 180 | case !ctx.Coins.IsGTE(tx.Fee2CreateIssue): // Did the caller provide enough coins? 181 | return abci.ErrInsufficientFunds.AppendLog("Tx Funds insufficient for creating a new issue") 182 | } 183 | 184 | //Return if the issue already exists, aka no error was thrown 185 | if _, err := getIssue(store, tx.Issue); err == nil { 186 | return abci.ErrInternalError.AppendLog("Cannot create an already existing issue") 187 | } 188 | 189 | // Create and Save P2VIssue, charge fee, return 190 | newP2VIssue := newP2VIssue(tx.Issue, tx.FeePerVote) 191 | store.Set(IssueKey(tx.Issue), wire.BinaryBytes(newP2VIssue)) 192 | chargeFee(store, ctx, tx.Fee2CreateIssue) 193 | return abci.OK 194 | } 195 | 196 | func (p2v *P2VPlugin) runTxVote(store types.KVStore, ctx types.CallContext, txBytes []byte) (res abci.Result) { 197 | 198 | // Decode tx 199 | var tx voteTx 200 | err := wire.ReadBinaryBytes(txBytes, &tx) 201 | if err != nil { 202 | return abci.ErrBaseEncodingError.AppendLog("Error decoding tx: " + err.Error()) 203 | } 204 | 205 | //Validate Tx 206 | if len(tx.Issue) == 0 { 207 | return abci.ErrInternalError.AppendLog("transaction issue must have a length greater than 0") 208 | } 209 | 210 | // Load P2VIssue 211 | p2vIssue, err := getIssue(store, tx.Issue) 212 | if err != nil { 213 | return abci.ErrInternalError.AppendLog("error loading issue: " + err.Error()) 214 | } 215 | 216 | // Did the caller provide enough coins? 217 | if !ctx.Coins.IsGTE(p2vIssue.FeePerVote) { 218 | return abci.ErrInsufficientFunds.AppendLog("Tx Funds insufficient for voting") 219 | } 220 | 221 | //Transaction Logic 222 | switch tx.VoteTypeByte { 223 | case TypeByteVoteFor: 224 | p2vIssue.VotesFor += 1 225 | case TypeByteVoteAgainst: 226 | p2vIssue.VotesAgainst += 1 227 | default: 228 | return abci.ErrInternalError.AppendLog("P2VTx.VoteTypeByte was not recognized") 229 | } 230 | 231 | // Save P2VIssue, charge fee, return 232 | store.Set(IssueKey(tx.Issue), wire.BinaryBytes(p2vIssue)) 233 | chargeFee(store, ctx, p2vIssue.FeePerVote) 234 | return abci.OK 235 | } 236 | 237 | func (p2v *P2VPlugin) InitChain(store types.KVStore, vals []*abci.Validator) { 238 | } 239 | 240 | func (p2v *P2VPlugin) BeginBlock(store types.KVStore, hash []byte, header *abci.Header) { 241 | } 242 | 243 | func (p2v *P2VPlugin) EndBlock(store types.KVStore, height uint64) (res abci.ResponseEndBlock) { 244 | return 245 | } 246 | -------------------------------------------------------------------------------- /paytovote/paytovote_test.go: -------------------------------------------------------------------------------- 1 | package paytovote 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/tendermint/basecoin/app" 10 | "github.com/tendermint/basecoin/state" 11 | "github.com/tendermint/basecoin/types" 12 | 13 | abci "github.com/tendermint/abci/types" 14 | "github.com/tendermint/go-wire" 15 | eyescli "github.com/tendermint/merkleeyes/client" 16 | cmn "github.com/tendermint/tmlibs/common" 17 | ) 18 | 19 | func TestP2VPlugin(t *testing.T) { 20 | assert := assert.New(t) 21 | 22 | // Basecoin initialization 23 | store := eyescli.NewLocalClient("", 0) //non-persistent instance of merkleeyes 24 | chainID := "test_chain_id" 25 | bcApp := app.NewBasecoin(store) 26 | //XXX test 27 | bcApp.SetOption("base/chain_id", chainID) 28 | 29 | // Add Counter plugin 30 | P2VPlugin := New() 31 | bcApp.RegisterPlugin(P2VPlugin) 32 | 33 | // Account initialization 34 | test1PrivAcc := types.PrivAccountFromSecret("test1") 35 | test1Acc := test1PrivAcc.Account 36 | 37 | // Seed Basecoin with account 38 | startBal := types.Coins{{"", 1000}, {"issueToken", 1000}, {"voteToken", 1000}} 39 | test1Acc.Balance = startBal 40 | accMarshal, err := json.Marshal(test1Acc) 41 | assert.Nil(err, "error Marshalling account") 42 | errStr := bcApp.SetOption("base/account", string(accMarshal)) 43 | assert.Equal("Success", errStr, errStr) 44 | 45 | bcApp.Commit() 46 | 47 | deliverTx := func(gas int64, 48 | fee types.Coin, 49 | inputCoins types.Coins, 50 | inputSequence int, 51 | txData []byte) abci.Result { 52 | 53 | // Construct an AppTx signature 54 | tx := &types.AppTx{ 55 | Gas: gas, 56 | Fee: fee, 57 | Name: P2VPlugin.Name(), 58 | Input: types.NewTxInput(test1Acc.PubKey, inputCoins, inputSequence), 59 | Data: txData, 60 | } 61 | 62 | // Sign request 63 | signBytes := tx.SignBytes(chainID) 64 | tx.Input.Signature = test1PrivAcc.Sign(signBytes) 65 | 66 | // Write request 67 | //txBytes := []byte(wire.BinaryBytes(struct{}{tx})) 68 | txBytes := wire.BinaryBytes(struct{ types.Tx }{tx}) 69 | return bcApp.DeliverTx(txBytes) 70 | } 71 | 72 | testBalance := func(expected types.Coins) { 73 | 74 | acc := state.GetAccount(bcApp.GetState(), test1Acc.PubKey.Address()) 75 | if acc == nil { 76 | panic("nil account when trying compare balance") 77 | } 78 | 79 | bal := acc.Balance 80 | if !bal.IsEqual(expected) { 81 | var expStr, balStr string 82 | for i := 0; i < len(expected); i++ { 83 | expStr += " " + expected[i].String() 84 | } 85 | for i := 0; i < len(bal); i++ { 86 | balStr += " " + bal[i].String() 87 | } 88 | 89 | panic(cmn.Fmt("bad balance expected %v, got %v", expStr, balStr)) 90 | } 91 | } 92 | 93 | //test for an issue that shouldn't exist 94 | testNoIssue := func(issue string) { 95 | _, err := getIssue(bcApp.GetState(), issue) 96 | if err == nil { 97 | panic(cmn.Fmt("issue that shouldn't exist was found, issue: %v", issue)) 98 | } 99 | } 100 | 101 | //test for an issue that should exist 102 | testIssue := func(issue string, expFor, expAgainst int) { 103 | p2vIssue, err := getIssue(bcApp.GetState(), issue) 104 | 105 | // return //TODO fix these tests, bad store being accessed 106 | 107 | //test for errors 108 | if err != nil { 109 | panic(cmn.Fmt("error loading issue %v for issue test, error: %v", issue, err.Error())) 110 | } 111 | 112 | if p2vIssue.VotesFor != expFor { 113 | panic(cmn.Fmt("expected %v votes-for, got %v votes-for, for issue %v", expFor, p2vIssue.VotesFor, issue)) 114 | } 115 | 116 | if p2vIssue.VotesAgainst != expAgainst { 117 | panic(cmn.Fmt("expected %v votes-against, got %v votes-against, for issue %v", expAgainst, p2vIssue.VotesAgainst, issue)) 118 | } 119 | } 120 | 121 | // REF: deliverTx(gas, fee, inputCoins, inputSequence, NewVoteTxBytes(issue, voteTypeByte)) 122 | // REF: deliverTx(gas, fee, inputCoins, inputSequence, NewCreateIssueTxBytes(issue, feePerVote, fee2CreateIssue)) 123 | 124 | issue1 := "free internet" 125 | issue2 := "commutate foobar" 126 | 127 | // Test a basic issue generation 128 | res := deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 2}}, 1, 129 | NewCreateIssueTxBytes(issue1, types.Coins{{"voteToken", 2}}, types.Coins{{"issueToken", 1}})) 130 | assert.True(res.IsOK(), res.String()) 131 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}})) 132 | testIssue(issue1, 0, 0) 133 | 134 | // Test a basic votes 135 | res = deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 2}}, 2, 136 | NewVoteTxBytes(issue1, TypeByteVoteFor)) 137 | assert.True(res.IsOK(), res.String()) 138 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}, {"voteToken", 2}})) 139 | testIssue(issue1, 1, 0) 140 | 141 | res = deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 2}}, 3, 142 | NewVoteTxBytes(issue1, TypeByteVoteAgainst)) 143 | assert.True(res.IsOK(), res.String()) 144 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}, {"voteToken", 4}})) 145 | testIssue(issue1, 1, 1) 146 | 147 | // Test prevented voting on non-existent issue 148 | res = deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 2}}, 4, 149 | NewVoteTxBytes(issue2, TypeByteVoteFor)) 150 | assert.True(res.IsErr(), res.String()) 151 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}, {"voteToken", 4}})) 152 | testNoIssue(issue2) 153 | 154 | // Test prevented duplicate issue generation 155 | res = deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 2}}, 5, 156 | NewCreateIssueTxBytes(issue1, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 1}})) 157 | assert.True(res.IsErr(), res.String()) 158 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}, {"voteToken", 4}})) 159 | 160 | // Test prevented issue generation from insufficient funds 161 | res = deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 2}}, 6, 162 | NewCreateIssueTxBytes(issue2, types.Coins{{"voteToken", 1}}, types.Coins{{"issueToken", 2}})) 163 | assert.True(res.IsErr(), res.String()) 164 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}, {"voteToken", 4}})) 165 | testNoIssue(issue2) 166 | 167 | // Test prevented voting from insufficient funds 168 | res = deliverTx(0, types.Coin{}, types.Coins{{"", 1}, {"issueToken", 1}, {"voteToken", 1}}, 7, 169 | NewVoteTxBytes(issue1, TypeByteVoteFor)) 170 | assert.True(res.IsErr(), res.String()) 171 | testBalance(startBal.Minus(types.Coins{{"issueToken", 1}, {"voteToken", 4}})) 172 | testIssue(issue1, 1, 1) 173 | } 174 | -------------------------------------------------------------------------------- /trader/Makefile: -------------------------------------------------------------------------------- 1 | all: get_vendor_deps test install 2 | 3 | test: 4 | go test `glide novendor` 5 | 6 | install: 7 | go install ./cmd/... 8 | 9 | get_vendor_deps: 10 | @go get github.com/Masterminds/glide 11 | glide install 12 | -------------------------------------------------------------------------------- /trader/README.md: -------------------------------------------------------------------------------- 1 | # Trader - let's play with finances 2 | 3 | Trader simulates some basic financial actions that are very common in the 4 | modern world, and shows how we can add these features to basecoin, in our quest 5 | to make the best financial system ever. Stable and secure and feature-rich, 6 | and super fast. This is going to be so good. 7 | 8 | ## Install 9 | 10 | Run `make all` in this directory. 11 | This will update all dependencies, run the test suite, and install the `trader` binary to your `$GOPATH/bin`. 12 | 13 | 14 | ## Escrow 15 | 16 | This first instrument we implement is an [escrow](./escrow). We can send money 17 | via an `AppTx` to create an escrow. We thereby specify who the intended 18 | recipient is, and who can releae the money (or return it). Note that we give 19 | this "arbiter" the power to send the money to the recipient or return it to the 20 | sender, but no way to take the money and put it in their own pocket. Removing 21 | more locations for fraud. 22 | 23 | When we create an escrow, we get a unique address back. Save this. Later on, 24 | the arbiter can send a message to this address to release the money to either 25 | the intended recipient, or back to the sender if they failed to deliver on 26 | their promise. And just in case, if too much time passed and no one did 27 | anything, you can always expire the escrow and recover the money (eg. if the 28 | arbiter lost their private key - ouch!). 29 | 30 | ### Data structure 31 | 32 | We create a separate data location for each escrow. It is stored in the 33 | following format, where `Sender`, `Recipient`, and `Arbiter` are all addresses. 34 | The value is determined from the money sent in creation, and the same amount is 35 | paid back when the escrow is resolved. The address of the escrow is determined 36 | from the hash for the data bytes, to guarantee uniqueness. 37 | 38 | ``` 39 | // EscrowData is our principal data structure in the db 40 | type EscrowData struct { 41 | Sender []byte 42 | Recipient []byte 43 | Arbiter []byte 44 | Expiration uint64 // height when the offer expires (0 = never) 45 | Amount types.Coins 46 | } 47 | ``` 48 | 49 | There are two basic operations one can perform on an escrow - creating it, and 50 | resolving it. Resolving it can be done either by a clear decision of the 51 | arbiter, or by a simple expiration. Thus, there are three transaction types 52 | for these two concepts. 53 | 54 | ### Testing with a CLI 55 | 56 | By this point, you have probably played with the basecoin-based cli a few times 57 | and are feeling real comfortable-like. So, let's just jump right in with a few 58 | commands: 59 | 60 | Setup basecoin server with default genesis: 61 | 62 | ``` 63 | trader unsafe_reset_all 64 | trader init 65 | ``` 66 | 67 | Now we can start Basecoin with the trader plugin and the default genesis: 68 | 69 | ``` 70 | trader start 71 | ``` 72 | 73 | Run basecoin client in another window. In this example, key.json will be the 74 | sender, key2.json the arbiter. And some empty account the receiver. 75 | 76 | ``` 77 | # check the three accounts 78 | trader account 1B1BE55F969F54064628A63B9559E7C21C925165 # sender 79 | trader account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 # arbiter 80 | trader account 2ABAA2CCFA1F618CF9C97F1FD59FC3EE4968FE8A # receiver 81 | 82 | # (optional) give the broke guy some cash 83 | trader tx send --chain_id trader_chain_id --from key.json --amount 20mycoin --to 2ABAA2CCFA1F618CF9C97F1FD59FC3EE4968FE8A 84 | trader account 2ABAA2CCFA1F618CF9C97F1FD59FC3EE4968FE8A # receiver 85 | 86 | # let's make an escrow 87 | trader tx escrow create --chain_id trader_chain_id --from key.json --amount 400mycoin --recv 2ABAA2CCFA1F618CF9C97F1FD59FC3EE4968FE8A --arbiter 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 88 | 89 | #-> TODO: need to get ESCROW_ID locally, broadcastTx response.... 90 | ESCROW_ID= 91 | 92 | # fails cuz the sender cannot release funds 93 | trader tx escrow pay --chain_id trader_chain_id --from key.json --amount 1mycoin --escrow $ESCROW_ID 94 | 95 | # note, that it didn't cost anything to fail :) 96 | trader account 1B1BE55F969F54064628A63B9559E7C21C925165 # sender 97 | 98 | # succeeds as the arbiter can 99 | trader tx escrow pay --chain_id trader_chain_id --from key2.json --amount 1mycoin --escrow $ESCROW_ID 100 | 101 | # but you pay the fees when the call works (1 mycoin here) 102 | trader account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 # arbiter 103 | 104 | # and the money was sent 105 | trader account 1B1BE55F969F54064628A63B9559E7C21C925165 # sender 106 | trader account 2ABAA2CCFA1F618CF9C97F1FD59FC3EE4968FE8A # receiver 107 | 108 | # but an error the second time the arbiter tries to send the same money (no re-entrant contracts) 109 | trader tx escrow pay --chain_id trader_chain_id --from key2.json --amount 1mycoin --escrow $ESCROW_ID 110 | 111 | # TODO: let's demo expiry and more 112 | 113 | # ASIDE: digging in with a debugger.... 114 | dlv debug ../cmd/trader/main.go -- tx escrow create --chain_id trader_chain_id --from key.json --amount 400mycoin --recv 2ABAA2CCFA1F618CF9C97F1FD59FC3EE4968FE8A --arbiter 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 115 | ``` 116 | 117 | ## Currency Options 118 | 119 | Moving on to a more complex example, we will create a [currency option](./options). 120 | This is the option to buy one set of coins (eg. 100 ETH) for another set of coins (eg. 2 BTC). 121 | There are two parties in the option - the issuer and the holder. 122 | The issuer bonds a certain set of Coin in the option and sets the price. 123 | The holder is the account that has the right to exercise the option, 124 | that is send the trade value, which goes to the original issuer, while the bonded value is released to the holder. 125 | If the option is not used in a given time, the bond returns to the issuer. 126 | 127 | On first glance, this is a similar set up to escrow, bonded coins that can be released by another transaction. 128 | However, there is one additional step. The option can be bought and sold without exercising it. 129 | That is, the holder can transfer the option to a new holder in return for some coin. 130 | And this transfer operation should be done atomically, without room for one party cheating. 131 | 132 | ### Data structure 133 | 134 | We create a separate data location for each option. 135 | It is stored in the following format, where `Sender`, `Recipient`, and `Arbiter` are all addresses. 136 | The value is determined from the money sent in creation, and the same amount is paid back when the escrow is resolved. 137 | The address of the escrow is determined from the hash for the data bytes, to guarantee uniqueness. 138 | 139 | ``` 140 | // OptionData is our principal data structure in the db 141 | type OptionData struct { 142 | OptionIssue 143 | OptionHolder 144 | } 145 | 146 | // OptionIssue is the constant part, created wth the option, never changes 147 | type OptionIssue struct { 148 | // this is for the normal option functionality 149 | Issuer []byte 150 | Serial int64 // this serial number is from the apptx that created it 151 | Expiration uint64 // height when the offer expires (0 = never) 152 | Bond types.Coins // this is stored upon creation of the option 153 | Trade types.Coins // this is the money that can exercise the option 154 | } 155 | 156 | // OptionHolder is the dynamic section of who can excercise the options 157 | type OptionHolder struct { 158 | // this is for buying/selling the option (should be a separate struct?) 159 | Holder []byte 160 | NewHolder []byte // set to allow for only one buyer, empty for any buyer 161 | Price types.Coins // required payment to transfer ownership 162 | } 163 | ``` 164 | 165 | We can perform the following actions on an option: 166 | 167 | * Create the option (sending Bond to the apptx, Holder=Issuer at first) 168 | * Offer the option for sale (Specifying Price) 169 | * Purchase the option (by sending Price to apptx, changes Holder) 170 | * Exercise the option (only the holder can do by sending Trade to apptx) 171 | * Disolve the option (either at expiration, or if Issuer=Holder and wants the Bond back) 172 | 173 | Thus, we need a transaction type for each of these actions 174 | 175 | ### Code Design 176 | 177 | I have attempted to abstract out some common patterns by this point in time 178 | . These patterns could be useful for anyone else attempting to build a basecoin plugin, so I will cover them briefly. 179 | 180 | 1. When working with a plugin, generally we prefix all Set/Get queries for the plugin-specific data with a standard prefix (eg. the plugin name). 181 | In order to avoid this boiler-plater, you can just wrap the store with a [PrefixStore](./prefix_store.go#L10), 182 | and then all `Get` and `Set` methods are automatically prefixed with the key. 183 | 1. The only non-local data we generally should touch is the `Accounts` themselves to update the balances. 184 | We can wrap the `KVStore` with an [Accountant](./options/util.go#L9), to easily `GetAccount` and `SetAccount`, 185 | as well as `Refund` all coins sent on the transaction for errors, or `Pay` some coins to a given address. 186 | 1. We define a plugin-specific [transaction type](./options/data.go#L113-L119) along with writing and parsing them. 187 | We can then just [register all supported transactions](./options/data.go#L13-L23) in the init function, and all parsing is taking care of. 188 | 1. The `Plugin` itself simply tracks the height, parses the transactions, 189 | and [delegates the work](./options/plugin.go#L37-L50) to the transactions themselves. 190 | The real code is in the transactions, which have a special interface to get all data they need. 191 | 1. Each new transaction you want to support, simply involves creating the [data structure](./options/tx.go#L11-L15), 192 | implementing the [Apply method]((./options/tx.go#L17-L42)), and [registering it](./options/data.go#L17) with go wire. 193 | 1. You can add a special [command for the plugin](./commands/options.go#L52-L63), 194 | with [subcommands for each transaction](./commands/options.go#L65-L76), 195 | and then a simple [parsing of args to data structure](./commands/options.go#L146-L161). 196 | This is not required for the plugin to function, but with a little work, 197 | you can [integrate it](./commands/options.go#L140-L144) in the [basecoin cli](./cmd/trader/main.go#L7), and allow a much better workflow to debug and demo it until you have a gui. 198 | 1. Make sure all data has a unique, deterministic, constant address. 199 | To do so, we can hash all or part of the data. 200 | To make it constant, only hash the constant part of the data (`OptionIssue`), 201 | the mutable parts of the data (`OptionHolder`) should not be included in this hash for obvious reasons. 202 | To guarantee uniqueness (should the same user issue the same command twice), 203 | it is nice to include the sequence number of the create transaction as part of the immutible section. 204 | 205 | Using these patterns should allow you to perform most actions you reasonably 206 | wish to perform with your basecoin plugin, while removing much boilerplate and 207 | bit switching. It is also very extensible if you wish to add a new transaction 208 | type. And allows easily setting up the scaffolding for [unit 209 | tests](./options/tx_test.go#L12-L43). 210 | 211 | ### Testing with a CLI 212 | 213 | You know the deal by now, so.... 214 | 215 | In one window start the server: 216 | 217 | ``` 218 | trader unsafe_reset_all 219 | trader init 220 | trader start 221 | ``` 222 | 223 | Run basecoin client in another window. In this example, key.json will be the issuer, key2.json the holder. 224 | 225 | ``` 226 | # check the two accounts 227 | trader account 1B1BE55F969F54064628A63B9559E7C21C925165 # issuer 228 | trader account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 # holder 229 | 230 | # let's make an option 231 | trader tx options create --chain_id trader_chain_id --from key.json --amount 400ETH --trade 4BTC 232 | 233 | #-> TODO: need to get OPTION_ID locally, broadcastTx response.... 234 | OPTION_ID= 235 | trader tx options query --chain_id trader_chain_id $OPTION_ID 236 | 237 | # we cannot exercise it cuz the we do not own the option yet 238 | trader tx options exercise --chain_id trader_chain_id --from key2.json --amount 4BTC --option $OPTION_ID 239 | 240 | # note, that it didn't cost anything to fail :) no 4 BTC loss.... 241 | trader account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 # sender 242 | 243 | # so, let us offer this for sale (only the current holder can) 244 | # also note this money is not used up (just needs to be non-zero to prevent spaming) 245 | trader tx options sell --chain_id trader_chain_id --from key.json --amount 10ETH --option $OPTION_ID --price 100mycoin 246 | 247 | # and now the holder can buy the rights to the option. 248 | # the money is used up to the price level (overpayment returned) 249 | trader tx options buy --chain_id trader_chain_id --from key2.json --amount 250mycoin --option $OPTION_ID 250 | 251 | # check the two accounts 252 | trader account 1B1BE55F969F54064628A63B9559E7C21C925165 # issuer 253 | trader account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 # holder 254 | 255 | # notice the issuer is down the 400 ETH stored as a bond in the option 256 | # and notice the only other change is the 100 mycoin payment from holder to issuer for ownership of the option 257 | # all other coin is returned untouched after the transaction 258 | 259 | # and see the option has changed owner 260 | trader tx options query --chain_id trader_chain_id $OPTION_ID 261 | 262 | # and now for the real trick, let's use this option 263 | trader tx options exercise --chain_id trader_chain_id --from key2.json --amount 2BTC --option $OPTION_ID 264 | 265 | # wait... it only works if you send the required amount 266 | trader tx options exercise --chain_id trader_chain_id --from key2.json --amount 4BTC --option $OPTION_ID 267 | 268 | # now, look at this, the issuer got the 4 BTC, the holder the 400 ETH 269 | # and we can even trade the rights to perform this operation :) 270 | trader account 1B1BE55F969F54064628A63B9559E7C21C925165 # issuer 271 | trader account 1DA7C74F9C219229FD54CC9F7386D5A3839F0090 # holder 272 | 273 | # and the option has now disappeared, so you can't use it again 274 | trader tx options query --chain_id trader_chain_id $OPTION_ID 275 | ``` 276 | 277 | This is just the start. There is also some methods for expiration and 278 | "disolving" the option if it will not be used. You could add a UI to enable 279 | market trading and just do the resolution on the blockchain. Or spend a couple 280 | days and build even more complex instruments, complete with unit tests and 281 | safety, so you don't go bankrupt from a bug. 282 | 283 | ## Attaching a GUI 284 | 285 | Coming soon! For now, see [the repository](https://github.com/tendermint/js-basecoin) 286 | -------------------------------------------------------------------------------- /trader/accountant.go: -------------------------------------------------------------------------------- 1 | package trader 2 | 3 | import ( 4 | "github.com/tendermint/basecoin/state" 5 | "github.com/tendermint/basecoin/types" 6 | ) 7 | 8 | // All concepts related to payments should go here 9 | type Accountant struct { 10 | store types.KVStore 11 | } 12 | 13 | func NewAccountant(store types.KVStore) Accountant { 14 | return Accountant{store} 15 | } 16 | 17 | func (a Accountant) GetAccount(addr []byte) *types.Account { 18 | return state.GetAccount(a.store, addr) 19 | } 20 | 21 | func (a Accountant) SetAccount(addr []byte, acc *types.Account) { 22 | state.SetAccount(a.store, addr, acc) 23 | } 24 | 25 | func (a Accountant) GetOrCreateAccount(addr []byte) *types.Account { 26 | acct := state.GetAccount(a.store, addr) 27 | if acct == nil { 28 | acct = &types.Account{} 29 | } 30 | return acct 31 | } 32 | 33 | func (a Accountant) Refund(ctx types.CallContext) { 34 | a.Pay(ctx.CallerAddress, ctx.Coins) 35 | } 36 | 37 | func (a Accountant) Pay(addr []byte, coins types.Coins) { 38 | acct := a.GetOrCreateAccount(addr) 39 | acct.Balance = acct.Balance.Plus(coins) 40 | a.SetAccount(addr, acct) 41 | } 42 | -------------------------------------------------------------------------------- /trader/cmd/trader/commands/escrow.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/tendermint/basecoin-examples/trader/plugins/escrow" 11 | "github.com/tendermint/basecoin-examples/trader/types" 12 | bcmd "github.com/tendermint/basecoin/cmd/commands" 13 | bc "github.com/tendermint/basecoin/types" 14 | wire "github.com/tendermint/go-wire" 15 | ) 16 | 17 | const EscrowName = "escrow" 18 | 19 | var ( 20 | //flags 21 | EscrowNodeFlag string 22 | EscrowRecvFlag string 23 | EscrowArbiterFlag string 24 | EscrowAddrFlag string 25 | EscrowExpireFlag uint64 26 | EscrowPayoutFlag bool 27 | 28 | //commands 29 | CmdEscrowTx = &cobra.Command{ 30 | Use: "escrow", 31 | Short: "Create and resolve escrows", 32 | } 33 | 34 | CmdEscrowCreateTx = &cobra.Command{ 35 | Use: "create", 36 | Short: "Create a new escrow by sending money", 37 | RunE: cmdEscrowCreateTx, 38 | } 39 | 40 | CmdEscrowResolveTx = &cobra.Command{ 41 | Use: "pay", 42 | Short: "Resolve the escrow by paying out of returning the money", 43 | RunE: cmdEscrowResolveTx, 44 | } 45 | 46 | CmdEscrowExpireTx = &cobra.Command{ 47 | Use: "expire", 48 | Short: "Call to expire the escrow if no action in a given time", 49 | RunE: cmdEscrowExpireTx, 50 | } 51 | 52 | CmdEscrowQuery = &cobra.Command{ 53 | Use: "query [address]", 54 | Short: "Return the contents of the given escrow", 55 | RunE: cmdEscrowQuery, 56 | } 57 | ) 58 | 59 | func init() { 60 | 61 | //register flags 62 | queryFlags := []bcmd.Flag2Register{ 63 | {&EscrowNodeFlag, "node", "tcp://localhost:46657", "Tendermint RPC address"}, 64 | } 65 | addrFlag := bcmd.Flag2Register{ 66 | &EscrowAddrFlag, "escrow", "", "The address of this escrow"} 67 | expireFlags := []bcmd.Flag2Register{ 68 | addrFlag, 69 | } 70 | resolveFlags := []bcmd.Flag2Register{ 71 | addrFlag, 72 | {&EscrowPayoutFlag, "abort-payout", false, "Set this flag if to return the money to the sender"}, 73 | } 74 | createTxFlags := []bcmd.Flag2Register{ 75 | {&EscrowRecvFlag, "recv", "", "Who is the intended recipient of the escrow"}, 76 | {&EscrowArbiterFlag, "arbiter", "", "Who is the arbiter of the escrow"}, 77 | {&EscrowExpireFlag, "expire", uint64(0), "The block height when the escrow expires"}, 78 | } 79 | bcmd.RegisterFlags(CmdEscrowQuery, queryFlags) 80 | bcmd.RegisterFlags(CmdEscrowExpireTx, expireFlags) 81 | bcmd.RegisterFlags(CmdEscrowResolveTx, resolveFlags) 82 | bcmd.RegisterFlags(CmdEscrowCreateTx, createTxFlags) 83 | 84 | //register subcommands of EscrowTxCmd 85 | CmdEscrowTx.AddCommand( 86 | CmdEscrowCreateTx, 87 | CmdEscrowResolveTx, 88 | CmdEscrowExpireTx, 89 | CmdEscrowQuery, 90 | ) 91 | 92 | //register with main tx command 93 | bcmd.RegisterTxSubcommand(CmdEscrowTx) 94 | bcmd.RegisterStartPlugin(EscrowName, 95 | func() bc.Plugin { return escrow.New(EscrowName) }) 96 | } 97 | 98 | func cmdEscrowCreateTx(cmd *cobra.Command, args []string) error { 99 | // convert destination address to bytes 100 | recv, err := hex.DecodeString(bcmd.StripHex(EscrowRecvFlag)) 101 | if err != nil { 102 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 103 | } 104 | 105 | // convert destination address to bytes 106 | arb, err := hex.DecodeString(bcmd.StripHex(EscrowArbiterFlag)) 107 | if err != nil { 108 | return errors.Errorf("Arbiter address is invalid hex: %v\n", err) 109 | } 110 | 111 | tx := types.CreateEscrowTx{ 112 | Recipient: recv, 113 | Arbiter: arb, 114 | Expiration: EscrowExpireFlag, 115 | } 116 | data := types.EscrowTxBytes(tx) 117 | return bcmd.AppTx(EscrowName, data) 118 | } 119 | 120 | func cmdEscrowResolveTx(cmd *cobra.Command, args []string) error { 121 | 122 | // convert destination address to bytes 123 | addr, err := hex.DecodeString(bcmd.StripHex(EscrowAddrFlag)) 124 | if err != nil { 125 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 126 | } 127 | 128 | tx := types.ResolveEscrowTx{ 129 | Escrow: addr, 130 | Payout: !EscrowPayoutFlag, 131 | } 132 | data := types.EscrowTxBytes(tx) 133 | return bcmd.AppTx(EscrowName, data) 134 | } 135 | 136 | func cmdEscrowExpireTx(cmd *cobra.Command, args []string) error { 137 | 138 | // convert destination address to bytes 139 | addr, err := hex.DecodeString(bcmd.StripHex(EscrowAddrFlag)) 140 | if err != nil { 141 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 142 | } 143 | 144 | tx := types.ExpireEscrowTx{ 145 | Escrow: addr, 146 | } 147 | data := types.EscrowTxBytes(tx) 148 | return bcmd.AppTx(EscrowName, data) 149 | } 150 | 151 | func cmdEscrowQuery(cmd *cobra.Command, args []string) error { 152 | if len(args) != 1 { 153 | return fmt.Errorf("account command requires an argument ([address])") //never stack trace 154 | } 155 | addrHex := bcmd.StripHex(args[0]) 156 | 157 | // convert destination address to bytes 158 | addr, err := hex.DecodeString(addrHex) 159 | if err != nil { 160 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 161 | } 162 | 163 | esc, err := getEscrow(EscrowNodeFlag, addr) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | fmt.Println(string(wire.JSONBytes(esc))) 169 | return nil 170 | } 171 | 172 | func getEscrow(tmAddr string, address []byte) (*types.EscrowData, error) { 173 | prefix := []byte(fmt.Sprintf("%s/", EscrowName)) 174 | key := append(prefix, address...) 175 | response, err := bcmd.Query(tmAddr, key) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | escrowBytes := response.Value 181 | 182 | if len(escrowBytes) == 0 { 183 | return nil, fmt.Errorf("Escrow bytes are empty for address: %X ", address) 184 | } 185 | esc, err := types.ParseEscrow(escrowBytes) 186 | if err != nil { 187 | return nil, fmt.Errorf("Error reading account %X error: %v", 188 | escrowBytes, err.Error()) 189 | } 190 | return &esc, nil 191 | } 192 | -------------------------------------------------------------------------------- /trader/cmd/trader/commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | bcmd "github.com/tendermint/basecoin/cmd/commands" 5 | ) 6 | 7 | func init() { 8 | //Change the GenesisJSON 9 | bcmd.GenesisJSON = `{ 10 | "app_hash": "", 11 | "chain_id": "trader_chain_id", 12 | "genesis_time": "0001-01-01T00:00:00.000Z", 13 | "validators": [ 14 | { 15 | "amount": 10, 16 | "name": "", 17 | "pub_key": { 18 | "type": "ed25519", 19 | "data":"7B90EA87E7DC0C7145C8C48C08992BE271C7234134343E8A8E8008E617DE7B30" 20 | } 21 | } 22 | ], 23 | "app_options": { 24 | "accounts": [{ 25 | "pub_key": { 26 | "type": "ed25519", 27 | "data": "619D3678599971ED29C7529DDD4DA537B97129893598A17C82E3AC9A8BA95279" 28 | }, 29 | "coins": [ 30 | { 31 | "denom": "ETH", 32 | "amount": 500 33 | }, 34 | { 35 | "denom": "mycoin", 36 | "amount": 1000 37 | } 38 | ] 39 | },{ 40 | "pub_key": { 41 | "type": "ed25519", 42 | "data": "352195DA90CB0B90C24295B90AEBA25A5A71BC61BAB2FE2387241D439698B7B8" 43 | }, 44 | "coins": [ 45 | { 46 | "denom": "BTC", 47 | "amount": 6 48 | }, 49 | { 50 | "denom": "mycoin", 51 | "amount": 500 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | }` 58 | 59 | } 60 | -------------------------------------------------------------------------------- /trader/cmd/trader/commands/options.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/spf13/cobra" 9 | 10 | bcmd "github.com/tendermint/basecoin/cmd/commands" 11 | bc "github.com/tendermint/basecoin/types" 12 | wire "github.com/tendermint/go-wire" 13 | 14 | "github.com/tendermint/basecoin-examples/trader/plugins/options" 15 | "github.com/tendermint/basecoin-examples/trader/types" 16 | ) 17 | 18 | const OptionName = "options" 19 | 20 | var ( 21 | //flags 22 | OptionNodeFlag string 23 | OptionAddrFlag string 24 | OptionExpireFlag uint64 25 | OptionSellToFlag string 26 | OptionTradeAmountFlag string 27 | OptionPriceAmountFlag string 28 | 29 | //commands 30 | CmdOptionsTx = &cobra.Command{ 31 | Use: "options", 32 | Short: "Create, trade, and exercise currency options", 33 | } 34 | 35 | CmdOptionsCreateTx = &cobra.Command{ 36 | Use: "create", 37 | Short: "Create a new option by sending money", 38 | RunE: cmdOptionCreateTx, 39 | } 40 | 41 | CmdOptionsSellTx = &cobra.Command{ 42 | Use: "sell", 43 | Short: "Offer to sell this option", 44 | RunE: cmdOptionSellTx, 45 | } 46 | 47 | CmdOptionsBuyTx = &cobra.Command{ 48 | Use: "buy", 49 | Short: "Attempt to buy this option", 50 | RunE: cmdOptionBuyTx, 51 | } 52 | 53 | CmdOptionsExerciseTx = &cobra.Command{ 54 | Use: "exercise", 55 | Short: "Exercise this option to trade currency at the given rate", 56 | RunE: cmdOptionExerciseTx, 57 | } 58 | 59 | CmdOptionsDissolveTx = &cobra.Command{ 60 | Use: "disolve", 61 | Short: "Attempt to disolve this option (if never sold, or already expired)", 62 | RunE: cmdOptionDissolveTx, 63 | } 64 | 65 | CmdOptionsQuery = &cobra.Command{ 66 | Use: "query [address]", 67 | Short: "Return the contents of the given option", 68 | RunE: cmdOptionQuery, 69 | } 70 | ) 71 | 72 | func init() { 73 | 74 | //Register Flags 75 | createTxFlags := []bcmd.Flag2Register{ 76 | {&OptionExpireFlag, "expire", uint64(0), "The block height when the option expires"}, 77 | {&OptionTradeAmountFlag, "trade", "", "Amount of coins to trade in format ,,..."}, 78 | } 79 | addrFlag := bcmd.Flag2Register{ 80 | &OptionAddrFlag, "option", "", "The address of this option"} 81 | sellTxFlags := []bcmd.Flag2Register{ 82 | addrFlag, 83 | {&OptionSellToFlag, "sellto", "", "Who to sell the options to (optional)"}, 84 | {&OptionPriceAmountFlag, "price", "", "Price to buy option in format ,,..."}, 85 | } 86 | buyTxFlags := []bcmd.Flag2Register{ 87 | addrFlag, 88 | } 89 | exerciseTxFlags := []bcmd.Flag2Register{ 90 | addrFlag, 91 | } 92 | dissolveTxFlags := []bcmd.Flag2Register{ 93 | addrFlag, 94 | } 95 | queryFlags := []bcmd.Flag2Register{ 96 | {&OptionNodeFlag, "node", "tcp://localhost:46657", "Tendermint RPC address"}, 97 | } 98 | bcmd.RegisterFlags(CmdOptionsCreateTx, createTxFlags) 99 | bcmd.RegisterFlags(CmdOptionsSellTx, sellTxFlags) 100 | bcmd.RegisterFlags(CmdOptionsBuyTx, buyTxFlags) 101 | bcmd.RegisterFlags(CmdOptionsExerciseTx, exerciseTxFlags) 102 | bcmd.RegisterFlags(CmdOptionsDissolveTx, dissolveTxFlags) 103 | bcmd.RegisterFlags(CmdOptionsQuery, queryFlags) 104 | 105 | //Register Subcommands 106 | CmdOptionsTx.AddCommand( 107 | CmdOptionsCreateTx, 108 | CmdOptionsSellTx, 109 | CmdOptionsBuyTx, 110 | CmdOptionsExerciseTx, 111 | CmdOptionsDissolveTx, 112 | CmdOptionsQuery, 113 | ) 114 | 115 | bcmd.RegisterTxSubcommand(CmdOptionsTx) 116 | bcmd.RegisterStartPlugin(OptionName, 117 | func() bc.Plugin { return options.New(OptionName) }) 118 | } 119 | 120 | func cmdOptionCreateTx(cmd *cobra.Command, args []string) error { 121 | 122 | tradeCoins, err := bc.ParseCoins(OptionTradeAmountFlag) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | tx := types.CreateOptionTx{ 128 | Expiration: OptionExpireFlag, 129 | Trade: tradeCoins, 130 | } 131 | data := types.OptionsTxBytes(tx) 132 | return bcmd.AppTx(OptionName, data) 133 | } 134 | 135 | func cmdOptionSellTx(cmd *cobra.Command, args []string) error { 136 | 137 | // convert destination address to bytes 138 | addr, err := hex.DecodeString(bcmd.StripHex(OptionAddrFlag)) 139 | if err != nil { 140 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 141 | } 142 | 143 | buyer, err := hex.DecodeString(bcmd.StripHex(OptionSellToFlag)) 144 | if err != nil { // this is optional, we can ignore it 145 | buyer = nil 146 | } 147 | 148 | priceCoins, err := bc.ParseCoins(OptionPriceAmountFlag) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | tx := types.SellOptionTx{ 154 | Addr: addr, 155 | NewHolder: buyer, 156 | Price: priceCoins, 157 | } 158 | data := types.OptionsTxBytes(tx) 159 | return bcmd.AppTx(OptionName, data) 160 | } 161 | 162 | func cmdOptionBuyTx(cmd *cobra.Command, args []string) error { 163 | 164 | // convert destination address to bytes 165 | addr, err := hex.DecodeString(bcmd.StripHex(OptionAddrFlag)) 166 | if err != nil { 167 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 168 | } 169 | 170 | tx := types.BuyOptionTx{ 171 | Addr: addr, 172 | } 173 | data := types.OptionsTxBytes(tx) 174 | return bcmd.AppTx(OptionName, data) 175 | } 176 | 177 | func cmdOptionExerciseTx(cmd *cobra.Command, args []string) error { 178 | 179 | // convert destination address to bytes 180 | addr, err := hex.DecodeString(bcmd.StripHex(OptionAddrFlag)) 181 | if err != nil { 182 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 183 | } 184 | 185 | tx := types.ExerciseOptionTx{ 186 | Addr: addr, 187 | } 188 | data := types.OptionsTxBytes(tx) 189 | return bcmd.AppTx(OptionName, data) 190 | } 191 | 192 | func cmdOptionDissolveTx(cmd *cobra.Command, args []string) error { 193 | 194 | // convert destination address to bytes 195 | addr, err := hex.DecodeString(bcmd.StripHex(OptionAddrFlag)) 196 | if err != nil { 197 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 198 | } 199 | 200 | tx := types.DisolveOptionTx{ 201 | Addr: addr, 202 | } 203 | data := types.OptionsTxBytes(tx) 204 | return bcmd.AppTx(OptionName, data) 205 | } 206 | 207 | func cmdOptionQuery(cmd *cobra.Command, args []string) error { 208 | if len(args) != 1 { 209 | return fmt.Errorf("account command requires an argument ([address])") //never stack trace 210 | } 211 | addrHex := bcmd.StripHex(args[0]) 212 | 213 | // convert destination address to bytes 214 | addr, err := hex.DecodeString(addrHex) 215 | if err != nil { 216 | return errors.Errorf("Recv address is invalid hex: %v\n", err) 217 | } 218 | 219 | opt, err := getOption(OptionNodeFlag, addr) 220 | if err != nil { 221 | return err 222 | } 223 | fmt.Println(string(wire.JSONBytes(opt))) 224 | return nil 225 | } 226 | 227 | func getOption(tmAddr string, address []byte) (*types.OptionData, error) { 228 | prefix := []byte(fmt.Sprintf("%s/", OptionName)) 229 | key := append(prefix, address...) 230 | response, err := bcmd.Query(tmAddr, key) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | optionBytes := response.Value 236 | 237 | if len(optionBytes) == 0 { 238 | return nil, fmt.Errorf("Option bytes are empty for address: %X ", address) 239 | } 240 | opt, err := types.ParseOptionData(optionBytes) 241 | if err != nil { 242 | return nil, fmt.Errorf("Error reading option %X error: %v", 243 | optionBytes, err.Error()) 244 | } 245 | return &opt, nil 246 | } 247 | -------------------------------------------------------------------------------- /trader/cmd/trader/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/tendermint/basecoin/cmd/commands" 9 | "github.com/tendermint/tmlibs/cli" 10 | 11 | // import _ to register escrow and options to apptx 12 | _ "github.com/tendermint/basecoin-examples/trader/cmd/trader/commands" 13 | ) 14 | 15 | func main() { 16 | 17 | var RootCmd = &cobra.Command{ 18 | Use: "trader", 19 | } 20 | 21 | RootCmd.AddCommand( 22 | commands.InitCmd, 23 | commands.StartCmd, 24 | commands.TxCmd, 25 | commands.QueryCmd, 26 | commands.KeyCmd, 27 | commands.VerifyCmd, 28 | commands.BlockCmd, 29 | commands.AccountCmd, 30 | commands.UnsafeResetAllCmd, 31 | commands.QuickVersionCmd("0.2.0"), 32 | ) 33 | 34 | cmd := cli.PrepareMainCmd(RootCmd, "TR", os.ExpandEnv("$HOME/.trader")) 35 | cmd.Execute() 36 | } 37 | -------------------------------------------------------------------------------- /trader/glide.lock: -------------------------------------------------------------------------------- 1 | hash: 997e4cc3339141ee01aa2adf656425a49ebf117e6ca9e81ba72b8f94fee3e86e 2 | updated: 2017-05-17T12:25:00.580569867+02:00 3 | imports: 4 | - name: github.com/bgentry/speakeasy 5 | version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd 6 | - name: github.com/btcsuite/btcd 7 | version: 1ae306021e323ae11c71ffb8546fbd9019e6cb6f 8 | subpackages: 9 | - btcec 10 | - name: github.com/BurntSushi/toml 11 | version: b26d9c308763d68093482582cea63d69be07a0f0 12 | - name: github.com/ebuchman/fail-test 13 | version: 95f809107225be108efcf10a3509e4ea6ceef3c4 14 | - name: github.com/fsnotify/fsnotify 15 | version: 4da3e2cfbabc9f751898f250b49f2439785783a1 16 | - name: github.com/go-kit/kit 17 | version: d67bb4c202e3b91377d1079b110a6c9ce23ab2f8 18 | subpackages: 19 | - log 20 | - log/level 21 | - log/term 22 | - name: github.com/go-logfmt/logfmt 23 | version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 24 | - name: github.com/go-playground/locales 25 | version: 1e5f1161c6416a5ff48840eb8724a394e48cc534 26 | subpackages: 27 | - currency 28 | - name: github.com/go-playground/universal-translator 29 | version: 71201497bace774495daed26a3874fd339e0b538 30 | - name: github.com/go-stack/stack 31 | version: 7a2f19628aabfe68f0766b59e74d6315f8347d22 32 | - name: github.com/golang/protobuf 33 | version: b50ceb1fa9818fa4d78b016c2d4ae025593a7ce3 34 | subpackages: 35 | - proto 36 | - ptypes/any 37 | - name: github.com/golang/snappy 38 | version: 553a641470496b2327abcac10b36396bd98e45c9 39 | - name: github.com/gorilla/context 40 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 41 | - name: github.com/gorilla/handlers 42 | version: 3a5767ca75ece5f7f1440b1d16975247f8d8b221 43 | - name: github.com/gorilla/mux 44 | version: 392c28fe23e1c45ddba891b0320b3b5df220beea 45 | - name: github.com/gorilla/websocket 46 | version: a91eba7f97777409bc2c443f5534d41dd20c5720 47 | - name: github.com/hashicorp/hcl 48 | version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca 49 | subpackages: 50 | - hcl/ast 51 | - hcl/parser 52 | - hcl/scanner 53 | - hcl/strconv 54 | - hcl/token 55 | - json/parser 56 | - json/scanner 57 | - json/token 58 | - name: github.com/inconshreveable/mousetrap 59 | version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 60 | - name: github.com/jmhodges/levigo 61 | version: c42d9e0ca023e2198120196f842701bb4c55d7b9 62 | - name: github.com/kr/logfmt 63 | version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 64 | - name: github.com/magiconair/properties 65 | version: 51463bfca2576e06c62a8504b5c0f06d61312647 66 | - name: github.com/mitchellh/mapstructure 67 | version: cc8532a8e9a55ea36402aa21efdf403a60d34096 68 | - name: github.com/pelletier/go-buffruneio 69 | version: c37440a7cf42ac63b919c752ca73a85067e05992 70 | - name: github.com/pelletier/go-toml 71 | version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a 72 | - name: github.com/pkg/errors 73 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 74 | - name: github.com/spf13/afero 75 | version: 9be650865eab0c12963d8753212f4f9c66cdcf12 76 | subpackages: 77 | - mem 78 | - name: github.com/spf13/cast 79 | version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 80 | - name: github.com/spf13/cobra 81 | version: 3454e0e28e69c1b8effa6b5123c8e4185e20d696 82 | - name: github.com/spf13/jwalterweatherman 83 | version: 8f07c835e5cc1450c082fe3a439cf87b0cbb2d99 84 | - name: github.com/spf13/pflag 85 | version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 86 | - name: github.com/spf13/viper 87 | version: 0967fc9aceab2ce9da34061253ac10fb99bba5b2 88 | - name: github.com/syndtr/goleveldb 89 | version: 8c81ea47d4c41a385645e133e15510fc6a2a74b4 90 | subpackages: 91 | - leveldb 92 | - leveldb/cache 93 | - leveldb/comparer 94 | - leveldb/errors 95 | - leveldb/filter 96 | - leveldb/iterator 97 | - leveldb/journal 98 | - leveldb/memdb 99 | - leveldb/opt 100 | - leveldb/storage 101 | - leveldb/table 102 | - leveldb/util 103 | - name: github.com/tendermint/abci 104 | version: 5dabeffb35c027d7087a12149685daa68989168b 105 | subpackages: 106 | - client 107 | - example/dummy 108 | - server 109 | - types 110 | - name: github.com/tendermint/basecoin 111 | version: bd62b21d6e2f7faa6dfeed62d5ac4b9bb3553031 112 | - name: github.com/tendermint/ed25519 113 | version: 1f52c6f8b8a5c7908aff4497c186af344b428925 114 | subpackages: 115 | - edwards25519 116 | - extra25519 117 | - name: github.com/tendermint/go-crypto 118 | version: 438b16f1f84ef002d7408ecd6fc3a3974cbc9559 119 | subpackages: 120 | - cmd 121 | - keys 122 | - keys/cryptostore 123 | - keys/server 124 | - keys/server/types 125 | - keys/storage/filestorage 126 | - name: github.com/tendermint/go-wire 127 | version: 97beaedf0f4dbc035309157c92be3b30cc6e5d74 128 | subpackages: 129 | - data 130 | - data/base58 131 | - name: github.com/tendermint/light-client 132 | version: 478876ca34b360df62f941d5e20cdd608fa0a466 133 | subpackages: 134 | - certifiers 135 | - certifiers/client 136 | - certifiers/files 137 | - commands 138 | - commands/proofs 139 | - commands/proxy 140 | - commands/seeds 141 | - commands/txs 142 | - proofs 143 | - name: github.com/tendermint/merkleeyes 144 | version: c722818b460381bc5b82e38c73ff6e22a9df624d 145 | subpackages: 146 | - app 147 | - client 148 | - iavl 149 | - name: github.com/tendermint/tendermint 150 | version: 11b5d11e9eec170e1d3dce165f0270d5c0759d69 151 | subpackages: 152 | - blockchain 153 | - config 154 | - consensus 155 | - mempool 156 | - node 157 | - p2p 158 | - p2p/upnp 159 | - proxy 160 | - rpc/client 161 | - rpc/core 162 | - rpc/core/types 163 | - rpc/grpc 164 | - rpc/lib 165 | - rpc/lib/client 166 | - rpc/lib/server 167 | - rpc/lib/types 168 | - state 169 | - state/txindex 170 | - state/txindex/kv 171 | - state/txindex/null 172 | - types 173 | - version 174 | - name: github.com/tendermint/tmlibs 175 | version: 8af1c70a8be17543eb33e9bfbbcdd8371e3201cc 176 | subpackages: 177 | - autofile 178 | - cli 179 | - clist 180 | - common 181 | - db 182 | - events 183 | - flowrate 184 | - log 185 | - logger 186 | - merkle 187 | - name: golang.org/x/crypto 188 | version: ab89591268e0c8b748cbe4047b00197516011af5 189 | subpackages: 190 | - curve25519 191 | - nacl/box 192 | - nacl/secretbox 193 | - openpgp/armor 194 | - openpgp/errors 195 | - poly1305 196 | - ripemd160 197 | - salsa20/salsa 198 | - name: golang.org/x/net 199 | version: c9b681d35165f1995d6f3034e61f8761d4b90c99 200 | subpackages: 201 | - context 202 | - http2 203 | - http2/hpack 204 | - idna 205 | - internal/timeseries 206 | - lex/httplex 207 | - trace 208 | - name: golang.org/x/sys 209 | version: 9c9d83fe39ed3fd2d9249fcf6b755891fff54b03 210 | subpackages: 211 | - unix 212 | - name: golang.org/x/text 213 | version: 470f45bf29f4147d6fbd7dfd0a02a848e49f5bf4 214 | subpackages: 215 | - secure/bidirule 216 | - transform 217 | - unicode/bidi 218 | - unicode/norm 219 | - name: google.golang.org/genproto 220 | version: 411e09b969b1170a9f0c467558eb4c4c110d9c77 221 | subpackages: 222 | - googleapis/rpc/status 223 | - name: google.golang.org/grpc 224 | version: a0c3e72252b6fbf4826bb143e450eb05588a9d6d 225 | subpackages: 226 | - codes 227 | - credentials 228 | - grpclb/grpc_lb_v1 229 | - grpclog 230 | - internal 231 | - keepalive 232 | - metadata 233 | - naming 234 | - peer 235 | - stats 236 | - status 237 | - tap 238 | - transport 239 | - name: gopkg.in/go-playground/validator.v9 240 | version: 6d8c18553ea1ac493d049edd6f102f52e618f085 241 | - name: gopkg.in/yaml.v2 242 | version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b 243 | testImports: 244 | - name: github.com/davecgh/go-spew 245 | version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 246 | subpackages: 247 | - spew 248 | - name: github.com/pmezard/go-difflib 249 | version: d8ed2627bdf02c080bf22230dbb337003b7aba2d 250 | subpackages: 251 | - difflib 252 | - name: github.com/stretchr/testify 253 | version: 4d4bfba8f1d1027c4fdbe371823030df51419987 254 | subpackages: 255 | - assert 256 | - require 257 | -------------------------------------------------------------------------------- /trader/glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/tendermint/basecoin-examples/trader 2 | import: 3 | - package: github.com/gorilla/websocket 4 | - package: github.com/pkg/errors 5 | - package: github.com/spf13/cobra 6 | - package: github.com/spf13/pflag 7 | - package: github.com/spf13/viper 8 | - package: github.com/tendermint/abci 9 | version: develop 10 | subpackages: 11 | - server 12 | - types 13 | - package: github.com/tendermint/basecoin 14 | version: develop 15 | - package: github.com/tendermint/go-crypto 16 | version: develop 17 | subpackages: 18 | - cmd 19 | - keys 20 | - package: github.com/tendermint/go-wire 21 | version: develop 22 | subpackages: 23 | - data 24 | - package: github.com/tendermint/light-client 25 | version: develop 26 | subpackages: 27 | - commands 28 | - commands/proofs 29 | - commands/seeds 30 | - commands/txs 31 | - proofs 32 | - package: github.com/tendermint/merkleeyes 33 | version: develop 34 | subpackages: 35 | - client 36 | - iavl 37 | - package: github.com/tendermint/tendermint 38 | version: develop 39 | subpackages: 40 | - config 41 | - node 42 | - proxy 43 | - rpc/client 44 | - rpc/core/types 45 | - rpc/lib/client 46 | - rpc/lib/types 47 | - types 48 | - package: github.com/tendermint/tmlibs 49 | version: develop 50 | subpackages: 51 | - cli 52 | - common 53 | - events 54 | - log 55 | - logger 56 | testImport: 57 | - package: github.com/stretchr/testify 58 | subpackages: 59 | - assert 60 | - require 61 | -------------------------------------------------------------------------------- /trader/plugins/escrow/plugin.go: -------------------------------------------------------------------------------- 1 | package escrow 2 | 3 | import ( 4 | "fmt" 5 | 6 | abci "github.com/tendermint/abci/types" 7 | "github.com/tendermint/basecoin-examples/trader" 8 | "github.com/tendermint/basecoin-examples/trader/types" 9 | bc "github.com/tendermint/basecoin/types" 10 | ) 11 | 12 | // Plugin is a plugin, storing all state prefixed with it's unique name 13 | type Plugin struct { 14 | name string 15 | height uint64 16 | } 17 | 18 | func New(name string) *Plugin { 19 | return &Plugin{name: name} 20 | } 21 | 22 | func (p Plugin) Name() string { 23 | return p.name 24 | } 25 | 26 | // SetOption not supported by Plugin 27 | func (p Plugin) SetOption(store bc.KVStore, key string, value string) (log string) { 28 | return fmt.Sprintf("Unknown key: %s", key) 29 | } 30 | 31 | // prefix let's us store all our info in a separate name-space 32 | func (p Plugin) prefix(store bc.KVStore) bc.KVStore { 33 | key := fmt.Sprintf("%s/", p.name) 34 | return trader.PrefixStore(store, []byte(key)) 35 | } 36 | 37 | // parse out which tx we use and then run it 38 | func (p Plugin) RunTx(store bc.KVStore, ctx bc.CallContext, txBytes []byte) (res abci.Result) { 39 | tx, err := types.ParseEscrowTx(txBytes) 40 | if err != nil { 41 | trader.NewAccountant(store).Refund(ctx) 42 | return abci.ErrEncodingError 43 | } 44 | 45 | // the tx only can mess with the escrow data due to the prefix 46 | res = p.Exec(store, ctx, tx) 47 | return res 48 | } 49 | 50 | func (p Plugin) Exec(store bc.KVStore, ctx bc.CallContext, tx types.EscrowTx) abci.Result { 51 | accts := trader.NewAccountant(store) 52 | pstore := p.prefix(store) 53 | 54 | switch t := tx.(type) { 55 | case types.CreateEscrowTx: 56 | return p.runCreateEscrow(pstore, accts, ctx, t) 57 | case types.ResolveEscrowTx: 58 | return p.runResolveEscrow(pstore, accts, ctx, t) 59 | case types.ExpireEscrowTx: 60 | return p.runExpireEscrow(pstore, accts, ctx, t) 61 | default: 62 | return abci.ErrUnknownRequest 63 | } 64 | } 65 | 66 | // placeholder empty to fulfill interface 67 | func (p *Plugin) InitChain(store bc.KVStore, vals []*abci.Validator) {} 68 | 69 | // track the height for expiration 70 | func (p *Plugin) BeginBlock(store bc.KVStore, hash []byte, header *abci.Header) { 71 | p.height = header.Height 72 | } 73 | 74 | func (p *Plugin) EndBlock(store bc.KVStore, height uint64) abci.ResponseEndBlock { 75 | p.height = height + 1 76 | return abci.ResponseEndBlock{} 77 | } 78 | 79 | func (p *Plugin) assertPlugin() bc.Plugin { 80 | return p 81 | } 82 | -------------------------------------------------------------------------------- /trader/plugins/escrow/tx.go: -------------------------------------------------------------------------------- 1 | package escrow 2 | 3 | import ( 4 | "bytes" 5 | 6 | abci "github.com/tendermint/abci/types" 7 | "github.com/tendermint/basecoin-examples/trader" 8 | "github.com/tendermint/basecoin-examples/trader/types" 9 | 10 | bc "github.com/tendermint/basecoin/types" 11 | ) 12 | 13 | func (p Plugin) runCreateEscrow(store bc.KVStore, 14 | accts trader.Accountant, 15 | ctx bc.CallContext, 16 | tx types.CreateEscrowTx) abci.Result { 17 | // TODO: require fees? limit the size of the escrow? 18 | 19 | data := types.EscrowData{ 20 | Sender: ctx.CallerAddress, 21 | Recipient: tx.Recipient, 22 | Arbiter: tx.Arbiter, 23 | Expiration: tx.Expiration, 24 | Amount: ctx.Coins, 25 | } 26 | // make sure all settings are valid, if not abort and return money 27 | if data.IsExpired(p.height) { 28 | accts.Refund(ctx) 29 | return abci.NewError(abci.CodeType_BaseInvalidInput, "Escrow already expired") 30 | } 31 | if len(data.Recipient) != 20 { 32 | accts.Refund(ctx) 33 | return abci.ErrBaseInvalidInput.AppendLog("Invalid recipient address") 34 | } 35 | if len(data.Arbiter) != 20 { 36 | accts.Refund(ctx) 37 | return abci.ErrBaseInvalidInput.AppendLog("Invalid arbiter address") 38 | } 39 | 40 | // create the escrow contract 41 | addr := data.Address() 42 | store.Set(addr, data.Bytes()) 43 | return abci.NewResultOK(addr, "Created Escrow") 44 | } 45 | 46 | func (p Plugin) runResolveEscrow(store bc.KVStore, 47 | accts trader.Accountant, 48 | ctx bc.CallContext, 49 | tx types.ResolveEscrowTx) abci.Result { 50 | // first load the data 51 | data := store.Get(tx.Escrow) 52 | if len(data) == 0 { // nil and []byte{} 53 | accts.Refund(ctx) 54 | return abci.ErrBaseUnknownAddress 55 | } 56 | 57 | esc, err := types.ParseEscrow(data) 58 | if err != nil { 59 | accts.Refund(ctx) 60 | return abci.NewError(abci.CodeType_BaseEncodingError, "Cannot parse data at location") 61 | } 62 | 63 | // only the Arbiter can resolve 64 | if !bytes.Equal(ctx.CallerAddress, esc.Arbiter) { 65 | accts.Refund(ctx) 66 | return abci.ErrUnauthorized 67 | } 68 | 69 | // Okay, now let's resolve this transaction! 70 | if tx.Payout { 71 | accts.Pay(esc.Recipient, esc.Amount) 72 | } else { 73 | accts.Pay(esc.Sender, esc.Amount) 74 | } 75 | 76 | // wipe out the escrow and return the payment 77 | store.Set(tx.Escrow, nil) 78 | return abci.OK.AppendLog("Escrow settled") 79 | } 80 | 81 | func (p Plugin) runExpireEscrow(store bc.KVStore, 82 | accts trader.Accountant, 83 | ctx bc.CallContext, 84 | tx types.ExpireEscrowTx) abci.Result { 85 | // first load the data 86 | data := store.Get(tx.Escrow) 87 | if len(data) == 0 { // nil and []byte{} 88 | accts.Refund(ctx) 89 | return abci.ErrBaseUnknownAddress 90 | } 91 | 92 | esc, err := types.ParseEscrow(data) 93 | if err != nil { 94 | accts.Refund(ctx) 95 | return abci.NewError(abci.CodeType_BaseEncodingError, "Cannot parse data at location") 96 | } 97 | 98 | // only resolve if expired 99 | if !esc.IsExpired(p.height) { 100 | accts.Refund(ctx) 101 | return abci.ErrUnauthorized 102 | } 103 | 104 | // wipe out the escrow and return the payment to sender 105 | accts.Pay(esc.Sender, esc.Amount) 106 | store.Set(tx.Escrow, nil) 107 | return abci.OK.AppendLog("Escrow expired") 108 | } 109 | -------------------------------------------------------------------------------- /trader/plugins/escrow/tx_test.go: -------------------------------------------------------------------------------- 1 | package escrow 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/tendermint/basecoin-examples/trader" 8 | "github.com/tendermint/basecoin-examples/trader/types" 9 | bc "github.com/tendermint/basecoin/types" 10 | cmn "github.com/tendermint/tmlibs/common" 11 | ) 12 | 13 | func TestTransactions(t *testing.T) { 14 | assert := assert.New(t) 15 | store := bc.NewMemKVStore() 16 | sender, recv, arb := cmn.RandBytes(20), cmn.RandBytes(20), cmn.RandBytes(20) 17 | 18 | money := bc.Coins{ 19 | { 20 | Amount: 1000, 21 | Denom: "ATOM", 22 | }, 23 | { 24 | Amount: 65, 25 | Denom: "BTC", 26 | }, 27 | } 28 | fees := bc.Coins{{ 29 | Amount: 3, 30 | Denom: "ATOM", 31 | }} 32 | 33 | tx := types.CreateEscrowTx{ 34 | Recipient: recv, 35 | Arbiter: arb, 36 | Expiration: 100, 37 | } 38 | ctx := bc.CallContext{ 39 | CallerAddress: sender, 40 | Coins: money, 41 | } 42 | plugin := Plugin{ 43 | height: 123, 44 | name: "escrow", 45 | } 46 | pstore := plugin.prefix(store) 47 | accts := trader.NewAccountant(store) 48 | 49 | // error if already expired 50 | as := accts.GetOrCreateAccount(sender).Balance 51 | res := plugin.Exec(store, ctx, tx) 52 | assert.True(res.IsErr()) 53 | assert.Equal(as.Plus(ctx.Coins), accts.GetAccount(sender).Balance) 54 | 55 | // we create the tx 56 | tx.Expiration = 500 57 | as = accts.GetOrCreateAccount(sender).Balance 58 | res = plugin.Exec(store, ctx, tx) 59 | assert.False(res.IsErr()) 60 | assert.Equal(as, accts.GetAccount(sender).Balance) 61 | addr := res.Data 62 | assert.NotEmpty(addr) 63 | 64 | // load the escrow data and make sure it is happy 65 | esc, err := types.LoadEscrow(pstore, addr) 66 | if assert.Nil(err) { 67 | assert.EqualValues(addr, esc.Address()) 68 | assert.Equal(sender, esc.Sender) 69 | assert.Equal(recv, esc.Recipient) 70 | assert.Equal(arb, esc.Arbiter) 71 | assert.Equal(uint64(500), esc.Expiration) 72 | assert.Equal(money, esc.Amount) 73 | } 74 | 75 | // try to complete it as the wrong person 76 | ctx = bc.CallContext{ 77 | CallerAddress: sender, 78 | Coins: fees, 79 | } 80 | rtx := types.ResolveEscrowTx{ 81 | Escrow: addr, 82 | Payout: false, 83 | } 84 | as = accts.GetAccount(sender).Balance 85 | res = plugin.Exec(store, ctx, rtx) 86 | assert.True(res.IsErr()) 87 | assert.Equal(as.Plus(ctx.Coins), accts.GetAccount(sender).Balance) 88 | 89 | // and the wrong locations 90 | ab := accts.GetOrCreateAccount(arb).Balance 91 | ctx = bc.CallContext{ 92 | CallerAddress: arb, 93 | Coins: fees, 94 | } 95 | rtx = types.ResolveEscrowTx{ 96 | Escrow: cmn.RandBytes(20), 97 | } 98 | res = plugin.Exec(store, ctx, rtx) 99 | assert.True(res.IsErr()) 100 | assert.Equal(ab.Plus(ctx.Coins), accts.GetAccount(arb).Balance) 101 | 102 | // try to expire, fails 103 | etx := types.ExpireEscrowTx{ 104 | Escrow: addr, 105 | } 106 | res = plugin.Exec(store, ctx, etx) 107 | assert.True(res.IsErr()) 108 | // assert.Equal(ctx.CallerAddress, pay.Addr) 109 | 110 | // complete as arbiter - yes! 111 | ar := accts.GetOrCreateAccount(recv).Balance 112 | rtx = types.ResolveEscrowTx{ 113 | Escrow: addr, 114 | Payout: true, 115 | } 116 | res = plugin.Exec(store, ctx, rtx) 117 | assert.False(res.IsErr()) 118 | assert.Equal(ar.Plus(money), accts.GetAccount(recv).Balance) 119 | 120 | // complete 2nd time -> error 121 | res = plugin.Exec(store, ctx, rtx) 122 | assert.True(res.IsErr()) 123 | 124 | // no data to be seen 125 | esc, err = types.LoadEscrow(store, addr) 126 | assert.NotNil(err) 127 | } 128 | -------------------------------------------------------------------------------- /trader/plugins/options/plugin.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | 6 | abci "github.com/tendermint/abci/types" 7 | "github.com/tendermint/basecoin-examples/trader" 8 | "github.com/tendermint/basecoin-examples/trader/types" 9 | bc "github.com/tendermint/basecoin/types" 10 | ) 11 | 12 | // Plugin is a options plugin, storing all state prefixed with it's unique name 13 | type Plugin struct { 14 | name string 15 | height uint64 16 | } 17 | 18 | func New(name string) *Plugin { 19 | return &Plugin{name: name} 20 | } 21 | 22 | func (p Plugin) Name() string { 23 | return p.name 24 | } 25 | 26 | // SetOption not supported by Plugin 27 | func (p Plugin) SetOption(store bc.KVStore, key string, value string) (log string) { 28 | return fmt.Sprintf("Unknown key: %s", key) 29 | } 30 | 31 | // prefix let's us store all our info in a separate name-space 32 | func (p Plugin) prefix(store bc.KVStore) bc.KVStore { 33 | key := fmt.Sprintf("%s/", p.name) 34 | return trader.PrefixStore(store, []byte(key)) 35 | } 36 | 37 | // parse out which tx we use and then run it 38 | func (p Plugin) RunTx(store bc.KVStore, ctx bc.CallContext, txBytes []byte) (res abci.Result) { 39 | tx, err := types.ParseOptionsTx(txBytes) 40 | if err != nil { 41 | trader.NewAccountant(store).Refund(ctx) // pay back unused money 42 | return abci.ErrEncodingError 43 | } 44 | 45 | // the tx only can mess with the option data due to the prefix 46 | return p.Exec(store, ctx, tx) 47 | } 48 | 49 | func (p Plugin) Exec(store bc.KVStore, ctx bc.CallContext, tx types.OptionsTx) abci.Result { 50 | accts := trader.NewAccountant(store) 51 | pstore := p.prefix(store) 52 | 53 | switch t := tx.(type) { 54 | case types.CreateOptionTx: 55 | return p.runCreateOption(pstore, accts, ctx, t) 56 | case types.SellOptionTx: 57 | return p.runSellOption(pstore, accts, ctx, t) 58 | case types.BuyOptionTx: 59 | return p.runBuyOption(pstore, accts, ctx, t) 60 | case types.ExerciseOptionTx: 61 | return p.runExerciseOption(pstore, accts, ctx, t) 62 | case types.DisolveOptionTx: 63 | return p.runDisolveOption(pstore, accts, ctx, t) 64 | default: 65 | return abci.ErrUnknownRequest 66 | } 67 | } 68 | 69 | // placeholder empty to fulfill interface 70 | func (p *Plugin) InitChain(store bc.KVStore, vals []*abci.Validator) {} 71 | 72 | // track the height for expiration 73 | func (p *Plugin) BeginBlock(store bc.KVStore, hash []byte, header *abci.Header) { 74 | p.height = header.Height 75 | } 76 | 77 | func (p *Plugin) EndBlock(store bc.KVStore, height uint64) abci.ResponseEndBlock { 78 | p.height = height + 1 79 | return abci.ResponseEndBlock{} 80 | } 81 | 82 | func (p *Plugin) assertPlugin() bc.Plugin { 83 | return p 84 | } 85 | -------------------------------------------------------------------------------- /trader/plugins/options/tx.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | 6 | abci "github.com/tendermint/abci/types" 7 | "github.com/tendermint/basecoin-examples/trader" 8 | "github.com/tendermint/basecoin-examples/trader/types" 9 | 10 | bc "github.com/tendermint/basecoin/types" 11 | ) 12 | 13 | func (p Plugin) runCreateOption(store bc.KVStore, 14 | accts trader.Accountant, 15 | ctx bc.CallContext, 16 | tx types.CreateOptionTx) abci.Result { 17 | 18 | issue := types.OptionIssue{ 19 | Issuer: ctx.CallerAddress, 20 | Serial: ctx.CallerAccount.Sequence, 21 | Expiration: tx.Expiration, 22 | Bond: ctx.Coins, 23 | Trade: tx.Trade, 24 | } 25 | data := types.OptionData{ 26 | OptionIssue: issue, 27 | OptionHolder: types.OptionHolder{ 28 | Holder: ctx.CallerAddress, 29 | }, 30 | } 31 | if data.IsExpired(p.height) { 32 | accts.Refund(ctx) 33 | return abci.ErrEncodingError.AppendLog("Already expired") 34 | } 35 | types.StoreOptionData(store, data) 36 | addr := data.Address() 37 | return abci.NewResultOK(addr, fmt.Sprintf("new option: %X", addr)) 38 | } 39 | 40 | func (p Plugin) runSellOption(store bc.KVStore, 41 | accts trader.Accountant, 42 | ctx bc.CallContext, 43 | tx types.SellOptionTx) abci.Result { 44 | 45 | // always return money sent, no need 46 | accts.Refund(ctx) 47 | 48 | data, err := types.LoadOptionData(store, tx.Addr) 49 | if err != nil { 50 | return abci.ErrEncodingError.AppendLog(err.Error()) 51 | } 52 | 53 | // make sure we can do this 54 | if !data.CanSell(ctx.CallerAddress) { 55 | return abci.ErrUnauthorized.AppendLog("Not option holder") 56 | } 57 | 58 | data.NewHolder = tx.NewHolder 59 | data.Price = tx.Price 60 | types.StoreOptionData(store, data) 61 | return abci.OK 62 | } 63 | 64 | func (p Plugin) runBuyOption(store bc.KVStore, 65 | accts trader.Accountant, 66 | ctx bc.CallContext, 67 | tx types.BuyOptionTx) abci.Result { 68 | 69 | data, err := types.LoadOptionData(store, tx.Addr) 70 | if err != nil { 71 | accts.Refund(ctx) 72 | return abci.ErrEncodingError.AppendLog(err.Error()) 73 | } 74 | 75 | // make sure we can do this 76 | if !data.CanBuy(ctx.CallerAddress) { 77 | accts.Refund(ctx) 78 | return abci.ErrUnauthorized.AppendLog("Can't buy this option") 79 | } 80 | 81 | // make sure there is enough money to buy 82 | remain := ctx.Coins.Minus(data.Price) 83 | if !remain.IsNonnegative() { 84 | accts.Refund(ctx) 85 | return abci.ErrInsufficientFunds.AppendLog("Must pay more for the option") 86 | } 87 | 88 | // send the money to the seller 89 | accts.Pay(data.Holder, data.Price) 90 | // transfer ownership 91 | data.Holder = ctx.CallerAddress 92 | data.NewHolder = nil 93 | data.Price = nil 94 | types.StoreOptionData(store, data) 95 | // and refund any overpayment 96 | accts.Pay(ctx.CallerAddress, remain) 97 | 98 | return abci.OK 99 | } 100 | 101 | func (p Plugin) runExerciseOption(store bc.KVStore, 102 | accts trader.Accountant, 103 | ctx bc.CallContext, 104 | tx types.ExerciseOptionTx) abci.Result { 105 | 106 | data, err := types.LoadOptionData(store, tx.Addr) 107 | if err != nil { 108 | accts.Refund(ctx) 109 | return abci.ErrEncodingError.AppendLog(err.Error()) 110 | } 111 | 112 | // make sure we can do this 113 | if !data.CanExercise(ctx.CallerAddress, p.height) { 114 | accts.Refund(ctx) 115 | return abci.ErrUnauthorized.AppendLog("Can't exercise this option") 116 | } 117 | 118 | // make sure there is enough money to trade 119 | remain := ctx.Coins.Minus(data.Trade) 120 | if !remain.IsNonnegative() { 121 | accts.Refund(ctx) 122 | return abci.ErrInsufficientFunds.AppendLog("Option requires higher trade value") 123 | } 124 | 125 | // pay back caller over-payment and the bond value 126 | accts.Pay(ctx.CallerAddress, remain) 127 | accts.Pay(ctx.CallerAddress, data.Bond) 128 | // the trade value goes to the original issuer 129 | accts.Pay(data.Issuer, data.Trade) 130 | 131 | // and remove this option from history 132 | types.DeleteOptionData(store, data) 133 | 134 | return abci.OK 135 | } 136 | 137 | func (p Plugin) runDisolveOption(store bc.KVStore, 138 | accts trader.Accountant, 139 | ctx bc.CallContext, 140 | tx types.DisolveOptionTx) abci.Result { 141 | 142 | // no need for payments, always return 143 | accts.Refund(ctx) 144 | 145 | data, err := types.LoadOptionData(store, tx.Addr) 146 | if err != nil { 147 | return abci.ErrEncodingError.AppendLog(err.Error()) 148 | } 149 | 150 | // make sure we can do this 151 | if !data.CanDissolve(ctx.CallerAddress, p.height) { 152 | return abci.ErrUnauthorized.AppendLog("Can't exercise this option") 153 | } 154 | 155 | // return bond to the issue 156 | accts.Pay(data.Issuer, data.Bond) 157 | // and remove this option from history 158 | types.DeleteOptionData(store, data) 159 | 160 | return abci.OK 161 | } 162 | -------------------------------------------------------------------------------- /trader/plugins/options/tx_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/tendermint/basecoin-examples/trader" 8 | "github.com/tendermint/basecoin-examples/trader/types" 9 | bc "github.com/tendermint/basecoin/types" 10 | cmn "github.com/tendermint/tmlibs/common" 11 | ) 12 | 13 | func TestBasicFlow(t *testing.T) { 14 | assert := assert.New(t) 15 | 16 | store := bc.NewMemKVStore() 17 | plugin := Plugin{ 18 | height: 200, 19 | name: "options", 20 | } 21 | pstore := plugin.prefix(store) 22 | accts := trader.NewAccountant(store) 23 | 24 | a, b, c := cmn.RandBytes(20), cmn.RandBytes(20), cmn.RandBytes(20) 25 | bond := bc.Coins{{Amount: 1000, Denom: "ATOM"}} 26 | trade := bc.Coins{{Amount: 5, Denom: "BTC"}} 27 | price := bc.Coins{{Amount: 10, Denom: "ETH"}} 28 | low := bc.Coins{{Amount: 8, Denom: "ETH"}} 29 | 30 | tx := types.CreateOptionTx{ 31 | Expiration: 100, 32 | Trade: trade, 33 | } 34 | ctx := bc.CallContext{ 35 | CallerAddress: a, 36 | Coins: bond, 37 | CallerAccount: &bc.Account{ 38 | Sequence: 20, 39 | }, 40 | } 41 | // rejected as already expired 42 | res := plugin.Exec(store, ctx, tx) 43 | assert.True(res.IsErr()) 44 | // acccept proper 45 | plugin.height = 50 46 | res = plugin.Exec(store, ctx, tx) 47 | assert.True(res.IsOK()) 48 | addr := res.Data 49 | assert.NotEmpty(addr) 50 | 51 | // let's see the bond is set properly 52 | data, err := types.LoadOptionData(pstore, addr) 53 | assert.Nil(err) 54 | assert.EqualValues(addr, data.Address()) 55 | assert.Equal(bond, data.Bond) 56 | assert.Equal(20, data.Serial) 57 | assert.Equal(a, data.Issuer) 58 | assert.Equal(a, data.Holder) 59 | 60 | // no one can buy it right now 61 | tx2 := types.BuyOptionTx{ 62 | Addr: addr, 63 | } 64 | ctxb := bc.CallContext{ 65 | CallerAddress: b, 66 | Coins: price, 67 | } 68 | // make sure money returned from failed purchase 69 | ab := accts.GetOrCreateAccount(b) 70 | assert.True(ab.Balance.IsZero()) 71 | res = plugin.Exec(store, ctxb, tx2) 72 | assert.True(res.IsErr()) 73 | ab = accts.GetOrCreateAccount(b) 74 | assert.False(ab.Balance.IsZero()) 75 | assert.Equal(price, ab.Balance) 76 | 77 | // let us place it for sale.... 78 | tx3 := types.SellOptionTx{ 79 | Addr: addr, 80 | Price: price, 81 | NewHolder: b, 82 | } 83 | ctx.Coins = low 84 | 85 | // make sure someone else cannot sell 86 | res = plugin.Exec(store, ctxb, tx3) 87 | assert.False(res.IsOK()) 88 | 89 | // make sure the sell offer succeeds and money is refunded 90 | foo := accts.GetOrCreateAccount(a).Balance 91 | res = plugin.Exec(store, ctx, tx3) 92 | assert.True(res.IsOK()) 93 | aa := accts.GetOrCreateAccount(a) 94 | assert.False(aa.Balance.IsZero()) 95 | // make sure increased by low during the call 96 | assert.Equal(low, aa.Balance.Minus(foo)) 97 | 98 | // now, we can buy it, but only b 99 | ctxc := bc.CallContext{ 100 | CallerAddress: c, 101 | Coins: price, 102 | } 103 | // c is not authorized 104 | res = plugin.Exec(store, ctxc, tx2) 105 | assert.False(res.IsOK()) 106 | // but b can go shopping! 107 | res = plugin.Exec(store, ctxb, tx2) 108 | assert.True(res.IsOK()) 109 | 110 | // finally, our happy b (not c) can make the final trade 111 | tx4 := types.ExerciseOptionTx{ 112 | Addr: addr, 113 | } 114 | // c is not authorized 115 | res = plugin.Exec(store, ctxc, tx4) 116 | assert.False(res.IsOK(), res.Log) 117 | ctxbl := bc.CallContext{ 118 | CallerAddress: b, 119 | Coins: low, 120 | } 121 | // b doesn't pay enought is not authorized 122 | res = plugin.Exec(store, ctxbl, tx4) 123 | assert.False(res.IsOK()) 124 | // now we pay enough 125 | ctxb = bc.CallContext{ 126 | CallerAddress: b, 127 | Coins: trade, 128 | } 129 | res = plugin.Exec(store, ctxb, tx4) 130 | assert.True(res.IsOK(), res.Log) 131 | 132 | // now, let's make sure the option is gone 133 | data, err = types.LoadOptionData(pstore, addr) 134 | assert.NotNil(err) 135 | 136 | // and the money is in everyone's account 137 | aa = accts.GetAccount(a) 138 | assert.False(aa.Balance.IsZero()) 139 | assert.True(aa.Balance.IsGTE(trade)) // a got the trade 140 | ab = accts.GetAccount(b) 141 | assert.False(ab.Balance.IsZero()) 142 | assert.True(ab.Balance.IsGTE(bond)) // b got the bond 143 | } 144 | -------------------------------------------------------------------------------- /trader/prefix_store.go: -------------------------------------------------------------------------------- 1 | package trader 2 | 3 | import "github.com/tendermint/basecoin/types" 4 | 5 | type prefixedStore struct { 6 | store types.KVStore 7 | prefix []byte 8 | } 9 | 10 | func PrefixStore(store types.KVStore, prefix []byte) types.KVStore { 11 | return prefixedStore{ 12 | store: store, 13 | prefix: prefix, 14 | } 15 | } 16 | 17 | func (p prefixedStore) Set(key, value []byte) { 18 | pkey := append(p.prefix, key...) 19 | p.store.Set(pkey, value) 20 | } 21 | 22 | func (p prefixedStore) Get(key []byte) []byte { 23 | pkey := append(p.prefix, key...) 24 | return p.store.Get(pkey) 25 | } 26 | -------------------------------------------------------------------------------- /trader/types/escrow.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tendermint/basecoin/types" 7 | wire "github.com/tendermint/go-wire" 8 | "golang.org/x/crypto/ripemd160" 9 | ) 10 | 11 | func init() { 12 | // register tx implementations with gowire 13 | wire.RegisterInterface( 14 | escrowwrap{}, 15 | wire.ConcreteType{O: CreateEscrowTx{}, Byte: 0x01}, 16 | wire.ConcreteType{O: ResolveEscrowTx{}, Byte: 0x02}, 17 | wire.ConcreteType{O: ExpireEscrowTx{}, Byte: 0x03}, 18 | ) 19 | } 20 | 21 | type EscrowTx interface{} 22 | 23 | type escrowwrap struct { 24 | EscrowTx 25 | } 26 | 27 | func ParseEscrowTx(data []byte) (EscrowTx, error) { 28 | holder := escrowwrap{} 29 | err := wire.ReadBinaryBytes(data, &holder) 30 | return holder.EscrowTx, err 31 | } 32 | 33 | func EscrowTxBytes(tx EscrowTx) []byte { 34 | return wire.BinaryBytes(escrowwrap{tx}) 35 | } 36 | 37 | // CreateEscrowTx is used to create an escrow in the first place 38 | type CreateEscrowTx struct { 39 | Recipient []byte 40 | Arbiter []byte 41 | Expiration uint64 // height when the offer expires 42 | // Sender and Amount come from the basecoin context 43 | } 44 | 45 | // ResolveEscrowTx must be signed by the Arbiter and resolves the escrow 46 | // by sending the money to Sender or Recipient as specified 47 | type ResolveEscrowTx struct { 48 | Escrow []byte 49 | Payout bool // if true, to Recipient, else back to Sender 50 | } 51 | 52 | // ExpireEscrowTx can be signed by anyone, and only succeeds if the 53 | // Expiration height has passed. All coins go back to the Sender 54 | // (Intended to be used by the sender to recover old payments) 55 | type ExpireEscrowTx struct { 56 | Escrow []byte 57 | } 58 | 59 | // EscrowData is our principal data structure in the db 60 | type EscrowData struct { 61 | Sender []byte 62 | Recipient []byte 63 | Arbiter []byte 64 | Expiration uint64 // height when the offer expires (0 = never) 65 | Amount types.Coins 66 | } 67 | 68 | func (d EscrowData) IsExpired(h uint64) bool { 69 | return (d.Expiration != 0 && h > d.Expiration) 70 | } 71 | 72 | // Address is the ripemd160 hash of the escrow contents, which is constant 73 | func (d EscrowData) Address() []byte { 74 | hasher := ripemd160.New() 75 | hasher.Write(d.Bytes()) 76 | return hasher.Sum(nil) 77 | } 78 | 79 | func (d EscrowData) Bytes() []byte { 80 | return wire.BinaryBytes(d) 81 | } 82 | 83 | func ParseEscrow(data []byte) (EscrowData, error) { 84 | d := EscrowData{} 85 | err := wire.ReadBinaryBytes(data, &d) 86 | return d, err 87 | } 88 | 89 | func LoadEscrow(store types.KVStore, addr []byte) (EscrowData, error) { 90 | data := store.Get(addr) 91 | if len(data) == 0 { 92 | return EscrowData{}, fmt.Errorf("No escrow at: %X", addr) 93 | } 94 | return ParseEscrow(data) 95 | } 96 | -------------------------------------------------------------------------------- /trader/types/escrow_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | bc "github.com/tendermint/basecoin/types" 9 | ) 10 | 11 | func TestEscrowData(t *testing.T) { 12 | assert := assert.New(t) 13 | data := EscrowData{ 14 | Sender: []byte("1234567890qwertyuiop"), 15 | Recipient: []byte("AS1234567890qwertyui"), 16 | Arbiter: []byte("ASDF1234567890qwerty"), 17 | Amount: bc.Coins{ 18 | { 19 | Amount: 1000, 20 | Denom: "ATOM", 21 | }, 22 | }, 23 | } 24 | 25 | // make sure expiration only has meaning if non-zero 26 | assert.False(data.IsExpired(100)) 27 | data.Expiration = 200 28 | assert.False(data.IsExpired(100)) 29 | assert.True(data.IsExpired(201)) 30 | 31 | // make sure we get a valid address 32 | addr := data.Address() 33 | assert.NotEmpty(addr) 34 | assert.Equal(20, len(addr)) 35 | 36 | // make sure serialization/deserialization works 37 | b := data.Bytes() 38 | assert.NotEmpty(b) 39 | d2, err := ParseEscrow(b) 40 | assert.Nil(err) 41 | assert.Equal(data, d2) 42 | 43 | // and make sure they have the same address 44 | assert.Equal(addr, d2.Address()) 45 | } 46 | 47 | func TestEscrowTxParse(t *testing.T) { 48 | assert := assert.New(t) 49 | ctx := CreateEscrowTx{ 50 | Recipient: []byte("AS1234567890qwertyui"), 51 | Arbiter: []byte("ASDF1234567890qwerty"), 52 | Expiration: 12345, 53 | } 54 | rtx := ResolveEscrowTx{ 55 | Escrow: []byte("1234567890qwertyuiop"), 56 | Payout: true, 57 | } 58 | etx := ExpireEscrowTx{ 59 | Escrow: []byte("1234567890qwertyuiop"), 60 | } 61 | 62 | // make sure all of them serialize and deserialize fine 63 | txs := []EscrowTx{ctx, rtx, etx} 64 | for i, tx := range txs { 65 | idx := strconv.Itoa(i) 66 | b := EscrowTxBytes(tx) 67 | if assert.NotEmpty(b, idx) { 68 | p, err := ParseEscrowTx(b) 69 | assert.Nil(err, idx) 70 | assert.NotNil(p, idx) 71 | assert.Equal(tx, p, idx) 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /trader/types/options.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tendermint/basecoin/types" 8 | wire "github.com/tendermint/go-wire" 9 | "golang.org/x/crypto/ripemd160" 10 | ) 11 | 12 | func init() { 13 | // register tx implementations with gowire 14 | wire.RegisterInterface( 15 | optionswrap{}, 16 | wire.ConcreteType{O: CreateOptionTx{}, Byte: 0x01}, 17 | wire.ConcreteType{O: SellOptionTx{}, Byte: 0x02}, 18 | wire.ConcreteType{O: BuyOptionTx{}, Byte: 0x03}, 19 | wire.ConcreteType{O: ExerciseOptionTx{}, Byte: 0x04}, 20 | wire.ConcreteType{O: DisolveOptionTx{}, Byte: 0x05}, 21 | ) 22 | } 23 | 24 | type OptionsTx interface{} 25 | 26 | type optionswrap struct { 27 | OptionsTx 28 | } 29 | 30 | func ParseOptionsTx(data []byte) (OptionsTx, error) { 31 | holder := optionswrap{} 32 | err := wire.ReadBinaryBytes(data, &holder) 33 | return holder.OptionsTx, err 34 | } 35 | 36 | func OptionsTxBytes(tx OptionsTx) []byte { 37 | return wire.BinaryBytes(optionswrap{tx}) 38 | } 39 | 40 | // CreateOptionTx is used to create an option in the first place 41 | type CreateOptionTx struct { 42 | Expiration uint64 // height when the offer expires 43 | Trade types.Coins // this is the money that can exercise the option 44 | } 45 | 46 | // SellOptionTx is used to offer the option for sale 47 | type SellOptionTx struct { 48 | Addr []byte // address of the refered option 49 | Price types.Coins // required payment to transfer ownership 50 | NewHolder []byte // set to allow for only one buyer, empty for any buyer 51 | } 52 | 53 | // BuyOptionTx is used to purchase the right to exercise the option 54 | type BuyOptionTx struct { 55 | Addr []byte // address of the refered option 56 | } 57 | 58 | // ExerciseOptionTx must send Trade and recieve Bond 59 | type ExerciseOptionTx struct { 60 | Addr []byte // address of the refered option 61 | } 62 | 63 | // DisolveOptionTx returns Bond to issue if expired or unpurchased 64 | type DisolveOptionTx struct { 65 | Addr []byte // address of the refered option 66 | } 67 | 68 | // OptionData is our principal data structure in the db 69 | type OptionData struct { 70 | OptionIssue 71 | OptionHolder 72 | } 73 | 74 | // OptionIssue is the constant part, created wth the option, never changes 75 | type OptionIssue struct { 76 | // this is for the normal option functionality 77 | Issuer []byte 78 | Serial int // this sequence number is from the apptx that created it 79 | Expiration uint64 // height when the offer expires (0 = never) 80 | Bond types.Coins // this is stored upon creation of the option 81 | Trade types.Coins // this is the money that can exercise the option 82 | } 83 | 84 | // OptionHolder is the dynamic section of who can excercise the options 85 | type OptionHolder struct { 86 | // this is for buying/selling the option (should be a separate struct?) 87 | Holder []byte 88 | NewHolder []byte // set to allow for only one buyer, empty for any buyer 89 | Price types.Coins // required payment to transfer ownership 90 | } 91 | 92 | func (i OptionIssue) IsExpired(h uint64) bool { 93 | return (i.Expiration != 0 && h > i.Expiration) 94 | } 95 | 96 | // Address is the ripemd160 hash of the constant part of the option 97 | func (i OptionIssue) Address() []byte { 98 | hasher := ripemd160.New() 99 | hasher.Write(i.Bytes()) 100 | return hasher.Sum(nil) 101 | } 102 | 103 | func (i OptionIssue) Bytes() []byte { 104 | return wire.BinaryBytes(i) 105 | } 106 | 107 | func (d OptionData) Bytes() []byte { 108 | return wire.BinaryBytes(d) 109 | } 110 | 111 | // To buy, this option must be for sale, and the buyer must be 112 | // listed (or an open sale) 113 | func (d OptionData) CanBuy(buyer []byte) bool { 114 | return !d.Price.IsZero() && 115 | (len(d.NewHolder) == 0 || bytes.Equal(buyer, d.NewHolder)) 116 | } 117 | 118 | func (d OptionData) CanSell(buyer []byte) bool { 119 | return bytes.Equal(buyer, d.Holder) 120 | } 121 | 122 | func (d OptionData) CanExercise(addr []byte, h uint64) bool { 123 | return bytes.Equal(addr, d.Holder) && !d.IsExpired(h) 124 | } 125 | 126 | // CanDissolve if it is expired, or the holder, issue, and caller are the same 127 | func (d OptionData) CanDissolve(addr []byte, h uint64) bool { 128 | return d.IsExpired(h) || 129 | (bytes.Equal(addr, d.Holder) && bytes.Equal(d.Holder, d.Issuer)) 130 | } 131 | 132 | func ParseOptionData(data []byte) (OptionData, error) { 133 | d := OptionData{} 134 | err := wire.ReadBinaryBytes(data, &d) 135 | return d, err 136 | } 137 | 138 | func LoadOptionData(store types.KVStore, addr []byte) (OptionData, error) { 139 | data := store.Get(addr) 140 | if len(data) == 0 { 141 | return OptionData{}, fmt.Errorf("No option at: %X", addr) 142 | } 143 | return ParseOptionData(data) 144 | } 145 | 146 | func StoreOptionData(store types.KVStore, data OptionData) { 147 | addr := data.Address() 148 | store.Set(addr, data.Bytes()) 149 | } 150 | 151 | func DeleteOptionData(store types.KVStore, data OptionData) { 152 | addr := data.Address() 153 | store.Set(addr, nil) 154 | } 155 | -------------------------------------------------------------------------------- /trader/types/options_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/tendermint/basecoin/types" 9 | cmn "github.com/tendermint/tmlibs/common" 10 | ) 11 | 12 | func TestOptionData(t *testing.T) { 13 | assert := assert.New(t) 14 | a, b, c := cmn.RandBytes(20), cmn.RandBytes(20), cmn.RandBytes(20) 15 | bond := types.Coins{{Amount: 1000, Denom: "ATOM"}} 16 | trade := types.Coins{{Amount: 5, Denom: "BTC"}} 17 | price := types.Coins{{Amount: 10, Denom: "ETH"}} 18 | 19 | data := OptionData{ 20 | OptionIssue: OptionIssue{ 21 | Issuer: a, 22 | Serial: 5, 23 | Expiration: uint64(20), 24 | Bond: bond, 25 | Trade: trade, 26 | }, 27 | OptionHolder: OptionHolder{ 28 | Holder: a, 29 | }, 30 | } 31 | 32 | addr := data.Address() 33 | assert.NotEmpty(addr) 34 | assert.True(data.IsExpired(30)) 35 | assert.False(data.IsExpired(10)) 36 | 37 | // make sure the initial state is only issuer dissolve or sell 38 | assert.True(data.CanSell(a)) 39 | assert.False(data.CanSell(b)) 40 | assert.False(data.CanBuy(a)) 41 | assert.False(data.CanBuy(b)) 42 | assert.True(data.CanDissolve(a, 10)) 43 | assert.False(data.CanDissolve(b, 10)) 44 | assert.True(data.CanDissolve(b, 50)) // b can dissolve if it ends up expired 45 | 46 | // set a price and make sure anyone can buy 47 | data.Price = price 48 | assert.True(data.CanBuy(b)) 49 | assert.True(data.CanBuy(c)) 50 | // or a closed sale 51 | data.NewHolder = b 52 | assert.True(data.CanBuy(b)) 53 | assert.False(data.CanBuy(c)) 54 | 55 | // we complete the sale and make sure the address didn't change 56 | data.Price = nil 57 | data.NewHolder = nil 58 | data.Holder = b 59 | newAddr := data.Address() 60 | assert.Equal(addr, newAddr) 61 | 62 | // now make sure the new buyer can sell and exercise 63 | assert.False(data.CanBuy(c)) 64 | assert.False(data.CanSell(a)) 65 | assert.True(data.CanSell(b)) 66 | assert.False(data.CanSell(c)) 67 | assert.False(data.CanExercise(a, 10)) 68 | assert.True(data.CanExercise(b, 10)) 69 | assert.False(data.CanExercise(c, 10)) 70 | // and neither the holder nor the issuer cannot dissolve 71 | assert.False(data.CanDissolve(a, 10)) 72 | assert.False(data.CanDissolve(b, 10)) 73 | } 74 | 75 | func TestOptionsTxParse(t *testing.T) { 76 | assert := assert.New(t) 77 | 78 | trade := types.Coins{ 79 | {Amount: 5, Denom: "BTC"}, 80 | {Amount: 1000, Denom: "ATOM"}, 81 | } 82 | price := types.Coins{{Amount: 3, Denom: "ETH"}} 83 | 84 | txs := []OptionsTx{ 85 | CreateOptionTx{ 86 | Expiration: 12345, 87 | Trade: trade, 88 | }, 89 | SellOptionTx{ 90 | Addr: cmn.RandBytes(20), 91 | Price: price, 92 | NewHolder: []byte{}, // note: nil is serialized/parsed as empty 93 | }, 94 | SellOptionTx{ 95 | Addr: cmn.RandBytes(20), 96 | Price: price, 97 | NewHolder: cmn.RandBytes(20), 98 | }, 99 | BuyOptionTx{ 100 | Addr: cmn.RandBytes(20), 101 | }, 102 | ExerciseOptionTx{ 103 | Addr: cmn.RandBytes(20), 104 | }, 105 | DisolveOptionTx{ 106 | Addr: cmn.RandBytes(20), 107 | }, 108 | } 109 | 110 | // make sure all of them serialize and deserialize fine 111 | for i, tx := range txs { 112 | idx := strconv.Itoa(i) 113 | b := OptionsTxBytes(tx) 114 | if assert.NotEmpty(b, idx) { 115 | p, err := ParseOptionsTx(b) 116 | assert.Nil(err, idx) 117 | assert.NotNil(p, idx) 118 | assert.Equal(tx, p, idx) 119 | } 120 | } 121 | } 122 | --------------------------------------------------------------------------------