diff --git a/CHANGELOG.md b/CHANGELOG.md index a8142e0e..e6af63e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,23 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * Updated Provenance proto set to 1.11.0 #371 +* Add Support for Tx Msg building #355 + * Now supports building Tx msgs through API + * POST `/api/v3/gov/types/supported` - Gives list of proposal types supported for governance msgs + * POST `/api/v3/gov/submit/{type}` - Crafts a Submit Proposal msg from the given data + * POST `/api/v3/gov/deposit` - Crafts a Deposit msg + * POST `/api/v3/gov/vote` - Crafts a Vote msg + * POST `/api/v3/staking/delegate` - Crafts a Delegate msg + * POST `/api/v3/staking/redelegate` - Crafts a Redelegate msg + * POST `/api/v3/staking/undelegate` - Crafts an Undelegate msg + * POST `/api/v3/staking/withdraw_rewards` - Crafts a Withdraw Rewards msg + * POST `/api/v3/staking/withdraw_commission` - Crafts a Withdraw Commission msg + * POST `/api/v2/accounts/send` - Crafts a Send msg + * Validations included to allow for better support + * Docs included to help with usage + +### Improvements +* Added Validation message collection to allow for multiple validations at once #355 ## [v4.3.1](https://github.com/provenance-io/explorer-service/releases/tag/v4.3.1) - 2022-06-24 ### Release Name: James of Ireland @@ -61,7 +78,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Bug Fixes * Fixed the Validator missed block count * Added else case for IBC Recvs where the effected Recv is in the same tx as an uneffected Recv, which makes the block error out -* Added VotWeighted msg type to `getGovMsgDetail()` function +* Added VoteWeighted msg type to `getGovMsgDetail()` function * Fixed how addresses were being associated with txs ### Data diff --git a/build.gradle.kts b/build.gradle.kts index c4f5ac98..104fbe37 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,7 +46,11 @@ subprojects { } tasks.withType { kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict", "-Xopt-in=kotlin.RequiresOptIn") + freeCompilerArgs = listOf( + "-Xjsr305=strict", + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlin.contracts.ExperimentalContracts" + ) jvmTarget = "11" languageVersion = "1.5" apiVersion = "1.5" diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 5d46cc50..ae9be5f4 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -55,6 +55,7 @@ object Versions { const val Grpc = "1.40.1" const val ProvProto = "1.11.0" const val Postgres = "42.2.23" + const val Protobuf = "3.19.1" // Testing const val Jupiter = "5.7.1" @@ -92,6 +93,7 @@ object Libraries { // Protobuf const val GrpcNetty = "io.grpc:grpc-netty:${Versions.Grpc}" const val ProvenanceProto = "io.provenance:proto-kotlin:${Versions.ProvProto}" + const val ProtobufKotlin = "com.google.protobuf:protobuf-kotlin:${Versions.Protobuf}" // Spring const val SpringBootDevTools = "org.springframework.boot:spring-boot-devtools:${Versions.SpringBoot}" diff --git a/docker/docker-compose-db.yml b/docker/docker-compose-db.yml index 5c054f7b..3233a5b5 100644 --- a/docker/docker-compose-db.yml +++ b/docker/docker-compose-db.yml @@ -1,25 +1,8 @@ version: '3.5' -services: - explorer-postgres: - image: postgres:13.2 - container_name: postgres-local-testnet - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password1 - ports: - - 5432:5432 - volumes: - - ./db-init:/docker-entrypoint-initdb.d/ # inits the db with username/password - - pg-local-testnet:/var/lib/postgresql/data - -volumes: - pg-local-testnet: - - #services: # explorer-postgres: # image: postgres:13.2 -# container_name: postgres-local-mainnet +# container_name: postgres-local-testnet # environment: # - POSTGRES_USER=postgres # - POSTGRES_PASSWORD=password1 @@ -27,7 +10,24 @@ volumes: # - 5432:5432 # volumes: # - ./db-init:/docker-entrypoint-initdb.d/ # inits the db with username/password -# - pg-local-mainnet:/var/lib/postgresql/data +# - pg-local-testnet:/var/lib/postgresql/data # #volumes: -# pg-local-mainnet: +# pg-local-testnet: + + +services: + explorer-postgres: + image: postgres:13.2 + container_name: postgres-local-mainnet + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password1 + ports: + - 5432:5432 + volumes: + - ./db-init:/docker-entrypoint-initdb.d/ # inits the db with username/password + - pg-local-mainnet:/var/lib/postgresql/data + +volumes: + pg-local-mainnet: diff --git a/docs/msgs/Account Msgs.md b/docs/msgs/Account Msgs.md new file mode 100644 index 00000000..adcd4d45 --- /dev/null +++ b/docs/msgs/Account Msgs.md @@ -0,0 +1,36 @@ +# Account Msgs +* [Delegate](#delegate) +* [Redelegate](#redelegate) +* [Undelegate](#undelegate) +* [Withdraw Rewards](#withdraw-rewards) +* [Withdraw Commission](#withdraw-commission) + + +## Send +To craft a MsgDelegate, use `/api/v3/accounts/send` +Example CURL request: +```shell +curl -X POST "http://localhost:8612/api/v2/accounts/send" \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -H "authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==" \ + --data-raw '{"from":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","funds":[{"amount":"100","denom":"nhash"}],"to":"pb1mcyukv73v57jm2cq48p6y666kqjed8suyphshq"}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "from": "string", + "funds": [ + { + "amount": "string", + "denom": "string" + } + ], + "to": "string" +} +``` +* `from` - The same address of the signer +* `to` - A valid standard address; must be different that the `from` address +* `amount` - A list of funds to send, in valid denoms diff --git a/docs/msgs/Crafting Msgs.md b/docs/msgs/Crafting Msgs.md new file mode 100644 index 00000000..b9d6ad0b --- /dev/null +++ b/docs/msgs/Crafting Msgs.md @@ -0,0 +1,34 @@ +# Crafting Msgs for Txs + +Explorer service now supports crafting specific msgs through APIs. This does not submit the msg, only provides validation +and formatting/packing. + +* [Governance Msgs](Gov%20Msgs.md) +* [Staking Msgs](Staking%20Msgs.md) +* [Account Msgs](Account%20Msgs.md) + +After validating and building the basic msg, the API will pack the msg into the `TxBody` object type, and return the following: +```json +{ + "json": { + "messages": [ + { + "@type": "/cosmos.gov.v1beta1.MsgVote", + "proposalId": "36", + "voter": "pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q", + "option": "VOTE_OPTION_YES" + } + ] + }, + "base64": [ + "ChsvY29zbW9zLmdvdi52MWJldGExLk1zZ1ZvdGUSLwgkEilwYjE5NDlhZWVqYXlmczRzcnRsOHNjNjcyaDJtMDN4YXJ6NDZ1bno0cRgB" + ] +} +``` +* `json` - A list of readable msgs to show what was built +* `base64` - The list of built msgs encoded into base64 for proper Tx usage + +The `json` is used to display to the user prior to signing, so they see what was built exactly. The `base64` is used to +build the Tx properly for submission. + + diff --git a/docs/msgs/Gov Msgs.md b/docs/msgs/Gov Msgs.md new file mode 100644 index 00000000..b19dd8a3 --- /dev/null +++ b/docs/msgs/Gov Msgs.md @@ -0,0 +1,175 @@ +# Governance Msgs +* [Submit Proposal](#submit-proposal) + + [Supported Proposal Types](#supported-proposal-types) + + TEXT + * PARAMETER_CHANGE + * SOFTWARE_UPGRADE + * CANCEL_SOFTWARE + * STORE_CODE + * INSTANTIATE_CONTRACT + + [Submit Proposal Crafting Request](#submit-proposal-crafting-request) +* [Vote and WeightedVote](#vote-and-weightedvote) +* [Deposit](#deposit) + + +## Submit Proposal +### Supported Proposal Types +There is an API to fetch the supported types and their content object structure: `/api/v3/gov/types/supported` +This will give you the enum value mapped to the content structure to be used when submitting a proposal msg creation +request. +```json +{ + "TEXT": {}, + "PARAMETER_CHANGE": { + "changes": [ + { + "subspace": "param_space", + "key": "param_key", + "value": "param_value" + }, + { + "subspace": "attribute", + "key": "not_a_real_key", + "value": "blah" + } + ] + }, + "SOFTWARE_UPGRADE": { + "name": "Test Name", + "height": 1000087, + "info": "This is info for the upgrade." + }, + "CANCEL_UPGRADE": {}, + "STORE_CODE": { + "runAs": "run as address", + "accessConfig": { + "type": "ACCESS_TYPE_EVERYBODY", + "address": "Only set to an address if ACCESS_TYPE_ONLY_ADDRESS, else null" + } + }, + "INSTANTIATE_CONTRACT": { + "runAs": "run as address", + "admin": "admin address or null", + "codeId": 140, + "label": "Unique label for easy identification", + "msg": "stringified JSON object for msg data to be used by the contract", + "funds": [ + { + "amount": "100", + "denom": "some_denom" + } + ] + } +} +``` + +### Submit Proposal Crafting Request +To craft a MsgSubmitProposal, use `/api/v3/gov/submit/{type}` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/gov/submit/{PROPOSAL_TYPE}' \ + -H 'accept: application/json' \ + -H 'authorization: {JWT_TOKEN}' \ + -H 'content-type: multipart/form-data' \ + -F 'request={ "content": {THIS IS THE CORRESPONDING OBJECT STRUCTURE STRINGIFIED}, "description": "Test", "initialDeposit": [ { "amount": "1000", "denom": "nhash" } ], "submitter": "THIS IS YOUR SUBMITTING ADDRESS", "title": "Test proposal" };type=application/json' \ + -F 'wasmFile=@{THIS IS THE PATH TO YOUR .wasm FILE};type=application/octet-stream' \ <------------ EXCLUDE IF NOT SUBMITTING A .wasm FILE + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "content": "string", + "description": "string", + "initialDeposit": [ + { + "amount": "string", + "denom": "string" + } + ], + "submitter": "string", + "title": "string" +} +``` +* `content` - This is a stringified JSON object as mapped to the PROPOSAL_TYPE enum values coming from the [Supported Types API](#supported-proposal-types) +* `description` - The description for the proposal +* `initialDeposit` - Can be an empty list if no deposit is attached to the proposal request; Should be in denom `nhash` +* `submitter` - The same address of the signer +* `title` - The title of the proposal + +Ensure the `request` is typed to `type=application/json`. + +For the `wasmFile` portion, ensure you have the path to the .wasm file, and it is typed to `type=application/octet-stream`. + + +## Vote and WeightedVote +To craft a MsgVote or MsgWeightedVote, use `/api/v3/gov/vote` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/gov/vote' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"proposalId":36,"voter":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","votes":[{"option":"VOTE_OPTION_YES","weight":100}]}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "proposalId": 0, + "voter": "string", + "votes": [ + { + "option": "UNRECOGNIZED", + "weight": 0 + } + ] +} +``` +* `proposalId` - A valid proposal ID; must be in Voting Period +* `voter` - The same address of the signer +* `votes` - A list of votes with weights; The weights must add up to 100 + * Supported vote options come from the Gov.VoteOption proto enum + * VOTE_OPTION_YES + * VOTE_OPTION_ABSTAIN + * VOTE_OPTION_NO + * VOTE_OPTION_NO_WITH_VETO + +If there is a single vote in the `votes` array, a MsgVote will be crafted. Otherwise, a MsgWeightedVote will be crafted. + +## Deposit +To craft a MsgDeposit, use `/api/v3/gov/deposit` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/gov/deposit' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"deposit":[{"amount":"10000","denom":"nhash"}],"depositor":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","proposalId":36}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "deposit": [ + { + "amount": "string", + "denom": "string" + } + ], + "depositor": "string", + "proposalId": 0 +} +``` +* `proposalId` - A valid proposal ID; must be in Deposit Period or Voting Period +* `depositor` - The same address of the signer +* `deposit` - A list of amounts to be used as a deposit against the proposal; typically the denom is `nhash`, with at +least one amount greater than zero + + + + + + diff --git a/docs/msgs/Staking Msgs.md b/docs/msgs/Staking Msgs.md new file mode 100644 index 00000000..593754df --- /dev/null +++ b/docs/msgs/Staking Msgs.md @@ -0,0 +1,133 @@ +# Staking Msgs +* [Delegate](#delegate) +* [Redelegate](#redelegate) +* [Undelegate](#undelegate) +* [Withdraw Rewards](#withdraw-rewards) +* [Withdraw Commission](#withdraw-commission) + + +## Delegate +To craft a MsgDelegate, use `/api/v3/staking/delegate` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/staking/delegate' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"amount":{"amount":"100","denom":"nhash"},"delegator":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","validator":"pbvaloper1q0xydatnq9pevcjsj7phs4kty98g8430j67u95"}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "amount": { + "amount": "string", + "denom": "string" + }, + "delegator": "string", + "validator": "string" +} +``` +* `delegator` - The same address of the signer +* `validator` - The operating address of a validator +* `amount` - The amount to delegate, in denom `nhash` + +## Redelegate +To craft a MsgBeginRedelegate, use `/api/v3/staking/redelegate` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/staking/delegate' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"amount":{"amount":"100","denom":"nhash"},"delegator":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","validatorDst":"pbvaloper1q0xydatnq9pevcjsj7phs4kty98g8430j67u95","validatorDst":"pbvaloper1q0xydatnq9pevcjsj7phs4kty98g8430j67u95"}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "amount": { + "amount": "string", + "denom": "string" + }, + "delegator": "string", + "validatorDst": "string", + "validatorSrc": "string" +} +``` +* `delegator` - The same address of the signer +* `validatorSrc` - The operating address of a validator the delegator is currently delegated to +* `validatorDst` - The operating address of a validator the delegator wants to move their delegation to; must be different that the source validator +* `amount` - The amount to redelegate, in denom `nhash` + +## Undelegate +To craft a MsgUndelegate, use `/api/v3/staking/undelegate` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/staking/delegate' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"amount":{"amount":"100","denom":"nhash"},"delegator":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","validator":"pbvaloper1q0xydatnq9pevcjsj7phs4kty98g8430j67u95"}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "amount": { + "amount": "string", + "denom": "string" + }, + "delegator": "string", + "validator": "string" +} +``` +* `delegator` - The same address of the signer +* `validator` - The operating address of a validator the delegator is currently delegated to +* `amount` - The amount to undelegate, in denom `nhash` + +## Withdraw Rewards +To craft a MsgWithdrawDelegatorReward, use `/api/v3/staking/withdraw_rewards` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/staking/withdraw_rewards' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"delegator":"pb1949aeejayfs4srtl8sc672h2m03xarz46unz4q","validator":"pbvaloper1q0xydatnq9pevcjsj7phs4kty98g8430j67u95"}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "delegator": "string", + "validator": "string" +} +``` +* `delegator` - The same address of the signer +* `validator` - The operating address of a validator the delegator is currently delegated to, and wishes to withdraw rewards from + +## Withdraw Commission +To craft a MsgWithdrawValidatorCommission, use `/api/v3/staking/withdraw_commission` +Example CURL request: +```shell +curl -X POST 'http://localhost:8612/api/v3/staking/withdraw_commission' \ + -H 'accept: application/json' \ + -H 'authorization: Bearer eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ==.eyJzdWIiOiJBMFBMcWdKQ3BaeklXK0xNckFzT003bFpXM2RMejRtaE00YnVWTmk0K2pnaSIsImlzcyI6InByb3ZlbmFuY2UuaW8iLCJpYXQiOjE2NTU0MDg2NjIsImV4cCI6MTY1NTQ5NTA2MiwiYWRkciI6InBiMTk0OWFlZWpheWZzNHNydGw4c2M2NzJoMm0wM3hhcno0NnVuejRxIn0=.Tbt2Qg62qoFhW959UUPL6wJcGc1tERfOaQhPTmn4FgFiG1+WuAZbQzQrFmtgJlqRQbAJZ3QF08UVJ5xiJi5R7A==' \ + -H 'content-type: application/json' \ + --data-raw '{"validator":"pbvaloper1q0xydatnq9pevcjsj7phs4kty98g8430j67u95"}' \ + --compressed +``` + +For the `request` portion, use the following structure: +```json +{ + "validator": "string" +} +``` +* `validator` - The operating address of a validator the signing address owns + diff --git a/service/build.gradle.kts b/service/build.gradle.kts index 5dd025f9..4b8458d9 100644 --- a/service/build.gradle.kts +++ b/service/build.gradle.kts @@ -22,6 +22,7 @@ sourceSets { dependencies { implementation(project(":database")) + implementation(Libraries.ProtobufKotlin) implementation(Libraries.ProvenanceProto) implementation(Libraries.KotlinReflect) implementation(Libraries.KotlinStdlib) diff --git a/service/src/main/kotlin/io/provenance/explorer/config/AppConfig.kt b/service/src/main/kotlin/io/provenance/explorer/config/AppConfig.kt new file mode 100644 index 00000000..7d6c4ff5 --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/config/AppConfig.kt @@ -0,0 +1,21 @@ +package io.provenance.explorer.config + +import io.provenance.explorer.config.interceptor.JwtInterceptor +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class AppConfig : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.let { super.addInterceptors(it) } + registry.addInterceptor(JwtInterceptor()).excludePathPatterns( + "/external/**", + "/swagger*/**", + "/webjars/**", + "/v2/api-docs*", + "/v3/api-docs*" + ) + } +} diff --git a/service/src/main/kotlin/io/provenance/explorer/config/GrpcLoggingInterceptor.kt b/service/src/main/kotlin/io/provenance/explorer/config/interceptor/GrpcLoggingInterceptor.kt similarity index 96% rename from service/src/main/kotlin/io/provenance/explorer/config/GrpcLoggingInterceptor.kt rename to service/src/main/kotlin/io/provenance/explorer/config/interceptor/GrpcLoggingInterceptor.kt index 520f2d86..b67435ca 100644 --- a/service/src/main/kotlin/io/provenance/explorer/config/GrpcLoggingInterceptor.kt +++ b/service/src/main/kotlin/io/provenance/explorer/config/interceptor/GrpcLoggingInterceptor.kt @@ -1,4 +1,4 @@ -package io.provenance.explorer.config +package io.provenance.explorer.config.interceptor import io.grpc.CallOptions import io.grpc.Channel diff --git a/service/src/main/kotlin/io/provenance/explorer/config/interceptor/JwtInterceptor.kt b/service/src/main/kotlin/io/provenance/explorer/config/interceptor/JwtInterceptor.kt new file mode 100644 index 00000000..32d996c7 --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/config/interceptor/JwtInterceptor.kt @@ -0,0 +1,46 @@ +package io.provenance.explorer.config.interceptor + +import io.provenance.explorer.OBJECT_MAPPER +import io.provenance.explorer.domain.extensions.fromBase64 +import org.springframework.http.HttpHeaders +import org.springframework.web.servlet.HandlerInterceptor +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class JwtInterceptor : HandlerInterceptor { + + companion object { + const val X_ADDRESS = "x-address" + const val X_PUBLIC_KEY = "x-public-key" + } + + override fun preHandle( + request: HttpServletRequest, + response: HttpServletResponse, + handler: Any + ): Boolean { + val jwt = request.getHeader(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer")?.trimStart() + + if (!jwt.isNullOrBlank()) { + val authPayload = jwt.toAuthPayload() + request.setAttribute(X_ADDRESS, authPayload.addr) + request.setAttribute(X_PUBLIC_KEY, authPayload.sub) + } + + return super.preHandle( + request, + response, + handler + ) + } +} + +fun String.toAuthPayload() = OBJECT_MAPPER.readValue(this.split(".")[1].fromBase64(), AuthPayload::class.java) + +data class AuthPayload( + val addr: String, + val sub: String, + val iss: String, + val exp: Long, + val iat: Long +) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt index 0a9e61b5..9d2daa48 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/entities/Governance.kt @@ -408,7 +408,7 @@ class ProposalMonitorRecord(id: EntityID) : IntEntity(id) { submittedHeight: Int, proposedCompletionHeight: Int, votingEndTime: DateTime, - proposalType: ProposalType, + proposalType: MonitorProposalType, dataHash: String ) = listOf( -1, @@ -458,4 +458,4 @@ class ProposalMonitorRecord(id: EntityID) : IntEntity(id) { var processed by ProposalMonitorTable.processed } -enum class ProposalType { STORE_CODE } +enum class MonitorProposalType { STORE_CODE } diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/exceptions/Validations.kt b/service/src/main/kotlin/io/provenance/explorer/domain/exceptions/Validations.kt new file mode 100644 index 00000000..03398bf7 --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/domain/exceptions/Validations.kt @@ -0,0 +1,27 @@ +package io.provenance.explorer.domain.exceptions + +import kotlin.contracts.contract + +inline fun requireToMessage(value: Boolean, lazyMessage: () -> String): String? { + contract { + returns() implies value + } + return if (!value) { lazyMessage() } else null +} + +inline fun requireNotNullToMessage(value: T?, lazyMessage: () -> String): String? { + contract { + returns() implies (value != null) + } + return if (value == null) { lazyMessage() } else null +} + +fun validate(vararg validations: String?) { + validations.filterNotNull() + .let { + if (it.isNotEmpty()) { + val msg = it.joinToString("; \n\t", "Validation Failures: \n\t") + throw InvalidArgumentException(msg) + } + } +} diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt index a5648996..cc72211b 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/CoinExtensions.kt @@ -33,3 +33,13 @@ fun String.toDecimal() = BigDecimal(this.toBigInteger(), 18).stripTrailingZeros( fun Double.toPercentage() = "${this * 100}%" fun List.avg() = this.sum() / this.size + +fun Int.padToDecString() = (this * 1e16).toString() + +fun List.isZero(): Boolean { + this.forEach { + if (it.amount.toLong() != 0L) + return false + } + return true +} diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/ProtoExtensions.kt b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/ProtoExtensions.kt new file mode 100644 index 00000000..24163ecb --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/ProtoExtensions.kt @@ -0,0 +1,32 @@ +package io.provenance.explorer.domain.extensions + +import com.fasterxml.jackson.databind.node.ObjectNode +import com.google.protobuf.Any +import com.google.protobuf.Message +import com.google.protobuf.util.JsonFormat +import cosmos.tx.v1beta1.TxOuterClass.TxBody +import io.provenance.explorer.OBJECT_MAPPER + +data class TxMessageBody( + val json: ObjectNode, + val base64: List +) + +fun Message.pack(): Any = Any.pack(this, "") + +fun Iterable.toTxBody(memo: String? = null, timeoutHeight: Long? = null): TxBody = + TxBody.newBuilder() + .addAllMessages(this) + .also { builder -> + memo?.run { builder.memo = this } + timeoutHeight?.run { builder.timeoutHeight = this } + } + .build() + +fun Any.toTxBody(memo: String? = null, timeoutHeight: Long? = null): TxBody = + listOf(this).toTxBody(memo, timeoutHeight) + +fun TxBody.toTxMessageBody(printer: JsonFormat.Printer) = TxMessageBody( + json = OBJECT_MAPPER.readValue(printer.print(this), ObjectNode::class.java), + base64 = this.messagesList.map { it.toByteArray().base64EncodeString() } +) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/MsgCreationModels.kt b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/MsgCreationModels.kt new file mode 100644 index 00000000..20fd9f9d --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/domain/models/explorer/MsgCreationModels.kt @@ -0,0 +1,154 @@ +package io.provenance.explorer.domain.models.explorer + +import cosmos.base.v1beta1.coin +import cosmos.gov.v1beta1.Gov +import cosmwasm.wasm.v1.Types + +//region Governance Msg Models +enum class ProposalType(val example: BaseProposal) { + TEXT(BaseProposal()), + PARAMETER_CHANGE( + ParameterChangeData( + listOf( + ParamChangeObj("param_space", "param_key", "param_value"), + ParamChangeObj("attribute", "not_a_real_key", "blah") + ) + ) + ), + SOFTWARE_UPGRADE(SoftwareUpgradeData("Test Name", 1000087, "This is info for the upgrade.")), + CANCEL_UPGRADE(BaseProposal()), + STORE_CODE( + StoreCodeData( + "run as address", + StoreCodeAccessConfig( + Types.AccessType.ACCESS_TYPE_EVERYBODY, + "Only set to an address if ${Types.AccessType.ACCESS_TYPE_ONLY_ADDRESS.name}, else null" + ) + ) + ), + INSTANTIATE_CONTRACT( + InstantiateContractData( + "run as address", + "admin address or null", + 140, + "Unique label for easy identification", + "stringified JSON object for msg data to be used by the contract", + listOf(CoinStr("100", "some_denom")) + ) + ) +} + +open class BaseProposal + +data class GovSubmitProposalRequest( + val submitter: String, + val title: String, + val description: String, + val content: String, + val initialDeposit: List +) + +data class SoftwareUpgradeData( + val name: String, + val height: Long, + val info: String +) : BaseProposal() + +data class ParameterChangeData( + val changes: List +) : BaseProposal() + +data class ParamChangeObj( + val subspace: String, + val key: String, + val value: String +) + +data class StoreCodeData( + val runAs: String, + val accessConfig: StoreCodeAccessConfig? = null, +) : BaseProposal() + +data class StoreCodeAccessConfig( + val type: Types.AccessType, + val address: String? = null +) + +data class InstantiateContractData( + val runAs: String, + val admin: String? = null, + val codeId: Int, + val label: String? = null, + val msg: String, + val funds: List = emptyList(), +) : BaseProposal() + +data class GovDepositRequest( + val proposalId: Long, + val depositor: String, + val deposit: List +) + +data class GovVoteRequest( + val proposalId: Long, + val voter: String, + val votes: List +) + +data class WeightedVoteOption( + val weight: Int, + val option: Gov.VoteOption +) + +//endregion + +//region Staking Msg Models +data class StakingDelegateRequest( + val delegator: String, + val validator: String, + val amount: CoinStr +) + +data class StakingRedelegateRequest( + val delegator: String, + val validatorSrc: String, + val validatorDst: String, + val amount: CoinStr +) + +data class StakingUndelegateRequest( + val delegator: String, + val validator: String, + val amount: CoinStr +) + +data class StakingWithdrawRewardsRequest( + val delegator: String, + val validator: String +) + +data class StakingWithdrawCommissionRequest( + val validator: String +) + +//endregion + +//region Bank Msg Models +data class BankSendRequest( + val from: String, + val to: String, + val funds: List +) + +//endregion + +fun List.mapToProtoCoin() = + this.groupBy({ it.denom }) { it.amount.toLong() } + .mapValues { (_, v) -> v.sum() } + .filter { it.value > 0L } + .map { (k, v) -> + coin { + denom = k + amount = v.toString() + } + } diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt index 07346048..8d7804a2 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/Domain.kt @@ -12,6 +12,7 @@ import cosmos.slashing.v1beta1.Slashing import io.grpc.Metadata import io.grpc.stub.AbstractStub import io.grpc.stub.MetadataUtils +import io.provenance.explorer.config.ExplorerProperties import io.provenance.explorer.config.ResourceNotFoundException import io.provenance.explorer.domain.core.logger import io.provenance.explorer.domain.core.toBech32Data @@ -93,6 +94,10 @@ fun Any.toAddress(hrpPrefix: String) = else -> null.also { logger().error("This typeUrl is not supported as a consensus address: $typeUrl") } } +fun String.isStandardAddress(props: ExplorerProperties) = + this.startsWith(props.provAccPrefix()) && !this.startsWith(props.provValOperPrefix()) +fun String.isValidatorAddress(props: ExplorerProperties) = this.startsWith(props.provValOperPrefix()) + // TODO: Once cosmos-sdk implements a grpc endpoint for this we can replace this with grpc Issue: https://github.com/cosmos/cosmos-sdk/issues/9437 fun getEscrowAccountAddress(portId: String, channelId: String, hrpPrefix: String): String { val contents = "$portId/$channelId".toByteArray() diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/MsgConverter.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/MsgConverter.kt index 33e0b656..2efafe4b 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/MsgConverter.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/extensions/MsgConverter.kt @@ -208,7 +208,7 @@ fun Any.toMsgRevoke() = this.unpack(cosmos.authz.v1beta1.Tx.MsgRevoke::class.jav fun Any.toMsgGrantAllowance() = this.unpack(cosmos.feegrant.v1beta1.Tx.MsgGrantAllowance::class.java) fun Any.toMsgRevokeAllowance() = this.unpack(cosmos.feegrant.v1beta1.Tx.MsgRevokeAllowance::class.java) -// ///////// ADDRESSES +//region ADDRESSES fun Any.getAssociatedAddresses(): List = when { typeUrl.endsWith("MsgSend") -> this.toMsgSend().let { listOf(it.fromAddress, it.toAddress) } @@ -342,7 +342,9 @@ fun Any.getAssociatedAddresses(): List = else -> listOf().also { logger().debug("This typeUrl is not yet supported as an address-based msg: $typeUrl") } } -// ///////// DENOMS +//endregion + +//region DENOMS fun Any.getAssociatedDenoms(): List = when { typeUrl.endsWith("MsgSend") -> this.toMsgSend().let { it.amountList.map { am -> am.denom } } @@ -382,7 +384,9 @@ fun Any.getAssociatedDenoms(): List = .also { logger().debug("This typeUrl is not yet supported as an asset-based msg: $typeUrl") } } -// ///////// IBC +//endregion + +//region IBC fun Any.isIbcTransferMsg() = typeUrl.endsWith("MsgTransfer") fun Any.getTxIbcClientChannel() = @@ -493,7 +497,9 @@ fun Any.getIbcLedgerMsgs() = else -> null.also { logger().debug("This typeUrl is not yet supported in as an ibc ledger msg: $typeUrl") } } -// ///////// METADATA (NFT/SCOPES) +//endregion + +//region METADATA (NFT/SCOPES) enum class MdEvents(val event: String, val idField: String) { // Contract Spec CSPC("provenance.metadata.v1.EventContractSpecificationCreated", "contract_specification_addr"), @@ -624,7 +630,9 @@ fun SessionIdComponents?.toMAddress() = this.sessionUuid.toMAddressSession(scope) } else null -// ///////// GOVERNANCE +//endregion + +//region GOVERNANCE enum class GovMsgType { PROPOSAL, VOTE, WEIGHTED, DEPOSIT } fun Any.getAssociatedGovMsgs() = @@ -636,7 +644,9 @@ fun Any.getAssociatedGovMsgs() = else -> null.also { logger().debug("This typeUrl is not a governance-based msg: $typeUrl") } } -// ///////// SMART CONTRACTS +//endregion + +//region SMART CONTRACTS fun Any.getAssociatedSmContractMsgs() = when { typeUrl.endsWith("v1.MsgStoreCode") -> null @@ -678,7 +688,9 @@ enum class SmContractEventKeys(val eventType: String, val eventKey: Map null.also { logger().debug("This typeUrl is not yet supported in as a Name msg: $typeUrl") } } -// ///////// DENOM EVENTS +//endregion + +//region DENOM EVENTS enum class DenomEvents(val event: String, val idField: String, val parse: Boolean = false) { TRANSFER("transfer", "amount", true), MARKER_TRANSFER("provenance.marker.v1.EventMarkerTransfer", "denom"), @@ -719,7 +733,9 @@ fun String.denomEventRegexParse() = if (this.isNotBlank()) this.split(",").map { Regex("^([0-9]+)(.*)\$").matchEntire(it)!!.groups[2]!!.value } else emptyList() -// ///////// ADDRESS EVENTS +//endregion + +//region ADDRESS EVENTS enum class AddressEvents(val event: String, vararg val idField: String) { TRANSFER("transfer", "sender", "recipient"), MARKER_TRANSFER("provenance.marker.v1.EventMarkerTransfer", "administrator", "to_address", "from_address"), @@ -748,7 +764,9 @@ fun getAddressEventByEvent(event: String) = AddressEvents.values().firstOrNull { fun String.scrubQuotes() = this.removeSurrounding("\"") -// ///////// MSG TO DEFINED EVENT +//endregion + +//region MSG TO DEFINED EVENT // This links a msg type to a specific event it always emits. Helps to identify actions within an ExecuteContract msg // When this is updated, update update_tx_fees() and update_market_rate() procedures as well enum class MsgToDefinedEvent(val msg: String, val definedEvent: String, val uniqueField: String) { @@ -776,7 +794,6 @@ fun getDefinedEventsByMsg(msg: String) = MsgToDefinedEvent.values().firstOrNull fun getDefinedEventsByEvent(event: String) = MsgToDefinedEvent.values().firstOrNull { it.definedEvent == event } fun getByDefinedEvent() = MsgToDefinedEvent.values().associateBy { it.definedEvent } -fun getExecuteContractTypeUrl() = - Any.pack(cosmwasm.wasm.v1.Tx.MsgExecuteContract.getDefaultInstance()).typeUrl - .split("/")[1] - .let { "/$it" } +fun getExecuteContractTypeUrl() = Any.pack(cosmwasm.wasm.v1.Tx.MsgExecuteContract.getDefaultInstance(), "").typeUrl + +//endregion diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AccountGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AccountGrpcClient.kt index 1a739a01..f6bb6226 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AccountGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AccountGrpcClient.kt @@ -13,7 +13,7 @@ import cosmos.staking.v1beta1.queryDelegatorDelegationsResponse import cosmos.staking.v1beta1.queryDelegatorUnbondingDelegationsRequest import cosmos.staking.v1beta1.queryRedelegationsRequest import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getPagination import org.springframework.stereotype.Component import java.net.URI diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AttributeGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AttributeGrpcClient.kt index 61533ea3..891a2150 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AttributeGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/AttributeGrpcClient.kt @@ -4,7 +4,7 @@ import io.grpc.ManagedChannelBuilder import io.provenance.attribute.v1.Attribute import io.provenance.attribute.v1.queryAttributesRequest import io.provenance.attribute.v1.queryParamsRequest -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getPagination import io.provenance.name.v1.queryResolveRequest import io.provenance.name.v1.queryReverseLookupRequest diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt index a47e5b1b..de6eb90e 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BankGrpcClient.kt @@ -1,7 +1,7 @@ package io.provenance.explorer.grpc.v1 import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.domain.core.logger import io.provenance.explorer.domain.extensions.toDecimalString import org.springframework.stereotype.Component diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BlockGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BlockGrpcClient.kt index 3b29bc48..a8a888db 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BlockGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/BlockGrpcClient.kt @@ -10,7 +10,7 @@ import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.provenance.explorer.KTOR_CLIENT_JAVA import io.provenance.explorer.config.ExplorerProperties -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.domain.exceptions.FigmentApiException import kotlinx.coroutines.runBlocking import org.springframework.stereotype.Component diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt index 4b9f68e0..828a1916 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/GovGrpcClient.kt @@ -6,7 +6,7 @@ import cosmos.gov.v1beta1.queryTallyResultRequest import cosmos.upgrade.v1beta1.queryAppliedPlanRequest import cosmos.upgrade.v1beta1.queryCurrentPlanRequest import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.domain.models.explorer.GovParamType import io.provenance.explorer.grpc.extensions.addBlockHeightToQuery import org.springframework.stereotype.Component diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/IbcGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/IbcGrpcClient.kt index 547bf9d0..9781395a 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/IbcGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/IbcGrpcClient.kt @@ -1,7 +1,7 @@ package io.provenance.explorer.grpc.v1 import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getEscrowAccountAddress import org.springframework.stereotype.Component import java.net.URI diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MarkerGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MarkerGrpcClient.kt index 2dd34742..4c5b866c 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MarkerGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MarkerGrpcClient.kt @@ -1,7 +1,7 @@ package io.provenance.explorer.grpc.v1 import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getPagination import io.provenance.explorer.grpc.extensions.toMarker import io.provenance.marker.v1.Balance diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MetadataGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MetadataGrpcClient.kt index 917d7c8a..d56601e2 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MetadataGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MetadataGrpcClient.kt @@ -1,7 +1,7 @@ package io.provenance.explorer.grpc.v1 import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getPagination import io.provenance.metadata.v1.QueryGrpcKt.QueryCoroutineStub import io.provenance.metadata.v1.contractSpecificationRequest diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MsgFeeGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MsgFeeGrpcClient.kt index c636fcff..d8c41bb5 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MsgFeeGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/MsgFeeGrpcClient.kt @@ -1,7 +1,7 @@ package io.provenance.explorer.grpc.v1 import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getPagination import io.provenance.msgfees.v1.QueryGrpcKt import io.provenance.msgfees.v1.queryAllMsgFeesRequest diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/SmartContractGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/SmartContractGrpcClient.kt index 6bb3c941..9df8f2c9 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/SmartContractGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/SmartContractGrpcClient.kt @@ -5,7 +5,7 @@ import cosmwasm.wasm.v1.QueryGrpc import cosmwasm.wasm.v1.QueryOuterClass import io.grpc.ManagedChannelBuilder import io.provenance.explorer.config.ExplorerProperties -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import org.springframework.stereotype.Component import java.net.URI import java.util.concurrent.TimeUnit diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/TransactionGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/TransactionGrpcClient.kt index ea5b82e1..1457a35a 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/TransactionGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/TransactionGrpcClient.kt @@ -4,7 +4,7 @@ import cosmos.base.abci.v1beta1.Abci import cosmos.tx.v1beta1.ServiceGrpc import cosmos.tx.v1beta1.ServiceOuterClass import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.domain.core.logger import io.provenance.explorer.domain.exceptions.TendermintApiException import io.provenance.explorer.grpc.extensions.getPaginationBuilder diff --git a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/ValidatorGrpcClient.kt b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/ValidatorGrpcClient.kt index bc4ba88d..4d71b51c 100644 --- a/service/src/main/kotlin/io/provenance/explorer/grpc/v1/ValidatorGrpcClient.kt +++ b/service/src/main/kotlin/io/provenance/explorer/grpc/v1/ValidatorGrpcClient.kt @@ -17,7 +17,7 @@ import cosmos.staking.v1beta1.queryValidatorRequest import cosmos.staking.v1beta1.queryValidatorUnbondingDelegationsRequest import cosmos.staking.v1beta1.queryValidatorsRequest import io.grpc.ManagedChannelBuilder -import io.provenance.explorer.config.GrpcLoggingInterceptor +import io.provenance.explorer.config.interceptor.GrpcLoggingInterceptor import io.provenance.explorer.grpc.extensions.getPagination import org.springframework.stereotype.Component import java.net.URI diff --git a/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt b/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt index 9b682996..d79a2b91 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/AccountService.kt @@ -1,14 +1,20 @@ package io.provenance.explorer.service +import com.google.protobuf.Any +import cosmos.bank.v1beta1.msgSend import io.provenance.attribute.v1.Attribute import io.provenance.explorer.config.ExplorerProperties import io.provenance.explorer.config.ResourceNotFoundException import io.provenance.explorer.domain.core.logger import io.provenance.explorer.domain.entities.AccountRecord +import io.provenance.explorer.domain.exceptions.requireNotNullToMessage +import io.provenance.explorer.domain.exceptions.requireToMessage +import io.provenance.explorer.domain.exceptions.validate import io.provenance.explorer.domain.extensions.NHASH import io.provenance.explorer.domain.extensions.USD_UPPER import io.provenance.explorer.domain.extensions.fromBase64 import io.provenance.explorer.domain.extensions.isAddressAsType +import io.provenance.explorer.domain.extensions.pack import io.provenance.explorer.domain.extensions.pageCountOfResults import io.provenance.explorer.domain.extensions.toAccountPubKey import io.provenance.explorer.domain.extensions.toBase64 @@ -20,14 +26,17 @@ import io.provenance.explorer.domain.extensions.toOffset import io.provenance.explorer.domain.models.explorer.AccountDetail import io.provenance.explorer.domain.models.explorer.AccountRewards import io.provenance.explorer.domain.models.explorer.AttributeObj +import io.provenance.explorer.domain.models.explorer.BankSendRequest import io.provenance.explorer.domain.models.explorer.CoinStr import io.provenance.explorer.domain.models.explorer.Delegation import io.provenance.explorer.domain.models.explorer.PagedResults import io.provenance.explorer.domain.models.explorer.Reward import io.provenance.explorer.domain.models.explorer.TokenCounts import io.provenance.explorer.domain.models.explorer.UnpaginatedDelegation +import io.provenance.explorer.domain.models.explorer.mapToProtoCoin import io.provenance.explorer.domain.models.explorer.toCoinStrWithPrice import io.provenance.explorer.grpc.extensions.getModuleAccName +import io.provenance.explorer.grpc.extensions.isStandardAddress import io.provenance.explorer.grpc.v1.AccountGrpcClient import io.provenance.explorer.grpc.v1.AttributeGrpcClient import io.provenance.explorer.grpc.v1.MetadataGrpcClient @@ -58,6 +67,10 @@ class AccountService( else throw ResourceNotFoundException("Invalid account: '$address'") } + fun validateAddress(address: String) = transaction { + requireNotNullToMessage(AccountRecord.findByAddress(address)) { "Address $address does not exist." } + } + fun getAccountDetail(address: String) = runBlocking { getAccountRaw(address).let { val attributes = async { attrClient.getAllAttributesForAddress(it.accountAddress) } @@ -201,6 +214,21 @@ class AccountService( ) } } + + fun createSend(request: BankSendRequest): Any { + validate( + validateAddress(request.from), + requireToMessage(request.to.isStandardAddress(props)) { "to must be a standard address format" }, + requireToMessage(request.to != request.from) { "The to address must be different that the from address" }, + *request.funds.map { assetService.validateDenom(it.denom) }.toTypedArray(), + requireToMessage(request.funds.none { it.amount.toLong() == 0L }) { "At least one deposit must have an amount greater than zero." } + ) + return msgSend { + fromAddress = request.from + toAddress = request.to + amount.addAll(request.funds.mapToProtoCoin()) + }.pack() + } } fun String.getAccountType() = this.split(".").last() diff --git a/service/src/main/kotlin/io/provenance/explorer/service/AssetService.kt b/service/src/main/kotlin/io/provenance/explorer/service/AssetService.kt index 21edfc0f..96649b1e 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/AssetService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/AssetService.kt @@ -18,6 +18,7 @@ import io.provenance.explorer.domain.entities.MarkerUnitRecord import io.provenance.explorer.domain.entities.TokenDistributionAmountsRecord import io.provenance.explorer.domain.entities.TokenDistributionPaginatedResultsRecord import io.provenance.explorer.domain.entities.TxMarkerJoinRecord +import io.provenance.explorer.domain.exceptions.requireNotNullToMessage import io.provenance.explorer.domain.extensions.pageCountOfResults import io.provenance.explorer.domain.extensions.toDateTime import io.provenance.explorer.domain.extensions.toObjectNode @@ -62,6 +63,9 @@ class AssetService( ) { protected val logger = logger(AssetService::class) + fun validateDenom(denom: String) = + requireNotNullToMessage(MarkerCacheRecord.findByDenom(denom)) { "Denom $denom does not exist." } + fun getAssets( statuses: List, page: Int, diff --git a/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt b/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt index c3a8571d..d82450b6 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/GovService.kt @@ -1,12 +1,29 @@ package io.provenance.explorer.service +import com.fasterxml.jackson.module.kotlin.readValue import com.google.protobuf.Any import com.google.protobuf.util.JsonFormat import cosmos.gov.v1beta1.Gov import cosmos.gov.v1beta1.Gov.VoteOption import cosmos.gov.v1beta1.Tx +import cosmos.gov.v1beta1.msgDeposit +import cosmos.gov.v1beta1.msgSubmitProposal +import cosmos.gov.v1beta1.msgVote +import cosmos.gov.v1beta1.msgVoteWeighted +import cosmos.gov.v1beta1.textProposal +import cosmos.gov.v1beta1.weightedVoteOption +import cosmos.params.v1beta1.paramChange +import cosmos.params.v1beta1.parameterChangeProposal import cosmos.upgrade.v1beta1.Upgrade +import cosmos.upgrade.v1beta1.cancelSoftwareUpgradeProposal +import cosmos.upgrade.v1beta1.plan +import cosmos.upgrade.v1beta1.softwareUpgradeProposal +import cosmwasm.wasm.v1.accessConfig +import cosmwasm.wasm.v1.instantiateContractProposal +import cosmwasm.wasm.v1.storeCodeProposal import cosmwasm.wasm.v1beta1.Proposal +import io.provenance.explorer.VANILLA_MAPPER +import io.provenance.explorer.config.ExplorerProperties import io.provenance.explorer.config.ResourceNotFoundException import io.provenance.explorer.domain.core.logger import io.provenance.explorer.domain.entities.AccountRecord @@ -15,18 +32,25 @@ import io.provenance.explorer.domain.entities.DepositType import io.provenance.explorer.domain.entities.GovDepositRecord import io.provenance.explorer.domain.entities.GovProposalRecord import io.provenance.explorer.domain.entities.GovVoteRecord +import io.provenance.explorer.domain.entities.MonitorProposalType import io.provenance.explorer.domain.entities.ProposalMonitorRecord -import io.provenance.explorer.domain.entities.ProposalType import io.provenance.explorer.domain.entities.SmCodeRecord import io.provenance.explorer.domain.entities.TxSmCodeRecord import io.provenance.explorer.domain.entities.ValidatorStateRecord +import io.provenance.explorer.domain.exceptions.InvalidArgumentException +import io.provenance.explorer.domain.exceptions.requireNotNullToMessage +import io.provenance.explorer.domain.exceptions.requireToMessage +import io.provenance.explorer.domain.exceptions.validate import io.provenance.explorer.domain.extensions.NHASH import io.provenance.explorer.domain.extensions.formattedString import io.provenance.explorer.domain.extensions.mhashToNhash +import io.provenance.explorer.domain.extensions.pack +import io.provenance.explorer.domain.extensions.padToDecString import io.provenance.explorer.domain.extensions.pageCountOfResults import io.provenance.explorer.domain.extensions.stringfy import io.provenance.explorer.domain.extensions.to256Hash import io.provenance.explorer.domain.extensions.toBase64 +import io.provenance.explorer.domain.extensions.toByteString import io.provenance.explorer.domain.extensions.toCoinStr import io.provenance.explorer.domain.extensions.toDateTime import io.provenance.explorer.domain.extensions.toDecimalString @@ -36,15 +60,29 @@ import io.provenance.explorer.domain.models.explorer.DepositPercentage import io.provenance.explorer.domain.models.explorer.DepositRecord import io.provenance.explorer.domain.models.explorer.GovAddrData import io.provenance.explorer.domain.models.explorer.GovAddress +import io.provenance.explorer.domain.models.explorer.GovDepositRequest import io.provenance.explorer.domain.models.explorer.GovMsgDetail import io.provenance.explorer.domain.models.explorer.GovParamType import io.provenance.explorer.domain.models.explorer.GovProposalDetail +import io.provenance.explorer.domain.models.explorer.GovSubmitProposalRequest import io.provenance.explorer.domain.models.explorer.GovTimeFrame +import io.provenance.explorer.domain.models.explorer.GovVoteRequest import io.provenance.explorer.domain.models.explorer.GovVotesDetail +import io.provenance.explorer.domain.models.explorer.InstantiateContractData import io.provenance.explorer.domain.models.explorer.PagedResults +import io.provenance.explorer.domain.models.explorer.ParameterChangeData import io.provenance.explorer.domain.models.explorer.ProposalHeader import io.provenance.explorer.domain.models.explorer.ProposalParamHeights import io.provenance.explorer.domain.models.explorer.ProposalTimings +import io.provenance.explorer.domain.models.explorer.ProposalType +import io.provenance.explorer.domain.models.explorer.ProposalType.CANCEL_UPGRADE +import io.provenance.explorer.domain.models.explorer.ProposalType.INSTANTIATE_CONTRACT +import io.provenance.explorer.domain.models.explorer.ProposalType.PARAMETER_CHANGE +import io.provenance.explorer.domain.models.explorer.ProposalType.SOFTWARE_UPGRADE +import io.provenance.explorer.domain.models.explorer.ProposalType.STORE_CODE +import io.provenance.explorer.domain.models.explorer.ProposalType.TEXT +import io.provenance.explorer.domain.models.explorer.SoftwareUpgradeData +import io.provenance.explorer.domain.models.explorer.StoreCodeData import io.provenance.explorer.domain.models.explorer.Tally import io.provenance.explorer.domain.models.explorer.TallyParams import io.provenance.explorer.domain.models.explorer.TxData @@ -53,15 +91,17 @@ import io.provenance.explorer.domain.models.explorer.VoteDbRecordAgg import io.provenance.explorer.domain.models.explorer.VoteRecord import io.provenance.explorer.domain.models.explorer.VotesTally import io.provenance.explorer.domain.models.explorer.VotingDetails +import io.provenance.explorer.domain.models.explorer.mapToProtoCoin import io.provenance.explorer.domain.models.explorer.toData +import io.provenance.explorer.grpc.extensions.isStandardAddress import io.provenance.explorer.grpc.extensions.toMsgDeposit import io.provenance.explorer.grpc.extensions.toMsgVote import io.provenance.explorer.grpc.extensions.toMsgVoteWeighted import io.provenance.explorer.grpc.v1.GovGrpcClient -import io.provenance.explorer.grpc.v1.SmartContractGrpcClient import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile import java.math.BigDecimal @Service @@ -69,8 +109,11 @@ class GovService( private val govClient: GovGrpcClient, private val protoPrinter: JsonFormat.Printer, private val valService: ValidatorService, - private val smContractClient: SmartContractGrpcClient, - private val cacheService: CacheService + private val smService: SmartContractService, + private val cacheService: CacheService, + private val accountService: AccountService, + private val assetService: AssetService, + private val props: ExplorerProperties ) { protected val logger = logger(GovService::class) @@ -120,12 +163,12 @@ class GovService( val proposal = govClient.getProposal(proposalId) ?: return@runBlocking null val (proposalType, dataHash) = when { txMsg.content.typeUrl.endsWith("v1beta1.StoreCodeProposal") -> - ProposalType.STORE_CODE to + MonitorProposalType.STORE_CODE to // base64(sha256(gzipUncompress(wasmByteCode))) == base64(storedCode.data_hash) txMsg.content.unpack(Proposal.StoreCodeProposal::class.java) .wasmByteCode.gzipUncompress().to256Hash() txMsg.content.typeUrl.endsWith("v1.StoreCodeProposal") -> - ProposalType.STORE_CODE to + MonitorProposalType.STORE_CODE to // base64(sha256(gzipUncompress(wasmByteCode))) == base64(storedCode.data_hash) txMsg.content.unpack(cosmwasm.wasm.v1.Proposal.StoreCodeProposal::class.java) .wasmByteCode.gzipUncompress().to256Hash() @@ -146,8 +189,8 @@ class GovService( } fun processProposal(proposalMon: ProposalMonitorRecord) = transaction { - when (ProposalType.valueOf(proposalMon.proposalType)) { - ProposalType.STORE_CODE -> { + when (MonitorProposalType.valueOf(proposalMon.proposalType)) { + MonitorProposalType.STORE_CODE -> { val creationHeight = BlockCacheRecord.getLastBlockBeforeTime(proposalMon.votingEndTime) + 1 val records = SmCodeRecord.all().sortedByDescending { it.id.value } val matching = records.firstOrNull { proposalMon.dataHash == it.dataHash } @@ -164,7 +207,7 @@ class GovService( } } ?: records.first().id.value.let { start -> - smContractClient.getSmCode(start.toLong() + 1)?.let { + smService.getSmCodeFromNode(start.toLong() + 1)?.let { if (it.codeInfo.dataHash.toBase64() == proposalMon.dataHash) { SmCodeRecord.getOrInsert(start + 1, it, creationHeight) proposalMon.apply { this.processed = true } @@ -301,7 +344,7 @@ class GovService( ) ) - fun getUpgradeProtoType() = Any.pack(Upgrade.SoftwareUpgradeProposal.getDefaultInstance()).typeUrl + fun getUpgradeProtoType() = Any.pack(Upgrade.SoftwareUpgradeProposal.getDefaultInstance(), "").typeUrl fun getProposalsList(page: Int, count: Int) = GovProposalRecord.getAllPaginated(page.toOffset(count), count) @@ -435,6 +478,172 @@ class GovService( PagedResults(total.toLong().pageCountOfResults(count), it, total.toLong()) } } + + private fun validateProposal(proposalId: Long, status: List) = + GovProposalRecord.findByProposalId(proposalId).let { + listOf( + requireNotNullToMessage(it) { "Proposal ID $proposalId does not exist." }, + requireToMessage( + status.map { it.name } + .contains(it.status) + ) { "Proposal ID $proposalId is not in the correct status for this action." } + ) + } + + fun createVote(request: GovVoteRequest): Any { + validate( + *validateProposal( + request.proposalId, + listOf(Gov.ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD) + ).toTypedArray(), + accountService.validateAddress(request.voter), + requireToMessage(request.votes.sumOf { it.weight } == 100) { "The sum of all submitted votes must be 100" }, + ) + return when (request.votes.size) { + 0 -> throw InvalidArgumentException("A vote option must be included in the request") + 1 -> msgVote { + proposalId = request.proposalId + voter = request.voter + option = request.votes.first().option + }.pack() + else -> msgVoteWeighted { + proposalId = request.proposalId + voter = request.voter + options.addAll( + request.votes.filter { it.weight > 0 }.map { + weightedVoteOption { + option = it.option + weight = it.weight.padToDecString() + } + } + ) + }.pack() + } + } + + fun createDeposit(request: GovDepositRequest): Any { + validate( + *validateProposal( + request.proposalId, + listOf( + Gov.ProposalStatus.PROPOSAL_STATUS_DEPOSIT_PERIOD, + Gov.ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD + ) + ).toTypedArray(), + accountService.validateAddress(request.depositor), + *request.deposit.map { assetService.validateDenom(it.denom) }.toTypedArray(), + requireToMessage(request.deposit.none { it.amount.toLong() == 0L }) { "At least one deposit must have an amount greater than zero." } + ) + return msgDeposit { + proposalId = request.proposalId + depositor = request.depositor + amount.addAll(request.deposit.mapToProtoCoin()) + }.pack() + } + + fun getSupportedProposalTypes() = ProposalType.values().map { it.name to it.example }.toMap() + + fun createSubmitProposal(type: ProposalType, request: GovSubmitProposalRequest, wasmFile: MultipartFile?) = + transaction { + val prevalidates = mutableListOf( + accountService.validateAddress(request.submitter), + *request.initialDeposit.map { assetService.validateDenom(it.denom) }.toTypedArray() + ) + when (type) { + TEXT -> textProposal { + title = request.title + description = request.description + }.pack() + CANCEL_UPGRADE -> cancelSoftwareUpgradeProposal { + title = request.title + description = request.description + }.pack() + PARAMETER_CHANGE -> + VANILLA_MAPPER.readValue(request.content).let { content -> + parameterChangeProposal { + title = request.title + description = request.description + changes.addAll( + content.changes.map { + paramChange { + subspace = it.subspace + key = it.key + value = it.value + } + } + ) + }.pack() + } + SOFTWARE_UPGRADE -> + VANILLA_MAPPER.readValue(request.content).let { content -> + softwareUpgradeProposal { + title = request.title + description = request.description + plan = plan { + name = content.name + height = content.height + info = content.info + } + }.pack() + } + STORE_CODE -> { + prevalidates.addAll( + listOf( + requireNotNullToMessage(wasmFile) { "Must have a WASM file submitted for a StoreCode proposal" }, + requireToMessage(wasmFile.bytes.isWASM()) { "Must have a .wasm file type for a StoreCode proposal" } + ) + ) + VANILLA_MAPPER.readValue(request.content).let { content -> + prevalidates.addAll( + listOf( + requireToMessage(content.runAs.isStandardAddress(props)) { "runAs must be a standard address format" }, + requireToMessage(content.accessConfig?.address?.isStandardAddress(props) ?: true) { "accessConfig.address must be a standard address format" } + ) + ) + storeCodeProposal { + title = request.title + description = request.description + runAs = content.runAs + wasmByteCode = wasmFile.bytes.gzipCompress().toByteString() + content.accessConfig?.let { config -> + instantiatePermission = accessConfig { + config.address?.let { this.address = it } + permission = config.type + } + } + }.pack() + } + } + INSTANTIATE_CONTRACT -> + VANILLA_MAPPER.readValue(request.content).let { content -> + prevalidates.addAll(content.funds.map { assetService.validateDenom(it.denom) }) + prevalidates.addAll( + listOf( + requireToMessage(content.runAs.isStandardAddress(props)) { "runAs must be a standard address format" }, + requireToMessage(content.admin?.isStandardAddress(props) ?: true) { "admin must be a standard address format" }, + requireNotNullToMessage(smService.getSmCodeFromNode(content.codeId.toLong())) { "codeId is not valid for instantiation" } + ) + ) + instantiateContractProposal { + title = request.title + description = request.description + runAs = content.runAs + content.admin?.let { this.admin = it } + codeId = content.codeId.toLong() + content.label?.let { this.label = it } + msg = content.msg.toByteArray().toByteString() + content.funds.mapToProtoCoin().let { if (it.isNotEmpty()) funds.addAll(it) } + }.pack() + } + }.let { msg -> + validate(*prevalidates.toTypedArray()) + msgSubmitProposal { + content = msg + proposer = request.submitter + request.initialDeposit.mapToProtoCoin().let { if (it.isNotEmpty()) initialDeposit.addAll(it) } + }.pack() + } + } } fun Any.getGovMsgDetail(txHash: String) = diff --git a/service/src/main/kotlin/io/provenance/explorer/service/SmartContractService.kt b/service/src/main/kotlin/io/provenance/explorer/service/SmartContractService.kt index 08424100..c892ba55 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/SmartContractService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/SmartContractService.kt @@ -26,25 +26,28 @@ import io.provenance.explorer.grpc.extensions.toMsgUpdateAdminOld import io.provenance.explorer.grpc.v1.SmartContractGrpcClient import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.stereotype.Service +import java.io.ByteArrayOutputStream import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream @Service class SmartContractService( - private val smContractClient: SmartContractGrpcClient, private val protoPrinter: JsonFormat.Printer, private val accountService: AccountService, private val scClient: SmartContractGrpcClient ) { protected val logger = logger(SmartContractService::class) + fun getSmCodeFromNode(codeId: Long) = scClient.getSmCode(codeId) + fun saveCode(codeId: Long, txInfo: TxData) = transaction { - smContractClient.getSmCode(codeId).let { + getSmCodeFromNode(codeId).let { SmCodeRecord.getOrInsert(codeId.toInt(), it, txInfo.blockHeight) } } fun saveContract(contract: String, txInfo: TxData) = transaction { - smContractClient.getSmContract(contract).let { + scClient.getSmContract(contract).let { accountService.saveAccount(contract, true) SmContractRecord.getOrInsert(it, txInfo.blockHeight) } @@ -98,18 +101,21 @@ fun SmCodeRecord.toCodeObject() = Code(this.id.value, this.creationHeight, this. fun SmContractRecord.toContractObject() = Contract(this.contractAddress, this.creationHeight, this.codeId, this.creator, this.admin, this.label) -fun ByteArray.isGZIPStream(): Boolean { - return ( - this[0] == GZIPInputStream.GZIP_MAGIC.toByte() && - this[1] == (GZIPInputStream.GZIP_MAGIC ushr 8).toByte() - ) -} +fun ByteArray.isGZIPStream(): Boolean = + this[0] == GZIPInputStream.GZIP_MAGIC.toByte() && this[1] == (GZIPInputStream.GZIP_MAGIC ushr 8).toByte() + +fun ByteArray.isWASM() = this.sliceArray(0 until 4).contentEquals(byteArrayOf(0x00, 0x61, 0x73, 0x6D)) fun ByteString.gzipUncompress() = if (this.toByteArray().isGZIPStream()) GZIPInputStream(this.toByteArray().inputStream()).use { it.readBytes() } else this.toByteArray() +fun ByteArray.gzipCompress(): ByteArray = ByteArrayOutputStream().use { byteStream -> + GZIPOutputStream(byteStream).use { it.write(this, 0, this.size) } + byteStream.toByteArray() +} + fun Any.getScMsgDetail(msgIdx: Int, tx: ServiceOuterClass.GetTxResponse): Pair? = when { typeUrl.endsWith("v1.MsgStoreCode") -> tx.txResponse.logsList[msgIdx].eventsList diff --git a/service/src/main/kotlin/io/provenance/explorer/service/StakingService.kt b/service/src/main/kotlin/io/provenance/explorer/service/StakingService.kt new file mode 100644 index 00000000..d1a6fb3e --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/service/StakingService.kt @@ -0,0 +1,112 @@ +package io.provenance.explorer.service + +import com.google.protobuf.Any +import cosmos.base.v1beta1.coin +import cosmos.distribution.v1beta1.msgWithdrawDelegatorReward +import cosmos.distribution.v1beta1.msgWithdrawValidatorCommission +import cosmos.staking.v1beta1.msgBeginRedelegate +import cosmos.staking.v1beta1.msgDelegate +import cosmos.staking.v1beta1.msgUndelegate +import io.provenance.explorer.domain.core.logger +import io.provenance.explorer.domain.entities.StakingValidatorCacheRecord +import io.provenance.explorer.domain.exceptions.InvalidArgumentException +import io.provenance.explorer.domain.exceptions.requireToMessage +import io.provenance.explorer.domain.exceptions.validate +import io.provenance.explorer.domain.extensions.isZero +import io.provenance.explorer.domain.extensions.pack +import io.provenance.explorer.domain.models.explorer.StakingDelegateRequest +import io.provenance.explorer.domain.models.explorer.StakingRedelegateRequest +import io.provenance.explorer.domain.models.explorer.StakingUndelegateRequest +import io.provenance.explorer.domain.models.explorer.StakingWithdrawCommissionRequest +import io.provenance.explorer.domain.models.explorer.StakingWithdrawRewardsRequest +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.stereotype.Service + +@Service +class StakingService( + private val accountService: AccountService, + private val validatorService: ValidatorService, + private val assetService: AssetService +) { + + protected val logger = logger(StakingService::class) + + fun createDelegate(request: StakingDelegateRequest): Any { + validate( + accountService.validateAddress(request.delegator), + validatorService.validateValidator(request.validator), + assetService.validateDenom(request.amount.denom), + requireToMessage(request.amount.amount.toLong() > 0L) { "Delegation amount must be greater than zero" } + ) + return msgDelegate { + delegatorAddress = request.delegator + validatorAddress = request.validator + amount = coin { + denom = request.amount.denom + amount = request.amount.amount + } + }.pack() + } + + fun createRedelegate(request: StakingRedelegateRequest): Any { + validate( + accountService.validateAddress(request.delegator), + validatorService.validateValidator(request.validatorSrc), + validatorService.validateValidator(request.validatorDst), + requireToMessage(request.validatorSrc != request.validatorDst) { "The destination validator must be different that the source validator" }, + assetService.validateDenom(request.amount.denom), + requireToMessage(request.amount.amount.toLong() > 0L) { "Redelegation amount must be greater than zero" } + ) + return msgBeginRedelegate { + delegatorAddress = request.delegator + validatorSrcAddress = request.validatorSrc + validatorDstAddress = request.validatorDst + amount = coin { + denom = request.amount.denom + amount = request.amount.amount + } + }.pack() + } + + fun createUndelegate(request: StakingUndelegateRequest): Any { + validate( + accountService.validateAddress(request.delegator), + validatorService.validateValidator(request.validator), + assetService.validateDenom(request.amount.denom), + requireToMessage(request.amount.amount.toLong() > 0L) { "Undelegation amount must be greater than zero" } + ) + return msgUndelegate { + delegatorAddress = request.delegator + validatorAddress = request.validator + amount = coin { + denom = request.amount.denom + amount = request.amount.amount + } + }.pack() + } + + fun createWithdrawRewards(request: StakingWithdrawRewardsRequest): Any { + validate( + accountService.validateAddress(request.delegator), + validatorService.validateValidator(request.validator) + ) + return msgWithdrawDelegatorReward { + delegatorAddress = request.delegator + validatorAddress = request.validator + }.pack() + } + + fun createWithdrawCommission(request: StakingWithdrawCommissionRequest): Any { + validate( + requireToMessage( + !validatorService.getValidatorCommission(request.validator).isZero() + ) { "Must have Validator commissions to withdraw." } + ) + return msgWithdrawValidatorCommission { validatorAddress = request.validator }.pack() + } + + fun validateWithdrawCommission(validator: String, xAddress: String) = transaction { + StakingValidatorCacheRecord.findByOperAddr(validator)?.let { it.accountAddress == xAddress } + ?: throw InvalidArgumentException("Validator $validator does not exist") + } +} diff --git a/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt b/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt index baa2f900..3a1a7121 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/ValidatorService.kt @@ -22,6 +22,7 @@ import io.provenance.explorer.domain.entities.ValidatorState.ACTIVE import io.provenance.explorer.domain.entities.ValidatorStateRecord import io.provenance.explorer.domain.entities.ValidatorsCacheRecord import io.provenance.explorer.domain.entities.updateHitCount +import io.provenance.explorer.domain.exceptions.requireNotNullToMessage import io.provenance.explorer.domain.extensions.NHASH import io.provenance.explorer.domain.extensions.average import io.provenance.explorer.domain.extensions.avg @@ -124,6 +125,9 @@ class ValidatorService( .let { ValidatorStateRecord.findByOperator(getActiveSet(), address)!! } } + fun validateValidator(validator: String) = + requireNotNullToMessage(StakingValidatorCacheRecord.findByOperAddr(validator)) { "Validator $validator does not exist." } + // Returns a validator detail object for the validator fun getValidator(address: String) = getValidatorOperatorAddress(address)?.let { addr -> @@ -388,6 +392,8 @@ class ValidatorService( UnpaginatedDelegation(recs, mapOf(Pair("unbondingTotal", total))) } + fun getValidatorCommission(address: String) = grpcClient.getValidatorCommission(address).commissionList + fun getCommissionInfo(address: String): ValidatorCommission { val validator = ValidatorStateRecord.findByOperator(getActiveSet(), address)?.json ?: throw ResourceNotFoundException("Invalid validator address: '$address'") @@ -395,7 +401,7 @@ class ValidatorService( val selfBonded = getValSelfBonded(validator) val delegatorCount = grpcClient.getStakingValidatorDelegations(validator.operatorAddress, 0, 10).pagination.total - val rewards = grpcClient.getValidatorCommission(address).commissionList.firstOrNull() + val rewards = getValidatorCommission(address).firstOrNull() return ValidatorCommission( CountStrTotal(validator.tokens, null, NHASH), CountStrTotal(selfBonded.first, null, selfBonded.second), diff --git a/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt b/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt index dc95f17d..80c9b054 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/async/AsyncCachingV2.kt @@ -76,6 +76,8 @@ import io.provenance.explorer.grpc.extensions.getSmContractEventByEvent import io.provenance.explorer.grpc.extensions.getTxIbcClientChannel import io.provenance.explorer.grpc.extensions.isIbcTransferMsg import io.provenance.explorer.grpc.extensions.isMetadataDeletionMsg +import io.provenance.explorer.grpc.extensions.isStandardAddress +import io.provenance.explorer.grpc.extensions.isValidatorAddress import io.provenance.explorer.grpc.extensions.mapEventAttrValues import io.provenance.explorer.grpc.extensions.scrubQuotes import io.provenance.explorer.grpc.extensions.toMsgAcknowledgement @@ -745,9 +747,9 @@ class AsyncCachingV2( } fun String.getAddressType(activeSet: Int, props: ExplorerProperties) = when { - this.startsWith(props.provValOperPrefix()) -> + this.isValidatorAddress(props) -> Pair(TxAddressJoinType.OPERATOR.name, ValidatorStateRecord.findByOperator(activeSet, this)?.operatorAddrId) - this.startsWith(props.provAccPrefix()) -> + this.isStandardAddress(props) -> Pair(TxAddressJoinType.ACCOUNT.name, AccountRecord.findByAddress(this)?.id?.value) else -> logger().debug("Address type is not supported: Addr $this").let { null } } diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v2/AccountController.kt b/service/src/main/kotlin/io/provenance/explorer/web/v2/AccountController.kt index 7c11a5ae..e44fafca 100644 --- a/service/src/main/kotlin/io/provenance/explorer/web/v2/AccountController.kt +++ b/service/src/main/kotlin/io/provenance/explorer/web/v2/AccountController.kt @@ -1,5 +1,11 @@ package io.provenance.explorer.web.v2 +import com.google.protobuf.util.JsonFormat +import io.provenance.explorer.config.interceptor.JwtInterceptor +import io.provenance.explorer.domain.extensions.TxMessageBody +import io.provenance.explorer.domain.extensions.toTxBody +import io.provenance.explorer.domain.extensions.toTxMessageBody +import io.provenance.explorer.domain.models.explorer.BankSendRequest import io.provenance.explorer.service.AccountService import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation @@ -9,6 +15,9 @@ import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestAttribute +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -24,7 +33,7 @@ import javax.validation.constraints.Min consumes = MediaType.APPLICATION_JSON_VALUE, tags = ["Account"] ) -class AccountController(private val accountService: AccountService) { +class AccountController(private val accountService: AccountService, private val printer: JsonFormat.Printer) { @ApiOperation("Returns account detail for the account address") @GetMapping("/{address}") @@ -83,4 +92,16 @@ class AccountController(private val accountService: AccountService) { @RequestParam(defaultValue = "10") @Min(1) @Max(50) count: Int, @ApiParam(defaultValue = "1", required = false) @RequestParam(defaultValue = "1") @Min(1) page: Int ) = ResponseEntity.ok(accountService.getNamesOwnedByAccount(address, page, count)) + + @ApiOperation(value = "Builds send transaction for submission to blockchain") + @PostMapping("/send") + fun createSend( + @ApiParam(value = "Data used to craft the Send msg type") + @RequestBody request: BankSendRequest, + @ApiParam(hidden = true) @RequestAttribute(name = JwtInterceptor.X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.from) + throw IllegalArgumentException("Unable to process create send; connected wallet does not match request") + return accountService.createSend(request).toTxBody().toTxMessageBody(printer) + } } diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt b/service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt index c0ce7df9..851704f5 100644 --- a/service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt +++ b/service/src/main/kotlin/io/provenance/explorer/web/v2/GovControllerV2.kt @@ -19,7 +19,7 @@ import javax.validation.constraints.Min @RestController @RequestMapping(path = ["/api/v2/gov"], produces = [MediaType.APPLICATION_JSON_VALUE]) @Api( - description = "Governance-related endpoints - V2", + description = "Governance-related endpoints", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, tags = ["Governance"] @@ -43,6 +43,7 @@ class GovControllerV2(private val govService: GovService) { @ApiOperation("Returns vote tallies and vote records of a proposal") @GetMapping("/proposals/{id}/votes") @Deprecated("Use /api/v3/gov/proposals/{id}/votes") + @java.lang.Deprecated fun getProposalVotes( @ApiParam(value = "The ID of the proposal") @PathVariable id: Long ) = ResponseEntity.ok(govService.getProposalVotes(id)) diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt b/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt index 26b1ddee..71a9d92d 100644 --- a/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt +++ b/service/src/main/kotlin/io/provenance/explorer/web/v3/GovControllerV3.kt @@ -1,5 +1,14 @@ package io.provenance.explorer.web.v3 +import com.google.protobuf.util.JsonFormat +import io.provenance.explorer.config.interceptor.JwtInterceptor.Companion.X_ADDRESS +import io.provenance.explorer.domain.extensions.TxMessageBody +import io.provenance.explorer.domain.extensions.toTxBody +import io.provenance.explorer.domain.extensions.toTxMessageBody +import io.provenance.explorer.domain.models.explorer.GovDepositRequest +import io.provenance.explorer.domain.models.explorer.GovSubmitProposalRequest +import io.provenance.explorer.domain.models.explorer.GovVoteRequest +import io.provenance.explorer.domain.models.explorer.ProposalType import io.provenance.explorer.service.GovService import io.swagger.annotations.Api import io.swagger.annotations.ApiOperation @@ -9,9 +18,14 @@ import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestAttribute +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile import javax.validation.constraints.Max import javax.validation.constraints.Min @@ -19,12 +33,12 @@ import javax.validation.constraints.Min @RestController @RequestMapping(path = ["/api/v3/gov"], produces = [MediaType.APPLICATION_JSON_VALUE]) @Api( - description = "Governance-related endpoints - V3", + description = "Governance-related endpoints", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, tags = ["Governance"] ) -class GovControllerV3(private val govService: GovService) { +class GovControllerV3(private val govService: GovService, private val printer: JsonFormat.Printer) { @ApiOperation("Returns vote tallies and vote records of a proposal") @GetMapping("/proposals/{id}/votes") @@ -34,4 +48,46 @@ class GovControllerV3(private val govService: GovService) { @ApiParam(value = "Record count between 1 and 50", defaultValue = "10", required = false) @RequestParam(defaultValue = "10") @Min(1) @Max(50) count: Int ) = ResponseEntity.ok(govService.getProposalVotesPaginated(id, page, count)) + + @ApiOperation("Return list of supported proposal types and associated objects to be used for SubmitProposal content string") + @GetMapping("/types/supported") + fun supportedProposalTypes() = ResponseEntity.ok(govService.getSupportedProposalTypes()) + + @ApiOperation(value = "Builds submit proposal transaction for submission to blockchain") + @PostMapping("/submit/{type}", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE]) + fun createSubmitProposal( + @ApiParam(value = "The supported proposal type") @PathVariable type: ProposalType, + @ApiParam(value = "Data used to craft the SubmitProposal msg type; `content` is a stringified json object conforming to the matching proposal type.") + @RequestPart request: GovSubmitProposalRequest, + @ApiParam(value = "A .wasm file type containing the WASM code; required for a StoreCode proposal type") + @RequestPart(required = false) wasmFile: MultipartFile? = null, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.submitter) + throw IllegalArgumentException("Unable to process create submit proposal; connected wallet does not match request") + return govService.createSubmitProposal(type, request, wasmFile).toTxBody().toTxMessageBody(printer) + } + + @ApiOperation(value = "Builds deposit transaction for submission to blockchain") + @PostMapping("/deposit") + fun createDeposit( + @ApiParam(value = "Data used to craft the Deposit msg type") @RequestBody request: GovDepositRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.depositor) + throw IllegalArgumentException("Unable to process create deposit; connected wallet does not match request") + return govService.createDeposit(request).toTxBody().toTxMessageBody(printer) + } + + @ApiOperation(value = "Builds vote transaction for submission to blockchain") + @PostMapping("/vote") + fun createVote( + @ApiParam(value = "Data used to craft the Vote and WeightedVote msg types") + @RequestBody request: GovVoteRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.voter) + throw IllegalArgumentException("Unable to process create vote; connected wallet does not match request") + return govService.createVote(request).toTxBody().toTxMessageBody(printer) + } } diff --git a/service/src/main/kotlin/io/provenance/explorer/web/v3/StakingController.kt b/service/src/main/kotlin/io/provenance/explorer/web/v3/StakingController.kt new file mode 100644 index 00000000..e1c168e3 --- /dev/null +++ b/service/src/main/kotlin/io/provenance/explorer/web/v3/StakingController.kt @@ -0,0 +1,95 @@ +package io.provenance.explorer.web.v3 + +import com.google.protobuf.util.JsonFormat +import io.provenance.explorer.config.interceptor.JwtInterceptor.Companion.X_ADDRESS +import io.provenance.explorer.domain.extensions.TxMessageBody +import io.provenance.explorer.domain.extensions.toTxBody +import io.provenance.explorer.domain.extensions.toTxMessageBody +import io.provenance.explorer.domain.models.explorer.StakingDelegateRequest +import io.provenance.explorer.domain.models.explorer.StakingRedelegateRequest +import io.provenance.explorer.domain.models.explorer.StakingUndelegateRequest +import io.provenance.explorer.domain.models.explorer.StakingWithdrawCommissionRequest +import io.provenance.explorer.domain.models.explorer.StakingWithdrawRewardsRequest +import io.provenance.explorer.service.StakingService +import io.swagger.annotations.Api +import io.swagger.annotations.ApiOperation +import io.swagger.annotations.ApiParam +import org.springframework.http.MediaType +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestAttribute +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Validated +@RestController +@RequestMapping(path = ["/api/v3/staking"], produces = [MediaType.APPLICATION_JSON_VALUE]) +@Api( + description = "Staking-related endpoints - V3", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE, + tags = ["Staking"] +) +class StakingController(private val stakingService: StakingService, private val printer: JsonFormat.Printer) { + + @ApiOperation(value = "Builds a delegate transaction for submission to blockchain") + @PostMapping("/delegate") + fun createDelegate( + @ApiParam(value = "Data used to craft the Delegate msg type") + @RequestBody request: StakingDelegateRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.delegator) + throw IllegalArgumentException("Unable to process create delegate; connected wallet does not match request") + return stakingService.createDelegate(request).toTxBody().toTxMessageBody(printer) + } + + @ApiOperation(value = "Builds a redelegate transaction for submission to blockchain") + @PostMapping("/redelegate") + fun createRedelegate( + @ApiParam(value = "Data used to craft the BeginRedelegate msg type") + @RequestBody request: StakingRedelegateRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.delegator) + throw IllegalArgumentException("Unable to process create redelegate; connected wallet does not match request") + return stakingService.createRedelegate(request).toTxBody().toTxMessageBody(printer) + } + + @ApiOperation(value = "Builds an undelegate transaction for submission to blockchain") + @PostMapping("/undelegate") + fun createUndelegate( + @ApiParam(value = "Data used to craft the Undelegate msg type") + @RequestBody request: StakingUndelegateRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.delegator) + throw IllegalArgumentException("Unable to process create undelegate; connected wallet does not match request") + return stakingService.createUndelegate(request).toTxBody().toTxMessageBody(printer) + } + + @ApiOperation(value = "Builds an withdraw rewards transaction for submission to blockchain") + @PostMapping("/withdraw_rewards") + fun createWithdrawRewards( + @ApiParam(value = "Data used to craft the WithdrawDelegatorReward msg type") + @RequestBody request: StakingWithdrawRewardsRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (xAddress != request.delegator) + throw IllegalArgumentException("Unable to process create withdraw rewards; connected wallet does not match request") + return stakingService.createWithdrawRewards(request).toTxBody().toTxMessageBody(printer) + } + + @ApiOperation(value = "Builds an withdraw commission transaction for submission to blockchain") + @PostMapping("/withdraw_commission") + fun createWithdrawCommission( + @ApiParam(value = "Data used to craft the WithdrawValidatorCommission msg type") + @RequestBody request: StakingWithdrawCommissionRequest, + @ApiParam(hidden = true) @RequestAttribute(name = X_ADDRESS, required = true) xAddress: String + ): TxMessageBody { + if (!stakingService.validateWithdrawCommission(request.validator, xAddress)) + throw IllegalArgumentException("Unable to process create withdraw commission; connected wallet does not match request") + return stakingService.createWithdrawCommission(request).toTxBody().toTxMessageBody(printer) + } +} diff --git a/service/src/main/resources/application-development.properties b/service/src/main/resources/application-development.properties index 21efa545..a6803859 100644 --- a/service/src/main/resources/application-development.properties +++ b/service/src/main/resources/application-development.properties @@ -17,17 +17,17 @@ explorer.swagger-url=localhost:8612 explorer.swagger-protocol=http #### MAINNET SETTINGS -#explorer.mainnet=true -#explorer.pb-url=grpcs://grpc.provenance.io:443 -#explorer.figment-url=https://pio-mainnet-1--lcd--archive.datahub.figment.io -#explorer.pricing-url=https://figure.tech/service-pricing-engine/external -#explorer.genesis-version-url=https://github.com/provenance-io/provenance/releases/download/v1.0.1/plan-v1.0.1.json +explorer.mainnet=true +explorer.pb-url=grpcs://grpc.provenance.io:443 +explorer.figment-url=https://pio-mainnet-1--lcd--archive.datahub.figment.io +explorer.pricing-url=https://figure.tech/service-pricing-engine/external +explorer.genesis-version-url=https://github.com/provenance-io/provenance/releases/download/v1.0.1/plan-v1.0.1.json #### TESTNET SETTINGS -explorer.mainnet=false -explorer.pricing-url=https://test.figure.tech/service-pricing-engine/external -explorer.figment-url=https://pio-testnet-1--lcd.datahub.figment.io -explorer.pb-url=grpcs://grpc.test.provenance.io:443 -explorer.genesis-version-url=https://github.com/provenance-io/provenance/releases/download/v0.2.0/plan-v0.2.0.json +#explorer.mainnet=false +#explorer.pricing-url=https://test.figure.tech/service-pricing-engine/external +#explorer.figment-url=https://pio-testnet-1--lcd.datahub.figment.io +#explorer.pb-url=grpcs://grpc.test.provenance.io:443 +#explorer.genesis-version-url=https://github.com/provenance-io/provenance/releases/download/v0.2.0/plan-v0.2.0.json