Skip to content
This repository has been archived by the owner on Feb 16, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2 from terra-project/hotfix_1.1
Browse files Browse the repository at this point in the history
v1.1
  • Loading branch information
ouiliame committed Mar 9, 2020
2 parents 6c175cc + d576d23 commit 037ea34
Show file tree
Hide file tree
Showing 15 changed files with 22,269 additions and 741 deletions.
5 changes: 5 additions & 0 deletions HISTORY.md
@@ -1,5 +1,10 @@
# Version Changelog

## 0.1.1 (2020-03-09)

- Option to change coin type other than LUNA for MnemonicKey
- Add `ParamChanges` to facilitate construction of `ParameterChangeProposal`s.

## 0.1.0 (2020-03-03)

- Hello Jigu!
Expand Down
1 change: 0 additions & 1 deletion README.md
Expand Up @@ -66,7 +66,6 @@ works by default in Jupyter ...

![jupyter](https://github.com/terra-project/jigu/blob/master/img/jupyter.png?raw=true)


## My First Transaction

### Connect to Soju testnet
Expand Down
2 changes: 1 addition & 1 deletion jigu/__version__.py
@@ -1,7 +1,7 @@
__title__ = "jigu"
__description__ = "Python SDK for Terra"
__url__ = "https://jigu.terra.money/"
__version__ = "0.1.0"
__version__ = "0.1.1"
__author__ = "Terraform Labs, PTE. LTD"
__author_email__ = "engineering@terra.money"
__license__ = "MIT"
Expand Down
9 changes: 6 additions & 3 deletions jigu/client/lcd/api/modules/gov.py
Expand Up @@ -55,9 +55,12 @@ def tally_for(self, proposal_id: str) -> Union[ApiResponse, Dict[str, Coin]]:
def params(self, key: str = None):
"""Puts all the parameters together."""
deposit = self.deposit_params()
vote = self.vote_params()
voting = self.voting_params()
tally = self.tally_params()
p = JiguBox({**deposit, **vote, **tally})
p = JiguBox(
{"deposit_params": deposit, "voting_params": voting, "tally_params": tally}
)

return project( # use response information of last entry, even if there is a delay
tally, p[key] if key else p
)
Expand All @@ -71,7 +74,7 @@ def deposit_params(
p["max_deposit_period"] = int(p["max_deposit_period"])
return project(res, p[key] if key else p)

def vote_params(self, key: str = None) -> Union[ApiResponse, Dict[str, int]]:
def voting_params(self, key: str = None) -> Union[ApiResponse, Dict[str, int]]:
res = self._api_get(f"/gov/parameters/voting")
p = JiguBox(res, box_recast={"voting_period": int})
return project(res, p[key] if key else p)
Expand Down
6 changes: 5 additions & 1 deletion jigu/client/object_query/wallet.py
Expand Up @@ -62,13 +62,17 @@ def create_tx(
memo=memo,
)

def sign_tx(self, *args, **kwargs):
"""Uses the Wallet's key to sign the transaction."""
return self.key.sign_tx(*args, **kwargs)

def create_and_sign_tx(
self, *msgs: StdMsg, fee: Optional[StdFee] = None, memo: str = "",
) -> StdTx:
"""Creates a sign message, signs it, and produces a transaction in one go.
Outputs a ready-to-broadcast `StdTx`.
"""
return self.key.sign_tx(self.create_tx(*msgs, fee=fee, memo=memo))
return self.sign_tx(self.create_tx(*msgs, fee=fee, memo=memo))

@contextmanager
def manual(self) -> Wallet:
Expand Down
252 changes: 246 additions & 6 deletions jigu/core/proposal/params.py
@@ -1,38 +1,278 @@
from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from typing import List

from bidict import bidict

from jigu.core.proposal import Content
from jigu.util.serdes import JiguBox
from jigu.core.sdk import Coins, Dec
from jigu.core.treasury import PolicyConstraints
from jigu.error import InvalidParamChange
from jigu.util.serdes import (
JiguBox,
JsonDeserializable,
JsonSerializable,
serialize_to_json,
)
from jigu.util.validation import Schemas as S

__all__ = ["ParamChanges", "ParameterChangeProposal"]

symbol = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$")

ParamChangeSchema = S.OBJECT(subspace=S.STRING, key=S.STRING, value=S.STRING)

__all__ = ["ParameterChangeProposal"]
# For each subspace, map JSON-param key to (ParamStore key, deserializing-fn, serialiazing-fn)
# Serializing function is necessary due to weird ways Cosmos's params module treats integers.

# TODO: This file could use some work.

# Define the deserialization and serialization functions for governance params
def deserialize_deposit(data: dict):
if data.get("min_deposit"):
data["min_deposit"] = Coins.deserialize(data["min_deposit"])
if data.get("max_deposit_period"):
data["max_deposit_period"] = int(data["max_deposit_period"])
return data


def serialize_deposit(data: dict):
if data.get("max_deposit_period"):
data["max_deposit_period"] = str(int(data["max_deposit_period"]))
return data


def deserialize_voting(data: dict):
if data.get("voting_period"):
data["voting_period"] = int(data["voting_period"])
return data


def serialize_voting(data: dict):
if data.get("voting_period"):
data["voting_period"] = str(int(data["voting_period"]))
return data


def deserialize_tally(data: dict):
if data.get("quorum"):
data["quorum"] = Dec(data["quorum"])
if data.get("threshold"):
data["threshold"] = Dec(data["threshold"])
if data.get("veto"):
data["veto"] = Dec(data["veto"])
return data


def serialize_tally(data: dict):
return data


# TODO: this could be refactored into XXXModuleParams class in core, with keys
# as attributes, and API requests for Module Params give you an instance of
# ModuleParams with all keys loaded with their current values. Then, to build
# param changes, you would instantiate an empty ModuleParams class with the
# values initialized to ModuleParams.EMPTY and then set each one.

PARAM_DEFNS = {
"distribution": {
"community_tax": ("communitytax", Dec),
"base_proposer_reward": ("baseproposerreward", Dec),
"bonus_proposer_reward": ("bonusproposerreward", Dec),
"withdraw_addr_enabled": ("withdrawaddrenabled", bool),
},
"staking": {
"unbonding_time": ("UnbondingTime", int, str),
"max_validators": ("MaxValidators", int, int),
"max_entries": ("KeyMaxEntries", int, int),
"bond_denom": ("BondDenom", str),
},
"slashing": {
"max_evidence_age": ("MaxEvidenceAge", int, str), # no longer in cosmos master/
"signed_blocks_window": ("SignedBlocksWindow", int, str),
"min_signed_per_window": ("MinSignedPerWindow", Dec),
"downtime_jail_duration": ("DowntimeJailDuration", int, str),
"slash_fraction_double_sign": ("SlashFractionDoubleSign", Dec),
"slash_fraction_downtime": ("SlashFractionDowntime", Dec),
},
"oracle": {
"vote_period": ("voteperiod", int, str),
"vote_threshold": ("votethreshold", Dec),
"reward_band": ("rewardband", Dec),
"reward_distribution_window": ("rewarddistributionwindow", int, str),
"whitelist": ("whitelist", None),
"slash_fraction": ("slashfraction", Dec),
"slash_window": ("slashwindow", int, str),
"min_valid_per_window": ("minvalidperwindow", Dec),
},
"market": {
"pool_recovery_period": ("poolrecoveryperiod", int, str),
"base_pool": ("basepool", Dec),
"min_spread": ("minspread", Dec),
"illiquid_tobin_tax_list": ("illiquidtobintaxlist", None),
},
"treasury": {
"tax_policy": ("taxpolicy", PolicyConstraints.deserialize),
"reward_policy": ("rewardpolicy", PolicyConstraints.deserialize),
"min_spread": ("minspread", Dec),
"seigniorage_burden_target": ("seigniorageburdentarget", Dec),
"mining_increment": ("miningincrement", Dec),
"window_short": ("windowshort", int, str),
"window_long": ("windowlong", int, str),
"window_probation": ("windowprobation", int, str),
},
"gov": {
"deposit_params": ("depositparams", deserialize_deposit, serialize_deposit),
"voting_params": ("votingparams", deserialize_voting, serialize_voting),
"tally_params": ("tallyparams", deserialize_tally, serialize_tally),
},
}

# create lookup table for deserialization
DES_LOOKUP_TABLE = {}
for subspace, keys in PARAM_DEFNS.items():
DES_LOOKUP_TABLE[subspace] = {d[0]: d[1] for k, d in keys.items()}

# DES_LOOKUP_TABLE[subspace][paramkey aka d[0]] = JSON param key
# DES_LOOKUP_TABLE["treasury"]["windowlong"] -> "window_long"

# create lookup table for serialization
PARAMSTORE_KEY_LOOKUP_TABLE = {}
for subspace, keys in PARAM_DEFNS.items():
PARAMSTORE_KEY_LOOKUP_TABLE[subspace] = bidict({k: d[0] for k, d in keys.items()})


class ParamChanges(JsonSerializable, JsonDeserializable):

__schema__ = S.ARRAY(ParamChangeSchema)

def __init__(self, changes: dict):
for k, v in changes.items():
m = symbol.match(k)
if not m:
raise InvalidParamChange(
f"Parameter change subspace could not be parsed: {k}"
)
if not isinstance(v, dict):
raise InvalidParamChange(
f"Parameter change value should be a dict but got: '{type(v)}' for {k}"
)
for sk, sv in v.items():
sm = symbol.match(sk)
if not sm:
raise InvalidParamChange(
f"Parameter change key could not be parsed - {k}: '{sk}'"
)
self.changes = JiguBox(changes)

def __repr__(self) -> str:
return f"<ParamChanges {self.changes!r}>"

def __getattr__(self, name: str) -> JiguBox:
return self.changes[name]

def __getitem__(self, item) -> JiguBox:
return self.changes[item]

@staticmethod
def _get_key(subspace, key, inverse=False):
"""Each parameter has a special key in the ParamStore that might not correspond
to the parameter's JSON-name. We use this function to look up the ParamStore key
for the JSON-name, which we are familiar with.
Set `inverse` to `True` to look up from the opposite direction for deserialization.
"""
try:
table = PARAMSTORE_KEY_LOOKUP_TABLE[subspace]
if inverse:
table = table.inverse
return table[key]
except KeyError:
return key

@staticmethod
def _marshal_value(subspace, key, value):
"""Formats the value for param change. Terra node expects all the values to be
JSON-encoded, and Amino:JSON int/int64/uint/uint64 expects quoted values for
JavaScript numeric support, and int16/uint16 need `int`.
"""
try:
if key not in PARAM_DEFNS[subspace]: # if not JSON-name
# try paramstore name
key = ParamChanges._get_key(subspace, key, inverse=True)
if len(PARAM_DEFNS[subspace][key]) == 3:
value = PARAM_DEFNS[subspace][key][2](value)
except KeyError:
pass
return serialize_to_json(value)

@staticmethod
def _unmarshal_value(subspace, key, value):
"""Looks up the correct type to decode the right type for the parameter change."""
if subspace in DES_LOOKUP_TABLE and key in DES_LOOKUP_TABLE[subspace]:
if DES_LOOKUP_TABLE[subspace][key] is not None:
return DES_LOOKUP_TABLE[subspace][key](value)
return value

@property
def pretty_data(self):
return self.changes.items()

def to_data(self) -> list:
param_changes = []
for subspace, v in self.changes.items():
for key, value in v.items():
param_changes.append(
{
"subspace": subspace,
"key": self._get_key(subspace, key),
"value": self._marshal_value(subspace, key, value),
}
)
return param_changes

@classmethod
def from_data(cls, data: list) -> ParamChanges:
changes = JiguBox(default_box=True)
for p in data:
subspace = p["subspace"]
key = cls._get_key(
subspace, p["key"], inverse=True
) # p["key"] is paramstore key, we are using json-name keys inside Jigu
value = cls._unmarshal_value(subspace, p["key"], json.loads(p["value"]))
changes[subspace][key] = value
return cls(changes)


@dataclass
class ParameterChangeProposal(Content):

type = "params/ParameterChangeProposal"
ParamChanges = ParamChanges # alias for easy access

__schema__ = S.OBJECT(
type=S.STRING_WITH_PATTERN(r"^params/ParameterChangeProposal\Z"),
value=S.OBJECT(
title=S.STRING, description=S.STRING, changes=S.ARRAY(ParamChangeSchema),
title=S.STRING, description=S.STRING, changes=ParamChanges.__schema__,
),
)

title: str
description: str
changes: List[JiguBox] = field(default_factory=list)
changes: ParamChanges = field(default_factory=ParamChanges)

def __post_init__(self):
# validation checks for key
if isinstance(self.changes, dict):
self.changes = ParamChanges(self.changes)

@classmethod
def from_data(cls, data: dict) -> ParameterChangeProposal:
data = data["value"]
return cls(
title=data["title"],
description=data["description"],
changes=[JiguBox(change) for change in data["changes"]],
changes=ParamChanges.from_data(data["changes"]),
)
4 changes: 4 additions & 0 deletions jigu/error.py
Expand Up @@ -55,6 +55,10 @@ class InvalidValConsPubKey(ValidationError, ValueError):
Consensus (Tendermint) pubkey (Bech32-encoded pubkey prefixed with terravalconspub-)."""


class InvalidParamChange(ValidationError, ValueError):
"""The parameter change does not adhere to specified schema."""


class JsonSchemaValidationError(ValidationError, JsonSchemaException):
"""Failed attempt to deserialize an object using a :class:`JsonDeserializable`."""

Expand Down
10 changes: 7 additions & 3 deletions jigu/key/__init__.py
Expand Up @@ -12,18 +12,22 @@

BECH32_PUBKEY_DATA_PREFIX = "eb5ae98721"

__all__ = ["derive_child", "derive_root", "Key"]
__all__ = ["derive_child", "derive_root", "LUNA_COIN_TYPE", "Key"]

LUNA_COIN_TYPE = 330


def derive_root(seed: bytes) -> BIP32Key:
return BIP32Key.fromEntropy(seed)


def derive_child(root: BIP32Key, account: int = 0, index: int = 0):
def derive_child(
root: BIP32Key, account: int = 0, index: int = 0, coin_type: int = LUNA_COIN_TYPE
):
# HD Path: 44'/330'/<acc>'/0/<idx>
return (
root.ChildKey(44 + BIP32_HARDEN)
.ChildKey(330 + BIP32_HARDEN)
.ChildKey(coin_type + BIP32_HARDEN)
.ChildKey(account + BIP32_HARDEN)
.ChildKey(0)
.ChildKey(index)
Expand Down

0 comments on commit 037ea34

Please sign in to comment.