diff --git a/starport/cmd/cmd.go b/starport/cmd/cmd.go index a6cfaacf3e..a395ed33ff 100644 --- a/starport/cmd/cmd.go +++ b/starport/cmd/cmd.go @@ -86,12 +86,17 @@ func printEvents(wg *sync.WaitGroup, bus events.Bus, s *clispinner.Spinner) { defer wg.Done() for event := range bus { - if event.IsOngoing() { + switch event.Status { + case events.StatusOngoing: s.SetText(event.Text()) s.Start() - } else { + case events.StatusDone: + icon := event.Icon + if icon == "" { + icon = clispinner.OK + } s.Stop() - fmt.Printf("%s %s\n", clispinner.OK, event.Description) + fmt.Printf("%s %s\n", icon, event.Text()) } } } diff --git a/starport/cmd/network_campaign.go b/starport/cmd/network_campaign.go index bacb6cf392..aa7116925c 100644 --- a/starport/cmd/network_campaign.go +++ b/starport/cmd/network_campaign.go @@ -16,6 +16,7 @@ func NewNetworkCampaign() *cobra.Command { NewNetworkCampaignList(), NewNetworkCampaignShow(), NewNetworkCampaignUpdate(), + NewNetworkCampaignAccount(), ) return c } diff --git a/starport/cmd/network_campaign_account.go b/starport/cmd/network_campaign_account.go new file mode 100644 index 0000000000..020ad34ea5 --- /dev/null +++ b/starport/cmd/network_campaign_account.go @@ -0,0 +1,142 @@ +package starportcmd + +import ( + "bytes" + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" + + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/entrywriter" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +var ( + campaignMainnetsAccSummaryHeader = []string{"Mainnet Account", "Shares"} + campaignVestingAccSummaryHeader = []string{"Vesting Account", "Total Shares", "Vesting", "End Time"} +) + +// NewNetworkCampaignAccount creates a new campaign account command that holds some other +// sub commands related to account for a campaign. +func NewNetworkCampaignAccount() *cobra.Command { + c := &cobra.Command{ + Use: "account", + Short: "Handle campaign accounts", + } + c.AddCommand( + newNetworkCampaignAccountList(), + ) + return c +} + +func newNetworkCampaignAccountList() *cobra.Command { + c := &cobra.Command{ + Use: "list [campaign-id]", + Short: "Show all mainnet and mainnet vesting of the campaign", + Args: cobra.ExactArgs(1), + RunE: newNetworkCampaignAccountListHandler, + } + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + return c +} + +func newNetworkCampaignAccountListHandler(cmd *cobra.Command, args []string) error { + nb, campaignID, err := networkChainLaunch(cmd, args) + if err != nil { + return err + } + defer nb.Cleanup() + + n, err := nb.Network() + if err != nil { + return err + } + + accountSummary := &bytes.Buffer{} + + // get all campaign accounts + mainnetAccs, vestingAccs, err := getAccounts(cmd.Context(), n, campaignID) + if err != nil { + return err + } + + mainnetAccEntries := make([][]string, 0) + for _, acc := range mainnetAccs { + mainnetAccEntries = append(mainnetAccEntries, []string{ + acc.Address, + acc.Shares.String(), + }) + } + if len(mainnetAccEntries) > 0 { + if err = entrywriter.MustWrite( + accountSummary, + campaignMainnetsAccSummaryHeader, + mainnetAccEntries..., + ); err != nil { + return err + } + } + + mainnetVestingAccEntries := make([][]string, 0) + for _, acc := range vestingAccs { + mainnetVestingAccEntries = append(mainnetVestingAccEntries, []string{ + acc.Address, + acc.TotalShares.String(), + acc.Vesting.String(), + strconv.FormatInt(acc.EndTime, 10), + }) + } + if len(mainnetVestingAccEntries) > 0 { + if err = entrywriter.MustWrite( + accountSummary, + campaignVestingAccSummaryHeader, + mainnetVestingAccEntries..., + ); err != nil { + return err + } + } + + nb.Spinner.Stop() + if accountSummary.Len() > 0 { + fmt.Print(accountSummary.String()) + } else { + fmt.Printf("%s %s\n", clispinner.Info, "no campaign account found") + } + return nil +} + +// getAccounts get all campaign mainnet and vesting accounts. +func getAccounts( + ctx context.Context, + n network.Network, + campaignID uint64, +) ( + []networktypes.MainnetAccount, + []networktypes.MainnetVestingAccount, + error, +) { + // start serving components. + g, ctx := errgroup.WithContext(ctx) + var ( + mainnetAccs []networktypes.MainnetAccount + vestingAccs []networktypes.MainnetVestingAccount + err error + ) + // get all campaign mainnet accounts + g.Go(func() error { + mainnetAccs, err = n.MainnetAccounts(ctx, campaignID) + return err + }) + + // get all campaign vesting accounts + g.Go(func() error { + vestingAccs, err = n.MainnetVestingAccounts(ctx, campaignID) + return err + }) + return mainnetAccs, vestingAccs, g.Wait() +} diff --git a/starport/cmd/network_campaign_list.go b/starport/cmd/network_campaign_list.go index 915da2d119..6f362c5edd 100644 --- a/starport/cmd/network_campaign_list.go +++ b/starport/cmd/network_campaign_list.go @@ -28,7 +28,6 @@ func NewNetworkCampaignList() *cobra.Command { } c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) - c.Flags().AddFlagSet(flagSetHome()) return c } diff --git a/starport/cmd/network_campaign_show.go b/starport/cmd/network_campaign_show.go index d5da593ebc..ef46dd256b 100644 --- a/starport/cmd/network_campaign_show.go +++ b/starport/cmd/network_campaign_show.go @@ -19,7 +19,6 @@ func NewNetworkCampaignShow() *cobra.Command { } c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) - c.Flags().AddFlagSet(flagSetHome()) return c } diff --git a/starport/cmd/network_chain_list.go b/starport/cmd/network_chain_list.go index f057b634bf..4f01aebefe 100644 --- a/starport/cmd/network_chain_list.go +++ b/starport/cmd/network_chain_list.go @@ -23,7 +23,6 @@ func NewNetworkChainList() *cobra.Command { } c.Flags().AddFlagSet(flagNetworkFrom()) c.Flags().AddFlagSet(flagSetKeyringBackend()) - c.Flags().AddFlagSet(flagSetHome()) return c } diff --git a/starport/cmd/network_chain_show.go b/starport/cmd/network_chain_show.go index 06f0c76b65..8ff948088a 100644 --- a/starport/cmd/network_chain_show.go +++ b/starport/cmd/network_chain_show.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/clispinner" "github.com/tendermint/starport/starport/pkg/cosmosutil" "github.com/tendermint/starport/starport/pkg/entrywriter" "github.com/tendermint/starport/starport/pkg/yaml" @@ -39,8 +40,8 @@ func NewNetworkChainShow() *cobra.Command { newNetworkChainShowValidators(), newNetworkChainShowPeers(), ) - c.PersistentFlags().AddFlagSet(flagNetworkFrom()) - c.PersistentFlags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) return c } @@ -191,7 +192,7 @@ func newNetworkChainShowAccounts() *cobra.Command { return err } - accountSummary := bytes.NewBufferString("") + accountSummary := &bytes.Buffer{} // get all chain genesis accounts genesisAccs, err := n.GenesisAccounts(cmd.Context(), launchID) @@ -239,7 +240,11 @@ func newNetworkChainShowAccounts() *cobra.Command { } } nb.Spinner.Stop() - fmt.Print(accountSummary.String()) + if accountSummary.Len() > 0 { + fmt.Print(accountSummary.String()) + } else { + fmt.Printf("%s %s\n", clispinner.Info, "empty chain account list") + } return nil }, } @@ -279,6 +284,7 @@ func newNetworkChainShowValidators() *cobra.Command { peer, }) } + nb.Spinner.Stop() if len(validatorEntries) > 0 { if err = entrywriter.MustWrite( validatorSummary, @@ -287,9 +293,10 @@ func newNetworkChainShowValidators() *cobra.Command { ); err != nil { return err } + fmt.Print(validatorSummary.String()) + } else { + fmt.Printf("%s %s\n", clispinner.Info, "no account found") } - nb.Spinner.Stop() - fmt.Print(validatorSummary.String()) return nil }, } diff --git a/starport/cmd/network_request_list.go b/starport/cmd/network_request_list.go index e9f3ae0132..fa86bb5990 100644 --- a/starport/cmd/network_request_list.go +++ b/starport/cmd/network_request_list.go @@ -23,9 +23,8 @@ func NewNetworkRequestList() *cobra.Command { RunE: networkRequestListHandler, Args: cobra.ExactArgs(1), } - c.Flags().AddFlagSet(flagSetKeyringBackend()) c.Flags().AddFlagSet(flagNetworkFrom()) - c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) return c } diff --git a/starport/cmd/network_request_show.go b/starport/cmd/network_request_show.go index 3b5125e2c8..8dacd42443 100644 --- a/starport/cmd/network_request_show.go +++ b/starport/cmd/network_request_show.go @@ -21,8 +21,6 @@ func NewNetworkRequestShow() *cobra.Command { Args: cobra.ExactArgs(2), } c.Flags().AddFlagSet(flagSetKeyringBackend()) - c.Flags().AddFlagSet(flagNetworkFrom()) - c.Flags().AddFlagSet(flagSetHome()) return c } diff --git a/starport/pkg/clispinner/icon.go b/starport/pkg/clispinner/icon.go index b0a4c1be17..af0807fa8a 100644 --- a/starport/pkg/clispinner/icon.go +++ b/starport/pkg/clispinner/icon.go @@ -6,6 +6,9 @@ var ( // OK is an OK mark. OK = color.New(color.FgGreen).SprintFunc()("✔") // NotOK is a red cross mark - NotOK = color.New(color.FgRed).SprintFunc()("✘") + NotOK = color.New(color.FgRed).SprintFunc()("✘") + // Bullet is a bullet mark Bullet = color.New(color.FgYellow).SprintFunc()("⋆") + // Info is an info mark + Info = color.New(color.FgYellow).SprintFunc()("𝓲") ) diff --git a/starport/pkg/events/events.go b/starport/pkg/events/events.go index 3864c7e8da..61cadce1b6 100644 --- a/starport/pkg/events/events.go +++ b/starport/pkg/events/events.go @@ -2,19 +2,34 @@ // for others to consume and display to end users in meaningful ways. package events -import "fmt" +import ( + "fmt" -// Event represents a state. -type Event struct { - // Status shows the current status of event. - status Status + "github.com/gookit/color" +) - // Description of the state. - Description string -} +type ( + // Event represents a state. + Event struct { + // Description of the state. + Description string + + // Status shows the current status of event. + Status Status -// Status shows if state is ongoing or completed. -type Status int + // TextColor of the text. + TextColor color.Color + + // Icon of the text. + Icon string + } + + // Status shows if state is ongoing or completed. + Status int + + // Option event options + Option func(*Event) +) const ( StatusOngoing Status = iota @@ -22,21 +37,26 @@ const ( ) // New creates a new event with given config. -func New(status Status, description string) Event { - return Event{status, description} +func New(status Status, description string, options ...Option) Event { + ev := Event{Status: status, Description: description} + for _, applyOption := range options { + applyOption(&ev) + } + return ev } // IsOngoing checks if state change that triggered this event is still ongoing. func (e Event) IsOngoing() bool { - return e.status == StatusOngoing + return e.Status == StatusOngoing } // Text returns the text state of event. func (e Event) Text() string { + text := e.Description if e.IsOngoing() { - return fmt.Sprintf("%s...", e.Description) + text = fmt.Sprintf("%s...", e.Description) } - return e.Description + return e.TextColor.Render(text) } // Bus is a send/receive event bus. diff --git a/starport/pkg/events/events_test.go b/starport/pkg/events/events_test.go index 0f46cb4783..c8e18cde9f 100644 --- a/starport/pkg/events/events_test.go +++ b/starport/pkg/events/events_test.go @@ -3,6 +3,7 @@ package events import ( "testing" + "github.com/gookit/color" "github.com/stretchr/testify/require" ) @@ -16,7 +17,7 @@ func TestBusSend(t *testing.T) { name: "send status ongoing event", bus: make(Bus), event: Event{ - status: StatusOngoing, + Status: StatusOngoing, Description: "description", }, }, @@ -24,7 +25,7 @@ func TestBusSend(t *testing.T) { name: "send status done event", bus: make(Bus), event: Event{ - status: StatusDone, + Status: StatusDone, Description: "description", }, }, @@ -32,7 +33,7 @@ func TestBusSend(t *testing.T) { name: "send event on nil bus", bus: nil, event: Event{ - status: StatusDone, + Status: StatusDone, Description: "description", }, }, @@ -76,7 +77,7 @@ func TestBusShutdown(t *testing.T) { func TestEventIsOngoing(t *testing.T) { type fields struct { status Status - Description string + description string } tests := []struct { name string @@ -89,8 +90,8 @@ func TestEventIsOngoing(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := Event{ - status: tt.fields.status, - Description: tt.fields.Description, + Status: tt.fields.status, + Description: tt.fields.description, } require.Equal(t, tt.want, e.IsOngoing()) }) @@ -100,7 +101,8 @@ func TestEventIsOngoing(t *testing.T) { func TestEventText(t *testing.T) { type fields struct { status Status - Description string + description string + textColor color.Color } tests := []struct { name string @@ -108,28 +110,41 @@ func TestEventText(t *testing.T) { want string }{ { - name: "status done", - fields: fields{StatusDone, "description"}, - want: "description", + name: "status done", + fields: fields{ + status: StatusDone, + description: "description", + textColor: color.Red, + }, + want: "description", }, { - name: "status ongoing", - fields: fields{StatusOngoing, "description"}, - want: "description...", + name: "status ongoing", + fields: fields{ + status: StatusOngoing, + description: "description", + textColor: color.Red, + }, + want: "description...", }, { - name: "status ongoing with empty description", - fields: fields{StatusOngoing, ""}, - want: "...", + name: "status ongoing with empty description", + fields: fields{ + status: StatusOngoing, + description: "", + textColor: color.Red, + }, + want: "...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := Event{ - status: tt.fields.status, - Description: tt.fields.Description, + Status: tt.fields.status, + Description: tt.fields.description, + TextColor: tt.fields.textColor, } - require.Equal(t, tt.want, e.Text()) + require.Equal(t, e.TextColor.Render(tt.want), e.Text()) }) } } @@ -145,9 +160,9 @@ func TestNew(t *testing.T) { want Event }{ {"zero value args", args{}, Event{}}, - {"large value args", args{99999, "description"}, Event{99999, "description"}}, - {"status ongoing", args{StatusOngoing, "description"}, Event{0, "description"}}, - {"status done", args{StatusDone, "description"}, Event{1, "description"}}, + {"large value args", args{status: 99999, description: "description"}, Event{Status: 99999, Description: "description"}}, + {"status ongoing", args{status: StatusOngoing, description: "description"}, Event{Status: 0, Description: "description"}}, + {"status done", args{status: StatusDone, description: "description"}, Event{Status: 1, Description: "description"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -161,8 +176,8 @@ func TestNewBus(t *testing.T) { name string event Event }{ - {"new bus with status done event", Event{StatusDone, "description"}}, - {"new bus with status ongoing event", Event{StatusOngoing, "description"}}, + {"new bus with status done event", Event{Status: StatusDone, Description: "description"}}, + {"new bus with status ongoing event", Event{Status: StatusOngoing, Description: "description"}}, {"new bus with zero value event", Event{}}, } for _, tt := range tests { diff --git a/starport/services/network/networktypes/campaign.go b/starport/services/network/networktypes/campaign.go index 2363cf8f3c..fe0a256bec 100644 --- a/starport/services/network/networktypes/campaign.go +++ b/starport/services/network/networktypes/campaign.go @@ -34,3 +34,36 @@ func ToCampaign(campaign campaigntypes.Campaign) Campaign { Metadata: string(campaign.Metadata), } } + +// MainnetAccount represents the campaign mainnet account of a chain on SPN +type MainnetAccount struct { + Address string `json:"Address"` + Shares campaigntypes.Shares `json:"Shares"` +} + +// ToMainnetAccount converts a mainnet account data from SPN and returns a MainnetAccount object +func ToMainnetAccount(acc campaigntypes.MainnetAccount) MainnetAccount { + return MainnetAccount{ + Address: acc.Address, + Shares: acc.Shares, + } +} + +// MainnetVestingAccount represents the campaign mainnet vesting account of a chain on SPN +type MainnetVestingAccount struct { + Address string `json:"Address"` + TotalShares campaigntypes.Shares `json:"TotalShares"` + Vesting campaigntypes.Shares `json:"Vesting"` + EndTime int64 `json:"EndTime"` +} + +// ToMainnetVestingAccount converts a mainnet vesting account data from SPN and returns a MainnetVestingAccount object +func ToMainnetVestingAccount(acc campaigntypes.MainnetVestingAccount) MainnetVestingAccount { + delaydVesting := acc.VestingOptions.GetDelayedVesting() + return MainnetVestingAccount{ + Address: acc.Address, + TotalShares: delaydVesting.TotalShares, + Vesting: delaydVesting.Vesting, + EndTime: delaydVesting.EndTime, + } +} diff --git a/starport/services/network/queries.go b/starport/services/network/queries.go index fe418e927c..606b5bb691 100644 --- a/starport/services/network/queries.go +++ b/starport/services/network/queries.go @@ -4,6 +4,7 @@ import ( "context" "github.com/pkg/errors" + campaigntypes "github.com/tendermint/spn/x/campaign/types" launchtypes "github.com/tendermint/spn/x/launch/types" "github.com/tendermint/starport/starport/pkg/events" @@ -14,9 +15,8 @@ import ( func (n Network) ChainLaunch(ctx context.Context, id uint64) (networktypes.ChainLaunch, error) { n.ev.Send(events.New(events.StatusOngoing, "Fetching chain information")) - res, err := launchtypes.NewQueryClient(n.cosmos.Context).Chain(ctx, &launchtypes.QueryGetChainRequest{ - LaunchID: id, - }) + res, err := launchtypes.NewQueryClient(n.cosmos.Context). + Chain(ctx, &launchtypes.QueryGetChainRequest{LaunchID: id}) if err != nil { return networktypes.ChainLaunch{}, err } @@ -29,7 +29,8 @@ func (n Network) ChainLaunches(ctx context.Context) ([]networktypes.ChainLaunch, var chainLaunches []networktypes.ChainLaunch n.ev.Send(events.New(events.StatusOngoing, "Fetching chains information")) - res, err := launchtypes.NewQueryClient(n.cosmos.Context).ChainAll(ctx, &launchtypes.QueryAllChainRequest{}) + res, err := launchtypes.NewQueryClient(n.cosmos.Context). + ChainAll(ctx, &launchtypes.QueryAllChainRequest{}) if err != nil { return chainLaunches, err } @@ -65,9 +66,12 @@ func (n Network) GenesisInformation(ctx context.Context, launchID uint64) (gi ne // GenesisAccounts returns the list of approved genesis accounts for a launch from SPN func (n Network) GenesisAccounts(ctx context.Context, launchID uint64) (genAccs []networktypes.GenesisAccount, err error) { n.ev.Send(events.New(events.StatusOngoing, "Fetching genesis accounts")) - res, err := launchtypes.NewQueryClient(n.cosmos.Context).GenesisAccountAll(ctx, &launchtypes.QueryAllGenesisAccountRequest{ - LaunchID: launchID, - }) + res, err := launchtypes.NewQueryClient(n.cosmos.Context). + GenesisAccountAll(ctx, + &launchtypes.QueryAllGenesisAccountRequest{ + LaunchID: launchID, + }, + ) if err != nil { return genAccs, err } @@ -82,9 +86,12 @@ func (n Network) GenesisAccounts(ctx context.Context, launchID uint64) (genAccs // VestingAccounts returns the list of approved genesis vesting accounts for a launch from SPN func (n Network) VestingAccounts(ctx context.Context, launchID uint64) (vestingAccs []networktypes.VestingAccount, err error) { n.ev.Send(events.New(events.StatusOngoing, "Fetching genesis vesting accounts")) - res, err := launchtypes.NewQueryClient(n.cosmos.Context).VestingAccountAll(ctx, &launchtypes.QueryAllVestingAccountRequest{ - LaunchID: launchID, - }) + res, err := launchtypes.NewQueryClient(n.cosmos.Context). + VestingAccountAll(ctx, + &launchtypes.QueryAllVestingAccountRequest{ + LaunchID: launchID, + }, + ) if err != nil { return vestingAccs, err } @@ -104,9 +111,12 @@ func (n Network) VestingAccounts(ctx context.Context, launchID uint64) (vestingA // GenesisValidators returns the list of approved genesis validators for a launch from SPN func (n Network) GenesisValidators(ctx context.Context, launchID uint64) (genVals []networktypes.GenesisValidator, err error) { n.ev.Send(events.New(events.StatusOngoing, "Fetching genesis validators")) - res, err := launchtypes.NewQueryClient(n.cosmos.Context).GenesisValidatorAll(ctx, &launchtypes.QueryAllGenesisValidatorRequest{ - LaunchID: launchID, - }) + res, err := launchtypes.NewQueryClient(n.cosmos.Context). + GenesisValidatorAll(ctx, + &launchtypes.QueryAllGenesisValidatorRequest{ + LaunchID: launchID, + }, + ) if err != nil { return genVals, err } @@ -117,3 +127,43 @@ func (n Network) GenesisValidators(ctx context.Context, launchID uint64) (genVal return genVals, nil } + +// MainnetAccounts returns the list of campaign mainnet accounts for a launch from SPN +func (n Network) MainnetAccounts(ctx context.Context, campaignID uint64) (genAccs []networktypes.MainnetAccount, err error) { + n.ev.Send(events.New(events.StatusOngoing, "Fetching campaign mainnet accounts")) + res, err := campaigntypes.NewQueryClient(n.cosmos.Context). + MainnetAccountAll(ctx, + &campaigntypes.QueryAllMainnetAccountRequest{ + CampaignID: campaignID, + }, + ) + if err != nil { + return genAccs, err + } + + for _, acc := range res.MainnetAccount { + genAccs = append(genAccs, networktypes.ToMainnetAccount(acc)) + } + + return genAccs, nil +} + +// MainnetVestingAccounts returns the list of campaign mainnet vesting accounts for a launch from SPN +func (n Network) MainnetVestingAccounts(ctx context.Context, campaignID uint64) (genAccs []networktypes.MainnetVestingAccount, err error) { + n.ev.Send(events.New(events.StatusOngoing, "Fetching campaign mainnet vesting accounts")) + res, err := campaigntypes.NewQueryClient(n.cosmos.Context). + MainnetVestingAccountAll(ctx, + &campaigntypes.QueryAllMainnetVestingAccountRequest{ + CampaignID: campaignID, + }, + ) + if err != nil { + return genAccs, err + } + + for _, acc := range res.MainnetVestingAccount { + genAccs = append(genAccs, networktypes.ToMainnetVestingAccount(acc)) + } + + return genAccs, nil +}