From 82f637649c75b317db63ef1486f4cf635b1fe739 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Fri, 27 Mar 2020 22:54:00 -0400 Subject: [PATCH] [R4R] Testnet-5k proposal (#404) * R4R: BEP3 module (#370) * bep3 module scaffold from cosmos/scaffold * Populated types, keeper with HTLT msgs, module params, and scaffolding for keys, and genesis * added KavaHTLT struct, UpdateHTLT struct, resolved compilation errors * refactored kavaHTLT struct <-> msgs * Implemented params, refactored UpdateKavaHTLT to UpdateKHTLT interface * Updated keeper with byTimeIndex methods * HTLT creation flow * adjustments in prep for repo config updates * App moudle updated for bep3, MsgCreateHTLT tested, HTLT keeper methods tested * Updated bep3 params to match spec * tests for MsgRefundHTLT, MsgDepositHTLT, MsgClaimHTLT * AddHtlt cli cmd, queryHtlts cmd, added conversion funcs for binance -> cosmos types, refactored MsgCreateHTLT from binance.AccAddress to sdk.AccAddress * working edits related to bep3-deputy compatibility * removed binance-chain go sdk dependency * updated msg ValidateBasic() return to sdk.Error type * implement MsgCalculateSwapID * added MsgCalculateSwapID test, updated randomNumberHash type to []byte * removed binance type conversions * msg codec registration * clean /types directory * CLI cmds:create htlt, query htlt * update keeper logic * handle MsgCreateHTLT * implement htlt type, msg types * implement global chain types * update querier * added go-ethereum to go mod * refactor QuerySwap to QueryHTLT * update HTLTMsg to MsgCreateHTLT * implemented htlt deposit * add token transfer to MsgCreateHTLT * implement refund, claim client txs * add refund/claim cmds to tx cmd * commiting go.sum for build * implemented keeper claim logic * add RandomNumberHash to create-htlt event * implement refund keeper logic * AddHTLT updated to CreateHTLT * added params keeper * updated params to single chain, added sample genesis file * implemented htlt keeper param checks * removed go-ethereum dependency * updated go.sum * housekeeping on keeper tests * updated cli tx cmds * ran go.tidy * remove links from module readme * updated coin construction in tests * added expectedIncome checks in ValidateBasic() * made ValidateAsset() more robust * update param format for tests * added basic HTLTByTime index * implement abci, fix expectedIncome validation * byTime index updated to blocks, added swap ID & expiration block to htlt * added not-expired check to HTLT claims * cross-chain mint/burn logic, htlt string type refactored to []byte * fix bnb_deputy_address param * remove abci panic * cmn.HexBytes, byTime index iteration update, claim-mint logic update * update genesis example * general codebase cleaning * renamed HTLT to AtomicSwap * staging for PR * updated naming conventions * refactor + revisions * removed code related to deposits & swap block index * added timestamp validation comment * post-refactor housekeeping * post refactor housekeeping (keeper) * remove GenesisAtomicSwap type * refactor asset supply logic * BeginBlocker expires swaps automatically * param asset.limit type updated to sdk.Int * remove claimed swaps from block index * fix DefaultDeputyAddress * removed BaseSwap * revisions * total genesis coins * updated tx examples * Automatically update fees for risky cdps (#381) * wip: sketch implementation * adding initial function to calcuate risky fees for cdps * adding todo comment to fix the function arguments * changing the function arguments * adding multiplication, print error, change types * get the number of periods, add comments and questios for code review * adding specification notes * removing old comment * replace collateral with collateral denom * remove todo and clarify comment * Update x/cdp/keeper/fees.go Co-Authored-By: Kevin Davis * Update x/cdp/keeper/fees.go Co-Authored-By: Kevin Davis * Update go.sum Co-Authored-By: Kevin Davis * Update x/cdp/keeper/fees.go Co-Authored-By: Kevin Davis * Update x/cdp/keeper/fees.go Co-Authored-By: Kevin Davis * Update x/cdp/keeper/fees.go Co-Authored-By: Kevin Davis * Update x/cdp/abci.go Co-Authored-By: Kevin Davis * updating fees * error handling and propogation * fix collat denom variable * fix build issues, error, variable names, parameter type * Update x/cdp/abci.go Use `err` as name instead of `e` Co-Authored-By: Kevin Davis * Update x/cdp/keeper/fees.go Co-Authored-By: Kevin Davis * fixing error variable name * changing call to method to compute risky cdps fees * changing the calcualation to select risky cdps to be based on normalized ratio * adding skeleton for test methods, adding skeleton helper function for creating cdps for use in tests * fixing function call to helper method * fix assignment, calling function that returns two variables instead of one * adding comment and fixing call to create cdps * adding create cdps function and updating / fixing the test. one of the expected tests is failing, need to figure out why that is. added a todo question to note it * logging the cdp object before and after updating. it seems that the fees are not set / written before or after * adding interim changed * updated * fixing normalized ratio * code cleanup * changing print of accumulated fees * removing debug code * remove completed todo * remove old variable * remove spewing print statement * remove dead todo * adding note about prices for future * remove dead code * try to fix test * fix types * fix types * fix context in call * changing back as new version breaks a test * fix * cleanup, removing logging and old code * remove dead code * removing changes to cdp test * Update x/cdp/keeper/fees.go Remove old comment as suggested Co-Authored-By: Kevin Davis * Update x/cdp/spec/04_begin_block.md Fix typo as suggested Co-Authored-By: Kevin Davis Co-authored-by: Kevin Davis * Add Savings Rate (#365) * fix: ensure cdp module accounts created at gensis * feat: add savings rate * chore: update alias * fix: update default test param values * chore: update spec for savings rate * fix: add distribution time to genesis state * fix: iterate over accounts using callback function * feat: use seprate mod account for savings rate * fix: remove mod account coins from total supply * address review comments * fix: genesis function initialization * fix: update alias * add comment about maintaining module account list * feat: add genesis example * R4R: bep3 module upgrades (#388) * bep3 module scaffold from cosmos/scaffold * Populated types, keeper with HTLT msgs, module params, and scaffolding for keys, and genesis * added KavaHTLT struct, UpdateHTLT struct, resolved compilation errors * refactored kavaHTLT struct <-> msgs * Implemented params, refactored UpdateKavaHTLT to UpdateKHTLT interface * Updated keeper with byTimeIndex methods * HTLT creation flow * adjustments in prep for repo config updates * App moudle updated for bep3, MsgCreateHTLT tested, HTLT keeper methods tested * Updated bep3 params to match spec * tests for MsgRefundHTLT, MsgDepositHTLT, MsgClaimHTLT * AddHtlt cli cmd, queryHtlts cmd, added conversion funcs for binance -> cosmos types, refactored MsgCreateHTLT from binance.AccAddress to sdk.AccAddress * working edits related to bep3-deputy compatibility * removed binance-chain go sdk dependency * updated msg ValidateBasic() return to sdk.Error type * implement MsgCalculateSwapID * added MsgCalculateSwapID test, updated randomNumberHash type to []byte * removed binance type conversions * msg codec registration * clean /types directory * CLI cmds:create htlt, query htlt * update keeper logic * handle MsgCreateHTLT * implement htlt type, msg types * implement global chain types * update querier * added go-ethereum to go mod * refactor QuerySwap to QueryHTLT * update HTLTMsg to MsgCreateHTLT * implemented htlt deposit * add token transfer to MsgCreateHTLT * implement refund, claim client txs * add refund/claim cmds to tx cmd * commiting go.sum for build * implemented keeper claim logic * add RandomNumberHash to create-htlt event * implement refund keeper logic * AddHTLT updated to CreateHTLT * added params keeper * updated params to single chain, added sample genesis file * implemented htlt keeper param checks * removed go-ethereum dependency * updated go.sum * housekeeping on keeper tests * updated cli tx cmds * ran go.tidy * remove links from module readme * updated coin construction in tests * added expectedIncome checks in ValidateBasic() * made ValidateAsset() more robust * update param format for tests * added basic HTLTByTime index * implement abci, fix expectedIncome validation * byTime index updated to blocks, added swap ID & expiration block to htlt * added not-expired check to HTLT claims * cross-chain mint/burn logic, htlt string type refactored to []byte * fix bnb_deputy_address param * remove abci panic * cmn.HexBytes, byTime index iteration update, claim-mint logic update * update genesis example * general codebase cleaning * renamed HTLT to AtomicSwap * staging for PR * updated naming conventions * refactor + revisions * removed code related to deposits & swap block index * added timestamp validation comment * post-refactor housekeeping * post refactor housekeeping (keeper) * remove GenesisAtomicSwap type * refactor asset supply logic * BeginBlocker expires swaps automatically * param asset.limit type updated to sdk.Int * remove claimed swaps from block index * fix DefaultDeputyAddress * removed BaseSwap * revisions * total genesis coins * updated tx examples * timestamp to unix * add past timestamp limit * update random number byte encoding * add recipient_other_chain to AtomicSwap * add TODO for timestamp arg parsing * generate secure random numbers * update tx cli * keeper tests * add bnb token * bep3 params test set up, test CreateAtomicSwap * swap table tests * Revert "bep3 params test set up, test CreateAtomicSwap" This reverts commits containing tests. * use tmtime.Now() * Kava distribution module (#387) * wip: kavadist module structure * feat: implement minting logic * wip: sketch module * wip: module level code * wip: bug fixes * wip: add tests * wip: resolve todos and tidy * fix: remove unused file * address review comments * fix: update genesis for guide (#394) * add kava_dist to sample genesis file (#396) * R4R: BEP3 module test suite (#395) * refactor secure rng * refactor common tests, implement keeper tests * implement asset tests * implement params, querier tests * implement keeper swap tests * refactor import naming conventions * implement core types tests * improve keeper swap tests * implement genesis types test * implement params test + revisions * implement duplicate swap test * implement duplicate swap ID test * R4R: BEP3 additional features + module test suite (#397) * update and reorder errors * implement swap deletion block delay * add swap deletion block delay, set up tests * add secure random number gen * implement AtomicSwapLongtermStorage index * fix syntax error * abci test updates * implement handler test * implement core genesis tests * update asset supply logic * implement functional asset supply * pretty print atomic swaps * requested revisions * fix test suite post merge * implement and integrate asset supply tests * update import genesis, add storage duration param * implement swap deletion ABCI test * go mod tidy * remove duplicated interface assertion * add new bep3 param to contrib genesis file * remove btc from supported assets * revisions: LongtermStorageDuration param * revisions: suite ctx, fix genesis, update contrib * implement AssetSupply type, store key, keeper * integrate supply and swaps; genesis, tests * remove legacy comments * requested revisions * update alias * Swagger Rest Automating Testing With Dredd (#390) * swagger testing and mods * fixed a test * fixed withdraw address * adding script to start the chain * fixed val rewards test * fix outstanding rewards test * fixed rewards * hooks skeleton * adding test file and hooks * updates on the hooks working * now creates a transaction and sends to the chain via rest api successfully * small fix - now works * 34 tests now passing successfully * fix print statement error * instructions on how to run the tests * changing function names when to run the hooks * adding instructions on how to setup and run the dredd tests * removing large error output file * removing binary file * removing more output logging files * creating a vote on a proposal to send to the blockchain * adding instructions on how to setup chain * adding function to get account number and sequence number * adding send msg to blockchain method * posting vote tx to blockchain - successfully prepares and sends vote to endpoint but endpoint returns 'inactive proposal with id' * successfully depositing 600 stake to a proposal * successfully depositing onto a proposal and then voting on it * got another governance test working now after submitting a vote to the blockchain * updating instructions on how to run * fixed another voting test * fixed deposits test * fixed another gov test * fix print line * fix circle ci build issue with println * improving instructions on how to build and run the hooks and dredd tests * improving instructions on how to build and run the hooks and dredd tests * finally fixed param change governance proposal test * trying to unskip tests wip * fixed gov/proposals test * fixed another test * fixed a slashing test * fixed another redelegation test * fixed another unbonding delegation test * fixed more staking tests * fixed another staking test * fixed another test * fixed more tests. 50 now passing, 15 failing * fixed mislabeled variable * managed to fix unjail test * fixed bank acct transfers test * change certain types from number to string to match the output, typo fix * another typo fix * fixed delegation test * finally figured out and fixed the latest blocks types mismatch - fixed the test * fixed staking delegators validators test * removed and noted unimplemented tests from yaml file. fixed blocks height test * fixed transcations test * adding functionality to send transfer of coins to blockchain, and to send delegations * updating the yaml to line up with a valid message format * added delegation method * adding test results showing 57 are now passing and only 5 failing * remove test yaml file from pull req * testing file updates * adding test memo * added undelegation hook method - fixed unbonding delegation test * fixed the get tx from hash test * adding not if you encounter validator set is different errors how to fix. 59 tests now passsing, 3 failing * adding test results showing 59 passing, 3 failing * finally fixed encode test - 60 tests now passing only 2 failing * adding test results 60 passing 1 failing * more test updates * finally fixed decode test - 61 tests now passing only 1 failing * test results 61 tests passing 1 failing * remove dead code * all 62 tests are now passingga swagger.yaml 0 failing * used for testing and generating transactions and testing hooks * updating run instructions * more instructions updates * updating the test file * adding note on reading from a file * refactoring code and cleanup * refactoring getting the keybase * code cleanup for address, keyname methods, remove unused code * more code cleanup around addresses * updating the instructions on how to run the dredd tests * adding comment * adding additional requirements to the go.mod dependency file * remove hardcoded home directory, read using os golang library * increase timeout in example run script * remove hardcoded home directory * reordering commands to get rid of errors if key directory is deleted * changing to use temporary directory * updating dredd timeout time * finally managed to get the script wroking using a temporary directory instead of the default * adding notes and comments * changing to use a temp directory instead of default directory * remove un-needed file * rename debugging tools folder * adding instructions to install dredd and npm * Update swagger-ui/startchain.sh Send output to dev null not to console Co-Authored-By: Kevin Davis * Update swagger-ui/startchain.sh Send output to dev null not to console Co-Authored-By: Kevin Davis * adding new version of test.go to setup the chain * adding todo to update instructions for new workflow * updating script to start and setup the chain * updating the transaction hash test * update the start chain script to setup the chain correctly * add the script to stop the chain and the rest server * updated the instructions for the new workflow so that all the tests pass the first time * updated the instructions on how to run the tests * update instructions for printing logs or not * updating the startchain script to add messages when starting the rest server and preparing transactions * adding print messages when stop chain is completed * updating test results to just include test output and not the debug log statements * cleaning up the messages that are printed to the user * moving files to their own directory * build go test file and remove previous binary * move instructions * updating instructions now that test file is auto built * building, running dredd tests, propagating error code, shut down blockchain all in one script * fix object type to array type for block latests * cleaning up the script * rename script as it now does all the setup, test running, shutdown, and cleanup * update instructions for new workflow * adding a shell script to call from the makefile * adding a make command to build and run all the dredd tests * update instructions to run using make * updated code review comment * minor update to instructions * update remove file command so doesn't print an error if the file has already been deleted * renaming folder and test * adjust code comment * removing example test results * updating instructions to remove reference to the test results * remove old hooks file * remove obsolete code comment * remove swagger file, will change references to the other one * remove shell script, will now use the one called from make instead * renaming as underscore messes up go build * clean up script, fix return code issues * cleanup output file * fix object to array issue * add comments to explain functionality * use variables for kvd home and kvcli home, check for errors * change the kvcli home directory. need to take this from command line * take kvcli from command line parameter to golang file * take kvcli directory from command line parameter Co-authored-by: John Maheswaran Co-authored-by: Kevin Davis * R4R: Update BEP3 rest endpoints + format example requests (#402) * update and reorder errors * implement swap deletion block delay * add swap deletion block delay, set up tests * add secure random number gen * implement AtomicSwapLongtermStorage index * fix syntax error * abci test updates * implement handler test * implement core genesis tests * update asset supply logic * implement functional asset supply * pretty print atomic swaps * requested revisions * fix test suite post merge * implement and integrate asset supply tests * update import genesis, add storage duration param * implement swap deletion ABCI test * go mod tidy * remove duplicated interface assertion * add new bep3 param to contrib genesis file * remove btc from supported assets * revisions: LongtermStorageDuration param * revisions: suite ctx, fix genesis, update contrib * implement AssetSupply type, store key, keeper * integrate supply and swaps; genesis, tests * remove legacy comments * requested revisions * update alias * rest queries * implement BEP3 REST txs * draft rest server readme + example json files * tested all swap rest examples * implement query swaps rest endpoint * feat: update genesis examples * fix: use post instead of put (#405) Co-authored-by: Denali Marsh Co-authored-by: John Maheswaran Co-authored-by: John Maheswaran --- Makefile | 3 + app/app.go | 41 +- app/test_common.go | 4 + .../genesis_savings_rate.json | 352 ++++++++ contrib/testnet-5000/README.md | 125 +++ .../genesis_examples/bep3_genesis.json | 244 ++++++ .../internal_proposed_genesis.json | 244 ++++++ .../rest_examples/broadcast-claim-swap.json | 29 + .../rest_examples/broadcast-create-swap.json | 41 + .../rest_examples/broadcast-refund-swap.json | 28 + .../rest_examples/claim-swap-unsigned.json | 21 + .../rest_examples/claim-swap.json | 15 + .../rest_examples/create-swap-unsigned.json | 33 + .../rest_examples/create-swap.json | 27 + .../rest_examples/refund-swap-unsigned.json | 20 + .../rest_examples/refund-swap.json | 14 + go.mod | 6 +- go.sum | 66 +- rest_test/how_to_run_dredd_tests.md | 25 + rest_test/run_all_tests_from_make.sh | 133 +++ rest_test/setup/setuptest.go | 349 ++++++++ rest_test/stopchain.sh | 7 + swagger-ui/swagger.yaml | 766 +++++++++++------- x/bep3/abci.go | 18 + x/bep3/abci_test.go | 242 ++++++ x/bep3/alias.go | 160 ++++ x/bep3/client/cli/query.go | 225 +++++ x/bep3/client/cli/tx.go | 181 +++++ x/bep3/client/rest/query.go | 164 ++++ x/bep3/client/rest/rest.go | 46 ++ x/bep3/client/rest/tx.go | 110 +++ x/bep3/genesis.go | 122 +++ x/bep3/genesis_test.go | 298 +++++++ x/bep3/handler.go | 89 ++ x/bep3/handler_test.go | 132 +++ x/bep3/integration_test.go | 87 ++ x/bep3/keeper/asset.go | 113 +++ x/bep3/keeper/asset_test.go | 415 ++++++++++ x/bep3/keeper/integration_test.go | 94 +++ x/bep3/keeper/keeper.go | 223 +++++ x/bep3/keeper/keeper_test.go | 370 +++++++++ x/bep3/keeper/params.go | 75 ++ x/bep3/keeper/params_test.go | 141 ++++ x/bep3/keeper/querier.go | 100 +++ x/bep3/keeper/querier_test.go | 167 ++++ x/bep3/keeper/swap.go | 266 ++++++ x/bep3/keeper/swap_test.go | 696 ++++++++++++++++ x/bep3/module.go | 136 ++++ x/bep3/spec/README.md | 17 + x/bep3/types/asset.go | 50 ++ x/bep3/types/codec.go | 20 + x/bep3/types/common_test.go | 35 + x/bep3/types/errors.go | 118 +++ x/bep3/types/events.go | 30 + x/bep3/types/expected_keepers.go | 19 + x/bep3/types/genesis.go | 66 ++ x/bep3/types/genesis_test.go | 104 +++ x/bep3/types/hash.go | 48 ++ x/bep3/types/hash_test.go | 61 ++ x/bep3/types/keys.go | 67 ++ x/bep3/types/msg.go | 260 ++++++ x/bep3/types/msg_test.go | 122 +++ x/bep3/types/params.go | 146 ++++ x/bep3/types/params_test.go | 217 +++++ x/bep3/types/querier.go | 54 ++ x/bep3/types/swap.go | 219 +++++ x/bep3/types/swap_test.go | 140 ++++ x/cdp/abci.go | 40 +- x/cdp/alias.go | 74 +- x/cdp/genesis.go | 23 +- x/cdp/integration_test.go | 61 +- x/cdp/keeper/auctions.go | 11 +- x/cdp/keeper/cdp_test.go | 1 + x/cdp/keeper/fees.go | 40 + x/cdp/keeper/fees_test.go | 59 ++ x/cdp/keeper/integration_test.go | 39 +- x/cdp/keeper/keeper.go | 14 +- x/cdp/keeper/savings.go | 95 +++ x/cdp/keeper/savings_test.go | 81 ++ x/cdp/keeper/seize.go | 18 +- x/cdp/module.go | 6 +- x/cdp/spec/04_begin_block.md | 16 +- x/cdp/spec/06_params.md | 13 +- x/cdp/types/cdp.go | 2 +- x/cdp/types/expected_keepers.go | 7 + x/cdp/types/genesis.go | 51 +- x/cdp/types/keys.go | 23 +- x/cdp/types/params.go | 83 +- x/kavadist/abci.go | 12 + x/kavadist/alias.go | 54 ++ x/kavadist/doc.go | 1 + x/kavadist/genesis.go | 39 + x/kavadist/handler.go | 18 + x/kavadist/keeper/keeper.go | 49 ++ x/kavadist/keeper/mint.go | 87 ++ x/kavadist/keeper/mint_test.go | 144 ++++ x/kavadist/keeper/params.go | 18 + x/kavadist/module.go | 153 ++++ x/kavadist/simulation/decoder.go | 12 + x/kavadist/simulation/params.go | 14 + x/kavadist/simulation/simulation.go | 22 + x/kavadist/types/codec.go | 17 + x/kavadist/types/errors.go | 11 + x/kavadist/types/events.go | 9 + x/kavadist/types/expected_keepers.go | 15 + x/kavadist/types/genesis.go | 54 ++ x/kavadist/types/keys.go | 26 + x/kavadist/types/params.go | 102 +++ x/kavadist/types/params_test.go | 99 +++ x/kavadist/types/querier.go | 6 + x/pricefeed/client/rest/tx.go | 2 +- 111 files changed, 10262 insertions(+), 515 deletions(-) create mode 100644 contrib/genesis_examples/genesis_savings_rate.json create mode 100644 contrib/testnet-5000/README.md create mode 100644 contrib/testnet-5000/genesis_examples/bep3_genesis.json create mode 100644 contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json create mode 100644 contrib/testnet-5000/rest_examples/broadcast-claim-swap.json create mode 100644 contrib/testnet-5000/rest_examples/broadcast-create-swap.json create mode 100644 contrib/testnet-5000/rest_examples/broadcast-refund-swap.json create mode 100644 contrib/testnet-5000/rest_examples/claim-swap-unsigned.json create mode 100644 contrib/testnet-5000/rest_examples/claim-swap.json create mode 100644 contrib/testnet-5000/rest_examples/create-swap-unsigned.json create mode 100644 contrib/testnet-5000/rest_examples/create-swap.json create mode 100644 contrib/testnet-5000/rest_examples/refund-swap-unsigned.json create mode 100644 contrib/testnet-5000/rest_examples/refund-swap.json create mode 100644 rest_test/how_to_run_dredd_tests.md create mode 100755 rest_test/run_all_tests_from_make.sh create mode 100644 rest_test/setup/setuptest.go create mode 100755 rest_test/stopchain.sh create mode 100644 x/bep3/abci.go create mode 100644 x/bep3/abci_test.go create mode 100644 x/bep3/alias.go create mode 100644 x/bep3/client/cli/query.go create mode 100644 x/bep3/client/cli/tx.go create mode 100644 x/bep3/client/rest/query.go create mode 100644 x/bep3/client/rest/rest.go create mode 100644 x/bep3/client/rest/tx.go create mode 100644 x/bep3/genesis.go create mode 100644 x/bep3/genesis_test.go create mode 100644 x/bep3/handler.go create mode 100644 x/bep3/handler_test.go create mode 100644 x/bep3/integration_test.go create mode 100644 x/bep3/keeper/asset.go create mode 100644 x/bep3/keeper/asset_test.go create mode 100644 x/bep3/keeper/integration_test.go create mode 100644 x/bep3/keeper/keeper.go create mode 100644 x/bep3/keeper/keeper_test.go create mode 100644 x/bep3/keeper/params.go create mode 100644 x/bep3/keeper/params_test.go create mode 100644 x/bep3/keeper/querier.go create mode 100644 x/bep3/keeper/querier_test.go create mode 100644 x/bep3/keeper/swap.go create mode 100644 x/bep3/keeper/swap_test.go create mode 100644 x/bep3/module.go create mode 100644 x/bep3/spec/README.md create mode 100644 x/bep3/types/asset.go create mode 100644 x/bep3/types/codec.go create mode 100644 x/bep3/types/common_test.go create mode 100644 x/bep3/types/errors.go create mode 100644 x/bep3/types/events.go create mode 100644 x/bep3/types/expected_keepers.go create mode 100644 x/bep3/types/genesis.go create mode 100644 x/bep3/types/genesis_test.go create mode 100644 x/bep3/types/hash.go create mode 100644 x/bep3/types/hash_test.go create mode 100644 x/bep3/types/keys.go create mode 100644 x/bep3/types/msg.go create mode 100644 x/bep3/types/msg_test.go create mode 100644 x/bep3/types/params.go create mode 100644 x/bep3/types/params_test.go create mode 100644 x/bep3/types/querier.go create mode 100644 x/bep3/types/swap.go create mode 100644 x/bep3/types/swap_test.go create mode 100644 x/cdp/keeper/savings.go create mode 100644 x/cdp/keeper/savings_test.go create mode 100644 x/kavadist/abci.go create mode 100644 x/kavadist/alias.go create mode 100644 x/kavadist/doc.go create mode 100644 x/kavadist/genesis.go create mode 100644 x/kavadist/handler.go create mode 100644 x/kavadist/keeper/keeper.go create mode 100644 x/kavadist/keeper/mint.go create mode 100644 x/kavadist/keeper/mint_test.go create mode 100644 x/kavadist/keeper/params.go create mode 100644 x/kavadist/module.go create mode 100644 x/kavadist/simulation/decoder.go create mode 100644 x/kavadist/simulation/params.go create mode 100644 x/kavadist/simulation/simulation.go create mode 100644 x/kavadist/types/codec.go create mode 100644 x/kavadist/types/errors.go create mode 100644 x/kavadist/types/events.go create mode 100644 x/kavadist/types/expected_keepers.go create mode 100644 x/kavadist/types/genesis.go create mode 100644 x/kavadist/types/keys.go create mode 100644 x/kavadist/types/params.go create mode 100644 x/kavadist/types/params_test.go create mode 100644 x/kavadist/types/querier.go diff --git a/Makefile b/Makefile index d2712d6f..11c1c996 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,9 @@ test-all: build test: @go test ./... +test_dredd: + rest_test/./run_all_tests_from_make.sh + # Kick start lots of sims on an AWS cluster. # This submits an AWS Batch job to run a lot of sims, each within a docker image. Results are uploaded to S3 start-remote-sims: diff --git a/app/app.go b/app/app.go index 3d4daa72..0bce4540 100644 --- a/app/app.go +++ b/app/app.go @@ -5,7 +5,9 @@ import ( "os" "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/bep3" "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" @@ -62,6 +64,8 @@ var ( auction.AppModuleBasic{}, cdp.AppModuleBasic{}, pricefeed.AppModuleBasic{}, + bep3.AppModuleBasic{}, + kavadist.AppModuleBasic{}, ) // module account permissions @@ -76,6 +80,9 @@ var ( auction.ModuleName: nil, cdp.ModuleName: {supply.Minter, supply.Burner}, cdp.LiquidatorMacc: {supply.Minter, supply.Burner}, + cdp.SavingsRateMacc: {supply.Minter}, + bep3.ModuleName: {supply.Minter, supply.Burner}, + kavadist.ModuleName: {supply.Minter}, } ) @@ -105,6 +112,8 @@ type App struct { auctionKeeper auction.Keeper cdpKeeper cdp.Keeper pricefeedKeeper pricefeed.Keeper + bep3Keeper bep3.Keeper + kavadistKeeper kavadist.Keeper // the module manager mm *module.Manager @@ -128,7 +137,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, gov.StoreKey, params.StoreKey, validatorvesting.StoreKey, - auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, + auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey, + kavadist.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) @@ -153,6 +163,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace) + bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace) + kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace) // add keepers app.accountKeeper = auth.NewAccountKeeper( @@ -229,7 +241,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, keys[pricefeed.StoreKey], pricefeedSubspace, pricefeed.DefaultCodespace) - // NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) app.auctionKeeper = auction.NewKeeper( app.cdc, keys[auction.StoreKey], @@ -242,7 +253,21 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.pricefeedKeeper, app.auctionKeeper, app.supplyKeeper, + app.accountKeeper, cdp.DefaultCodespace) + app.bep3Keeper = bep3.NewKeeper( + app.cdc, + keys[bep3.StoreKey], + app.supplyKeeper, + bep3Subspace, + bep3.DefaultCodespace) + app.kavadistKeeper = kavadist.NewKeeper( + app.cdc, + keys[kavadist.StoreKey], + kavadistSubspace, + app.supplyKeeper, + kavadist.DefaultCodespace, + ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -264,15 +289,16 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), auction.NewAppModule(app.auctionKeeper, app.supplyKeeper), - cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), + cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper, app.supplyKeeper), pricefeed.NewAppModule(app.pricefeedKeeper), + bep3.NewAppModule(app.bep3Keeper, app.supplyKeeper), + kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. - // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp. So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions. - app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, auction.ModuleName, cdp.ModuleName) + app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, bep3.ModuleName) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName) @@ -285,7 +311,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, - pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, // TODO is this order ok? + pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, // TODO is this order ok? ) app.mm.RegisterInvariants(&app.crisisKeeper) @@ -305,9 +331,10 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, distr.NewAppModule(app.distrKeeper, app.supplyKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper), - cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), // TODO how is the order be decided here? Is this order correct? + cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper, app.supplyKeeper), // TODO how is the order be decided here? Is this order correct? pricefeed.NewAppModule(app.pricefeedKeeper), auction.NewAppModule(app.auctionKeeper, app.supplyKeeper), + kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper), ) app.sm.RegisterStoreDecoders() diff --git a/app/test_common.go b/app/test_common.go index 7bb0da05..be6032dd 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -28,7 +28,9 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/bep3" "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) @@ -67,6 +69,8 @@ func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } +func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper } +func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper } // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { diff --git a/contrib/genesis_examples/genesis_savings_rate.json b/contrib/genesis_examples/genesis_savings_rate.json new file mode 100644 index 00000000..538d5b72 --- /dev/null +++ b/contrib/genesis_examples/genesis_savings_rate.json @@ -0,0 +1,352 @@ +{ + "genesis_time": "2020-03-07T18:27:07.837213082Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "100000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "previous_distribution_time": "1970-01-01T00:00:00Z", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "5000000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "10000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 0, + "stability_fee": "1.000000001547126" + }, + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "10000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "1000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd", + "savings_rate": "0.95" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "savings_distribution_frequency": "120000000000", + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "bank": { + "send_enabled": true + }, + "params": null, + "bep3": { + "params": { + "bnb_deputy_address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj", + "min_block_lock": "80", + "max_block_lock": "600", + "supported_assets": [ + { + "denom": "ukava", + "coin_id": "459", + "limit": "1", + "active": false + } + ] + }, + "atomic_swaps": [], + "assets_supplies": [] + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp:usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "kava1xq4cspcgl9thzmn6lkvd6dlx28wsr63zw4mlmf" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "btc", + "market_id": "btc:usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "btc:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "8700.0" + }, + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "xrp:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "0.25" + } + ] + }, + "gov": { + "starting_proposal_id": "1", + "deposits": null, + "votes": null, + "proposals": null, + "deposit_params": { + "min_deposit": [ + { + "denom": "ukava", + "amount": "10000000" + } + ], + "max_deposit_period": "172800000000000" + }, + "voting_params": { + "voting_period": "172800000000000" + }, + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + } + }, + "staking": { + "params": { + "unbonding_time": "1814400000000000", + "max_validators": 100, + "max_entries": 7, + "bond_denom": "ukava" + }, + "last_total_power": "0", + "last_validator_powers": null, + "validators": null, + "delegations": null, + "unbonding_delegations": null, + "redelegations": null, + "exported": false + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "ukava", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + }, + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "coins": [ + { + "denom": "ukava", + "amount": "1000000000000" + }, + { + "denom": "btc", + "amount": "10000000000" + }, + { + "denom": "xrp", + "amount": "1000000000000" + } + ], + "public_key": null, + "account_number": "0", + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "address": "kava19dp7luf32mlqnw6mhpgk0g37ule7wm2g8gck8a", + "coins": [ + { + "denom": "ukava", + "amount": "1000000000000" + } + ], + "public_key": null, + "account_number": "0", + "sequence": "0" + } + } + ] + }, + "auction": { + "next_auction_id": "1", + "params": { + "max_auction_duration": "172800000000000", + "bid_duration": "3600000000000" + }, + "auctions": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "supply": { + "supply": [] + }, + "crisis": { + "constant_fee": { + "denom": "ukava", + "amount": "1000" + } + }, + "distribution": { + "fee_pool": { + "community_pool": [] + }, + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "withdraw_addr_enabled": true, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "kava-tester", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte", + "pubkey": "kavavalconspub1zcjduepq3qyhcxfpa0u2efeu3htuxjz0248khl2cqfcm8jz2n0dr2e2a6tuqqafg2g", + "value": { + "denom": "ukava", + "amount": "10000000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43" + }, + "signature": "ZIzw2qsaJzsNuROW1JYYH1ZOA3jrc4ZCHvrCxirWNlEZqIvnyC42nBQLIPQ+d+PIcpldLVy0KAkb8NBXj9G0nQ==" + } + ], + "memo": "6fff8e9b327f0811e7a25c1419781167f82ec7b3@172.31.40.66:26656" + } + } + ] + }, + "slashing": { + "params": { + "max_evidence_age": "120000000000", + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600000000000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": {}, + "missed_blocks": {} + } + } +} \ No newline at end of file diff --git a/contrib/testnet-5000/README.md b/contrib/testnet-5000/README.md new file mode 100644 index 00000000..f85311ad --- /dev/null +++ b/contrib/testnet-5000/README.md @@ -0,0 +1,125 @@ +# Testnet-5000 + +Testnet-5000 introduces transfers between Kava and Bnbchain via BEP3. + +This guide will walk you through interacting with the blockchains and transferring coins via the rest server. To send transactions, we'll create an unsigned request, sign it, and broadcast it to the Kava blockchain. + +## Rest server requests + +### Setup + +We'll be using Kava's CLI to build, sign, and broadcast the transactions: + +```bash + # Download kvcli + make install +``` + +Before making a request, query account information for the signing account. Note the 'accountnumber' and 'sequence' fields, we'll need them later in order to send our request: + +```bash + kvcli q auth account $(kvcli keys show testuser -a) +``` + +### Create swap + +Use the example file in `rest_examples/create-swap.json` to format the request. First, update the header parameters 'from', 'chain-id', 'account_number', 'sequence'. + +Next, we'll update the swap's creation parameters. For that, we need a unique random number that will be used to claim the funds. + +WARNING: Don't use `calc-rnh` for the generation of secrets in production. These values should be generated client-side for the safety of user funds. + +```bash + # Generate a sample random number, timestamp, and random number hash + kvcli q bep3 calc-rnh now + + # Expected output: + # Random number: 110802331073994018312675691928205725441742309715720953510374321628333109608728 + # Timestamp: 1585203985 + # Random number hash: 4644fc2d9a2389c60e621785b873ae187e320eaded1687edaa120961428eba9e +``` + +In the same json file, populate each of the following parameters + +- from +- to +- recipient_other_chain +- sender_other_chain +- random_number_hash +- timestamp +- amount +- expected_income +- height_span +- cross_chain + +Once each parameter is populated, it's time to create our swap: + +```bash + # Create an unsigned request + curl -H "Content-Type: application/json" -X POST -d @./contrib/testnet-5000/rest_examples/create-swap.json http://127.0.0.1:1317/bep3/swap/create | jq > ./contrib/testnet-5000/rest_examples/create-swap-unsigned.json + + # Sign the request + kvcli tx sign ./contrib/testnet-5000/rest_examples/create-swap-unsigned.json --from testnetdeputy --offline --chain-id testing --sequence 0 --account-number 5 | jq > ./contrib/testnet-5000/rest_examples/broadcast-create-swap.json + + # Broadcast the request + kvcli tx broadcast ./contrib/testnet-5000/rest_examples/broadcast-create-swap.json +``` + +The tx broadcast will log information in the terminal, including the txhash. This tx hash can be used to get information about the transaction, including the swap creation event that includes the swap's ID: + +```bash + # Get information about the transaction + curl -H "Content-Type: application/json" -X GET http://localhost:1317/txs/81A1955216F6D985ECB4770E29B9BCED8F73A42D0C0FD566372CF673CCB81587 +``` + +Congratulations, you've just created a swap on Kava! The swap will be automatically relayed over to Bnbchain where it it can be claimed using the secret random number from above. + +# Claim swap + +Only unexpired swaps can be claimed. To claim a swap, we'll use the secret random number that matches this swap's timestamp and random number hash. + +Generally, claimable swaps must be created on Bnbchain. +// TODO: add link to Bnbchain document with interaction steps + +Use the example file in `rest_examples/claim-swap.json` to format the request. Again, update the header parameters 'from', 'account_number', 'sequence'. Check your account using the command from above to ensure that the parameters match the blockchain's state. + +In the same json file, populate each listed parameter: + +- swap_id +- random_number + +Once the `swap_id` parameter is populated, it's time to claim our swap: + +```bash + # Create an unsigned request + curl -H "Content-Type: application/json" -X POST -d @./contrib/testnet-5000/rest_examples/claim-swap.json http://127.0.0.1:1317/bep3/swap/claim | jq > ./contrib/testnet-5000/rest_examples/claim-swap-unsigned.json + + # Sign the request + kvcli tx sign ./contrib/testnet-5000/rest_examples/claim-swap-unsigned.json --from user --offline --chain-id testing --sequence 0 --account-number 1 | jq > ./contrib/testnet-5000/rest_examples/broadcast-claim-swap.json + + # Broadcast the request + kvcli tx broadcast ./contrib/testnet-5000/rest_examples/broadcast-claim-swap.json +``` + +# Refund swap + +Only expired swaps may be refunded. + +Use the example file in `rest_examples/refund-swap.json` to format the request. Again, update the header parameters 'from', 'account_number', 'sequence'. Check your account using the command from above to ensure that the parameters match the blockchain's state. + +In the same json file, populate each parameter: + +- swap_id + +Once the `swap_id` parameter is populated, it's time to refund our swap: + +```bash + # Create an unsigned request + curl -H "Content-Type: application/json" -X POST -d @./contrib/testnet-5000/rest_examples/refund-swap.json http://127.0.0.1:1317/bep3/swap/refund | jq > ./contrib/testnet-5000/rest_examples/refund-swap-unsigned.json + + # Sign the request + kvcli tx sign ./contrib/testnet-5000/rest_examples/refund-swap-unsigned.json --from user --offline --chain-id testing --sequence 0 --account-number 1 | jq > ./contrib/testnet-5000/rest_examples/broadcast-refund-swap.json + + # Broadcast the request + kvcli tx broadcast ./contrib/testnet-5000/rest_examples/broadcast-refund-swap.json +``` diff --git a/contrib/testnet-5000/genesis_examples/bep3_genesis.json b/contrib/testnet-5000/genesis_examples/bep3_genesis.json new file mode 100644 index 00000000..4853c045 --- /dev/null +++ b/contrib/testnet-5000/genesis_examples/bep3_genesis.json @@ -0,0 +1,244 @@ +{ + "genesis_time": "2020-03-27T21:00:00Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": ["ed25519"] + } + }, + "app_hash": "", + "app_state": { + "auth": { + "accounts": [], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "kavadist": { + "params": { + "active": true, + "periods": [ + { + "start": "2020-03-28T15:20:00Z", + "end": "2021-03-28T15:20:00Z", + "inflation": "1.000000003022265980" + } + ] + }, + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "bank": { + "send_enabled": true + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "50000000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "bnb", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "bnb:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "10000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd", + "savings_rate": "0.95" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "savings_distribution_frequency": "120000000000", + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "previous_distribution_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "gov": { + "deposit_params": { + "max_deposit_period": "600000000000", + "min_deposit": [ + { + "amount": "200000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "3600000000000" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "supply": { + "supply": [] + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "bnb", + "market_id": "bnb:usd", + "oracles": ["kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "2020-04-20T00:00:00Z", + "market_id": "bnb:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "12.2639061184" + } + ] + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "600000000000", + "max_auction_duration": "172800000000000", + "increment_surplus": "0.01", + "increment_debt": "0.01", + "increment_collateral": "0.01" + } + }, + "bep3": { + "params": { + "bnb_deputy_address": "kava15wmww3chakqllq4n3ksm37lx36qz067gxe6f0k", + "min_block_lock": "80", + "max_block_lock": "600", + "supported_assets": [ + { + "denom": "bnb", + "coin_id": "714", + "limit": "100000000000", + "active": true + } + ] + } + }, + "genutil": { + "gentxs": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "params": null + } +} diff --git a/contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json b/contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json new file mode 100644 index 00000000..4853c045 --- /dev/null +++ b/contrib/testnet-5000/genesis_examples/internal_proposed_genesis.json @@ -0,0 +1,244 @@ +{ + "genesis_time": "2020-03-27T21:00:00Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": ["ed25519"] + } + }, + "app_hash": "", + "app_state": { + "auth": { + "accounts": [], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "kavadist": { + "params": { + "active": true, + "periods": [ + { + "start": "2020-03-28T15:20:00Z", + "end": "2021-03-28T15:20:00Z", + "inflation": "1.000000003022265980" + } + ] + }, + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "bank": { + "send_enabled": true + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "50000000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "bnb", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "bnb:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "10000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd", + "savings_rate": "0.95" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "savings_distribution_frequency": "120000000000", + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "previous_distribution_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "gov": { + "deposit_params": { + "max_deposit_period": "600000000000", + "min_deposit": [ + { + "amount": "200000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "3600000000000" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "supply": { + "supply": [] + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "bnb", + "market_id": "bnb:usd", + "oracles": ["kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw"], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "2020-04-20T00:00:00Z", + "market_id": "bnb:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "12.2639061184" + } + ] + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "600000000000", + "max_auction_duration": "172800000000000", + "increment_surplus": "0.01", + "increment_debt": "0.01", + "increment_collateral": "0.01" + } + }, + "bep3": { + "params": { + "bnb_deputy_address": "kava15wmww3chakqllq4n3ksm37lx36qz067gxe6f0k", + "min_block_lock": "80", + "max_block_lock": "600", + "supported_assets": [ + { + "denom": "bnb", + "coin_id": "714", + "limit": "100000000000", + "active": true + } + ] + } + }, + "genutil": { + "gentxs": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "params": null + } +} diff --git a/contrib/testnet-5000/rest_examples/broadcast-claim-swap.json b/contrib/testnet-5000/rest_examples/broadcast-claim-swap.json new file mode 100644 index 00000000..e0ca95bf --- /dev/null +++ b/contrib/testnet-5000/rest_examples/broadcast-claim-swap.json @@ -0,0 +1,29 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "bep3/MsgClaimAtomicSwap", + "value": { + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "swap_id": "3CA20F0152F03B0AABE73E7AA1DDF78B9048EDE5A9A73846E1EF53BEBBFA4185", + "random_number": "E3D0A98B459F72231DA69C3BD771C1E721FEF4BE83C14B80DC805BA71019EEBE" + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "AsGjurKnae7kuQQawGO3FLzrjeLCRO2W6orV74fekVYo" + }, + "signature": "aOM9ui+LOSw9GDHe2cKWXPno8Oa1dInphCCJE4WVC5Q/3Kv0j4a6fkfT7sR9uXEQX5rDN7CAH2WrmWxE7E6P7Q==" + } + ], + "memo": "" + } +} diff --git a/contrib/testnet-5000/rest_examples/broadcast-create-swap.json b/contrib/testnet-5000/rest_examples/broadcast-create-swap.json new file mode 100644 index 00000000..b37d8457 --- /dev/null +++ b/contrib/testnet-5000/rest_examples/broadcast-create-swap.json @@ -0,0 +1,41 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "bep3/MsgCreateAtomicSwap", + "value": { + "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x", + "to": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "recipient_other_chain": "tbnb1etjxt3t5r9899w3tf7dnktdue5ngu4ncdwckvt", + "sender_other_chain": "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h", + "random_number_hash": "C0544B7F4B890A673EA3F61BDB4650FBFE2F3E56BDA1B397D6D592FCA7163C8C", + "timestamp": "1585252531", + "amount": [ + { + "denom": "bnb", + "amount": "555555" + } + ], + "expected_income": "555555bnb", + "height_span": "360", + "cross_chain": true + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A6ExM8g1WQtVD7U5hg+whlWvPza6p24ABFqv6ofv0lQn" + }, + "signature": "1bC1pe1oH20pScI2FimQ98VfvUA/galjDBNkaXcbZKYsE3ig7oGr322PraO2tO44OlY0AiZ1LCVZ15kCxnXO3w==" + } + ], + "memo": "" + } +} diff --git a/contrib/testnet-5000/rest_examples/broadcast-refund-swap.json b/contrib/testnet-5000/rest_examples/broadcast-refund-swap.json new file mode 100644 index 00000000..d3fc0070 --- /dev/null +++ b/contrib/testnet-5000/rest_examples/broadcast-refund-swap.json @@ -0,0 +1,28 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "bep3/MsgRefundAtomicSwap", + "value": { + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "swap_id": "1B00244021EC239867E5B8C44BCD98E40F3148806A8C0A8FD3418872986BECBA" + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "AsGjurKnae7kuQQawGO3FLzrjeLCRO2W6orV74fekVYo" + }, + "signature": "y/T0zLHuvk7oRgTqU9bc6wnWm5JHzhnEk/U1xSWVkxVD2NvC9MvI6lHjXy1iBzyn388x4U8DQ+sxsT/BBA1ehg==" + } + ], + "memo": "" + } +} diff --git a/contrib/testnet-5000/rest_examples/claim-swap-unsigned.json b/contrib/testnet-5000/rest_examples/claim-swap-unsigned.json new file mode 100644 index 00000000..eeed7deb --- /dev/null +++ b/contrib/testnet-5000/rest_examples/claim-swap-unsigned.json @@ -0,0 +1,21 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "bep3/MsgClaimAtomicSwap", + "value": { + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "swap_id": "3CA20F0152F03B0AABE73E7AA1DDF78B9048EDE5A9A73846E1EF53BEBBFA4185", + "random_number": "E3D0A98B459F72231DA69C3BD771C1E721FEF4BE83C14B80DC805BA71019EEBE" + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": null, + "memo": "" + } +} diff --git a/contrib/testnet-5000/rest_examples/claim-swap.json b/contrib/testnet-5000/rest_examples/claim-swap.json new file mode 100644 index 00000000..54db1637 --- /dev/null +++ b/contrib/testnet-5000/rest_examples/claim-swap.json @@ -0,0 +1,15 @@ +{ + "base_req": { + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "memo": "", + "chain_id": "testing", + "account_number": "1", + "sequence": "0", + "gas": "500000", + "gas_adjustment": "1.0", + "simulate": false + }, + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "swap_id": "3ca20f0152f03b0aabe73e7aa1ddf78b9048ede5a9a73846e1ef53bebbfa4185", + "random_number": "e3d0a98b459f72231da69c3bd771c1e721fef4be83c14b80dc805ba71019eebe" +} diff --git a/contrib/testnet-5000/rest_examples/create-swap-unsigned.json b/contrib/testnet-5000/rest_examples/create-swap-unsigned.json new file mode 100644 index 00000000..c610123b --- /dev/null +++ b/contrib/testnet-5000/rest_examples/create-swap-unsigned.json @@ -0,0 +1,33 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "bep3/MsgCreateAtomicSwap", + "value": { + "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x", + "to": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "recipient_other_chain": "tbnb1etjxt3t5r9899w3tf7dnktdue5ngu4ncdwckvt", + "sender_other_chain": "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h", + "random_number_hash": "C0544B7F4B890A673EA3F61BDB4650FBFE2F3E56BDA1B397D6D592FCA7163C8C", + "timestamp": "1585252531", + "amount": [ + { + "denom": "bnb", + "amount": "555555" + } + ], + "expected_income": "555555bnb", + "height_span": "360", + "cross_chain": true + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": null, + "memo": "" + } +} diff --git a/contrib/testnet-5000/rest_examples/create-swap.json b/contrib/testnet-5000/rest_examples/create-swap.json new file mode 100644 index 00000000..c891d1e8 --- /dev/null +++ b/contrib/testnet-5000/rest_examples/create-swap.json @@ -0,0 +1,27 @@ +{ + "base_req": { + "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x", + "memo": "", + "chain_id": "testing", + "account_number": "5", + "sequence": "0", + "gas": "500000", + "gas_adjustment": "1.0", + "simulate": false + }, + "from": "kava1sl8glhaa9f9tep0d9h8gdcfmwcatghtdrfcd2x", + "to": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "recipient_other_chain": "tbnb1etjxt3t5r9899w3tf7dnktdue5ngu4ncdwckvt", + "sender_other_chain": "tbnb10uypsspvl6jlxcx5xse02pag39l8xpe7a3468h", + "random_number_hash": "c0544b7f4b890a673ea3f61bdb4650fbfe2f3e56bda1b397d6d592fca7163c8c", + "timestamp": "1585252531", + "amount": [ + { + "denom": "bnb", + "amount": "555555" + } + ], + "expected_income": "555555bnb", + "height_span": "360", + "cross_chain": true +} diff --git a/contrib/testnet-5000/rest_examples/refund-swap-unsigned.json b/contrib/testnet-5000/rest_examples/refund-swap-unsigned.json new file mode 100644 index 00000000..faa176c2 --- /dev/null +++ b/contrib/testnet-5000/rest_examples/refund-swap-unsigned.json @@ -0,0 +1,20 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "bep3/MsgRefundAtomicSwap", + "value": { + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "swap_id": "1B00244021EC239867E5B8C44BCD98E40F3148806A8C0A8FD3418872986BECBA" + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": null, + "memo": "" + } +} diff --git a/contrib/testnet-5000/rest_examples/refund-swap.json b/contrib/testnet-5000/rest_examples/refund-swap.json new file mode 100644 index 00000000..4cbf7ff1 --- /dev/null +++ b/contrib/testnet-5000/rest_examples/refund-swap.json @@ -0,0 +1,14 @@ +{ + "base_req": { + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "memo": "", + "chain_id": "testing", + "account_number": "1", + "sequence": "0", + "gas": "500000", + "gas_adjustment": "1.0", + "simulate": false + }, + "from": "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p", + "swap_id": "1b00244021ec239867e5b8c44bcd98e40f3148806a8c0a8fd3418872986becba" +} diff --git a/go.mod b/go.mod index ffba0067..10583537 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,18 @@ module github.com/kava-labs/kava go 1.13 require ( + github.com/btcsuite/btcd v0.20.1-beta // indirect github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 + github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d // indirect + github.com/gogo/protobuf v1.3.0 github.com/gorilla/mux v1.7.3 - github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 // indirect + github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.4.0 github.com/tendermint/go-amino v0.15.0 github.com/tendermint/tendermint v0.32.7 github.com/tendermint/tm-db v0.2.0 + golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 // indirect gopkg.in/yaml.v2 v2.2.4 ) diff --git a/go.sum b/go.sum index cb578ad0..4a1f0603 100644 --- a/go.sum +++ b/go.sum @@ -18,11 +18,13 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= +github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= @@ -36,16 +38,11 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cosmos/cosmos-sdk v0.34.4-0.20190925161702-9d0bed8f4f4e h1:V8WpJTIAjajE2PE+1wWCG5LUYkWQal+aH6uqPUiZ9Qc= -github.com/cosmos/cosmos-sdk v0.34.4-0.20190925161702-9d0bed8f4f4e/go.mod h1:gwKdI16dOjylNYJkaHbcx0TcEIHyRs1xyc5qROmjCJE= -github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c h1:LM81MVa0CG0Q118dOp6f2Q0MoYIMRTi26zoyqZiL8sY= -github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c/go.mod h1:FxjFo2Y2ZuqUczTcVfnDUG413OgofBbxFNXA72eaUR4= github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 h1:yb+E8HGzFnO0YwLS6OCBIAVWtN8KfCYoKeO9mgAmQN0= github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1/go.mod h1:IGBhkbOK1ebLqMWjtgo99zUxWHsA5IOb6N9CI8nHs0Y= -github.com/cosmos/cosmos-sdk v0.37.1 h1:mz5W3Au32VIPPtrY65dheVYeVDSFfS3eSSmuIj+cXsI= -github.com/cosmos/cosmos-sdk v0.37.4 h1:1ioXxkpiS+wOgaUbROeDIyuF7hciU5nti0TSyBmV2Ok= -github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J96OTjeFFc= github.com/cosmos/ledger-cosmos-go v0.10.3/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY= github.com/cosmos/ledger-go v0.9.2 h1:Nnao/dLwaVTk1Q5U9THldpUMMXU94BOTWPddSmVB6pI= @@ -59,8 +56,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= @@ -72,8 +67,6 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= @@ -81,7 +74,6 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.6.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -145,11 +137,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.8.5 h1:2ucYeik+NtUTg+IAiNQtoFC5ZGs5mIVidI7Ome0Br3Y= -github.com/klauspost/compress v1.8.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -158,19 +145,11 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/libp2p/go-buffer-pool v0.0.1 h1:9Rrn/H46cXjaA2HQ5Y8lyhOS1NhTkZ4yuEs2r3Eechg= -github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ= github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -187,8 +166,6 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= -github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.5.0 h1:5BakdOZdtKJ1FFk6QdL8iSGrMWsXgchNJcrnarjbmJQ= github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -212,21 +189,13 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= -github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 h1:o0pGzJnfjk0E+CZg6jxQrADfk9WYO9fMuLOtSP4owtE= -github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75/go.mod h1:/L9q8uCsB8BOWdzLK+6WIwkAlcMfKhFCZY0n8/CLHRY= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -242,7 +211,6 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -272,26 +240,15 @@ github.com/tendermint/go-amino v0.15.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoM github.com/tendermint/iavl v0.12.4 h1:hd1woxUGISKkfUWBA4mmmTwOua6PQZTJM/F0FDrmMV8= github.com/tendermint/iavl v0.12.4/go.mod h1:8LHakzt8/0G3/I8FUU0ReNx98S/EP6eyPJkAUvEXT/o= github.com/tendermint/tendermint v0.32.1/go.mod h1:jmPDAKuNkev9793/ivn/fTBnfpA9mGBww8MPRNPNxnU= -github.com/tendermint/tendermint v0.32.3 h1:GEnWpGQ795h5oTFNbfBLsY0LW/CW2j6p6HtiYNfxsgg= -github.com/tendermint/tendermint v0.32.3/go.mod h1:ZK2c29jl1QRYznIRyRWRDsmm1yvtPzBRT00x4t1JToY= -github.com/tendermint/tendermint v0.32.5 h1:2hCLwuzfCKZxXSe/+iMEl+ChJWKJx6g/Wcvq3NMxVN4= -github.com/tendermint/tendermint v0.32.5/go.mod h1:D2+A3pNjY+Po72X0mTfaXorFhiVI8dh/Zg640FGyGtE= -github.com/tendermint/tendermint v0.32.6 h1:HozXi0USWvKrWuEh5ratnJV10ykkTy4nwXUi0UvPVzg= github.com/tendermint/tendermint v0.32.6/go.mod h1:D2+A3pNjY+Po72X0mTfaXorFhiVI8dh/Zg640FGyGtE= github.com/tendermint/tendermint v0.32.7 h1:Szu5Fm1L3pvn3t4uQxPAcP+7ndZEQKgLie/yokM56rU= github.com/tendermint/tendermint v0.32.7/go.mod h1:D2+A3pNjY+Po72X0mTfaXorFhiVI8dh/Zg640FGyGtE= -github.com/tendermint/tm-db v0.1.1 h1:G3Xezy3sOk9+ekhjZ/kjArYIs1SmwV+1OUgNkj7RgV0= github.com/tendermint/tm-db v0.1.1/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= github.com/tendermint/tm-db v0.2.0 h1:rJxgdqn6fIiVJZy4zLpY1qVlyD0TU6vhkT4kEf71TQQ= github.com/tendermint/tm-db v0.2.0/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.5.0 h1:dhq+O9pmNZFF6qAXpasMO1xSm7dL4qEz2ylfZN8BG9w= -github.com/valyala/fasthttp v1.5.0/go.mod h1:eriCz9OhZjKCGfJ185a/IDgNl0bg9IbzfpcslMZXU1c= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8= @@ -307,7 +264,6 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -320,9 +276,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA= golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -338,12 +292,8 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= @@ -363,7 +313,6 @@ google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoA google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -378,10 +327,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/rest_test/how_to_run_dredd_tests.md b/rest_test/how_to_run_dredd_tests.md new file mode 100644 index 00000000..96c3ebe9 --- /dev/null +++ b/rest_test/how_to_run_dredd_tests.md @@ -0,0 +1,25 @@ +/////////////////////////////////////////////////////////////////////////////////////// +///// IF YOU FOLLOW THESE INSTRUCTIONS ALL THE TESTS SHOULD PASS THE FIRST TIME /////// +/////////////////////////////////////////////////////////////////////////////////////// + +Instructions on how to run the `dredd` tests + +(Prerequisite) Make sure that you have the latest versions of `node` and `npm` and `npx` installed. Then install `dredd` globally using the following command folder: + +`npm install dredd --global` + +(Running tests) Run `make test_dredd` from the `kava` directory. + +This builds the `test.go` file, creates the genesis state for the blockchain, starts the blockchain, starts the rest server, sends the required transactions to the blockchain, runs all the `dredd` tests, shuts +down the blockchain, cleans up, and propagates up an error code if the tests do not all pass. +**IMPORTANT** - It takes about 3 minutes for the script to run and complete: +**When you run the script 62 tests should pass and zero should fail** + +(Shutdown) If the script fails or you stop it midway using `ctrl-c` then you should manually stop the blockchain and rest server using the following script. If you let it complete +it will automatically shut down the blockchain, rest server and clean up. + +`./stopchain.sh` + +**DEBUGGING NOTE**: If you start getting `Validator set is different` errors then you need to try starting the chain from scratch (do NOT just use `unsafe-reset-all`, instead use `./stopchain.sh` and then `./run_all_tests.sh`) + + diff --git a/rest_test/run_all_tests_from_make.sh b/rest_test/run_all_tests_from_make.sh new file mode 100755 index 00000000..1b0ca862 --- /dev/null +++ b/rest_test/run_all_tests_from_make.sh @@ -0,0 +1,133 @@ +#! /bin/bash +# TODO import from development environment in envkey +password="password" +validatorMnemonic="equip town gesture square tomorrow volume nephew minute witness beef rich gadget actress egg sing secret pole winter alarm law today check violin uncover" + +# address: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c +# address: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 + +faucet="chief access utility giant burger winner jar false naive mobile often perfect advice village love enroll embark bacon under flock harbor render father since" + +# address: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz +# address: kavavaloper1ls82zzghsx0exkpr52m8vht5jqs3un0c5j2c04 + +# variables for home directories for kvd and kvcli +kvdHome=/tmp/kvdHome +kvcliHome=/tmp/kvcliHome + +# Remove any existing data directory +rm -rf $kvdHome +rm -rf $kvcliHome + +# make the directories +mkdir /tmp/kvdHome +mkdir /tmp/kvcliHome + +# create validator key +printf "$password\n$validatorMnemonic\n" | kvcli keys add vlad --recover --home $kvcliHome +# create faucet key +printf "$password\n$faucet\n" | kvcli --home $kvcliHome keys add faucet --recover --home $kvcliHome + +# function used to show that it is still loading +showLoading() { + mypid=$! + loadingText=$1 + + echo -ne "$loadingText\r" + + while kill -0 $mypid 2>/dev/null; do + echo -ne "$loadingText.\r" + sleep 0.5 + echo -ne "$loadingText..\r" + sleep 0.5 + echo -ne "$loadingText...\r" + sleep 0.5 + echo -ne "\r\033[K" + echo -ne "$loadingText\r" + sleep 0.5 + done + + echo "$loadingText...finished" +} + +# Create new data directory +{ +kvd --home $kvdHome init --chain-id=testing vlad # doesn't need to be the same as the validator +} > /dev/null 2>&1 +kvcli --home $kvcliHome config chain-id testing # or set trust-node true + +# add validator account to genesis +kvd --home $kvdHome add-genesis-account $(kvcli --home $kvcliHome keys show vlad -a) 10000000000000stake +# add faucet account to genesis +kvd --home $kvdHome add-genesis-account $(kvcli --home $kvcliHome keys show faucet -a) 10000000000000stake,1000000000000xrp,100000000000btc + +# Create a delegation tx for the validator and add to genesis +printf "$password\n" | kvd --home $kvdHome gentx --name vlad --home-client $kvcliHome +{ +kvd --home $kvdHome collect-gentxs +} > /dev/null 2>&1 + +# start the blockchain in the background, wait until it starts making blocks +{ +kvd start --home $kvdHome & kvdPid="$!" +} > /dev/null 2>&1 + +printf "\n" +sleep 10 & showLoading "Starting rest server, please wait" +# start the rest server. Use ./stopchain.sh to stop both rest server and the blockchain +{ +kvcli rest-server --laddr tcp://127.0.0.1:1317 --chain-id=testing --home $kvcliHome & kvcliPid="$!" +} > /dev/null 2>&1 +printf "\n" +sleep 10 & showLoading "Preparing blockchain setup transactions, please wait" +printf "\n" + +# build the go setup test file +rm -f rest_test/setuptest +go build rest_test/setup/setuptest.go & showLoading "Building go test file, please wait" + +# run the go code to send transactions to the chain and set it up correctly +./setuptest $kvcliHome & showLoading "Sending messages to blockchain" +printf "\n" +printf "Blockchain setup completed" +printf "\n\n" + +############################ +# Now run the dredd tests +############################ + +dredd swagger-ui/swagger.yaml localhost:1317 2>&1 | tee output & showLoading "Running dredd tests" + +######################################################## +# Now run the check the return code from the dredd command. +# If 0 then all test passed OK, otherwise some failed and propagate the error +######################################################## + +# check that the error code was zero +if [ $? -eq 0 ] +then + # check that all the tests passed (ie zero failing) + if [[ $(cat output | grep "0 failing") ]] + then + # check for no errors + if [[ $(cat output | grep "0 errors") ]] + then + echo "Success" + rm setuptest & showLoading "Cleaning up go binary" + # kill the kvd and kvcli processes (blockchain and rest api) + pgrep kvd | xargs kill + pgrep kvcli | xargs kill & showLoading "Stopping blockchain" + rm -f output + exit 0 + fi + fi +fi + +# otherwise return an error code and redirect stderr to stdout so user sees the error output +echo "Failure" >&2 +rm setuptest & showLoading "Cleaning up go binary" +# kill the kvd and kvcli processes (blockchain and rest api) +pgrep kvd | xargs kill +pgrep kvcli | xargs kill & showLoading "Stopping blockchain" +rm -f output +exit 1 diff --git a/rest_test/setup/setuptest.go b/rest_test/setup/setuptest.go new file mode 100644 index 00000000..9d66f390 --- /dev/null +++ b/rest_test/setup/setuptest.go @@ -0,0 +1,349 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/cosmos/cosmos-sdk/client/keys" + crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkrest "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth" + authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/kava-labs/kava/app" + "github.com/tendermint/go-amino" +) + +func init() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + app.SetBip44CoinType(config) + config.Seal() +} + +func main() { + sendProposal() + sendDeposit() + sendVote() + sendDelegation() + sendUndelegation() + sendCoins() + + sendProposal() + sendDeposit() + sendVote() + sendDelegation() + sendUndelegation() + + sendCoins() + +} + +func sendProposal() { + // get the address + address := getTestAddress() + // get the keyname and password + keyname, password := getKeynameAndPassword() + + proposalContent := gov.ContentFromProposalType("A Test Title", "A test description on this proposal.", gov.ProposalTypeText) + addr, err := sdk.AccAddressFromBech32(address) // validator address + if err != nil { + panic(err) + } + + // create a message to send to the blockchain + msg := gov.NewMsgSubmitProposal( + proposalContent, + sdk.NewCoins(sdk.NewInt64Coin("stake", 1000)), + addr, + ) + + // helper methods for transactions + cdc := app.MakeCodec() // make codec for the app + + // get the keybase + keybase := getKeybase() + + // SEND THE PROPOSAL + // cast to the generic msg type + msgToSend := []sdk.Msg{msg} + + // send the PROPOSAL message to the blockchain + sendMsgToBlockchain(cdc, address, keyname, password, msgToSend, keybase) + +} + +func sendDeposit() { + // get the address + address := getTestAddress() + // get the keyname and password + keyname, password := getKeynameAndPassword() + + addr, err := sdk.AccAddressFromBech32(address) // validator + if err != nil { + panic(err) + } + + // helper methods for transactions + cdc := app.MakeCodec() // make codec for the app + + // get the keybase + keybase := getKeybase() + + // NOW SEND THE DEPOSIT + + // create a deposit transaction to send to the proposal + amount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 10000000)) + deposit := gov.NewMsgDeposit(addr, 2, amount) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml + depositToSend := []sdk.Msg{deposit} + + sendMsgToBlockchain(cdc, address, keyname, password, depositToSend, keybase) + +} + +func sendVote() { + // get the address + address := getTestAddress() + // get the keyname and password + keyname, password := getKeynameAndPassword() + + addr, err := sdk.AccAddressFromBech32(address) // validator + if err != nil { + panic(err) + } + + // helper methods for transactions + cdc := app.MakeCodec() // make codec for the app + + // get the keybase + keybase := getKeybase() + + // NOW SEND THE VOTE + + // create a vote on a proposal to send to the blockchain + vote := gov.NewMsgVote(addr, uint64(2), types.OptionYes) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml + + // send a vote to the blockchain + voteToSend := []sdk.Msg{vote} + sendMsgToBlockchain(cdc, address, keyname, password, voteToSend, keybase) + +} + +// this should send coins from one address to another +func sendCoins() { + // get the address + address := getTestAddress() + // get the keyname and password + keyname, password := getKeynameAndPassword() + + addrFrom, err := sdk.AccAddressFromBech32(address) // validator + if err != nil { + panic(err) + } + + addrTo, err := sdk.AccAddressFromBech32("kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz") // TODO IMPORTANT this is the faucet address + if err != nil { + panic(err) + } + + // helper methods for transactions + cdc := app.MakeCodec() // make codec for the app + + // get the keybase + keybase := getKeybase() + + // create coins + amount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 2000000)) + + coins := bank.NewMsgSend(addrFrom, addrTo, amount) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml + coinsToSend := []sdk.Msg{coins} + + // NOW SEND THE COINS + + // send the coin message to the blockchain + sendMsgToBlockchain(cdc, address, keyname, password, coinsToSend, keybase) + +} + +func getTestAddress() (address string) { + // the test address - TODO IMPORTANT make sure this lines up with startchain.sh + address = "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c" + return address +} + +func getKeynameAndPassword() (keyname string, password string) { + keyname = "vlad" // TODO - IMPORTANT this must match the keys in the startchain.sh script + password = "password" // TODO - IMPORTANT this must match the keys in the startchain.sh script + return keyname, password +} + +// this should send a delegation +func sendDelegation() { + // get the address + address := getTestAddress() + // get the keyname and password + keyname, password := getKeynameAndPassword() + + addrFrom, err := sdk.AccAddressFromBech32(address) // validator + if err != nil { + panic(err) + } + + // helper methods for transactions + cdc := app.MakeCodec() // make codec for the app + + // get the keybase + keybase := getKeybase() + + // get the validator address for delegation + valAddr, err := sdk.ValAddressFromBech32("kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0") // **FAUCET** + if err != nil { + panic(err) + } + + // create delegation amount + delAmount := sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000) + delegation := staking.NewMsgDelegate(addrFrom, valAddr, delAmount) + delegationToSend := []sdk.Msg{delegation} + + // send the delegation to the blockchain + sendMsgToBlockchain(cdc, address, keyname, password, delegationToSend, keybase) +} + +// this should send a MsgUndelegate +func sendUndelegation() { + // get the address + address := getTestAddress() + // get the keyname and password + keyname, password := getKeynameAndPassword() + + addrFrom, err := sdk.AccAddressFromBech32(address) // validator + if err != nil { + panic(err) + } + + // helper methods for transactions + cdc := app.MakeCodec() // make codec for the app + + // get the keybase + keybase := getKeybase() + + // get the validator address for delegation + valAddr, err := sdk.ValAddressFromBech32("kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0") // **FAUCET** + if err != nil { + panic(err) + } + + // create delegation amount + undelAmount := sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000000) + undelegation := staking.NewMsgUndelegate(addrFrom, valAddr, undelAmount) + delegationToSend := []sdk.Msg{undelegation} + + // send the delegation to the blockchain + sendMsgToBlockchain(cdc, address, keyname, password, delegationToSend, keybase) + +} + +func getKeybase() crkeys.Keybase { + // create a keybase + // IMPORTANT - TAKE THIS FROM COMMAND LINE PARAMETER and does NOT work with tilde i.e. ~/ does NOT work + keybase, err := keys.NewKeyBaseFromDir(os.Args[1]) + if err != nil { + panic(err) + } + + return keybase +} + +// sendMsgToBlockchain sends a message to the blockchain via the rest api +func sendMsgToBlockchain(cdc *amino.Codec, address string, keyname string, + password string, msg []sdk.Msg, keybase crkeys.Keybase) { + + // get the account number and sequence number + accountNumber, sequenceNumber := getAccountNumberAndSequenceNumber(cdc, address) + + txBldr := auth.NewTxBuilderFromCLI(). + WithTxEncoder(authclient.GetTxEncoder(cdc)).WithChainID("testing"). + WithKeybase(keybase).WithAccountNumber(accountNumber). + WithSequence(sequenceNumber) + + // build and sign the transaction + // this is the *Amino* encoded version of the transaction + // fmt.Printf("%+v", txBldr.Keybase()) + txBytes, err := txBldr.BuildAndSign("vlad", "password", msg) + if err != nil { + panic(err) + } + // fmt.Printf("txBytes: %s", txBytes) + + // need to convert the Amino encoded version back to an actual go struct + var tx auth.StdTx + cdc.UnmarshalBinaryLengthPrefixed(txBytes, &tx) // might be unmarshal binary bare + + // now we re-marshall it again into json + jsonBytes, err := cdc.MarshalJSON( + authrest.BroadcastReq{ + Tx: tx, + Mode: "block", + }, + ) + if err != nil { + panic(err) + } + // fmt.Println("post body: ", string(jsonBytes)) + + resp, err := http.Post("http://localhost:1317/txs", "application/json", bytes.NewBuffer(jsonBytes)) + if err != nil { + panic(err) + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + fmt.Printf("\n\nBody:\n\n") + fmt.Println(string(body)) + +} + +// getAccountNumberAndSequenceNumber gets an account number and sequence number from the blockchain +func getAccountNumberAndSequenceNumber(cdc *amino.Codec, address string) (accountNumber uint64, sequenceNumber uint64) { + + // we need to setup the account number and sequence in order to have a valid transaction + resp, err := http.Get("http://localhost:1317/auth/accounts/" + address) + if err != nil { + panic(err) + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + var bodyUnmarshalled sdkrest.ResponseWithHeight + err = cdc.UnmarshalJSON(body, &bodyUnmarshalled) + if err != nil { + panic(err) + } + + var account authexported.Account + err = cdc.UnmarshalJSON(bodyUnmarshalled.Result, &account) + if err != nil { + panic(err) + } + + return account.GetAccountNumber(), account.GetSequence() + +} diff --git a/rest_test/stopchain.sh b/rest_test/stopchain.sh new file mode 100755 index 00000000..d392bbbd --- /dev/null +++ b/rest_test/stopchain.sh @@ -0,0 +1,7 @@ +#! /bin/bash + +# THIS IS A BASH SCRIPT THAT STOPS THE BLOCKCHAIN AND THE REST API +echo "Stopping blockchain" +pgrep kvd | xargs kill +pgrep kvcli | xargs kill +echo "COMPLETED" \ No newline at end of file diff --git a/swagger-ui/swagger.yaml b/swagger-ui/swagger.yaml index e3b526d1..9d564f22 100644 --- a/swagger-ui/swagger.yaml +++ b/swagger-ui/swagger.yaml @@ -152,7 +152,7 @@ description: Block height required: true type: number - x-example: 1 + x-example: 2 responses: 200: description: The block at a specific height @@ -231,7 +231,7 @@ description: Tx hash required: true type: string - x-example: BCBE20E8D46758B96AE5883B792858296AC06E51435490FBDCAE25A72B3CC76B + x-example: B901DB8552BC5F5F82E5709EC37AAD8C0F8005CDB3608E5580055CC4D0D9B275 responses: 200: description: Tx with the provided hash @@ -256,8 +256,8 @@ - in: query name: message.sender type: string - description: "transaction tags with sender: 'GET /txs?message.action=send&message.sender=kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv'" - x-example: "kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv" + description: "transaction tags with sender: 'GET /txs?message.action=send&message.sender=kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9'" + x-example: "kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9" - in: query name: page description: Page number @@ -268,6 +268,11 @@ description: Maximum number of items per page type: integer x-example: 1 + - in: query + name: txheight # this should actually be tx.height but dredd doesn't handle periods in get parameter names so no period allows the test to pass + description: Transaction height + type: integer + x-example: 1 responses: 200: description: All txs matching the provided events @@ -295,10 +300,10 @@ type: object properties: tx: - $ref: "#/definitions/StdTx" + $ref: "#/definitions/PostStdTx" mode: type: string - example: sync + example: block responses: 200: description: Tx broadcasting result @@ -317,15 +322,13 @@ produces: - application/json parameters: - - in: body - name: tx - description: The tx to encode + - description: "" + name: EncodeBody + in: body required: true schema: type: object - properties: - tx: - $ref: "#/definitions/StdTx" + $ref: "#/definitions/EncodeTx" responses: 200: description: The tx was successfully decoded and re-encoded @@ -336,7 +339,7 @@ type: string example: The base64-encoded Amino-serialized bytes for the tx 400: - description: The tx was malformated + description: The tx was malformatted 500: description: Server internal error /txs/decode: @@ -359,7 +362,7 @@ properties: tx: type: string - example: SvBiXe4KPqijYZoKFFHEzJ8c2HPAfv2EFUcIhx0yPagwEhTy0vPA+GGhCEslKXa4Af0uB+mfShoMCgVzdGFrZRIDMTAwEgQQwJoM + example: ogEoKBapCjyoo2GaChRKWendsRagTF1ACC1nxzjVxW3xJBIU/A6hCReBn5NYI6K2dl10kCEeTfgaCgoFc3Rha2USATESEAoKCgVzdGFrZRIBMRDAmgwaQhJAdlC1HbNw+ux6lRrK3mNdmaH62NE3ThD8SswlDcnhFex7pKSNhaxE4m6TgDhosoK6EyU0LnOZKutXKECNSvO+WCIIdGVzdG1lbW8= responses: 200: description: The tx was successfully decoded @@ -382,14 +385,18 @@ description: Account address in bech32 format required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c responses: 200: description: Account balances schema: - type: array - items: - $ref: "#/definitions/Coin" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/Coin" 500: description: Server internal error /bank/accounts/{address}/transfers: @@ -407,7 +414,7 @@ description: Account address in bech32 format required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 - in: body name: account description: The sender and tx information @@ -422,8 +429,8 @@ items: $ref: "#/definitions/Coin" responses: - 202: - description: Tx was succesfully generated + 200: + description: Tx was successfully generated schema: $ref: "#/definitions/StdTx" 400: @@ -443,7 +450,7 @@ description: Account address required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 responses: 200: description: Account information on the blockchain @@ -476,7 +483,7 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 get: summary: Get all delegations from a delegator tags: @@ -487,47 +494,55 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Delegation" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/Delegation" 400: description: Invalid delegator address 500: description: Internal Server Error - post: - summary: Submit delegation - parameters: - - in: body - name: delegation - description: The password of the account to remove from the KMS - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - delegator_address: - $ref: "#/definitions/Address" - validator_address: - $ref: "#/definitions/ValidatorAddress" - delegation: - $ref: "#/definitions/Coin" - tags: - - Staking - consumes: - - application/json - produces: - - application/json - responses: - 200: - description: OK - schema: - $ref: "#/definitions/BroadcastTxCommitResult" - 400: - description: Invalid delegator address or delegation request body - 401: - description: Key password is wrong - 500: - description: Internal Server Error + + # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE: + # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go + + # post: + # summary: Submit delegation + # parameters: + # - in: body + # name: delegation + # description: The password of the account to remove from the KMS + # schema: + # type: object + # properties: + # base_req: + # $ref: "#/definitions/BaseReq" + # delegator_address: + # $ref: "#/definitions/Address" + # validator_address: + # $ref: "#/definitions/ValidatorAddress" + # delegation: + # $ref: "#/definitions/Coin" + # tags: + # - Staking + # consumes: + # - application/json + # produces: + # - application/json + # responses: + # 200: + # description: OK + # schema: + # $ref: "#/definitions/BroadcastTxCommitResult" + # 400: + # description: Invalid delegator address or delegation request body + # 401: + # description: Key password is wrong + # 500: + # description: Internal Server Error /staking/delegators/{delegatorAddr}/delegations/{validatorAddr}: parameters: - in: path @@ -535,13 +550,13 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path name: validatorAddr description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Query the current delegation between a delegator and a validator tags: @@ -564,7 +579,7 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 get: summary: Get all unbonding delegations from a delegator tags: @@ -575,48 +590,57 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/UnbondingDelegation" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/UnbondingDelegation" + 400: description: Invalid delegator address 500: description: Internal Server Error - post: - summary: Submit an unbonding delegation - parameters: - - in: body - name: delegation - description: The password of the account to remove from the KMS - schema: - type: object - properties: - base_req: - $ref: "#/definitions/BaseReq" - delegator_address: - $ref: "#/definitions/Address" - validator_address: - $ref: "#/definitions/ValidatorAddress" - shares: - type: string - example: "100" - tags: - - Staking - consumes: - - application/json - produces: - - application/json - responses: - 200: - description: OK - schema: - $ref: "#/definitions/BroadcastTxCommitResult" - 400: - description: Invalid delegator address or unbonding delegation request body - 401: - description: Key password is wrong - 500: - description: Internal Server Error + + # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE: + # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go + + # post: + # summary: Submit an unbonding delegation + # parameters: + # - in: body + # name: delegation + # description: The password of the account to remove from the KMS + # schema: + # type: object + # properties: + # base_req: + # $ref: "#/definitions/BaseReq" + # delegator_address: + # $ref: "#/definitions/Address" + # validator_address: + # $ref: "#/definitions/ValidatorAddress" + # shares: + # type: string + # example: "100" + # tags: + # - Staking + # consumes: + # - application/json + # produces: + # - application/json + # responses: + # 200: + # description: OK + # schema: + # $ref: "#/definitions/BroadcastTxCommitResult" + # 400: + # description: Invalid delegator address or unbonding delegation request body + # 401: + # description: Key password is wrong + # 500: + # description: Internal Server Error /staking/delegators/{delegatorAddr}/unbonding_delegations/{validatorAddr}: parameters: - in: path @@ -624,13 +648,13 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path name: validatorAddr description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Query all unbonding delegations between a delegator and a validator tags: @@ -672,55 +696,63 @@ responses: 200: description: OK - schema: - type: array - items: - $ref: "#/definitions/Redelegation" - 500: - description: Internal Server Error - /staking/delegators/{delegatorAddr}/redelegations: - parameters: - - in: path - name: delegatorAddr - description: Bech32 AccAddress of Delegator - required: true - type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv - post: - summary: Submit a redelegation - parameters: - - in: body - name: delegation - description: The sender and tx information schema: type: object properties: - base_req: - $ref: "#/definitions/BaseReq" - delegator_address: - $ref: "#/definitions/Address" - validator_src_addressess: - $ref: "#/definitions/ValidatorAddress" - validator_dst_address: - $ref: "#/definitions/ValidatorAddress" - shares: + height: type: string - example: "100" - tags: - - Staking - consumes: - - application/json - produces: - - application/json - responses: - 200: - description: Tx was succesfully generated - schema: - $ref: "#/definitions/StdTx" - 400: - description: Invalid delegator address or redelegation request body + result: + items: + $ref: "#/definitions/Redelegation" 500: description: Internal Server Error + + # THE BELOW ENDPOINT IS NOT IMPLEMENTED IN COSMOS, SEE: + # https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/rest/query.go + + # /staking/delegators/{delegatorAddr}/redelegations: + # parameters: + # - in: path + # name: delegatorAddr + # description: Bech32 AccAddress of Delegator + # required: true + # type: string + # x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c + # post: + # summary: Submit a redelegation + # parameters: + # - in: body + # name: delegation + # description: The sender and tx information + # schema: + # type: object + # properties: + # base_req: + # $ref: "#/definitions/BaseReq" + # delegator_address: + # $ref: "#/definitions/Address" + # validator_src_addresses: + # $ref: "#/definitions/ValidatorAddress" + # validator_dst_address: + # $ref: "#/definitions/ValidatorAddress" + # shares: + # type: string + # example: "100" + # tags: + # - Staking + # consumes: + # - application/json + # produces: + # - application/json + # responses: + # 200: + # description: Tx was successfully generated + # schema: + # $ref: "#/definitions/StdTx" + # 400: + # description: Invalid delegator address or redelegation request body + # 500: + # description: Internal Server Error /staking/delegators/{delegatorAddr}/validators: parameters: - in: path @@ -728,7 +760,7 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 get: summary: Query all validators that a delegator is bonded to tags: @@ -739,9 +771,13 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Validator" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/Validator" 400: description: Invalid delegator address 500: @@ -753,13 +789,13 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path name: validatorAddr description: Bech32 ValAddress of Delegator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Query a validator that a delegator is bonded to tags: @@ -802,9 +838,13 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Validator" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/Validator" 500: description: Internal Server Error /staking/validators/{validatorAddr}: @@ -814,7 +854,7 @@ description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Query the information from a single validator tags: @@ -825,7 +865,14 @@ 200: description: OK schema: - $ref: "#/definitions/Validator" + type: object + properties: + height: + type: string + result: + # type: array // TODO what if there are multiple validators? should it be an array? + items: + $ref: "#/definitions/Validator" 400: description: Invalid validator address 500: @@ -837,7 +884,7 @@ description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj get: summary: Get all delegations from a validator tags: @@ -848,9 +895,13 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Delegation" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/Delegation" 400: description: Invalid validator address 500: @@ -862,7 +913,7 @@ description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj get: summary: Get all unbonding delegations from a validator tags: @@ -873,9 +924,13 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/UnbondingDelegation" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/UnbondingDelegation" 400: description: Invalid validator address 500: @@ -986,9 +1041,14 @@ 200: description: OK schema: - type: array items: - $ref: "#/definitions/SigningInfo" + type: object + properties: + height: + type: string + result: + items: + $ref: "#/definitions/SigningInfo" 400: description: Invalid validator public key for one of the validators 500: @@ -1009,7 +1069,7 @@ name: validatorAddr required: true in: path - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj - description: "" name: UnjailBody in: body @@ -1018,10 +1078,10 @@ type: object properties: base_req: - $ref: "#/definitions/StdTx" + $ref: "#/definitions/BaseReq" responses: 200: - description: Tx was succesfully generated + description: Tx was successfully generated schema: $ref: "#/definitions/BroadcastTxCommitResult" 400: @@ -1079,8 +1139,10 @@ $ref: "#/definitions/BaseReq" title: type: string + example: Test_title description: type: string + example: my_test_description proposal_type: type: string example: "text" @@ -1092,7 +1154,7 @@ $ref: "#/definitions/Coin" responses: 200: - description: Tx was succesfully generated + description: Tx was successfully generated schema: $ref: "#/definitions/StdTx" 400: @@ -1126,9 +1188,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/TextProposal" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/TextProposal" 400: description: Invalid query parameters 500: @@ -1155,10 +1222,10 @@ $ref: "#/definitions/BaseReq" title: type: string - x-example: "Param Change" + example: Param_Change description: type: string - x-example: "Update max validators" + example: Update_max_validators proposer: $ref: "#/definitions/Address" deposit: @@ -1171,7 +1238,7 @@ $ref: "#/definitions/ParamChange" responses: 200: - description: The transaction was succesfully generated + description: The transaction was successfully generated schema: $ref: "#/definitions/StdTx" 400: @@ -1242,9 +1309,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Deposit" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/Deposit" 400: description: Invalid proposal id 500: @@ -1311,7 +1383,7 @@ name: depositor required: true in: path - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c responses: 200: description: OK @@ -1342,9 +1414,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Vote" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/Vote" 400: description: Invalid proposal id 500: @@ -1410,7 +1487,7 @@ name: voter required: true in: path - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c responses: 200: description: OK @@ -1530,7 +1607,7 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava167w96tdvmazakdwkw2u57227eduula2cy572lf + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c get: summary: Get the total rewards balance from all delegations description: Get the sum of all the rewards earned by delegations by a single delegator @@ -1581,13 +1658,13 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava16xyempempp92x9hyzz9wrgf94r6j9h5f06pxxv + x-example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c - in: path name: validatorAddr description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Query a delegation reward description: Query a single delegation reward by a delegator @@ -1599,9 +1676,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Coin" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/Coin" 400: description: Invalid delegator address 500: @@ -1640,7 +1722,7 @@ description: Bech32 AccAddress of Delegator required: true type: string - x-example: kava167w96tdvmazakdwkw2u57227eduula2cy572lf + x-example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 get: summary: Get the rewards withdrawal address description: Get the delegations' rewards withdrawal address. This is the address in which the user will receive the reward funds @@ -1652,7 +1734,13 @@ 200: description: OK schema: - $ref: "#/definitions/Address" + type: object + properties: + height: + type: string + result: + $ref: "#/definitions/Address" + 400: description: Invalid delegator address 500: @@ -1693,7 +1781,7 @@ description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Validator distribution information description: Query the distribution information of a single validator @@ -1717,7 +1805,7 @@ description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Fee distribution outstanding rewards of a single validator tags: @@ -1728,9 +1816,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Coin" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/Coin" 500: description: Internal Server Error /distribution/validators/{validatorAddr}/rewards: @@ -1740,7 +1833,7 @@ description: Bech32 OperatorAddress of validator required: true type: string - x-example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + x-example: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 get: summary: Commission and self-delegation rewards of a single validator description: Query the commission and self-delegation rewards of validator. @@ -1752,9 +1845,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Coin" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/Coin" 400: description: Invalid validator address 500: @@ -1797,9 +1895,14 @@ 200: description: OK schema: - type: array - items: - $ref: "#/definitions/Coin" + type: object + properties: + height: + type: string + result: + type: array + items: + $ref: "#/definitions/Coin" 500: description: Internal Server Error /distribution/parameters: @@ -1859,7 +1962,7 @@ 200: description: OK schema: - type: string + $ref: "#/definitions/StandardResponse" 500: description: Internal Server Error /minting/annual-provisions: @@ -1873,7 +1976,7 @@ 200: description: OK schema: - type: string + $ref: "#/definitions/StandardResponse" 500: description: Internal Server Error /supply/total: @@ -1908,7 +2011,7 @@ 200: description: OK schema: - type: string + $ref: "#/definitions/StandardResponse" 400: description: Invalid coin denomination 500: @@ -1982,7 +2085,7 @@ hash: $ref: "#/definitions/Hash" height: - type: integer + type: string KVPair: type: object properties: @@ -1992,8 +2095,6 @@ type: string Msg: type: object - required: - - from properties: type: type: string @@ -2001,26 +2102,72 @@ value: type: object properties: - from: + from_address: type: string - example: kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq + example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c + to_address: + type: string + example: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz + amount: + type: array + items: + $ref: "#/definitions/Coin" + PostMsg: + type: object + properties: + type: + type: string + example: "cosmos-sdk/MsgSend" + value: + type: object + properties: + from_address: + type: string + example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c + to_address: + type: string + example: kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz + amount: + type: array + items: + $ref: "#/definitions/PostCoin1" + + Address: type: string description: bech32 encoded address - example: kava1depk54cuajgkzea6zpgkq36tnjwdzv4afc3d27 + example: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c ValidatorAddress: type: string description: bech32 encoded address - example: kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l + example: kavavaloper1q53rwutgpzx7szcrgzqguxyccjpzt9j44jzrtj Coin: type: object properties: denom: type: string - example: ukava + example: stake amount: type: string - example: "50" + example: "1" + PostCoin1: + type: object + properties: + denom: + type: string + example: stake + amount: + type: string + example: "2000000" + PostCoin2: + type: object + properties: + denom: + type: string + example: stake + amount: + type: string + example: "2000" Hash: type: string example: EE5F3404034C524501629B56E0DDC38FAD651F04 @@ -2029,7 +2176,7 @@ properties: signature: type: string - example: "W1HfKcc4F0rCSoxheZ7fsrB5nGK58U4gKysuzsmUwhloVnCxmbCx289uVMMvQN6tOcQsz7hMVTJrXSA1xzevvw==" + example: "dlC1HbNw+ux6lRrK3mNdmaH62NE3ThD8SswlDcnhFex7pKSNhaxE4m6TgDhosoK6EyU0LnOZKutXKECNSvO+WA==" pubkey: type: object properties: @@ -2038,7 +2185,24 @@ example: "tendermint/PubKeySecp256k1" value: type: string - example: "Agey31h/NYpcy0sYm4liHMrXJMzbQUrgV4uHd/w09CXN" + example: "AmWAim83Qp+kIcj3RT7i327b3l0EHwzCrGVGXusb70B7" + + PostSignature: + type: object + properties: + signature: + type: string + example: "KzBeQp/2+47oiqc16BImnOYidSAesZ3kgR3S8Fy+baFlvDlDv7goU/+rm4c7+woudNte3uZAG0CuUeHsF+Ld8Q==" + pubkey: + type: object + properties: + type: + type: string + example: "tendermint/PubKeySecp256k1" + value: + type: string + example: "AmWAim83Qp+kIcj3RT7i327b3l0EHwzCrGVGXusb70B7" + TxQuery: type: object properties: @@ -2046,8 +2210,8 @@ type: string example: "D085138D913993919295FF4B0A9107F1F2CDE0D37A87CE0644E217CBF3B49656" height: - type: number - example: 368 + type: string + example: "368" tx: $ref: "#/definitions/StdTx" result: @@ -2069,20 +2233,20 @@ type: object properties: total_count: - type: number - example: 1 + type: string + example: "1" count: - type: number - example: 1 + type: string + example: "1" page_number: - type: number - example: 1 + type: string + example: "1" page_total: - type: number - example: 1 + type: string + example: "1" limit: - type: number - example: 30 + type: string + example: "30" txs: type: array items: @@ -2099,16 +2263,53 @@ properties: gas: type: string + example: "200000" amount: type: array items: $ref: "#/definitions/Coin" memo: type: string + example: "testmemo" signatures: type: array items: $ref: "#/definitions/Signature" + + PostStdTx: + type: object + properties: + msg: + type: array + items: + $ref: "#/definitions/PostMsg" + fee: + type: object + properties: + gas: + type: string + example: "200000" + amount: + type: array + items: + $ref: "#/definitions/PostCoin2" + memo: + type: string + example: "" + signatures: + type: array + items: + $ref: "#/definitions/PostSignature" + + EncodeTx: + type: object + properties: + type: + type: string + example: "cosmos-sdk/StdTx" + value: + type: object + $ref: "#/definitions/StdTx" BlockID: type: object properties: @@ -2118,8 +2319,8 @@ type: object properties: total: - type: number - example: 0 + type: string + example: "0" hash: $ref: "#/definitions/Hash" BlockHeader: @@ -2129,19 +2330,19 @@ type: string example: kavahub-2 height: - type: number - example: 1 + type: string + example: "1" time: type: string example: "2017-12-30T05:53:09.287+01:00" num_txs: - type: number - example: 0 + type: string + example: "0" last_block_id: $ref: "#/definitions/BlockID" total_txs: - type: number - example: 35 + type: string + example: "35" last_commit_hash: $ref: "#/definitions/Hash" data_hash: @@ -2174,46 +2375,54 @@ properties: header: $ref: "#/definitions/BlockHeader" - txs: - type: array - items: - type: string + data: + type: object + properties: + txs: + type: array + x-nullable: true evidence: - type: array - items: - type: string + type: object + properties: + evidence: + type: object + x-nullable: true last_commit: type: object properties: block_id: $ref: "#/definitions/BlockID" precommits: - type: array - items: - type: object - properties: - validator_address: - type: string - validator_index: - type: string - example: "0" - height: - type: string - example: "0" - round: - type: string - example: "0" - timestamp: - type: string - example: "2017-12-30T05:53:09.287+01:00" - type: - type: number - example: 2 - block_id: - $ref: "#/definitions/BlockID" - signature: - type: string - example: "7uTC74QlknqYWEwg7Vn6M8Om7FuZ0EO4bjvuj6rwH1mTUJrRuMMZvAAqT9VjNgP0RA/TDp6u/92AqrZfXJSpBQ==" + # have to use an 'object' type for precommits as it is nullable + $ref: '#/definitions/Precommits' + Precommits: + type: array + items: + type: object + properties: + validator_address: + type: string + validator_index: + type: string + example: "0" + height: + type: string + example: "2" + round: + type: string + example: "0" + timestamp: + type: string + example: "2017-12-30T05:53:09.287+01:00" + type: + type: number + example: 2 + block_id: + $ref: "#/definitions/BlockID" + signature: + type: string + example: "7uTC74QlknqYWEwg7Vn6M8Om7FuZ0EO4bjvuj6rwH1mTUJrRuMMZvAAqT9VjNgP0RA/TDp6u/92AqrZfXJSpBQ==" + BlockQuery: type: object properties: @@ -2251,11 +2460,11 @@ properties: from: type: string - example: "kava1g9ahr6xhht5rmqven628nklxluzyv8z9jqjcmc" + example: "kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9" description: Sender address or Keybase name to generate a transaction memo: type: string - example: "Sent via Cosmos Voyager 🚀" + example: Sent_via_Kava chain_id: type: string example: "Cosmos-Hub" @@ -2539,4 +2748,11 @@ total: type: array items: - $ref: "#/definitions/Coin" \ No newline at end of file + $ref: "#/definitions/Coin" + StandardResponse: + type: object + properties: + height: + type: string + result: + type: string \ No newline at end of file diff --git a/x/bep3/abci.go b/x/bep3/abci.go new file mode 100644 index 00000000..b5c2d424 --- /dev/null +++ b/x/bep3/abci.go @@ -0,0 +1,18 @@ +package bep3 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BeginBlocker runs at the start of every block +func BeginBlocker(ctx sdk.Context, k Keeper) { + err := k.UpdateExpiredAtomicSwaps(ctx) + if err != nil { + panic(err) + } + + err = k.DeleteClosedAtomicSwapsFromLongtermStorage(ctx) + if err != nil { + panic(err) + } +} diff --git a/x/bep3/abci_test.go b/x/bep3/abci_test.go new file mode 100644 index 00000000..37ca6eb2 --- /dev/null +++ b/x/bep3/abci_test.go @@ -0,0 +1,242 @@ +package bep3_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3" +) + +type ABCITestSuite struct { + suite.Suite + keeper bep3.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress + swapIDs []cmn.HexBytes + randomNumbers []cmn.HexBytes +} + +func (suite *ABCITestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + // Set up auth GenesisState + _, addrs := app.GeneratePrivKeyAddressPairs(11) + coins := []sdk.Coins{} + for j := 0; j < 11; j++ { + coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000))) + } + authGS := app.NewAuthGenState(addrs, coins) + // Initialize test app + tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(addrs[0])) + + suite.ctx = ctx + suite.app = tApp + suite.addrs = addrs + suite.ResetKeeper() +} + +func (suite *ABCITestSuite) ResetKeeper() { + suite.keeper = suite.app.GetBep3Keeper() + + var swapIDs []cmn.HexBytes + var randomNumbers []cmn.HexBytes + for i := 0; i < 10; i++ { + // Set up atomic swap variables + expireHeight := int64(360) + amount := cs(c("bnb", int64(100))) + timestamp := ts(i) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + // Create atomic swap and check err to confirm creation + err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight, + suite.addrs[0], suite.addrs[i], TestSenderOtherChain, TestRecipientOtherChain, + amount, amount.String(), true) + suite.Nil(err) + + // Store swap's calculated ID and secret random number + swapID := bep3.CalculateSwapID(randomNumberHash, suite.addrs[0], TestSenderOtherChain) + swapIDs = append(swapIDs, swapID) + randomNumbers = append(randomNumbers, randomNumber.Bytes()) + } + suite.swapIDs = swapIDs + suite.randomNumbers = randomNumbers +} + +func (suite *ABCITestSuite) TestBeginBlocker_UpdateExpiredAtomicSwaps() { + testCases := []struct { + name string + firstCtx sdk.Context + secondCtx sdk.Context + expectedStatus bep3.SwapStatus + expectInStorage bool + }{ + { + name: "normal", + firstCtx: suite.ctx, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 10), + expectedStatus: bep3.Open, + expectInStorage: true, + }, + { + name: "after expiration", + firstCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400), + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 410), + expectedStatus: bep3.Expired, + expectInStorage: true, + }, + { + name: "after completion", + firstCtx: suite.ctx, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 10), + expectedStatus: bep3.Completed, + expectInStorage: true, + }, + { + name: "after deletion", + firstCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400), + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400 + bep3.DefaultLongtermStorageDuration), + expectedStatus: bep3.NULL, + expectInStorage: false, + }, + } + + for _, tc := range testCases { + // Reset keeper and run the initial begin blocker + suite.ResetKeeper() + suite.Run(tc.name, func() { + bep3.BeginBlocker(tc.firstCtx, suite.keeper) + + switch tc.expectedStatus { + case bep3.Completed: + for i, swapID := range suite.swapIDs { + err := suite.keeper.ClaimAtomicSwap(tc.firstCtx, suite.addrs[5], swapID, suite.randomNumbers[i]) + suite.Nil(err) + } + case bep3.NULL: + for _, swapID := range suite.swapIDs { + err := suite.keeper.RefundAtomicSwap(tc.firstCtx, suite.addrs[5], swapID) + suite.Nil(err) + } + } + + // Run the second begin blocker + bep3.BeginBlocker(tc.secondCtx, suite.keeper) + + // Check each swap's availibility and status + for _, swapID := range suite.swapIDs { + storedSwap, found := suite.keeper.GetAtomicSwap(tc.secondCtx, swapID) + if tc.expectInStorage { + suite.True(found) + } else { + suite.False(found) + } + suite.Equal(tc.expectedStatus, storedSwap.Status) + } + }) + } +} + +func (suite *ABCITestSuite) TestBeginBlocker_DeleteClosedAtomicSwapsFromLongtermStorage() { + type Action int + const ( + NULL Action = 0x00 + Refund Action = 0x01 + Claim Action = 0x02 + ) + + testCases := []struct { + name string + firstCtx sdk.Context + action Action + secondCtx sdk.Context + expectInStorage bool + }{ + { + name: "no action with long storage duration", + firstCtx: suite.ctx, + action: NULL, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + bep3.DefaultLongtermStorageDuration), + expectInStorage: true, + }, + { + name: "claim with short storage duration", + firstCtx: suite.ctx, + action: Claim, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 5000), + expectInStorage: true, + }, + { + name: "claim with long storage duration", + firstCtx: suite.ctx, + action: Claim, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + bep3.DefaultLongtermStorageDuration), + expectInStorage: false, + }, + { + name: "refund with short storage duration", + firstCtx: suite.ctx, + action: Refund, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 5000), + expectInStorage: true, + }, + { + name: "refund with long storage duration", + firstCtx: suite.ctx, + action: Refund, + secondCtx: suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + bep3.DefaultLongtermStorageDuration), + expectInStorage: false, + }, + } + + for _, tc := range testCases { + // Reset keeper and run the initial begin blocker + suite.ResetKeeper() + suite.Run(tc.name, func() { + bep3.BeginBlocker(tc.firstCtx, suite.keeper) + + switch tc.action { + case Claim: + for i, swapID := range suite.swapIDs { + err := suite.keeper.ClaimAtomicSwap(tc.firstCtx, suite.addrs[5], swapID, suite.randomNumbers[i]) + suite.Nil(err) + } + case Refund: + for _, swapID := range suite.swapIDs { + swap, _ := suite.keeper.GetAtomicSwap(tc.firstCtx, swapID) + refundCtx := suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + swap.ExpireHeight) + bep3.BeginBlocker(refundCtx, suite.keeper) + err := suite.keeper.RefundAtomicSwap(refundCtx, suite.addrs[5], swapID) + suite.Nil(err) + // Add expire height to second ctx block height + tc.secondCtx = tc.secondCtx.WithBlockHeight(tc.secondCtx.BlockHeight() + swap.ExpireHeight) + } + } + + // Run the second begin blocker + bep3.BeginBlocker(tc.secondCtx, suite.keeper) + + // Check each swap's availibility and status + for _, swapID := range suite.swapIDs { + _, found := suite.keeper.GetAtomicSwap(tc.secondCtx, swapID) + if tc.expectInStorage { + suite.True(found) + } else { + suite.False(found) + } + } + }) + } +} + +func TestABCITestSuite(t *testing.T) { + suite.Run(t, new(ABCITestSuite)) +} diff --git a/x/bep3/alias.go b/x/bep3/alias.go new file mode 100644 index 00000000..c020c3af --- /dev/null +++ b/x/bep3/alias.go @@ -0,0 +1,160 @@ +// nolint +// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen) +package bep3 + +import ( + "github.com/kava-labs/kava/x/bep3/client/rest" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" +) + +const ( + AddrByteCount = types.AddrByteCount + AttributeKeyAmount = types.AttributeKeyAmount + AttributeKeyAtomicSwapID = types.AttributeKeyAtomicSwapID + AttributeKeyClaimSender = types.AttributeKeyClaimSender + AttributeKeyDirection = types.AttributeKeyDirection + AttributeKeyExpectedIncome = types.AttributeKeyExpectedIncome + AttributeKeyExpireHeight = types.AttributeKeyExpireHeight + AttributeKeyRandomNumber = types.AttributeKeyRandomNumber + AttributeKeyRandomNumberHash = types.AttributeKeyRandomNumberHash + AttributeKeyRecipient = types.AttributeKeyRecipient + AttributeKeyRefundSender = types.AttributeKeyRefundSender + AttributeKeySender = types.AttributeKeySender + AttributeKeySenderOtherChain = types.AttributeKeySenderOtherChain + AttributeKeyTimestamp = types.AttributeKeyTimestamp + AttributeValueCategory = types.AttributeValueCategory + CalcSwapID = types.CalcSwapID + ClaimAtomicSwap = types.ClaimAtomicSwap + CodeAssetNotActive = types.CodeAssetNotActive + CodeAssetNotSupported = types.CodeAssetNotSupported + CodeAssetSupplyNotFound = types.CodeAssetSupplyNotFound + CodeAtomicSwapAlreadyExists = types.CodeAtomicSwapAlreadyExists + CodeAtomicSwapNotFound = types.CodeAtomicSwapNotFound + CodeExceedsAvailableSupply = types.CodeExceedsAvailableSupply + CodeExceedsSupplyLimit = types.CodeExceedsSupplyLimit + CodeInvalidClaimSecret = types.CodeInvalidClaimSecret + CodeInvalidCurrentSupply = types.CodeInvalidCurrentSupply + CodeInvalidHeightSpan = types.CodeInvalidHeightSpan + CodeInvalidIncomingSupply = types.CodeInvalidIncomingSupply + CodeInvalidOutgoingSupply = types.CodeInvalidOutgoingSupply + CodeInvalidTimestamp = types.CodeInvalidTimestamp + CodeSwapNotClaimable = types.CodeSwapNotClaimable + CodeSwapNotRefundable = types.CodeSwapNotRefundable + Completed = types.Completed + CreateAtomicSwap = types.CreateAtomicSwap + DefaultCodespace = types.DefaultCodespace + DefaultLongtermStorageDuration = types.DefaultLongtermStorageDuration + DefaultParamspace = types.DefaultParamspace + DepositAtomicSwap = types.DepositAtomicSwap + EventTypeClaimAtomicSwap = types.EventTypeClaimAtomicSwap + EventTypeCreateAtomicSwap = types.EventTypeCreateAtomicSwap + EventTypeDepositAtomicSwap = types.EventTypeDepositAtomicSwap + EventTypeRefundAtomicSwap = types.EventTypeRefundAtomicSwap + Expired = types.Expired + INVALID = types.INVALID + Incoming = types.Incoming + Int64Size = types.Int64Size + MaxExpectedIncomeLength = types.MaxExpectedIncomeLength + MaxOtherChainAddrLength = types.MaxOtherChainAddrLength + ModuleName = types.ModuleName + NULL = types.NULL + Open = types.Open + Outgoing = types.Outgoing + QuerierRoute = types.QuerierRoute + QueryGetAssetSupply = types.QueryGetAssetSupply + QueryGetAtomicSwap = types.QueryGetAtomicSwap + QueryGetAtomicSwaps = types.QueryGetAtomicSwaps + QueryGetParams = types.QueryGetParams + RandomNumberHashLength = types.RandomNumberHashLength + RandomNumberLength = types.RandomNumberLength + RefundAtomicSwap = types.RefundAtomicSwap + RouterKey = types.RouterKey + StoreKey = types.StoreKey + SwapIDLength = types.SwapIDLength +) + +var ( + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + RegisterRoutes = rest.RegisterRoutes + BytesToHex = types.BytesToHex + CalculateRandomHash = types.CalculateRandomHash + CalculateSwapID = types.CalculateSwapID + DefaultGenesisState = types.DefaultGenesisState + DefaultParams = types.DefaultParams + ErrAssetNotActive = types.ErrAssetNotActive + ErrAssetNotSupported = types.ErrAssetNotSupported + ErrAssetSupplyNotFound = types.ErrAssetSupplyNotFound + ErrAtomicSwapAlreadyExists = types.ErrAtomicSwapAlreadyExists + ErrAtomicSwapNotFound = types.ErrAtomicSwapNotFound + ErrExceedsAvailableSupply = types.ErrExceedsAvailableSupply + ErrExceedsSupplyLimit = types.ErrExceedsSupplyLimit + ErrInvalidClaimSecret = types.ErrInvalidClaimSecret + ErrInvalidCurrentSupply = types.ErrInvalidCurrentSupply + ErrInvalidHeightSpan = types.ErrInvalidHeightSpan + ErrInvalidIncomingSupply = types.ErrInvalidIncomingSupply + ErrInvalidOutgoingSupply = types.ErrInvalidOutgoingSupply + ErrInvalidTimestamp = types.ErrInvalidTimestamp + ErrSwapNotClaimable = types.ErrSwapNotClaimable + ErrSwapNotRefundable = types.ErrSwapNotRefundable + GenerateSecureRandomNumber = types.GenerateSecureRandomNumber + GetAtomicSwapByHeightKey = types.GetAtomicSwapByHeightKey + HexToBytes = types.HexToBytes + NewAssetSupply = types.NewAssetSupply + NewAtomicSwap = types.NewAtomicSwap + NewGenesisState = types.NewGenesisState + NewMsgClaimAtomicSwap = types.NewMsgClaimAtomicSwap + NewMsgCreateAtomicSwap = types.NewMsgCreateAtomicSwap + NewMsgRefundAtomicSwap = types.NewMsgRefundAtomicSwap + NewParams = types.NewParams + NewQueryAssetSupply = types.NewQueryAssetSupply + NewQueryAtomicSwapByID = types.NewQueryAtomicSwapByID + NewQueryAtomicSwaps = types.NewQueryAtomicSwaps + NewSwapDirectionFromString = types.NewSwapDirectionFromString + NewSwapStatusFromString = types.NewSwapStatusFromString + ParamKeyTable = types.ParamKeyTable + RegisterCodec = types.RegisterCodec + Uint64FromBytes = types.Uint64FromBytes + Uint64ToBytes = types.Uint64ToBytes + + // variable aliases + AbsoluteMaximumBlockLock = types.AbsoluteMaximumBlockLock + AbsoluteMinimumBlockLock = types.AbsoluteMinimumBlockLock + AssetSupplyKeyPrefix = types.AssetSupplyKeyPrefix + AtomicSwapByBlockPrefix = types.AtomicSwapByBlockPrefix + AtomicSwapCoinsAccAddr = types.AtomicSwapCoinsAccAddr + AtomicSwapKeyPrefix = types.AtomicSwapKeyPrefix + AtomicSwapLongtermStoragePrefix = types.AtomicSwapLongtermStoragePrefix + DefaultMaxBlockLock = types.DefaultMaxBlockLock + DefaultMinBlockLock = types.DefaultMinBlockLock + DefaultSupportedAssets = types.DefaultSupportedAssets + KeyBnbDeputyAddress = types.KeyBnbDeputyAddress + KeyMaxBlockLock = types.KeyMaxBlockLock + KeyMinBlockLock = types.KeyMinBlockLock + KeySupportedAssets = types.KeySupportedAssets + ModuleCdc = types.ModuleCdc +) + +type ( + Keeper = keeper.Keeper + AssetParam = types.AssetParam + AssetParams = types.AssetParams + AssetSupplies = types.AssetSupplies + AssetSupply = types.AssetSupply + AtomicSwap = types.AtomicSwap + AtomicSwaps = types.AtomicSwaps + CodeType = types.CodeType + GenesisState = types.GenesisState + MsgClaimAtomicSwap = types.MsgClaimAtomicSwap + MsgCreateAtomicSwap = types.MsgCreateAtomicSwap + MsgRefundAtomicSwap = types.MsgRefundAtomicSwap + Params = types.Params + QueryAssetSupply = types.QueryAssetSupply + QueryAtomicSwapByID = types.QueryAtomicSwapByID + QueryAtomicSwaps = types.QueryAtomicSwaps + SupplyKeeper = types.SupplyKeeper + Swap = types.Swap + SwapDirection = types.SwapDirection + SwapStatus = types.SwapStatus +) diff --git a/x/bep3/client/cli/query.go b/x/bep3/client/cli/query.go new file mode 100644 index 00000000..4523970d --- /dev/null +++ b/x/bep3/client/cli/query.go @@ -0,0 +1,225 @@ +package cli + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/spf13/cobra" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/x/bep3/types" +) + +// GetQueryCmd returns the cli query commands for this module +func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + // Group bep3 queries under a subcommand + bep3QueryCmd := &cobra.Command{ + Use: "bep3", + Short: "Querying commands for the bep3 module", + } + + bep3QueryCmd.AddCommand(client.GetCommands( + QueryCalcSwapIDCmd(queryRoute, cdc), + QueryCalcRandomNumberHashCmd(queryRoute, cdc), + QueryGetAtomicSwapCmd(queryRoute, cdc), + QueryGetAssetSupplyCmd(queryRoute, cdc), + QueryGetAtomicSwapsCmd(queryRoute, cdc), + QueryParamsCmd(queryRoute, cdc), + )...) + + return bep3QueryCmd +} + +// QueryCalcRandomNumberHashCmd calculates the random number hash for a number and timestamp +func QueryCalcRandomNumberHashCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "calc-rnh [unix-timestamp]", + Short: "calculates an example random number hash from an optional timestamp", + Example: "bep3 calc-rnh now", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + userTimestamp := "now" + if len(args) > 0 { + userTimestamp = args[0] + } + + // Timestamp defaults to time.Now() unless it's explicitly set + var timestamp int64 + if strings.Compare(userTimestamp, "now") == 0 { + timestamp = tmtime.Now().Unix() + } else { + userTimestamp, err := strconv.ParseInt(userTimestamp, 10, 64) + if err != nil { + return err + } + timestamp = userTimestamp + } + + // Load hex-encoded cryptographically strong pseudo-random number + randomNumber, err := types.GenerateSecureRandomNumber() + if err != nil { + return err + } + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + // Prepare random number, timestamp, and hash for output + randomNumberStr := fmt.Sprintf("Random number: %s\n", randomNumber) + timestampStr := fmt.Sprintf("Timestamp: %d\n", timestamp) + randomNumberHashStr := fmt.Sprintf("Random number hash: %s", hex.EncodeToString(randomNumberHash)) + output := []string{randomNumberStr, timestampStr, randomNumberHashStr} + return cliCtx.PrintOutput(strings.Join(output, "")) + }, + } +} + +// QueryCalcSwapIDCmd calculates the swapID for a random number hash, sender, and sender other chain +func QueryCalcSwapIDCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "calc-swapid [random-number-hash] [sender] [sender-other-chain]", + Short: "calculate swap ID for the given random number hash, sender, and sender other chain", + Example: "bep3 calc-swapid 0677bd8a303dd981810f34d8e5cc6507f13b391899b84d3c1be6c6045a17d747 kava15qdefkmwswysgg4qxgcqpqr35k3m49pkx2jdfnw bnb1ud3q90r98l3mhd87kswv3h8cgrymzeljct8qn7", + Args: cobra.MinimumNArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Parse query params + randomNumberHash, err := types.HexToBytes(args[0]) + if err != nil { + return err + } + sender := sdk.AccAddress(args[1]) + senderOtherChain := args[2] + + // Calculate swap ID and convert to human-readable string + swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain) + return cliCtx.PrintOutput(hex.EncodeToString(swapID)) + }, + } +} + +// QueryGetAssetSupplyCmd queries as asset's current in swap supply, active, supply, and supply limit +func QueryGetAssetSupplyCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "supply [denom]", + Short: "get information about an asset's supply", + Example: "bep3 supply bnb", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare query params + bz, err := cdc.MarshalJSON(types.NewQueryAssetSupply([]byte(args[0]))) + if err != nil { + return err + } + + // Execute query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAssetSupply), bz) + if err != nil { + return err + } + + // Decode and print results + var assetSupply types.AssetSupply + cdc.MustUnmarshalJSON(res, &assetSupply) + return cliCtx.PrintOutput(assetSupply) + }, + } +} + +// QueryGetAtomicSwapCmd queries an AtomicSwap by swapID +func QueryGetAtomicSwapCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "swap [swap-id]", + Short: "get atomic swap information", + Example: "bep3 swap 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Decode swapID's hex encoded string to []byte + swapID, err := types.HexToBytes(args[0]) + if err != nil { + return err + } + + // Prepare query params + bz, err := cdc.MarshalJSON(types.NewQueryAtomicSwapByID(swapID)) + if err != nil { + return err + } + + // Execute query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAtomicSwap), bz) + if err != nil { + return err + } + + var atomicSwap types.AtomicSwap + cdc.MustUnmarshalJSON(res, &atomicSwap) + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(atomicSwap.String()) + }, + } +} + +// QueryGetAtomicSwapsCmd queries AtomicSwaps in the store +func QueryGetAtomicSwapsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "swaps", + Short: "get a list of active atomic swaps", + Example: "bep3 swaps", + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAtomicSwaps), nil) + if err != nil { + return err + } + + var atomicSwaps types.AtomicSwaps + cdc.MustUnmarshalJSON(res, &atomicSwaps) + + if len(atomicSwaps) == 0 { + return fmt.Errorf("There are currently no atomic swaps") + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(atomicSwaps.String()) + }, + } +} + +// QueryParamsCmd queries the bep3 module parameters +func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "params", + Short: "get the bep3 module parameters", + Example: "bep3 params", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Query + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams) + res, _, err := cliCtx.QueryWithData(route, nil) + if err != nil { + return err + } + + // Decode and print results + var out types.Params + cdc.MustUnmarshalJSON(res, &out) + return cliCtx.PrintOutput(out) + }, + } +} diff --git a/x/bep3/client/cli/tx.go b/x/bep3/client/cli/tx.go new file mode 100644 index 00000000..957b0e8a --- /dev/null +++ b/x/bep3/client/cli/tx.go @@ -0,0 +1,181 @@ +package cli + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/spf13/cobra" + tmtime "github.com/tendermint/tendermint/types/time" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + bep3TxCmd := &cobra.Command{ + Use: "bep3", + Short: "bep3 transactions subcommands", + } + + bep3TxCmd.AddCommand(client.PostCommands( + GetCmdCreateAtomicSwap(cdc), + GetCmdClaimAtomicSwap(cdc), + GetCmdRefundAtomicSwap(cdc), + )...) + + return bep3TxCmd +} + +// GetCmdCreateAtomicSwap cli command for creating atomic swaps +func GetCmdCreateAtomicSwap(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "create [to] [recipient-other-chain] [sender-other-chain] [timestamp] [coins] [expected-income] [height-span] [cross-chain]", + Short: "create a new atomic swap", + Example: fmt.Sprintf("%s tx %s create kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7 bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7 now 100bnb 100bnb 360 true --from validator", + version.ClientName, types.ModuleName), + Args: cobra.ExactArgs(8), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + + from := cliCtx.GetFromAddress() // same as KavaExecutor.DeputyAddress (for cross-chain) + to, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + recipientOtherChain := args[1] // same as OtherExecutor.DeputyAddress + senderOtherChain := args[2] + + // Timestamp defaults to time.Now() unless it's explicitly set + var timestamp int64 + if strings.Compare(args[3], "now") == 0 { + timestamp = tmtime.Now().Unix() + } else { + timestamp, err = strconv.ParseInt(args[3], 10, 64) + if err != nil { + return err + } + } + + // Generate cryptographically strong pseudo-random number + randomNumber, err := types.GenerateSecureRandomNumber() + if err != nil { + return err + } + + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + // Print random number, timestamp, and hash to user's console + fmt.Printf("\nRandom number: %s\n", hex.EncodeToString(randomNumber.Bytes())) + fmt.Printf("Timestamp: %d\n", timestamp) + fmt.Printf("Random number hash: %s\n\n", hex.EncodeToString(randomNumberHash)) + + coins, err := sdk.ParseCoins(args[4]) + if err != nil { + return err + } + + expectedIncome := args[5] + + heightSpan, err := strconv.ParseInt(args[6], 10, 64) + if err != nil { + return err + } + + crossChain, err := strconv.ParseBool(args[7]) + if err != nil { + return err + } + + msg := types.NewMsgCreateAtomicSwap( + from, to, recipientOtherChain, senderOtherChain, randomNumberHash, + timestamp, coins, expectedIncome, heightSpan, crossChain, + ) + + err = msg.ValidateBasic() + if err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +// GetCmdClaimAtomicSwap cli command for claiming an atomic swap +func GetCmdClaimAtomicSwap(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "claim [swap-id] [random-number]", + Short: "claim coins in an atomic swap using the secret number", + Example: fmt.Sprintf("%s tx %s claim 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af 56f13e6a5cd397447f8b5f8c82fdb5bbf56127db75269f5cc14e50acd8ac9a4c --from accA", version.ClientName, types.ModuleName), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + + from := cliCtx.GetFromAddress() + + swapID, err := types.HexToBytes(args[0]) + if err != nil { + return err + } + + if len(strings.TrimSpace(args[1])) == 0 { + return fmt.Errorf("random-number cannot be empty") + } + + randomNumber, err := types.HexToBytes(args[1]) + if err != nil { + return err + } + + msg := types.NewMsgClaimAtomicSwap(from, swapID, randomNumber) + + err = msg.ValidateBasic() + if err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +// GetCmdRefundAtomicSwap cli command for claiming an atomic swap +func GetCmdRefundAtomicSwap(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "refund [swap-id]", + Short: "refund the coins in an atomic swap", + Example: fmt.Sprintf("%s tx %s refund 6682c03cc3856879c8fb98c9733c6b0c30758299138166b6523fe94628b1d3af --from accA", version.ClientName, types.ModuleName), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + + from := cliCtx.GetFromAddress() + + swapID, err := types.HexToBytes(args[0]) + if err != nil { + return err + } + + msg := types.NewMsgRefundAtomicSwap(from, swapID) + + err = msg.ValidateBasic() + if err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} diff --git a/x/bep3/client/rest/query.go b/x/bep3/client/rest/query.go new file mode 100644 index 00000000..7170cf03 --- /dev/null +++ b/x/bep3/client/rest/query.go @@ -0,0 +1,164 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/kava-labs/kava/x/bep3/types" +) + +const restSwapID = "swap-id" +const restDenom = "denom" + +func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/swap/{%s}", types.ModuleName, restSwapID), queryAtomicSwapHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/swaps", types.ModuleName), queryAtomicSwapsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/supply/{%s}", types.ModuleName, restDenom), queryAssetSupplyHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET") + +} + +func queryAtomicSwapHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[restSwapID]) == 0 { + err := fmt.Errorf("%s required but not specified", restSwapID) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + swapID, err := types.HexToBytes(vars[restSwapID]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.QueryAtomicSwapByID{SwapID: swapID}) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/%s/%s", types.ModuleName, types.QueryGetAtomicSwap), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Decode and return results + cliCtx = cliCtx.WithHeight(height) + + var swap types.AtomicSwap + err = cliCtx.Codec.UnmarshalJSON(res, &swap) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(swap)) + } +} + +func queryAtomicSwapsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetAtomicSwaps) + + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Decode and return results + cliCtx = cliCtx.WithHeight(height) + + var swaps types.AtomicSwaps + err = cliCtx.Codec.UnmarshalJSON(res, &swaps) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + + // using empty slice so json returns [] instead of null when there's no swaps + sliceSwaps := types.AtomicSwaps{} + for _, s := range swaps { + sliceSwaps = append(sliceSwaps, s) + } + rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(sliceSwaps)) + } +} + +func queryAssetSupplyHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + denom := []byte(vars[restDenom]) + params := types.NewQueryAssetSupply(denom) + + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/%s/%s", types.ModuleName, types.QueryGetAssetSupply), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Decode and return results + cliCtx = cliCtx.WithHeight(height) + + var assetSupply types.AssetSupply + err = cliCtx.Codec.UnmarshalJSON(res, &assetSupply) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(assetSupply)) + } +} + +func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryGetParams) + + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/bep3/client/rest/rest.go b/x/bep3/client/rest/rest.go new file mode 100644 index 00000000..02777077 --- /dev/null +++ b/x/bep3/client/rest/rest.go @@ -0,0 +1,46 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// RegisterRoutes registers bep3-related REST handlers to a router +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + registerQueryRoutes(cliCtx, r) + registerTxRoutes(cliCtx, r) +} + +// PostCreateSwapReq defines the properties of a swap create request's body +type PostCreateSwapReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + From sdk.AccAddress `json:"from" yaml:"from"` + To sdk.AccAddress `json:"to" yaml:"to"` + RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"` + SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"` + RandomNumberHash cmn.HexBytes `json:"random_number_hash" yaml:"random_number_hash"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + Amount sdk.Coins `json:"amount" yaml:"amount"` + ExpectedIncome string `json:"expected_income" yaml:"expected_income"` + HeightSpan int64 `json:"height_span" yaml:"height_span"` + CrossChain bool `json:"cross_chain" yaml:"cross_chain"` +} + +// PostClaimSwapReq defines the properties of a swap claim request's body +type PostClaimSwapReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + From sdk.AccAddress `json:"from" yaml:"from"` + SwapID cmn.HexBytes `json:"swap_id" yaml:"swap_id"` + RandomNumber cmn.HexBytes `json:"random_number" yaml:"random_number"` +} + +// PostRefundSwapReq defines the properties of swap refund request's body +type PostRefundSwapReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + From sdk.AccAddress `json:"from" yaml:"from"` + SwapID cmn.HexBytes `json:"swap_id" yaml:"swap_id"` +} diff --git a/x/bep3/client/rest/tx.go b/x/bep3/client/rest/tx.go new file mode 100644 index 00000000..022993e3 --- /dev/null +++ b/x/bep3/client/rest/tx.go @@ -0,0 +1,110 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/gorilla/mux" + + "github.com/kava-labs/kava/x/bep3/types" +) + +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/swap/create", types.ModuleName), postCreateHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc(fmt.Sprintf("/%s/swap/claim", types.ModuleName), postClaimHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc(fmt.Sprintf("/%s/swap/refund", types.ModuleName), postRefundHandlerFn(cliCtx)).Methods("POST") +} + +func postCreateHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var req PostCreateSwapReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // Create and return msg + msg := types.NewMsgCreateAtomicSwap( + req.From, + req.To, + req.RecipientOtherChain, + req.SenderOtherChain, + req.RandomNumberHash, + req.Timestamp, + req.Amount, + req.ExpectedIncome, + req.HeightSpan, + req.CrossChain, + ) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var req PostClaimSwapReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // Create and return msg + msg := types.NewMsgClaimAtomicSwap( + req.From, + req.SwapID, + req.RandomNumber, + ) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func postRefundHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var req PostRefundSwapReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + senderAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Create and return msg + msg := types.NewMsgRefundAtomicSwap( + senderAddr, + req.SwapID, + ) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/bep3/genesis.go b/x/bep3/genesis.go new file mode 100644 index 00000000..37f08878 --- /dev/null +++ b/x/bep3/genesis.go @@ -0,0 +1,122 @@ +package bep3 + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// InitGenesis initializes the store state from a genesis state. +func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper SupplyKeeper, gs GenesisState) { + if err := gs.Validate(); err != nil { + panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) + } + + keeper.SetParams(ctx, gs.Params) + + // Initialize supported assets + for _, asset := range gs.Params.SupportedAssets { + zeroCoin := sdk.NewCoin(asset.Denom, sdk.NewInt(0)) + supply := NewAssetSupply(asset.Denom, zeroCoin, zeroCoin, zeroCoin, sdk.NewCoin(asset.Denom, asset.Limit)) + keeper.SetAssetSupply(ctx, supply, []byte(asset.Denom)) + } + + // Increment an asset's incoming, current, and outgoing supply + // It it required that assets are supported but they do not have to be active + for _, supply := range gs.AssetSupplies { + // Asset must be supported but does not have to be active + coin, found := keeper.GetAssetByDenom(ctx, supply.Denom) + if !found { + panic(fmt.Sprintf("invalid asset supply: %s is not a supported asset", coin.Denom)) + } + if !coin.Limit.Equal(supply.Limit.Amount) { + panic(fmt.Sprintf("supported asset limit %s does not equal asset supply %s", coin.Limit, supply.Limit.Amount)) + } + + // Increment current, incoming, and outgoing asset supplies + err := keeper.IncrementCurrentAssetSupply(ctx, supply.CurrentSupply) + if err != nil { + panic(err) + } + err = keeper.IncrementIncomingAssetSupply(ctx, supply.IncomingSupply) + if err != nil { + panic(err) + } + err = keeper.IncrementOutgoingAssetSupply(ctx, supply.OutgoingSupply) + if err != nil { + panic(err) + } + } + + var incomingSupplies sdk.Coins + var outgoingSupplies sdk.Coins + for _, swap := range gs.AtomicSwaps { + if swap.Validate() != nil { + panic(fmt.Sprintf("invalid swap %s", swap.GetSwapID())) + } + + // Atomic swap assets must be both supported and active + err := keeper.ValidateLiveAsset(ctx, swap.Amount[0]) + if err != nil { + panic(err) + } + + keeper.SetAtomicSwap(ctx, swap) + + // Add swap to block index or longterm storage based on swap.Status + // Increment incoming or outgoing supply based on swap.Direction + switch swap.Direction { + case Incoming: + switch swap.Status { + case Open: + // This index expires unclaimed swaps + keeper.InsertIntoByBlockIndex(ctx, swap) + incomingSupplies = incomingSupplies.Add(swap.Amount) + case Expired: + incomingSupplies = incomingSupplies.Add(swap.Amount) + case Completed: + // This index stores swaps until deletion + keeper.InsertIntoLongtermStorage(ctx, swap) + default: + panic(fmt.Sprintf("swap %s has invalid status %s", swap.GetSwapID(), swap.Status.String())) + } + case Outgoing: + switch swap.Status { + case Open: + keeper.InsertIntoByBlockIndex(ctx, swap) + outgoingSupplies = outgoingSupplies.Add(swap.Amount) + case Expired: + outgoingSupplies = outgoingSupplies.Add(swap.Amount) + case Completed: + keeper.InsertIntoLongtermStorage(ctx, swap) + default: + panic(fmt.Sprintf("swap %s has invalid status %s", swap.GetSwapID(), swap.Status.String())) + } + default: + panic(fmt.Sprintf("swap %s has invalid direction %s", swap.GetSwapID(), swap.Direction.String())) + } + } + + // Asset's given incoming/outgoing supply much match the amount of coins in incoming/outgoing atomic swaps + supplies := keeper.GetAllAssetSupplies(ctx) + for _, supply := range supplies { + incomingSupply := incomingSupplies.AmountOf(supply.Denom) + if !supply.IncomingSupply.Amount.Equal(incomingSupply) { + panic(fmt.Sprintf("asset's incoming supply %s does not match amount %s in incoming atomic swaps", + supply.IncomingSupply, incomingSupply)) + } + outgoingSupply := outgoingSupplies.AmountOf(supply.Denom) + if !supply.OutgoingSupply.Amount.Equal(outgoingSupply) { + panic(fmt.Sprintf("asset's outgoing supply %s does not match amount %s in outgoing atomic swaps", + supply.OutgoingSupply, outgoingSupply)) + } + } +} + +// ExportGenesis writes the current store values to a genesis file, which can be imported again with InitGenesis +func ExportGenesis(ctx sdk.Context, k Keeper) (data GenesisState) { + params := k.GetParams(ctx) + swaps := k.GetAllAtomicSwaps(ctx) + assets := k.GetAllAssetSupplies(ctx) + return NewGenesisState(params, swaps, assets) +} diff --git a/x/bep3/genesis_test.go b/x/bep3/genesis_test.go new file mode 100644 index 00000000..7b76af77 --- /dev/null +++ b/x/bep3/genesis_test.go @@ -0,0 +1,298 @@ +package bep3_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3" +) + +type GenesisTestSuite struct { + suite.Suite + + app app.TestApp + ctx sdk.Context + keeper bep3.Keeper + addrs []sdk.AccAddress +} + +func (suite *GenesisTestSuite) SetupTest() { + tApp := app.NewTestApp() + suite.ctx = tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + suite.keeper = tApp.GetBep3Keeper() + suite.app = tApp + + _, addrs := app.GeneratePrivKeyAddressPairs(3) + suite.addrs = addrs +} + +func (suite *GenesisTestSuite) TestGenesisState() { + + type GenState func() app.GenesisState + + testCases := []struct { + name string + genState GenState + expectPass bool + }{ + { + name: "default", + genState: func() app.GenesisState { + return NewBep3GenStateMulti(suite.addrs[0]) + }, + expectPass: true, + }, + { + name: "import atomic swaps and asset supplies", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + _, addrs := app.GeneratePrivKeyAddressPairs(3) + var swaps bep3.AtomicSwaps + var supplies bep3.AssetSupplies + for i := 0; i < 3; i++ { + swap, supply := loadSwapAndSupply(addrs[i], i) + swaps = append(swaps, swap) + supplies = append(supplies, supply) + } + gs.AtomicSwaps = swaps + gs.AssetSupplies = supplies + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: true, + }, + { + name: "incoming supply doesn't match amount in incoming atomic swaps", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + _, addrs := app.GeneratePrivKeyAddressPairs(1) + swap, _ := loadSwapAndSupply(addrs[0], 2) + gs.AtomicSwaps = bep3.AtomicSwaps{swap} + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "current supply above limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + assetParam, _ := suite.keeper.GetAssetByDenom(suite.ctx, "bnb") + gs.AssetSupplies = bep3.AssetSupplies{ + bep3.AssetSupply{ + Denom: "bnb", + IncomingSupply: c("bnb", 0), + OutgoingSupply: c("bnb", 0), + CurrentSupply: c("bnb", assetParam.Limit.Add(i(1)).Int64()), + Limit: c("bnb", assetParam.Limit.Int64()), + }, + } + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "incoming supply above limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + // Set up overlimit amount + assetParam, _ := suite.keeper.GetAssetByDenom(suite.ctx, "bnb") + overLimitAmount := assetParam.Limit.Add(i(1)) + + // Set up an atomic swap with amount equal to the currently asset supply + _, addrs := app.GeneratePrivKeyAddressPairs(2) + timestamp := ts(0) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + swap := bep3.NewAtomicSwap(cs(c("bnb", overLimitAmount.Int64())), randomNumberHash, + int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain, + TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming) + gs.AtomicSwaps = bep3.AtomicSwaps{swap} + + // Set up asset supply with overlimit current supply + gs.AssetSupplies = bep3.AssetSupplies{ + bep3.AssetSupply{ + Denom: "bnb", + IncomingSupply: c("bnb", assetParam.Limit.Add(i(1)).Int64()), + OutgoingSupply: c("bnb", 0), + CurrentSupply: c("bnb", 0), + Limit: c("bnb", assetParam.Limit.Int64()), + }, + } + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "incoming supply + current supply above limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + // Set up overlimit amount + assetParam, _ := suite.keeper.GetAssetByDenom(suite.ctx, "bnb") + halfLimit := assetParam.Limit.Int64() / 2 + overHalfLimit := halfLimit + 1 + + // Set up an atomic swap with amount equal to the currently asset supply + _, addrs := app.GeneratePrivKeyAddressPairs(2) + timestamp := ts(0) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + swap := bep3.NewAtomicSwap(cs(c("bnb", halfLimit)), randomNumberHash, + int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain, + TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming) + gs.AtomicSwaps = bep3.AtomicSwaps{swap} + + // Set up asset supply with overlimit current supply + gs.AssetSupplies = bep3.AssetSupplies{ + bep3.AssetSupply{ + Denom: "bnb", + IncomingSupply: c("bnb", halfLimit), + OutgoingSupply: c("bnb", 0), + CurrentSupply: c("bnb", overHalfLimit), + Limit: c("bnb", assetParam.Limit.Int64()), + }, + } + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "asset supply denom is not a supported asset", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.AssetSupplies = bep3.AssetSupplies{ + bep3.AssetSupply{ + Denom: "fake", + IncomingSupply: c("fake", 0), + OutgoingSupply: c("fake", 0), + CurrentSupply: c("fake", 0), + Limit: c("fake", StandardSupplyLimit.Int64()), + }, + } + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "atomic swap asset type is unsupported", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + _, addrs := app.GeneratePrivKeyAddressPairs(2) + timestamp := ts(0) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + swap := bep3.NewAtomicSwap(cs(c("fake", 500000)), randomNumberHash, + int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain, + TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming) + + gs.AtomicSwaps = bep3.AtomicSwaps{swap} + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "atomic swap status is invalid", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + _, addrs := app.GeneratePrivKeyAddressPairs(2) + timestamp := ts(0) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + swap := bep3.NewAtomicSwap(cs(c("bnb", 5000)), randomNumberHash, + int64(360), timestamp, suite.addrs[0], addrs[1], TestSenderOtherChain, + TestRecipientOtherChain, 0, bep3.NULL, true, bep3.Incoming) + + gs.AtomicSwaps = bep3.AtomicSwaps{swap} + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "minimum block lock below limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.MinBlockLock = 1 + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "minimum block lock above limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.MinBlockLock = 500000 + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "maximum block lock below limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.MaxBlockLock = 1 + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "maximum block lock above limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.MaxBlockLock = 100000000 + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "empty supported asset denom", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.SupportedAssets[0].Denom = "" + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "negative supported asset limit", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.SupportedAssets[0].Limit = i(-100) + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + { + name: "duplicate supported asset denom", + genState: func() app.GenesisState { + gs := baseGenState(suite.addrs[0]) + gs.Params.SupportedAssets[1].Denom = "bnb" + return app.GenesisState{"bep3": bep3.ModuleCdc.MustMarshalJSON(gs)} + }, + expectPass: false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + if tc.expectPass { + suite.Run(tc.name, func() { + suite.NotPanics(func() { + suite.app.InitializeFromGenesisStates(tc.genState()) + }) + }) + } else { + suite.Run(tc.name, func() { + suite.Panics(func() { + suite.app.InitializeFromGenesisStates(tc.genState()) + }) + }) + } + } +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/bep3/handler.go b/x/bep3/handler.go new file mode 100644 index 00000000..27accffe --- /dev/null +++ b/x/bep3/handler.go @@ -0,0 +1,89 @@ +package bep3 + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewHandler creates an sdk.Handler for all the bep3 type messages +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + switch msg := msg.(type) { + case MsgCreateAtomicSwap: + return handleMsgCreateAtomicSwap(ctx, k, msg) + case MsgClaimAtomicSwap: + return handleMsgClaimAtomicSwap(ctx, k, msg) + case MsgRefundAtomicSwap: + return handleMsgRefundAtomicSwap(ctx, k, msg) + default: + errMsg := fmt.Sprintf("unrecognized %s message type: %T", ModuleName, msg) + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +// handleMsgCreateAtomicSwap handles requests to create a new AtomicSwap +func handleMsgCreateAtomicSwap(ctx sdk.Context, k Keeper, msg MsgCreateAtomicSwap) sdk.Result { + err := k.CreateAtomicSwap(ctx, msg.RandomNumberHash, msg.Timestamp, msg.HeightSpan, msg.From, msg.To, + msg.SenderOtherChain, msg.RecipientOtherChain, msg.Amount, msg.ExpectedIncome, msg.CrossChain) + + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return sdk.Result{ + Events: ctx.EventManager().Events(), + } +} + +// handleMsgClaimAtomicSwap handles requests to claim funds in an active AtomicSwap +func handleMsgClaimAtomicSwap(ctx sdk.Context, k Keeper, msg MsgClaimAtomicSwap) sdk.Result { + + err := k.ClaimAtomicSwap(ctx, msg.From, msg.SwapID, msg.RandomNumber) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return sdk.Result{ + Events: ctx.EventManager().Events(), + } +} + +// handleMsgRefundAtomicSwap handles requests to refund an active AtomicSwap +func handleMsgRefundAtomicSwap(ctx sdk.Context, k Keeper, msg MsgRefundAtomicSwap) sdk.Result { + + err := k.RefundAtomicSwap(ctx, msg.From, msg.SwapID) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return sdk.Result{ + Events: ctx.EventManager().Events(), + } +} diff --git a/x/bep3/handler_test.go b/x/bep3/handler_test.go new file mode 100644 index 00000000..b905b23d --- /dev/null +++ b/x/bep3/handler_test.go @@ -0,0 +1,132 @@ +package bep3_test + +import ( + "encoding/hex" + "fmt" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3" +) + +type HandlerTestSuite struct { + suite.Suite + + ctx sdk.Context + app app.TestApp + handler sdk.Handler + keeper bep3.Keeper + addrs []sdk.AccAddress +} + +func (suite *HandlerTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + keeper := tApp.GetBep3Keeper() + + // Set up genesis state and initialize + _, addrs := app.GeneratePrivKeyAddressPairs(3) + coins := []sdk.Coins{} + for j := 0; j < 3; j++ { + coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000))) + } + authGS := app.NewAuthGenState(addrs, coins) + tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(addrs[0])) + + suite.addrs = addrs + suite.handler = bep3.NewHandler(keeper) + suite.keeper = keeper + suite.app = tApp + suite.ctx = ctx +} + +func (suite *HandlerTestSuite) AddAtomicSwap() (cmn.HexBytes, cmn.HexBytes) { + expireHeight := int64(360) + amount := cs(c("bnb", int64(50000))) + timestamp := ts(0) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + // Create atomic swap and check err to confirm creation + err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight, + suite.addrs[0], suite.addrs[1], TestSenderOtherChain, TestRecipientOtherChain, + amount, amount.String(), true) + suite.Nil(err) + + swapID := bep3.CalculateSwapID(randomNumberHash, suite.addrs[0], TestSenderOtherChain) + return swapID, randomNumber.Bytes() +} + +func (suite *HandlerTestSuite) TestMsgCreateAtomicSwap() { + amount := cs(c("bnb", int64(10000))) + timestamp := ts(0) + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + msg := bep3.NewMsgCreateAtomicSwap( + suite.addrs[0], suite.addrs[2], TestRecipientOtherChain, TestSenderOtherChain, + randomNumberHash, timestamp, amount, amount.String(), int64(300), true) + + res := suite.handler(suite.ctx, msg) + suite.True(res.IsOK()) +} + +func (suite *HandlerTestSuite) TestMsgClaimAtomicSwap() { + // Attempt claim msg on fake atomic swap + badRandomNumber, _ := bep3.GenerateSecureRandomNumber() + badRandomNumberHash := bep3.CalculateRandomHash(badRandomNumber.Bytes(), ts(0)) + badSwapID := bep3.CalculateSwapID(badRandomNumberHash, suite.addrs[0], TestSenderOtherChain) + badMsg := bep3.NewMsgClaimAtomicSwap(suite.addrs[0], badSwapID, badRandomNumber.Bytes()) + badRes := suite.handler(suite.ctx, badMsg) + suite.False(badRes.IsOK()) + suite.True(strings.Contains(badRes.Log, fmt.Sprintf("AtomicSwap %s was not found", hex.EncodeToString(badSwapID)))) + + // Add an atomic swap before attempting new claim msg + swapID, randomNumber := suite.AddAtomicSwap() + msg := bep3.NewMsgClaimAtomicSwap(suite.addrs[0], swapID, randomNumber) + res := suite.handler(suite.ctx, msg) + suite.True(res.IsOK()) +} + +func (suite *HandlerTestSuite) TestMsgRefundAtomicSwap() { + // Attempt refund msg on fake atomic swap + badRandomNumber, _ := bep3.GenerateSecureRandomNumber() + badRandomNumberHash := bep3.CalculateRandomHash(badRandomNumber.Bytes(), ts(0)) + badSwapID := bep3.CalculateSwapID(badRandomNumberHash, suite.addrs[0], TestSenderOtherChain) + badMsg := bep3.NewMsgRefundAtomicSwap(suite.addrs[0], badSwapID) + badRes := suite.handler(suite.ctx, badMsg) + suite.False(badRes.IsOK()) + suite.True(strings.Contains(badRes.Log, fmt.Sprintf("AtomicSwap %s was not found", hex.EncodeToString(badSwapID)))) + + // Add an atomic swap and build refund msg + swapID, _ := suite.AddAtomicSwap() + msg := bep3.NewMsgRefundAtomicSwap(suite.addrs[0], swapID) + + // Attempt to refund active atomic swap + res1 := suite.handler(suite.ctx, msg) + suite.True(strings.Contains(res1.Log, "atomic swap is still active and cannot be refunded")) + suite.False(res1.IsOK()) + + // Expire the atomic swap with begin blocker and attempt refund + laterCtx := suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400) + bep3.BeginBlocker(laterCtx, suite.keeper) + res2 := suite.handler(laterCtx, msg) + suite.True(res2.IsOK()) +} + +func (suite *HandlerTestSuite) TestInvalidMsg() { + res := suite.handler(suite.ctx, sdk.NewTestMsg()) + suite.False(res.IsOK()) + suite.True(strings.Contains(res.Log, "unrecognized bep3 message type")) +} + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} diff --git a/x/bep3/integration_test.go b/x/bep3/integration_test.go new file mode 100644 index 00000000..bb9bc7c2 --- /dev/null +++ b/x/bep3/integration_test.go @@ -0,0 +1,87 @@ +package bep3_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3" +) + +const ( + TestSenderOtherChain = "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7" + TestRecipientOtherChain = "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7" + TestDeputy = "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj" + TestUser = "kava1vry5lhegzlulehuutcr7nmdlmktw88awp0a39p" +) + +var ( + StandardSupplyLimit = i(100000000000) + DenomMap = map[int]string{0: "btc", 1: "eth", 2: "bnb", 3: "xrp", 4: "dai"} +) + +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func d(de int64) sdk.Dec { return sdk.NewDec(de) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } +func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() } + +func NewBep3GenStateMulti(deputy sdk.AccAddress) app.GenesisState { + bep3Genesis := baseGenState(deputy) + return app.GenesisState{bep3.ModuleName: bep3.ModuleCdc.MustMarshalJSON(bep3Genesis)} +} + +func baseGenState(deputy sdk.AccAddress) bep3.GenesisState { + bep3Genesis := bep3.GenesisState{ + Params: bep3.Params{ + BnbDeputyAddress: deputy, + MinBlockLock: bep3.DefaultMinBlockLock, // 80 + MaxBlockLock: bep3.DefaultMaxBlockLock, // 360 + SupportedAssets: bep3.AssetParams{ + bep3.AssetParam{ + Denom: "btc", + CoinID: 714, + Limit: StandardSupplyLimit, + Active: true, + }, + bep3.AssetParam{ + Denom: "eth", + CoinID: 999999, + Limit: StandardSupplyLimit, + Active: true, + }, + bep3.AssetParam{ + Denom: "bnb", + CoinID: 99999, + Limit: StandardSupplyLimit, + Active: true, + }, + bep3.AssetParam{ + Denom: "inc", + CoinID: 9999, + Limit: i(100), + Active: false, + }, + }, + }, + } + return bep3Genesis +} + +func loadSwapAndSupply(addr sdk.AccAddress, index int) (bep3.AtomicSwap, bep3.AssetSupply) { + coin := c(DenomMap[index], 50000) + expireOffset := int64((index * 15) + 360) // Default expire height + offet to match timestamp + timestamp := ts(index) // One minute apart + randomNumber, _ := bep3.GenerateSecureRandomNumber() + randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) + swap := bep3.NewAtomicSwap(cs(coin), randomNumberHash, + expireOffset, timestamp, addr, addr, TestSenderOtherChain, + TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming) + + supply := bep3.NewAssetSupply(coin.Denom, coin, c(coin.Denom, 0), + c(coin.Denom, 0), c(coin.Denom, StandardSupplyLimit.Int64())) + + return swap, supply +} diff --git a/x/bep3/keeper/asset.go b/x/bep3/keeper/asset.go new file mode 100644 index 00000000..df586cee --- /dev/null +++ b/x/bep3/keeper/asset.go @@ -0,0 +1,113 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/bep3/types" +) + +// IncrementCurrentAssetSupply increments an asset's supply by the coin +func (k Keeper) IncrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) sdk.Error { + supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom)) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + + // Resulting current supply must be under asset's limit + if !supply.Limit.IsGTE(supply.CurrentSupply.Add(coin)) { + return types.ErrExceedsSupplyLimit(k.codespace, coin, supply.CurrentSupply, supply.Limit) + } + + supply.CurrentSupply = supply.CurrentSupply.Add(coin) + k.SetAssetSupply(ctx, supply, []byte(coin.Denom)) + return nil +} + +// DecrementCurrentAssetSupply decrement an asset's supply by the coin +func (k Keeper) DecrementCurrentAssetSupply(ctx sdk.Context, coin sdk.Coin) sdk.Error { + supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom)) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + + // Resulting current supply must be greater than or equal to 0 + // Use sdk.Int instead of sdk.Coin to prevent panic if true + if supply.CurrentSupply.Amount.Sub(coin.Amount).IsNegative() { + return types.ErrInvalidCurrentSupply(k.codespace, coin, supply.CurrentSupply) + } + + supply.CurrentSupply = supply.CurrentSupply.Sub(coin) + k.SetAssetSupply(ctx, supply, []byte(coin.Denom)) + return nil +} + +// IncrementIncomingAssetSupply increments an asset's incoming supply +func (k Keeper) IncrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) sdk.Error { + supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom)) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + + // Result of (current + incoming + amount) must be under asset's limit + totalSupply := supply.CurrentSupply.Add(supply.IncomingSupply) + if !supply.Limit.IsGTE(totalSupply.Add(coin)) { + return types.ErrExceedsSupplyLimit(k.codespace, coin, totalSupply, supply.Limit) + } + + supply.IncomingSupply = supply.IncomingSupply.Add(coin) + k.SetAssetSupply(ctx, supply, []byte(coin.Denom)) + return nil +} + +// DecrementIncomingAssetSupply decrements an asset's incoming supply +func (k Keeper) DecrementIncomingAssetSupply(ctx sdk.Context, coin sdk.Coin) sdk.Error { + supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom)) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + + // Resulting incoming supply must be greater than or equal to 0 + // Use sdk.Int instead of sdk.Coin to prevent panic if true + if supply.IncomingSupply.Amount.Sub(coin.Amount).IsNegative() { + return types.ErrInvalidIncomingSupply(k.codespace, coin, supply.IncomingSupply) + } + + supply.IncomingSupply = supply.IncomingSupply.Sub(coin) + k.SetAssetSupply(ctx, supply, []byte(coin.Denom)) + return nil +} + +// IncrementOutgoingAssetSupply increments an asset's outoing supply +func (k Keeper) IncrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) sdk.Error { + supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom)) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + + // Result of (outgoing + amount) must be less than current supply + if !supply.CurrentSupply.IsGTE(supply.OutgoingSupply.Add(coin)) { + return types.ErrExceedsAvailableSupply(k.codespace, coin, + supply.CurrentSupply.Amount.Sub(supply.OutgoingSupply.Amount)) + } + + supply.OutgoingSupply = supply.OutgoingSupply.Add(coin) + k.SetAssetSupply(ctx, supply, []byte(coin.Denom)) + return nil +} + +// DecrementOutgoingAssetSupply decrements an asset's outoing supply +func (k Keeper) DecrementOutgoingAssetSupply(ctx sdk.Context, coin sdk.Coin) sdk.Error { + supply, found := k.GetAssetSupply(ctx, []byte(coin.Denom)) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + + // Resulting outgoing supply must be greater than or equal to 0 + // Use sdk.Int instead of sdk.Coin to prevent panic if true + if supply.OutgoingSupply.Amount.Sub(coin.Amount).IsNegative() { + return types.ErrInvalidOutgoingSupply(k.codespace, coin, supply.OutgoingSupply) + } + + supply.OutgoingSupply = supply.OutgoingSupply.Sub(coin) + k.SetAssetSupply(ctx, supply, []byte(coin.Denom)) + return nil +} diff --git a/x/bep3/keeper/asset_test.go b/x/bep3/keeper/asset_test.go new file mode 100644 index 00000000..9c167289 --- /dev/null +++ b/x/bep3/keeper/asset_test.go @@ -0,0 +1,415 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type AssetTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *AssetTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + // Initialize genesis state + deputy, _ := sdk.AccAddressFromBech32(TestDeputy) + tApp.InitializeFromGenesisStates(NewBep3GenStateMulti(deputy)) + + keeper := tApp.GetBep3Keeper() + + // Set asset supply with standard value for testing + supply := types.AssetSupply{ + Denom: "bnb", + IncomingSupply: c("bnb", 5), + OutgoingSupply: c("bnb", 5), + CurrentSupply: c("bnb", 40), + Limit: c("bnb", 50), + } + keeper.SetAssetSupply(ctx, supply, []byte(supply.Denom)) + + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + return +} + +func (suite *AssetTestSuite) TestIncrementCurrentAssetSupply() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 5), + }, + true, + }, + { + "equal limit", + args{ + coin: c("bnb", 10), + }, + true, + }, + { + "exceeds limit", + args{ + coin: c("bnb", 11), + }, + false, + }, + { + "unsupported asset", + args{ + coin: c("xyz", 5), + }, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + supplyKeyPrefix := []byte(tc.args.coin.Denom) + + preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coin) + postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + + if tc.expectPass { + suite.True(found) + suite.NoError(err) + suite.Equal(preSupply.CurrentSupply.Add(tc.args.coin), postSupply.CurrentSupply) + } else { + suite.Error(err) + suite.Equal(preSupply.CurrentSupply, postSupply.CurrentSupply) + } + }) + } +} + +func (suite *AssetTestSuite) TestDecrementCurrentAssetSupply() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 30), + }, + true, + }, + { + "equal current", + args{ + coin: c("bnb", 40), + }, + true, + }, + { + "exceeds current", + args{ + coin: c("bnb", 41), + }, + false, + }, + { + "unsupported asset", + args{ + coin: c("xyz", 30), + }, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + supplyKeyPrefix := []byte(tc.args.coin.Denom) + + preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + err := suite.keeper.DecrementCurrentAssetSupply(suite.ctx, tc.args.coin) + postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + + if tc.expectPass { + suite.True(found) + suite.NoError(err) + suite.True(preSupply.CurrentSupply.Sub(tc.args.coin).IsEqual(postSupply.CurrentSupply)) + } else { + suite.Error(err) + suite.Equal(preSupply.CurrentSupply, postSupply.CurrentSupply) + } + }) + } +} + +func (suite *AssetTestSuite) TestIncrementIncomingAssetSupply() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 2), + }, + true, + }, + { + "incoming + current = limit", + args{ + coin: c("bnb", 5), + }, + true, + }, + { + "incoming + current > limit", + args{ + coin: c("bnb", 6), + }, + false, + }, + { + "unsupported asset", + args{ + coin: c("xyz", 2), + }, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + supplyKeyPrefix := []byte(tc.args.coin.Denom) + + preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + err := suite.keeper.IncrementIncomingAssetSupply(suite.ctx, tc.args.coin) + postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + + if tc.expectPass { + suite.True(found) + suite.NoError(err) + suite.Equal(preSupply.IncomingSupply.Add(tc.args.coin), postSupply.IncomingSupply) + } else { + suite.Error(err) + suite.Equal(preSupply.IncomingSupply, postSupply.IncomingSupply) + } + }) + } +} + +func (suite *AssetTestSuite) TestDecrementIncomingAssetSupply() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 4), + }, + true, + }, + { + "equal incoming", + args{ + coin: c("bnb", 5), + }, + true, + }, + { + "exceeds incoming", + args{ + coin: c("bnb", 6), + }, + false, + }, + { + "unsupported asset", + args{ + coin: c("xyz", 4), + }, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + supplyKeyPrefix := []byte(tc.args.coin.Denom) + + preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + err := suite.keeper.DecrementIncomingAssetSupply(suite.ctx, tc.args.coin) + postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + + if tc.expectPass { + suite.True(found) + suite.NoError(err) + suite.True(preSupply.IncomingSupply.Sub(tc.args.coin).IsEqual(postSupply.IncomingSupply)) + } else { + suite.Error(err) + suite.Equal(preSupply.IncomingSupply, postSupply.IncomingSupply) + } + }) + } +} + +func (suite *AssetTestSuite) TestIncrementOutgoingAssetSupply() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 30), + }, + true, + }, + { + "outgoing + amount = current", + args{ + coin: c("bnb", 35), + }, + true, + }, + { + "outoing + amount > current", + args{ + coin: c("bnb", 36), + }, + false, + }, + { + "unsupported asset", + args{ + coin: c("xyz", 30), + }, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + supplyKeyPrefix := []byte(tc.args.coin.Denom) + + preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + err := suite.keeper.IncrementOutgoingAssetSupply(suite.ctx, tc.args.coin) + postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + + if tc.expectPass { + suite.True(found) + suite.NoError(err) + suite.Equal(preSupply.OutgoingSupply.Add(tc.args.coin), postSupply.OutgoingSupply) + } else { + suite.Error(err) + suite.Equal(preSupply.OutgoingSupply, postSupply.OutgoingSupply) + } + }) + } +} + +func (suite *AssetTestSuite) TestDecrementOutgoingAssetSupply() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 4), + }, + true, + }, + { + "equal outgoing", + args{ + coin: c("bnb", 5), + }, + true, + }, + { + "exceeds outgoing", + args{ + coin: c("bnb", 6), + }, + false, + }, + { + "unsupported asset", + args{ + coin: c("xyz", 4), + }, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + supplyKeyPrefix := []byte(tc.args.coin.Denom) + + preSupply, found := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + err := suite.keeper.DecrementOutgoingAssetSupply(suite.ctx, tc.args.coin) + postSupply, _ := suite.keeper.GetAssetSupply(suite.ctx, supplyKeyPrefix) + + if tc.expectPass { + suite.True(found) + suite.NoError(err) + suite.True(preSupply.OutgoingSupply.Sub(tc.args.coin).IsEqual(postSupply.OutgoingSupply)) + } else { + suite.Error(err) + suite.Equal(preSupply.OutgoingSupply, postSupply.OutgoingSupply) + } + }) + } +} + +func TestAssetTestSuite(t *testing.T) { + suite.Run(t, new(AssetTestSuite)) +} diff --git a/x/bep3/keeper/integration_test.go b/x/bep3/keeper/integration_test.go new file mode 100644 index 00000000..5e4d4a37 --- /dev/null +++ b/x/bep3/keeper/integration_test.go @@ -0,0 +1,94 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/tendermint/tendermint/crypto" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const ( + TestSenderOtherChain = "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7" + TestRecipientOtherChain = "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7" + TestDeputy = "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj" +) + +var ( + StandardSupplyLimit = i(100000000000) + DenomMap = map[int]string{0: "btc", 1: "eth", 2: "bnb", 3: "xrp", 4: "dai"} + TestUser1 = sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))) + TestUser2 = sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser2"))) +) + +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } +func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() } + +func NewBep3GenStateMulti(deputy sdk.AccAddress) app.GenesisState { + bep3Genesis := types.GenesisState{ + Params: bep3.Params{ + BnbDeputyAddress: deputy, + MinBlockLock: types.DefaultMinBlockLock, // 80 + MaxBlockLock: types.DefaultMaxBlockLock, // 360 + SupportedAssets: types.AssetParams{ + types.AssetParam{ + Denom: "bnb", + CoinID: 714, + Limit: StandardSupplyLimit, + Active: true, + }, + types.AssetParam{ + Denom: "inc", + CoinID: 9999, + Limit: i(100), + Active: false, + }, + }, + }, + } + return app.GenesisState{bep3.ModuleName: bep3.ModuleCdc.MustMarshalJSON(bep3Genesis)} +} + +func atomicSwaps(ctx sdk.Context, count int) types.AtomicSwaps { + var swaps types.AtomicSwaps + for i := 0; i < count; i++ { + swap := atomicSwap(ctx, i) + swaps = append(swaps, swap) + } + return swaps +} + +func atomicSwap(ctx sdk.Context, index int) types.AtomicSwap { + expireOffset := int64((index * 15) + 360) // Default expire height + offet to match timestamp + timestamp := ts(index) // One minute apart + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + return types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, + ctx.BlockHeight()+expireOffset, timestamp, TestUser1, TestUser2, + TestSenderOtherChain, TestRecipientOtherChain, 0, types.Open, true, + types.Incoming) +} + +func assetSupplies(count int) types.AssetSupplies { + if count > 5 { // Max 5 asset supplies + return types.AssetSupplies{} + } + + var supplies types.AssetSupplies + + for i := 0; i < count; i++ { + supply := assetSupply(DenomMap[i]) + supplies = append(supplies, supply) + } + return supplies +} + +func assetSupply(denom string) types.AssetSupply { + return types.NewAssetSupply(denom, c(denom, 0), c(denom, 0), c(denom, 0), c(denom, 10000)) +} diff --git a/x/bep3/keeper/keeper.go b/x/bep3/keeper/keeper.go new file mode 100644 index 00000000..0a0641af --- /dev/null +++ b/x/bep3/keeper/keeper.go @@ -0,0 +1,223 @@ +package keeper + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/tendermint/tendermint/libs/log" +) + +// Keeper of the bep3 store +type Keeper struct { + key sdk.StoreKey + cdc *codec.Codec + paramSubspace subspace.Subspace + supplyKeeper types.SupplyKeeper + codespace sdk.CodespaceType +} + +// NewKeeper creates a bep3 keeper +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, sk types.SupplyKeeper, paramstore subspace.Subspace, codespace sdk.CodespaceType) Keeper { + if addr := sk.GetModuleAddress(types.ModuleName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } + keeper := Keeper{ + key: key, + cdc: cdc, + paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), + supplyKeeper: sk, + codespace: codespace, + } + return keeper +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +// ------------------------------------------ +// Atomic Swaps +// ------------------------------------------ + +// SetAtomicSwap puts the AtomicSwap into the store, and updates any indexes. +func (k Keeper) SetAtomicSwap(ctx sdk.Context, atomicSwap types.AtomicSwap) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(atomicSwap) + store.Set(atomicSwap.GetSwapID(), bz) +} + +// GetAtomicSwap gets an AtomicSwap from the store. +func (k Keeper) GetAtomicSwap(ctx sdk.Context, swapID []byte) (types.AtomicSwap, bool) { + var atomicSwap types.AtomicSwap + + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix) + bz := store.Get(swapID) + if bz == nil { + return atomicSwap, false + } + + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &atomicSwap) + return atomicSwap, true +} + +// RemoveAtomicSwap removes an AtomicSwap from the AtomicSwapKeyPrefix. +func (k Keeper) RemoveAtomicSwap(ctx sdk.Context, swapID []byte) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix) + store.Delete(swapID) +} + +// IterateAtomicSwaps provides an iterator over all stored AtomicSwaps. +// For each AtomicSwap, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateAtomicSwaps(ctx sdk.Context, cb func(atomicSwap types.AtomicSwap) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AtomicSwapKeyPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var atomicSwap types.AtomicSwap + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &atomicSwap) + + if cb(atomicSwap) { + break + } + } +} + +// GetAllAtomicSwaps returns all AtomicSwaps from the store +func (k Keeper) GetAllAtomicSwaps(ctx sdk.Context) (atomicSwaps types.AtomicSwaps) { + k.IterateAtomicSwaps(ctx, func(atomicSwap types.AtomicSwap) bool { + atomicSwaps = append(atomicSwaps, atomicSwap) + return false + }) + return +} + +// ------------------------------------------ +// Atomic Swap Block Index +// ------------------------------------------ + +// InsertIntoByBlockIndex adds a swap ID and expiration time into the byBlock index. +func (k Keeper) InsertIntoByBlockIndex(ctx sdk.Context, atomicSwap types.AtomicSwap) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix) + store.Set(types.GetAtomicSwapByHeightKey(atomicSwap.ExpireHeight, atomicSwap.GetSwapID()), atomicSwap.GetSwapID()) +} + +// RemoveFromByBlockIndex removes an AtomicSwap from the byBlock index. +func (k Keeper) RemoveFromByBlockIndex(ctx sdk.Context, atomicSwap types.AtomicSwap) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix) + store.Delete(types.GetAtomicSwapByHeightKey(atomicSwap.ExpireHeight, atomicSwap.GetSwapID())) +} + +// IterateAtomicSwapsByBlock provides an iterator over AtomicSwaps ordered by AtomicSwap expiration block +// For each AtomicSwap cb will be called. If cb returns true the iterator will close and stop. +func (k Keeper) IterateAtomicSwapsByBlock(ctx sdk.Context, inclusiveCutoffTime uint64, cb func(swapID []byte) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapByBlockPrefix) + iterator := store.Iterator( + nil, // start at the very start of the prefix store + sdk.PrefixEndBytes(types.Uint64ToBytes(inclusiveCutoffTime)), // end of range + ) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + + id := iterator.Value() + + if cb(id) { + break + } + } +} + +// ------------------------------------------ +// Atomic Swap Longterm Storage Index +// ------------------------------------------ + +// InsertIntoLongtermStorage adds a swap ID and deletion time into the longterm storage index. +// Completed swaps are stored for 1 week. +func (k Keeper) InsertIntoLongtermStorage(ctx sdk.Context, atomicSwap types.AtomicSwap) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix) + store.Set(types.GetAtomicSwapByHeightKey(atomicSwap.ClosedBlock+types.DefaultLongtermStorageDuration, + atomicSwap.GetSwapID()), atomicSwap.GetSwapID()) +} + +// RemoveFromLongtermStorage removes a swap from the into the longterm storage index +func (k Keeper) RemoveFromLongtermStorage(ctx sdk.Context, atomicSwap types.AtomicSwap) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix) + store.Delete(types.GetAtomicSwapByHeightKey(atomicSwap.ClosedBlock+types.DefaultLongtermStorageDuration, + atomicSwap.GetSwapID())) +} + +// IterateAtomicSwapsLongtermStorage provides an iterator over AtomicSwaps ordered by deletion height. +// For each AtomicSwap cb will be called. If cb returns true the iterator will close and stop. +func (k Keeper) IterateAtomicSwapsLongtermStorage(ctx sdk.Context, inclusiveCutoffTime uint64, + cb func(swapID []byte) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AtomicSwapLongtermStoragePrefix) + iterator := store.Iterator( + nil, // start at the very start of the prefix store + sdk.PrefixEndBytes(types.Uint64ToBytes(inclusiveCutoffTime)), // end of range + ) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + + id := iterator.Value() + + if cb(id) { + break + } + } +} + +// ------------------------------------------ +// Asset Supplies +// ------------------------------------------ + +// GetAssetSupply gets an asset's current supply from the store. +func (k Keeper) GetAssetSupply(ctx sdk.Context, denom []byte) (types.AssetSupply, bool) { + var supply types.AssetSupply + + store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyKeyPrefix) + bz := store.Get(denom) + if bz == nil { + return types.AssetSupply{}, false + } + + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &supply) + return supply, true +} + +// SetAssetSupply updates an asset's current active supply +func (k Keeper) SetAssetSupply(ctx sdk.Context, supply types.AssetSupply, denom []byte) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AssetSupplyKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(supply) + store.Set(denom, bz) +} + +// IterateAssetSupplies provides an iterator over current asset supplies. +// For each asset supply, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateAssetSupplies(ctx sdk.Context, cb func(supply types.AssetSupply) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.key), types.AssetSupplyKeyPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var supply types.AssetSupply + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &supply) + + if cb(supply) { + break + } + } +} + +// GetAllAssetSupplies returns current asset supplies from the store as an array of sdk.Coin +func (k Keeper) GetAllAssetSupplies(ctx sdk.Context) (supplies types.AssetSupplies) { + k.IterateAssetSupplies(ctx, func(supply types.AssetSupply) bool { + supplies = append(supplies, supply) + return false + }) + return +} diff --git a/x/bep3/keeper/keeper_test.go b/x/bep3/keeper/keeper_test.go new file mode 100644 index 00000000..9837a747 --- /dev/null +++ b/x/bep3/keeper/keeper_test.go @@ -0,0 +1,370 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const LongtermStorageDuration = 86400 + +type KeeperTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *KeeperTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + suite.ResetChain() + return +} + +func (suite *KeeperTestSuite) ResetChain() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + keeper := tApp.GetBep3Keeper() + + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper +} + +func (suite *KeeperTestSuite) TestGetSetAtomicSwap() { + suite.ResetChain() + + // Set new atomic swap + atomicSwap := atomicSwap(suite.ctx, 1) + suite.keeper.SetAtomicSwap(suite.ctx, atomicSwap) + + // Check atomic swap in store + s, found := suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID()) + suite.True(found) + suite.Equal(atomicSwap, s) + + // Check fake atomic swap not in store + fakeSwapID := types.CalculateSwapID(atomicSwap.RandomNumberHash, TestUser2, "otheraddress") + _, found = suite.keeper.GetAtomicSwap(suite.ctx, fakeSwapID) + suite.False(found) +} + +func (suite *KeeperTestSuite) TestRemoveAtomicSwap() { + suite.ResetChain() + + // Set new atomic swap + atomicSwap := atomicSwap(suite.ctx, 1) + suite.keeper.SetAtomicSwap(suite.ctx, atomicSwap) + + // Check atomic swap in store + s, found := suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID()) + suite.True(found) + suite.Equal(atomicSwap, s) + + suite.keeper.RemoveAtomicSwap(suite.ctx, atomicSwap.GetSwapID()) + + // Check atomic swap not in store + _, found = suite.keeper.GetAtomicSwap(suite.ctx, atomicSwap.GetSwapID()) + suite.False(found) +} +func (suite *KeeperTestSuite) TestIterateAtomicSwaps() { + suite.ResetChain() + + // Set atomic swaps + atomicSwaps := atomicSwaps(suite.ctx, 4) + for _, s := range atomicSwaps { + suite.keeper.SetAtomicSwap(suite.ctx, s) + } + + // Read each atomic swap from the store + var readAtomicSwaps types.AtomicSwaps + suite.keeper.IterateAtomicSwaps(suite.ctx, func(a types.AtomicSwap) bool { + readAtomicSwaps = append(readAtomicSwaps, a) + return false + }) + + // Check expected values + suite.Equal(len(atomicSwaps), len(readAtomicSwaps)) +} + +func (suite *KeeperTestSuite) TestGetAllAtomicSwaps() { + suite.ResetChain() + + // Set atomic swaps + atomicSwaps := atomicSwaps(suite.ctx, 4) + for _, s := range atomicSwaps { + suite.keeper.SetAtomicSwap(suite.ctx, s) + } + + // Get and check atomic swaps + res := suite.keeper.GetAllAtomicSwaps(suite.ctx) + suite.Equal(4, len(res)) +} + +func (suite *KeeperTestSuite) TestInsertIntoByBlockIndex() { + suite.ResetChain() + + // Set new atomic swap in by block index + atomicSwap := atomicSwap(suite.ctx, 1) + suite.keeper.InsertIntoByBlockIndex(suite.ctx, atomicSwap) + + // Block index lacks getter methods, must use iteration to get count of swaps in store + var swapIDs [][]byte + suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(atomicSwap.ExpireHeight+1), func(id []byte) bool { + swapIDs = append(swapIDs, id) + return false + }) + suite.Equal(len(swapIDs), 1) + + // Marshal the expected swapID + cdc := suite.app.Codec() + res, _ := cdc.MarshalBinaryBare(atomicSwap.GetSwapID()) + expectedSwapID := res[1:] + + suite.Equal(expectedSwapID, swapIDs[0]) +} + +func (suite *KeeperTestSuite) TestRemoveFromByBlockIndex() { + suite.ResetChain() + + // Set new atomic swap in by block index + atomicSwap := atomicSwap(suite.ctx, 1) + suite.keeper.InsertIntoByBlockIndex(suite.ctx, atomicSwap) + + // Check stored data in block index + var swapIDsPre [][]byte + suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(atomicSwap.ExpireHeight+1), func(id []byte) bool { + swapIDsPre = append(swapIDsPre, id) + return false + }) + suite.Equal(len(swapIDsPre), 1) + + suite.keeper.RemoveFromByBlockIndex(suite.ctx, atomicSwap) + + // Check stored data not in block index + var swapIDsPost [][]byte + suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(atomicSwap.ExpireHeight+1), func(id []byte) bool { + swapIDsPost = append(swapIDsPost, id) + return false + }) + suite.Equal(len(swapIDsPost), 0) +} + +func (suite *KeeperTestSuite) TestIterateAtomicSwapsByBlock() { + suite.ResetChain() + + type args struct { + blockCtx sdk.Context + swap types.AtomicSwap + } + + var testCases []args + for i := 0; i < 8; i++ { + // Set up context 100 blocks apart + blockCtx := suite.ctx.WithBlockHeight(int64(i) * 100) + + // Initialize a new atomic swap (different randomNumberHash = different swap IDs) + timestamp := tmtime.Now().Add(time.Duration(i) * time.Minute).Unix() + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + atomicSwap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, + blockCtx.BlockHeight(), timestamp, TestUser1, TestUser2, + TestSenderOtherChain, TestRecipientOtherChain, 0, types.Open, + true, types.Incoming) + + // Insert into block index + suite.keeper.InsertIntoByBlockIndex(blockCtx, atomicSwap) + // Add to local block index + testCases = append(testCases, args{blockCtx, atomicSwap}) + } + + // Set up the expected swap IDs for a given cutoff block + cutoffBlock := int64(450) + var expectedSwapIDs [][]byte + for _, tc := range testCases { + if tc.blockCtx.BlockHeight() < cutoffBlock || tc.blockCtx.BlockHeight() == cutoffBlock { + expectedSwapIDs = append(expectedSwapIDs, tc.swap.GetSwapID()) + } + } + + // Read the swap IDs from store for a given cutoff block + var readSwapIDs [][]byte + suite.keeper.IterateAtomicSwapsByBlock(suite.ctx, uint64(cutoffBlock), func(id []byte) bool { + readSwapIDs = append(readSwapIDs, id) + return false + }) + + suite.Equal(expectedSwapIDs, readSwapIDs) +} + +func (suite *KeeperTestSuite) TestInsertIntoLongtermStorage() { + suite.ResetChain() + + // Set atomic swap in longterm storage + atomicSwap := atomicSwap(suite.ctx, 1) + atomicSwap.ClosedBlock = suite.ctx.BlockHeight() + suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap) + + // Longterm storage lacks getter methods, must use iteration to get count of swaps in store + var swapIDs [][]byte + suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool { + swapIDs = append(swapIDs, id) + return false + }) + suite.Equal(len(swapIDs), 1) + + // Marshal the expected swapID + cdc := suite.app.Codec() + res, _ := cdc.MarshalBinaryBare(atomicSwap.GetSwapID()) + expectedSwapID := res[1:] + + suite.Equal(expectedSwapID, swapIDs[0]) +} + +func (suite *KeeperTestSuite) TestRemoveFromLongtermStorage() { + suite.ResetChain() + + // Set atomic swap in longterm storage + atomicSwap := atomicSwap(suite.ctx, 1) + atomicSwap.ClosedBlock = suite.ctx.BlockHeight() + suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap) + + // Longterm storage lacks getter methods, must use iteration to get count of swaps in store + var swapIDs [][]byte + suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool { + swapIDs = append(swapIDs, id) + return false + }) + suite.Equal(len(swapIDs), 1) + + suite.keeper.RemoveFromLongtermStorage(suite.ctx, atomicSwap) + + // Check stored data not in block index + var swapIDsPost [][]byte + suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(atomicSwap.ClosedBlock+LongtermStorageDuration), func(id []byte) bool { + swapIDsPost = append(swapIDsPost, id) + return false + }) + suite.Equal(len(swapIDsPost), 0) +} + +func (suite *KeeperTestSuite) TestIterateAtomicSwapsLongtermStorage() { + suite.ResetChain() + + // Set up atomic swaps with stagged closed blocks + var swaps types.AtomicSwaps + for i := 0; i < 8; i++ { + timestamp := tmtime.Now().Unix() + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + atomicSwap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, + suite.ctx.BlockHeight(), timestamp, TestUser1, TestUser2, + TestSenderOtherChain, TestRecipientOtherChain, 100, types.Open, + true, types.Incoming) + + // Set closed block staggered by 100 blocks and insert into longterm storage + atomicSwap.ClosedBlock = int64(i) * 100 + suite.keeper.InsertIntoLongtermStorage(suite.ctx, atomicSwap) + // Add to local longterm storage + swaps = append(swaps, atomicSwap) + } + + // Set up the expected swap IDs for a given cutoff block. + cutoffBlock := int64(LongtermStorageDuration + 350) + var expectedSwapIDs [][]byte + for _, swap := range swaps { + if swap.ClosedBlock+LongtermStorageDuration < cutoffBlock || + swap.ClosedBlock+LongtermStorageDuration == cutoffBlock { + expectedSwapIDs = append(expectedSwapIDs, swap.GetSwapID()) + } + } + + // Read the swap IDs from store for a given cutoff block + var readSwapIDs [][]byte + suite.keeper.IterateAtomicSwapsLongtermStorage(suite.ctx, uint64(cutoffBlock), func(id []byte) bool { + readSwapIDs = append(readSwapIDs, id) + return false + }) + + // At the cutoff block, iteration should return half of the swap IDs + suite.Equal(len(swaps)/2, len(expectedSwapIDs)) + suite.Equal(len(swaps)/2, len(readSwapIDs)) + // Should be the same IDs + suite.Equal(expectedSwapIDs, readSwapIDs) +} + +func (suite *KeeperTestSuite) TestGetSetAssetSupply() { + suite.ResetChain() + + denom := "bnb" + // Put asset supply in store + assetSupply := types.NewAssetSupply(denom, c(denom, 0), c(denom, 0), c(denom, 50000), c(denom, 100000)) + suite.keeper.SetAssetSupply(suite.ctx, assetSupply, []byte(denom)) + + // Check asset in store + storedAssetSupply, found := suite.keeper.GetAssetSupply(suite.ctx, []byte(denom)) + suite.True(found) + suite.Equal(assetSupply, storedAssetSupply) + + // Check fake asset supply not in store + fakeDenom := "xyz" + _, found = suite.keeper.GetAssetSupply(suite.ctx, []byte(fakeDenom)) + suite.False(found) +} + +func (suite *KeeperTestSuite) TestIterateAssetSupplies() { + suite.ResetChain() + + // Set asset supplies + supplies := assetSupplies(5) + for _, supply := range supplies { + suite.keeper.SetAssetSupply(suite.ctx, supply, []byte(supply.Denom)) + } + + // Read each asset supply from the store + var readSupplies types.AssetSupplies + suite.keeper.IterateAssetSupplies(suite.ctx, func(a types.AssetSupply) bool { + readSupplies = append(readSupplies, a) + return false + }) + + // Check expected values + for i := 0; i < len(supplies); i++ { + suite.Contains(readSupplies, supplies[i]) + } +} + +func (suite *KeeperTestSuite) TestGetAllAssetSupplies() { + suite.ResetChain() + + // Set asset supplies + count := 3 + supplies := assetSupplies(count) + for _, supply := range supplies { + suite.keeper.SetAssetSupply(suite.ctx, supply, []byte(supply.Denom)) + } + + // Get all asset supplies + readSupplies := suite.keeper.GetAllAssetSupplies(suite.ctx) + suite.Equal(count, len(readSupplies)) + + // Check expected values + for i := 0; i < count; i++ { + suite.Contains(readSupplies, supplies[i]) + } +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/bep3/keeper/params.go b/x/bep3/keeper/params.go new file mode 100644 index 00000000..7b8cc88c --- /dev/null +++ b/x/bep3/keeper/params.go @@ -0,0 +1,75 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/bep3/types" +) + +// GetParams returns the total set of bep3 parameters. +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { + k.paramSubspace.GetParamSet(ctx, ¶ms) + return params +} + +// SetParams sets the bep3 parameters to the param space. +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramSubspace.SetParamSet(ctx, ¶ms) +} + +// GetBnbDeputyAddress returns the Bnbchain's deputy address +func (k Keeper) GetBnbDeputyAddress(ctx sdk.Context) sdk.AccAddress { + params := k.GetParams(ctx) + return params.BnbDeputyAddress +} + +// GetMaxBlockLock returns the maximum block lock +func (k Keeper) GetMaxBlockLock(ctx sdk.Context) int64 { + params := k.GetParams(ctx) + return params.MaxBlockLock +} + +// GetMinBlockLock returns the minimum block lock +func (k Keeper) GetMinBlockLock(ctx sdk.Context) int64 { + params := k.GetParams(ctx) + return params.MinBlockLock +} + +// GetAssets returns a list containing all supported assets +func (k Keeper) GetAssets(ctx sdk.Context) (types.AssetParams, bool) { + params := k.GetParams(ctx) + return params.SupportedAssets, len(params.SupportedAssets) > 0 +} + +// GetAssetByDenom returns an asset by its denom +func (k Keeper) GetAssetByDenom(ctx sdk.Context, denom string) (types.AssetParam, bool) { + params := k.GetParams(ctx) + for _, asset := range params.SupportedAssets { + if asset.Denom == denom { + return asset, true + } + } + return types.AssetParam{}, false +} + +// GetAssetByCoinID returns an asset by its denom +func (k Keeper) GetAssetByCoinID(ctx sdk.Context, coinID int) (types.AssetParam, bool) { + params := k.GetParams(ctx) + for _, asset := range params.SupportedAssets { + if asset.CoinID == coinID { + return asset, true + } + } + return types.AssetParam{}, false +} + +// ValidateLiveAsset checks if an asset is both supported and active +func (k Keeper) ValidateLiveAsset(ctx sdk.Context, coin sdk.Coin) sdk.Error { + asset, found := k.GetAssetByDenom(ctx, coin.Denom) + if !found { + return types.ErrAssetNotSupported(k.codespace, coin.Denom) + } + if !asset.Active { + return types.ErrAssetNotActive(k.codespace, asset.Denom) + } + return nil +} diff --git a/x/bep3/keeper/params_test.go b/x/bep3/keeper/params_test.go new file mode 100644 index 00000000..2e3fd4b7 --- /dev/null +++ b/x/bep3/keeper/params_test.go @@ -0,0 +1,141 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type ParamsTestSuite struct { + suite.Suite + + keeper keeper.Keeper + addrs []sdk.AccAddress + app app.TestApp + ctx sdk.Context +} + +func (suite *ParamsTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + _, addrs := app.GeneratePrivKeyAddressPairs(10) + tApp.InitializeFromGenesisStates(NewBep3GenStateMulti(addrs[0])) + suite.keeper = tApp.GetBep3Keeper() + suite.ctx = ctx + suite.addrs = addrs +} + +func (suite *ParamsTestSuite) TestGetSetBnbDeputyAddress() { + params := suite.keeper.GetParams(suite.ctx) + params.BnbDeputyAddress = suite.addrs[1] + suite.NotPanics(func() { suite.keeper.SetParams(suite.ctx, params) }) + + params = suite.keeper.GetParams(suite.ctx) + suite.Equal(suite.addrs[1], params.BnbDeputyAddress) + addr := suite.keeper.GetBnbDeputyAddress(suite.ctx) + suite.Equal(suite.addrs[1], addr) +} + +func (suite *ParamsTestSuite) TestGetMaxBlockLock() { + params := suite.keeper.GetParams(suite.ctx) + maxBlockLock := params.MaxBlockLock + + res := suite.keeper.GetMaxBlockLock(suite.ctx) + suite.Equal(maxBlockLock, res) +} + +func (suite *ParamsTestSuite) TestGetMinBlockLock() { + params := suite.keeper.GetParams(suite.ctx) + minBlockLock := params.MinBlockLock + + res := suite.keeper.GetMinBlockLock(suite.ctx) + suite.Equal(minBlockLock, res) +} + +func (suite *ParamsTestSuite) TestGetAssets() { + params := suite.keeper.GetParams(suite.ctx) + assets := params.SupportedAssets + + res, found := suite.keeper.GetAssets(suite.ctx) + suite.True(found) + suite.Equal(assets, res) +} + +func (suite *ParamsTestSuite) TestGetAssetByDenom() { + params := suite.keeper.GetParams(suite.ctx) + asset := params.SupportedAssets[0] + + res, found := suite.keeper.GetAssetByDenom(suite.ctx, asset.Denom) + suite.True(found) + suite.Equal(asset, res) +} + +func (suite *ParamsTestSuite) TestGetAssetByCoinID() { + params := suite.keeper.GetParams(suite.ctx) + asset := params.SupportedAssets[0] + + res, found := suite.keeper.GetAssetByCoinID(suite.ctx, asset.CoinID) + suite.True(found) + suite.Equal(asset, res) +} + +func (suite *AssetTestSuite) TestValidateLiveAsset() { + type args struct { + coin sdk.Coin + } + testCases := []struct { + name string + args args + expectedError sdk.CodeType + expectPass bool + }{ + { + "normal", + args{ + coin: c("bnb", 1), + }, + sdk.CodeType(0), + true, + }, + { + "asset not supported", + args{ + coin: c("bad", 1), + }, + types.CodeAssetNotSupported, + false, + }, + { + "asset not active", + args{ + coin: c("inc", 1), + }, + types.CodeAssetNotActive, + false, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + err := suite.keeper.ValidateLiveAsset(suite.ctx, tc.args.coin) + + if tc.expectPass { + suite.NoError(err) + } else { + suite.Error(err) + suite.Equal(tc.expectedError, err.Result().Code) + } + }) + } +} + +func TestParamsTestSuite(t *testing.T) { + suite.Run(t, new(ParamsTestSuite)) +} diff --git a/x/bep3/keeper/querier.go b/x/bep3/keeper/querier.go new file mode 100644 index 00000000..a64db015 --- /dev/null +++ b/x/bep3/keeper/querier.go @@ -0,0 +1,100 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/bep3/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// NewQuerier is the module level router for state queries +func NewQuerier(keeper Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { + switch path[0] { + case types.QueryGetAssetSupply: + return queryAssetSupply(ctx, req, keeper) + case types.QueryGetAtomicSwap: + return queryAtomicSwap(ctx, req, keeper) + case types.QueryGetAtomicSwaps: + return queryAtomicSwaps(ctx, req, keeper) + case types.QueryGetParams: + return queryGetParams(ctx, req, keeper) + default: + return nil, sdk.ErrUnknownRequest("unknown bep3 query endpoint") + } + } +} + +func queryAssetSupply(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Decode request + var requestParams types.QueryAssetSupply + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + assetSupply, found := keeper.GetAssetSupply(ctx, []byte(requestParams.Denom)) + if !found { + return nil, sdk.ErrInternal("Not found") + } + + // Encode results + bz, err := codec.MarshalJSONIndent(keeper.cdc, assetSupply) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return bz, nil +} + +func queryAtomicSwap(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Decode request + var requestParams types.QueryAtomicSwapByID + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + // Lookup atomic swap + atomicSwap, found := keeper.GetAtomicSwap(ctx, requestParams.SwapID) + if !found { + return nil, sdk.ErrInternal("Not found") + } + + // Encode results + bz, err := codec.MarshalJSONIndent(keeper.cdc, atomicSwap) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return bz, nil +} + +func queryAtomicSwaps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var swaps types.AtomicSwaps + + keeper.IterateAtomicSwaps(ctx, func(s types.AtomicSwap) bool { + swaps = append(swaps, s) + return false + }) + + bz, err2 := codec.MarshalJSONIndent(keeper.cdc, swaps) + if err2 != nil { + return nil, sdk.ErrInternal("could not marshal result to JSON") + } + + return bz, nil +} + +// query params in the bep3 store +func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Get params + params := keeper.GetParams(ctx) + + // Encode results + bz, err := codec.MarshalJSONIndent(keeper.cdc, params) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil +} diff --git a/x/bep3/keeper/querier_test.go b/x/bep3/keeper/querier_test.go new file mode 100644 index 00000000..c7355e4d --- /dev/null +++ b/x/bep3/keeper/querier_test.go @@ -0,0 +1,167 @@ +package keeper_test + +import ( + "encoding/hex" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const ( + custom = "custom" +) + +type QuerierTestSuite struct { + suite.Suite + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + querier sdk.Querier + addrs []sdk.AccAddress + isSupplyDenom map[string]bool + swapIDs []cmn.HexBytes + isSwapID map[string]bool +} + +func (suite *QuerierTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + // Set up auth GenesisState + _, addrs := app.GeneratePrivKeyAddressPairs(10) + coins := []sdk.Coins{} + for j := 0; j < 10; j++ { + coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000))) + } + authGS := app.NewAuthGenState(addrs, coins) + + tApp.InitializeFromGenesisStates( + authGS, + NewBep3GenStateMulti(addrs[0]), + ) + + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetBep3Keeper() + suite.querier = keeper.NewQuerier(suite.keeper) + suite.addrs = addrs + + // Create atomic swaps and save IDs + var swapIDs []cmn.HexBytes + isSwapID := make(map[string]bool) + for i := 0; i < 10; i++ { + // Set up atomic swap variables + expireHeight := int64(360) + amount := cs(c("bnb", 100)) + timestamp := ts(0) + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + // Create atomic swap and check err + err := suite.keeper.CreateAtomicSwap(suite.ctx, randomNumberHash, timestamp, expireHeight, + addrs[0], suite.addrs[i], TestSenderOtherChain, TestRecipientOtherChain, amount, + amount.String(), true) + suite.Nil(err) + + // Calculate swap ID and save + swapID := types.CalculateSwapID(randomNumberHash, addrs[0], TestSenderOtherChain) + swapIDs = append(swapIDs, swapID) + isSwapID[hex.EncodeToString(swapID)] = true + } + suite.swapIDs = swapIDs + suite.isSwapID = isSwapID +} + +func (suite *QuerierTestSuite) TestQueryAssetSupply() { + ctx := suite.ctx.WithIsCheckTx(false) + + // Set up request query + denom := "bnb" + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAssetSupply}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAssetSupply(cmn.HexBytes(denom))), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryGetAssetSupply}, query) + suite.Nil(err) + suite.NotNil(bz) + + // Unmarshal the bytes into type asset supply + var supply types.AssetSupply + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &supply)) + + expectedSupply := types.NewAssetSupply(denom, c(denom, 1000), + c(denom, 0), c(denom, 0), c(denom, StandardSupplyLimit.Int64())) + suite.Equal(supply, expectedSupply) +} + +func (suite *QuerierTestSuite) TestQueryAtomicSwap() { + ctx := suite.ctx.WithIsCheckTx(false) + + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAtomicSwap}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAtomicSwapByID(suite.swapIDs[0])), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryGetAtomicSwap}, query) + suite.Nil(err) + suite.NotNil(bz) + + // Unmarshal the bytes into type atomic swap + var swap types.AtomicSwap + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &swap)) + + // Check the returned atomic swap's ID + suite.True(suite.isSwapID[hex.EncodeToString(swap.GetSwapID())]) +} + +func (suite *QuerierTestSuite) TestQueryAtomicSwaps() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAtomicSwaps}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAtomicSwaps(1, 100)), + } + + bz, err := suite.querier(ctx, []string{types.QueryGetAtomicSwaps}, query) + suite.Nil(err) + suite.NotNil(bz) + + var swaps types.AtomicSwaps + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &swaps)) + + suite.Equal(len(suite.swapIDs), len(swaps)) + for _, swap := range swaps { + suite.True(suite.isSwapID[hex.EncodeToString(swap.GetSwapID())]) + } +} + +func (suite *QuerierTestSuite) TestQueryParams() { + ctx := suite.ctx.WithIsCheckTx(false) + bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{}) + suite.Nil(err) + suite.NotNil(bz) + + var p types.Params + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p)) + + bep3GenesisState := NewBep3GenStateMulti(suite.addrs[0]) + gs := types.GenesisState{} + types.ModuleCdc.UnmarshalJSON(bep3GenesisState["bep3"], &gs) + suite.Equal(gs.Params, p) +} + +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QuerierTestSuite)) +} diff --git a/x/bep3/keeper/swap.go b/x/bep3/keeper/swap.go new file mode 100644 index 00000000..cbc9a26c --- /dev/null +++ b/x/bep3/keeper/swap.go @@ -0,0 +1,266 @@ +package keeper + +import ( + "bytes" + "encoding/hex" + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/bep3/types" +) + +// CreateAtomicSwap creates a new AtomicSwap +func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, timestamp int64, heightSpan int64, + sender sdk.AccAddress, recipient sdk.AccAddress, senderOtherChain, recipientOtherChain string, + amount sdk.Coins, expectedIncome string, crossChain bool) sdk.Error { + // Confirm that this is not a duplicate swap + swapID := types.CalculateSwapID(randomNumberHash, sender, senderOtherChain) + _, found := k.GetAtomicSwap(ctx, swapID) + if found { + return types.ErrAtomicSwapAlreadyExists(k.codespace, swapID) + } + + // The heightSpan period should be more than 10 minutes and less than one week + // Assume average block time interval is 10 second. 10 mins = 60 blocks, 1 week = 60480 blocks + if heightSpan < k.GetMinBlockLock(ctx) || heightSpan > k.GetMaxBlockLock(ctx) { + return types.ErrInvalidHeightSpan(k.codespace, heightSpan, k.GetMinBlockLock(ctx), k.GetMaxBlockLock(ctx)) + } + + // Unix timestamp must be in range [-15 mins, 30 mins] of the current time + pastTimestampLimit := ctx.BlockTime().Add(time.Duration(-15) * time.Minute).Unix() + futureTimestampLimit := ctx.BlockTime().Add(time.Duration(30) * time.Minute).Unix() + if timestamp < pastTimestampLimit || timestamp >= futureTimestampLimit { + return types.ErrInvalidTimestamp(k.codespace) + } + + // Sanity check on recipient address + if recipient.Empty() { + return sdk.ErrInvalidAddress("invalid (empty) recipient address") + } + + if len(amount) != 1 { + return sdk.ErrInternal("amount must contain exactly one coin") + } + + err := k.ValidateLiveAsset(ctx, amount[0]) + if err != nil { + return err + } + + var direction types.SwapDirection + deputy := k.GetBnbDeputyAddress(ctx) + if sender.Equals(deputy) { + direction = types.Incoming + } else { + direction = types.Outgoing + } + + switch direction { + case types.Incoming: + err := k.IncrementIncomingAssetSupply(ctx, amount[0]) + if err != nil { + return err + } + case types.Outgoing: + err := k.IncrementOutgoingAssetSupply(ctx, amount[0]) + if err != nil { + return err + } + default: + return sdk.ErrInternal("invalid swap direction") + } + + // Transfer coins to module + err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount) + if err != nil { + return err + } + + // Store the details of the swap + atomicSwap := types.NewAtomicSwap(amount, randomNumberHash, ctx.BlockHeight()+heightSpan, + timestamp, sender, recipient, senderOtherChain, recipientOtherChain, 0, types.Open, + crossChain, direction) + + // Emit 'create_atomic_swap' event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCreateAtomicSwap, + sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", atomicSwap.Sender)), + sdk.NewAttribute(types.AttributeKeyRecipient, fmt.Sprintf("%s", atomicSwap.Recipient)), + sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))), + sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))), + sdk.NewAttribute(types.AttributeKeyTimestamp, fmt.Sprintf("%d", atomicSwap.Timestamp)), + sdk.NewAttribute(types.AttributeKeySenderOtherChain, fmt.Sprintf("%s", atomicSwap.SenderOtherChain)), + sdk.NewAttribute(types.AttributeKeyExpireHeight, fmt.Sprintf("%d", atomicSwap.ExpireHeight)), + sdk.NewAttribute(types.AttributeKeyAmount, fmt.Sprintf("%s", atomicSwap.Amount[0].String())), + sdk.NewAttribute(types.AttributeKeyExpectedIncome, fmt.Sprintf("%s", expectedIncome)), + sdk.NewAttribute(types.AttributeKeyDirection, fmt.Sprintf("%s", atomicSwap.Direction.String())), + ), + ) + + k.SetAtomicSwap(ctx, atomicSwap) + k.InsertIntoByBlockIndex(ctx, atomicSwap) + return nil +} + +// ClaimAtomicSwap validates a claim attempt, and if successful, sends the escrowed amount and closes the AtomicSwap +func (k Keeper) ClaimAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte, randomNumber []byte) sdk.Error { + atomicSwap, found := k.GetAtomicSwap(ctx, swapID) + if !found { + return types.ErrAtomicSwapNotFound(k.codespace, swapID) + } + + // Only open atomic swaps can be claimed + if atomicSwap.Status != types.Open { + return types.ErrSwapNotClaimable(k.codespace) + } + + // Calculate hashed secret using submitted number + hashedSubmittedNumber := types.CalculateRandomHash(randomNumber, atomicSwap.Timestamp) + hashedSecret := types.CalculateSwapID(hashedSubmittedNumber, atomicSwap.Sender, atomicSwap.SenderOtherChain) + + // Confirm that secret unlocks the atomic swap + if !bytes.Equal(hashedSecret, atomicSwap.GetSwapID()) { + return types.ErrInvalidClaimSecret(k.codespace, hashedSecret, atomicSwap.GetSwapID()) + } + + switch atomicSwap.Direction { + case types.Incoming: + err := k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0]) + if err != nil { + return err + } + err = k.IncrementCurrentAssetSupply(ctx, atomicSwap.Amount[0]) + if err != nil { + return err + } + case types.Outgoing: + err := k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0]) + if err != nil { + return err + } + err = k.DecrementCurrentAssetSupply(ctx, atomicSwap.Amount[0]) + if err != nil { + return err + } + default: + return sdk.ErrInternal("invalid swap direction") + } + + // Send intended recipient coins + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Recipient, atomicSwap.Amount) + if err != nil { + return err + } + + // Emit 'claim_atomic_swap' event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaimAtomicSwap, + sdk.NewAttribute(types.AttributeKeyClaimSender, fmt.Sprintf("%s", from)), + sdk.NewAttribute(types.AttributeKeyRecipient, fmt.Sprintf("%s", atomicSwap.Recipient)), + sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))), + sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))), + sdk.NewAttribute(types.AttributeKeyRandomNumber, fmt.Sprintf("%s", hex.EncodeToString(randomNumber))), + ), + ) + + // Complete swap + atomicSwap.Status = types.Completed + atomicSwap.ClosedBlock = ctx.BlockHeight() + k.SetAtomicSwap(ctx, atomicSwap) + + // Remove from byBlock index and transition to longterm storage + k.RemoveFromByBlockIndex(ctx, atomicSwap) + k.InsertIntoLongtermStorage(ctx, atomicSwap) + return nil +} + +// RefundAtomicSwap refunds an AtomicSwap, sending assets to the original sender and closing the AtomicSwap +func (k Keeper) RefundAtomicSwap(ctx sdk.Context, from sdk.AccAddress, swapID []byte) sdk.Error { + atomicSwap, found := k.GetAtomicSwap(ctx, swapID) + if !found { + return types.ErrAtomicSwapNotFound(k.codespace, swapID) + } + // Only expired swaps may be refunded + if atomicSwap.Status != types.Expired { + return types.ErrSwapNotRefundable(k.codespace) + } + + switch atomicSwap.Direction { + case types.Incoming: + err := k.DecrementIncomingAssetSupply(ctx, atomicSwap.Amount[0]) + if err != nil { + return err + } + case types.Outgoing: + err := k.DecrementOutgoingAssetSupply(ctx, atomicSwap.Amount[0]) + if err != nil { + return err + } + default: + return sdk.ErrInternal("invalid swap direction") + } + + // Refund coins to original swap sender + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, atomicSwap.Sender, atomicSwap.Amount) + if err != nil { + return err + } + + // Emit 'refund_atomic_swap' event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeRefundAtomicSwap, + sdk.NewAttribute(types.AttributeKeyRefundSender, fmt.Sprintf("%s", from)), + sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", atomicSwap.Sender)), + sdk.NewAttribute(types.AttributeKeyAtomicSwapID, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.GetSwapID()))), + sdk.NewAttribute(types.AttributeKeyRandomNumberHash, fmt.Sprintf("%s", hex.EncodeToString(atomicSwap.RandomNumberHash))), + ), + ) + + // Complete swap + atomicSwap.Status = types.Completed + atomicSwap.ClosedBlock = ctx.BlockHeight() + k.SetAtomicSwap(ctx, atomicSwap) + + // Transition to longterm storage + k.InsertIntoLongtermStorage(ctx, atomicSwap) + return nil +} + +// UpdateExpiredAtomicSwaps finds all AtomicSwaps that are past (or at) their ending times and expires them. +func (k Keeper) UpdateExpiredAtomicSwaps(ctx sdk.Context) sdk.Error { + var expiredSwaps [][]byte + k.IterateAtomicSwapsByBlock(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool { + expiredSwaps = append(expiredSwaps, id) + return false + }) + + // Expire incomplete swaps (claimed swaps have already been removed from byBlock index) + for _, id := range expiredSwaps { + swap, _ := k.GetAtomicSwap(ctx, id) + swap.Status = types.Expired + k.SetAtomicSwap(ctx, swap) + k.RemoveFromByBlockIndex(ctx, swap) + } + return nil +} + +// DeleteClosedAtomicSwapsFromLongtermStorage removes swaps one week after completion +func (k Keeper) DeleteClosedAtomicSwapsFromLongtermStorage(ctx sdk.Context) sdk.Error { + var swapsToDelete [][]byte + k.IterateAtomicSwapsLongtermStorage(ctx, uint64(ctx.BlockHeight()), func(id []byte) bool { + swapsToDelete = append(swapsToDelete, id) + return false + }) + + // Delete closed atomic swaps + for _, id := range swapsToDelete { + swap, _ := k.GetAtomicSwap(ctx, id) + k.RemoveAtomicSwap(ctx, swap.GetSwapID()) + k.RemoveFromLongtermStorage(ctx, swap) + } + return nil +} diff --git a/x/bep3/keeper/swap_test.go b/x/bep3/keeper/swap_test.go new file mode 100644 index 00000000..7cfb1447 --- /dev/null +++ b/x/bep3/keeper/swap_test.go @@ -0,0 +1,696 @@ +package keeper_test + +import ( + "fmt" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3" + "github.com/kava-labs/kava/x/bep3/keeper" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type AtomicSwapTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + deputy sdk.AccAddress + addrs []sdk.AccAddress + timestamps []int64 + randomNumberHashes []cmn.HexBytes + swapIDs []cmn.HexBytes + randomNumbers []cmn.HexBytes +} + +const ( + STARING_BNB_BALANCE = int64(1000000000) + BNB_DENOM = "bnb" +) + +func (suite *AtomicSwapTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + // Initialize test app and set context + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + // Create and load 20 accounts with bnb tokens + coins := []sdk.Coins{} + for i := 0; i < 20; i++ { + coins = append(coins, cs(c(BNB_DENOM, STARING_BNB_BALANCE))) + } + _, addrs := app.GeneratePrivKeyAddressPairs(20) + deputy := addrs[0] + authGS := app.NewAuthGenState(addrs, coins) + + // Initialize genesis state + tApp.InitializeFromGenesisStates(authGS, NewBep3GenStateMulti(deputy)) + + suite.app = tApp + suite.ctx = ctx + suite.deputy = deputy + suite.addrs = addrs + suite.keeper = suite.app.GetBep3Keeper() + suite.GenerateSwapDetails() +} + +func (suite *AtomicSwapTestSuite) GenerateSwapDetails() { + var timestamps []int64 + var randomNumberHashes []cmn.HexBytes + var randomNumbers []cmn.HexBytes + for i := 0; i < 10; i++ { + // Set up atomic swap details + timestamp := ts(i) + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + timestamps = append(timestamps, timestamp) + randomNumberHashes = append(randomNumberHashes, randomNumberHash) + randomNumbers = append(randomNumbers, randomNumber.Bytes()) + } + suite.timestamps = timestamps + suite.randomNumberHashes = randomNumberHashes + suite.randomNumbers = randomNumbers +} + +func (suite *AtomicSwapTestSuite) TestCreateAtomicSwap() { + currentTmTime := tmtime.Now() + type args struct { + randomNumberHash []byte + timestamp int64 + heightSpan int64 + sender sdk.AccAddress + recipient sdk.AccAddress + senderOtherChain string + recipientOtherChain string + coins sdk.Coins + expectedIncome string + crossChain bool + direction types.SwapDirection + } + testCases := []struct { + name string + blockTime time.Time + args args + expectPass bool + shouldBeFound bool + }{ + { + "incoming swap", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[0], + timestamp: suite.timestamps[0], + heightSpan: int64(360), + sender: suite.deputy, + recipient: suite.addrs[1], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM), + crossChain: true, + direction: types.Incoming, + }, + true, + true, + }, + { + "outgoing swap", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[0], + timestamp: suite.timestamps[0], + heightSpan: int64(360), + sender: suite.addrs[1], + recipient: suite.addrs[2], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM), + crossChain: true, + direction: types.Outgoing, + }, + true, + true, + }, + { + "unsupported asset", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[1], + timestamp: suite.timestamps[1], + heightSpan: int64(360), + sender: suite.deputy, + recipient: suite.addrs[2], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c("xyz", 50000)), + expectedIncome: "50000xyz", + crossChain: true, + direction: types.Incoming, + }, + false, + false, + }, + { + "past timestamp", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[2], + timestamp: suite.timestamps[2] - 2000, + heightSpan: int64(360), + sender: suite.deputy, + recipient: suite.addrs[3], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM), + crossChain: true, + direction: types.Incoming, + }, + false, + false, + }, + { + "future timestamp", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[3], + timestamp: suite.timestamps[3] + 5000, + heightSpan: int64(360), + sender: suite.deputy, + recipient: suite.addrs[4], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM), + crossChain: true, + direction: types.Incoming, + }, + false, + false, + }, + { + "small height span", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[4], + timestamp: suite.timestamps[4], + heightSpan: int64(5), + sender: suite.deputy, + recipient: suite.addrs[5], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM), + crossChain: true, + direction: types.Incoming, + }, + false, + false, + }, + { + "big height span", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[5], + timestamp: suite.timestamps[5], + heightSpan: int64(1000000), + sender: suite.deputy, + recipient: suite.addrs[6], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: fmt.Sprintf("50000%s", BNB_DENOM), + crossChain: true, + direction: types.Incoming, + }, + false, + false, + }, + { + "zero amount", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[6], + timestamp: suite.timestamps[6], + heightSpan: int64(360), + sender: suite.deputy, + recipient: suite.addrs[7], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 0)), + expectedIncome: fmt.Sprintf("0%s", BNB_DENOM), + crossChain: true, + direction: types.Incoming, + }, + false, + false, + }, + { + "duplicate swap", + currentTmTime, + args{ + randomNumberHash: suite.randomNumberHashes[0], + timestamp: suite.timestamps[0], + heightSpan: int64(360), + sender: suite.deputy, + recipient: suite.addrs[1], + senderOtherChain: TestSenderOtherChain, + recipientOtherChain: TestRecipientOtherChain, + coins: cs(c(BNB_DENOM, 50000)), + expectedIncome: "50000bnb", + crossChain: true, + direction: types.Incoming, + }, + false, + true, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // Increment current asset supply to support outgoing swaps + if tc.args.direction == types.Outgoing { + err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, tc.args.coins[0]) + suite.Nil(err) + } + + // Load asset denom (required for zero coins test case) + var swapAssetDenom string + if len(tc.args.coins) == 1 { + swapAssetDenom = tc.args.coins[0].Denom + } else { + swapAssetDenom = BNB_DENOM + } + + // Load sender's account prior to swap creation + ak := suite.app.GetAccountKeeper() + senderAccPre := ak.GetAccount(suite.ctx, tc.args.sender) + senderBalancePre := senderAccPre.GetCoins().AmountOf(swapAssetDenom) + assetSupplyPre, _ := suite.keeper.GetAssetSupply(suite.ctx, []byte(swapAssetDenom)) + + // Create atomic swap + err := suite.keeper.CreateAtomicSwap(suite.ctx, tc.args.randomNumberHash, tc.args.timestamp, + tc.args.heightSpan, tc.args.sender, tc.args.recipient, tc.args.senderOtherChain, + tc.args.recipientOtherChain, tc.args.coins, tc.args.expectedIncome, tc.args.crossChain) + + // Load sender's account after swap creation + senderAccPost := ak.GetAccount(suite.ctx, tc.args.sender) + senderBalancePost := senderAccPost.GetCoins().AmountOf(swapAssetDenom) + assetSupplyPost, _ := suite.keeper.GetAssetSupply(suite.ctx, []byte(swapAssetDenom)) + + // Load expected swap ID + expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain) + + if tc.expectPass { + suite.NoError(err) + + // Check coins moved + suite.Equal(senderBalancePre.Sub(tc.args.coins[0].Amount), senderBalancePost) + + // Check incoming/outgoing asset supply increased + switch tc.args.direction { + case types.Incoming: + suite.Equal(assetSupplyPre.IncomingSupply.Add(tc.args.coins[0]), assetSupplyPost.IncomingSupply) + case types.Outgoing: + suite.Equal(assetSupplyPre.OutgoingSupply.Add(tc.args.coins[0]), assetSupplyPost.OutgoingSupply) + default: + suite.Fail("should not have invalid direction") + } + + // Check swap in store + actualSwap, found := suite.keeper.GetAtomicSwap(suite.ctx, expectedSwapID) + suite.True(found) + suite.NotNil(actualSwap) + + // Confirm swap contents + expectedSwap := types.Swap( + types.AtomicSwap{ + Amount: tc.args.coins, + RandomNumberHash: tc.args.randomNumberHash, + ExpireHeight: suite.ctx.BlockHeight() + tc.args.heightSpan, + Timestamp: tc.args.timestamp, + Sender: tc.args.sender, + Recipient: tc.args.recipient, + SenderOtherChain: tc.args.senderOtherChain, + RecipientOtherChain: tc.args.recipientOtherChain, + ClosedBlock: 0, + Status: types.Open, + CrossChain: tc.args.crossChain, + Direction: tc.args.direction, + }) + suite.Equal(expectedSwap, actualSwap) + } else { + suite.Error(err) + // Check coins not moved + suite.Equal(senderBalancePre, senderBalancePost) + + // Check incoming/outgoing asset supply not increased + switch tc.args.direction { + case types.Incoming: + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + case types.Outgoing: + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + default: + suite.Fail("should not have invalid direction") + } + + // Check if swap found in store + _, found := suite.keeper.GetAtomicSwap(suite.ctx, expectedSwapID) + if !tc.shouldBeFound { + suite.False(found) + } else { + suite.True(found) + } + } + }) + } +} + +func (suite *AtomicSwapTestSuite) TestClaimAtomicSwap() { + suite.SetupTest() + invalidRandomNumber, _ := types.GenerateSecureRandomNumber() + type args struct { + swapID []byte + randomNumber []byte + direction types.SwapDirection + } + testCases := []struct { + name string + claimCtx sdk.Context + args args + expectPass bool + }{ + { + "normal incoming swap", + suite.ctx, + args{ + swapID: []byte{}, + randomNumber: []byte{}, + direction: types.Incoming, + }, + true, + }, + { + "normal outgoing swap", + suite.ctx, + args{ + swapID: []byte{}, + randomNumber: []byte{}, + direction: types.Outgoing, + }, + true, + }, + { + "invalid random number", + suite.ctx, + args{ + swapID: []byte{}, + randomNumber: invalidRandomNumber.Bytes(), + direction: types.Incoming, + }, + false, + }, + { + "wrong swap ID", + suite.ctx, + args{ + swapID: types.CalculateSwapID(suite.randomNumberHashes[3], suite.addrs[6], TestRecipientOtherChain), + randomNumber: []byte{}, + direction: types.Outgoing, + }, + false, + }, + { + "past expiration", + suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 2000), + args{ + swapID: []byte{}, + randomNumber: []byte{}, + direction: types.Incoming, + }, + false, + }, + } + + for i, tc := range testCases { + suite.GenerateSwapDetails() + suite.Run(tc.name, func() { + expectedRecipient := suite.addrs[5] + expectedClaimAmount := cs(c(BNB_DENOM, 50000)) + sender := suite.deputy + + // Set sender to other and increment current asset supply for outgoing swap + if tc.args.direction == types.Outgoing { + sender = suite.addrs[6] + err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, expectedClaimAmount[0]) + suite.Nil(err) + } + + // Create atomic swap + err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i], + int64(360), sender, expectedRecipient, TestSenderOtherChain, TestRecipientOtherChain, + expectedClaimAmount, expectedClaimAmount.String(), true) + suite.NoError(err) + + realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain) + + // If args contains an invalid swap ID claim attempt will use it instead of the real swap ID + var claimSwapID []byte + if len(tc.args.swapID) == 0 { + claimSwapID = realSwapID + } else { + claimSwapID = tc.args.swapID + } + + // If args contains an invalid random number claim attempt will use it instead of the real random number + var claimRandomNumber []byte + if len(tc.args.randomNumber) == 0 { + claimRandomNumber = suite.randomNumbers[i] + } else { + claimRandomNumber = tc.args.randomNumber + } + + // Run the beginblocker before attempting claim + bep3.BeginBlocker(tc.claimCtx, suite.keeper) + + // Load expected recipient's account prior to claim attempt + ak := suite.app.GetAccountKeeper() + expectedRecipientAccPre := ak.GetAccount(tc.claimCtx, expectedRecipient) + expectedRecipientBalancePre := expectedRecipientAccPre.GetCoins().AmountOf(expectedClaimAmount[0].Denom) + // Load asset supplies prior to claim attempt + assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.claimCtx, []byte(expectedClaimAmount[0].Denom)) + + // Attempt to claim atomic swap + err = suite.keeper.ClaimAtomicSwap(tc.claimCtx, expectedRecipient, claimSwapID, claimRandomNumber) + + // Load expected recipient's account after the claim attempt + expectedRecipientAccPost := ak.GetAccount(tc.claimCtx, expectedRecipient) + expectedRecipientBalancePost := expectedRecipientAccPost.GetCoins().AmountOf(expectedClaimAmount[0].Denom) + // Load asset supplies after the claim attempt + assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.claimCtx, []byte(expectedClaimAmount[0].Denom)) + + if tc.expectPass { + suite.NoError(err) + // Check coins moved + suite.Equal(expectedRecipientBalancePre.Add(expectedClaimAmount[0].Amount), expectedRecipientBalancePost) + + // Check asset supply changes + switch tc.args.direction { + case types.Incoming: + // Check incoming supply decreased + suite.True(assetSupplyPre.IncomingSupply.Amount.Sub(expectedClaimAmount[0].Amount).Equal(assetSupplyPost.IncomingSupply.Amount)) + // Check current supply increased + suite.Equal(assetSupplyPre.CurrentSupply.Add(expectedClaimAmount[0]), assetSupplyPost.CurrentSupply) + // Check outgoing supply not changed + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + case types.Outgoing: + // Check incoming supply not changed + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + // Check current supply decreased + suite.Equal(assetSupplyPre.CurrentSupply.Sub(expectedClaimAmount[0]), assetSupplyPost.CurrentSupply) + // Check outgoing supply decreased + suite.True(assetSupplyPre.OutgoingSupply.Sub(expectedClaimAmount[0]).IsEqual(assetSupplyPost.OutgoingSupply)) + default: + suite.Fail("should not have invalid direction") + } + } else { + suite.Error(err) + // Check coins not moved + suite.Equal(expectedRecipientBalancePre, expectedRecipientBalancePost) + + // Check asset supply has not changed + switch tc.args.direction { + case types.Incoming: + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply) + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + case types.Outgoing: + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply) + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + default: + suite.Fail("should not have invalid direction") + } + } + }) + } +} + +func (suite *AtomicSwapTestSuite) TestRefundAtomicSwap() { + suite.SetupTest() + + type args struct { + swapID []byte + direction types.SwapDirection + } + testCases := []struct { + name string + refundCtx sdk.Context + args args + expectPass bool + }{ + { + "normal incoming swap", + suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400), + args{ + swapID: []byte{}, + direction: types.Incoming, + }, + true, + }, + { + "normal outgoing swap", + suite.ctx.WithBlockHeight(suite.ctx.BlockHeight() + 400), + args{ + swapID: []byte{}, + direction: types.Outgoing, + }, + true, + }, + { + "before expiration", + suite.ctx, + args{ + swapID: []byte{}, + direction: types.Incoming, + }, + false, + }, + { + "wrong swapID", + suite.ctx, + args{ + swapID: types.CalculateSwapID(suite.randomNumberHashes[6], suite.addrs[1], TestRecipientOtherChain), + direction: types.Incoming, + }, + false, + }, + } + + for i, tc := range testCases { + suite.GenerateSwapDetails() + suite.Run(tc.name, func() { + // Create atomic swap + expectedRefundAmount := cs(c(BNB_DENOM, 50000)) + sender := suite.deputy + + // Set sender to other and increment current asset supply for outgoing swap + if tc.args.direction == types.Outgoing { + sender = suite.addrs[6] + err := suite.keeper.IncrementCurrentAssetSupply(suite.ctx, expectedRefundAmount[0]) + suite.Nil(err) + } + + err := suite.keeper.CreateAtomicSwap(suite.ctx, suite.randomNumberHashes[i], suite.timestamps[i], + int64(360), sender, suite.addrs[8], TestSenderOtherChain, TestRecipientOtherChain, + expectedRefundAmount, expectedRefundAmount.String(), true) + suite.NoError(err) + + realSwapID := types.CalculateSwapID(suite.randomNumberHashes[i], sender, TestSenderOtherChain) + + // If args contains an invalid swap ID refund attempt will use it instead of the real swap ID + var refundSwapID []byte + if len(tc.args.swapID) == 0 { + refundSwapID = realSwapID + } else { + refundSwapID = tc.args.swapID + } + + // Run the beginblocker before attempting refund + bep3.BeginBlocker(tc.refundCtx, suite.keeper) + + // Load sender's account prior to swap refund + ak := suite.app.GetAccountKeeper() + originalSenderAccPre := ak.GetAccount(tc.refundCtx, sender) + originalSenderBalancePre := originalSenderAccPre.GetCoins().AmountOf(expectedRefundAmount[0].Denom) + // Load asset supply prior to swap refund + assetSupplyPre, _ := suite.keeper.GetAssetSupply(tc.refundCtx, []byte(expectedRefundAmount[0].Denom)) + + // Attempt to refund atomic swap + err = suite.keeper.RefundAtomicSwap(tc.refundCtx, sender, refundSwapID) + + // Load sender's account after refund + originalSenderAccPost := ak.GetAccount(tc.refundCtx, sender) + originalSenderBalancePost := originalSenderAccPost.GetCoins().AmountOf(expectedRefundAmount[0].Denom) + // Load asset supply after to swap refund + assetSupplyPost, _ := suite.keeper.GetAssetSupply(tc.refundCtx, []byte(expectedRefundAmount[0].Denom)) + + if tc.expectPass { + suite.NoError(err) + // Check coins moved + suite.Equal(originalSenderBalancePre.Add(expectedRefundAmount[0].Amount), originalSenderBalancePost) + + // Check asset supply changes + switch tc.args.direction { + case types.Incoming: + // Check incoming supply decreased + suite.True(assetSupplyPre.IncomingSupply.Sub(expectedRefundAmount[0]).IsEqual(assetSupplyPost.IncomingSupply)) + // Check current, outgoing supply not changed + suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply) + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + case types.Outgoing: + // Check incoming, current supply not changed + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply) + // Check outgoing supply decreased + suite.True(assetSupplyPre.OutgoingSupply.Sub(expectedRefundAmount[0]).IsEqual(assetSupplyPost.OutgoingSupply)) + default: + suite.Fail("should not have invalid direction") + } + } else { + suite.Error(err) + // Check coins not moved + suite.Equal(originalSenderBalancePre, originalSenderBalancePost) + + // Check asset supply has not changed + switch tc.args.direction { + case types.Incoming: + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply) + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + case types.Outgoing: + suite.Equal(assetSupplyPre.IncomingSupply, assetSupplyPost.IncomingSupply) + suite.Equal(assetSupplyPre.CurrentSupply, assetSupplyPost.CurrentSupply) + suite.Equal(assetSupplyPre.OutgoingSupply, assetSupplyPost.OutgoingSupply) + default: + suite.Fail("should not have invalid direction") + } + } + }) + } +} + +func TestAtomicSwapTestSuite(t *testing.T) { + suite.Run(t, new(AtomicSwapTestSuite)) +} diff --git a/x/bep3/module.go b/x/bep3/module.go new file mode 100644 index 00000000..65db1b6a --- /dev/null +++ b/x/bep3/module.go @@ -0,0 +1,136 @@ +package bep3 + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/mux" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/bep3/client/cli" + "github.com/kava-labs/kava/x/bep3/client/rest" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the bep3 module. +type AppModuleBasic struct{} + +// Name returns the bep3 module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the bep3 module's types for the given codec. +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis returns default genesis state as raw bytes for the bep3 +// module. +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the bep3 module. +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) + if err != nil { + return err + } + return gs.Validate() +} + +// RegisterRESTRoutes registers the REST routes for the bep3 module. +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + rest.RegisterRoutes(ctx, rtr) +} + +// GetTxCmd returns the root tx command for the bep3 module. +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetTxCmd(cdc) +} + +// GetQueryCmd returns no root query command for the bep3 module. +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetQueryCmd(StoreKey, cdc) +} + +// AppModule implements the sdk.AppModule interface. +type AppModule struct { + AppModuleBasic + + keeper Keeper + supplyKeeper SupplyKeeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper, supplyKeeper SupplyKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + supplyKeeper: supplyKeeper, + } +} + +// Name returns the bep3 module's name. +func (AppModule) Name() string { + return ModuleName +} + +// RegisterInvariants registers the bep3 module invariants. +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route returns the message routing key for the bep3 module. +func (AppModule) Route() string { + return RouterKey +} + +// NewHandler returns an sdk.Handler for the bep3 module. +func (am AppModule) NewHandler() sdk.Handler { + return NewHandler(am.keeper) +} + +// QuerierRoute returns the bep3 module's querier route name. +func (AppModule) QuerierRoute() string { + return QuerierRoute +} + +// NewQuerierHandler returns the bep3 module sdk.Querier. +func (am AppModule) NewQuerierHandler() sdk.Querier { + return NewQuerier(am.keeper) +} + +// InitGenesis performs genesis initialization for the bep3 module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the bep3 +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// BeginBlock returns the begin blocker for the bep3 module. +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + BeginBlocker(ctx, am.keeper) +} + +// EndBlock returns the end blocker for the bep3 module. It returns no validator updates. +func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/bep3/spec/README.md b/x/bep3/spec/README.md new file mode 100644 index 00000000..86472c50 --- /dev/null +++ b/x/bep3/spec/README.md @@ -0,0 +1,17 @@ +# bep3 module specification + +## Abstract + + + +## Contents + +// TODO: Create the below files if they are needed. + diff --git a/x/bep3/types/asset.go b/x/bep3/types/asset.go new file mode 100644 index 00000000..51ef07e5 --- /dev/null +++ b/x/bep3/types/asset.go @@ -0,0 +1,50 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// AssetSupply contains information about an asset's supply +type AssetSupply struct { + Denom string `json:"denom" yaml:"denom"` + IncomingSupply sdk.Coin `json:"incoming_supply" yaml:"incoming_supply"` + OutgoingSupply sdk.Coin `json:"outgoing_supply" yaml:"outgoing_supply"` + CurrentSupply sdk.Coin `json:"current_supply" yaml:"current_supply"` + Limit sdk.Coin `json:"limit" yaml:"limit"` +} + +// NewAssetSupply initializes a new AssetSupply +func NewAssetSupply(denom string, incomingSupply, outgoingSupply, currentSupply, limit sdk.Coin) AssetSupply { + return AssetSupply{ + Denom: denom, + IncomingSupply: incomingSupply, + OutgoingSupply: outgoingSupply, + CurrentSupply: currentSupply, + Limit: limit, + } +} + +// String implements stringer +func (a AssetSupply) String() string { + return fmt.Sprintf("Asset Supply"+ + "\n Denom: %s"+ + "\n Incoming supply: %s"+ + "\n Outgoing supply: %s"+ + "\n Current supply: %s"+ + "\n Limit: %s"+ + a.Denom, a.IncomingSupply, a.OutgoingSupply, a.CurrentSupply, a.Limit) +} + +// AssetSupplies is a slice of AssetSupply +type AssetSupplies []AssetSupply + +// String implements stringer +func (supplies AssetSupplies) String() string { + out := "" + for _, supply := range supplies { + out += supply.String() + "\n" + } + return out +} diff --git a/x/bep3/types/codec.go b/x/bep3/types/codec.go new file mode 100644 index 00000000..ec9f2229 --- /dev/null +++ b/x/bep3/types/codec.go @@ -0,0 +1,20 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +var ModuleCdc = codec.New() + +func init() { + RegisterCodec(ModuleCdc) +} + +// RegisterCodec registers concrete types on amino +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*Swap)(nil), nil) + + cdc.RegisterConcrete(MsgCreateAtomicSwap{}, "bep3/MsgCreateAtomicSwap", nil) + cdc.RegisterConcrete(MsgRefundAtomicSwap{}, "bep3/MsgRefundAtomicSwap", nil) + cdc.RegisterConcrete(MsgClaimAtomicSwap{}, "bep3/MsgClaimAtomicSwap", nil) +} diff --git a/x/bep3/types/common_test.go b/x/bep3/types/common_test.go new file mode 100644 index 00000000..11fd749e --- /dev/null +++ b/x/bep3/types/common_test.go @@ -0,0 +1,35 @@ +package types_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/bep3/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +func i(in int64) sdk.Int { return sdk.NewInt(in) } +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } +func ts(minOffset int) int64 { return tmtime.Now().Add(time.Duration(minOffset) * time.Minute).Unix() } + +func atomicSwaps(count int) types.AtomicSwaps { + var swaps types.AtomicSwaps + for i := 0; i < count; i++ { + swap := atomicSwap(i) + swaps = append(swaps, swap) + } + return swaps +} + +func atomicSwap(index int) types.AtomicSwap { + expireOffset := int64((index * 15) + 360) // Default expire height + offet to match timestamp + timestamp := ts(index) // One minute apart + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + + swap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, expireOffset, timestamp, kavaAddrs[0], + kavaAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 0, types.Open, true, types.Incoming) + + return swap +} diff --git a/x/bep3/types/errors.go b/x/bep3/types/errors.go new file mode 100644 index 00000000..06eb951f --- /dev/null +++ b/x/bep3/types/errors.go @@ -0,0 +1,118 @@ +package types + +import ( + "encoding/hex" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// CodeType is the local code type +type CodeType = sdk.CodeType + +const ( + // DefaultCodespace default bep3 codespace + DefaultCodespace sdk.CodespaceType = ModuleName + CodeInvalidTimestamp CodeType = 1 + CodeInvalidHeightSpan CodeType = 2 + CodeAssetNotSupported CodeType = 3 + CodeAssetNotActive CodeType = 4 + CodeAssetSupplyNotFound CodeType = 5 + CodeExceedsSupplyLimit CodeType = 6 + CodeExceedsAvailableSupply CodeType = 7 + CodeInvalidCurrentSupply CodeType = 8 + CodeInvalidIncomingSupply CodeType = 9 + CodeInvalidOutgoingSupply CodeType = 10 + CodeInvalidClaimSecret CodeType = 11 + CodeAtomicSwapAlreadyExists CodeType = 12 + CodeAtomicSwapNotFound CodeType = 13 + CodeSwapNotRefundable CodeType = 14 + CodeSwapNotClaimable CodeType = 15 +) + +// ErrInvalidTimestamp error for when an timestamp is outside of bounds. Assumes block time of 10 seconds. +func ErrInvalidTimestamp(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidTimestamp, fmt.Sprintf("Timestamp can neither be 15 minutes ahead of the current time, nor 30 minutes later")) +} + +// ErrInvalidHeightSpan error a proposed height span is outside of lock time range +func ErrInvalidHeightSpan(codespace sdk.CodespaceType, heightspan int64, minLockTime int64, maxLockTime int64) sdk.Error { + return sdk.NewError(codespace, CodeInvalidHeightSpan, fmt.Sprintf("height span %d is outside acceptable range %d - %d", heightspan, minLockTime, maxLockTime)) +} + +// ErrAssetNotSupported error for when an asset is not supported +func ErrAssetNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error { + return sdk.NewError(codespace, CodeAssetNotSupported, fmt.Sprintf("asset %s is not on the list of supported assets", denom)) +} + +// ErrAssetNotActive error for when an asset is currently inactive +func ErrAssetNotActive(codespace sdk.CodespaceType, denom string) sdk.Error { + return sdk.NewError(codespace, CodeAssetNotActive, fmt.Sprintf("asset %s is currently inactive", denom)) +} + +// ErrAssetSupplyNotFound error for when an asset's supply is not found in the store +func ErrAssetSupplyNotFound(codespace sdk.CodespaceType, denom string) sdk.Error { + return sdk.NewError(codespace, CodeAssetSupplyNotFound, fmt.Sprintf("%s asset supply not found in store", denom)) +} + +// ErrExceedsSupplyLimit error for when the proposed supply increase would put the supply above limit +func ErrExceedsSupplyLimit(codespace sdk.CodespaceType, increase, current, limit sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeExceedsSupplyLimit, + fmt.Sprintf("a supply increase of %s puts current asset supply %s over supply limit %s", increase, current, limit)) +} + +// ErrExceedsAvailableSupply error for when the proposed outgoing amount exceeds the total available supply +func ErrExceedsAvailableSupply(codespace sdk.CodespaceType, increase sdk.Coin, available sdk.Int) sdk.Error { + return sdk.NewError(codespace, CodeExceedsAvailableSupply, + fmt.Sprintf("an outgoing swap with amount %s exceeds total available supply %s", + increase, sdk.NewCoin(increase.Denom, available))) +} + +// ErrInvalidCurrentSupply error for when the proposed decrease would result in a negative current supply +func ErrInvalidCurrentSupply(codespace sdk.CodespaceType, decrease, current sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeInvalidCurrentSupply, + fmt.Sprintf("a supply decrease of %s puts current asset supply %s below 0", decrease, current)) +} + +// ErrInvalidIncomingSupply error for when the proposed decrease would result in a negative incoming supply +func ErrInvalidIncomingSupply(codespace sdk.CodespaceType, decrease, incoming sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeInvalidIncomingSupply, + fmt.Sprintf("a supply decrease of %s puts incoming asset supply %s below 0", decrease, incoming)) +} + +// ErrInvalidOutgoingSupply error for when the proposed decrease would result in a negative outgoing supply +func ErrInvalidOutgoingSupply(codespace sdk.CodespaceType, decrease, outgoing sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeInvalidOutgoingSupply, + fmt.Sprintf("a supply decrease of %s puts outgoing asset supply %s below 0", decrease, outgoing)) +} + +// ErrInvalidClaimSecret error when a submitted secret doesn't match an AtomicSwap's swapID +func ErrInvalidClaimSecret(codespace sdk.CodespaceType, submittedSecret []byte, swapID []byte) sdk.Error { + return sdk.NewError(codespace, CodeInvalidClaimSecret, + fmt.Sprintf("hashed claim attempt %s does not match %s", + hex.EncodeToString(submittedSecret), + hex.EncodeToString(swapID), + ), + ) +} + +// ErrAtomicSwapAlreadyExists error for when an AtomicSwap with this swapID already exists +func ErrAtomicSwapAlreadyExists(codespace sdk.CodespaceType, swapID cmn.HexBytes) sdk.Error { + return sdk.NewError(codespace, CodeAtomicSwapAlreadyExists, fmt.Sprintf("atomic swap %s already exists", swapID)) +} + +// ErrAtomicSwapNotFound error for when an atomic swap is not found +func ErrAtomicSwapNotFound(codespace sdk.CodespaceType, id []byte) sdk.Error { + return sdk.NewError(codespace, CodeAtomicSwapNotFound, fmt.Sprintf("AtomicSwap %s was not found", hex.EncodeToString(id))) +} + +// ErrSwapNotRefundable error for when an AtomicSwap has not expired and cannot be refunded +func ErrSwapNotRefundable(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeSwapNotRefundable, fmt.Sprintf("atomic swap is still active and cannot be refunded")) +} + +// ErrSwapNotClaimable error for when an atomic swap is not open and cannot be claimed +func ErrSwapNotClaimable(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeSwapNotClaimable, fmt.Sprintf("atomic swap is not claimable")) +} diff --git a/x/bep3/types/events.go b/x/bep3/types/events.go new file mode 100644 index 00000000..cb320386 --- /dev/null +++ b/x/bep3/types/events.go @@ -0,0 +1,30 @@ +package types + +// bep3 module event types +const ( + EventTypeCreateAtomicSwap = "createAtomicSwap" + EventTypeDepositAtomicSwap = "depositAtomicSwap" + EventTypeClaimAtomicSwap = "claimAtomicSwap" + EventTypeRefundAtomicSwap = "refundAtomicSwap" + + // Common + AttributeKeySender = "sender" + AttributeKeyRecipient = "recipient" + AttributeKeyAtomicSwapID = "atomic_swap_id" + AttributeKeyRandomNumberHash = "random_number_hash" + AttributeKeyTimestamp = "timestamp" + AttributeKeySenderOtherChain = "sender_other_chain" + AttributeKeyExpireHeight = "expire_height" + AttributeKeyAmount = "amount" + AttributeKeyExpectedIncome = "expected_income" + AttributeKeyDirection = "direction" + + // Claim + AttributeKeyClaimSender = "claim_sender" + AttributeKeyRandomNumber = "random_number" + + // Refund + AttributeKeyRefundSender = "refund_sender" + + AttributeValueCategory = ModuleName +) diff --git a/x/bep3/types/expected_keepers.go b/x/bep3/types/expected_keepers.go new file mode 100644 index 00000000..83141c9e --- /dev/null +++ b/x/bep3/types/expected_keepers.go @@ -0,0 +1,19 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" +) + +// SupplyKeeper defines the expected supply Keeper +type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI + + SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error + + BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error +} diff --git a/x/bep3/types/genesis.go b/x/bep3/types/genesis.go new file mode 100644 index 00000000..a2159e13 --- /dev/null +++ b/x/bep3/types/genesis.go @@ -0,0 +1,66 @@ +package types + +import ( + "bytes" + "encoding/hex" + "fmt" +) + +// GenesisState - all bep3 state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + AtomicSwaps AtomicSwaps `json:"atomic_swaps" yaml:"atomic_swaps"` + AssetSupplies AssetSupplies `json:"assets_supplies" yaml:"assets_supplies"` +} + +// NewGenesisState creates a new GenesisState object +func NewGenesisState(params Params, swaps AtomicSwaps, supplies AssetSupplies) GenesisState { + return GenesisState{ + Params: params, + AtomicSwaps: swaps, + AssetSupplies: supplies, + } +} + +// DefaultGenesisState - default GenesisState used by Cosmos Hub +func DefaultGenesisState() GenesisState { + return NewGenesisState( + DefaultParams(), + AtomicSwaps{}, + AssetSupplies{}, + ) +} + +// Equal checks whether two GenesisState structs are equivalent. +func (gs GenesisState) Equal(gs2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(gs) + b2 := ModuleCdc.MustMarshalBinaryBare(gs2) + return bytes.Equal(b1, b2) +} + +// IsEmpty returns true if a GenesisState is empty. +func (gs GenesisState) IsEmpty() bool { + return gs.Equal(GenesisState{}) +} + +// Validate validates genesis inputs. It returns error if validation of any input fails. +func (gs GenesisState) Validate() error { + if err := gs.Params.Validate(); err != nil { + return err + } + denoms := map[string]bool{} + for _, a := range gs.AssetSupplies { + if denoms[a.Denom] { + return fmt.Errorf("found duplicate asset denom %s", a.Denom) + } + denoms[a.Denom] = true + } + ids := map[string]bool{} + for _, a := range gs.AtomicSwaps { + if ids[hex.EncodeToString(a.GetSwapID())] { + return fmt.Errorf("found duplicate atomic swap ID %s", hex.EncodeToString(a.GetSwapID())) + } + ids[hex.EncodeToString(a.GetSwapID())] = true + } + return nil +} diff --git a/x/bep3/types/genesis_test.go b/x/bep3/types/genesis_test.go new file mode 100644 index 00000000..b72f9be8 --- /dev/null +++ b/x/bep3/types/genesis_test.go @@ -0,0 +1,104 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" +) + +type GenesisTestSuite struct { + suite.Suite + swaps types.AtomicSwaps + supplies types.AssetSupplies +} + +func (suite *GenesisTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + count := 10 + suite.swaps = atomicSwaps(count) + + incomingSupply := int64(count * 50000) + supply := types.NewAssetSupply("bnb", c("bnb", incomingSupply), + c("bnb", 0), c("bnb", 0), c("bnb", 100000000000)) + suite.supplies = types.AssetSupplies{supply} +} + +func (suite *GenesisTestSuite) TestValidate() { + type args struct { + swaps types.AtomicSwaps + supplies types.AssetSupplies + } + testCases := []struct { + name string + args args + expectPass bool + }{ + { + "default", + args{ + swaps: types.AtomicSwaps{}, + supplies: types.AssetSupplies{}, + }, + true, + }, + { + "with swaps", + args{ + swaps: suite.swaps, + supplies: types.AssetSupplies{}, + }, + true, + }, + { + "with supplies", + args{ + swaps: types.AtomicSwaps{}, + supplies: suite.supplies, + }, + true, + }, + { + "duplicate swaps", + args{ + swaps: types.AtomicSwaps{suite.swaps[2], suite.swaps[2]}, + supplies: types.AssetSupplies{}, + }, + false, + }, + { + "duplicate supplies", + args{ + swaps: types.AtomicSwaps{}, + supplies: types.AssetSupplies{suite.supplies[0], suite.supplies[0]}, + }, + false, + }} + + for _, tc := range testCases { + suite.SetupTest() + suite.Run(tc.name, func() { + var gs types.GenesisState + if tc.name == "default" { + gs = types.DefaultGenesisState() + } else { + gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, tc.args.supplies) + } + + err := gs.Validate() + if tc.expectPass { + suite.Nil(err) + } else { + suite.Error(err) + } + }) + } +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/bep3/types/hash.go b/x/bep3/types/hash.go new file mode 100644 index 00000000..bcb23fcf --- /dev/null +++ b/x/bep3/types/hash.go @@ -0,0 +1,48 @@ +package types + +import ( + "crypto/rand" + "encoding/binary" + "errors" + "math/big" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto/tmhash" +) + +// GenerateSecureRandomNumber generates cryptographically strong pseudo-random number +func GenerateSecureRandomNumber() (*big.Int, error) { + max := new(big.Int) + max.Exp(big.NewInt(2), big.NewInt(256), nil) // 256-bits integer i.e. 2^256 + + // Generate number between 0 - max + randomNumber, err := rand.Int(rand.Reader, max) + if err != nil { + return big.NewInt(0), errors.New("random number generation error") + } + + // Catch random numbers that encode to hexadecimal poorly + if len(randomNumber.Text(16)) != 64 { + return GenerateSecureRandomNumber() + } + + return randomNumber, nil +} + +// CalculateRandomHash calculates the hash of a number and timestamp +func CalculateRandomHash(randomNumber []byte, timestamp int64) []byte { + data := make([]byte, RandomNumberLength+Int64Size) + copy(data[:RandomNumberLength], randomNumber) + binary.BigEndian.PutUint64(data[RandomNumberLength:], uint64(timestamp)) + return tmhash.Sum(data) +} + +// CalculateSwapID calculates the hash of a RandomNumberHash, sdk.AccAddress, and string +func CalculateSwapID(randomNumberHash []byte, sender sdk.AccAddress, senderOtherChain string) []byte { + senderOtherChain = strings.ToLower(senderOtherChain) + data := randomNumberHash + data = append(data, []byte(sender)...) + data = append(data, []byte(senderOtherChain)...) + return tmhash.Sum(data) +} diff --git a/x/bep3/types/hash_test.go b/x/bep3/types/hash_test.go new file mode 100644 index 00000000..0161a059 --- /dev/null +++ b/x/bep3/types/hash_test.go @@ -0,0 +1,61 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" +) + +type HashTestSuite struct { + suite.Suite + addrs []sdk.AccAddress + timestamps []int64 +} + +func (suite *HashTestSuite) SetupTest() { + // Generate 10 addresses + _, addrs := app.GeneratePrivKeyAddressPairs(10) + + // Generate 10 timestamps + var timestamps []int64 + for i := 0; i < 10; i++ { + timestamps = append(timestamps, ts(i)) + } + + suite.addrs = addrs + suite.timestamps = timestamps + return +} + +func (suite *HashTestSuite) TestGenerateSecureRandomNumber() { + secureRandomNumber, err := types.GenerateSecureRandomNumber() + suite.Nil(err) + suite.NotNil(secureRandomNumber) + suite.Equal(64, len(secureRandomNumber.Text(16))) +} + +func (suite *HashTestSuite) TestCalculateRandomHash() { + randomNumber, _ := types.GenerateSecureRandomNumber() + hash := types.CalculateRandomHash(randomNumber.Bytes(), suite.timestamps[0]) + suite.NotNil(hash) + suite.Equal(32, len(hash)) +} + +func (suite *HashTestSuite) TestCalculateSwapID() { + randomNumber, _ := types.GenerateSecureRandomNumber() + hash := types.CalculateRandomHash(randomNumber.Bytes(), suite.timestamps[3]) + swapID := types.CalculateSwapID(hash, suite.addrs[3], suite.addrs[5].String()) + suite.NotNil(swapID) + suite.Equal(32, len(swapID)) + + diffHash := types.CalculateRandomHash(randomNumber.Bytes(), suite.timestamps[2]) + diffSwapID := types.CalculateSwapID(diffHash, suite.addrs[3], suite.addrs[5].String()) + suite.NotEqual(swapID, diffSwapID) +} + +func TestHashTestSuite(t *testing.T) { + suite.Run(t, new(HashTestSuite)) +} \ No newline at end of file diff --git a/x/bep3/types/keys.go b/x/bep3/types/keys.go new file mode 100644 index 00000000..fa5316dc --- /dev/null +++ b/x/bep3/types/keys.go @@ -0,0 +1,67 @@ +package types + +import ( + "encoding/binary" + "encoding/hex" +) + +const ( + // ModuleName is the name of the module + ModuleName = "bep3" + + // StoreKey to be used when creating the KVStore + StoreKey = ModuleName + + // RouterKey to be used for routing msgs + RouterKey = ModuleName + + // QuerierRoute is the querier route for bep3 + QuerierRoute = ModuleName + + // DefaultParamspace default namestore + DefaultParamspace = ModuleName +) + +// DefaultLongtermStorageDuration is 1 week (assuming a block time of 7 seconds) +const DefaultLongtermStorageDuration int64 = 86400 + +// Key prefixes +var ( + AtomicSwapKeyPrefix = []byte{0x00} // prefix for keys that store AtomicSwaps + AtomicSwapByBlockPrefix = []byte{0x01} // prefix for keys of the AtomicSwapsByBlock index + AssetSupplyKeyPrefix = []byte{0x02} // prefix for keys that store global asset supply counts + AtomicSwapLongtermStoragePrefix = []byte{0x03} // prefix for keys of the AtomicSwapLongtermStorage index +) + +// GetAtomicSwapByHeightKey is used by the AtomicSwapByBlock index and AtomicSwapLongtermStorage index +func GetAtomicSwapByHeightKey(height int64, swapID []byte) []byte { + return append(Uint64ToBytes(uint64(height)), swapID...) +} + +// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys. +func Uint64ToBytes(id uint64) []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(id)) + return bz +} + +// Uint64FromBytes converts some fixed length bytes back into a uint64. +func Uint64FromBytes(bz []byte) uint64 { + return binary.BigEndian.Uint64(bz) +} + +// BytesToHex converts data from []byte to a hex-encoded string +func BytesToHex(data []byte) string { + encodedData := make([]byte, hex.EncodedLen(len(data))) + hex.Encode(encodedData, data) + return string(encodedData) +} + +// HexToBytes converts data from a hex-encoded string to []bytes +func HexToBytes(data string) ([]byte, error) { + decodedData, err := hex.DecodeString(data) + if err != nil { + return []byte{}, err + } + return decodedData, nil +} diff --git a/x/bep3/types/msg.go b/x/bep3/types/msg.go new file mode 100644 index 00000000..15d84fe4 --- /dev/null +++ b/x/bep3/types/msg.go @@ -0,0 +1,260 @@ +package types + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto" + cmn "github.com/tendermint/tendermint/libs/common" +) + +const ( + CreateAtomicSwap = "createAtomicSwap" + DepositAtomicSwap = "depositAtomicSwap" + ClaimAtomicSwap = "claimAtomicSwap" + RefundAtomicSwap = "refundAtomicSwap" + CalcSwapID = "calcSwapID" + + Int64Size = 8 + RandomNumberHashLength = 32 + RandomNumberLength = 32 + AddrByteCount = 20 + MaxOtherChainAddrLength = 64 + SwapIDLength = 32 + MaxExpectedIncomeLength = 64 +) + +var ( + // kava prefix address: [INSERT BEP3-DEPUTY ADDRESS] + // tkava prefix address: [INSERT BEP3-DEPUTY ADDRESS] + AtomicSwapCoinsAccAddr = sdk.AccAddress(crypto.AddressHash([]byte("KavaAtomicSwapCoins"))) +) + +// MsgCreateAtomicSwap contains an AtomicSwap struct +type MsgCreateAtomicSwap struct { + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + RecipientOtherChain string `json:"recipient_other_chain"` + SenderOtherChain string `json:"sender_other_chain"` + RandomNumberHash cmn.HexBytes `json:"random_number_hash"` + Timestamp int64 `json:"timestamp"` + Amount sdk.Coins `json:"amount"` + ExpectedIncome string `json:"expected_income"` + HeightSpan int64 `json:"height_span"` + CrossChain bool `json:"cross_chain"` +} + +// NewMsgCreateAtomicSwap initializes a new MsgCreateAtomicSwap +func NewMsgCreateAtomicSwap(from sdk.AccAddress, to sdk.AccAddress, recipientOtherChain, + senderOtherChain string, randomNumberHash cmn.HexBytes, timestamp int64, + amount sdk.Coins, expectedIncome string, heightSpan int64, crossChain bool) MsgCreateAtomicSwap { + return MsgCreateAtomicSwap{ + From: from, + To: to, + RecipientOtherChain: recipientOtherChain, + SenderOtherChain: senderOtherChain, + RandomNumberHash: randomNumberHash, + Timestamp: timestamp, + Amount: amount, + ExpectedIncome: expectedIncome, + HeightSpan: heightSpan, + CrossChain: crossChain, + } +} + +// Route establishes the route for the MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) Route() string { return RouterKey } + +// Type is the name of MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) Type() string { return CreateAtomicSwap } + +// String prints the MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) String() string { + return fmt.Sprintf("AtomicSwap{%v#%v#%v#%v#%v#%v#%v#%v#%v#%v}", + msg.From, msg.To, msg.RecipientOtherChain, msg.SenderOtherChain, + msg.RandomNumberHash, msg.Timestamp, msg.Amount, msg.ExpectedIncome, + msg.HeightSpan, msg.CrossChain) +} + +// GetInvolvedAddresses gets the addresses involved in a MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress { + return append(msg.GetSigners(), AtomicSwapCoinsAccAddr) +} + +// GetSigners gets the signers of a MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +// ValidateBasic validates the MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) ValidateBasic() sdk.Error { + if len(msg.From) != AddrByteCount { + return sdk.ErrInternal(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.From))) + } + if len(msg.To) != AddrByteCount { + return sdk.ErrInternal(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.To))) + } + if !msg.CrossChain && len(msg.RecipientOtherChain) != 0 { + return sdk.ErrInternal(fmt.Sprintf("must leave recipient address on other chain to empty for single chain swap")) + } + if !msg.CrossChain && len(msg.SenderOtherChain) != 0 { + return sdk.ErrInternal(fmt.Sprintf("must leave sender address on other chain to empty for single chain swap")) + } + if msg.CrossChain && len(msg.RecipientOtherChain) == 0 { + return sdk.ErrInternal(fmt.Sprintf("missing recipient address on other chain for cross chain swap")) + } + if len(msg.RecipientOtherChain) > MaxOtherChainAddrLength { + return sdk.ErrInternal(fmt.Sprintf("the length of recipient address on other chain should be less than %d", MaxOtherChainAddrLength)) + } + if len(msg.SenderOtherChain) > MaxOtherChainAddrLength { + return sdk.ErrInternal(fmt.Sprintf("the length of sender address on other chain should be less than %d", MaxOtherChainAddrLength)) + } + if len(msg.RandomNumberHash) != RandomNumberHashLength { + return sdk.ErrInternal(fmt.Sprintf("the length of random number hash should be %d", RandomNumberHashLength)) + } + if msg.Timestamp <= 0 { + return sdk.ErrInternal("timestamp must be positive") + } + if !msg.Amount.IsAllPositive() { + return sdk.ErrInternal(fmt.Sprintf("the swapped out coin must be positive")) + } + if len(msg.ExpectedIncome) > MaxExpectedIncomeLength { + return sdk.ErrInternal(fmt.Sprintf("the length of expected income should be less than %d", MaxExpectedIncomeLength)) + } + expectedIncomeCoins, err := sdk.ParseCoins(msg.ExpectedIncome) + if err != nil || expectedIncomeCoins == nil { + return sdk.ErrInternal(fmt.Sprintf("expected income %s must be in valid format e.g. 10000ukava", msg.ExpectedIncome)) + } + if expectedIncomeCoins.IsAnyGT(msg.Amount) { + return sdk.ErrInternal(fmt.Sprintf("expected income %s cannot be greater than amount %s", msg.ExpectedIncome, msg.Amount.String())) + } + if msg.HeightSpan <= 0 { + return sdk.ErrInternal("height span must be positive") + } + return nil +} + +// GetSignBytes gets the sign bytes of a MsgCreateAtomicSwap +func (msg MsgCreateAtomicSwap) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// MsgClaimAtomicSwap defines a AtomicSwap claim +type MsgClaimAtomicSwap struct { + From sdk.AccAddress `json:"from"` + SwapID cmn.HexBytes `json:"swap_id"` + RandomNumber cmn.HexBytes `json:"random_number"` +} + +// NewMsgClaimAtomicSwap initializes a new MsgClaimAtomicSwap +func NewMsgClaimAtomicSwap(from sdk.AccAddress, swapID, randomNumber []byte) MsgClaimAtomicSwap { + return MsgClaimAtomicSwap{ + From: from, + SwapID: swapID, + RandomNumber: randomNumber, + } +} + +// Route establishes the route for the MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) Route() string { return RouterKey } + +// Type is the name of MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) Type() string { return ClaimAtomicSwap } + +// String prints the MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) String() string { + return fmt.Sprintf("claimAtomicSwap{%v#%v#%v}", msg.From, msg.SwapID, msg.RandomNumber) +} + +// GetInvolvedAddresses gets the addresses involved in a MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress { + return append(msg.GetSigners(), AtomicSwapCoinsAccAddr) +} + +// GetSigners gets the signers of a MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +// ValidateBasic validates the MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) ValidateBasic() sdk.Error { + if len(msg.From) != AddrByteCount { + return sdk.ErrInternal(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.From))) + } + if len(msg.SwapID) != SwapIDLength { + return sdk.ErrInternal(fmt.Sprintf("the length of swapID should be %d", SwapIDLength)) + } + if len(msg.RandomNumber) == 0 { + return sdk.ErrInternal("the length of random number cannot be 0") + } + return nil +} + +// GetSignBytes gets the sign bytes of a MsgClaimAtomicSwap +func (msg MsgClaimAtomicSwap) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} + +// MsgRefundAtomicSwap defines a refund msg +type MsgRefundAtomicSwap struct { + From sdk.AccAddress `json:"from"` + SwapID cmn.HexBytes `json:"swap_id"` +} + +// NewMsgRefundAtomicSwap initializes a new MsgRefundAtomicSwap +func NewMsgRefundAtomicSwap(from sdk.AccAddress, swapID []byte) MsgRefundAtomicSwap { + return MsgRefundAtomicSwap{ + From: from, + SwapID: swapID, + } +} + +// Route establishes the route for the MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) Route() string { return RouterKey } + +// Type is the name of MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) Type() string { return RefundAtomicSwap } + +// String prints the MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) String() string { + return fmt.Sprintf("refundAtomicSwap{%v#%v}", msg.From, msg.SwapID) +} + +// GetInvolvedAddresses gets the addresses involved in a MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) GetInvolvedAddresses() []sdk.AccAddress { + return append(msg.GetSigners(), AtomicSwapCoinsAccAddr) +} + +// GetSigners gets the signers of a MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +// ValidateBasic validates the MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) ValidateBasic() sdk.Error { + if len(msg.From) != AddrByteCount { + return sdk.ErrInternal(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(msg.From))) + } + if len(msg.SwapID) != SwapIDLength { + return sdk.ErrInternal(fmt.Sprintf("the length of swapID should be %d", SwapIDLength)) + } + return nil +} + +// GetSignBytes gets the sign bytes of a MsgRefundAtomicSwap +func (msg MsgRefundAtomicSwap) GetSignBytes() []byte { + b, err := json.Marshal(msg) + if err != nil { + panic(err) + } + return b +} diff --git a/x/bep3/types/msg_test.go b/x/bep3/types/msg_test.go new file mode 100644 index 00000000..30cdd2ef --- /dev/null +++ b/x/bep3/types/msg_test.go @@ -0,0 +1,122 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" + cmn "github.com/tendermint/tendermint/libs/common" +) + +var ( + coinsSingle = sdk.NewCoins(sdk.NewInt64Coin("bnb", int64(50000))) + coinsZero = sdk.Coins{sdk.Coin{}} + binanceAddrs = []sdk.AccAddress{ + sdk.AccAddress(crypto.AddressHash([]byte("BinanceTest1"))), + sdk.AccAddress(crypto.AddressHash([]byte("BinanceTest2"))), + } + kavaAddrs = []sdk.AccAddress{ + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))), + sdk.AccAddress(crypto.AddressHash([]byte("KavaTest2"))), + } + randomNumberBytes = []byte{15} + timestampInt64 = int64(100) + randomNumberHash = types.CalculateRandomHash(randomNumberBytes, timestampInt64) +) + +func TestMsgCreateAtomicSwap(t *testing.T) { + tests := []struct { + description string + from sdk.AccAddress + to sdk.AccAddress + recipientOtherChain string + senderOtherChain string + randomNumberHash cmn.HexBytes + timestamp int64 + amount sdk.Coins + expectedIncome string + heightSpan int64 + crossChain bool + expectPass bool + }{ + {"normal", binanceAddrs[0], kavaAddrs[0], "", "", randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 500, false, true}, + {"cross-chain", binanceAddrs[0], kavaAddrs[0], kavaAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 80000, true, true}, + {"with other chain fields", binanceAddrs[0], kavaAddrs[0], kavaAddrs[0].String(), binanceAddrs[0].String(), randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 500, false, false}, + {"cross-cross no other chain fields", binanceAddrs[0], kavaAddrs[0], "", "", randomNumberHash, timestampInt64, coinsSingle, "50000bnb", 500, true, false}, + {"zero coins", binanceAddrs[0], kavaAddrs[0], "", "", randomNumberHash, timestampInt64, coinsZero, "50000bnb", 500, true, false}, + } + + for i, tc := range tests { + msg := types.NewMsgCreateAtomicSwap( + tc.from, + tc.to, + tc.recipientOtherChain, + tc.senderOtherChain, + tc.randomNumberHash, + tc.timestamp, + tc.amount, + tc.expectedIncome, + tc.heightSpan, + tc.crossChain, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgClaimAtomicSwap(t *testing.T) { + swapID := types.CalculateSwapID(randomNumberHash, binanceAddrs[0], "") + + tests := []struct { + description string + from sdk.AccAddress + swapID cmn.HexBytes + randomNumber cmn.HexBytes + expectPass bool + }{ + {"normal", binanceAddrs[0], swapID, randomNumberHash, true}, + } + + for i, tc := range tests { + msg := types.NewMsgClaimAtomicSwap( + tc.from, + tc.swapID, + tc.randomNumber, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgRefundAtomicSwap(t *testing.T) { + swapID := types.CalculateSwapID(randomNumberHash, binanceAddrs[0], "") + + tests := []struct { + description string + from sdk.AccAddress + swapID cmn.HexBytes + expectPass bool + }{ + {"normal", binanceAddrs[0], swapID, true}, + } + + for i, tc := range tests { + msg := types.NewMsgRefundAtomicSwap( + tc.from, + tc.swapID, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} diff --git a/x/bep3/types/params.go b/x/bep3/types/params.go new file mode 100644 index 00000000..cc8cc18f --- /dev/null +++ b/x/bep3/types/params.go @@ -0,0 +1,146 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// Parameter keys +var ( + KeyBnbDeputyAddress = []byte("BnbDeputyAddress") + KeyMinBlockLock = []byte("MinBlockLock") + KeyMaxBlockLock = []byte("MaxBlockLock") + KeySupportedAssets = []byte("SupportedAssets") + + AbsoluteMaximumBlockLock int64 = 10000 + AbsoluteMinimumBlockLock int64 = 50 + DefaultMinBlockLock int64 = 80 + DefaultMaxBlockLock int64 = 600 + DefaultSupportedAssets = AssetParams{ + AssetParam{ + Denom: "bnb", + CoinID: 714, + Limit: sdk.NewInt(100000000000), + Active: true, + }, + } +) + +// Params governance parameters for bep3 module +type Params struct { + BnbDeputyAddress sdk.AccAddress `json:"bnb_deputy_address" yaml:"bnb_deputy_address"` // Bnbchain deputy address + MinBlockLock int64 `json:"min_block_lock" yaml:"min_block_lock"` // AtomicSwap minimum block lock + MaxBlockLock int64 `json:"max_block_lock" yaml:"max_block_lock"` // AtomicSwap maximum block lock + SupportedAssets AssetParams `json:"supported_assets" yaml:"supported_assets"` // Supported assets +} + +// String implements fmt.Stringer +func (p Params) String() string { + return fmt.Sprintf(`Params: + Bnbchain deputy address: %s, + Min block lock: %d, + Max block lock: %d, + Supported assets: %s`, + p.BnbDeputyAddress.String(), p.MinBlockLock, p.MaxBlockLock, p.SupportedAssets) +} + +// NewParams returns a new params object +func NewParams(bnbDeputyAddress sdk.AccAddress, minBlockLock, maxBlockLock int64, supportedAssets AssetParams, +) Params { + return Params{ + BnbDeputyAddress: bnbDeputyAddress, + MinBlockLock: minBlockLock, + MaxBlockLock: maxBlockLock, + SupportedAssets: supportedAssets, + } +} + +// DefaultParams returns default params for bep3 module +func DefaultParams() Params { + defaultBnbDeputyAddress, _ := sdk.AccAddressFromBech32("kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj") + return NewParams(defaultBnbDeputyAddress, DefaultMinBlockLock, DefaultMaxBlockLock, DefaultSupportedAssets) +} + +// AssetParam governance parameters for each asset within a supported chain +type AssetParam struct { + Denom string `json:"denom" yaml:"denom"` // name of the asset + CoinID int `json:"coin_id" yaml:"coin_id"` // internationally recognized coin ID + Limit sdk.Int `json:"limit" yaml:"limit"` // asset supply limit + Active bool `json:"active" yaml:"active"` // denotes if asset is available or paused +} + +// String implements fmt.Stringer +func (ap AssetParam) String() string { + return fmt.Sprintf(`Asset: + Denom: %s + Coin ID: %d + Limit: %s + Active: %t`, + ap.Denom, ap.CoinID, ap.Limit.String(), ap.Active) +} + +// AssetParams array of AssetParam +type AssetParams []AssetParam + +// String implements fmt.Stringer +func (aps AssetParams) String() string { + out := "Asset Params\n" + for _, ap := range aps { + out += fmt.Sprintf("%s\n", ap) + } + return out +} + +// ParamKeyTable Key declaration for parameters +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) +} + +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs +// pairs of bep3 module's parameters. +// nolint +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyBnbDeputyAddress, Value: &p.BnbDeputyAddress}, + {Key: KeyMinBlockLock, Value: &p.MinBlockLock}, + {Key: KeyMaxBlockLock, Value: &p.MaxBlockLock}, + {Key: KeySupportedAssets, Value: &p.SupportedAssets}, + } +} + +// Validate ensure that params have valid values +func (p Params) Validate() error { + if p.MinBlockLock < AbsoluteMinimumBlockLock { + return fmt.Errorf(fmt.Sprintf("minimum block lock cannot be less than %d", AbsoluteMinimumBlockLock)) + } + if p.MinBlockLock >= p.MaxBlockLock { + return fmt.Errorf("maximum block lock must be greater than minimum block lock") + } + if p.MaxBlockLock > AbsoluteMaximumBlockLock { + return fmt.Errorf(fmt.Sprintf("maximum block lock cannot be greater than %d", AbsoluteMaximumBlockLock)) + } + coinIDs := make(map[int]bool) + coinDenoms := make(map[string]bool) + for _, asset := range p.SupportedAssets { + if len(asset.Denom) == 0 { + return fmt.Errorf("asset denom cannot be empty") + } + if asset.CoinID < 0 { + return fmt.Errorf(fmt.Sprintf("asset %s must be a positive integer", asset.Denom)) + } + if !asset.Limit.IsPositive() { + return fmt.Errorf(fmt.Sprintf("asset %s must have a positive supply limit", asset.Denom)) + } + if coinDenoms[asset.Denom] { + return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate denom", asset.Denom)) + } + coinDenoms[asset.Denom] = true + if coinIDs[asset.CoinID] { + return fmt.Errorf(fmt.Sprintf("asset %s cannot have duplicate coin id %d", asset.Denom, asset.CoinID)) + } + coinIDs[asset.CoinID] = true + } + return nil +} diff --git a/x/bep3/types/params_test.go b/x/bep3/types/params_test.go new file mode 100644 index 00000000..26a2e7e8 --- /dev/null +++ b/x/bep3/types/params_test.go @@ -0,0 +1,217 @@ +package types_test + +import ( + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" +) + +type ParamsTestSuite struct { + suite.Suite + addr sdk.AccAddress +} + +func (suite *ParamsTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + _, addrs := app.GeneratePrivKeyAddressPairs(1) + suite.addr = addrs[0] + return +} + +func (suite *ParamsTestSuite) TestParamValidation() { + type LoadParams func() types.Params + + type args struct { + bnbDeputyAddress sdk.AccAddress + minBlockLock int64 + maxBlockLock int64 + supportedAssets types.AssetParams + } + + testCases := []struct { + name string + args args + expectPass bool + expectedErr string + }{ + { + name: "default", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.DefaultSupportedAssets, + }, + expectPass: true, + expectedErr: "", + }, + { + name: "minimum block lock below limit", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: 1, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.DefaultSupportedAssets, + }, + expectPass: false, + expectedErr: "minimum block lock cannot be less than", + }, + { + name: "minimum block lock above limit", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: 500000, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.DefaultSupportedAssets, + }, + expectPass: false, + expectedErr: "maximum block lock must be greater than minimum block lock", + }, + { + name: "maximum block lock below limit", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: 1, + supportedAssets: types.DefaultSupportedAssets, + }, + expectPass: false, + expectedErr: "maximum block lock must be greater than minimum block lock", + }, + { + name: "maximum block lock above limit", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: 100000000, + supportedAssets: types.DefaultSupportedAssets, + }, + expectPass: false, + expectedErr: "maximum block lock cannot be greater than", + }, + { + name: "empty asset denom", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.AssetParams{ + types.AssetParam{ + Denom: "", + CoinID: 714, + Limit: sdk.NewInt(100000000000), + Active: true, + }, + }, + }, + expectPass: false, + expectedErr: "asset denom cannot be empty", + }, + { + name: "negative asset coin ID", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.AssetParams{ + types.AssetParam{ + Denom: "bnb", + CoinID: -1, + Limit: sdk.NewInt(100000000000), + Active: true, + }, + }, + }, + expectPass: false, + expectedErr: "must be a positive integer", + }, + { + name: "negative asset limit", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.AssetParams{ + types.AssetParam{ + Denom: "bnb", + CoinID: 714, + Limit: sdk.NewInt(-10000), + Active: true, + }, + }, + }, + expectPass: false, + expectedErr: "must have a positive supply limit", + }, + { + name: "duplicate asset denom", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.AssetParams{ + types.AssetParam{ + Denom: "bnb", + CoinID: 714, + Limit: sdk.NewInt(100000000000), + Active: true, + }, + types.AssetParam{ + Denom: "bnb", + CoinID: 114, + Limit: sdk.NewInt(500000000), + Active: false, + }, + }, + }, + expectPass: false, + expectedErr: "cannot have duplicate denom", + }, + { + name: "duplicate asset coin ID", + args: args{ + bnbDeputyAddress: suite.addr, + minBlockLock: types.DefaultMinBlockLock, + maxBlockLock: types.DefaultMaxBlockLock, + supportedAssets: types.AssetParams{ + types.AssetParam{ + Denom: "bnb", + CoinID: 714, + Limit: sdk.NewInt(100000000000), + Active: true, + }, + types.AssetParam{ + Denom: "fake", + CoinID: 714, + Limit: sdk.NewInt(500000000), + Active: false, + }, + }, + }, + expectPass: false, + expectedErr: "cannot have duplicate coin id", + }, + } + + for _, tc := range testCases { + params := types.NewParams(tc.args.bnbDeputyAddress, tc.args.minBlockLock, + tc.args.maxBlockLock, tc.args.supportedAssets) + + err := params.Validate() + if tc.expectPass { + suite.Nil(err) + } else { + suite.NotNil(err) + suite.True(strings.Contains(err.Error(), tc.expectedErr)) + } + } +} + +func TestParamsTestSuite(t *testing.T) { + suite.Run(t, new(ParamsTestSuite)) +} diff --git a/x/bep3/types/querier.go b/x/bep3/types/querier.go new file mode 100644 index 00000000..8a88456a --- /dev/null +++ b/x/bep3/types/querier.go @@ -0,0 +1,54 @@ +package types + +import ( + cmn "github.com/tendermint/tendermint/libs/common" +) + +const ( + // QueryGetAssetSupply command for getting info about an asset's supply + QueryGetAssetSupply = "supply" + // QueryGetAtomicSwap command for getting info about an atomic swap + QueryGetAtomicSwap = "swap" + // QueryGetAtomicSwaps command for getting a list of atomic swaps + QueryGetAtomicSwaps = "swaps" + // QueryGetParams command for getting module params + QueryGetParams = "parameters" +) + +// QueryAssetSupply contains the params for query 'custom/bep3/supply' +type QueryAssetSupply struct { + Denom cmn.HexBytes `json:"denom" yaml:"denom"` +} + +// NewQueryAssetSupply creates a new QueryAssetSupply +func NewQueryAssetSupply(denom cmn.HexBytes) QueryAssetSupply { + return QueryAssetSupply{ + Denom: denom, + } +} + +// QueryAtomicSwapByID contains the params for query 'custom/bep3/swap' +type QueryAtomicSwapByID struct { + SwapID cmn.HexBytes `json:"swap_id" yaml:"swap_id"` +} + +// NewQueryAtomicSwapByID creates a new QueryAtomicSwapByID +func NewQueryAtomicSwapByID(swapBytes cmn.HexBytes) QueryAtomicSwapByID { + return QueryAtomicSwapByID{ + SwapID: swapBytes, + } +} + +// QueryAtomicSwaps contains the params for an AtomicSwaps query +type QueryAtomicSwaps struct { + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` +} + +// NewQueryAtomicSwaps creates a new QueryAtomicSwaps +func NewQueryAtomicSwaps(page int, limit int) QueryAtomicSwaps { + return QueryAtomicSwaps{ + Page: page, + Limit: limit, + } +} diff --git a/x/bep3/types/swap.go b/x/bep3/types/swap.go new file mode 100644 index 00000000..1a7dbacb --- /dev/null +++ b/x/bep3/types/swap.go @@ -0,0 +1,219 @@ +package types + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// Swap is an interface for handling common actions +type Swap interface { + GetSwapID() cmn.HexBytes + GetModuleAccountCoins() sdk.Coins + Validate() error +} + +// AtomicSwap contains the information for an atomic swap +type AtomicSwap struct { + Swap `json:"swap" yaml:"swap"` + Amount sdk.Coins `json:"amount" yaml:"amount"` + RandomNumberHash cmn.HexBytes `json:"random_number_hash" yaml:"random_number_hash"` + ExpireHeight int64 `json:"expire_height" yaml:"expire_height"` + Timestamp int64 `json:"timestamp" yaml:"timestamp"` + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Recipient sdk.AccAddress `json:"recipient" yaml:"recipient"` + SenderOtherChain string `json:"sender_other_chain" yaml:"sender_other_chain"` + RecipientOtherChain string `json:"recipient_other_chain" yaml:"recipient_other_chain"` + ClosedBlock int64 `json:"closed_block" yaml:"closed_block"` + Status SwapStatus `json:"status" yaml:"status"` + CrossChain bool `json:"cross_chain" yaml:"cross_chain"` + Direction SwapDirection `json:"direction" yaml:"direction"` +} + +// NewAtomicSwap returns a new AtomicSwap +func NewAtomicSwap(amount sdk.Coins, randomNumberHash cmn.HexBytes, expireHeight, timestamp int64, sender, + recipient sdk.AccAddress, senderOtherChain string, recipientOtherChain string, closedBlock int64, + status SwapStatus, crossChain bool, direction SwapDirection) AtomicSwap { + return AtomicSwap{ + Amount: amount, + RandomNumberHash: randomNumberHash, + ExpireHeight: expireHeight, + Timestamp: timestamp, + Sender: sender, + Recipient: recipient, + SenderOtherChain: senderOtherChain, + RecipientOtherChain: recipientOtherChain, + ClosedBlock: closedBlock, + Status: status, + CrossChain: crossChain, + Direction: direction, + } +} + +// GetSwapID calculates the ID of an atomic swap +func (a AtomicSwap) GetSwapID() cmn.HexBytes { + return CalculateSwapID(a.RandomNumberHash, a.Sender, a.SenderOtherChain) +} + +// GetModuleAccountCoins returns the swap's amount as sdk.Coins +func (a AtomicSwap) GetModuleAccountCoins() sdk.Coins { + return sdk.NewCoins(a.Amount...) +} + +// Validate verifies that recipient is not empty +func (a AtomicSwap) Validate() error { + if len(a.Sender) != AddrByteCount { + return fmt.Errorf(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Sender))) + } + if len(a.Recipient) != AddrByteCount { + return fmt.Errorf(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Recipient))) + } + if len(a.RandomNumberHash) != RandomNumberHashLength { + return fmt.Errorf(fmt.Sprintf("the length of random number hash should be %d", RandomNumberHashLength)) + } + if !a.Amount.IsAllPositive() { + return fmt.Errorf(fmt.Sprintf("the swapped out coin must be positive")) + } + return nil +} + +// String implements stringer +func (a AtomicSwap) String() string { + return fmt.Sprintf("Atomic Swap"+ + "\n ID: %s"+ + "\n Status: %s"+ + "\n Amount: %s"+ + "\n Random number hash: %s"+ + "\n Expire height: %d"+ + "\n Timestamp: %d"+ + "\n Sender: %s"+ + "\n Recipient: %s"+ + "\n Sender other chain: %s"+ + "\n Recipient other chain: %s"+ + "\n Closed block: %d"+ + "\n Cross chain: %t"+ + "\n Direction: %s", + a.GetSwapID(), a.Status.String(), a.Amount.String(), + hex.EncodeToString(a.RandomNumberHash), a.ExpireHeight, + a.Timestamp, a.Sender.String(), a.Recipient.String(), + a.SenderOtherChain, a.RecipientOtherChain, a.ClosedBlock, + a.CrossChain, a.Direction) +} + +// AtomicSwaps is a slice of AtomicSwap +type AtomicSwaps []AtomicSwap + +// String implements stringer +func (swaps AtomicSwaps) String() string { + out := "" + for _, swap := range swaps { + out += swap.String() + "\n" + } + return out +} + +// SwapStatus is the status of an AtomicSwap +type SwapStatus byte + +const ( + NULL SwapStatus = 0x00 + Open SwapStatus = 0x01 + Completed SwapStatus = 0x02 + Expired SwapStatus = 0x03 +) + +// NewSwapStatusFromString converts string to SwapStatus type +func NewSwapStatusFromString(str string) SwapStatus { + switch str { + case "Open", "open": + return Open + case "Completed", "completed": + return Completed + case "Expired", "expired": + return Expired + default: + return NULL + } +} + +// String returns the string representation of a SwapStatus +func (status SwapStatus) String() string { + switch status { + case Open: + return "Open" + case Completed: + return "Completed" + case Expired: + return "Expired" + default: + return "NULL" + } +} + +// MarshalJSON marshals the SwapStatus +func (status SwapStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(status.String()) +} + +// UnmarshalJSON unmarshals the SwapStatus +func (status *SwapStatus) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + *status = NewSwapStatusFromString(s) + return nil +} + +// SwapDirection is the direction of an AtomicSwap +type SwapDirection byte + +const ( + INVALID SwapDirection = 0x00 + Incoming SwapDirection = 0x01 + Outgoing SwapDirection = 0x02 +) + +// NewSwapDirectionFromString converts string to SwapDirection type +func NewSwapDirectionFromString(str string) SwapDirection { + switch str { + case "Incoming", "incoming", "inc", "I", "i": + return Incoming + case "Outgoing", "outgoing", "out", "O", "o": + return Outgoing + default: + return INVALID + } +} + +// String returns the string representation of a SwapDirection +func (direction SwapDirection) String() string { + switch direction { + case Incoming: + return "Incoming" + case Outgoing: + return "Outgoing" + default: + return "INVALID" + } +} + +// MarshalJSON marshals the SwapDirection +func (direction SwapDirection) MarshalJSON() ([]byte, error) { + return json.Marshal(direction.String()) +} + +// UnmarshalJSON unmarshals the SwapDirection +func (direction *SwapDirection) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + *direction = NewSwapDirectionFromString(s) + return nil +} diff --git a/x/bep3/types/swap_test.go b/x/bep3/types/swap_test.go new file mode 100644 index 00000000..db22dab0 --- /dev/null +++ b/x/bep3/types/swap_test.go @@ -0,0 +1,140 @@ +package types_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/bep3/types" + "github.com/stretchr/testify/suite" + cmn "github.com/tendermint/tendermint/libs/common" +) + +type AtomicSwapTestSuite struct { + suite.Suite + addrs []sdk.AccAddress + timestamps []int64 + randomNumberHashes []cmn.HexBytes +} + +func (suite *AtomicSwapTestSuite) SetupTest() { + // Generate 10 addresses + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + _, addrs := app.GeneratePrivKeyAddressPairs(10) + + // Generate 10 timestamps and random number hashes + var timestamps []int64 + var randomNumberHashes []cmn.HexBytes + for i := 0; i < 10; i++ { + timestamp := ts(i) + randomNumber, _ := types.GenerateSecureRandomNumber() + randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) + timestamps = append(timestamps, timestamp) + randomNumberHashes = append(randomNumberHashes, randomNumberHash) + } + + suite.addrs = addrs + suite.timestamps = timestamps + suite.randomNumberHashes = randomNumberHashes + return +} + +func (suite *AtomicSwapTestSuite) TestNewAtomicSwap() { + type args struct { + amount sdk.Coins + randomNumberHash cmn.HexBytes + expireHeight int64 + timestamp int64 + sender sdk.AccAddress + recipient sdk.AccAddress + recipientOtherChain string + senderOtherChain string + closedBlock int64 + status types.SwapStatus + crossChain bool + direction types.SwapDirection + } + testCases := []struct { + description string + args args + expectPass bool + }{ + { + "normal", + args{ + amount: cs(c("bnb", 50000)), + randomNumberHash: suite.randomNumberHashes[0], + expireHeight: int64(360), + timestamp: suite.timestamps[0], + sender: suite.addrs[0], + recipient: suite.addrs[5], + recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + closedBlock: 0, + status: types.Open, + crossChain: true, + direction: types.Incoming, + }, + true, + }, + { + "invalid random number hash length", + args{ + amount: cs(c("bnb", 50000)), + randomNumberHash: suite.randomNumberHashes[1][0:20], + expireHeight: int64(360), + timestamp: suite.timestamps[1], + sender: suite.addrs[1], + recipient: suite.addrs[5], + recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + closedBlock: 0, + status: types.Open, + crossChain: true, + direction: types.Incoming, + }, + false, + }, + { + "invalid amount", + args{ + amount: cs(c("bnb", 0)), + randomNumberHash: suite.randomNumberHashes[2], + expireHeight: int64(360), + timestamp: suite.timestamps[2], + sender: suite.addrs[2], + recipient: suite.addrs[5], + recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + closedBlock: 0, + status: types.Open, + crossChain: true, + direction: types.Incoming, + }, + false, + }, + } + + for _, tc := range testCases { + // Create atomic swap + swap := types.NewAtomicSwap(tc.args.amount, tc.args.randomNumberHash, tc.args.expireHeight, + tc.args.timestamp, tc.args.sender, tc.args.recipient, tc.args.senderOtherChain, + tc.args.recipientOtherChain, tc.args.closedBlock, tc.args.status, tc.args.crossChain, + tc.args.direction) + + if tc.expectPass { + suite.Nil(swap.Validate()) + suite.Equal(tc.args.amount, swap.GetModuleAccountCoins()) + expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain) + suite.Equal(cmn.HexBytes(expectedSwapID), swap.GetSwapID()) + } else { + suite.Error(swap.Validate()) + } + + } +} + +func TestAtomicSwapTestSuite(t *testing.T) { + suite.Run(t, new(AtomicSwapTestSuite)) +} diff --git a/x/cdp/abci.go b/x/cdp/abci.go index cc36bd74..f31e81ee 100644 --- a/x/cdp/abci.go +++ b/x/cdp/abci.go @@ -15,13 +15,31 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { if !found { previousBlockTime = ctx.BlockTime() } - timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix()) + previousDistTime, found := k.GetPreviousSavingsDistribution(ctx) + if !found { + previousDistTime = ctx.BlockTime() + k.SetPreviousSavingsDistribution(ctx, previousDistTime) + } + blockTimeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix()) for _, cp := range params.CollateralParams { for _, dp := range params.DebtParams { - k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed) + k.HandleNewDebt(ctx, cp.Denom, dp.Denom, blockTimeElapsed) } - err := k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) + // call our update fees method for the risky cdps + err := k.UpdateFeesForRiskyCdps(ctx, cp.Denom, cp.MarketID) + // handle if an error is returned then propagate up + if err != nil { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBeginBlockerFatal, + sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)), + sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)), + ), + ) + } + + err = k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) if err != nil { ctx.EventManager().EmitEvent( sdk.NewEvent( @@ -42,6 +60,22 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { ), ) } + distTimeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistTime.Unix()) + if distTimeElapsed.GTE(sdk.NewInt(int64(params.SavingsDistributionFrequency.Seconds()))) { + for _, dp := range params.DebtParams { + err := k.DistributeSavingsRate(ctx, dp.Denom) + if err != nil { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBeginBlockerFatal, + sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)), + sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)), + ), + ) + } + } + k.SetPreviousSavingsDistribution(ctx, ctx.BlockTime()) + } k.SetPreviousBlockTime(ctx, ctx.BlockTime()) return } diff --git a/x/cdp/alias.go b/x/cdp/alias.go index d542be32..7cae19d8 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -1,8 +1,8 @@ // nolint // autogenerated code using github.com/rigelrozanski/multitool // aliases generated for the following subdirectories: -// ALIASGEN: github.com/kava-labs/kava/x/cdp/types/ -// ALIASGEN: github.com/kava-labs/kava/x/cdp/keeper/ +// ALIASGEN: github.com/kava-labs/kava/x/cdp/keeper +// ALIASGEN: github.com/kava-labs/kava/x/cdp/types package cdp import ( @@ -11,6 +11,7 @@ import ( ) const ( + BaseDigitFactor = keeper.BaseDigitFactor DefaultCodespace = types.DefaultCodespace CodeCdpAlreadyExists = types.CodeCdpAlreadyExists CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid @@ -47,7 +48,9 @@ const ( QuerierRoute = types.QuerierRoute DefaultParamspace = types.DefaultParamspace LiquidatorMacc = types.LiquidatorMacc + SavingsRateMacc = types.SavingsRateMacc QueryGetCdp = types.QueryGetCdp + QueryGetCdpDeposits = types.QueryGetCdpDeposits QueryGetCdps = types.QueryGetCdps QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization QueryGetParams = types.QueryGetParams @@ -58,7 +61,10 @@ const ( var ( // functions aliases + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier NewCDP = types.NewCDP + NewAugmentedCDP = types.NewAugmentedCDP RegisterCodec = types.RegisterCodec NewDeposit = types.NewDeposit ErrCdpAlreadyExists = types.ErrCdpAlreadyExists @@ -105,45 +111,49 @@ var ( ParamKeyTable = types.ParamKeyTable NewQueryCdpsParams = types.NewQueryCdpsParams NewQueryCdpParams = types.NewQueryCdpParams + NewQueryCdpDeposits = types.NewQueryCdpDeposits NewQueryCdpsByRatioParams = types.NewQueryCdpsByRatioParams ValidSortableDec = types.ValidSortableDec SortableDecBytes = types.SortableDecBytes ParseDecBytes = types.ParseDecBytes RelativePow = types.RelativePow - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - CdpIDKeyPrefix = types.CdpIDKeyPrefix - CdpKeyPrefix = types.CdpKeyPrefix - CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix - CdpIDKey = types.CdpIDKey - DebtDenomKey = types.DebtDenomKey - GovDenomKey = types.GovDenomKey - DepositKeyPrefix = types.DepositKeyPrefix - PrincipalKeyPrefix = types.PrincipalKeyPrefix - PreviousBlockTimeKey = types.PreviousBlockTimeKey - KeyGlobalDebtLimit = types.KeyGlobalDebtLimit - KeyCollateralParams = types.KeyCollateralParams - KeyDebtParams = types.KeyDebtParams - KeyCircuitBreaker = types.KeyCircuitBreaker - KeyDebtThreshold = types.KeyDebtThreshold - KeySurplusThreshold = types.KeySurplusThreshold - DefaultGlobalDebt = types.DefaultGlobalDebt - DefaultCircuitBreaker = types.DefaultCircuitBreaker - DefaultCollateralParams = types.DefaultCollateralParams - DefaultDebtParams = types.DefaultDebtParams - DefaultCdpStartingID = types.DefaultCdpStartingID - DefaultDebtDenom = types.DefaultDebtDenom - DefaultGovDenom = types.DefaultGovDenom - DefaultSurplusThreshold = types.DefaultSurplusThreshold - DefaultDebtThreshold = types.DefaultDebtThreshold - DefaultPreviousBlockTime = types.DefaultPreviousBlockTime - MaxSortableDec = types.MaxSortableDec + ModuleCdc = types.ModuleCdc + CdpIDKeyPrefix = types.CdpIDKeyPrefix + CdpKeyPrefix = types.CdpKeyPrefix + CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix + CdpIDKey = types.CdpIDKey + DebtDenomKey = types.DebtDenomKey + GovDenomKey = types.GovDenomKey + DepositKeyPrefix = types.DepositKeyPrefix + PrincipalKeyPrefix = types.PrincipalKeyPrefix + PreviousBlockTimeKey = types.PreviousBlockTimeKey + PreviousDistributionTimeKey = types.PreviousDistributionTimeKey + KeyGlobalDebtLimit = types.KeyGlobalDebtLimit + KeyCollateralParams = types.KeyCollateralParams + KeyDebtParams = types.KeyDebtParams + KeyDistributionFrequency = types.KeyDistributionFrequency + KeyCircuitBreaker = types.KeyCircuitBreaker + KeyDebtThreshold = types.KeyDebtThreshold + KeySurplusThreshold = types.KeySurplusThreshold + DefaultGlobalDebt = types.DefaultGlobalDebt + DefaultCircuitBreaker = types.DefaultCircuitBreaker + DefaultCollateralParams = types.DefaultCollateralParams + DefaultDebtParams = types.DefaultDebtParams + DefaultCdpStartingID = types.DefaultCdpStartingID + DefaultDebtDenom = types.DefaultDebtDenom + DefaultGovDenom = types.DefaultGovDenom + DefaultSurplusThreshold = types.DefaultSurplusThreshold + DefaultDebtThreshold = types.DefaultDebtThreshold + DefaultPreviousBlockTime = types.DefaultPreviousBlockTime + DefaultPreviousDistributionTime = types.DefaultPreviousDistributionTime + DefaultSavingsDistributionFrequency = types.DefaultSavingsDistributionFrequency + MaxSortableDec = types.MaxSortableDec ) type ( + Keeper = keeper.Keeper CDP = types.CDP CDPs = types.CDPs AugmentedCDP = types.AugmentedCDP @@ -165,6 +175,6 @@ type ( DebtParams = types.DebtParams QueryCdpsParams = types.QueryCdpsParams QueryCdpParams = types.QueryCdpParams + QueryCdpDeposits = types.QueryCdpDeposits QueryCdpsByRatioParams = types.QueryCdpsByRatioParams - Keeper = keeper.Keeper ) diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index 45e34b46..0a4e6880 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -7,11 +7,26 @@ import ( ) // InitGenesis sets initial genesis state for cdp module -func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) { +func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, sk SupplyKeeper, gs GenesisState) { + if err := gs.Validate(); err != nil { panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) } + // check if the module accounts exists + cdpModuleAcc := sk.GetModuleAccount(ctx, ModuleName) + if cdpModuleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", ModuleName)) + } + liqModuleAcc := sk.GetModuleAccount(ctx, LiquidatorMacc) + if liqModuleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", LiquidatorMacc)) + } + savingsRateMacc := sk.GetModuleAccount(ctx, SavingsRateMacc) + if savingsRateMacc == nil { + panic(fmt.Sprintf("%s module account has not been set", SavingsRateMacc)) + } + // validate denoms - check that any collaterals in the params are in the pricefeed, // pricefeed MUST call InitGenesis before cdp collateralMap := make(map[string]int) @@ -84,6 +99,10 @@ func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { if !found { previousBlockTime = DefaultPreviousBlockTime } + previousDistributionTime, found := k.GetPreviousSavingsDistribution(ctx) + if !found { + previousDistributionTime = DefaultPreviousDistributionTime + } - return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousBlockTime) + return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousBlockTime, previousDistributionTime) } diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index 8ea36252..68c644ee 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -39,9 +39,10 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), - SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, - DebtAuctionThreshold: cdp.DefaultDebtThreshold, + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, + SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency, CollateralParams: cdp.CollateralParams{ { Denom: asset, @@ -61,14 +62,16 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { ReferenceAsset: "usd", ConversionFactor: i(6), DebtFloor: i(10000000), + SavingsRate: d("0.95"), }, }, }, - StartingCdpID: cdp.DefaultCdpStartingID, - DebtDenom: cdp.DefaultDebtDenom, - GovDenom: cdp.DefaultGovDenom, - CDPs: cdp.CDPs{}, - PreviousBlockTime: cdp.DefaultPreviousBlockTime, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + PreviousDistributionTime: cdp.DefaultPreviousDistributionTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } @@ -101,9 +104,10 @@ func NewPricefeedGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), - SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, - DebtAuctionThreshold: cdp.DefaultDebtThreshold, + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, + SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency, CollateralParams: cdp.CollateralParams{ { Denom: "xrp", @@ -134,20 +138,23 @@ func NewCDPGenStateMulti() app.GenesisState { ReferenceAsset: "usd", ConversionFactor: i(6), DebtFloor: i(10000000), + SavingsRate: d("0.95"), }, { Denom: "susd", ReferenceAsset: "usd", ConversionFactor: i(6), DebtFloor: i(10000000), + SavingsRate: d("0.95"), }, }, }, - StartingCdpID: cdp.DefaultCdpStartingID, - DebtDenom: cdp.DefaultDebtDenom, - GovDenom: cdp.DefaultGovDenom, - CDPs: cdp.CDPs{}, - PreviousBlockTime: cdp.DefaultPreviousBlockTime, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + PreviousDistributionTime: cdp.DefaultPreviousDistributionTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } @@ -207,6 +214,9 @@ func badGenStates() []badGenState { g13 := baseGenState() g13.GovDenom = "" + g14 := baseGenState() + g14.Params.DebtParams[0].SavingsRate = d("4.0") + return []badGenState{ badGenState{Genesis: g1, Reason: "duplicate collateral denom"}, badGenState{Genesis: g2, Reason: "duplicate collateral prefix"}, @@ -220,15 +230,17 @@ func badGenStates() []badGenState { badGenState{Genesis: g11, Reason: "negative auction size"}, badGenState{Genesis: g12, Reason: "invalid liquidation penalty"}, badGenState{Genesis: g13, Reason: "gov denom not set"}, + badGenState{Genesis: g14, Reason: "invalid savings rate"}, } } func baseGenState() cdp.GenesisState { return cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), - SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, - DebtAuctionThreshold: cdp.DefaultDebtThreshold, + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, + SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency, CollateralParams: cdp.CollateralParams{ { Denom: "xrp", @@ -264,10 +276,11 @@ func baseGenState() cdp.GenesisState { }, }, }, - StartingCdpID: cdp.DefaultCdpStartingID, - DebtDenom: cdp.DefaultDebtDenom, - GovDenom: cdp.DefaultGovDenom, - CDPs: cdp.CDPs{}, - PreviousBlockTime: cdp.DefaultPreviousBlockTime, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + PreviousDistributionTime: cdp.DefaultPreviousDistributionTime, } } diff --git a/x/cdp/keeper/auctions.go b/x/cdp/keeper/auctions.go index 6e39bbbb..1d74f6ba 100644 --- a/x/cdp/keeper/auctions.go +++ b/x/cdp/keeper/auctions.go @@ -220,11 +220,12 @@ func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) sdk.Error { return err } } - remainingSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc) - if remainingSurplus.GTE(params.SurplusAuctionThreshold) { - for _, dp := range params.DebtParams { - surplusLot := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom) - _, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(dp.Denom, surplusLot), k.GetGovDenom(ctx)) + + for _, dp := range params.DebtParams { + surplus := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom) + if surplus.GTE(params.SurplusAuctionThreshold) { + surplusLot := sdk.NewCoin(dp.Denom, surplus) + _, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, surplusLot, k.GetGovDenom(ctx)) if err != nil { return err } diff --git a/x/cdp/keeper/cdp_test.go b/x/cdp/keeper/cdp_test.go index 89e35136..a76dd8a2 100644 --- a/x/cdp/keeper/cdp_test.go +++ b/x/cdp/keeper/cdp_test.go @@ -114,6 +114,7 @@ func (suite *CdpTestSuite) TestGetSetCdp() { _, addrs := app.GeneratePrivKeyAddressPairs(1) cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now())) suite.keeper.SetCDP(suite.ctx, cdp) + t, found := suite.keeper.GetCDP(suite.ctx, "xrp", types.DefaultCdpStartingID) suite.True(found) suite.Equal(cdp, t) diff --git a/x/cdp/keeper/fees.go b/x/cdp/keeper/fees.go index 0c5671f6..53af1138 100644 --- a/x/cdp/keeper/fees.go +++ b/x/cdp/keeper/fees.go @@ -27,6 +27,46 @@ func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coins, periods sdk. return newFees } +// UpdateFeesForRiskyCdps calculates fees for risky CDPs +// The overall logic is first select the CDPs with 10% of the liquidation ratio +// Then we call calculate fees on each of those CDPs +// Next we store the result of the fees in the cdp.AccumulatedFees field +// Finally we set the cdp.FeesUpdated time to the current block time (ctx.BlockTime()) since that +// is when we made the update +func (k Keeper) UpdateFeesForRiskyCdps(ctx sdk.Context, collateralDenom string, marketID string) sdk.Error { + + + price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) + if err != nil { + return err + } + + liquidationRatio := k.getLiquidationRatio(ctx, collateralDenom) + + // NOTE - we have a fixed cutoff at 110% - this may or may not be changed in the future + normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio)).Mul(sdk.MustNewDecFromStr("1.1")) + + // now iterate over all the cdps based on collateral ratio + k.IterateCdpsByCollateralRatio(ctx, collateralDenom, normalizedRatio, func(cdp types.CDP) bool { + + // get the number of periods + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + + // now calculate and store additional fees + additionalFees := k.CalculateFees(ctx, cdp.Principal, periods, collateralDenom) + + // now add the additional fees to the accumulated fees for the cdp + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(additionalFees) + + // and set the fees updated time to the current block time since we just updated it + cdp.FeesUpdated = ctx.BlockTime() + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) + return false // this returns true when you want to stop iterating. Since we want to iterate through all we return false + }) + return nil +} + // IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coins) { for _, pc := range principal { diff --git a/x/cdp/keeper/fees_test.go b/x/cdp/keeper/fees_test.go index 48a5fd2d..4367d368 100644 --- a/x/cdp/keeper/fees_test.go +++ b/x/cdp/keeper/fees_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "math/rand" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" @@ -76,6 +77,64 @@ func (suite *FeeTestSuite) TestCalculateFeesPrecisionLoss() { } +// createCdps is a helper function to create two CDPs each with zero fees +func (suite *FeeTestSuite) createCdps() { + // create 2 accounts in the state and give them some coins + // create two private key pair addresses + _, addrs := app.GeneratePrivKeyAddressPairs(2) + ak := suite.app.GetAccountKeeper() + // setup the first account + acc := ak.NewAccountWithAddress(suite.ctx, addrs[0]) + acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000))) + + ak.SetAccount(suite.ctx, acc) + // now setup the second account + acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1]) + acc2.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000))) + ak.SetAccount(suite.ctx, acc2) + + // now create two cdps with the addresses we just created + // use the created account to create a cdp that SHOULD have fees updated + // to get a ratio between 100 - 110% of liquidation ratio we can use 200xrp ($50) and 24 usdx (208% collateralization with liquidation ratio of 200%) + // create CDP for the first address + err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 24000000))) + suite.NoError(err) // check that no error was thrown + + // use the other account to create a cdp that SHOULD NOT have fees updated - 500% collateralization + // create CDP for the second address + err = suite.keeper.AddCdp(suite.ctx, addrs[1], cs(c("xrp", 200000000)), cs(c("usdx", 10000000))) + suite.NoError(err) // check that no error was thrown + +} + +// UpdateFeesForRiskyCdpsTest tests the functionality for updating the fees for risky CDPs +func (suite *FeeTestSuite) TestUpdateFeesForRiskyCdps() { + // this helper function creates two CDPs with id 1 and 2 respectively, each with zero fees + suite.createCdps() + + // move the context forward in time so that cdps will have fees accumulate if CalculateFees is called + // note - time must be moved forward by a sufficient amount in order for additional + // fees to accumulate, in this example 60 seconds + suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 60)) + err := suite.keeper.UpdateFeesForRiskyCdps(suite.ctx, "xrp", "xrp:usd") + suite.NoError(err) // check that we don't have any error + + // cdp we expect fees to accumulate for + cdp1, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 1) + // check fees are not zero + // check that the fees have been updated + suite.False(cdp1.AccumulatedFees.Empty()) + // now check that we have the correct amount of fees overall (2 USDX for this scenario) + suite.Equal(sdk.NewInt(2), cdp1.AccumulatedFees.AmountOf("usdx")) + + // cdp we expect fees to not accumulate for + cdp2, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 2) + + // check fees are zero + suite.True(cdp2.AccumulatedFees.Empty()) + +} + func (suite *FeeTestSuite) TestGetSetPreviousBlockTime() { now := tmtime.Now() diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 6905882a..0714b69e 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -39,9 +39,10 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), - SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, - DebtAuctionThreshold: cdp.DefaultDebtThreshold, + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, + SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency, CollateralParams: cdp.CollateralParams{ { Denom: asset, @@ -61,14 +62,16 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { ReferenceAsset: "usd", ConversionFactor: i(6), DebtFloor: i(10000000), + SavingsRate: d("0.9"), }, }, }, - StartingCdpID: cdp.DefaultCdpStartingID, - DebtDenom: cdp.DefaultDebtDenom, - GovDenom: cdp.DefaultGovDenom, - CDPs: cdp.CDPs{}, - PreviousBlockTime: cdp.DefaultPreviousBlockTime, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + PreviousDistributionTime: cdp.DefaultPreviousDistributionTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } @@ -101,9 +104,10 @@ func NewPricefeedGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), - SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, - DebtAuctionThreshold: cdp.DefaultDebtThreshold, + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, + SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency, CollateralParams: cdp.CollateralParams{ { Denom: "xrp", @@ -134,20 +138,23 @@ func NewCDPGenStateMulti() app.GenesisState { ReferenceAsset: "usd", ConversionFactor: i(6), DebtFloor: i(10000000), + SavingsRate: d("0.95"), }, { Denom: "susd", ReferenceAsset: "usd", ConversionFactor: i(6), DebtFloor: i(10000000), + SavingsRate: d("0.95"), }, }, }, - StartingCdpID: cdp.DefaultCdpStartingID, - DebtDenom: cdp.DefaultDebtDenom, - GovDenom: cdp.DefaultGovDenom, - CDPs: cdp.CDPs{}, - PreviousBlockTime: cdp.DefaultPreviousBlockTime, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + PreviousDistributionTime: cdp.DefaultPreviousDistributionTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go index 94570d2b..98d416a1 100644 --- a/x/cdp/keeper/keeper.go +++ b/x/cdp/keeper/keeper.go @@ -18,21 +18,12 @@ type Keeper struct { pricefeedKeeper types.PricefeedKeeper supplyKeeper types.SupplyKeeper auctionKeeper types.AuctionKeeper + accountKeeper types.AccountKeeper codespace sdk.CodespaceType } // NewKeeper creates a new keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, ak types.AuctionKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { - - // ensure cdp module account is set - if addr := sk.GetModuleAddress(types.ModuleName); addr == nil { - panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) - } - - // ensure liquidator module account is set - if addr := sk.GetModuleAddress(types.LiquidatorMacc); addr == nil { - panic(fmt.Sprintf("%s module account has not been set", types.LiquidatorMacc)) - } +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, ak types.AuctionKeeper, sk types.SupplyKeeper, ack types.AccountKeeper, codespace sdk.CodespaceType) Keeper { return Keeper{ key: key, @@ -41,6 +32,7 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pricefeedKeeper: pfk, auctionKeeper: ak, supplyKeeper: sk, + accountKeeper: ack, codespace: codespace, } } diff --git a/x/cdp/keeper/savings.go b/x/cdp/keeper/savings.go new file mode 100644 index 00000000..68678e24 --- /dev/null +++ b/x/cdp/keeper/savings.go @@ -0,0 +1,95 @@ +package keeper + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" + auctiontypes "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +// DistributeSavingsRate distributes surplus that has accumulated in the liquidator account to address holding stable coins according the the savings rate +func (k Keeper) DistributeSavingsRate(ctx sdk.Context, debtDenom string) sdk.Error { + dp, found := k.GetDebtParam(ctx, debtDenom) + if !found { + return types.ErrDebtNotSupported(k.codespace, debtDenom) + } + savingsRateMacc := k.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc) + surplusToDistribute := savingsRateMacc.GetCoins().AmountOf(dp.Denom) + if surplusToDistribute.IsZero() { + return nil + } + + modAccountCoins := k.getModuleAccountCoins(ctx, dp.Denom) + totalSupplyLessModAccounts := k.supplyKeeper.GetSupply(ctx).GetTotal().Sub(modAccountCoins) + surplusDistributed := sdk.ZeroInt() + var iterationErr sdk.Error + k.accountKeeper.IterateAccounts(ctx, func(acc authexported.Account) (stop bool) { + _, ok := acc.(supplyexported.ModuleAccountI) + if ok { + // don't distribute savings rate to module accounts + return false + } + debtAmount := acc.GetCoins().AmountOf(debtDenom) + if !debtAmount.IsPositive() { + return false + } + // (balance * rewardToDisribute) / totalSupply + // interest is the ratable fraction of savings rate owed to that account, rounded using bankers rounding + interest := (sdk.NewDecFromInt(debtAmount).Mul(sdk.NewDecFromInt(surplusToDistribute))).Quo(sdk.NewDecFromInt(totalSupplyLessModAccounts.AmountOf(debtDenom))).RoundInt() + // sanity check, if we are going to over-distribute due to rounding, distribute only the remaining savings rate that hasn't been distributed. + if interest.GT(surplusToDistribute.Sub(surplusDistributed)) { + interest = surplusToDistribute.Sub(surplusDistributed) + } + // sanity check - don't send saving rate if the rounded amount is zero + if !interest.IsPositive() { + return false + } + interestCoins := sdk.NewCoins(sdk.NewCoin(debtDenom, interest)) + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.SavingsRateMacc, acc.GetAddress(), interestCoins) + if err != nil { + iterationErr = err + return true + } + surplusDistributed = surplusDistributed.Add(interest) + return false + }) + if iterationErr != nil { + return iterationErr + } + return nil +} + +// GetPreviousSavingsDistribution get the time of the previous savings rate distribution +func (k Keeper) GetPreviousSavingsDistribution(ctx sdk.Context) (distTime time.Time, found bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDistributionTimeKey) + b := store.Get([]byte{}) + if b == nil { + return time.Time{}, false + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &distTime) + return distTime, true +} + +// SetPreviousSavingsDistribution set the time of the previous block +func (k Keeper) SetPreviousSavingsDistribution(ctx sdk.Context, distTime time.Time) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDistributionTimeKey) + store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(distTime)) +} + +func (k Keeper) getModuleAccountCoins(ctx sdk.Context, denom string) sdk.Coins { + // NOTE: these are the module accounts that could end up holding stable denoms at some point. + // Since there are currently no api methods to 'GetAllModuleAccounts', this function will need to be updated if a + // new module account is added which can hold stable denoms. + savingsRateMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc).GetCoins().AmountOf(denom) + cdpMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom) + auctionMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, auctiontypes.ModuleName).GetCoins().AmountOf(denom) + liquidatorMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(denom) + feeMaccCoinAmount := k.supplyKeeper.GetModuleAccount(ctx, authtypes.FeeCollectorName).GetCoins().AmountOf(denom) + totalModAccountAmount := savingsRateMaccCoinAmount.Add(cdpMaccCoinAmount).Add(auctionMaccCoinAmount).Add(liquidatorMaccCoinAmount).Add(feeMaccCoinAmount) + return sdk.NewCoins(sdk.NewCoin(denom, totalModAccountAmount)) +} diff --git a/x/cdp/keeper/savings_test.go b/x/cdp/keeper/savings_test.go new file mode 100644 index 00000000..5e5a910e --- /dev/null +++ b/x/cdp/keeper/savings_test.go @@ -0,0 +1,81 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type SavingsTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress +} + +func (suite *SavingsTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + _, addrs := app.GeneratePrivKeyAddressPairs(3) + authGS := app.NewAuthGenState( + addrs, + []sdk.Coins{ + cs(c("usdx", 100000)), cs(c("usdx", 50000)), cs(c("usdx", 50000)), + }, + ) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + sk := tApp.GetSupplyKeeper() + macc := sk.GetModuleAccount(ctx, types.SavingsRateMacc) + err := sk.MintCoins(ctx, macc.GetName(), cs(c("usdx", 10000))) + suite.NoError(err) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.keeper = keeper + suite.ctx = ctx + suite.addrs = addrs +} + +func (suite *SavingsTestSuite) TestApplySavingsRate() { + err := suite.keeper.DistributeSavingsRate(suite.ctx, "usdx") + suite.NoError(err) + ak := suite.app.GetAccountKeeper() + acc0 := ak.GetAccount(suite.ctx, suite.addrs[0]) + suite.Equal(cs(c("usdx", 105000)), acc0.GetCoins()) + acc1 := ak.GetAccount(suite.ctx, suite.addrs[1]) + suite.Equal(cs(c("usdx", 52500)), acc1.GetCoins()) + acc2 := ak.GetAccount(suite.ctx, suite.addrs[2]) + suite.Equal(cs(c("usdx", 52500)), acc2.GetCoins()) + sk := suite.app.GetSupplyKeeper() + macc := sk.GetModuleAccount(suite.ctx, types.SavingsRateMacc) + suite.True(macc.GetCoins().AmountOf("usdx").IsZero()) +} + +func (suite *SavingsTestSuite) TestGetSetPreviousDistributionTime() { + now := tmtime.Now() + + _, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx) + suite.False(f) + + suite.NotPanics(func() { suite.keeper.SetPreviousSavingsDistribution(suite.ctx, now) }) + + pdt, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx) + suite.True(f) + suite.Equal(now, pdt) + +} + +func TestSavingsTestSuite(t *testing.T) { + suite.Run(t, new(SavingsTestSuite)) +} diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go index 08645a2b..2a63b14f 100644 --- a/x/cdp/keeper/seize.go +++ b/x/cdp/keeper/seize.go @@ -82,15 +82,24 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { // HandleNewDebt compounds the accumulated fees for the input collateral and principal coins. // the following operations are performed: -// 1. mints the fee coins in the liquidator module account, -// 2. mints the same amount of debt coins in the cdp module account +// 1. The fees accumulated since the last block are calculated +// 2. The fees are minted, split between the liquidator module account (surplus) and the savings rate module account (savings rate) according to the savings rate parameter. +// 2. An equal amount of debt coins are minted in the cdp module account // 3. updates the total amount of principal for the input collateral type in the store, func (k Keeper) HandleNewDebt(ctx sdk.Context, collateralDenom string, principalDenom string, periods sdk.Int) { + dp, _ := k.GetDebtParam(ctx, principalDenom) + savingsRate := dp.SavingsRate previousDebt := k.GetTotalPrincipal(ctx, collateralDenom, principalDenom) feeCoins := sdk.NewCoins(sdk.NewCoin(principalDenom, previousDebt)) newFees := k.CalculateFees(ctx, feeCoins, periods, collateralDenom) + if newFees.IsZero() { + return + } + newFeesSavings := sdk.NewDecFromInt(newFees.AmountOf(principalDenom)).Mul(savingsRate).RoundInt() + newFeesSurplus := newFees.AmountOf(principalDenom).Sub(newFeesSavings) k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees) - k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, newFees) + k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(principalDenom, newFeesSurplus))) + k.supplyKeeper.MintCoins(ctx, types.SavingsRateMacc, sdk.NewCoins(sdk.NewCoin(principalDenom, newFeesSavings))) k.SetTotalPrincipal(ctx, collateralDenom, principalDenom, feeCoins.Add(newFees).AmountOf(principalDenom)) } @@ -100,6 +109,9 @@ func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, li if err != nil { return err } + // price = $0.5 + // liquidation ratio = 1.5 + // normalizedRatio = (1/(0.5/1.5)) = 3 normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio)) cdpsToLiquidate := k.GetAllCdpsByDenomAndRatio(ctx, denom, normalizedRatio) for _, c := range cdpsToLiquidate { diff --git a/x/cdp/module.go b/x/cdp/module.go index 108189de..5c2febb1 100644 --- a/x/cdp/module.go +++ b/x/cdp/module.go @@ -97,14 +97,16 @@ type AppModule struct { keeper Keeper pricefeedKeeper PricefeedKeeper + supplyKeeper SupplyKeeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper, pricefeedKeeper PricefeedKeeper) AppModule { +func NewAppModule(keeper Keeper, pricefeedKeeper PricefeedKeeper, supplyKeeper SupplyKeeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, pricefeedKeeper: pricefeedKeeper, + supplyKeeper: supplyKeeper, } } @@ -140,7 +142,7 @@ func (am AppModule) NewQuerierHandler() sdk.Querier { func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { var genesisState GenesisState ModuleCdc.MustUnmarshalJSON(data, &genesisState) - InitGenesis(ctx, am.keeper, am.pricefeedKeeper, genesisState) + InitGenesis(ctx, am.keeper, am.pricefeedKeeper, am.supplyKeeper, genesisState) return []abci.ValidatorUpdate{} } diff --git a/x/cdp/spec/04_begin_block.md b/x/cdp/spec/04_begin_block.md index e8257c0c..77cd41e4 100644 --- a/x/cdp/spec/04_begin_block.md +++ b/x/cdp/spec/04_begin_block.md @@ -3,6 +3,7 @@ At the start of every block the BeginBlocker of the cdp module: - updates total CDP fees +- update fees for individual "risky" CDPs - liquidates CDPs under the collateral ratio - nets out system debt and, if necessary, starts auctions to re-balance it - records the last block time @@ -13,6 +14,13 @@ At the start of every block the BeginBlocker of the cdp module: - An equal amount of debt coins are minted and sent to the system's CDP module account. - An equal amount of stable asset coins are minted and sent to the system's liquidator module account +## Update risky cdps + +- UpdateFeesForRiskyCdps calculates fees for risky CDPs +- Select the CDPs with 10% of the liquidation ratio - the risky CDPs +- Calculate additional accumulated fees on each of those CDPs +- Update the fees updated time for the CDP to the current block time + ## Liquidate CDP - Get every cdp that is under the liquidation ratio for its collateral type. @@ -26,9 +34,15 @@ At the start of every block the BeginBlocker of the cdp module: - Burn the maximum possible equal amount of debt and stable asset from the liquidator module account. - If there is enough debt remaining for an auction, start one. -- If there is enough surplus stable asset remaining for an auction, start one. +- If there is enough surplus stable asset, minus surplus reserved for the savings rate, remaining for an auction, start one. - Otherwise do nothing, leave debt/surplus to accumulate over subsequent blocks. +## Distribute Surplus Stable Asset According to the Savings Rate + +- If `SavingsDistributionFrequency` seconds have elapsed since the previous distribution, the savings rate is applied to all accounts that hold stable asset. +- Each account that holds stable asset is distributed a ratable portion of the surplus that is apportioned to the savings rate. +- If distribution occurred, the time of the distribution is recorded. + ## Update Previous Block Time The current block time is recorded. diff --git a/x/cdp/spec/06_params.md b/x/cdp/spec/06_params.md index fbde95d7..4297619c 100644 --- a/x/cdp/spec/06_params.md +++ b/x/cdp/spec/06_params.md @@ -2,12 +2,13 @@ The cdp module contains the following parameters: -| Key | Type | Example | Description | -|------------------|-------------------------|------------------------------------|------------------------------------------------------------------| -| CollateralParams | array (CollateralParam) | [{see below}] | array of params for each enabled collateral type | -| DebtParams | array (DebtParam) | [{see below}] | array of params for each enabled pegged asset | -| GlobalDebtLimit | array (coin) | [{"denom":"usdx","amount":"1000"}] | maximum pegged assets that can be minted across the whole system | -| CircuitBreaker | bool | false | flag to disable user interactions with the system | +| Key | Type | Example | Description | +|------------------ |-------------------------|------------------------------------|------------------------------------------------------------------| +| CollateralParams | array (CollateralParam) | [{see below}] | array of params for each enabled collateral type | +| DebtParams | array (DebtParam) | [{see below}] | array of params for each enabled pegged asset | +| GlobalDebtLimit | array (coin) | [{"denom":"usdx","amount":"1000"}] | maximum pegged assets that can be minted across the whole system | +| SavingsDistributionFrequency | string (int) | "84600" | number of seconds between distribution of the savings rate| +| CircuitBreaker | bool | false | flag to disable user interactions with the system | Each CollateralParam has the following parameters: diff --git a/x/cdp/types/cdp.go b/x/cdp/types/cdp.go index 8c5365be..da06c836 100644 --- a/x/cdp/types/cdp.go +++ b/x/cdp/types/cdp.go @@ -39,7 +39,7 @@ func (cdp CDP) String() string { Collateral Type: %s Collateral: %s Principal: %s - Fees: %s + AccumulatedFees: %s Fees Last Updated: %s`, cdp.Owner, cdp.ID, diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go index 402458dc..dcbd8314 100644 --- a/x/cdp/types/expected_keepers.go +++ b/x/cdp/types/expected_keepers.go @@ -4,6 +4,7 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" pftypes "github.com/kava-labs/kava/x/pricefeed/types" ) @@ -21,6 +22,7 @@ type SupplyKeeper interface { SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) sdk.Error BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error + GetSupply(ctx sdk.Context) (supply supplyexported.SupplyI) } // PricefeedKeeper defines the expected interface for the pricefeed @@ -39,3 +41,8 @@ type AuctionKeeper interface { StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) } + +// AccountKeeper expected interface for the account keeper (noalias) +type AccountKeeper interface { + IterateAccounts(ctx sdk.Context, cb func(account authexported.Account) (stop bool)) +} diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 4097e6d4..1d1da37b 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -8,38 +8,41 @@ import ( // GenesisState is the state that must be provided at genesis. type GenesisState struct { - Params Params `json:"params" yaml:"params"` - CDPs CDPs `json:"cdps" yaml:"cdps"` - Deposits Deposits `json:"deposits" yaml:"deposits"` - StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"` - DebtDenom string `json:"debt_denom" yaml:"debt_denom"` - GovDenom string `json:"gov_denom" yaml:"gov_denom"` - PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` + Params Params `json:"params" yaml:"params"` + CDPs CDPs `json:"cdps" yaml:"cdps"` + Deposits Deposits `json:"deposits" yaml:"deposits"` + StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"` + DebtDenom string `json:"debt_denom" yaml:"debt_denom"` + GovDenom string `json:"gov_denom" yaml:"gov_denom"` + PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` + PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml"previous_distribution_time"` } // NewGenesisState returns a new genesis state -func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, debtDenom, govDenom string, previousBlockTime time.Time) GenesisState { +func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, debtDenom, govDenom string, previousBlockTime time.Time, previousDistTime time.Time) GenesisState { return GenesisState{ - Params: params, - CDPs: cdps, - Deposits: deposits, - StartingCdpID: startingCdpID, - DebtDenom: debtDenom, - GovDenom: govDenom, - PreviousBlockTime: previousBlockTime, + Params: params, + CDPs: cdps, + Deposits: deposits, + StartingCdpID: startingCdpID, + DebtDenom: debtDenom, + GovDenom: govDenom, + PreviousBlockTime: previousBlockTime, + PreviousDistributionTime: previousDistTime, } } // DefaultGenesisState returns a default genesis state func DefaultGenesisState() GenesisState { return GenesisState{ - Params: DefaultParams(), - CDPs: CDPs{}, - Deposits: Deposits{}, - StartingCdpID: DefaultCdpStartingID, - DebtDenom: DefaultDebtDenom, - GovDenom: DefaultGovDenom, - PreviousBlockTime: DefaultPreviousBlockTime, + Params: DefaultParams(), + CDPs: CDPs{}, + Deposits: Deposits{}, + StartingCdpID: DefaultCdpStartingID, + DebtDenom: DefaultDebtDenom, + GovDenom: DefaultGovDenom, + PreviousBlockTime: DefaultPreviousBlockTime, + PreviousDistributionTime: DefaultPreviousDistributionTime, } } @@ -55,6 +58,10 @@ func (gs GenesisState) Validate() error { return fmt.Errorf("previous block time not set") } + if gs.PreviousDistributionTime.Equal(time.Time{}) { + return fmt.Errorf("previous distribution time not set") + } + if gs.DebtDenom == "" { return fmt.Errorf("debt denom not set") diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go index 6f34524a..de0de209 100644 --- a/x/cdp/types/keys.go +++ b/x/cdp/types/keys.go @@ -25,6 +25,9 @@ const ( // LiquidatorMacc module account for liquidator LiquidatorMacc = "liquidator" + + // SavingsRateMacc module account for savings rate + SavingsRateMacc = "savings" ) var sep = []byte(":") @@ -43,18 +46,20 @@ var sep = []byte(":") // - 0x06:totalPrincipal // - 0x07:feeRate // - 0x08:previousBlockTime +// - 0x09:previousDistributionTime // KVStore key prefixes var ( - CdpIDKeyPrefix = []byte{0x00} - CdpKeyPrefix = []byte{0x01} - CollateralRatioIndexPrefix = []byte{0x02} - CdpIDKey = []byte{0x03} - DebtDenomKey = []byte{0x04} - GovDenomKey = []byte{0x05} - DepositKeyPrefix = []byte{0x06} - PrincipalKeyPrefix = []byte{0x07} - PreviousBlockTimeKey = []byte{0x08} + CdpIDKeyPrefix = []byte{0x00} + CdpKeyPrefix = []byte{0x01} + CollateralRatioIndexPrefix = []byte{0x02} + CdpIDKey = []byte{0x03} + DebtDenomKey = []byte{0x04} + GovDenomKey = []byte{0x05} + DepositKeyPrefix = []byte{0x06} + PrincipalKeyPrefix = []byte{0x07} + PreviousBlockTimeKey = []byte{0x08} + PreviousDistributionTimeKey = []byte{0x09} ) var lenPositiveDec = len(SortableDecBytes(sdk.OneDec())) diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index 2d8c78d4..db54cbf5 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -11,34 +11,38 @@ import ( // Parameter keys var ( - KeyGlobalDebtLimit = []byte("GlobalDebtLimit") - KeyCollateralParams = []byte("CollateralParams") - KeyDebtParams = []byte("DebtParams") - KeyCircuitBreaker = []byte("CircuitBreaker") - KeyDebtThreshold = []byte("DebtThreshold") - KeySurplusThreshold = []byte("SurplusThreshold") - DefaultGlobalDebt = sdk.Coins{} - DefaultCircuitBreaker = false - DefaultCollateralParams = CollateralParams{} - DefaultDebtParams = DebtParams{} - DefaultCdpStartingID = uint64(1) - DefaultDebtDenom = "debt" - DefaultGovDenom = "ukava" - DefaultSurplusThreshold = sdk.NewInt(1000000000) - DefaultDebtThreshold = sdk.NewInt(1000000000) - DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) - minCollateralPrefix = 0 - maxCollateralPrefix = 255 + KeyGlobalDebtLimit = []byte("GlobalDebtLimit") + KeyCollateralParams = []byte("CollateralParams") + KeyDebtParams = []byte("DebtParams") + KeyDistributionFrequency = []byte("DistributionFrequency") + KeyCircuitBreaker = []byte("CircuitBreaker") + KeyDebtThreshold = []byte("DebtThreshold") + KeySurplusThreshold = []byte("SurplusThreshold") + DefaultGlobalDebt = sdk.Coins{} + DefaultCircuitBreaker = false + DefaultCollateralParams = CollateralParams{} + DefaultDebtParams = DebtParams{} + DefaultCdpStartingID = uint64(1) + DefaultDebtDenom = "debt" + DefaultGovDenom = "ukava" + DefaultSurplusThreshold = sdk.NewInt(1000000000) + DefaultDebtThreshold = sdk.NewInt(1000000000) + DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) + DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0)) + DefaultSavingsDistributionFrequency = time.Hour * 24 * 2 + minCollateralPrefix = 0 + maxCollateralPrefix = 255 ) // Params governance parameters for cdp module type Params struct { - CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` - DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` - GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` - SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"` - DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"` - CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` + CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` + DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` + GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` + SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"` + DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"` + SavingsDistributionFrequency time.Duration `json:"savings_distribution_frequency" yaml:"savings_distribution_frequency"` + CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` } // String implements fmt.Stringer @@ -49,26 +53,28 @@ func (p Params) String() string { Debt Params: %s Surplus Auction Threshold: %s Debt Auction Threshold: %s + Savings Distribution Frequency: %s Circuit Breaker: %t`, - p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.CircuitBreaker, + p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.SavingsDistributionFrequency, p.CircuitBreaker, ) } // NewParams returns a new params object -func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, breaker bool) Params { +func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, distributionFreq time.Duration, breaker bool) Params { return Params{ - GlobalDebtLimit: debtLimit, - CollateralParams: collateralParams, - DebtParams: debtParams, - DebtAuctionThreshold: debtThreshold, - SurplusAuctionThreshold: surplusThreshold, - CircuitBreaker: breaker, + GlobalDebtLimit: debtLimit, + CollateralParams: collateralParams, + DebtParams: debtParams, + DebtAuctionThreshold: debtThreshold, + SurplusAuctionThreshold: surplusThreshold, + SavingsDistributionFrequency: distributionFreq, + CircuitBreaker: breaker, } } // DefaultParams returns default params for cdp module func DefaultParams() Params { - return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultCircuitBreaker) + return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultSavingsDistributionFrequency, DefaultCircuitBreaker) } // CollateralParam governance parameters for each collateral type within the cdp module @@ -116,7 +122,8 @@ type DebtParam struct { Denom string `json:"denom" yaml:"denom"` ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` - DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust + DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust + SavingsRate sdk.Dec `json:"savings_rate" yaml:"savings_rate"` // the percentage of stability fees that are redirected to savings rate } func (dp DebtParam) String() string { @@ -155,6 +162,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker}, {Key: KeySurplusThreshold, Value: &p.SurplusAuctionThreshold}, {Key: KeyDebtThreshold, Value: &p.DebtAuctionThreshold}, + {Key: KeyDistributionFrequency, Value: &p.SavingsDistributionFrequency}, } } @@ -167,6 +175,9 @@ func (p Params) Validate() error { if found { return fmt.Errorf("duplicate debt denom: %s", dp.Denom) } + if dp.SavingsRate.LT(sdk.ZeroDec()) || dp.SavingsRate.GT(sdk.OneDec()) { + return fmt.Errorf("savings rate should be between 0 and 1, is %s for %s", dp.SavingsRate, dp.Denom) + } debtDenoms[dp.Denom] = 1 } @@ -233,5 +244,9 @@ func (p Params) Validate() error { if !p.DebtAuctionThreshold.IsPositive() { return fmt.Errorf("debt auction threshold should be positive, is %s", p.DebtAuctionThreshold) } + + if p.SavingsDistributionFrequency.Seconds() <= float64(0) { + return fmt.Errorf("savings distribution frequency should be positive, is %s", p.SavingsDistributionFrequency) + } return nil } diff --git a/x/kavadist/abci.go b/x/kavadist/abci.go new file mode 100644 index 00000000..de7b80ce --- /dev/null +++ b/x/kavadist/abci.go @@ -0,0 +1,12 @@ +package kavadist + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func BeginBlocker(ctx sdk.Context, k Keeper) { + err := k.MintPeriodInflation(ctx) + if err != nil { + panic(err) + } +} diff --git a/x/kavadist/alias.go b/x/kavadist/alias.go new file mode 100644 index 00000000..e9295e14 --- /dev/null +++ b/x/kavadist/alias.go @@ -0,0 +1,54 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/kava-labs/kava/x/kavadist/keeper +// ALIASGEN: github.com/kava-labs/kava/x/kavadist/types +package kavadist + +import ( + "github.com/kava-labs/kava/x/kavadist/keeper" + "github.com/kava-labs/kava/x/kavadist/types" +) + +const ( + DefaultCodespace = types.DefaultCodespace + EventTypeKavaDist = types.EventTypeKavaDist + AttributeKeyInflation = types.AttributeKeyInflation + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + KavaDistMacc = types.KavaDistMacc + QueryGetParams = types.QueryGetParams +) + +var ( + // functions aliases + NewKeeper = keeper.NewKeeper + RegisterCodec = types.RegisterCodec + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + + // variable aliases + ModuleCdc = types.ModuleCdc + CurrentDistPeriodKey = types.CurrentDistPeriodKey + PreviousBlockTimeKey = types.PreviousBlockTimeKey + KeyActive = types.KeyActive + KeyPeriods = types.KeyPeriods + DefaultActive = types.DefaultActive + DefaultPeriods = types.DefaultPeriods + DefaultPreviousBlockTime = types.DefaultPreviousBlockTime + GovDenom = types.GovDenom +) + +type ( + Keeper = keeper.Keeper + GenesisState = types.GenesisState + Params = types.Params + Period = types.Period + Periods = types.Periods +) diff --git a/x/kavadist/doc.go b/x/kavadist/doc.go new file mode 100644 index 00000000..5926f7e4 --- /dev/null +++ b/x/kavadist/doc.go @@ -0,0 +1 @@ +package kavadist diff --git a/x/kavadist/genesis.go b/x/kavadist/genesis.go new file mode 100644 index 00000000..c4219a02 --- /dev/null +++ b/x/kavadist/genesis.go @@ -0,0 +1,39 @@ +package kavadist + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/kavadist/types" +) + +// InitGenesis initializes the store state from a genesis state. +func InitGenesis(ctx sdk.Context, k Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) { + if err := gs.Validate(); err != nil { + panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) + } + + k.SetParams(ctx, gs.Params) + + // only set the previous block time if it's different than default + if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) { + k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime) + } + + // check if the module account exists + moduleAcc := supplyKeeper.GetModuleAccount(ctx, KavaDistMacc) + if moduleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", KavaDistMacc)) + } + +} + +// ExportGenesis export genesis state for cdp module +func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { + params := k.GetParams(ctx) + previousBlockTime, found := k.GetPreviousBlockTime(ctx) + if !found { + previousBlockTime = DefaultPreviousBlockTime + } + return NewGenesisState(params,previousBlockTime) +} diff --git a/x/kavadist/handler.go b/x/kavadist/handler.go new file mode 100644 index 00000000..585efbbb --- /dev/null +++ b/x/kavadist/handler.go @@ -0,0 +1,18 @@ +package kavadist + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewHandler creates an sdk.Handler for kavadist messages +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + switch msg := msg.(type) { + default: + errMsg := fmt.Sprintf("unrecognized cdp msg type: %T", msg) + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} \ No newline at end of file diff --git a/x/kavadist/keeper/keeper.go b/x/kavadist/keeper/keeper.go new file mode 100644 index 00000000..cf8729c6 --- /dev/null +++ b/x/kavadist/keeper/keeper.go @@ -0,0 +1,49 @@ +package keeper + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/kava-labs/kava/x/kavadist/types" +) + +// Keeper keeper for the cdp module +type Keeper struct { + key sdk.StoreKey + cdc *codec.Codec + paramSubspace subspace.Subspace + supplyKeeper types.SupplyKeeper + codespace sdk.CodespaceType +} + +// NewKeeper creates a new keeper +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { + + return Keeper{ + key: key, + cdc: cdc, + paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), + supplyKeeper: sk, + codespace: codespace, + } +} + +// GetPreviousBlockTime get the blocktime for the previous block +func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) + b := store.Get([]byte{}) + if b == nil { + return time.Time{}, false + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime) + return blockTime, true +} + +// SetPreviousBlockTime set the time of the previous block +func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) + store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime)) +} diff --git a/x/kavadist/keeper/mint.go b/x/kavadist/keeper/mint.go new file mode 100644 index 00000000..4a1da2c3 --- /dev/null +++ b/x/kavadist/keeper/mint.go @@ -0,0 +1,87 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + cdptypes "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/kavadist/types" +) + +// MintPeriodInflation mints new tokens according to the inflation schedule specified in the paramters +func (k Keeper) MintPeriodInflation(ctx sdk.Context) sdk.Error { + params := k.GetParams(ctx) + if !params.Active { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeKavaDist, + sdk.NewAttribute(types.AttributeKeyStatus, types.AttributeValueInactive), + ), + ) + return nil + } + previousBlockTime, found := k.GetPreviousBlockTime(ctx) + if !found { + previousBlockTime = ctx.BlockTime() + k.SetPreviousBlockTime(ctx, previousBlockTime) + return nil + } + for _, period := range params.Periods { + // Case 1 - period is fully expired + if period.End.Before(previousBlockTime) { + continue + } + // Case 2 - period has ended since the previous block time + if period.End.After(previousBlockTime) && period.End.Before(ctx.BlockTime()) { + // calculate time elapsed relative to the periods end time + timeElapsed := sdk.NewInt(period.End.Unix() - previousBlockTime.Unix()) + err := k.mintInflationaryCoins(ctx, period.Inflation, timeElapsed, types.GovDenom) + if err != nil { + return err + } + // update the value of previousBlockTime so that the next period starts from the end of the last + // period and not the original value of previousBlockTime + previousBlockTime = period.End + } + // Case 3 - period is ongoing + if (period.Start.Before(previousBlockTime) || period.Start.Equal(previousBlockTime)) && period.End.After(ctx.BlockTime()) { + // calculate time elapsed relative to the current block time + timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix()) + err := k.mintInflationaryCoins(ctx, period.Inflation, timeElapsed, types.GovDenom) + if err != nil { + return err + } + } + // Case 4 - period hasn't started + if period.Start.After(ctx.BlockTime()) || period.Start.Equal(ctx.BlockTime()) { + continue + } + } + k.SetPreviousBlockTime(ctx, ctx.BlockTime()) + return nil +} + +func (k Keeper) mintInflationaryCoins(ctx sdk.Context, inflationRate sdk.Dec, timePeriods sdk.Int, denom string) sdk.Error { + totalSupply := k.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(denom) + // used to scale accumulator calculations by 10^18 + scalar := sdk.NewInt(1000000000000000000) + // convert inflation rate to integer + inflationInt := inflationRate.Mul(sdk.NewDecFromInt(scalar)).TruncateInt() + // calculate the multiplier (amount to multiply the total supply by to achieve the desired inflation) + // multiply the result by 10^-18 because RelativePow returns the result scaled by 10^18 + accumulator := sdk.NewDecFromInt(cdptypes.RelativePow(inflationInt, timePeriods, scalar)).Mul(sdk.SmallestDec()) + // calculate the number of coins to mint + amountToMint := (sdk.NewDecFromInt(totalSupply).Mul(accumulator)).Sub(sdk.NewDecFromInt(totalSupply)).TruncateInt() + if amountToMint.IsZero() { + return nil + } + err := k.supplyKeeper.MintCoins(ctx, types.KavaDistMacc, sdk.NewCoins(sdk.NewCoin(denom, amountToMint))) + if err != nil { + return err + } + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeKavaDist, + sdk.NewAttribute(types.AttributeKeyInflation, amountToMint.String()), + ), + ) + return nil +} diff --git a/x/kavadist/keeper/mint_test.go b/x/kavadist/keeper/mint_test.go new file mode 100644 index 00000000..c9b77753 --- /dev/null +++ b/x/kavadist/keeper/mint_test.go @@ -0,0 +1,144 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/kavadist/keeper" + "github.com/kava-labs/kava/x/kavadist/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type MintTestSuite struct { + suite.Suite + + keeper keeper.Keeper + supplyKeeper types.SupplyKeeper + app app.TestApp + ctx sdk.Context +} + +var ( + p = types.Periods{ + types.Period{ + Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + } +) + +func (suite *MintTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + tApp := app.NewTestApp() + _, addrs := app.GeneratePrivKeyAddressPairs(1) + coins := []sdk.Coins{sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000000000000)))} + authGS := app.NewAuthGenState( + addrs, coins) + + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + params := types.NewParams(true, p) + gs := app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(types.NewGenesisState(params, types.DefaultPreviousBlockTime))} + tApp.InitializeFromGenesisStates( + authGS, + gs, + ) + keeper := tApp.GetKavadistKeeper() + sk := tApp.GetSupplyKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + suite.supplyKeeper = sk + +} + +func (suite *MintTestSuite) TestMintExpiredPeriod() { + initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom) + suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)) }) + ctx := suite.ctx.WithBlockTime(time.Date(2022, 1, 1, 0, 7, 0, 0, time.UTC)) + err := suite.keeper.MintPeriodInflation(ctx) + suite.NoError(err) + finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom) + suite.Equal(initialSupply, finalSupply) +} + +func (suite *MintTestSuite) TestMintPeriodNotStarted() { + initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom) + suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)) }) + ctx := suite.ctx.WithBlockTime(time.Date(2019, 1, 1, 0, 7, 0, 0, time.UTC)) + err := suite.keeper.MintPeriodInflation(ctx) + suite.NoError(err) + finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom) + suite.Equal(initialSupply, finalSupply) +} + +func (suite *MintTestSuite) TestMintOngoingPeriod() { + initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom) + suite.NotPanics(func() { + suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2020, time.March, 1, 1, 0, 1, 0, time.UTC)) + }) + ctx := suite.ctx.WithBlockTime(time.Date(2021, 2, 28, 23, 59, 59, 0, time.UTC)) + err := suite.keeper.MintPeriodInflation(ctx) + suite.NoError(err) + finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom) + suite.True(finalSupply.GT(initialSupply)) + mAcc := suite.supplyKeeper.GetModuleAccount(ctx, types.ModuleName) + mAccSupply := mAcc.GetCoins().AmountOf(types.GovDenom) + suite.True(mAccSupply.Equal(finalSupply.Sub(initialSupply))) + // expect that inflation is ~10% + expectedSupply := sdk.NewDecFromInt(initialSupply).Mul(sdk.MustNewDecFromStr("1.1")) + supplyError := sdk.OneDec().Sub((sdk.NewDecFromInt(finalSupply).Quo(expectedSupply))).Abs() + suite.True(supplyError.LTE(sdk.MustNewDecFromStr("0.001"))) +} + +func (suite *MintTestSuite) TestMintPeriodTransition() { + initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom) + params := suite.keeper.GetParams(suite.ctx) + periods := types.Periods{ + p[0], + types.Period{ + Start: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + } + params.Periods = periods + suite.NotPanics(func() { + suite.keeper.SetParams(suite.ctx, params) + }) + suite.NotPanics(func() { + suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2020, time.March, 1, 1, 0, 1, 0, time.UTC)) + }) + ctx := suite.ctx.WithBlockTime(time.Date(2021, 3, 10, 0, 0, 0, 0, time.UTC)) + err := suite.keeper.MintPeriodInflation(ctx) + suite.NoError(err) + finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom) + suite.True(finalSupply.GT(initialSupply)) +} + +func (suite *MintTestSuite) TestMintNotActive() { + initialSupply := suite.supplyKeeper.GetSupply(suite.ctx).GetTotal().AmountOf(types.GovDenom) + params := suite.keeper.GetParams(suite.ctx) + params.Active = false + suite.NotPanics(func() { + suite.keeper.SetParams(suite.ctx, params) + }) + suite.NotPanics(func() { + suite.keeper.SetPreviousBlockTime(suite.ctx, time.Date(2020, time.March, 1, 1, 0, 1, 0, time.UTC)) + }) + ctx := suite.ctx.WithBlockTime(time.Date(2021, 2, 28, 23, 59, 59, 0, time.UTC)) + err := suite.keeper.MintPeriodInflation(ctx) + suite.NoError(err) + finalSupply := suite.supplyKeeper.GetSupply(ctx).GetTotal().AmountOf(types.GovDenom) + suite.Equal(initialSupply, finalSupply) +} + +func TestMintTestSuite(t *testing.T) { + suite.Run(t, new(MintTestSuite)) +} diff --git a/x/kavadist/keeper/params.go b/x/kavadist/keeper/params.go new file mode 100644 index 00000000..526cb255 --- /dev/null +++ b/x/kavadist/keeper/params.go @@ -0,0 +1,18 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/kavadist/types" +) + +// GetParams returns the params from the store +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + var p types.Params + k.paramSubspace.GetParamSet(ctx, &p) + return p +} + +// SetParams sets params on the store +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramSubspace.SetParamSet(ctx, ¶ms) +} diff --git a/x/kavadist/module.go b/x/kavadist/module.go new file mode 100644 index 00000000..218655c6 --- /dev/null +++ b/x/kavadist/module.go @@ -0,0 +1,153 @@ +package kavadist + +import ( + "encoding/json" + "math/rand" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + sim "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/kava-labs/kava/x/kavadist/simulation" + "github.com/kava-labs/kava/x/kavadist/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleSimulation = AppModuleSimulation{} +) + +// AppModuleBasic app module basics object +type AppModuleBasic struct{} + +// Name get module name +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// ValidateGenesis module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) + if err != nil { + return err + } + return gs.Validate() +} + +// RegisterRESTRoutes registers no REST routes for the crisis module. +func (AppModuleBasic) RegisterRESTRoutes(_ context.CLIContext, _ *mux.Router) {} + +// GetTxCmd returns the root tx command for the crisis module. +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { return nil } + +// GetQueryCmd returns no root query command for the crisis module. +func (AppModuleBasic) GetQueryCmd(_ *codec.Codec) *cobra.Command { return nil } + +//____________________________________________________________________________ + +// AppModuleSimulation defines the module simulation functions used by the cdp module. +type AppModuleSimulation struct{} + +// RegisterStoreDecoder registers a decoder for cdp module's types +func (AppModuleSimulation) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[StoreKey] = simulation.DecodeStore +} + +// GenerateGenesisState creates a randomized GenState of the cdp module +func (AppModuleSimulation) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// RandomizedParams creates randomized cdp param changes for the simulator. +func (AppModuleSimulation) RandomizedParams(r *rand.Rand) []sim.ParamChange { + return simulation.ParamChanges(r) +} + +//____________________________________________________________________________ + +// AppModule app module type +type AppModule struct { + AppModuleBasic + AppModuleSimulation + + keeper Keeper + supplyKeeper types.SupplyKeeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + AppModuleSimulation: AppModuleSimulation{}, + keeper: keeper, + supplyKeeper: supplyKeeper, + } +} + +// Name module name +func (AppModule) Name() string { + return ModuleName +} + +// RegisterInvariants register module invariants +func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// Route module message route name +func (AppModule) Route() string { + return ModuleName +} + +// NewHandler module handler +func (am AppModule) NewHandler() sdk.Handler { + return NewHandler(am.keeper) +} + +// QuerierRoute module querier route name +func (AppModule) QuerierRoute() string { + return ModuleName +} + +// NewQuerierHandler returns no sdk.Querier. +func (AppModule) NewQuerierHandler() sdk.Querier { return nil } + +// InitGenesis module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState) + + return []abci.ValidatorUpdate{} +} + +// ExportGenesis module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// BeginBlock module begin-block +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + BeginBlocker(ctx, am.keeper) +} + +// EndBlock module end-block +func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/kavadist/simulation/decoder.go b/x/kavadist/simulation/decoder.go new file mode 100644 index 00000000..edef9051 --- /dev/null +++ b/x/kavadist/simulation/decoder.go @@ -0,0 +1,12 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/codec" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// DecodeStore unmarshals the KVPair's Value to the corresponding cdp type +func DecodeStore(cdc *codec.Codec, kvA, kvB cmn.KVPair) string { + // TODO implement this + return "" +} diff --git a/x/kavadist/simulation/params.go b/x/kavadist/simulation/params.go new file mode 100644 index 00000000..8c1f7aff --- /dev/null +++ b/x/kavadist/simulation/params.go @@ -0,0 +1,14 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// ParamChanges defines the parameters that can be modified by param change proposals +// on the simulation +func ParamChanges(r *rand.Rand) []simulation.ParamChange { + // TODO implement this + return []simulation.ParamChange{} +} diff --git a/x/kavadist/simulation/simulation.go b/x/kavadist/simulation/simulation.go new file mode 100644 index 00000000..a1fbd964 --- /dev/null +++ b/x/kavadist/simulation/simulation.go @@ -0,0 +1,22 @@ +package simulation + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/kava-labs/kava/x/kavadist/types" +) + +// RandomizedGenState generates a random GenesisState for cdp +func RandomizedGenState(simState *module.SimulationState) { + + // TODO implement this fully + // - randomly generating the genesis params + // - overwriting with genesis provided to simulation + genesis := types.DefaultGenesisState() + + fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesis)) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis) +} diff --git a/x/kavadist/types/codec.go b/x/kavadist/types/codec.go new file mode 100644 index 00000000..16afc367 --- /dev/null +++ b/x/kavadist/types/codec.go @@ -0,0 +1,17 @@ +package types + +import "github.com/cosmos/cosmos-sdk/codec" + +// ModuleCdc generic sealed codec to be used throughout module +var ModuleCdc *codec.Codec + +func init() { + cdc := codec.New() + RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + ModuleCdc = cdc.Seal() +} + +// RegisterCodec registers the necessary types for cdp module +func RegisterCodec(cdc *codec.Codec) { +} diff --git a/x/kavadist/types/errors.go b/x/kavadist/types/errors.go new file mode 100644 index 00000000..173f7a49 --- /dev/null +++ b/x/kavadist/types/errors.go @@ -0,0 +1,11 @@ +// DONTCOVER +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Error codes specific to kavadist module +const ( + DefaultCodespace sdk.CodespaceType = ModuleName +) diff --git a/x/kavadist/types/events.go b/x/kavadist/types/events.go new file mode 100644 index 00000000..9bdb0fed --- /dev/null +++ b/x/kavadist/types/events.go @@ -0,0 +1,9 @@ +package types + +// Event types for cdp module +const ( + EventTypeKavaDist = ModuleName + AttributeKeyInflation = "kava_dist_inflation" + AttributeKeyStatus = "kava_dist_status" + AttributeValueInactive = "inactive" +) diff --git a/x/kavadist/types/expected_keepers.go b/x/kavadist/types/expected_keepers.go new file mode 100644 index 00000000..a2e54262 --- /dev/null +++ b/x/kavadist/types/expected_keepers.go @@ -0,0 +1,15 @@ +package types // noalias + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/supply/exported" +) + +// SupplyKeeper defines the expected supply keeper +type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, name string) exported.ModuleAccountI + GetSupply(ctx sdk.Context) (supply exported.SupplyI) + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) sdk.Error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error +} diff --git a/x/kavadist/types/genesis.go b/x/kavadist/types/genesis.go new file mode 100644 index 00000000..e857ab0a --- /dev/null +++ b/x/kavadist/types/genesis.go @@ -0,0 +1,54 @@ +package types + +import ( + "bytes" + "fmt" + "time" +) + +// GenesisState is the state that must be provided at genesis. +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` +} + +// NewGenesisState returns a new genesis state +func NewGenesisState(params Params, previousBlockTime time.Time) GenesisState { + return GenesisState{ + Params: params, + PreviousBlockTime: previousBlockTime, + } +} + +// DefaultGenesisState returns a default genesis state +func DefaultGenesisState() GenesisState { + return GenesisState{ + Params: DefaultParams(), + PreviousBlockTime: DefaultPreviousBlockTime, + } +} + +// Validate performs basic validation of genesis data returning an +// error for any failed validation criteria. +func (gs GenesisState) Validate() error { + + if err := gs.Params.Validate(); err != nil { + return err + } + if gs.PreviousBlockTime.Equal(time.Time{}) { + return fmt.Errorf("previous block time not set") + } + return nil +} + +// Equal checks whether two gov GenesisState structs are equivalent +func (gs GenesisState) Equal(gs2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(gs) + b2 := ModuleCdc.MustMarshalBinaryBare(gs2) + return bytes.Equal(b1, b2) +} + +// IsEmpty returns true if a GenesisState is empty +func (gs GenesisState) IsEmpty() bool { + return gs.Equal(GenesisState{}) +} diff --git a/x/kavadist/types/keys.go b/x/kavadist/types/keys.go new file mode 100644 index 00000000..82ed8361 --- /dev/null +++ b/x/kavadist/types/keys.go @@ -0,0 +1,26 @@ +package types + +const ( + // ModuleName name that will be used throughout the module + ModuleName = "kavadist" + + // StoreKey Top level store key where all module items will be stored + StoreKey = ModuleName + + // RouterKey Top level router key + RouterKey = ModuleName + + // QuerierRoute Top level query string + QuerierRoute = ModuleName + + // DefaultParamspace default name for parameter store + DefaultParamspace = ModuleName + + // KavaDistMacc module account for kavadist + KavaDistMacc = ModuleName +) + +var ( + CurrentDistPeriodKey = []byte{0x00} + PreviousBlockTimeKey = []byte{0x01} +) diff --git a/x/kavadist/types/params.go b/x/kavadist/types/params.go new file mode 100644 index 00000000..6cb955b0 --- /dev/null +++ b/x/kavadist/types/params.go @@ -0,0 +1,102 @@ +package types + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" + cdptypes "github.com/kava-labs/kava/x/cdp/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +// Parameter keys and default values +var ( + KeyActive = []byte("Active") + KeyPeriods = []byte("Periods") + DefaultActive = false + DefaultPeriods = Periods{} + DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) + GovDenom = cdptypes.DefaultGovDenom +) + +// Params governance parameters for kavadist module +type Params struct { + Active bool `json:"active" yaml:"active"` + Periods Periods `json:"periods" yaml:"periods"` +} + +// Period stores the specified start and end dates, and the inflation, expressed as a decimal representing the yearly APR of KAVA tokens that will be minted during that period +type Period struct { + Start time.Time `json:"start" yaml:"start"` // example "2020-03-01T15:20:00Z" + End time.Time `json:"end" yaml:"end"` // example "2020-06-01T15:20:00Z" + Inflation sdk.Dec `json:"inflation" yaml:"inflation"` // example "1.000000003022265980" - 10% inflation +} + +// String implements fmt.Stringer +func (pr Period) String() string { + return fmt.Sprintf(`Period: + Start: %s + End: %s + Inflation: %s`, pr.Start, pr.End, pr.Inflation) +} + +// Periods array of Period +type Periods []Period + +// String implements fmt.Stringer +func (prs Periods) String() string { + out := "Periods\n" + for _, pr := range prs { + out += fmt.Sprintf("%s\n", pr) + } + return out +} + +// NewParams returns a new params object +func NewParams(active bool, periods Periods) Params { + return Params{ + Active: active, + Periods: periods, + } +} + +// DefaultParams returns default params for kavadist module +func DefaultParams() Params { + return NewParams(DefaultActive, DefaultPeriods) +} + +// String implements fmt.Stringer +func (p Params) String() string { + return fmt.Sprintf(`Params: + Active: %t + Periods %s`, p.Active, p.Periods) +} + +// ParamKeyTable Key declaration for parameters +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) +} + +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyActive, Value: &p.Active}, + {Key: KeyPeriods, Value: &p.Periods}, + } +} + +// Validate checks that the parameters have valid values. +func (p Params) Validate() error { + prevEnd := tmtime.Canonical(time.Unix(0, 0)) + for _, pr := range p.Periods { + if pr.End.Before(pr.Start) { + return fmt.Errorf("end time for period is before start time: %s", pr) + } + if pr.Start.Before(prevEnd) { + return fmt.Errorf("periods must be in chronological order: %s", p.Periods) + } + prevEnd = pr.End + } + return nil +} diff --git a/x/kavadist/types/params_test.go b/x/kavadist/types/params_test.go new file mode 100644 index 00000000..f2e02b90 --- /dev/null +++ b/x/kavadist/types/params_test.go @@ -0,0 +1,99 @@ +package types_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/kavadist/types" + "github.com/stretchr/testify/suite" +) + +type paramTest struct { + params types.Params + expectPass bool +} + +type ParamTestSuite struct { + suite.Suite + + tests []paramTest +} + +func (suite *ParamTestSuite) SetupTest() { + p1 := types.Params{ + Active: true, + Periods: types.Periods{ + types.Period{ + Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + types.Period{ + Start: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + }, + } + p2 := types.Params{ + Active: true, + Periods: types.Periods{ + types.Period{ + Start: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + types.Period{ + Start: time.Date(2023, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2024, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + }, + } + p3 := types.Params{ + Active: true, + Periods: types.Periods{ + types.Period{ + Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2021, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + types.Period{ + Start: time.Date(2020, time.March, 1, 1, 0, 0, 0, time.UTC), + End: time.Date(2022, time.March, 1, 1, 0, 0, 0, time.UTC), + Inflation: sdk.MustNewDecFromStr("1.000000003022265980"), + }, + }, + } + + suite.tests = []paramTest{ + paramTest{ + params: p1, + expectPass: true, + }, + paramTest{ + params: p2, + expectPass: false, + }, + paramTest{ + params: p3, + expectPass: false, + }, + } +} + +func (suite *ParamTestSuite) TestParamValidation() { + for _, t := range suite.tests { + err := t.params.Validate() + if t.expectPass { + suite.NoError(err) + } else { + suite.Error(err) + } + } +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(ParamTestSuite)) +} diff --git a/x/kavadist/types/querier.go b/x/kavadist/types/querier.go new file mode 100644 index 00000000..104ab89a --- /dev/null +++ b/x/kavadist/types/querier.go @@ -0,0 +1,6 @@ +package types + +// Querier routes for the kavadist module +const ( + QueryGetParams = "params" +) diff --git a/x/pricefeed/client/rest/tx.go b/x/pricefeed/client/rest/tx.go index 5ba7b8dc..6e0b87e6 100644 --- a/x/pricefeed/client/rest/tx.go +++ b/x/pricefeed/client/rest/tx.go @@ -15,7 +15,7 @@ import ( ) func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc(fmt.Sprintf("/%s/postprice", types.ModuleName), postPriceHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc(fmt.Sprintf("/%s/postprice", types.ModuleName), postPriceHandlerFn(cliCtx)).Methods("POST") }