Feature/initial mixnet contract (#515)
* Starting on cosmwasm smart contracts * Mixnet contract now builds * Removing license and notice files, the monorepo already has these. * Removing generated README content * Simplified development instructions a bit. * Converted some network monitor files to use SPDX license headers * Renamed packaget to mixnet-contracts * Depending on the Nym topology crate * Renaming contract package in usage * Renamed "announce" to "register_node" in the defined messages * Fixed package name for mixnet contracts in defined release annotations * Added the mixnet contracts to the Cargo lock file * Renamed some fields in our contract topology * Using the stringy mixnode from the validator client. * Removing mix nodes count from state, we can infer that * Saving generated code in comment as it's a useful example for now * Renamed "count" to "get_topology" * Adding the beginnings of a validator client (in Typescript) * Starting to integrate example code. WIP. * Ignoring generated accounts * Making a few less mixnodes :) * Adding shebang to start script * Cranking up the Nym-related gas limits, as otherwise contract upload fails. * Simplest mixnode example is now working * Removing the external client code, it messed up wasm compilation. Will copy/paste for now. * Contract now wants to add a MixNode rather than an IP string * Adding mixnodes via contract now works (!) * Simplified mixnode registration example * Further mixnode-adding simplification. * Adding author name * Fixed description * Sent funds are now required to bond a mixnode * Ensuring that we send correct coin denomination * Unbonding now works (!). Quite primitivist. * Checking that unbonding works from the client. * Setting up a thief account to play with * Checking to see whether thief can unbond a node (it fails, happily) * Adding a more specific error for when an account attempts to unbond but owns no bonds * Figured out how to test contract balances * Set the console messages to explain things a little more nicely * Tests for insufficient funds result * Using more async in driver example * Added a bit more explanation of the actions taken by the driver example * Locking down wasm instantiate a bit more * Docs clarifications on how to run example * README clarification * Corrected the commit hash in the wasmd build command, it was still set for 0.14.0 * Moved models from types into state * Starting work on range queries * whitespace * Going back to slow but reliabel node uploads and disabling new contract upload * Cranking gas fees temporarily * Mixnode key retrieval working and tested * Range retrievals now working well * Removing unused clone * Compressing tests a bit * Testing node retrieval on large numbers of nodes. Not sure whether MockStorage has the same space limitations as production storage does. * Getting rid of spelling warning * Removing unused responses * Minor cleanup * Starting to map my way out of the tuples * Slightly more meaningful variable names * Returning a StdResult from nodes query * Fighting through the unwraps :) * Unfucking a bit more * Starting to use ranged nodes in contract * Testing node retrieval from range store * Ditching generated tests * Adding works, still need to test removing mixnodes * Attempting to remove a mixnode returns an error when no nodes exist * Un-registering when no accounts exist (edit) * un-registering someone else's mixnode fails * Ensuring proper ownership * Testing for only 1 mixnode getting deleted * Testing single-node retrieval * Removing mixnode working * Removed unused imports and unused variable warnings * Made handler functions private * Tested for error response on mixnode removal * Ensured proper post-state on mixnode removal * Using Vec<Coin> for currency equality comparisons * Removed todo, this amount is only for logging purposes anyway * Refactoring tests a bit * Adding a few storytelling comments * Putting helper methods into alphabetical order * Drying up mixnode adding in tests * Using the new add_mixnode helper * Checking full object equality in test * Removing the GetNodes handler * Taking a more "storytelling" approach to the contract tests * We need a few more methods to run our example driver * We now need to make a new address for each node we want to have, as each sending account can only have one node * HumanAddr not needed * Making call sequence a little more readable * Added the results of today's experiments with the REST API to the validator client readme * Corrected console.log message * Adding a note about how to run tests * More contract exercising fun * Updating mocha * Whitespace * Adding a note about running tests * Adding typed rest client * Starting to mess with typescript paging client * Removing the rest client, we'll use the cosmjs one for this * Noting a few more contract requirements * Starting client restructuring * Importing cosmjs stargate client * Starting to work on the chain cache * Cleanup * Removing type annotations which hilariously worked, confusing the compiler * Might as well do each cache individually * Renaming chaincache so that it handles only mixnodes * Renaming chaincache * Setting dynamic per-page value to ease testing * Using perPage in tests * Moving tests back into their own special home so they don't bloat our package * Ignoring generated docs * Adding TypeDoc documentation generator * Removing unused NetClient import * Added docs generation * Noting existence of docs generation * Starting to test paged responses * Working paging tests * Clarified test names a bit * Removed console.logs * Added a test for two full pages. * Formatting * Starting to query for mix nodes * Removed the topology in preparation for paging * Removing unused struct * Getting ready for series-based paging * We're now setting page size limits on list retrievals * Pagination starting to work, needs more testing * Moved test support stuff into its own home * Removing duplicate testy code * Testing all paging stuff in the contract * Removed useless method duplicate * Moving queries into their own file * Removing redundant tests * Testing default paging limit * Testing max paging limit * We don't need to c/p pagination stuff from the cw-plus contracts, removing * Testing pagination * Making next key calculation explict via a function * Removing temporary variable * Commenting final state * Incorporating the PagedResponse * On the road to a working TypeScript client * Adding some logging utilities * Paged retrieval working but needs improvement - it's very brittle * Getting the loop right * Removing unused logger * Setting up a request count * Documenting the ins and outs of the client network interface * Removing requestCount as we're not using it yet * Success! Making paginated requests for mixnodes! * Differentiating between MixNode and MixNodeBond * Checking that Fred can upload a mixnode * Fixing export * Adding the ability for client to get balances * Docs fix * Converting interfaces to types * Changing `mixNodes()` to `getMixNodes()` on client * We might as well return the nodes we've just retrieved when we refresh * Starting work on unbonding * Fixed a caching bug which was causing multiple result sets to be cached * Using the sender address as the key for removal * Importing some result stuff so we can find out what happened on execution * Minor messing around to prove that the sequence fully works * Displaying a nicer message on mixnode unbond * Renamed announce to bond in validator client * Fixed unstable clippy warnings * Removing commented fields * Comment spacing * Changed announce to bond in example code * Making the test accounts directory configurable * Rebuilt * Loading keys from the local ./accounts directory * Ignoring contract lockfile * Saving out a contract lockfile so things continue working after contract upload * Splitting the driver example into smaller self-contained examples * Deleting the example that Andrew hates so much * Making dependabot happy * Stricter equals * Removing unused import
This commit is contained in:
@@ -25,3 +25,4 @@ v6-topology.json
|
||||
/explorer/downloads/topology.json
|
||||
/explorer/public/downloads/mixmining.json
|
||||
/explorer/public/downloads/topology.json
|
||||
/clients/validator/examples/nym-driver-example/current-contract.txt
|
||||
|
||||
Generated
+35
@@ -2360,6 +2360,29 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be77ed66abed6954aabf6a3e31a84706bedbf93750d267e92ef4a6d90bbd6a61"
|
||||
dependencies = [
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11af7a475c9ee266cfaa9e303a47c830ebe072bf3101ab907a7b7b9d816fa01d"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"serde_derive_internals",
|
||||
"syn 1.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.0"
|
||||
@@ -2430,6 +2453,17 @@ dependencies = [
|
||||
"syn 1.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.57"
|
||||
@@ -2995,6 +3029,7 @@ dependencies = [
|
||||
"log",
|
||||
"mockito",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"serde",
|
||||
"tokio",
|
||||
"topology",
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
|
||||
[profile.release.package.mixnet-contracts]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
||||
incremental = false
|
||||
overflow-checks = true
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ The platform is composed of multiple Rust crates. Top-level executable binary cr
|
||||
* nym-socks5-client - a Socks5 proxy you can run on your machine, and use with existing applications
|
||||
* nym-gateway - acts sort of like a mailbox for mixnet messages, removing the need for directly delivery to potentially offline or firewalled devices.
|
||||
* nym-network-monitor - sends packets through the full system to check that they are working as expected, and stores node uptime histories as the basis of a rewards system ("mixmining" or "proof-of-mixing").
|
||||
* nym-explorer - a (projected) block explorer and (existing) mixnet viewer.
|
||||
* nym-explorer - a (projected) block explorer and (existing) mixnet viewer.
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://github.com/nymtech/nym/actions?query=branch%3Adevelop)
|
||||
@@ -32,6 +32,6 @@ There's a `.env.sample-dev` file provided which you can rename to `.env` if you
|
||||
|
||||
You can chat to us in [Keybase](https://keybase.io). Download their chat app, then click **Teams -> Join a team**. Type **nymtech.friends** into the team name and hit **continue**. For general chat, hang out in the **#general** channel. Our development takes places in the **#dev** channel. Node operators should be in the **#node-operators** channel.
|
||||
|
||||
### Licensing and copyright information
|
||||
### Licensing and copyright information
|
||||
|
||||
This program is available as open source under the terms of the Apache 2.0 license. However, some elements are being licensed under CC0-1.0 and MIT. For accurate information, please check individual files.
|
||||
|
||||
@@ -89,12 +89,12 @@ impl Config {
|
||||
) -> Self {
|
||||
Config {
|
||||
ack_key,
|
||||
ack_wait_addition,
|
||||
ack_wait_multiplier,
|
||||
self_recipient,
|
||||
average_message_sending_delay,
|
||||
average_packet_delay_duration,
|
||||
average_ack_delay_duration,
|
||||
average_message_sending_delay,
|
||||
ack_wait_multiplier,
|
||||
ack_wait_addition,
|
||||
packet_mode,
|
||||
vpn_key_reuse_limit,
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ impl MixnetResponseListener {
|
||||
.unwrap();
|
||||
|
||||
MixnetResponseListener {
|
||||
controller_sender,
|
||||
buffer_requester,
|
||||
mix_response_receiver,
|
||||
controller_sender,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"linebreak-style": "off",
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true
|
||||
}
|
||||
],
|
||||
"space-before-blocks": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.nyc_output
|
||||
.vscode
|
||||
coverage
|
||||
dist
|
||||
docs
|
||||
examples/accounts
|
||||
node_modules
|
||||
@@ -0,0 +1,5 @@
|
||||
coverage
|
||||
dist
|
||||
examples
|
||||
node_modules
|
||||
tests
|
||||
@@ -0,0 +1 @@
|
||||
15.0.1
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cache": false,
|
||||
"check-coverage": true,
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"include": [
|
||||
"**/src/*.ts"
|
||||
],
|
||||
"reporter": [
|
||||
"cobertura",
|
||||
"text-summary"
|
||||
],
|
||||
"statements": 90,
|
||||
"branches": 90,
|
||||
"functions": 90,
|
||||
"lines": 90
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
Nym Validator Client
|
||||
====================
|
||||
|
||||
A TypeScript client for interacting with CosmWasm smart contracts in Nym validators.
|
||||
|
||||
Running examples
|
||||
-----------------
|
||||
|
||||
With the code checked out, `cd examples`. This folder contains runnable example code that will set up a blockchain and allow you to interact with it through the client.
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
You can also trigger test execution with a test watcher. I don't have the centuries of life left to me that are needed to fight through the arcana of wiring up a working TypeScript mocha triggered execution setup, so for now my Cargo-based hack is:
|
||||
|
||||
|
||||
```
|
||||
cargo watch -s "cd clients/validator && npm test"
|
||||
```
|
||||
|
||||
It's ugly but works fine if you have Cargo installed. TypeScript setup help happily accepted here.
|
||||
|
||||
Generating Documentation
|
||||
------------------------
|
||||
|
||||
You can generate docs by running `npm run docs`. Generated output will appear in the `docs` directory.
|
||||
@@ -0,0 +1,124 @@
|
||||
This is a bunch of scripts for trying out CosmWasm contracts on a (local) blockchain node.
|
||||
|
||||
They work with CosmWasm v0.14.x. To set up a local node, you'll first need to check out the Nym validator source code, build it, and install it into your Go bin directory. Let's put it on your Desktop and install it:
|
||||
|
||||
```
|
||||
cd ~/Desktop
|
||||
git clone git@github.com:nymtech/nymd.git
|
||||
cd nymd
|
||||
git checkout v0.14.1
|
||||
go build -o nymd -mod=readonly -tags "netgo,ledger" -ldflags '-X github.com/cosmos/cosmos-sdk/version.Name=nymd -X github.com/cosmos/cosmos-sdk/version.AppName=nymd -X github.com/CosmWasm/wasmd/app.NodeDir=.nymd -X github.com/cosmos/cosmos-sdk/version.Version=0.14.1 -X github.com/cosmos/cosmos-sdk/version.Commit=1920f80d181adbeaedac1eeea1c1c6e1704d3e49 -X github.com/CosmWasm/wasmd/app.Bech32Prefix=nym -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"' -trimpath ./cmd/wasmd
|
||||
mv nymd $GOBIN
|
||||
```
|
||||
|
||||
That will build and install the `nymd` binary in Go's bin directory; assuming that's in your $PATH, the commands below will work.
|
||||
|
||||
There's an examples folder inside the validator client, which will set up Cosmos-based accounts, then initialize and start a local Cosmos blockchain for development purposes. First, from the top-level `nym` directory:
|
||||
|
||||
```
|
||||
cd clients/validator/examples
|
||||
```
|
||||
|
||||
(Re)generate accounts by running the following. NOTE: it's destructive, it'll wipe your previous wasm accounts!
|
||||
|
||||
```
|
||||
./reset_accounts.sh
|
||||
```
|
||||
|
||||
Init the blockchain running with:
|
||||
|
||||
```
|
||||
./init.sh
|
||||
```
|
||||
|
||||
Start it with:
|
||||
|
||||
```
|
||||
./start.sh
|
||||
```
|
||||
|
||||
Congratulations! You now have a running CosmWasm blockchain that you can upload contract code into.
|
||||
|
||||
## Using it in TypeScript
|
||||
|
||||
From the `clients/validators/examples` directory:
|
||||
|
||||
```
|
||||
cd nym-driver-example
|
||||
npm install
|
||||
npx nodemon --exec ts-node ./index.ts --watch . --ext .ts
|
||||
```
|
||||
|
||||
That will give you a running daemon that watches for changes in TypeScript files. It will execute a series of actions in the blockchain and explain what it's doing in the console, so you can see how things work.
|
||||
|
||||
Running `ts-node` on the index file would do basically the same thing, without a watcher.
|
||||
|
||||
|
||||
## Hitting the CosmWasm REST API directly:
|
||||
|
||||
Let's assume for the moment that the contract address you want to work on is `nym10pyejy66429refv3g35g2t7am0was7ya69su6d`.
|
||||
|
||||
Basic information about the contract can be retrieved from the Cosmos REST API's `cosmwasm` module.
|
||||
|
||||
The following routes are available:
|
||||
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d/state
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d/history
|
||||
|
||||
### Querying active contracts
|
||||
|
||||
There are also two query routes, one for smart queries and one for raw queries based on a key:
|
||||
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d/smart/{query}
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d/raw/{key}
|
||||
|
||||
Let's try using a smart query to retrieve our topology. We know the above URL structure for a smart query. What do we put in the `{query}` parameter?
|
||||
|
||||
Our contract has a `GetTopology {}` query struct (which is annotated as `snake_case` in the contract code, and takes an empty struct as a value), so we can construct the following smart query param:
|
||||
|
||||
```
|
||||
{ "get_topology": {} }
|
||||
```
|
||||
|
||||
HOWEVER
|
||||
|
||||
We can't just send this:
|
||||
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d/smart/{ "get_topology": {} }
|
||||
|
||||
Instead, we need to encode the param `{ "get_topology": {} }` as Base64, and tell the REST API that this is how it's encoded by adding a query string `encoding` parameter, e.g. `?encoding=base64`.
|
||||
|
||||
`{ "get_topology": {} }` in Base64 is `eyAiZ2V0X3RvcG9sb2d5Ijoge30gfQ==`. You can get this without making a driver program by going here:
|
||||
|
||||
https://onlineasciitools.com/convert-ascii-to-base64
|
||||
|
||||
(and in case you ever want to go the other way and decode some Base64 to verify what you've entered, go here: https://onlineasciitools.com/convert-base64-to-ascii)
|
||||
|
||||
The final query we end up with is:
|
||||
|
||||
http://localhost:1317/wasm/contract/nym10pyejy66429refv3g35g2t7am0was7ya69su6d/smart/eyAiZ2V0X3RvcG9sb2d5Ijoge30gfQ==?encoding=base64
|
||||
|
||||
This works if the topology is small.
|
||||
|
||||
It fails past a certain number of nodes (I'm not sure yet how many).
|
||||
|
||||
Note that output is once again in Base64, if you want to decode the output you'll need to do it.
|
||||
|
||||
I got back:
|
||||
|
||||
```
|
||||
{"height":"14036","result":{"smart":"eyJtaXhfbm9kZV9ib25kcyI6W3siYW1vdW50IjpbeyJkZW5vbSI6InVueW0iLCJhbW91bnQiOiIxMDAwMDAwMDAwIn1dLCJvd25lciI6Im55bTFkcWRyZGplOTdmMjZ4aG5xbjk3c2FyMndyNWdqOWQ5NDZ2a3NqayIsIm1peF9ub2RlIjp7Imhvc3QiOiIxOTIuMTY4LjEuMSIsImxheWVyIjoxLCJsb2NhdGlvbiI6InRoZSBpbnRlcm5ldCIsInNwaGlueF9rZXkiOiJteXNwaGlueGtleSIsInZlcnNpb24iOiIwLjkuMiJ9fSx7ImFtb3VudCI6W3siZGVub20iOiJ1bnltIiwiYW1vdW50IjoiMTAwMDAwMDAwMCJ9XSwib3duZXIiOiJueW0xa2tqYTQ5N2NwYzc5ZGwyNDJ3bjdxbHBhOWU4cGxuNGw2dnpyeGMiLCJtaXhfbm9kZSI6eyJob3N0IjoiMTkyLjE2OC4xLjEiLCJsYXllciI6MSwibG9jYXRpb24iOiJ0aGUgaW50ZXJuZXQiLCJzcGhpbnhfa2V5IjoibXlzcGhpbnhrZXkiLCJ2ZXJzaW9uIjoiMC45LjIifX0seyJhbW91bnQiOlt7ImRlbm9tIjoidW55bSIsImFtb3VudCI6IjEwMDAwMDAwMDAifV0sIm93bmVyIjoibnltMWxxaGtwbDlzcHJ3c3A2M2RjNDZldTlrdm11Y2Nna243MzBqOXRsIiwibWl4X25vZGUiOnsiaG9zdCI6IjE5Mi4xNjguMS4xIiwibGF5ZXIiOjEsImxvY2F0aW9uIjoidGhlIGludGVybmV0Iiwic3BoaW54X2tleSI6Im15c3BoaW54a2V5IiwidmVyc2lvbiI6IjAuOS4yIn19LHsiYW1vdW50IjpbeyJkZW5vbSI6InVueW0iLCJhbW91bnQiOiIxMDAwMDAwMDAwIn1dLCJvd25lciI6Im55bTFueTlzY3J6OTJuYWM1bGEzcTc3bXRuZWd6Z2V6YzY1cnZrcGdhYSIsIm1peF9ub2RlIjp7Imhvc3QiOiIxOTIuMTY4LjEuMSIsImxheWVyIjoxLCJsb2NhdGlvbiI6InRoZSBpbnRlcm5ldCIsInNwaGlueF9rZXkiOiJteXNwaGlueGtleSIsInZlcnNpb24iOiIwLjkuMiJ9fV19"}}```
|
||||
|
||||
Decoding the big Base64 response string from `smart` gives:
|
||||
|
||||
```
|
||||
{"mix_node_bonds":[{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1dqdrdje97f26xhnqn97sar2wr5gj9d946vksjk","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1kkja497cpc79dl242wn7qlpa9e8pln4l6vzrxc","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1lqhkpl9sprwsp63dc46eu9kvmuccgkn730j9tl","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1ny9scrz92nac5la3q77mtnegzgezc65rvkpgaa","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}}]}
|
||||
```
|
||||
|
||||
Pretty cool!
|
||||
|
||||
```
|
||||
{"mix_node_bonds":[{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1054nh6a049r2nd73zjyfya6svrrwtwuwplr782","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1332slmq7uqvwrmxvt73zltz0tpdr0pmpge7jnu","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1a4utjp6ysqygt0hdstv9697a5y0ujsg9vvg7m4","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1dqdrdje97f26xhnqn97sar2wr5gj9d946vksjk","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1gem5crjkput2lg7wzpr3jdpver0spazf34fggu","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1kkja497cpc79dl242wn7qlpa9e8pln4l6vzrxc","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1l8yf0qaq39d5xvcmpyx0mhxckn77x8gz4phmjz","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1lctclx3jnskam7llxqdyw0mmfk24znn9lelram","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1lqhkpl9sprwsp63dc46eu9kvmuccgkn730j9tl","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1m2nl7z7adm8ualkkhmpv24fx5ny8c69c38fkjl","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1ny9scrz92nac5la3q77mtnegzgezc65rvkpgaa","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1q5jzekzm8p925hvmwpz0jasd3nzy4tz8h8l32m","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1tauxc4ezv55metq05706actwkptdhsh7qenej7","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1v7a3udzuq7tw3mt926rjtv6yc8pjr2pnmep450","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1vfmv8repca7tl39teqevrg4x0a6ed83t35g7vc","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1xjgyc24v7y7w3gk2kekdngj6zu4dm6ghsc32tu","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1y8dgzduv3lrepvaft44ek6mynjr2fytacs52wh","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1zrlutwux422ks3k06khd3ljtuhfvf9jwzpqgm9","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}},{"amount":[{"denom":"unym","amount":"1000000000"}],"owner":"nym1zw9pdjtpdljhsjmghnz0dp3memcjgapmc3pmvh","mix_node":{"host":"192.168.1.1","layer":1,"location":"the internet","sphinx_key":"mysphinxkey","version":"0.9.2"}}]}```
|
||||
|
||||
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Don't get confused by the ComsWasm docs currently up. They're old. CosmWasm (and Tendermint) now combines nymd + wasmcli into nymd.
|
||||
|
||||
# RESET the chain every time it starts
|
||||
rm -rf ~/.nymd/
|
||||
|
||||
# default home is ~/.nymd
|
||||
# if you want to setup multiple apps on your local make sure to change this value
|
||||
export APP_HOME="$HOME/.nymd"
|
||||
|
||||
# initialize nymd configuration files
|
||||
nymd init nymnet --chain-id nymnet --home "${APP_HOME}"
|
||||
|
||||
# add minimum gas prices config to app configuration file
|
||||
sed -i -r 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025unym"/' "${APP_HOME}/config/app.toml"
|
||||
|
||||
# enable the rpc server
|
||||
sed -i -r 's/enable = false/enable = true/' "$APP_HOME/config/app.toml"
|
||||
|
||||
# disallow everybody else from running smart contract code in our blockchain
|
||||
python set_contract_upload_permissions.py
|
||||
|
||||
# nymd keys add dave # adds a >key for dave if one doesn't already exist
|
||||
DAVE_ADDRESS=$(nymd keys show dave -a)
|
||||
|
||||
# add your wallet addresses to genesis
|
||||
nymd add-genesis-account "$DAVE_ADDRESS" 1000000000000000unym,100000000000000000stake --home "$APP_HOME"
|
||||
|
||||
# add dave's address as validator's address
|
||||
nymd gentx dave 1000000000stake --chain-id nymnet --home "$APP_HOME"
|
||||
|
||||
# collect gentxs to genesis
|
||||
nymd collect-gentxs --home "$APP_HOME"
|
||||
|
||||
# validate the genesis file
|
||||
nymd validate-genesis --home "$APP_HOME"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// npx ts-node get-balance.ts
|
||||
|
||||
import ValidatorClient from "nym-validator-client";
|
||||
|
||||
async function newClient(mnemonic: string): Promise<ValidatorClient> {
|
||||
let contract = "nym18vd8fpwxzck93qlwghaj6arh4p7c5n8974s0uv";
|
||||
let client = ValidatorClient.connect(contract, mnemonic, "http://localhost:26657");
|
||||
return client;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let davePass = "jar travel copy apology neglect disease water fruit gaze possible session normal exclude onion carry matter object dumb tackle assist inspire kind airport crowd";
|
||||
let dave = await newClient(davePass);
|
||||
console.log("dave address: ", dave.address);
|
||||
await dave.getBalance(dave.address)
|
||||
.then(response => console.log(`dave: ${JSON.stringify(response)}`))
|
||||
.catch(err => console.log(err));
|
||||
|
||||
let fredPass = "pride moral airport someone involve rabbit else napkin cheese hello tent stove rabbit mean help small ship embark concert aim journey void fly output";
|
||||
let fred = await newClient(fredPass);
|
||||
console.log("fred address: ", fred.address);
|
||||
await fred.getBalance(fred.address)
|
||||
.then(response => console.log(`fred: ${JSON.stringify(response)}`))
|
||||
.catch(err => console.log(err));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,24 @@
|
||||
// npx ts-node get-topology.ts
|
||||
|
||||
import ValidatorClient from "nym-validator-client";
|
||||
|
||||
async function createClient(): Promise<ValidatorClient> {
|
||||
let contract = "nym18vd8fpwxzck93qlwghaj6arh4p7c5n8974s0uv";
|
||||
let dave = "jar travel copy apology neglect disease water fruit gaze possible session normal exclude onion carry matter object dumb tackle assist inspire kind airport crowd";
|
||||
let client = ValidatorClient.connect(contract, dave, "http://localhost:26657");
|
||||
return client;
|
||||
}
|
||||
|
||||
async function getMixnodes(client: ValidatorClient) {
|
||||
await client.refreshMixNodes().then(response => console.log(response)).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function main() {
|
||||
let client = await createClient();
|
||||
getMixnodes(client);
|
||||
}
|
||||
|
||||
main();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "nym-driver-example",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"author": "Dave Hrycyszyn",
|
||||
"license": "MIT",
|
||||
"description": "The simplest cosmjs typescript client to work as an example driver program",
|
||||
"repository": "https://github.com/nymtech/nym",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"prettier": "^2.2.1",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^14.14.22",
|
||||
"nodemon": "^2.0.7",
|
||||
"nym-validator-client": "file:../../",
|
||||
"save-dev": "0.0.1-security",
|
||||
"tasktimer": "^3.0.0",
|
||||
"ts-node": "^9.1.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// npx ts-node watch-topology.ts
|
||||
|
||||
import { TaskTimer } from 'tasktimer';
|
||||
import ValidatorClient from "nym-validator-client";
|
||||
|
||||
|
||||
async function createClient(): Promise<ValidatorClient> {
|
||||
let contract = "nym18vd8fpwxzck93qlwghaj6arh4p7c5n8974s0uv";
|
||||
let dave = "jar travel copy apology neglect disease water fruit gaze possible session normal exclude onion carry matter object dumb tackle assist inspire kind airport crowd";
|
||||
let client = ValidatorClient.connect(contract, dave, "http://localhost:26657");
|
||||
return client;
|
||||
}
|
||||
|
||||
async function getMixnodes(client: ValidatorClient) {
|
||||
await client.refreshMixNodes().then(response => console.log(response)).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function main() {
|
||||
let client = await createClient();
|
||||
const timer = new TaskTimer(1000);
|
||||
timer.add(task => getMixnodes(client)).start();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,18 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@tsconfig/recommended@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.1.tgz#7619bad397e06ead1c5182926c944e0ca6177f52"
|
||||
integrity sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==
|
||||
|
||||
prettier@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
|
||||
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
|
||||
|
||||
typescript@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
||||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# If you do not yet have any accounts in your computer's keychain, you'll need to set some up:
|
||||
|
||||
add_account() {
|
||||
user=$1
|
||||
|
||||
nymd keys add "$user" &> tmp.txt
|
||||
mnemonic=$(< tmp.txt tail -n1)
|
||||
address=$(nymd keys show "$user" -a)
|
||||
echo "$mnemonic" > "accounts/$user.key"
|
||||
echo "$address" > "accounts/$user.address"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm tmp.txt
|
||||
}
|
||||
|
||||
echo "Deleting accounts, don't worry about the scary message when it happens..."
|
||||
nymd keys delete dave -y
|
||||
nymd keys delete fred -y
|
||||
nymd keys delete bob -y
|
||||
nymd keys delete thief -y
|
||||
echo "Accounts deleted."
|
||||
echo ""
|
||||
echo "Re-adding accounts..."
|
||||
mkdir -p accounts
|
||||
add_account dave
|
||||
add_account fred
|
||||
add_account bob
|
||||
add_account thief
|
||||
|
||||
cleanup
|
||||
echo "All done."
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/python
|
||||
import json
|
||||
|
||||
# This script alters the genesis file so that only user account "dave"
|
||||
# can upload smart contracts.
|
||||
|
||||
genesis_filename = "/home/dave/.nymd/config/genesis.json"
|
||||
dave_address_filename = "./accounts/dave.address"
|
||||
|
||||
with open(dave_address_filename, "r") as dave_address_file:
|
||||
dave_address = dave_address_file.readline()
|
||||
|
||||
|
||||
genesis_file = open(genesis_filename, "r")
|
||||
genesis_json = json.load(genesis_file)
|
||||
genesis_file.close()
|
||||
wasm_params = genesis_json['app_state']['wasm']['params']
|
||||
wasm_uploads = wasm_params['code_upload_access']
|
||||
|
||||
# Set wasm upload capability
|
||||
wasm_uploads['permission'] = "OnlyAddress"
|
||||
wasm_uploads['address'] = dave_address.rstrip()
|
||||
|
||||
# Set wasm instantiate capability
|
||||
wasm_params['instantiate_default_permission'] = "OnlyAddress"
|
||||
|
||||
print(wasm_params)
|
||||
print(wasm_uploads)
|
||||
|
||||
print(genesis_json)
|
||||
genesis_file = open(genesis_filename, "w")
|
||||
json.dump(genesis_json, genesis_file)
|
||||
genesis_file.close()
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# run the node
|
||||
nymd start --home $HOME/.nymd
|
||||
Generated
+3924
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "nym-validator-client",
|
||||
"version": "0.1.0",
|
||||
"description": "A TypeScript client for interacting with CosmWasm smart contracts in Nym validators",
|
||||
"repository": "https://code.constructiveproof.com/dave/validator-client",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "ts-mocha tests/**/*.test.ts",
|
||||
"coverage": "nyc npm test",
|
||||
"lint": "eslint \"**/*.ts\"",
|
||||
"docs": "typedoc --out docs src/index.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.15",
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/mocha": "^8.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
||||
"@typescript-eslint/parser": "^4.14.0",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^7.18.0",
|
||||
"mocha": "^8.2.1",
|
||||
"moq.ts": "^7.2.0",
|
||||
"nyc": "^15.1.0",
|
||||
"ts-mocha": "^8.0.0",
|
||||
"typedoc": "^0.20.27",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/cosmwasm": "^0.24.0-alpha.18",
|
||||
"@cosmjs/cosmwasm-stargate": "^0.24.0-alpha.18",
|
||||
"@cosmjs/crypto": "^0.24.0-alpha.18",
|
||||
"@cosmjs/launchpad": "^0.24.0-alpha.18",
|
||||
"@cosmjs/proto-signing": "^0.24.0-alpha.18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"linebreak-style": "off",
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true
|
||||
}
|
||||
],
|
||||
"space-before-blocks": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
15.0.1
|
||||
@@ -0,0 +1,54 @@
|
||||
import { MixNodeBond } from "../types";
|
||||
import { INetClient, PagedResponse } from "../net-client"
|
||||
|
||||
export { MixnodesCache };
|
||||
|
||||
/**
|
||||
* There are serious limits in smart contract systems, but we need to keep track of
|
||||
* potentially thousands of nodes. MixnodeCache instances repeatedly make requests for
|
||||
* paged data about what mixnodes exist, and keep them locally in memory so that they're
|
||||
* available for querying.
|
||||
* */
|
||||
export default class MixnodesCache {
|
||||
mixNodes: MixNodeBond[]
|
||||
netClient: INetClient
|
||||
perPage: number
|
||||
|
||||
constructor(netClient: INetClient, perPage: number) {
|
||||
this.netClient = netClient;
|
||||
this.mixNodes = [];
|
||||
this.perPage = perPage;
|
||||
}
|
||||
|
||||
/// Makes repeated requests to assemble a full list of nodes.
|
||||
/// Requests continue to be make as long as `shouldMakeAnotherRequest()`
|
||||
// returns true.
|
||||
async refreshMixNodes(contractAddress: string) {
|
||||
this.mixNodes = [];
|
||||
let response: PagedResponse;
|
||||
let next: string | undefined;
|
||||
do {
|
||||
response = await this.netClient.getMixNodes(contractAddress, this.perPage, next);
|
||||
response.nodes.forEach(node => this.mixNodes.push(node));
|
||||
next = response.start_next_after;
|
||||
} while (this.shouldMakeAnotherRequest(response))
|
||||
return this.mixNodes;
|
||||
}
|
||||
|
||||
/// The paging interface on the smart contracts is a bit gross at the moment.
|
||||
/// This returns `true` if the `start_next_after` property of the response is set
|
||||
/// and the page we've just got back is the same length as perPage on this
|
||||
/// NetClient instance (we don't have any idea whether there is a next page
|
||||
/// so if both these things are true we should make another request);
|
||||
/// otherwise returns false.
|
||||
shouldMakeAnotherRequest(response: PagedResponse): boolean {
|
||||
let next = response.start_next_after;
|
||||
let nextExists: boolean = (next !== null && next !== undefined && next !== "");
|
||||
let fullPage: boolean = response.nodes.length == this.perPage;
|
||||
if (fullPage && nextExists) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import NetClient, { INetClient, PagedResponse } from "./net-client";
|
||||
import { MixNode, MixNodeBond } from "./types";
|
||||
import * as fs from "fs";
|
||||
import { Bip39, Random } from "@cosmjs/crypto";
|
||||
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
|
||||
import MixnodesCache from "./caches/mixnodes";
|
||||
import { Coin, coins } from "@cosmjs/launchpad";
|
||||
import { BroadcastTxResponse } from "@cosmjs/stargate/types"
|
||||
import { ExecuteResult, InstantiateOptions, InstantiateResult, UploadMeta, UploadResult } from "@cosmjs/cosmwasm";
|
||||
|
||||
export { coins };
|
||||
export default class ValidatorClient {
|
||||
url: string;
|
||||
private netClient: INetClient;
|
||||
private mixNodesCache: MixnodesCache;
|
||||
private wallet: DirectSecp256k1HdWallet
|
||||
readonly address: string;
|
||||
private contractAddress: string;
|
||||
|
||||
private constructor(url: string, netClient: INetClient, wallet: DirectSecp256k1HdWallet, address: string, contractAddress: string) {
|
||||
this.url = url;
|
||||
this.netClient = netClient;
|
||||
this.mixNodesCache = new MixnodesCache(netClient, 100);
|
||||
this.address = address;
|
||||
this.wallet = wallet;
|
||||
this.contractAddress = contractAddress;
|
||||
}
|
||||
|
||||
static async connect(contractAddress: string, mnemonic: string, url: string,) {
|
||||
const wallet = await ValidatorClient.buildWallet(mnemonic);
|
||||
const [{ address }] = await wallet.getAccounts();
|
||||
const netClient = await NetClient.connect(contractAddress, wallet, url);
|
||||
return new ValidatorClient(url, netClient, wallet, address, contractAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a named mnemonic from the system's keystore.
|
||||
*
|
||||
* @param keyName the name of the key in the keystore
|
||||
* @returns the mnemonic as a string
|
||||
*/
|
||||
static loadMnemonic(keyPath: string) {
|
||||
try {
|
||||
const mnemonic = fs.readFileSync(keyPath, "utf8");
|
||||
return mnemonic.trim();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return "fight with type system later";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random mnemonic, useful for creating new accounts.
|
||||
* @returns a fresh mnemonic.
|
||||
*/
|
||||
static randomMnemonic(): string {
|
||||
const mnemonic = Bip39.encode(Random.getBytes(16)).toString();
|
||||
return mnemonic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mnemonic A mnemonic from which to generate a public/private keypair.
|
||||
* @returns the address for this client wallet
|
||||
*/
|
||||
async mnemonicToAddress(mnemonic: string): Promise<string> {
|
||||
const wallet = await ValidatorClient.buildWallet(mnemonic);
|
||||
const [{ address }] = await wallet.getAccounts()
|
||||
return address
|
||||
}
|
||||
|
||||
static async buildWallet(mnemonic: string): Promise<DirectSecp256k1HdWallet> {
|
||||
return DirectSecp256k1HdWallet.fromMnemonic(mnemonic, undefined, "nym");
|
||||
}
|
||||
|
||||
getBalance(address: string): Promise<Coin | null> {
|
||||
return this.netClient.getBalance(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or refresh the list of mixnodes in the network.
|
||||
*
|
||||
* @returns an array containing all known `MixNodeBond`s.
|
||||
*
|
||||
* TODO: We will want to put this puppy on a timer, but for the moment we can
|
||||
* just get things strung together and refresh it manually.
|
||||
*/
|
||||
refreshMixNodes(): Promise<MixNodeBond[]> {
|
||||
return this.mixNodesCache.refreshMixNodes(this.contractAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mixnodes from the local client cache.
|
||||
*
|
||||
* @returns an array containing all `MixNodeBond`s in the client's local cache.
|
||||
*/
|
||||
getMixNodes(): MixNodeBond[] {
|
||||
return this.mixNodesCache.mixNodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce a mixnode, paying a fee.
|
||||
*/
|
||||
async bond(mixNode: MixNode): Promise<ExecuteResult> {
|
||||
const bond = [{ amount: "1000000000", denom: "unym" }];
|
||||
const result = await this.netClient.executeContract(this.address, this.contractAddress, { register_mixnode: { mix_node: mixNode } }, "adding mixnode", bond);
|
||||
console.log(`account ${this.address} added mixnode with ${mixNode.host}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbond a mixnode, removing it from the network and reclaiming staked coins
|
||||
*/
|
||||
async unbond(): Promise<ExecuteResult> {
|
||||
const result = await this.netClient.executeContract(this.address, this.contractAddress, { un_register_mixnode: {} })
|
||||
console.log(`account ${this.address} unbonded mixnode`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// TODO: if we just keep a reference to the SigningCosmWasmClient somewhere we can probably go direct
|
||||
// to it in the case of these methods below.
|
||||
|
||||
/**
|
||||
* Send funds from one address to another.
|
||||
*/
|
||||
async send(senderAddress: string, recipientAddress: string, coins: readonly Coin[], memo?: string): Promise<BroadcastTxResponse> {
|
||||
return this.netClient.sendTokens(senderAddress, recipientAddress, coins, memo);
|
||||
}
|
||||
|
||||
async upload(senderAddress: string, wasmCode: Uint8Array, meta?: UploadMeta, memo?: string): Promise<UploadResult> {
|
||||
return this.netClient.upload(senderAddress, wasmCode, meta, memo);
|
||||
}
|
||||
|
||||
public instantiate(senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, options?: InstantiateOptions): Promise<InstantiateResult> {
|
||||
return this.netClient.instantiate(senderAddress, codeId, initMsg, label, options);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { SigningCosmWasmClient, SigningCosmWasmClientOptions } from '@cosmjs/cosmwasm-stargate';
|
||||
import { MixNodeBond } from './types'
|
||||
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
|
||||
import { GasPrice } from '@cosmjs/launchpad';
|
||||
import { Coin } from "@cosmjs/launchpad"
|
||||
import { BroadcastTxResponse } from "@cosmjs/stargate/types"
|
||||
import { Options, nymGasLimits, defaultOptions } from "./stargate-helper"
|
||||
import { ExecuteResult, InstantiateOptions, InstantiateResult, UploadMeta, UploadResult } from '@cosmjs/cosmwasm';
|
||||
|
||||
export interface INetClient {
|
||||
getBalance(address: string): Promise<Coin | null>;
|
||||
getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise<PagedResponse>;
|
||||
executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult>;
|
||||
instantiate(senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, options?: InstantiateOptions): Promise<InstantiateResult>;
|
||||
sendTokens(senderAddress: string, recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<BroadcastTxResponse>;
|
||||
upload(senderAddress: string, wasmCode: Uint8Array, meta?: UploadMeta, memo?: string): Promise<UploadResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of network communication between this code and the validator.
|
||||
* Depends on `SigningCosmWasClient`, which signs all requests using keypairs
|
||||
* derived from on bech32 mnemonics.
|
||||
*
|
||||
* Wraps several methods from CosmWasmSigningClient so we can mock them for
|
||||
* unit testing.
|
||||
*/
|
||||
export default class NetClient implements INetClient {
|
||||
private clientAddress: string;
|
||||
private cosmClient: SigningCosmWasmClient;
|
||||
|
||||
private constructor(clientAddress: string, cosmClient: SigningCosmWasmClient) {
|
||||
this.clientAddress = clientAddress;
|
||||
this.cosmClient = cosmClient;
|
||||
}
|
||||
|
||||
public static async connect(contractAddress: string, wallet: DirectSecp256k1HdWallet, url?: string, opts?: Partial<Options>): Promise<INetClient> {
|
||||
const options: Options = { ...defaultOptions, ...opts }
|
||||
const [{ address }] = await wallet.getAccounts();
|
||||
const signerOptions: SigningCosmWasmClientOptions = {
|
||||
gasPrice: GasPrice.fromString("0.025unym"),
|
||||
gasLimits: nymGasLimits,
|
||||
};
|
||||
const client = await SigningCosmWasmClient.connectWithSigner(options.httpUrl, wallet, signerOptions);
|
||||
|
||||
let netClient = new NetClient(address, client);
|
||||
return netClient;
|
||||
}
|
||||
|
||||
public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise<PagedResponse> {
|
||||
if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here.
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } });
|
||||
} else {
|
||||
return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit, start_after } });
|
||||
}
|
||||
}
|
||||
|
||||
public getBalance(address: string): Promise<Coin | null> {
|
||||
return this.cosmClient.getBalance(address, "unym");
|
||||
}
|
||||
|
||||
|
||||
public executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult> {
|
||||
return this.cosmClient.execute(senderAddress, contractAddress, handleMsg, memo, transferAmount);
|
||||
}
|
||||
|
||||
public sendTokens(senderAddress: string, recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<BroadcastTxResponse> {
|
||||
return this.cosmClient.sendTokens(senderAddress, recipientAddress, transferAmount, memo);
|
||||
}
|
||||
|
||||
public upload(senderAddress: string, wasmCode: Uint8Array, meta?: UploadMeta, memo?: string): Promise<UploadResult> {
|
||||
return this.cosmClient.upload(senderAddress, wasmCode, meta, memo);
|
||||
}
|
||||
|
||||
public instantiate(senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, options?: InstantiateOptions): Promise<InstantiateResult> {
|
||||
return this.cosmClient.instantiate(senderAddress, codeId, initMsg, label, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// One page of a possible multi-page set of mixnodes. The paging interface is quite
|
||||
/// inconvenient, as we don't have the two pieces of information we need to know
|
||||
/// in order to do paging nicely (namely `currentPage` and `totalPages` parameters).
|
||||
///
|
||||
/// Instead, we have only `start_next_page_after`, i.e. the key of the last record
|
||||
/// on this page. In order to get the *next* page, CosmWasm looks at that value,
|
||||
/// finds the next record, and builds the next page starting there. This happens
|
||||
/// **in series** rather than **in parallel** (!).
|
||||
///
|
||||
/// So we have some consistency problems:
|
||||
///
|
||||
/// * we can't make requests at a given block height, so the result set
|
||||
/// which we assemble over time may change while requests are being made.
|
||||
/// * at some point we will make a request for a `start_next_page_after` key
|
||||
/// which has just been deleted from the database.
|
||||
///
|
||||
/// TODO: more robust error handling on the "deleted key" case.
|
||||
export type PagedResponse = {
|
||||
nodes: MixNodeBond[],
|
||||
per_page: number, // TODO: camelCase
|
||||
start_next_after: string, // TODO: camelCase
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SigningCosmWasmClient, SigningCosmWasmClientOptions } from "@cosmjs/cosmwasm-stargate";
|
||||
import { Bip39, Random } from "@cosmjs/crypto";
|
||||
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
|
||||
import * as fs from "fs";
|
||||
import axios from 'axios';
|
||||
import { GasLimits, GasPrice, logs } from "@cosmjs/launchpad";
|
||||
import { CosmWasmFeeTable } from "@cosmjs/cosmwasm";
|
||||
|
||||
|
||||
export interface Options {
|
||||
httpUrl: string;
|
||||
networkId: string;
|
||||
feeToken: string;
|
||||
gasPrice: number;
|
||||
bech32prefix: string;
|
||||
}
|
||||
|
||||
export const nymGasLimits: GasLimits<CosmWasmFeeTable> = {
|
||||
upload: 2_500_000,
|
||||
init: 500_000,
|
||||
migrate: 200_000,
|
||||
exec: 9_000_000_000,
|
||||
send: 80_000,
|
||||
changeAdmin: 80_000,
|
||||
};
|
||||
|
||||
export const defaultOptions: Options = {
|
||||
httpUrl: "http://localhost:26657",
|
||||
networkId: "nymnet",
|
||||
feeToken: "unym",
|
||||
gasPrice: 0.025,
|
||||
bech32prefix: "nym",
|
||||
};
|
||||
|
||||
const downloadWasm = async (url: string): Promise<Uint8Array> => {
|
||||
const r = await axios.get(url, { responseType: "arraybuffer" });
|
||||
if (r.status !== 200) {
|
||||
throw new Error(`Download error: ${r.status}`);
|
||||
}
|
||||
return r.data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": [
|
||||
"tests",
|
||||
"dist",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import MixnodesCache from "./caches/mixnodes";
|
||||
import { Coin } from "@cosmjs/launchpad/";
|
||||
|
||||
export type MixNodeBond = { // TODO: change name to MixNodeBond
|
||||
owner: string,
|
||||
mix_node: MixNode, // TODO: camelCase this later once everything else works
|
||||
|
||||
amount: Coin[],
|
||||
}
|
||||
|
||||
export type MixNode = {
|
||||
host: string,
|
||||
layer: number,
|
||||
location: string,
|
||||
sphinx_key: string, // TODO: camelCase this later once everything else works
|
||||
version: string,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function log(text: string, thing: any) {
|
||||
let msg = JSON.stringify(thing);
|
||||
console.log(`${text}: ${msg}`);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { assert } from 'chai';
|
||||
import INetClient from '../../src/net-client';
|
||||
import { Fixtures } from '../fixtures'
|
||||
import { Mock, Times } from 'moq.ts';
|
||||
import { MixnodesCache } from '../../src/caches/mixnodes'
|
||||
|
||||
describe("Caching mixnodes: when the validator returns", () => {
|
||||
context("an empty list", () => {
|
||||
it("Should return an empty list", async () => {
|
||||
const perPage = 100;
|
||||
const contractAddress = "mockContractAddress";
|
||||
const emptyPromise = Promise.resolve(Fixtures.MixNodesResp.empty());
|
||||
const mockClient = new Mock<INetClient>().setup(netClient => netClient.getMixNodes(contractAddress, perPage, undefined)).returns(emptyPromise);
|
||||
const cache = new MixnodesCache(mockClient.object(), perPage);
|
||||
|
||||
await cache.refreshMixNodes(contractAddress);
|
||||
|
||||
mockClient.verify(netClient => netClient.getMixNodes(contractAddress, perPage, undefined), Times.Exactly(1));
|
||||
assert.deepEqual([], cache.mixNodes);
|
||||
});
|
||||
})
|
||||
context("a list of nodes that fits in a page", () => {
|
||||
it("Should return that one page list", async () => {
|
||||
const perPage = 2;
|
||||
const contractAddress = "mockContractAddress";
|
||||
const onePagePromise = Promise.resolve(Fixtures.MixNodesResp.onePage());
|
||||
const mockClient = new Mock<INetClient>().setup(netClient => netClient.getMixNodes(contractAddress, perPage, undefined)).returns(onePagePromise);
|
||||
const cache = new MixnodesCache(mockClient.object(), perPage);
|
||||
|
||||
await cache.refreshMixNodes(contractAddress);
|
||||
|
||||
mockClient.verify(netClient => netClient.getMixNodes(contractAddress, perPage, undefined), Times.Exactly(1));
|
||||
assert.deepEqual(Fixtures.MixNodes.list2(), cache.mixNodes);
|
||||
})
|
||||
})
|
||||
|
||||
context("a list of nodes that is longer than one page", () => {
|
||||
it("Should return the full list assembled from all pages", async () => {
|
||||
const perPage = 2; // we get back 2 per page
|
||||
const contractAddress = "mockContractAddress";
|
||||
const fullPageResult = Fixtures.MixNodesResp.page1of2();
|
||||
const halfPageResult = Fixtures.MixNodesResp.halfPage2of2();
|
||||
const mockClient = new Mock<INetClient>()
|
||||
mockClient.setup(instance => instance.getMixNodes(contractAddress, perPage, undefined)).returns(Promise.resolve(fullPageResult));
|
||||
mockClient.setup(instance => instance.getMixNodes(contractAddress, perPage, fullPageResult.start_next_after)).returns(Promise.resolve(halfPageResult));
|
||||
const cache = new MixnodesCache(mockClient.object(), perPage);
|
||||
|
||||
await cache.refreshMixNodes(contractAddress); // should make multiple paginated requests because there are two pages in the response fixture
|
||||
mockClient.verify(instance => instance.getMixNodes(contractAddress, perPage, undefined), Times.Exactly(1));
|
||||
mockClient.verify(instance => instance.getMixNodes(contractAddress, perPage, fullPageResult.start_next_after), Times.Exactly(1));
|
||||
|
||||
assert.deepEqual(Fixtures.MixNodes.list3(), cache.mixNodes); // there are a total of 3 nodes in the validator lists, we get them all back
|
||||
})
|
||||
})
|
||||
|
||||
context("a list of nodes that is two filled pages", () => {
|
||||
it("Should return the full list assembled from all pages", async () => {
|
||||
const perPage = 2; // we get back 2 per page
|
||||
const contractAddress = "mockContractAddress";
|
||||
const fullPageResult1 = Fixtures.MixNodesResp.page1of2();
|
||||
const fullPageResult2 = Fixtures.MixNodesResp.fullPage2of2();
|
||||
const mockClient = new Mock<INetClient>()
|
||||
mockClient.setup(netClient => netClient.getMixNodes(contractAddress, perPage, undefined)).returns(Promise.resolve(fullPageResult1));
|
||||
mockClient.setup(netClient => netClient.getMixNodes(contractAddress, perPage, fullPageResult1.start_next_after)).returns(Promise.resolve(fullPageResult2));
|
||||
|
||||
const cache = new MixnodesCache(mockClient.object(), perPage);
|
||||
|
||||
await cache.refreshMixNodes(contractAddress); // should make multiple paginated requests because there are two pages in the response fixture
|
||||
mockClient.verify(netClient => netClient.getMixNodes(contractAddress, perPage, undefined), Times.Exactly(1));
|
||||
mockClient.verify(netClient => netClient.getMixNodes(contractAddress, perPage, fullPageResult1.start_next_after), Times.Exactly(1));
|
||||
|
||||
assert.deepEqual(Fixtures.MixNodes.list4(), cache.mixNodes); // there are a total of 3 nodes in the validator lists, we get them all back
|
||||
})
|
||||
})
|
||||
|
||||
context("refreshing the cache twice", () => {
|
||||
it("returns one full list assembled from all pages", async () => {
|
||||
const perPage = 2; // we get back 2 per page
|
||||
const contractAddress = "mockContractAddress";
|
||||
const fullPageResult1 = Fixtures.MixNodesResp.page1of2();
|
||||
const fullPageResult2 = Fixtures.MixNodesResp.fullPage2of2();
|
||||
const mockClient = new Mock<INetClient>()
|
||||
mockClient.setup(netClient => netClient.getMixNodes(contractAddress, perPage, undefined)).returns(Promise.resolve(fullPageResult1));
|
||||
mockClient.setup(netClient => netClient.getMixNodes(contractAddress, perPage, fullPageResult1.start_next_after)).returns(Promise.resolve(fullPageResult2));
|
||||
|
||||
const cache = new MixnodesCache(mockClient.object(), perPage);
|
||||
|
||||
await cache.refreshMixNodes(contractAddress); // should make multiple paginated requests because there are two pages in the response fixture
|
||||
await cache.refreshMixNodes(contractAddress);
|
||||
// mockClient.verify(netClient => netClient.getMixNodes(contractAddress, perPage, undefined), Times.Exactly(1));
|
||||
// mockClient.verify(netClient => netClient.getMixNodes(contractAddress, perPage, fullPageResult1.start_next_after), Times.Exactly(1));
|
||||
|
||||
assert.deepEqual(Fixtures.MixNodes.list4(), cache.mixNodes); // there are a total of 3 nodes in the validator lists, we get them all back
|
||||
})
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { coins } from '@cosmjs/launchpad';
|
||||
import { PagedResponse } from '../src/net-client';
|
||||
import { MixNodeBond } from '../src/types'
|
||||
|
||||
export namespace Fixtures {
|
||||
export class MixNodes {
|
||||
static single(): MixNodeBond {
|
||||
return {
|
||||
amount: coins(666, "unym"),
|
||||
owner: "bob",
|
||||
mix_node: {
|
||||
host: "1.1.1.1",
|
||||
layer: 1,
|
||||
location: "London, UK",
|
||||
sphinx_key: "foo",
|
||||
version: "0.10.0",
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static list1(): MixNodeBond[] {
|
||||
return [MixNodes.single()]
|
||||
}
|
||||
|
||||
static list2(): MixNodeBond[] {
|
||||
return [MixNodes.single(), MixNodes.single()]
|
||||
}
|
||||
|
||||
static list3(): MixNodeBond[] {
|
||||
return [MixNodes.single(), MixNodes.single(), MixNodes.single()]
|
||||
}
|
||||
|
||||
static list4(): MixNodeBond[] {
|
||||
return [MixNodes.single(), MixNodes.single(), MixNodes.single(), MixNodes.single()]
|
||||
}
|
||||
}
|
||||
|
||||
export class MixNodesResp {
|
||||
static empty(): PagedResponse {
|
||||
return {
|
||||
nodes: [],
|
||||
per_page: 2,
|
||||
start_next_after: null,
|
||||
}
|
||||
}
|
||||
|
||||
static onePage(): PagedResponse {
|
||||
return {
|
||||
nodes: MixNodes.list2(),
|
||||
per_page: 2,
|
||||
start_next_after: null
|
||||
}
|
||||
}
|
||||
|
||||
static page1of2(): PagedResponse {
|
||||
return {
|
||||
nodes: MixNodes.list2(),
|
||||
per_page: 2,
|
||||
start_next_after: "2"
|
||||
}
|
||||
}
|
||||
|
||||
static halfPage2of2(): PagedResponse {
|
||||
return {
|
||||
nodes: MixNodes.list1(),
|
||||
per_page: 2,
|
||||
start_next_after: null
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static fullPage2of2(): PagedResponse {
|
||||
return {
|
||||
nodes: MixNodes.list2(),
|
||||
per_page: 2,
|
||||
start_next_after: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"typedocOptions": {
|
||||
"entryPoints": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"out": "docs"
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"examples",
|
||||
"tests",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -8,8 +8,9 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.10", features = ["json"] }
|
||||
schemars = "0.7.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
crypto = { path = "../../crypto" }
|
||||
topology = { path = "../../topology" }
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
use crate::models::node::NodeInfo;
|
||||
use crypto::asymmetric::{encryption, identity};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use std::io;
|
||||
@@ -40,7 +41,7 @@ impl From<identity::KeyRecoveryError> for ConversionError {
|
||||
}
|
||||
|
||||
// used for mixnode to register themselves
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MixRegistrationInfo {
|
||||
#[serde(flatten)]
|
||||
@@ -73,7 +74,7 @@ impl MixRegistrationInfo {
|
||||
}
|
||||
|
||||
// actual entry in topology
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisteredMix {
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct NodeInfo {
|
||||
pub(crate) mix_host: String,
|
||||
|
||||
@@ -19,8 +19,8 @@ impl Response {
|
||||
/// Constructor for responses
|
||||
pub fn new(connection_id: ConnectionId, data: Vec<u8>, is_closed: bool) -> Self {
|
||||
Response {
|
||||
connection_id,
|
||||
data,
|
||||
connection_id,
|
||||
is_closed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[alias]
|
||||
wasm = "build --release --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib"
|
||||
schema = "run --example schema"
|
||||
@@ -0,0 +1,49 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: rust:1.44.1
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Version information
|
||||
command: rustc --version; cargo --version; rustup --version
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }}
|
||||
- run:
|
||||
name: Add wasm32 target
|
||||
command: rustup target add wasm32-unknown-unknown
|
||||
- run:
|
||||
name: Unit tests
|
||||
env: RUST_BACKTRACE=1
|
||||
command: cargo unit-test --locked
|
||||
- run:
|
||||
name: Build
|
||||
command: cargo wasm --locked
|
||||
- run:
|
||||
name: Format source code
|
||||
command: cargo fmt
|
||||
- run:
|
||||
name: Build and run schema generator
|
||||
command: cargo schema --locked
|
||||
- run:
|
||||
name: Ensure checked-in source code and schemas are up-to-date
|
||||
command: |
|
||||
CHANGES_IN_REPO=$(git status --porcelain)
|
||||
if [[ -n "$CHANGES_IN_REPO" ]]; then
|
||||
echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:"
|
||||
git status && git --no-pager diff
|
||||
exit 1
|
||||
fi
|
||||
- save_cache:
|
||||
paths:
|
||||
- /usr/local/cargo/registry
|
||||
- target/debug/.fingerprint
|
||||
- target/debug/build
|
||||
- target/debug/deps
|
||||
- target/wasm32-unknown-unknown/release/.fingerprint
|
||||
- target/wasm32-unknown-unknown/release/build
|
||||
- target/wasm32-unknown-unknown/release/deps
|
||||
key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }}
|
||||
@@ -0,0 +1,11 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
Generated
+203
@@ -0,0 +1,203 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-derive"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de70197a0fe4e402aa260da00ec62d9bf091afe87866e9f3347691d591b60f89"
|
||||
dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-schema"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a98b56c1c839e5cfda1be0e0f152728b34bdcd5a2e921905947f6d088bab2c68"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-std"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02173c4eb78ef76863f5190f60a45278d11e7c8b142327c149e18128b2b97f85"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cosmwasm-derive",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-storage"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2cdb5f6c3181c921c02b0018af66178e60bc7be088f81ec730b22ff51240404"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
|
||||
[[package]]
|
||||
name = "mixnet-contracts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cosmwasm-storage",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be77ed66abed6954aabf6a3e31a84706bedbf93750d267e92ef4a6d90bbd6a61"
|
||||
dependencies = [
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11af7a475c9ee266cfaa9e303a47c830ebe072bf3101ab907a7b7b9d816fa01d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "974ef1bd2ad8a507599b336595454081ff68a9599b4890af7643c0c0ed73a62c"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-json-wasm"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "120bad73306616e91acd7ceed522ba96032a51cffeef3cc813de7f367df71e37"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8dee1f300f838c8ac340ecb0112b3ac472464fa67e87292bdb3dfc9c49128e17"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "mixnet-contracts"
|
||||
version = "0.1.0"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
exclude = [
|
||||
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
|
||||
"contract.wasm",
|
||||
"hash.txt",
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[workspace] # adding a blank workspace to keep it out of the global workspace.
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
rpath = false
|
||||
lto = true
|
||||
debug-assertions = false
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
incremental = false
|
||||
overflow-checks = true
|
||||
|
||||
[features]
|
||||
# for more explicit tests, cargo test --features=backtraces
|
||||
backtraces = ["cosmwasm-std/backtraces"]
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = { version = "0.13.2", features = ["iterator"] }
|
||||
cosmwasm-storage = { version = "0.13.2", features = ["iterator"] }
|
||||
schemars = "0.7"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0.23" }
|
||||
|
||||
[dev-dependencies]
|
||||
cosmwasm-schema = { version = "0.13.2" }
|
||||
@@ -0,0 +1,95 @@
|
||||
# Developing
|
||||
|
||||
You probably could use some help on how to build and test the contract, as well as prepare it for production. This
|
||||
file attempts to provide a brief overview, assuming you have installed a recent
|
||||
version of Rust already (eg. 1.47.0+).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, make sure you have [rustup](https://rustup.rs/) along with a
|
||||
recent `rustc` and `cargo` version installed. Currently, we are testing on 1.44.1+.
|
||||
|
||||
And you need to have the `wasm32-unknown-unknown` target installed as well.
|
||||
|
||||
You can check that via:
|
||||
|
||||
```sh
|
||||
rustc --version
|
||||
cargo --version
|
||||
rustup target list --installed
|
||||
# if wasm32 is not listed above, run this
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
## Compiling and running tests
|
||||
|
||||
Now that you created your custom contract, make sure you can compile and run it before
|
||||
making any changes. Go into the repository and do:
|
||||
|
||||
```sh
|
||||
# this will produce a wasm build in ./target/wasm32-unknown-unknown/release/YOUR_NAME_HERE.wasm
|
||||
cargo wasm
|
||||
|
||||
# this runs unit tests with helpful backtraces
|
||||
RUST_BACKTRACE=1 cargo unit-test
|
||||
|
||||
# auto-generate json schema
|
||||
cargo schema
|
||||
```
|
||||
|
||||
### Understanding the tests
|
||||
|
||||
The main code is in `src/contract.rs` and the unit tests there run in pure rust,
|
||||
which makes them very quick to execute and give nice output on failures, especially
|
||||
if you do `RUST_BACKTRACE=1 cargo unit-test`.
|
||||
|
||||
We consider testing critical for anything on a blockchain, and recommend to always keep
|
||||
the tests up to date.
|
||||
|
||||
## Generating JSON Schema
|
||||
|
||||
While the Wasm calls (`init`, `handle`, `query`) accept JSON, this is not enough
|
||||
information to use it. We need to expose the schema for the expected messages to the
|
||||
clients. You can generate this schema by calling `cargo schema`, which will output
|
||||
4 files in `./schema`, corresponding to the 3 message types the contract accepts,
|
||||
as well as the internal `State`.
|
||||
|
||||
These files are in standard json-schema format, which should be usable by various
|
||||
client side tools, either to auto-generate codecs, or just to validate incoming
|
||||
json wrt. the defined schema.
|
||||
|
||||
## Preparing the Wasm bytecode for production
|
||||
|
||||
Before we upload it to a chain, we need to ensure the smallest output size possible,
|
||||
as this will be included in the body of a transaction. We also want to have a
|
||||
reproducible build process, so third parties can verify that the uploaded Wasm
|
||||
code did indeed come from the claimed rust code.
|
||||
|
||||
To solve both these issues, we have produced `rust-optimizer`, a docker image to
|
||||
produce an extremely small build output in a consistent manner. The suggest way
|
||||
to run it is this:
|
||||
|
||||
```sh
|
||||
docker run --rm -v "$(pwd)":/code \
|
||||
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
|
||||
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
|
||||
cosmwasm/rust-optimizer:0.10.7
|
||||
```
|
||||
|
||||
We must mount the contract code to `/code`. You can use a absolute path instead
|
||||
of `$(pwd)` if you don't want to `cd` to the directory first. The other two
|
||||
volumes are nice for speedup. Mounting `/code/target` in particular is useful
|
||||
to avoid docker overwriting your local dev files with root permissions.
|
||||
Note the `/code/target` cache is unique for each contract being compiled to limit
|
||||
interference, while the registry cache is global.
|
||||
|
||||
This is rather slow compared to local compilations, especially the first compile
|
||||
of a given contract. The use of the two volume caches is very useful to speed up
|
||||
following compiles of the same contract.
|
||||
|
||||
This produces an `artifacts` directory with a `PROJECT_NAME.wasm`, as well as
|
||||
`checksums.txt`, containing the Sha256 hash of the wasm file.
|
||||
The wasm file is compiled deterministically (anyone else running the same
|
||||
docker on the same git commit should get the identical file with the same Sha256 hash).
|
||||
It is also stripped and minimized for upload to a blockchain (we will also
|
||||
gzip it in the uploading process to make it even smaller).
|
||||
@@ -0,0 +1,62 @@
|
||||
# Importing
|
||||
|
||||
In [Publishing](./Publishing.md), we discussed how you can publish your contract to the world.
|
||||
This looks at the flip-side, how can you use someone else's contract (which is the same
|
||||
question as how they will use your contract). Let's go through the various stages.
|
||||
|
||||
## Verifying Artifacts
|
||||
|
||||
Before using remote code, you most certainly want to verify it is honest.
|
||||
|
||||
The simplest audit of the repo is to simply check that the artifacts in the repo
|
||||
are correct. This involves recompiling the claimed source with the claimed builder
|
||||
and validating that the locally compiled code (hash) matches the code hash that was
|
||||
uploaded. This will verify that the source code is the correct preimage. Which allows
|
||||
one to audit the original (Rust) source code, rather than looking at wasm bytecode.
|
||||
|
||||
We have a script to do this automatic verification steps that can
|
||||
easily be run by many individuals. Please check out
|
||||
[`cosmwasm-verify`](https://github.com/CosmWasm/cosmwasm-verify/blob/master/README.md)
|
||||
to see a simple shell script that does all these steps and easily allows you to verify
|
||||
any uploaded contract.
|
||||
|
||||
## Reviewing
|
||||
|
||||
Once you have done the quick programatic checks, it is good to give at least a quick
|
||||
look through the code. A glance at `examples/schema.rs` to make sure it is outputing
|
||||
all relevant structs from `contract.rs`, and also ensure `src/lib.rs` is just the
|
||||
default wrapper (nothing funny going on there). After this point, we can dive into
|
||||
the contract code itself. Check the flows for the handle methods, any invariants and
|
||||
permission checks that should be there, and a reasonable data storage format.
|
||||
|
||||
You can dig into the contract as far as you want, but it is important to make sure there
|
||||
are no obvious backdoors at least.
|
||||
|
||||
## Decentralized Verification
|
||||
|
||||
It's not very practical to do a deep code review on every dependency you want to use,
|
||||
which is a big reason for the popularity of code audits in the blockchain world. We trust
|
||||
some experts review in lieu of doing the work ourselves. But wouldn't it be nice to do this
|
||||
in a decentralized manner and peer-review each other's contracts? Bringing in deeper domain
|
||||
knowledge and saving fees.
|
||||
|
||||
Luckily, there is an amazing project called [crev](https://github.com/crev-dev/cargo-crev/blob/master/cargo-crev/README.md)
|
||||
that provides `A cryptographically verifiable code review system for the cargo (Rust) package manager`.
|
||||
|
||||
I highly recommend that CosmWasm contract developers get set up with this. At minimum, we
|
||||
can all add a review on a package that programmatically checked out that the json schemas
|
||||
and wasm bytecode do match the code, and publish our claim, so we don't all rely on some
|
||||
central server to say it validated this. As we go on, we can add deeper reviews on standard
|
||||
packages.
|
||||
|
||||
If you want to use `cargo-crev`, please follow their
|
||||
[getting started guide](https://github.com/crev-dev/cargo-crev/blob/master/cargo-crev/src/doc/getting_started.md)
|
||||
and once you have made your own *proof repository* with at least one *trust proof*,
|
||||
please make a PR to the [`cawesome-wasm`]() repo with a link to your repo and
|
||||
some public name or pseudonym that people know you by. This allows people who trust you
|
||||
to also reuse your proofs.
|
||||
|
||||
There is a [standard list of proof repos](https://github.com/crev-dev/cargo-crev/wiki/List-of-Proof-Repositories)
|
||||
with some strong rust developers in there. This may cover dependencies like `serde` and `snafu`
|
||||
but will not hit any CosmWasm-related modules, so we look to bootstrap a very focused
|
||||
review community.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Publishing Contracts
|
||||
|
||||
This is an overview of how to publish the contract's source code in this repo.
|
||||
We use Cargo's default registry [crates.io](https://crates.io/) for publishing contracts written in Rust.
|
||||
|
||||
## Preparation
|
||||
|
||||
Ensure the `Cargo.toml` file in the repo is properly configured. In particular, you want to
|
||||
choose a name starting with `cw-`, which will help a lot finding CosmWasm contracts when
|
||||
searching on crates.io. For the first publication, you will probably want version `0.1.0`.
|
||||
If you have tested this on a public net already and/or had an audit on the code,
|
||||
you can start with `1.0.0`, but that should imply some level of stability and confidence.
|
||||
You will want entries like the following in `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
name = "cw-escrow"
|
||||
version = "0.1.0"
|
||||
description = "Simple CosmWasm contract for an escrow with arbiter and timeout"
|
||||
repository = "https://github.com/confio/cosmwasm-examples"
|
||||
```
|
||||
|
||||
You will also want to add a valid [SPDX license statement](https://spdx.org/licenses/),
|
||||
so others know the rules for using this crate. You can use any license you wish,
|
||||
even a commercial license, but we recommend choosing one of the following, unless you have
|
||||
specific requirements.
|
||||
|
||||
* Permissive: [`Apache-2.0`](https://spdx.org/licenses/Apache-2.0.html#licenseText) or [`MIT`](https://spdx.org/licenses/MIT.html#licenseText)
|
||||
* Copyleft: [`GPL-3.0-or-later`](https://spdx.org/licenses/GPL-3.0-or-later.html#licenseText) or [`AGPL-3.0-or-later`](https://spdx.org/licenses/AGPL-3.0-or-later.html#licenseText)
|
||||
* Commercial license: `Commercial` (not sure if this works, I cannot find examples)
|
||||
|
||||
It is also helpful to download the LICENSE text (linked to above) and store this
|
||||
in a LICENSE file in your repo. Now, you have properly configured your crate for use
|
||||
in a larger ecosystem.
|
||||
|
||||
### Updating schema
|
||||
|
||||
To allow easy use of the contract, we can publish the schema (`schema/*.json`) together
|
||||
with the source code.
|
||||
|
||||
```sh
|
||||
cargo schema
|
||||
```
|
||||
|
||||
Ensure you check in all the schema files, and make a git commit with the final state.
|
||||
This commit will be published and should be tagged. Generally, you will want to
|
||||
tag with the version (eg. `v0.1.0`), but in the `cosmwasm-examples` repo, we have
|
||||
multiple contracts and label it like `escrow-0.1.0`. Don't forget a
|
||||
`git push && git push --tags`
|
||||
|
||||
### Note on build results
|
||||
|
||||
Build results like Wasm bytecode or expected hash don't need to be updated since
|
||||
the don't belong to the source publication. However, they are excluded from packaging
|
||||
in `Cargo.toml` which allows you to commit them to your git repository if you like.
|
||||
|
||||
```toml
|
||||
exclude = ["artifacts"]
|
||||
```
|
||||
|
||||
A single source code can be built with multiple different optimizers, so
|
||||
we should not make any strict assumptions on the tooling that will be used.
|
||||
|
||||
## Publishing
|
||||
|
||||
Now that your package is properly configured and all artifacts are committed, it
|
||||
is time to share it with the world.
|
||||
Please refer to the [complete instructions for any questions](https://rurust.github.io/cargo-docs-ru/crates-io.html),
|
||||
but I will try to give a quick overview of the happy path here.
|
||||
|
||||
### Registry
|
||||
|
||||
You will need an account on [crates.io](https://crates.io) to publish a rust crate.
|
||||
If you don't have one already, just click on "Log in with GitHub" in the top-right
|
||||
to quickly set up a free account. Once inside, click on your username (top-right),
|
||||
then "Account Settings". On the bottom, there is a section called "API Access".
|
||||
If you don't have this set up already, create a new token and use `cargo login`
|
||||
to set it up. This will now authenticate you with the `cargo` cli tool and allow
|
||||
you to publish.
|
||||
|
||||
### Uploading
|
||||
|
||||
Once this is set up, make sure you commit the current state you want to publish.
|
||||
Then try `cargo publish --dry-run`. If that works well, review the files that
|
||||
will be published via `cargo package --list`. If you are satisfied, you can now
|
||||
officially publish it via `cargo publish`.
|
||||
|
||||
Congratulations, your package is public to the world.
|
||||
|
||||
### Sharing
|
||||
|
||||
Once you have published your package, people can now find it by
|
||||
[searching for "cw-" on crates.io](https://crates.io/search?q=cw).
|
||||
But that isn't exactly the simplest way. To make things easier and help
|
||||
keep the ecosystem together, we suggest making a PR to add your package
|
||||
to the [`cawesome-wasm`](https://github.com/cosmwasm/cawesome-wasm) list.
|
||||
|
||||
### Organizations
|
||||
|
||||
Many times you are writing a contract not as a solo developer, but rather as
|
||||
part of an organization. You will want to allow colleagues to upload new
|
||||
versions of the contract to crates.io when you are on holiday.
|
||||
[These instructions show how]() you can set up your crate to allow multiple maintainers.
|
||||
|
||||
You can add another owner to the crate by specifying their github user. Note, you will
|
||||
now both have complete control of the crate, and they can remove you:
|
||||
|
||||
`cargo owner --add ethanfrey`
|
||||
|
||||
You can also add an existing github team inside your organization:
|
||||
|
||||
`cargo owner --add github:confio:developers`
|
||||
|
||||
The team will allow anyone who is currently in the team to publish new versions of the crate.
|
||||
And this is automatically updated when you make changes on github. However, it will not allow
|
||||
anyone in the team to add or remove other owners.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Nym Mixnet Contract
|
||||
|
||||
This is the [cosmwasm](https://www.cosmwasm.com) smart contract which runs the Nym mixnet.
|
||||
|
||||
## Compiling in development
|
||||
|
||||
```
|
||||
RUSTFLAGS='-C link-arg=-s' cargo wasm
|
||||
```
|
||||
|
||||
## CI Support
|
||||
|
||||
We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml)
|
||||
and [Circle CI](.circleci/config.yml) in the generated project, so you can
|
||||
get up and running with CI right away.
|
||||
|
||||
One note is that the CI runs all `cargo` commands
|
||||
with `--locked` to ensure it uses the exact same versions as you have locally. This also means
|
||||
you must have an up-to-date `Cargo.lock` file, which is not auto-generated.
|
||||
The first time you set up the project (or after adding any dep), you should ensure the
|
||||
`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by
|
||||
running `cargo check` or `cargo unit-test`.
|
||||
|
||||
## Using your project
|
||||
|
||||
Once you have your custom repo, you should check out [Developing](./Developing.md) to explain
|
||||
more on how to run tests and develop code. Or go through the
|
||||
[online tutorial](https://www.cosmwasm.com/docs/getting-started/intro) to get a better feel
|
||||
of how to develop.
|
||||
|
||||
[Publishing](./Publishing.md) contains useful information on how to publish your contract
|
||||
to the world, once you are ready to deploy it on a running blockchain. And
|
||||
[Importing](./Importing.md) contains information about pulling in other contracts or crates
|
||||
that have been published.
|
||||
|
||||
Please replace this README file with information about your specific project. You can keep
|
||||
the `Developing.md` and `Publishing.md` files as useful referenced, but please set some
|
||||
proper description in the README.
|
||||
@@ -0,0 +1 @@
|
||||
d8a3bddfd2d9f530ca373dfadf60a83eb1a199febec596a3ed37024a22012767 mixnode.wasm
|
||||
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
use std::env::current_dir;
|
||||
use std::fs::create_dir_all;
|
||||
|
||||
use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
|
||||
|
||||
use mixnet_contracts::state::State;
|
||||
use mixnet_contracts::{
|
||||
msg::{HandleMsg, InitMsg, QueryMsg},
|
||||
state::MixNodeBond,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let mut out_dir = current_dir().unwrap();
|
||||
out_dir.push("schema");
|
||||
create_dir_all(&out_dir).unwrap();
|
||||
remove_schemas(&out_dir).unwrap();
|
||||
|
||||
export_schema(&schema_for!(InitMsg), &out_dir);
|
||||
export_schema(&schema_for!(HandleMsg), &out_dir);
|
||||
export_schema(&schema_for!(QueryMsg), &out_dir);
|
||||
export_schema(&schema_for!(State), &out_dir);
|
||||
export_schema(&schema_for!(MixNodeBond), &out_dir);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# stable
|
||||
newline_style = "unix"
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
|
||||
# unstable... should we require `rustup run nightly cargo fmt` ?
|
||||
# or just update the style guide when they are stable?
|
||||
#fn_single_line = true
|
||||
#format_code_in_doc_comments = true
|
||||
#overflow_delimited_expr = true
|
||||
#reorder_impl_items = true
|
||||
#struct_field_align_threshold = 20
|
||||
#struct_lit_single_line = true
|
||||
#report_todo = "Always"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "HandleMsg",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"register_mixnode"
|
||||
],
|
||||
"properties": {
|
||||
"register_mixnode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_node"
|
||||
],
|
||||
"properties": {
|
||||
"mix_node": {
|
||||
"$ref": "#/definitions/MixNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"un_register_mixnode"
|
||||
],
|
||||
"properties": {
|
||||
"un_register_mixnode": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"MixNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"host",
|
||||
"layer",
|
||||
"location",
|
||||
"sphinx_key",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"layer": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"sphinx_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "InitMsg",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_topology"
|
||||
],
|
||||
"properties": {
|
||||
"get_topology": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "State",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_node_bonds",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"mix_node_bonds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/MixNodeBond"
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/HumanAddr"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HumanAddr": {
|
||||
"type": "string"
|
||||
},
|
||||
"MixNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"host",
|
||||
"layer",
|
||||
"location",
|
||||
"sphinx_key",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"layer": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"sphinx_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MixNodeBond": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"mix_node",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"mix_node": {
|
||||
"$ref": "#/definitions/MixNode"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/HumanAddr"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Uint128": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Topology",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mix_node_bonds"
|
||||
],
|
||||
"properties": {
|
||||
"mix_node_bonds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/MixNodeBond"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"Coin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"denom"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"$ref": "#/definitions/Uint128"
|
||||
},
|
||||
"denom": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HumanAddr": {
|
||||
"type": "string"
|
||||
},
|
||||
"MixNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"host",
|
||||
"layer",
|
||||
"location",
|
||||
"sphinx_key",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"layer": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"sphinx_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MixNodeBond": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount",
|
||||
"mix_node",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Coin"
|
||||
}
|
||||
},
|
||||
"mix_node": {
|
||||
"$ref": "#/definitions/MixNode"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/HumanAddr"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Uint128": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
use crate::msg::{HandleMsg, InitMsg, QueryMsg};
|
||||
use crate::queries::query_mixnodes_paged;
|
||||
use crate::state::{config, MixNode, MixNodeBond, State};
|
||||
use crate::{error::ContractError, state::mixnodes, state::mixnodes_read};
|
||||
use cosmwasm_std::{
|
||||
attr, coins, to_binary, BankMsg, Binary, Deps, DepsMut, Env, HandleResponse, InitResponse,
|
||||
MessageInfo, StdResult,
|
||||
};
|
||||
|
||||
/// Instantiate the contract.
|
||||
///
|
||||
/// `deps` contains Storage, API and Querier
|
||||
/// `env` contains block, message and contract info
|
||||
/// `msg` is the contract initialization message, sort of like a constructor call.
|
||||
pub fn init(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
info: MessageInfo,
|
||||
_msg: InitMsg,
|
||||
) -> Result<InitResponse, ContractError> {
|
||||
let state = State { owner: info.sender };
|
||||
config(deps.storage).save(&state)?;
|
||||
Ok(InitResponse::default())
|
||||
}
|
||||
|
||||
/// Handle an incoming message
|
||||
pub fn handle(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: HandleMsg,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
match msg {
|
||||
HandleMsg::RegisterMixnode { mix_node } => try_add_mixnode(deps, info, mix_node),
|
||||
HandleMsg::UnRegisterMixnode {} => try_remove_mixnode(deps, info, env),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_add_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
mix_node: MixNode,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let stake = &info.sent_funds[0];
|
||||
|
||||
// check that the denomination is correct
|
||||
if stake.denom != "unym" {
|
||||
return Err(ContractError::WrongDenom {});
|
||||
}
|
||||
// check that we have at least 1000 nym in our bond
|
||||
if stake.amount < coins(1000_000000, "unym")[0].amount {
|
||||
return Err(ContractError::InsufficientBond {});
|
||||
}
|
||||
|
||||
let bond = MixNodeBond {
|
||||
amount: info.sent_funds,
|
||||
owner: info.sender.clone(),
|
||||
mix_node,
|
||||
};
|
||||
|
||||
mixnodes(deps.storage).save(info.sender.as_bytes(), &bond)?;
|
||||
Ok(HandleResponse::default())
|
||||
}
|
||||
|
||||
fn try_remove_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
// find the bond, return ContractError::MixNodeBondNotFound if it doesn't exist
|
||||
let mixnode_bond = match mixnodes_read(deps.storage).may_load(info.sender.as_bytes())? {
|
||||
None => return Err(ContractError::MixNodeBondNotFound {}),
|
||||
Some(bond) => bond,
|
||||
};
|
||||
|
||||
// send bonded funds back to the bond owner
|
||||
let messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender.clone(),
|
||||
amount: mixnode_bond.amount.clone(),
|
||||
}
|
||||
.into()];
|
||||
|
||||
// remove the bond from the list of bonded mixnodes
|
||||
mixnodes(deps.storage).remove(info.sender.as_bytes());
|
||||
|
||||
// log our actions
|
||||
let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)];
|
||||
|
||||
Ok(HandleResponse {
|
||||
messages,
|
||||
attributes,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
|
||||
match msg {
|
||||
QueryMsg::GetMixNodes { start_after, limit } => {
|
||||
to_binary(&query_mixnodes_paged(deps, start_after, limit)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::queries::PagedResponse;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::*;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, from_binary};
|
||||
|
||||
#[test]
|
||||
fn initialize_contract() {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let env = mock_env();
|
||||
let msg = InitMsg {};
|
||||
let info = mock_info("creator", &[]);
|
||||
|
||||
let res = init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
assert_eq!(0, res.messages.len());
|
||||
|
||||
// mix_node_bonds should be empty after initialization
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
env.clone(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len()); // there are no mixnodes in the list when it's just been initialized
|
||||
|
||||
// Contract balance should match what we initialized it as
|
||||
assert_eq!(
|
||||
coins(0, "unym"),
|
||||
query_contract_balance(env.contract.address, deps)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_add() {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
// if we don't send enough funds
|
||||
let info = mock_info("anyone", &coins(999_999999, "unym"));
|
||||
let msg = HandleMsg::RegisterMixnode {
|
||||
mix_node: helpers::mix_node_fixture(),
|
||||
};
|
||||
|
||||
// we are informed that we didn't send enough funds
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(result, Err(ContractError::InsufficientBond {}));
|
||||
|
||||
// no mixnode was inserted into the topology
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
assert_eq!(0, page.nodes.len());
|
||||
|
||||
// if we send enough funds
|
||||
let info = mock_info("anyone", &coins(1000_000000, "unym"));
|
||||
let msg = HandleMsg::RegisterMixnode {
|
||||
mix_node: helpers::mix_node_fixture(),
|
||||
};
|
||||
|
||||
// we get back a message telling us everything was OK
|
||||
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
|
||||
assert_eq!(HandleResponse::default(), handle_response);
|
||||
|
||||
// we can query topology and the new node is there
|
||||
let query_response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&query_response).unwrap();
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!(
|
||||
helpers::mix_node_fixture().location,
|
||||
page.nodes[0].mix_node.location
|
||||
)
|
||||
|
||||
// adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet.
|
||||
// if we attempt to register a second node from the same address, should we get an error? It would probably be polite.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnode_remove() {
|
||||
let env = mock_env();
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let msg = InitMsg {};
|
||||
let info = mock_info("creator", &[]);
|
||||
init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
|
||||
// try un-registering when no nodes exist yet
|
||||
let info = mock_info("anyone", &coins(999_9999, "unym"));
|
||||
let msg = HandleMsg::UnRegisterMixnode {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
|
||||
// we're told that there is no node for our address
|
||||
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
|
||||
|
||||
// let's add a node owned by bob
|
||||
helpers::add_mixnode("bob", coins(1000_000000, "unym"), &mut deps);
|
||||
|
||||
// attempt to un-register fred's node, which doesn't exist
|
||||
let info = mock_info("fred", &coins(999_9999, "unym"));
|
||||
let msg = HandleMsg::UnRegisterMixnode {};
|
||||
let result = handle(deps.as_mut(), mock_env(), info, msg);
|
||||
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
|
||||
|
||||
// bob's node is still there
|
||||
let res = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let page: PagedResponse = from_binary(&res).unwrap();
|
||||
let first_node = &page.nodes[0];
|
||||
assert_eq!(1, page.nodes.len());
|
||||
assert_eq!("bob", first_node.owner);
|
||||
|
||||
// add a node owned by fred
|
||||
let fred_bond = coins(1666_000000, "unym");
|
||||
helpers::add_mixnode("fred", fred_bond.clone(), &mut deps);
|
||||
|
||||
// let's make sure we now have 2 nodes:
|
||||
assert_eq!(2, helpers::get_mix_nodes(&mut deps).len());
|
||||
|
||||
// un-register fred's node
|
||||
let info = mock_info("fred", &coins(999_9999, "unym"));
|
||||
let msg = HandleMsg::UnRegisterMixnode {};
|
||||
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
|
||||
|
||||
// we should see log messages come back showing an unbond message
|
||||
let expected_attributes = vec![
|
||||
attr("action", "unbond"),
|
||||
attr("tokens", fred_bond.clone()[0].amount),
|
||||
attr("account", "fred"),
|
||||
];
|
||||
|
||||
// we should see a funds transfer from the contract back to fred
|
||||
let expected_messages = vec![BankMsg::Send {
|
||||
from_address: env.contract.address,
|
||||
to_address: info.sender,
|
||||
amount: fred_bond,
|
||||
}
|
||||
.into()];
|
||||
|
||||
// run the handler and check that we got back the correct results
|
||||
let expected = HandleResponse {
|
||||
messages: expected_messages,
|
||||
attributes: expected_attributes,
|
||||
data: None,
|
||||
};
|
||||
assert_eq!(remove_fred, expected);
|
||||
|
||||
// only 1 node now exists, owned by bob:
|
||||
let mix_node_bonds = helpers::get_mix_nodes(&mut deps);
|
||||
assert_eq!(1, mix_node_bonds.len());
|
||||
assert_eq!("bob", mix_node_bonds[0].owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use cosmwasm_std::StdError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Custom errors for contract failure conditions.
|
||||
///
|
||||
/// Add any other custom errors you like here.
|
||||
/// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details.
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum ContractError {
|
||||
#[error("{0}")]
|
||||
Std(#[from] StdError),
|
||||
|
||||
#[error("Not enough funds sent for mixnode bond")]
|
||||
InsufficientBond {},
|
||||
|
||||
#[error("Account does not own any mixnode bonds")]
|
||||
MixNodeBondNotFound {},
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized {},
|
||||
|
||||
#[error("Wrong coin denomination, you must send unym")]
|
||||
WrongDenom {},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod contract;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod queries;
|
||||
pub mod state;
|
||||
pub mod support;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
cosmwasm_std::create_entry_points!(contract);
|
||||
@@ -0,0 +1,24 @@
|
||||
use cosmwasm_std::HumanAddr;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::state::MixNode;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct InitMsg {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HandleMsg {
|
||||
RegisterMixnode { mix_node: MixNode },
|
||||
UnRegisterMixnode {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
GetMixNodes {
|
||||
limit: Option<u32>,
|
||||
start_after: Option<HumanAddr>,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// settings for pagination
|
||||
use crate::state::MixNodeBond;
|
||||
use crate::state::PREFIX_MIXNODES;
|
||||
use cosmwasm_std::Deps;
|
||||
use cosmwasm_std::HumanAddr;
|
||||
use cosmwasm_std::Order;
|
||||
use cosmwasm_std::StdResult;
|
||||
use cosmwasm_storage::bucket_read;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const MAX_LIMIT: u32 = 30;
|
||||
const DEFAULT_LIMIT: u32 = 10;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct PagedResponse {
|
||||
pub nodes: Vec<MixNodeBond>,
|
||||
pub per_page: usize,
|
||||
pub start_next_after: Option<HumanAddr>,
|
||||
}
|
||||
|
||||
pub fn query_mixnodes_paged(
|
||||
deps: Deps,
|
||||
start_after: Option<HumanAddr>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedResponse> {
|
||||
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let bucket = bucket_read::<MixNodeBond>(deps.storage, PREFIX_MIXNODES);
|
||||
let res = bucket
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit);
|
||||
let node_tuples = res.collect::<StdResult<Vec<(Vec<u8>, MixNodeBond)>>>()?;
|
||||
let nodes = node_tuples.into_iter().map(|item| item.1).collect();
|
||||
let start_next_after = last_node_owner(&nodes);
|
||||
|
||||
let response = PagedResponse {
|
||||
nodes,
|
||||
per_page: limit,
|
||||
start_next_after,
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm
|
||||
/// to get the succeeding key as the start of the next page.
|
||||
fn calculate_start_value(
|
||||
start_after: std::option::Option<cosmwasm_std::HumanAddr>,
|
||||
) -> Option<Vec<u8>> {
|
||||
start_after.as_ref().map(|addr| {
|
||||
let mut bytes = addr.as_bytes().to_owned();
|
||||
bytes.push(0);
|
||||
bytes
|
||||
})
|
||||
}
|
||||
|
||||
fn last_node_owner(nodes: &Vec<MixNodeBond>) -> Option<HumanAddr> {
|
||||
match nodes.last() {
|
||||
None => None,
|
||||
Some(node) => Some(node.owner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::mixnodes;
|
||||
use crate::support::tests::helpers;
|
||||
|
||||
#[test]
|
||||
fn mixnodes_empty_on_init() {
|
||||
let deps = helpers::init_contract();
|
||||
let response = query_mixnodes_paged(deps.as_ref(), None, Option::from(2)).unwrap();
|
||||
assert_eq!(0, response.nodes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnodes_paged_retrieval_obeys_limits() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let storage = deps.as_mut().storage;
|
||||
let limit = 2;
|
||||
for n in 0..10000 {
|
||||
let key = format!("bond{}", n);
|
||||
let node = helpers::mixnode_bond_fixture();
|
||||
mixnodes(storage).save(key.as_bytes(), &node).unwrap();
|
||||
}
|
||||
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
|
||||
assert_eq!(limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnodes_paged_retrieval_has_default_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let storage = deps.as_mut().storage;
|
||||
for n in 0..100 {
|
||||
let key = format!("bond{}", n);
|
||||
let node = helpers::mixnode_bond_fixture();
|
||||
mixnodes(storage).save(key.as_bytes(), &node).unwrap();
|
||||
}
|
||||
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, None).unwrap();
|
||||
|
||||
let expected_limit = 10;
|
||||
assert_eq!(expected_limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixnodes_paged_retrieval_has_max_limit() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let storage = deps.as_mut().storage;
|
||||
for n in 0..10000 {
|
||||
let key = format!("bond{}", n);
|
||||
let node = helpers::mixnode_bond_fixture();
|
||||
mixnodes(storage).save(key.as_bytes(), &node).unwrap();
|
||||
}
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000;
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = 30;
|
||||
assert_eq!(expected_limit, page1.nodes.len() as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagination_works() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let node = helpers::mixnode_bond_fixture();
|
||||
mixnodes(&mut deps.storage)
|
||||
.save("1".as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
let per_page = 2;
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.nodes.len());
|
||||
|
||||
// save another
|
||||
mixnodes(&mut deps.storage)
|
||||
.save("2".as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
// page1 should have 2 results on it
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.nodes.len());
|
||||
|
||||
mixnodes(&mut deps.storage)
|
||||
.save("3".as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
// page1 still has 2 results
|
||||
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.nodes.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = HumanAddr::from("2");
|
||||
let page2 = query_mixnodes_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.nodes.len());
|
||||
|
||||
// save another one
|
||||
mixnodes(&mut deps.storage)
|
||||
.save("4".as_bytes(), &node)
|
||||
.unwrap();
|
||||
|
||||
let start_after = HumanAddr::from("2");
|
||||
let page2 = query_mixnodes_paged(
|
||||
deps.as_ref(),
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.nodes.len());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use cosmwasm_std::Coin;
|
||||
use cosmwasm_std::{HumanAddr, Storage};
|
||||
use cosmwasm_storage::{
|
||||
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
// Contract-level stuff
|
||||
|
||||
pub static CONFIG_KEY: &[u8] = b"config";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct State {
|
||||
pub owner: HumanAddr,
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<State> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
// Mixnode-related stuff
|
||||
|
||||
pub const PREFIX_MIXNODES: &[u8] = b"mixnodes";
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct MixNodeBond {
|
||||
pub(crate) amount: Vec<Coin>,
|
||||
pub(crate) owner: HumanAddr,
|
||||
pub(crate) mix_node: MixNode,
|
||||
}
|
||||
|
||||
impl Display for MixNodeBond {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// Write strictly the first element into the supplied output
|
||||
// stream: `f`. Returns `fmt::Result` which indicates whether the
|
||||
// operation succeeded or failed. Note that `write!` uses syntax which
|
||||
// is very similar to `println!`.
|
||||
write!(f, "amount: {:?}, owner: {}", self.amount, self.owner)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub struct MixNode {
|
||||
pub(crate) host: String,
|
||||
pub(crate) layer: u64,
|
||||
pub(crate) location: String,
|
||||
pub(crate) sphinx_key: String,
|
||||
pub(crate) version: String,
|
||||
}
|
||||
|
||||
pub fn mixnodes(storage: &mut dyn Storage) -> Bucket<MixNodeBond> {
|
||||
bucket(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
pub fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket<MixNodeBond> {
|
||||
bucket_read(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers::mixnode_bond_fixture;
|
||||
use cosmwasm_std::testing::MockStorage;
|
||||
|
||||
#[test]
|
||||
fn mixnode_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = mixnode_bond_fixture();
|
||||
let bond2 = mixnode_bond_fixture();
|
||||
mixnodes(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
mixnodes(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = mixnodes_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = mixnodes_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod tests;
|
||||
@@ -0,0 +1,92 @@
|
||||
#[cfg(test)]
|
||||
pub mod helpers {
|
||||
use super::*;
|
||||
use crate::contract::init;
|
||||
use crate::contract::query;
|
||||
use crate::contract::try_add_mixnode;
|
||||
use crate::msg::InitMsg;
|
||||
use crate::msg::QueryMsg;
|
||||
use crate::queries::PagedResponse;
|
||||
use crate::state::MixNode;
|
||||
use crate::state::MixNodeBond;
|
||||
use cosmwasm_std::coins;
|
||||
use cosmwasm_std::from_binary;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
use cosmwasm_std::testing::MockStorage;
|
||||
use cosmwasm_std::Coin;
|
||||
use cosmwasm_std::HumanAddr;
|
||||
use cosmwasm_std::OwnedDeps;
|
||||
use cosmwasm_std::{Empty, MemoryStorage};
|
||||
|
||||
pub fn add_mixnode(
|
||||
pubkey: &str,
|
||||
stake: Vec<Coin>,
|
||||
deps: &mut OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
) {
|
||||
let info = mock_info(pubkey, &stake);
|
||||
try_add_mixnode(deps.as_mut(), info, helpers::mix_node_fixture()).unwrap();
|
||||
}
|
||||
|
||||
pub fn get_mix_nodes(
|
||||
deps: &mut OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
) -> Vec<MixNodeBond> {
|
||||
let result = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::GetMixNodes {
|
||||
start_after: None,
|
||||
limit: Option::from(2),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let page: PagedResponse = from_binary(&result).unwrap();
|
||||
page.nodes
|
||||
}
|
||||
|
||||
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies(&[]);
|
||||
let msg = InitMsg {};
|
||||
let env = mock_env();
|
||||
let info = mock_info("creator", &[]);
|
||||
init(deps.as_mut(), env.clone(), info, msg).unwrap();
|
||||
return deps;
|
||||
}
|
||||
|
||||
pub fn mix_node_fixture() -> MixNode {
|
||||
MixNode {
|
||||
host: "mix.node.org".to_string(),
|
||||
layer: 1,
|
||||
location: "Sweden".to_string(),
|
||||
sphinx_key: "sphinx".to_string(),
|
||||
version: "0.10.0".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mixnode_bond_fixture() -> MixNodeBond {
|
||||
let mix_node = MixNode {
|
||||
host: "1.1.1.1".to_string(),
|
||||
layer: 1,
|
||||
location: "London".to_string(),
|
||||
sphinx_key: "1234".to_string(),
|
||||
version: "0.10.0".to_string(),
|
||||
};
|
||||
MixNodeBond {
|
||||
amount: coins(50, "unym"),
|
||||
owner: HumanAddr::from("foo"),
|
||||
mix_node,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_contract_balance(
|
||||
address: HumanAddr,
|
||||
deps: OwnedDeps<MockStorage, MockApi, MockQuerier>,
|
||||
) -> Vec<Coin> {
|
||||
let querier = deps.as_ref().querier;
|
||||
vec![querier.query_balance(address, "unym").unwrap()]
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2020 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crypto::asymmetric::identity;
|
||||
use futures::stream::Stream;
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::monitor::preparer::{PacketPreparer, PreparedPackets};
|
||||
use crate::monitor::processor::ReceivedProcessor;
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::chunker::Chunker;
|
||||
use crate::monitor::sender::GatewayPackets;
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::gateways_reader::GatewayMessages;
|
||||
use crate::test_packet::TestPacket;
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::gateways_reader::{GatewayChannel, GatewayMessages, GatewaysReader};
|
||||
use crate::monitor::processor::ReceivedProcessorSender;
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender};
|
||||
use crate::TIME_CHUNK_SIZE;
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::monitor::preparer::InvalidNode;
|
||||
use crate::test_packet::TestPacket;
|
||||
|
||||
@@ -111,10 +111,7 @@ impl OutboundRequestFilter {
|
||||
/// Attempts to get the root domain, shorn of subdomains, using publicsuffix.
|
||||
fn get_domain_root(&self, host: &str) -> Option<String> {
|
||||
match self.domain_list.parse_domain(host) {
|
||||
Ok(d) => match d.root() {
|
||||
Some(root) => Some(root.to_string()),
|
||||
None => None, // no domain root matches
|
||||
},
|
||||
Ok(d) => d.root().map(|root| root.to_string()),
|
||||
Err(_) => {
|
||||
log::warn!("Error parsing domain: {:?}", host);
|
||||
None // domain couldn't be parsed
|
||||
|
||||
Reference in New Issue
Block a user