-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: improve docs, comments, tests, and small refactors #303
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -32,20 +32,36 @@ tool for other options. | |||||
|
||||||
## Writing a schedule | ||||||
|
||||||
When the `--write` flag is set, the tool will write a schedule in JSON to | ||||||
stdout. The following flags control the output: | ||||||
When the `--write` flag is set, the tool will write a schedule to stdout. | ||||||
The format is the the format expected by the `create-periodic-vesting-account` | ||||||
or `create-clawback-vesting-account` cli commands, namely a JSON | ||||||
object with the members: | ||||||
|
||||||
- `--coins:` The coins to vest, e.g. `100ubld,50urun`. | ||||||
- `--months`: The number of months to vest over. | ||||||
- `--time`: The time of day of the vesting event, in 24-hour HH:MM format. | ||||||
Defaults to midnight. | ||||||
- `"start_time"`: integer UNIX time; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll stick with "UNIX time" for the concept, since a) it matches the the Go |
||||||
- `"periods"`: a JSON array of JSON objects with members: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming there's not another layer of serialization here (e.g., output looks like
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct - I've added an example and moved this up to a "format" section, where the JSON context is more clear and I can drop the superfluous and confusing repeats. |
||||||
- "coins": string giving the text coins format for the amount vested at this event; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think example output would help, especially for this field. Probably just a URL pointing to something that already exists. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a URL for the coins text format. I'll add a multi-denom example. |
||||||
- "length_seconds": positive integer seconds from the last vesting event, or from the start time for the first vesting event. | ||||||
|
||||||
The following flags control the output: | ||||||
|
||||||
- `--coins:` The total coins to vest, e.g. `100ubld,50urun`. | ||||||
- `--months`: The number of monthly to vesting events. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's a typo here, but the explanation could still be a little more clear.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
- `--start`: The vesting start time: i.e. the first event happens in the | ||||||
next month. Specified in the format `YYYY-MM-DD` or `YYYY-MM-DDThh:mm`, | ||||||
e.g. `2006-01-02T15:04` for 3:04pm on January 2, 2006. | ||||||
- `--cliffs`: One or more vesting cliffs in `YYYY-MM-DD` or `YYYY-MM-DDThh:mm` | ||||||
- `--time`: The time of day of the vesting events, in 24-hour HH:MM format. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. There's a note above that times are interpreted in the local timezone. I added a repeat of that comment here to clarify. The time-of-day of the start time has no bearing on the time-of-day of the later vesting events. |
||||||
Defaults to midnight. | ||||||
- `--cliffs`: Vesting cliffs in `YYYY-MM-DD` or `YYYY-MM-DDThh:mm` | ||||||
format. Only the latest one will have any effect, but it is useful to let | ||||||
the computer do that calculation to avoid mistakes. Multiple cliff dates | ||||||
can be separated by commas or given as multiple arguments. | ||||||
can be separated by commas or given as multiple arguments. Cliffs are not required. | ||||||
|
||||||
The vesting events will occur each month following the start time on the same | ||||||
day of the month (or the last day of the month, if the month does not have a | ||||||
sufficient number of days), for the specified number of months. The total coins | ||||||
to vest will be divided as evenly as possible among all the vesting events. | ||||||
Lastly, all events before the last cliff time, if any, are consolidated into a single even | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think "consolidated" is sufficient. |
||||||
at the last cliff time with the sum of the event amounts. | ||||||
|
||||||
## Reading a schedule | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -4,7 +4,7 @@ | |||||
"encoding/json" | ||||||
"flag" | ||||||
"fmt" | ||||||
"io/ioutil" | ||||||
"io" | ||||||
"os" | ||||||
"strings" | ||||||
"time" | ||||||
|
@@ -13,6 +13,9 @@ | |||||
"github.com/cosmos/cosmos-sdk/x/auth/vesting/client/cli" | ||||||
) | ||||||
|
||||||
// vestcalc is a utility for creating or reading period files | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vocabulary consistency nit.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
// for use in some vesting account types. See README.md for usage. | ||||||
|
||||||
// divide returns the division of total as evenly as possible. | ||||||
// Divisor must be 1 or greater and total must be nonnegative. | ||||||
func divide(total sdk.Int, divisor int) ([]sdk.Int, error) { | ||||||
|
@@ -24,17 +27,29 @@ | |||||
return nil, fmt.Errorf("total must be nonnegative") | ||||||
} | ||||||
divisions := make([]sdk.Int, divisor) | ||||||
// Calculate truncated division and the remainder. | ||||||
// Fact: remainder < divisions, hence fits in int64 | ||||||
|
||||||
// Ideally we could compute total of the first i divisions as | ||||||
// cumulative(i) = floor((total * i) / divisor) | ||||||
// and so | ||||||
// divisions[i] = cumulative(i + 1) - cumulative(i) | ||||||
// but this could lead to numeric overflow for large values of total. | ||||||
// Instead, we'll compute | ||||||
// truncated = floor(total / divisor) | ||||||
// so that | ||||||
// total = truncated * divisor + remainder | ||||||
// where remainder < divisor, then divide the remainder via the | ||||||
// above algorithm - which now won't overflow - and sum the | ||||||
// truncated and slices of the remainder to form the divisions. | ||||||
truncated := total.QuoRaw(div64) | ||||||
remainder := total.ModRaw(div64) | ||||||
fraction := sdk.NewInt(0) // portion of remainder which has been doled out | ||||||
cumulative := sdk.NewInt(0) // portion of remainder which has been doled out | ||||||
for i := int64(0); i < div64; i++ { | ||||||
// multiply will not overflow since remainder and i are < 2^63 | ||||||
nextFraction := remainder.MulRaw(i + 1).QuoRaw(div64) | ||||||
divisions[i] = truncated.Add(nextFraction.Sub(fraction)) | ||||||
fraction = nextFraction | ||||||
// multiply will not overflow since remainder and div64 are < 2^63 | ||||||
nextCumulative := remainder.MulRaw(i + 1).QuoRaw(div64) | ||||||
divisions[i] = truncated.Add(nextCumulative.Sub(cumulative)) | ||||||
cumulative = nextCumulative | ||||||
} | ||||||
|
||||||
// Integrity check | ||||||
sum := sdk.NewInt(0) | ||||||
for _, x := range divisions { | ||||||
|
@@ -46,6 +61,8 @@ | |||||
return divisions, nil | ||||||
} | ||||||
|
||||||
// divideCoins divides the coins into divisor separate parts as evenly as possible. | ||||||
// Divisor must be positive. Returns an array holding the division. | ||||||
func divideCoins(coins sdk.Coins, divisor int) ([]sdk.Coins, error) { | ||||||
if divisor < 1 { | ||||||
return nil, fmt.Errorf("divisor must be 1 or greater") | ||||||
|
@@ -81,6 +98,7 @@ | |||||
// monthlyVestTimes generates timestamps for successive months after startTime. | ||||||
// The monthly events occur at the given time of day. If the month is not | ||||||
// long enough for the desired date, the last day of the month is used. | ||||||
// The number of months must be postive. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh nice, CI caught this typo! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. |
||||||
func monthlyVestTimes(startTime time.Time, months int, timeOfDay time.Time) ([]time.Time, error) { | ||||||
if months < 1 { | ||||||
return nil, fmt.Errorf("must have at least one vesting period") | ||||||
|
@@ -101,6 +119,7 @@ | |||||
times[i-1] = time.Date(tm.Year(), tm.Month(), tm.Day(), hour, minute, second, 0, location) | ||||||
} | ||||||
// Integrity check: dates must be sequential and 26-33 days apart. | ||||||
// (Jan 31 to Feb 28 or Feb 28 to Mar 31, plus slop for DST.) | ||||||
lastTime := startTime | ||||||
for _, tm := range times { | ||||||
duration := tm.Sub(lastTime) | ||||||
|
@@ -115,7 +134,7 @@ | |||||
return times, nil | ||||||
} | ||||||
|
||||||
// marshalVestingData gives the JSON encoding. | ||||||
// marshalVestingData writes the vesting data as JSON. | ||||||
func marshalVestingData(data cli.VestingData) ([]byte, error) { | ||||||
return json.MarshalIndent(data, "", " ") | ||||||
} | ||||||
|
@@ -133,7 +152,8 @@ | |||||
Coins sdk.Coins | ||||||
} | ||||||
|
||||||
// zipEvents generates events by zipping corresponding amounts and times. | ||||||
// zipEvents generates events by zipping corresponding amounts and times | ||||||
// from equal-sized arrays, returning an event array of the same size. | ||||||
func zipEvents(divisions []sdk.Coins, times []time.Time) ([]event, error) { | ||||||
n := len(divisions) | ||||||
if len(times) != n { | ||||||
|
@@ -166,24 +186,19 @@ | |||||
// into a single event, leaving subsequent events unchanged. | ||||||
func applyCliff(events []event, cliff time.Time) ([]event, error) { | ||||||
newEvents := []event{} | ||||||
coins := sdk.NewCoins() | ||||||
for _, e := range events { | ||||||
if !e.Time.After(cliff) { | ||||||
coins = coins.Add(e.Coins...) | ||||||
continue | ||||||
} | ||||||
if !coins.IsZero() { | ||||||
cliffEvent := event{Time: cliff, Coins: coins} | ||||||
newEvents = append(newEvents, cliffEvent) | ||||||
coins = sdk.NewCoins() | ||||||
} | ||||||
newEvents = append(newEvents, e) | ||||||
preCliffAmount := sdk.NewCoins() | ||||||
i := 0 | ||||||
for ; i < len(events) && !events[i].Time.After(cliff); i++ { | ||||||
preCliffAmount = preCliffAmount.Add(events[i].Coins...) | ||||||
} | ||||||
if !coins.IsZero() { | ||||||
// special case if all events are before the cliff | ||||||
cliffEvent := event{Time: cliff, Coins: coins} | ||||||
if !preCliffAmount.IsZero() { | ||||||
cliffEvent := event{Time: cliff, Coins: preCliffAmount} | ||||||
newEvents = append(newEvents, cliffEvent) | ||||||
} | ||||||
for ; i < len(events); i++ { | ||||||
newEvents = append(newEvents, events[i]) | ||||||
} | ||||||
|
||||||
// integrity check | ||||||
oldTotal := sdk.NewCoins() | ||||||
for _, e := range events { | ||||||
|
@@ -196,6 +211,7 @@ | |||||
if !oldTotal.IsEqual(newTotal) { | ||||||
return nil, fmt.Errorf("applying vesting cliff changed total from %s to %s", oldTotal, newTotal) | ||||||
} | ||||||
|
||||||
return newEvents, nil | ||||||
} | ||||||
|
||||||
|
@@ -335,6 +351,8 @@ | |||||
// isoDate is time.Time as a flag.Value in shortIsoFmt. | ||||||
type isoDate struct{ time.Time } | ||||||
|
||||||
var _ flag.Value = &isoDate{} | ||||||
|
||||||
// Set implements flag.Value.Set(). | ||||||
func (id *isoDate) Set(s string) error { | ||||||
t, err := parseIso(s) | ||||||
|
@@ -360,6 +378,8 @@ | |||||
// isoDateList is []time.Time as a flag.Value in repeated or comma-separated shortIsoFmt. | ||||||
type isoDateList []time.Time | ||||||
|
||||||
var _ flag.Value = &isoDateList{} | ||||||
|
||||||
// Set implements flag.Value.Set(). | ||||||
func (dates *isoDateList) Set(s string) error { | ||||||
for _, ds := range strings.Split(s, ",") { | ||||||
|
@@ -395,6 +415,8 @@ | |||||
// isoTime is time.Time as a flagValue in HH:MM format. | ||||||
type isoTime struct{ time.Time } | ||||||
|
||||||
var _ flag.Value = &isoTime{} | ||||||
|
||||||
// Set implements flag.Value.Set(). | ||||||
func (it *isoTime) Set(s string) error { | ||||||
t, err := time.Parse(hhmmFmt, s) | ||||||
|
@@ -431,10 +453,10 @@ | |||||
flagWrite = flag.Bool("write", false, "Write periods file to stdout.") | ||||||
) | ||||||
|
||||||
// readCmd reads periods in JSON from stdin and writes a sequence of vesting | ||||||
// events in local time to stdout. | ||||||
// readCmd reads a schedule file from stdin and writes a sequence of vesting | ||||||
// events in local time to stdout. See README.md for the format. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In local time? Oof. If that's the case we should probably find a way to convey UTC offsets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it matches the anticipated usage pattern. The comment about timezones in |
||||||
func readCmd() { | ||||||
bzIn, err := ioutil.ReadAll(os.Stdin) | ||||||
bzIn, err := io.ReadAll(os.Stdin) | ||||||
if err != nil { | ||||||
fmt.Fprintf(os.Stderr, "cannot read stdin: %v", err) | ||||||
return | ||||||
|
@@ -458,7 +480,9 @@ | |||||
|
||||||
// writeConfig bundles data needed for the write operation. | ||||||
type writeConfig struct { | ||||||
Coins sdk.Coins | ||||||
// Coins is the total amount to be vested. | ||||||
Coins sdk.Coins | ||||||
// Months is the number of months to vest over. Must be positive. | ||||||
Months int | ||||||
TimeOfDay time.Time | ||||||
Start time.Time | ||||||
|
@@ -483,9 +507,7 @@ | |||||
return wc, nil | ||||||
} | ||||||
|
||||||
// generateEvents generates vesting events for the given amount and | ||||||
// denomination across the given monthly vesting events with the given start | ||||||
// time and subject to the vesting cliff times, if any. | ||||||
// generateEvents generates vesting events from the writeConfig. | ||||||
func (wc writeConfig) generateEvents() ([]event, error) { | ||||||
divisions, err := divideCoins(wc.Coins, wc.Months) | ||||||
if err != nil { | ||||||
|
@@ -509,13 +531,13 @@ | |||||
return events, nil | ||||||
} | ||||||
|
||||||
// convertRelative converts absolute-time events to relative periods. | ||||||
// convertRelative converts absolute-time events to VestingData relative to the Start time. | ||||||
func (wc writeConfig) convertRelative(events []event) cli.VestingData { | ||||||
return eventsToVestingData(wc.Start, events) | ||||||
} | ||||||
|
||||||
// writeCmd generates a set of vesting events based on flags and writes a | ||||||
// sequences of periods in JSON format to stdout. | ||||||
// writeCmd generates a set of vesting events based on parsed flags | ||||||
// and writes a schedule file to stdout. | ||||||
func writeCmd() { | ||||||
wc, err := genWriteConfig() | ||||||
if err != nil { | ||||||
|
@@ -536,7 +558,8 @@ | |||||
fmt.Println(string(bz)) | ||||||
} | ||||||
|
||||||
// main executes either readCmd() or writeCmd() based on flags. | ||||||
// main parses the flags and executes a subcommand based on flags. | ||||||
// See README.md for flags and subcommands. | ||||||
func main() { | ||||||
flag.Parse() | ||||||
switch { | ||||||
|
@@ -547,5 +570,6 @@ | |||||
default: | ||||||
fmt.Fprintln(os.Stderr, "Must specify one of --read or --write") | ||||||
flag.Usage() | ||||||
os.Exit(1) | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSON implies text. However, from all the format-related comments, it could be confusing to have it spread out. I've added a single format section with an example that I hope makes it more clear.