Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,24 @@ func decodeSmallField(ref *GenesisStateRef, key string, into any) error {
}

func (cfg InitChainerConfig) applyBalance(ctx sdk.Context, bal Balance) {
acc := cfg.acck.NewAccountWithAddress(ctx, bal.Address)
cfg.acck.SetAccount(ctx, acc)
if bal.IsVesting() {
baseAcc := std.NewBaseAccountWithAddress(bal.Address)
Comment thread
julienrbrt marked this conversation as resolved.
Outdated
var acc std.Account
var err error
switch bal.Vesting.Type {
case std.VestingDelayed:
acc, err = std.NewDelayedVestingAccount(&baseAcc, *bal.Vesting)
default: // VestingContinuous (empty string) — linear vesting
acc, err = std.NewContinuousVestingAccount(&baseAcc, *bal.Vesting)
}
if err != nil {
panic(fmt.Errorf("invalid vesting account for %s: %w", bal.Address, err))
}
cfg.acck.SetAccount(ctx, acc)
Comment thread
julienrbrt marked this conversation as resolved.
} else {
acc := cfg.acck.NewAccountWithAddress(ctx, bal.Address)
cfg.acck.SetAccount(ctx, acc)
}
if err := cfg.bankk.SetCoins(ctx, bal.Address, bal.Amount); err != nil {
panic(err)
}
Expand Down
95 changes: 86 additions & 9 deletions gno.land/pkg/gnoland/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ import (
"fmt"
"io"
"slices"
"strconv"
"strings"

bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/std"
)

// Balance represents a genesis account balance with an optional vesting schedule.
type Balance struct {
Address bft.Address
Amount std.Coins
Address bft.Address `json:"address" yaml:"address"`
Amount std.Coins `json:"amount" yaml:"amount"`
Vesting *std.VestingSchedule `json:"vesting,omitempty" yaml:"vesting,omitempty"`
}

// IsVesting returns true if this balance entry creates a vesting account.
func (b Balance) IsVesting() bool {
return b.Vesting != nil && !b.Vesting.IsZero()
}

func (b *Balance) Verify() error {
Expand All @@ -26,25 +34,83 @@ func (b *Balance) Verify() error {
return ErrBalanceEmptyAmount
}

if b.Vesting != nil {
if err := b.Vesting.Validate(); err != nil {
return fmt.Errorf("invalid vesting schedule: %w", err)
}
if !b.Amount.IsAllGTE(b.Vesting.OriginalVesting) {
return fmt.Errorf(
"original vesting amount (%s) exceeds total balance (%s)",
b.Vesting.OriginalVesting, b.Amount,
)
}
}

return nil
}

func (b *Balance) Parse(entry string) error {
parts := strings.Split(strings.TrimSpace(entry), "=") // <address>=<coins>
if len(parts) != 2 {
// Format: <address>=<coins> [;vesting=<coins>,<start>,<end> [;type=delayed]]
// The vesting suffix is optional.
parts := strings.SplitN(strings.TrimSpace(entry), ";", 3)
balancePart := parts[0]

kv := strings.SplitN(balancePart, "=", 2)
if len(kv) != 2 {
return fmt.Errorf("malformed entry: %q", entry)
}

var err error

b.Address, err = crypto.AddressFromBech32(parts[0])
b.Address, err = crypto.AddressFromBech32(kv[0])
if err != nil {
return fmt.Errorf("invalid address %q: %w", parts[0], err)
return fmt.Errorf("invalid address %q: %w", kv[0], err)
}

b.Amount, err = std.ParseCoins(parts[1])
b.Amount, err = std.ParseCoins(kv[1])
if err != nil {
return fmt.Errorf("invalid amount %q: %w", parts[1], err)
return fmt.Errorf("invalid amount %q: %w", kv[1], err)
}

// Parse optional vesting suffix.
if len(parts) >= 2 {
vestingPart := parts[1]
if !strings.HasPrefix(vestingPart, "vesting=") {
return fmt.Errorf("malformed vesting option: %q", vestingPart)
}
vestingValue := strings.TrimPrefix(vestingPart, "vesting=")

// vesting=<coins>,<start_time>,<end_time>
fields := strings.SplitN(vestingValue, ",", 3)
if len(fields) != 3 {
return fmt.Errorf("malformed vesting schedule: expected <coins>,<start>,<end>, got %q", vestingValue)
}

var schedule std.VestingSchedule
schedule.OriginalVesting, err = std.ParseCoins(fields[0])
if err != nil {
return fmt.Errorf("invalid vesting amount %q: %w", fields[0], err)
}
schedule.StartTime, err = strconv.ParseInt(fields[1], 10, 64)
if err != nil {
return fmt.Errorf("invalid vesting start time %q: %w", fields[1], err)
}
schedule.EndTime, err = strconv.ParseInt(fields[2], 10, 64)
if err != nil {
return fmt.Errorf("invalid vesting end time %q: %w", fields[2], err)
}

// Parse optional type discriminator.
if len(parts) == 3 {
typePart := parts[2]
if typePart == "type=delayed" {
schedule.Type = std.VestingDelayed
} else {
return fmt.Errorf("unknown vesting type: %q", typePart)
}
}

b.Vesting = &schedule
}

return nil
Expand All @@ -59,7 +125,18 @@ func (b Balance) MarshalAmino() (string, error) {
}

func (b Balance) String() string {
return fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String())
s := fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String())
if b.Vesting != nil && !b.Vesting.IsZero() {
s += fmt.Sprintf(";vesting=%s,%d,%d",
b.Vesting.OriginalVesting.String(),
b.Vesting.StartTime,
b.Vesting.EndTime,
)
if b.Vesting.Type == std.VestingDelayed {
s += ";type=delayed"
}
}
return s
}

type Balances map[crypto.Address]Balance
Expand Down
Loading
Loading